@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.
Files changed (143) hide show
  1. package/build/.tsbuildinfo +1 -1
  2. package/build/agents/call-terms.js +1 -0
  3. package/build/agents/harness/execution-handle.js +1 -0
  4. package/build/agents/harness/outcome.js +31 -5
  5. package/build/agents/harness/projection.js +151 -78
  6. package/build/agents/harness/router.js +2 -1
  7. package/build/agents/providers/claude-agent-sdk/adapter.js +13 -1
  8. package/build/agents/providers/claude-agent-sdk/registration.js +2 -1
  9. package/build/agents/providers/codex-app-server/adapter.js +43 -10
  10. package/build/agents/providers/codex-app-server/events.js +23 -0
  11. package/build/agents/providers/codex-app-server/registration.js +2 -1
  12. package/build/agents/providers/opencode-sdk/adapter.js +7 -0
  13. package/build/agents/providers/opencode-sdk/registration.js +2 -1
  14. package/build/agents/providers/pi/adapter.js +10 -0
  15. package/build/agents/providers/pi/events.js +4 -3
  16. package/build/agents/providers/pi/registration.js +3 -1
  17. package/build/agents/types.js +2 -0
  18. package/build/cli/commands/akuma/akuma/meta.js +1 -1
  19. package/build/cli/commands/akuma/list/meta-ls.js +1 -1
  20. package/build/cli/commands/akuma/view/meta.js +1 -1
  21. package/build/cli/commands/contract/amend/meta.js +4 -4
  22. package/build/cli/commands/contract/arc/handler.js +1 -1
  23. package/build/cli/commands/contract/arc/meta.js +4 -4
  24. package/build/cli/commands/contract/audit/handler.js +16 -0
  25. package/build/cli/commands/contract/audit/meta.js +16 -0
  26. package/build/cli/commands/contract/bind/meta.js +20 -11
  27. package/build/cli/commands/contract/catalog.js +2 -0
  28. package/build/cli/commands/contract/forfeit/meta.js +1 -1
  29. package/build/cli/commands/contract/log/meta.js +1 -1
  30. package/build/cli/commands/contract/petition/handler.js +1 -2
  31. package/build/cli/commands/contract/petition/meta.js +6 -6
  32. package/build/cli/commands/contract/renew/handler.js +2 -2
  33. package/build/cli/commands/contract/renew/meta.js +4 -4
  34. package/build/cli/commands/metadata.js +3 -2
  35. package/build/cli/commands/projection/call/meta.js +4 -1
  36. package/build/cli/commands/projection/kill/meta.js +1 -1
  37. package/build/cli/commands/projection/revive/meta.js +4 -1
  38. package/build/cli/commands/projection/status/meta.js +1 -1
  39. package/build/cli/commands/projection/tell/handler.js +1 -1
  40. package/build/cli/commands/projection/tell/meta.js +4 -1
  41. package/build/cli/commands/projection/wait/meta.js +1 -1
  42. package/build/cli/commands/skills/install/meta.js +1 -1
  43. package/build/cli/commands/skills/skills/meta.js +1 -1
  44. package/build/cli/commands/system/completion/meta.js +1 -1
  45. package/build/cli/commands/system/dump-env/meta.js +1 -1
  46. package/build/cli/commands/system/guide/meta.js +1 -1
  47. package/build/cli/commands/task/add/handler.js +2 -2
  48. package/build/cli/commands/task/add/meta.js +4 -1
  49. package/build/cli/commands/task/doctor/meta.js +1 -1
  50. package/build/cli/commands/task/done/meta.js +1 -1
  51. package/build/cli/commands/task/drop/meta.js +1 -1
  52. package/build/cli/commands/task/ls/handler.js +10 -4
  53. package/build/cli/commands/task/ls/meta.js +3 -3
  54. package/build/cli/commands/task/show/meta.js +1 -1
  55. package/build/cli/commands/task/start/meta.js +1 -1
  56. package/build/cli/commands/task/stop/meta.js +1 -1
  57. package/build/cli/commands/task/task/meta.js +1 -1
  58. package/build/cli/commands/task/update/handler.js +2 -3
  59. package/build/cli/commands/task/update/meta.js +4 -1
  60. package/build/cli/completion.js +9 -2
  61. package/build/cli/flags.js +19 -3
  62. package/build/cli/help.js +3 -3
  63. package/build/cli/index.js +59 -75
  64. package/build/cli/output-stream-error-policy.js +19 -5
  65. package/build/cli/parse-flags.js +19 -2
  66. package/build/cli/parse-metadata.js +1 -7
  67. package/build/cli/parse-selectors.js +1 -0
  68. package/build/cli/parse.js +16 -7
  69. package/build/cli/projection-address.js +6 -1
  70. package/build/cli/render/audit.js +121 -0
  71. package/build/cli/render/errors.js +18 -1
  72. package/build/cli/render/format.js +5 -2
  73. package/build/cli/render/misc.js +3 -1
  74. package/build/cli/render/projection-activity.js +12 -6
  75. package/build/cli/render/status.js +28 -18
  76. package/build/cli/render/success-response.js +6 -5
  77. package/build/cli/render/task.js +12 -43
  78. package/build/cli/render/terminal-failure.js +7 -2
  79. package/build/cli/render/wait.js +36 -8
  80. package/build/config/env-keys.js +2 -0
  81. package/build/config/env.js +3 -0
  82. package/build/core/amend.js +20 -94
  83. package/build/core/arc.js +14 -6
  84. package/build/core/audit/candidate.js +48 -0
  85. package/build/core/audit/coordinates.js +180 -0
  86. package/build/core/audit/evidence.js +158 -0
  87. package/build/core/audit/facade.js +42 -0
  88. package/build/core/audit/readiness.js +4 -0
  89. package/build/core/audit/report.js +38 -0
  90. package/build/core/audit.js +1 -0
  91. package/build/core/bind.js +1 -1
  92. package/build/core/call/execution.js +1 -0
  93. package/build/core/contract-view.js +2 -3
  94. package/build/core/draft.js +33 -20
  95. package/build/core/entry.js +3 -4
  96. package/build/core/markdown/lex.js +2 -1
  97. package/build/core/projection/generation/database.js +41 -16
  98. package/build/core/projection/generation/model.js +22 -2
  99. package/build/core/projection/generation/projection-generation-execution.js +20 -7
  100. package/build/core/projection/generation/projection-generation-launcher.js +4 -4
  101. package/build/core/projection/generation/projection-generation-runner.js +1 -0
  102. package/build/core/projection/generation/store.js +1 -1
  103. package/build/core/projection/generation/transitions.js +19 -3
  104. package/build/core/projection/index.js +2 -2
  105. package/build/core/projection/projection-execution-observer.js +35 -21
  106. package/build/core/projection/projection-life-observer.js +16 -2
  107. package/build/core/projection/projection-status.js +67 -22
  108. package/build/core/projection/projection-terminal-failure.js +2 -0
  109. package/build/core/projection/projection-wait.js +6 -9
  110. package/build/core/projection/projection-wake.js +30 -6
  111. package/build/core/renew.js +41 -22
  112. package/build/core/scope.js +129 -2
  113. package/build/core/seal.js +36 -15
  114. package/build/core/settlement/petition-claim-gates.js +4 -33
  115. package/build/core/settlement/petition-preview.js +1 -1
  116. package/build/core/settlement/petition.js +10 -3
  117. package/build/core/settlement/settlement.js +3 -10
  118. package/build/core/settlement/verification.js +59 -0
  119. package/build/core/status/board.js +48 -28
  120. package/build/core/stored-agent-event.js +3 -2
  121. package/build/core/task/board.js +1 -0
  122. package/build/core/task/commands.js +20 -12
  123. package/build/core/task/document.js +5 -0
  124. package/build/core/task/index.js +1 -1
  125. package/build/core/task/model.js +8 -1
  126. package/build/core/task/query.js +214 -0
  127. package/build/core/task/settlement-git.js +115 -16
  128. package/build/core/task/settlement-policy.js +17 -9
  129. package/build/core/task/source-board.js +1 -0
  130. package/build/core/task/task-contract.js +125 -51
  131. package/build/core/task/task-store-repository.js +1 -0
  132. package/build/core/task/task.js +2 -0
  133. package/build/flow-error.js +2 -4
  134. package/build/generated/version.js +2 -2
  135. package/build/git/diff/parsers.js +23 -13
  136. package/build/git/diff/structured.js +127 -0
  137. package/build/index.js +7 -7
  138. package/build/telemetry/logger.js +4 -2
  139. package/package.json +1 -1
  140. package/skills/keiyaku/SKILL.md +2 -2
  141. package/skills/keiyaku-akuma/SKILL.md +17 -6
  142. package/skills/keiyaku-task/SKILL.md +16 -7
  143. 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
- if (left.pri !== right.pri)
13
- return left.pri - right.pri;
14
- if (left.createdAt !== right.createdAt)
15
- return left.createdAt < right.createdAt ? -1 : 1;
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
- async function readTaskStatusSummary(cwd) {
19
- try {
20
- const board = await readTaskBoard(taskRepositoryFromStore(await openTaskStore(cwd)));
21
- const eligible = board.tasks.filter((task) => !task.contractId && (task.state === "open" || task.state === "in_progress"));
22
- const inProgress = eligible.filter((task) => task.state === "in_progress").length;
23
- const topTasks = [
24
- ...eligible.filter((task) => task.state === "in_progress").sort(compareTaskStatusRows),
25
- ...eligible.filter((task) => task.state === "open" && !task.blocked).sort(compareTaskStatusRows),
26
- ]
27
- .slice(0, 5)
28
- .map(({ id, title, pri, createdAt, state, blocked }) => ({ id, title, pri, createdAt, state, blocked }));
29
- const ready = eligible.filter((task) => task.state === "open" && !task.blocked).length;
30
- const blocked = eligible.filter((task) => task.state === "open" && task.blocked).length;
31
- if (board.tasks.length === 0 && board.diseases.length === 0)
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
- total: board.tasks.length,
35
- inProgress,
36
- ready,
37
- blocked,
38
- omitted: inProgress + ready - topTasks.length,
39
- topTasks,
40
- diseases: board.diseases,
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
- && nonNegativeSafeInteger(warning.retryDelayMs)
18
+ && nonNegativeFinite(warning.retryDelayMs)
18
19
  && safeIntegerOrNull(warning.errorStatus);
19
20
  }
20
21
  if (warning.reason === "refusal" || warning.reason === "overload") {
@@ -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", deps.now()),
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 document = documentFromView(requireExisting(board, id));
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)
@@ -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";
@@ -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
+ }