@astrosheep/keiyaku 2.9.4 → 2.9.6
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/build/.tsbuildinfo +1 -1
- package/build/agents/call-terms.js +1 -0
- package/build/agents/harness/execution-handle.js +1 -0
- package/build/agents/harness/outcome.js +31 -5
- package/build/agents/harness/projection.js +151 -78
- package/build/agents/harness/router.js +2 -1
- package/build/agents/providers/claude-agent-sdk/adapter.js +13 -1
- package/build/agents/providers/claude-agent-sdk/registration.js +2 -1
- package/build/agents/providers/codex-app-server/adapter.js +43 -10
- package/build/agents/providers/codex-app-server/events.js +23 -0
- package/build/agents/providers/codex-app-server/registration.js +2 -1
- package/build/agents/providers/opencode-sdk/adapter.js +7 -0
- package/build/agents/providers/opencode-sdk/registration.js +2 -1
- package/build/agents/providers/pi/adapter.js +10 -0
- package/build/agents/providers/pi/events.js +4 -3
- package/build/agents/providers/pi/registration.js +3 -1
- package/build/agents/types.js +2 -0
- package/build/cli/commands/akuma/akuma/meta.js +1 -1
- package/build/cli/commands/akuma/list/meta-ls.js +1 -1
- package/build/cli/commands/akuma/view/meta.js +1 -1
- package/build/cli/commands/contract/amend/meta.js +4 -4
- package/build/cli/commands/contract/arc/handler.js +1 -1
- package/build/cli/commands/contract/arc/meta.js +4 -4
- package/build/cli/commands/contract/audit/handler.js +16 -0
- package/build/cli/commands/contract/audit/meta.js +16 -0
- package/build/cli/commands/contract/bind/meta.js +20 -11
- package/build/cli/commands/contract/catalog.js +2 -0
- package/build/cli/commands/contract/forfeit/meta.js +1 -1
- package/build/cli/commands/contract/log/meta.js +1 -1
- package/build/cli/commands/contract/petition/handler.js +1 -2
- package/build/cli/commands/contract/petition/meta.js +6 -6
- package/build/cli/commands/contract/renew/handler.js +2 -2
- package/build/cli/commands/contract/renew/meta.js +4 -4
- package/build/cli/commands/metadata.js +3 -2
- package/build/cli/commands/projection/call/meta.js +4 -1
- package/build/cli/commands/projection/kill/meta.js +1 -1
- package/build/cli/commands/projection/revive/meta.js +4 -1
- package/build/cli/commands/projection/status/meta.js +1 -1
- package/build/cli/commands/projection/tell/handler.js +1 -1
- package/build/cli/commands/projection/tell/meta.js +4 -1
- package/build/cli/commands/projection/wait/meta.js +1 -1
- package/build/cli/commands/skills/install/meta.js +1 -1
- package/build/cli/commands/skills/skills/meta.js +1 -1
- package/build/cli/commands/system/completion/meta.js +1 -1
- package/build/cli/commands/system/dump-env/meta.js +1 -1
- package/build/cli/commands/system/guide/meta.js +1 -1
- package/build/cli/commands/task/add/handler.js +2 -2
- package/build/cli/commands/task/add/meta.js +4 -1
- package/build/cli/commands/task/doctor/meta.js +1 -1
- package/build/cli/commands/task/done/meta.js +1 -1
- package/build/cli/commands/task/drop/meta.js +1 -1
- package/build/cli/commands/task/ls/handler.js +10 -4
- package/build/cli/commands/task/ls/meta.js +3 -3
- package/build/cli/commands/task/show/meta.js +1 -1
- package/build/cli/commands/task/start/meta.js +1 -1
- package/build/cli/commands/task/stop/meta.js +1 -1
- package/build/cli/commands/task/task/meta.js +1 -1
- package/build/cli/commands/task/update/handler.js +2 -3
- package/build/cli/commands/task/update/meta.js +4 -1
- package/build/cli/completion.js +9 -2
- package/build/cli/flags.js +19 -3
- package/build/cli/help.js +3 -3
- package/build/cli/index.js +59 -75
- package/build/cli/output-stream-error-policy.js +19 -5
- package/build/cli/parse-flags.js +19 -2
- package/build/cli/parse-metadata.js +1 -7
- package/build/cli/parse-selectors.js +1 -0
- package/build/cli/parse.js +16 -7
- package/build/cli/projection-address.js +6 -1
- package/build/cli/render/audit.js +121 -0
- package/build/cli/render/errors.js +18 -1
- package/build/cli/render/format.js +5 -2
- package/build/cli/render/misc.js +3 -1
- package/build/cli/render/projection-activity.js +12 -6
- package/build/cli/render/status.js +28 -18
- package/build/cli/render/success-response.js +6 -5
- package/build/cli/render/task.js +12 -43
- package/build/cli/render/terminal-failure.js +7 -2
- package/build/cli/render/wait.js +36 -8
- package/build/config/env-keys.js +2 -0
- package/build/config/env.js +3 -0
- package/build/core/amend.js +20 -94
- package/build/core/arc.js +14 -6
- package/build/core/audit/candidate.js +48 -0
- package/build/core/audit/coordinates.js +180 -0
- package/build/core/audit/evidence.js +158 -0
- package/build/core/audit/facade.js +42 -0
- package/build/core/audit/readiness.js +4 -0
- package/build/core/audit/report.js +38 -0
- package/build/core/audit.js +1 -0
- package/build/core/bind.js +1 -1
- package/build/core/call/execution.js +1 -0
- package/build/core/contract-view.js +2 -3
- package/build/core/draft.js +33 -20
- package/build/core/entry.js +3 -4
- package/build/core/markdown/lex.js +2 -1
- package/build/core/projection/generation/database.js +41 -16
- package/build/core/projection/generation/model.js +22 -2
- package/build/core/projection/generation/projection-generation-execution.js +20 -7
- package/build/core/projection/generation/projection-generation-launcher.js +4 -4
- package/build/core/projection/generation/projection-generation-runner.js +1 -0
- package/build/core/projection/generation/store.js +1 -1
- package/build/core/projection/generation/transitions.js +19 -3
- package/build/core/projection/index.js +2 -2
- package/build/core/projection/projection-execution-observer.js +35 -21
- package/build/core/projection/projection-life-observer.js +16 -2
- package/build/core/projection/projection-status.js +67 -22
- package/build/core/projection/projection-terminal-failure.js +2 -0
- package/build/core/projection/projection-wait.js +6 -9
- package/build/core/projection/projection-wake.js +30 -6
- package/build/core/renew.js +41 -22
- package/build/core/scope.js +129 -2
- package/build/core/seal.js +36 -15
- package/build/core/settlement/petition-claim-gates.js +4 -33
- package/build/core/settlement/petition-preview.js +1 -1
- package/build/core/settlement/petition.js +10 -3
- package/build/core/settlement/settlement.js +3 -10
- package/build/core/settlement/verification.js +59 -0
- package/build/core/status/board.js +48 -28
- package/build/core/stored-agent-event.js +3 -2
- package/build/core/task/board.js +1 -0
- package/build/core/task/commands.js +20 -12
- package/build/core/task/document.js +5 -0
- package/build/core/task/index.js +1 -1
- package/build/core/task/model.js +8 -1
- package/build/core/task/query.js +214 -0
- package/build/core/task/settlement-git.js +115 -16
- package/build/core/task/settlement-policy.js +17 -9
- package/build/core/task/source-board.js +1 -0
- package/build/core/task/task-contract.js +125 -51
- package/build/core/task/task-store-repository.js +1 -0
- package/build/core/task/task.js +2 -0
- package/build/flow-error.js +2 -4
- package/build/generated/version.js +2 -2
- package/build/git/diff/parsers.js +23 -13
- package/build/git/diff/structured.js +127 -0
- package/build/index.js +7 -7
- package/build/telemetry/logger.js +4 -2
- package/package.json +1 -1
- package/skills/keiyaku/SKILL.md +2 -2
- package/skills/keiyaku-akuma/SKILL.md +17 -6
- package/skills/keiyaku-task/SKILL.md +16 -7
- package/skills/keiyaku-workflow/SKILL.md +48 -13
|
@@ -3,42 +3,62 @@ import { observeGitRepository } from "../../git/branches.js";
|
|
|
3
3
|
import { readProjectionStatusBoard } from "../projection/index.js";
|
|
4
4
|
import { deriveQueueReadModel, queueMembership, } from "../settlement/index.js";
|
|
5
5
|
import { findOpenArc, getMaxArc, receiptsAfterFence } from "../seal.js";
|
|
6
|
-
import { openTaskStore, readTaskBoard, taskRepositoryFromStore, } from "../task/index.js";
|
|
6
|
+
import { openTaskStore, readTaskBoard, taskRepositoryFromStore, taskUpdatedAt, } from "../task/index.js";
|
|
7
7
|
import { deriveBootstrapDiagnostics, deriveContractState, hasCurrentPetitionVerdict, isTerminalState, } from "./lifecycle.js";
|
|
8
8
|
import { commissionTarget, evaluateBaseDrift, latestBase, resolveDefaultBranchFromSettings, } from "./drift.js";
|
|
9
9
|
import { computeClaimReconciliation, } from "./reconciliation.js";
|
|
10
10
|
import { readStatusLedgers } from "./ledger-batch.js";
|
|
11
11
|
function compareTaskStatusRows(left, right) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
if (
|
|
15
|
-
return
|
|
12
|
+
const leftUpdated = taskUpdatedAt(left);
|
|
13
|
+
const rightUpdated = taskUpdatedAt(right);
|
|
14
|
+
if (leftUpdated !== rightUpdated)
|
|
15
|
+
return leftUpdated > rightUpdated ? -1 : 1;
|
|
16
16
|
return left.id < right.id ? -1 : left.id > right.id ? 1 : 0;
|
|
17
17
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
return undefined;
|
|
18
|
+
function timestampAge(timestamp, now) {
|
|
19
|
+
const value = Date.parse(timestamp);
|
|
20
|
+
return Number.isFinite(value) ? Math.max(0, now - value) : 0;
|
|
21
|
+
}
|
|
22
|
+
export function deriveTaskStatusSummary(board, now) {
|
|
23
|
+
const eligible = board.tasks.filter((task) => !task.contractId && (task.state === "open" || task.state === "in_progress"));
|
|
24
|
+
const inProgress = eligible.filter((task) => task.state === "in_progress").length;
|
|
25
|
+
const topTasks = [
|
|
26
|
+
...eligible.filter((task) => task.state === "in_progress").sort(compareTaskStatusRows),
|
|
27
|
+
...eligible.filter((task) => task.state === "open" && !task.blocked).sort(compareTaskStatusRows),
|
|
28
|
+
]
|
|
29
|
+
.slice(0, 5)
|
|
30
|
+
.map((task) => {
|
|
31
|
+
const updatedAt = taskUpdatedAt(task);
|
|
33
32
|
return {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
33
|
+
id: task.id,
|
|
34
|
+
title: task.title,
|
|
35
|
+
pri: task.pri,
|
|
36
|
+
state: task.state,
|
|
37
|
+
blocked: task.blocked,
|
|
38
|
+
createdAt: task.createdAt,
|
|
39
|
+
updatedAt,
|
|
40
|
+
ageMs: timestampAge(task.createdAt, now),
|
|
41
|
+
activityAgeMs: timestampAge(updatedAt, now),
|
|
41
42
|
};
|
|
43
|
+
});
|
|
44
|
+
const ready = eligible.filter((task) => task.state === "open" && !task.blocked).length;
|
|
45
|
+
const blocked = eligible.filter((task) => task.state === "open" && task.blocked).length;
|
|
46
|
+
if (board.tasks.length === 0 && board.diseases.length === 0)
|
|
47
|
+
return undefined;
|
|
48
|
+
return {
|
|
49
|
+
total: board.tasks.length,
|
|
50
|
+
inProgress,
|
|
51
|
+
ready,
|
|
52
|
+
blocked,
|
|
53
|
+
omitted: inProgress + ready - topTasks.length,
|
|
54
|
+
topTasks,
|
|
55
|
+
diseases: board.diseases,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
async function readTaskStatusSummary(cwd, now) {
|
|
59
|
+
try {
|
|
60
|
+
const board = await readTaskBoard(taskRepositoryFromStore(await openTaskStore(cwd)));
|
|
61
|
+
return deriveTaskStatusSummary(board, now);
|
|
42
62
|
}
|
|
43
63
|
catch (error) {
|
|
44
64
|
return {
|
|
@@ -148,7 +168,7 @@ function deriveLastActiveAge(entries, now) {
|
|
|
148
168
|
export async function readKanshiBoard(cwd, now, opts) {
|
|
149
169
|
const targetFilter = opts?.target ?? null;
|
|
150
170
|
const loadedSettings = await loadKeiyakuSettings(cwd);
|
|
151
|
-
const tasks = await readTaskStatusSummary(cwd);
|
|
171
|
+
const tasks = await readTaskStatusSummary(cwd, now);
|
|
152
172
|
const repository = await observeGitRepository(cwd);
|
|
153
173
|
if (repository.capability === "unknown")
|
|
154
174
|
throw repository.error;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { activityEventBodySchema, AGENT_EVENT_TEXT_MAX_CHARS, AGENT_UNKNOWN_PAYLOAD_MAX_CHARS, EXEC_EVENT_BUFFER_HARD_CAP, } from "../agents/harness/index.js";
|
|
2
2
|
import { isSubagentProvider } from "../keiyaku.js";
|
|
3
|
-
const FAIL_CODES = new Set(["SUBAGENT_TIMEOUT", "SUBAGENT_NON_ZERO_EXIT", "SUBAGENT_SPAWN_ERROR", "SUBAGENT_EXEC_ERROR", "SUBAGENT_PROVIDER_UNAVAILABLE", "PROJECTION_STATE_ERROR"]);
|
|
3
|
+
const FAIL_CODES = new Set(["SUBAGENT_TIMEOUT", "SUBAGENT_TRANSPORT_ERROR", "SUBAGENT_POLICY_BUDGET", "SUBAGENT_NON_ZERO_EXIT", "SUBAGENT_SPAWN_ERROR", "SUBAGENT_EXEC_ERROR", "SUBAGENT_PROVIDER_UNAVAILABLE", "PROJECTION_STATE_ERROR"]);
|
|
4
4
|
const KNOWN = new Set(["message", "unknown", "activity", "thought", "plan", "turn", "usage", "warning", "diagnostic", "truncation", "done", "fail"]);
|
|
5
5
|
const record = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
6
6
|
const finite = (value) => typeof value === "number" && Number.isFinite(value);
|
|
7
|
+
const nonNegativeFinite = (value) => finite(value) && value >= 0;
|
|
7
8
|
const optionalString = (value, key) => value[key] === undefined || typeof value[key] === "string";
|
|
8
9
|
const nonNegativeSafeInteger = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
9
10
|
const safeIntegerOrNull = (value) => value === null || (typeof value === "number" && Number.isSafeInteger(value));
|
|
@@ -14,7 +15,7 @@ function isValidStoredWarning(warning) {
|
|
|
14
15
|
if (warning.reason === "retry") {
|
|
15
16
|
return nonNegativeSafeInteger(warning.attempt)
|
|
16
17
|
&& nonNegativeSafeInteger(warning.maxRetries)
|
|
17
|
-
&&
|
|
18
|
+
&& nonNegativeFinite(warning.retryDelayMs)
|
|
18
19
|
&& safeIntegerOrNull(warning.errorStatus);
|
|
19
20
|
}
|
|
20
21
|
if (warning.reason === "refusal" || warning.reason === "overload") {
|
package/build/core/task/board.js
CHANGED
|
@@ -163,6 +163,7 @@ export function buildTaskBoardFromDocuments(documents) {
|
|
|
163
163
|
parent: task.parent,
|
|
164
164
|
from: [...task.from],
|
|
165
165
|
createdAt: task.createdAt,
|
|
166
|
+
...(task.updatedAt === undefined ? {} : { updatedAt: task.updatedAt }),
|
|
166
167
|
creator: task.creator,
|
|
167
168
|
...(task.startedAt === undefined ? {} : { startedAt: task.startedAt }),
|
|
168
169
|
...(task.contractId === undefined ? {} : { contractId: task.contractId }),
|
|
@@ -2,6 +2,7 @@ import { FlowError } from "../../flow-error.js";
|
|
|
2
2
|
import { IdentifierSlugError, normalizeIdentifierSlug, SUFFIXABLE_IDENTIFIER_SLUG_MAX_UTF8_BYTES, } from "../identifier-slug.js";
|
|
3
3
|
import { requireCanonicalTaskId, requireHealthy, resolveTaskId, } from "./board.js";
|
|
4
4
|
import { dateTimestamp, invalid, nonblank, } from "./validation.js";
|
|
5
|
+
import { advancedTaskUpdatedAt } from "./model.js";
|
|
5
6
|
export async function readTaskBoard(repository) {
|
|
6
7
|
return repository.read();
|
|
7
8
|
}
|
|
@@ -15,6 +16,7 @@ function documentFromView(view) {
|
|
|
15
16
|
parent: view.parent,
|
|
16
17
|
from: [...view.from],
|
|
17
18
|
createdAt: view.createdAt,
|
|
19
|
+
...(view.updatedAt === undefined ? {} : { updatedAt: view.updatedAt }),
|
|
18
20
|
creator: view.creator,
|
|
19
21
|
...(view.startedAt === undefined ? {} : { startedAt: view.startedAt }),
|
|
20
22
|
...(view.contractId === undefined ? {} : { contractId: view.contractId }),
|
|
@@ -74,6 +76,7 @@ export async function createTask(repository, input, deps) {
|
|
|
74
76
|
}
|
|
75
77
|
id = `${baseId}-${ordinal}`;
|
|
76
78
|
}
|
|
79
|
+
const now = deps.now();
|
|
77
80
|
const document = {
|
|
78
81
|
id,
|
|
79
82
|
title: input.title,
|
|
@@ -82,14 +85,15 @@ export async function createTask(repository, input, deps) {
|
|
|
82
85
|
needs,
|
|
83
86
|
parent,
|
|
84
87
|
from,
|
|
85
|
-
createdAt: dateTimestamp("createdAt",
|
|
88
|
+
createdAt: dateTimestamp("createdAt", now),
|
|
89
|
+
updatedAt: dateTimestamp("updatedAt", now),
|
|
86
90
|
creator: input.creator,
|
|
87
91
|
body: input.body ?? "",
|
|
88
92
|
};
|
|
89
93
|
return replaceDocument(document, `Add task ${id}`);
|
|
90
94
|
});
|
|
91
95
|
}
|
|
92
|
-
export async function updateTask(repository, fullIdValue, patch) {
|
|
96
|
+
export async function updateTask(repository, fullIdValue, patch, now = new Date()) {
|
|
93
97
|
const hasChange = Object.values(patch).some((value) => value !== undefined);
|
|
94
98
|
if (!hasChange)
|
|
95
99
|
throw new FlowError("EMPTY_PARAM", "task update requires at least one change");
|
|
@@ -118,17 +122,20 @@ export async function updateTask(repository, fullIdValue, patch) {
|
|
|
118
122
|
if (JSON.stringify(document) === JSON.stringify(documentFromView(current))) {
|
|
119
123
|
throw new FlowError("EMPTY_PARAM", "task update requires at least one change");
|
|
120
124
|
}
|
|
125
|
+
document.updatedAt = advancedTaskUpdatedAt(current, now);
|
|
121
126
|
return replaceDocument(document, `Update task ${id}`);
|
|
122
127
|
});
|
|
123
128
|
}
|
|
124
|
-
async function transition(repository, fullIdValue, apply, subject) {
|
|
129
|
+
async function transition(repository, fullIdValue, apply, subject, now) {
|
|
125
130
|
return repository.mutate((board) => {
|
|
126
131
|
requireHealthy(board);
|
|
127
132
|
const id = fullIdValue === undefined
|
|
128
133
|
? selectSoleInProgress(board).id
|
|
129
134
|
: resolveTaskId(board, fullIdValue);
|
|
130
|
-
const
|
|
135
|
+
const current = requireExisting(board, id);
|
|
136
|
+
const document = documentFromView(current);
|
|
131
137
|
apply(document);
|
|
138
|
+
document.updatedAt = advancedTaskUpdatedAt(current, now);
|
|
132
139
|
return replaceDocument(document, subject(id));
|
|
133
140
|
});
|
|
134
141
|
}
|
|
@@ -147,18 +154,18 @@ export async function startTask(repository, fullId, now) {
|
|
|
147
154
|
invalid(`task ${document.id} is ${document.state}, not open`);
|
|
148
155
|
document.state = "in_progress";
|
|
149
156
|
document.startedAt = dateTimestamp("startedAt", now);
|
|
150
|
-
}, (id) => `Start task ${id}
|
|
157
|
+
}, (id) => `Start task ${id}`, now);
|
|
151
158
|
}
|
|
152
|
-
export async function stopTask(repository, fullId) {
|
|
159
|
+
export async function stopTask(repository, fullId, now = new Date()) {
|
|
153
160
|
return transition(repository, fullId, (document) => {
|
|
154
161
|
rejectBound(document);
|
|
155
162
|
if (document.state !== "in_progress")
|
|
156
163
|
invalid(`task ${document.id} is ${document.state}, not in_progress`);
|
|
157
164
|
document.state = "open";
|
|
158
165
|
delete document.startedAt;
|
|
159
|
-
}, (id) => `Stop task ${id}
|
|
166
|
+
}, (id) => `Stop task ${id}`, now);
|
|
160
167
|
}
|
|
161
|
-
export async function doneTask(repository, fullId) {
|
|
168
|
+
export async function doneTask(repository, fullId, now = new Date()) {
|
|
162
169
|
return transition(repository, fullId, (document) => {
|
|
163
170
|
rejectBound(document);
|
|
164
171
|
if (document.state !== "open" && document.state !== "in_progress") {
|
|
@@ -166,9 +173,9 @@ export async function doneTask(repository, fullId) {
|
|
|
166
173
|
}
|
|
167
174
|
document.state = "done";
|
|
168
175
|
delete document.startedAt;
|
|
169
|
-
}, (id) => `Done task ${id}
|
|
176
|
+
}, (id) => `Done task ${id}`, now);
|
|
170
177
|
}
|
|
171
|
-
export async function dropTask(repository, fullId) {
|
|
178
|
+
export async function dropTask(repository, fullId, now = new Date()) {
|
|
172
179
|
return transition(repository, fullId, (document) => {
|
|
173
180
|
rejectBound(document, "drop");
|
|
174
181
|
if (document.state !== "open" && document.state !== "in_progress") {
|
|
@@ -176,7 +183,7 @@ export async function dropTask(repository, fullId) {
|
|
|
176
183
|
}
|
|
177
184
|
document.state = "drop";
|
|
178
185
|
delete document.startedAt;
|
|
179
|
-
}, (id) => `Drop task ${id}
|
|
186
|
+
}, (id) => `Drop task ${id}`, now);
|
|
180
187
|
}
|
|
181
188
|
export function selectSoleInProgress(board) {
|
|
182
189
|
const candidates = board.tasks.filter((task) => task.state === "in_progress");
|
|
@@ -190,7 +197,7 @@ export function selectSoleInProgress(board) {
|
|
|
190
197
|
}
|
|
191
198
|
/** Reopen task pointers whose contract authority has disappeared. The caller
|
|
192
199
|
* supplies the ledger fact; this operation itself remains storage-neutral. */
|
|
193
|
-
export async function rollbackOrphanTaskPointers(repository, contractIds) {
|
|
200
|
+
export async function rollbackOrphanTaskPointers(repository, contractIds, now = new Date()) {
|
|
194
201
|
const before = await repository.read();
|
|
195
202
|
const orphans = before.tasks
|
|
196
203
|
.filter((task) => task.contractId !== undefined && !contractIds.has(task.contractId))
|
|
@@ -205,6 +212,7 @@ export async function rollbackOrphanTaskPointers(repository, contractIds) {
|
|
|
205
212
|
return [];
|
|
206
213
|
const document = documentFromView(current);
|
|
207
214
|
document.state = "open";
|
|
215
|
+
document.updatedAt = advancedTaskUpdatedAt(current, now);
|
|
208
216
|
delete document.startedAt;
|
|
209
217
|
delete document.contractId;
|
|
210
218
|
return [document];
|
|
@@ -9,6 +9,7 @@ const FIELD_ORDER = [
|
|
|
9
9
|
"parent",
|
|
10
10
|
"from",
|
|
11
11
|
"createdAt",
|
|
12
|
+
"updatedAt",
|
|
12
13
|
"creator",
|
|
13
14
|
"startedAt",
|
|
14
15
|
"contractId",
|
|
@@ -134,6 +135,8 @@ export function parseTaskDocument(source) {
|
|
|
134
135
|
if (from.includes(id))
|
|
135
136
|
invalid("task field 'from' must not reference itself");
|
|
136
137
|
const createdAt = canonicalTimestamp("createdAt", values.get("createdAt"));
|
|
138
|
+
const updatedValue = values.get("updatedAt");
|
|
139
|
+
const updatedAt = updatedValue === undefined ? undefined : canonicalTimestamp("updatedAt", updatedValue);
|
|
137
140
|
const creator = nonblank("creator", values.get("creator"));
|
|
138
141
|
const startedValue = values.get("startedAt");
|
|
139
142
|
const contractValue = values.get("contractId");
|
|
@@ -154,6 +157,7 @@ export function parseTaskDocument(source) {
|
|
|
154
157
|
parent,
|
|
155
158
|
from,
|
|
156
159
|
createdAt,
|
|
160
|
+
...(updatedAt === undefined ? {} : { updatedAt }),
|
|
157
161
|
creator,
|
|
158
162
|
...(startedAt === undefined ? {} : { startedAt }),
|
|
159
163
|
...(contractId === undefined ? {} : { contractId }),
|
|
@@ -174,6 +178,7 @@ function serializeUnchecked(document) {
|
|
|
174
178
|
parent: document.parent,
|
|
175
179
|
from: document.from,
|
|
176
180
|
createdAt: document.createdAt,
|
|
181
|
+
...(document.updatedAt === undefined ? {} : { updatedAt: document.updatedAt }),
|
|
177
182
|
creator: document.creator,
|
|
178
183
|
};
|
|
179
184
|
if (document.startedAt !== undefined)
|
package/build/core/task/index.js
CHANGED
|
@@ -9,4 +9,4 @@ export { openTaskCommandRepository, readTaskDoctor, taskCommandContractId, } fro
|
|
|
9
9
|
export { openTaskStore } from "./task-git-store.js";
|
|
10
10
|
export { taskRepositoryFromStore } from "./task-store-repository.js";
|
|
11
11
|
export { isTaskId } from "./validation.js";
|
|
12
|
-
export { buildTaskTree, createTask, doneTask, dropTask, parseTaskDocument, readTaskBoard, resolveTaskReadId, serializeTaskDocument, startTask, stopTask, updateTask, } from "./task.js";
|
|
12
|
+
export { buildTaskTree, createTask, doneTask, dropTask, parseTaskDocument, readTaskBoard, resolveTaskReadId, serializeTaskDocument, startTask, stopTask, updateTask, deriveTaskDisplay, selectTaskList, taskUpdatedAt, advancedTaskUpdatedAt, } from "./task.js";
|
package/build/core/task/model.js
CHANGED
|
@@ -1 +1,8 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export function taskUpdatedAt(task) {
|
|
2
|
+
return task.updatedAt ?? task.createdAt;
|
|
3
|
+
}
|
|
4
|
+
export function advancedTaskUpdatedAt(task, observed) {
|
|
5
|
+
const observedMs = observed instanceof Date ? observed.getTime() : Date.parse(observed);
|
|
6
|
+
const priorMs = Date.parse(taskUpdatedAt(task));
|
|
7
|
+
return new Date(Math.max(observedMs, priorMs + 1)).toISOString();
|
|
8
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { FlowError } from "../../flow-error.js";
|
|
2
|
+
import { isCommissionId } from "../ids.js";
|
|
3
|
+
import { taskUpdatedAt } from "./model.js";
|
|
4
|
+
import { isTaskId } from "./validation.js";
|
|
5
|
+
export const TASK_FILTER_FIELDS = [
|
|
6
|
+
"title", "state", "priority", "parent", "needs", "blocks", "contract", "created", "updated",
|
|
7
|
+
];
|
|
8
|
+
export const TASK_FILTER_OPERATORS = ["=", "!=", "~", "<", "<=", ">", ">="];
|
|
9
|
+
export const TASK_FILTER_GRAMMAR = "FIELD OP VALUE[,VALUE...] [| FIELD OP VALUE[,VALUE...]]...";
|
|
10
|
+
export const TASK_FILTER_COMPLETION = "FIELD OP VALUE[,VALUE...]|FIELD OP VALUE[,VALUE...]";
|
|
11
|
+
const FILTER_FIELDS = new Set(TASK_FILTER_FIELDS);
|
|
12
|
+
const ORDERED_FIELDS = new Set(["priority", "created", "updated"]);
|
|
13
|
+
const TITLE_OPERATORS = new Set(["=", "!=", "~"]);
|
|
14
|
+
const ORDERED_OPERATORS = new Set(["=", "!=", "<", "<=", ">", ">="]);
|
|
15
|
+
const DISPLAY_VALUES = new Set(["ready", "blocked", "in_progress", "done", "dropped", "bound"]);
|
|
16
|
+
const SORT_KEYS = new Set(["priority", "modified", "created"]);
|
|
17
|
+
const RELATIVE_TIME = /^now-([1-9][0-9]*)([smhdw])$/;
|
|
18
|
+
const UNIT_MS = { s: 1_000, m: 60_000, h: 3_600_000, d: 86_400_000, w: 604_800_000 };
|
|
19
|
+
export function deriveTaskDisplay(task) {
|
|
20
|
+
if (task.state === "done")
|
|
21
|
+
return "done";
|
|
22
|
+
if (task.state === "drop")
|
|
23
|
+
return "dropped";
|
|
24
|
+
if (task.contractId !== undefined)
|
|
25
|
+
return "bound";
|
|
26
|
+
if (task.state === "in_progress")
|
|
27
|
+
return "in_progress";
|
|
28
|
+
return task.blocked ? "blocked" : "ready";
|
|
29
|
+
}
|
|
30
|
+
function invalidFilter(detail, recoveredTerms = []) {
|
|
31
|
+
throw new FlowError("INVALID_TARGET", `invalid task filter: ${detail}`, {
|
|
32
|
+
facts: {
|
|
33
|
+
kind: "invalid_task_filter",
|
|
34
|
+
detail,
|
|
35
|
+
grammar: TASK_FILTER_GRAMMAR,
|
|
36
|
+
fields: [...TASK_FILTER_FIELDS],
|
|
37
|
+
operators: [...TASK_FILTER_OPERATORS],
|
|
38
|
+
recoveredTerms: recoveredTerms.map((term) => ({
|
|
39
|
+
field: term.field,
|
|
40
|
+
operator: term.operator,
|
|
41
|
+
values: [...term.values],
|
|
42
|
+
})),
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
function parseTaskFilterTerm(term, expression, recoveredTerms) {
|
|
47
|
+
const match = /^([a-z][a-z0-9_-]*)(!=|<=|>=|~|=|<|>)(.*)$/.exec(term);
|
|
48
|
+
if (!match)
|
|
49
|
+
invalidFilter(`'${expression}' must use ${TASK_FILTER_GRAMMAR}`, recoveredTerms);
|
|
50
|
+
const field = match[1];
|
|
51
|
+
const operator = match[2];
|
|
52
|
+
if (!FILTER_FIELDS.has(field))
|
|
53
|
+
invalidFilter(`unknown field '${field}'`, recoveredTerms);
|
|
54
|
+
if (field === "title" && !TITLE_OPERATORS.has(operator)) {
|
|
55
|
+
invalidFilter(`field '${field}' does not support operator '${operator}'`, recoveredTerms);
|
|
56
|
+
}
|
|
57
|
+
if (field !== "title" && (ORDERED_FIELDS.has(field)
|
|
58
|
+
? !ORDERED_OPERATORS.has(operator)
|
|
59
|
+
: operator !== "=" && operator !== "!=")) {
|
|
60
|
+
invalidFilter(`field '${field}' does not support operator '${operator}'`, recoveredTerms);
|
|
61
|
+
}
|
|
62
|
+
const values = match[3].split(",");
|
|
63
|
+
if (values.some((value) => value.length === 0))
|
|
64
|
+
invalidFilter(`field '${field}' has an empty value`, recoveredTerms);
|
|
65
|
+
validateFilterValues(field, values, recoveredTerms);
|
|
66
|
+
return { field, operator, values };
|
|
67
|
+
}
|
|
68
|
+
export function parseTaskFilter(expression) {
|
|
69
|
+
const terms = expression.split("|");
|
|
70
|
+
const parsed = [];
|
|
71
|
+
for (const term of terms) {
|
|
72
|
+
if (term.length === 0)
|
|
73
|
+
invalidFilter(`'${expression}' must use ${TASK_FILTER_GRAMMAR}`, parsed);
|
|
74
|
+
parsed.push(parseTaskFilterTerm(term, expression, parsed));
|
|
75
|
+
}
|
|
76
|
+
return parsed;
|
|
77
|
+
}
|
|
78
|
+
function parseTimestamp(value, nowMs, recoveredTerms = []) {
|
|
79
|
+
const relative = RELATIVE_TIME.exec(value);
|
|
80
|
+
if (relative) {
|
|
81
|
+
const amount = Number(relative[1]);
|
|
82
|
+
const offset = amount * UNIT_MS[relative[2]];
|
|
83
|
+
if (!Number.isSafeInteger(offset))
|
|
84
|
+
invalidFilter(`timestamp '${value}' is outside the supported range`, recoveredTerms);
|
|
85
|
+
return nowMs - offset;
|
|
86
|
+
}
|
|
87
|
+
const timestamp = Date.parse(value);
|
|
88
|
+
if (!Number.isFinite(timestamp) || new Date(timestamp).toISOString() !== value) {
|
|
89
|
+
invalidFilter(`timestamp '${value}' must be canonical ISO-8601 or now-<duration>`, recoveredTerms);
|
|
90
|
+
}
|
|
91
|
+
return timestamp;
|
|
92
|
+
}
|
|
93
|
+
function validateFilterValues(field, values, recoveredTerms) {
|
|
94
|
+
if (field === "title")
|
|
95
|
+
return;
|
|
96
|
+
if (field === "state") {
|
|
97
|
+
for (const value of values)
|
|
98
|
+
if (!DISPLAY_VALUES.has(value))
|
|
99
|
+
invalidFilter(`unknown state '${value}'`, recoveredTerms);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (field === "priority") {
|
|
103
|
+
for (const value of values)
|
|
104
|
+
if (!/^[0-3]$/.test(value))
|
|
105
|
+
invalidFilter(`priority '${value}' must be 0 through 3`, recoveredTerms);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (field === "created" || field === "updated") {
|
|
109
|
+
for (const value of values)
|
|
110
|
+
parseTimestamp(value, 0, recoveredTerms);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (field === "parent" || field === "needs" || field === "blocks") {
|
|
114
|
+
for (const value of values) {
|
|
115
|
+
if (field === "parent" && value === "none")
|
|
116
|
+
continue;
|
|
117
|
+
if (!isTaskId(value))
|
|
118
|
+
invalidFilter(`value '${value}' must be a canonical full task id`, recoveredTerms);
|
|
119
|
+
}
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (field === "contract") {
|
|
123
|
+
for (const value of values) {
|
|
124
|
+
if (value !== "none" && !isCommissionId(value))
|
|
125
|
+
invalidFilter(`value '${value}' must be a full commission id or none`, recoveredTerms);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
export function parseTaskSort(value) {
|
|
130
|
+
if (value === undefined)
|
|
131
|
+
return undefined;
|
|
132
|
+
const keys = value.split(",");
|
|
133
|
+
if (keys.some((key) => !SORT_KEYS.has(key))) {
|
|
134
|
+
const invalid = keys.find((key) => !SORT_KEYS.has(key));
|
|
135
|
+
throw new FlowError("INVALID_TARGET", `invalid task sort key '${String(invalid)}'; expected priority, modified, or created`);
|
|
136
|
+
}
|
|
137
|
+
if (new Set(keys).size !== keys.length) {
|
|
138
|
+
throw new FlowError("INVALID_TARGET", "task sort keys must not repeat");
|
|
139
|
+
}
|
|
140
|
+
return keys;
|
|
141
|
+
}
|
|
142
|
+
function compareText(left, right) {
|
|
143
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
144
|
+
}
|
|
145
|
+
function compareValue(left, right, operator) {
|
|
146
|
+
switch (operator) {
|
|
147
|
+
case "=": return left === right;
|
|
148
|
+
case "!=": return left !== right;
|
|
149
|
+
case "<": return left < right;
|
|
150
|
+
case "<=": return left <= right;
|
|
151
|
+
case ">": return left > right;
|
|
152
|
+
case ">=": return left >= right;
|
|
153
|
+
case "~": return false;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function matchesFilter(task, filter, nowMs) {
|
|
157
|
+
return filter.values.some((raw) => {
|
|
158
|
+
if (filter.field === "title") {
|
|
159
|
+
if (filter.operator === "~")
|
|
160
|
+
return task.title.toLowerCase().includes(raw.toLowerCase());
|
|
161
|
+
return compareValue(task.title, raw, filter.operator);
|
|
162
|
+
}
|
|
163
|
+
if (filter.field === "state")
|
|
164
|
+
return compareValue(deriveTaskDisplay(task), raw, filter.operator);
|
|
165
|
+
if (filter.field === "priority")
|
|
166
|
+
return compareValue(task.pri, Number(raw), filter.operator);
|
|
167
|
+
if (filter.field === "created")
|
|
168
|
+
return compareValue(Date.parse(task.createdAt), parseTimestamp(raw, nowMs), filter.operator);
|
|
169
|
+
if (filter.field === "updated")
|
|
170
|
+
return compareValue(Date.parse(taskUpdatedAt(task)), parseTimestamp(raw, nowMs), filter.operator);
|
|
171
|
+
const expected = raw === "none" ? null : raw;
|
|
172
|
+
if (filter.field === "parent")
|
|
173
|
+
return compareValue(task.parent ?? "\0", expected ?? "\0", filter.operator);
|
|
174
|
+
if (filter.field === "contract")
|
|
175
|
+
return compareValue(task.contractId ?? "\0", expected ?? "\0", filter.operator);
|
|
176
|
+
const relations = filter.field === "needs" ? task.needs : task.blocks;
|
|
177
|
+
return filter.operator === "=" ? relations.includes(raw) : !relations.includes(raw);
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
function selected(task, selection) {
|
|
181
|
+
const terminal = task.state === "done" || task.state === "drop";
|
|
182
|
+
return selection === "all" || (selection === "done" ? terminal : !terminal);
|
|
183
|
+
}
|
|
184
|
+
function displayOrder(task) {
|
|
185
|
+
const display = deriveTaskDisplay(task);
|
|
186
|
+
return display === "in_progress" ? 0 : display === "ready" ? 1 : display === "blocked" ? 2 : display === "bound" ? 3 : display === "done" ? 4 : 5;
|
|
187
|
+
}
|
|
188
|
+
function defaultCompare(left, right) {
|
|
189
|
+
return displayOrder(left) - displayOrder(right)
|
|
190
|
+
|| left.pri - right.pri
|
|
191
|
+
|| compareText(left.createdAt, right.createdAt)
|
|
192
|
+
|| compareText(left.id, right.id);
|
|
193
|
+
}
|
|
194
|
+
function explicitCompare(keys, left, right) {
|
|
195
|
+
for (const key of keys) {
|
|
196
|
+
const difference = key === "priority"
|
|
197
|
+
? left.pri - right.pri
|
|
198
|
+
: key === "modified"
|
|
199
|
+
? compareText(taskUpdatedAt(right), taskUpdatedAt(left))
|
|
200
|
+
: compareText(right.createdAt, left.createdAt);
|
|
201
|
+
if (difference !== 0)
|
|
202
|
+
return difference;
|
|
203
|
+
}
|
|
204
|
+
return compareText(left.id, right.id);
|
|
205
|
+
}
|
|
206
|
+
export function selectTaskList(tasks, options = {}) {
|
|
207
|
+
const filters = (options.filters ?? []).map(parseTaskFilter);
|
|
208
|
+
const sort = parseTaskSort(options.sort);
|
|
209
|
+
const nowMs = options.nowMs ?? Date.now();
|
|
210
|
+
return tasks
|
|
211
|
+
.filter((task) => selected(task, options.selection ?? "default"))
|
|
212
|
+
.filter((task) => filters.every((filter) => filter.some((term) => matchesFilter(task, term, nowMs))))
|
|
213
|
+
.sort(sort === undefined ? defaultCompare : (left, right) => explicitCompare(sort, left, right));
|
|
214
|
+
}
|