@astrosheep/keiyaku 2.9.9 → 2.9.11

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 (230) hide show
  1. package/README.md +93 -174
  2. package/build/.tsbuildinfo +1 -1
  3. package/build/agents/harness/control-types.js +18 -1
  4. package/build/agents/harness/index.js +2 -0
  5. package/build/agents/harness/outcome-error.js +81 -0
  6. package/build/agents/harness/projection.js +2 -1
  7. package/build/agents/harness/router.js +5 -1
  8. package/build/agents/providers/claude-agent-sdk/adapter.js +18 -4
  9. package/build/agents/providers/claude-agent-sdk/registration.js +11 -1
  10. package/build/agents/providers/claude-agent-sdk/session.js +19 -5
  11. package/build/agents/providers/codex-app-server/adapter.js +11 -6
  12. package/build/agents/providers/codex-app-server/events.js +67 -6
  13. package/build/agents/providers/codex-app-server/registration.js +11 -1
  14. package/build/agents/providers/opencode-sdk/adapter.js +8 -1
  15. package/build/agents/providers/opencode-sdk/events.js +24 -2
  16. package/build/agents/providers/opencode-sdk/registration.js +11 -1
  17. package/build/agents/providers/pi/checkpoint.js +9 -1
  18. package/build/agents/providers/pi/events.js +2 -0
  19. package/build/agents/providers/pi/registration.js +14 -1
  20. package/build/cli/commands/akuma/akuma/handler.js +1 -1
  21. package/build/cli/commands/akuma/akuma/meta.js +3 -3
  22. package/build/cli/commands/akuma/catalog.js +2 -2
  23. package/build/cli/commands/akuma/{view → show}/handler.js +5 -5
  24. package/build/cli/commands/akuma/{view → show}/meta.js +3 -3
  25. package/build/cli/commands/akuma.js +1 -1
  26. package/build/cli/commands/contract/amend/handler.js +5 -4
  27. package/build/cli/commands/contract/arc/handler.js +5 -4
  28. package/build/cli/commands/contract/audit/handler.js +2 -2
  29. package/build/cli/commands/contract/bind/handler.js +12 -3
  30. package/build/cli/commands/contract/bind/meta.js +45 -15
  31. package/build/cli/commands/contract/forfeit/handler.js +9 -5
  32. package/build/cli/commands/contract/log/handler.js +2 -2
  33. package/build/cli/commands/contract/petition/handler.js +34 -7
  34. package/build/cli/commands/contract/renew/handler.js +7 -7
  35. package/build/cli/commands/contract/renew/meta.js +3 -3
  36. package/build/cli/commands/metadata.js +10 -6
  37. package/build/cli/commands/projection/call/meta.js +1 -1
  38. package/build/cli/commands/projection/catalog.js +2 -0
  39. package/build/cli/commands/projection/history/handler.js +21 -0
  40. package/build/cli/commands/projection/history/meta.js +9 -0
  41. package/build/cli/commands/projection/shared.js +3 -4
  42. package/build/cli/commands/projection/status/handler.js +3 -3
  43. package/build/cli/commands/projection/wait/handler.js +20 -11
  44. package/build/cli/commands/task/add/handler.js +14 -5
  45. package/build/cli/commands/task/add/meta.js +2 -2
  46. package/build/cli/commands/task/catalog.js +4 -0
  47. package/build/cli/commands/task/done/handler.js +1 -4
  48. package/build/cli/commands/task/done/meta.js +1 -1
  49. package/build/cli/commands/task/drop/meta.js +1 -1
  50. package/build/cli/commands/task/hold/handler.js +3 -0
  51. package/build/cli/commands/task/hold/meta.js +8 -0
  52. package/build/cli/commands/task/log/handler.js +1 -1
  53. package/build/cli/commands/task/log/meta.js +2 -2
  54. package/build/cli/commands/task/resume/handler.js +3 -0
  55. package/build/cli/commands/task/resume/meta.js +8 -0
  56. package/build/cli/commands/task/shared.js +39 -14
  57. package/build/cli/commands/task/show/handler.js +1 -1
  58. package/build/cli/commands/task/show/meta.js +1 -1
  59. package/build/cli/commands/task/start/meta.js +1 -1
  60. package/build/cli/commands/task/stop/handler.js +1 -4
  61. package/build/cli/commands/task/stop/meta.js +1 -1
  62. package/build/cli/commands/task/update/handler.js +11 -3
  63. package/build/cli/commands/task/update/meta.js +3 -3
  64. package/build/cli/completion.js +3 -2
  65. package/build/cli/flags.js +51 -14
  66. package/build/cli/help.js +4 -1
  67. package/build/cli/index.js +40 -25
  68. package/build/cli/parse-flags.js +40 -7
  69. package/build/cli/parse-metadata.js +5 -2
  70. package/build/cli/parse-selectors.js +2 -3
  71. package/build/cli/parse.js +40 -6
  72. package/build/cli/projection-address.js +12 -10
  73. package/build/cli/render/amendment-warning.js +8 -0
  74. package/build/cli/render/arc.js +6 -2
  75. package/build/cli/render/audit.js +400 -81
  76. package/build/cli/render/kanshi.js +6 -1
  77. package/build/cli/render/lifecycle.js +56 -0
  78. package/build/cli/render/misc.js +24 -28
  79. package/build/cli/render/petition.js +5 -0
  80. package/build/cli/render/projection-activity.js +3 -2
  81. package/build/cli/render/projection-history.js +64 -0
  82. package/build/cli/render/shared.js +23 -9
  83. package/build/cli/render/status.js +9 -16
  84. package/build/cli/render/task.js +51 -26
  85. package/build/cli/render/terminal-failure.js +3 -1
  86. package/build/cli/render/wait.js +3 -5
  87. package/build/cli/subagent-guard.js +1 -1
  88. package/build/cli/types.js +1 -1
  89. package/build/config/env-keys.js +2 -0
  90. package/build/config/env.js +23 -0
  91. package/build/config/settings/schema.js +3 -1
  92. package/build/core/address-carrier.js +89 -0
  93. package/build/core/addressing.js +255 -196
  94. package/build/core/amend.js +115 -38
  95. package/build/core/amendment-criteria.js +17 -0
  96. package/build/core/amendment-warning.js +10 -0
  97. package/build/core/arc.js +184 -83
  98. package/build/core/audit/candidate.js +110 -9
  99. package/build/core/audit/coordinates.js +120 -65
  100. package/build/core/audit/evidence.js +19 -23
  101. package/build/core/audit/facade.js +34 -11
  102. package/build/core/audit/report.js +5 -5
  103. package/build/core/bind-reconciliation.js +390 -0
  104. package/build/core/bind-workspace.js +12 -20
  105. package/build/core/bind.js +242 -273
  106. package/build/core/call/call.js +1 -0
  107. package/build/core/call/context.js +46 -71
  108. package/build/core/call/prompt.js +6 -3
  109. package/build/core/context.js +49 -35
  110. package/build/core/contract-carrier-runtime.js +408 -0
  111. package/build/core/contract-carrier.js +270 -0
  112. package/build/core/contract-view.js +33 -27
  113. package/build/core/contract.js +5 -1
  114. package/build/core/derived-replay.js +207 -0
  115. package/build/core/draft.js +125 -57
  116. package/build/core/forfeit.js +113 -56
  117. package/build/core/identifier-slug.js +63 -13
  118. package/build/core/ids.js +4 -4
  119. package/build/core/lifecycle-history.js +35 -0
  120. package/build/core/lifecycle-recovery.js +117 -0
  121. package/build/core/lifecycle-runner.js +377 -0
  122. package/build/core/log.js +37 -11
  123. package/build/core/outcome-base.js +181 -0
  124. package/build/core/process-group.js +139 -0
  125. package/build/core/projection/generation/database.js +82 -7
  126. package/build/core/projection/generation/model.js +9 -36
  127. package/build/core/projection/generation/projection-generation-continuation.js +10 -22
  128. package/build/core/projection/generation/projection-generation-execution.js +6 -5
  129. package/build/core/projection/index.js +5 -4
  130. package/build/core/projection/projection-activity.js +27 -8
  131. package/build/core/projection/projection-alias.js +23 -10
  132. package/build/core/projection/projection-core.js +21 -7
  133. package/build/core/projection/projection-execution-observer.js +91 -39
  134. package/build/core/projection/projection-history.js +474 -0
  135. package/build/core/projection/projection-runner-lock.js +12 -0
  136. package/build/core/projection/projection-status-observation.js +365 -0
  137. package/build/core/projection/projection-status.js +21 -358
  138. package/build/core/projection/projection-terminal-failure.js +13 -8
  139. package/build/core/projection/projection-wait.js +7 -2
  140. package/build/core/projection/tell/launch-store.js +244 -0
  141. package/build/core/projection/tell/model.js +3 -3
  142. package/build/core/projection/tell/store.js +70 -118
  143. package/build/core/registry.js +22 -87
  144. package/build/core/renew-build.js +424 -0
  145. package/build/core/renew-plan.js +239 -0
  146. package/build/core/renew-rewrite.js +77 -0
  147. package/build/core/renew.js +295 -520
  148. package/build/core/repository-ledger/accepted-fold-read.js +19 -0
  149. package/build/core/repository-ledger/accepted-tail.js +116 -0
  150. package/build/core/repository-ledger/admission.js +38 -0
  151. package/build/core/repository-ledger/authoritative-transaction.js +1 -0
  152. package/build/core/repository-ledger/claim-evidence.js +147 -0
  153. package/build/core/repository-ledger/codec.js +297 -0
  154. package/build/core/repository-ledger/current-state-store.js +853 -0
  155. package/build/core/repository-ledger/fold-repository.js +375 -0
  156. package/build/core/repository-ledger/fold.js +248 -0
  157. package/build/core/repository-ledger/git-object-store.js +243 -0
  158. package/build/core/repository-ledger/history-read.js +15 -0
  159. package/build/core/repository-ledger/incremental-read.js +153 -0
  160. package/build/core/repository-ledger/index.js +15 -0
  161. package/build/core/repository-ledger/inventory-plan.js +75 -0
  162. package/build/core/repository-ledger/inventory.js +310 -0
  163. package/build/core/repository-ledger/publication-admission.js +125 -0
  164. package/build/core/repository-ledger/publication.js +353 -0
  165. package/build/core/repository-ledger/read-model.js +469 -0
  166. package/build/core/repository-ledger/transaction-membership.js +78 -0
  167. package/build/core/repository-ledger/traversal.js +404 -0
  168. package/build/core/repository-ledger/write-transaction.js +339 -0
  169. package/build/core/seal.js +168 -161
  170. package/build/core/settlement/claim-delivery.js +42 -215
  171. package/build/core/settlement/claim.js +235 -153
  172. package/build/core/settlement/index.js +4 -4
  173. package/build/core/settlement/petition-claim-gates.js +52 -33
  174. package/build/core/settlement/petition-forfeit.js +12 -5
  175. package/build/core/settlement/petition-head-guard.js +8 -5
  176. package/build/core/settlement/petition-preview.js +68 -56
  177. package/build/core/settlement/petition-window.js +50 -0
  178. package/build/core/settlement/petition.js +252 -131
  179. package/build/core/settlement/queue-read-model.js +72 -95
  180. package/build/core/settlement/queue-seat-allocation.js +35 -126
  181. package/build/core/settlement/queue.js +1 -1
  182. package/build/core/settlement/settlement.js +172 -80
  183. package/build/core/settlement/verdict.js +112 -66
  184. package/build/core/settlement/verification-coordination.js +305 -0
  185. package/build/core/settlement/verification-supervisor.js +275 -0
  186. package/build/core/settlement/verification.js +398 -31
  187. package/build/core/status/board.js +114 -119
  188. package/build/core/status/lifecycle.js +7 -80
  189. package/build/core/structured-query.js +23 -0
  190. package/build/core/target-ref.js +1 -1
  191. package/build/core/task/board.js +59 -24
  192. package/build/core/task/commands.js +181 -33
  193. package/build/core/task/coordinate.js +56 -0
  194. package/build/core/task/document.js +25 -9
  195. package/build/core/task/index.js +7 -4
  196. package/build/core/task/query.js +17 -27
  197. package/build/core/task/settlement-git.js +164 -179
  198. package/build/core/task/settlement-policy.js +15 -11
  199. package/build/core/task/source-board.js +31 -13
  200. package/build/core/task/task-bind-preparation.js +193 -0
  201. package/build/core/task/task-contract.js +102 -193
  202. package/build/core/task/task-file-transaction.js +171 -0
  203. package/build/core/task/task-git-runtime.js +76 -14
  204. package/build/core/task/task-git-store.js +68 -169
  205. package/build/core/task/task-store-repository.js +13 -6
  206. package/build/core/task/task-worktree.js +6 -33
  207. package/build/core/task/task.js +1 -1
  208. package/build/core/task/tree.js +3 -3
  209. package/build/core/task/validation.js +4 -3
  210. package/build/core/transcripts.js +93 -1
  211. package/build/core/worktree-bootstrap.js +44 -36
  212. package/build/core/worktree-path.js +210 -77
  213. package/build/flow-error.js +7 -7
  214. package/build/generated/version.js +2 -2
  215. package/build/git/core.js +76 -13
  216. package/build/git/refs.js +168 -46
  217. package/build/git/streaming-batch.js +73 -13
  218. package/package.json +1 -1
  219. package/skills/keiyaku/SKILL.md +1 -1
  220. package/skills/keiyaku-akuma/SKILL.md +3 -3
  221. package/skills/keiyaku-task/SKILL.md +4 -2
  222. package/skills/keiyaku-workflow/SKILL.md +88 -203
  223. package/build/core/entry.js +0 -305
  224. package/build/core/ledger-batch.js +0 -194
  225. package/build/core/ledger.js +0 -84
  226. package/build/core/projection/leash.js +0 -87
  227. package/build/core/ref-log.js +0 -119
  228. package/build/core/renew-session.js +0 -128
  229. package/build/core/status/ledger-batch.js +0 -1
  230. package/build/core/status/reconciliation.js +0 -137
@@ -1,14 +1,19 @@
1
1
  import { availableParallelism } from "node:os";
2
+ import * as fs from "node:fs/promises";
3
+ import * as path from "node:path";
2
4
  import { loadKeiyakuSettings } from "../../config/settings/loader.js";
3
5
  import { observeGitRepository } from "../../git/branches.js";
6
+ import { currentLifecycleState } from "../repository-ledger/read-model.js";
7
+ import { parseCarrierIdentity } from "../contract-carrier.js";
8
+ import { KEIYAKU_FILE } from "../../keiyaku.js";
9
+ import { lookupCurrentContract, } from "../repository-ledger/read-model.js";
4
10
  import { readExactProjectionStatus, readOperationalProjectionStatusBoard, } from "../projection/index.js";
5
11
  import { deriveQueueReadModel, queueMembership, } from "../settlement/index.js";
6
- import { findOpenArc, getMaxArc, receiptsAfterFence } from "../seal.js";
7
12
  import { openTaskStore, readTaskBoard, taskRepositoryFromStore, taskUpdatedAt, } from "../task/index.js";
8
- import { deriveBootstrapDiagnostics, deriveContractState, hasCurrentPetitionVerdict, isTerminalState, } from "./lifecycle.js";
9
- import { commissionTarget, evaluateBaseDrift, latestBase, resolveDefaultBranchFromSettings, } from "./drift.js";
10
- import { computeClaimReconciliation, } from "./reconciliation.js";
11
- import { readStatusLedgers } from "./ledger-batch.js";
13
+ import { replayBlockingFact, } from "./lifecycle.js";
14
+ import { findRegisteredContractWorktreePathFromModel, stableRepoRoot } from "../worktree-path.js";
15
+ import { evaluateBaseDrift, resolveDefaultBranchFromSettings, } from "./drift.js";
16
+ import { readRepositoryLifecycleSummaries as readStatusLifecycle } from "../repository-ledger/read-model.js";
12
17
  // Git object reads benefit from overlap, but a hard cap avoids process and disk contention.
13
18
  const STATUS_DRIFT_MAX_CONCURRENCY = 8;
14
19
  const STATUS_DRIFT_CONCURRENCY = Math.min(STATUS_DRIFT_MAX_CONCURRENCY, availableParallelism());
@@ -24,10 +29,11 @@ function timestampAge(timestamp, now) {
24
29
  return Number.isFinite(value) ? Math.max(0, now - value) : 0;
25
30
  }
26
31
  export function deriveTaskStatusSummary(board, now) {
27
- const eligible = board.tasks.filter((task) => !task.contractId && (task.state === "open" || task.state === "in_progress"));
32
+ const eligible = board.tasks.filter((task) => !task.contractId && (task.state === "open" || task.state === "in_progress" || task.state === "on_hold"));
28
33
  const inProgress = eligible.filter((task) => task.state === "in_progress").length;
34
+ const onHold = eligible.filter((task) => task.state === "on_hold").length;
29
35
  const topTasks = eligible
30
- .filter((task) => task.state === "in_progress")
36
+ .filter((task) => task.state === "in_progress" || task.state === "on_hold")
31
37
  .sort(compareTaskStatusRows)
32
38
  .map((task) => {
33
39
  const updatedAt = taskUpdatedAt(task);
@@ -48,6 +54,7 @@ export function deriveTaskStatusSummary(board, now) {
48
54
  return {
49
55
  total: eligible.length,
50
56
  inProgress,
57
+ onHold,
51
58
  ready,
52
59
  blocked,
53
60
  omitted: ready + blocked,
@@ -64,6 +71,7 @@ async function readTaskStatusSummary(cwd, now) {
64
71
  return {
65
72
  total: 0,
66
73
  inProgress: 0,
74
+ onHold: 0,
67
75
  ready: 0,
68
76
  blocked: 0,
69
77
  omitted: 0,
@@ -77,10 +85,7 @@ function isQueueHeadStall(input) {
77
85
  return (input.queuePosition === 1 &&
78
86
  input.queueLength > 1 &&
79
87
  input.state.state === "petitioned" &&
80
- !hasCurrentPetitionVerdict(input.entries));
81
- }
82
- function findBindEntry(entries) {
83
- return entries.find((entry) => entry.kind === "bind");
88
+ !input.hasVerdict);
84
89
  }
85
90
  function contractSlugFromId(id) {
86
91
  const dot = id.indexOf(".");
@@ -88,44 +93,25 @@ function contractSlugFromId(id) {
88
93
  return id.slice(0, dot);
89
94
  return id;
90
95
  }
91
- function deriveCurrentArc(entries, bindObjective, lifecycle) {
92
- // No explicit arc has ever existed: derive a0 from bind objective.
93
- if (getMaxArc(entries) === 0) {
94
- const a0Open = lifecycle.state === "bound" || lifecycle.state === "active";
95
- return {
96
- number: 0,
97
- objective: bindObjective ?? "",
98
- state: a0Open ? "open" : "closed",
99
- };
100
- }
101
- const arcObjectives = new Map();
102
- let currentArc = null;
103
- for (const entry of entries) {
104
- if (entry.kind === "arc") {
105
- arcObjectives.set(entry.data.arc, entry.data.objective);
106
- currentArc = {
107
- number: entry.data.arc,
108
- objective: entry.data.objective,
109
- state: "open",
110
- };
111
- continue;
112
- }
113
- if (entry.kind === "seal" && entry.data.arc > 0) {
114
- currentArc = {
115
- number: entry.data.arc,
116
- objective: arcObjectives.get(entry.data.arc) ?? bindObjective ?? "",
117
- state: "closed",
118
- };
119
- }
120
- }
121
- if (currentArc)
122
- return currentArc;
96
+ function deriveCurrentArc(model) {
123
97
  return {
124
- number: 0,
125
- objective: bindObjective ?? "",
126
- state: "closed",
98
+ number: model.intent.arc,
99
+ objective: model.intent.objective,
100
+ state: model.phase === "active" ? "open" : "closed",
127
101
  };
128
102
  }
103
+ function bindTitle(model) {
104
+ const bind = model.history.find(({ entry }) => entry.kind === "bind")?.entry;
105
+ return bind?.kind === "bind" ? bind.data.title : model.intent.title;
106
+ }
107
+ function lifecycleDiseaseReason(read, contract) {
108
+ return read.diagnostics.filter((diagnostic) => diagnostic.contract === contract).map((diagnostic) => diagnostic.detail).join("; ");
109
+ }
110
+ function bootstrapReplayBlockers(model) {
111
+ return model.bootstrapReplay.state === "pending"
112
+ ? [replayBlockingFact(model.contract, "bootstrap")]
113
+ : [];
114
+ }
129
115
  function deriveNextStep(input) {
130
116
  if (input.state.state === "corrupt") {
131
117
  return { kind: "inspect-log", detail: "inspect log" };
@@ -152,8 +138,8 @@ function deriveNextStep(input) {
152
138
  }
153
139
  return { kind: "arc", detail: "arc" };
154
140
  }
155
- function deriveLastActiveAge(entries, now) {
156
- const last = entries.at(-1);
141
+ function deriveLastActiveAge(model, now) {
142
+ const last = model.history.at(-1)?.entry;
157
143
  if (!last)
158
144
  return {};
159
145
  const atMs = Date.parse(last.at);
@@ -202,7 +188,6 @@ export async function readKanshiBoard(cwd, now, opts) {
202
188
  : exactProjection ? { rows: [exactProjection] } : null,
203
189
  queueLength: 0,
204
190
  diseases: loadedSettings.diseases,
205
- claimReconciliation: null,
206
191
  targetFilter,
207
192
  // Task and contract boards are repository organs; a non-repository cwd
208
193
  // cannot truthfully turn their absence into a zero count.
@@ -217,109 +202,121 @@ export async function readKanshiBoard(cwd, now, opts) {
217
202
  catch {
218
203
  defaultBranch = null;
219
204
  }
220
- const terminalById = new Map();
221
- const ledgers = await readStatusLedgers(cwd);
222
- const readableLedgers = [];
223
- for (const ledger of ledgers) {
224
- if ("error" in ledger)
225
- continue;
226
- readableLedgers.push(ledger);
227
- terminalById.set(ledger.contractId, isTerminalState(deriveContractState(ledger.entries)));
228
- }
229
- const queueModel = deriveQueueReadModel(readableLedgers);
205
+ const lifecycle = await readStatusLifecycle(cwd);
206
+ const stableRoot = await stableRepoRoot(cwd);
207
+ const terminalById = new Map(lifecycle.contracts.map((model) => [
208
+ model.contract,
209
+ model.phase === "claimed" || model.phase === "forfeited",
210
+ ]));
211
+ const queueModel = deriveQueueReadModel(lifecycle);
230
212
  const boardQueueLength = queueModel.targetQueues.reduce((sum, queue) => sum + queue.ordered.length, 0);
231
- const baseDrifts = await evaluateDistinctBaseDrifts(cwd, readableLedgers.flatMap((item) => {
232
- const state = deriveContractState(item.entries);
233
- return isTerminalState(state) ? [] : (() => {
234
- const base = latestBase(item.entries);
235
- return base ? [base] : [];
236
- })();
213
+ const baseDrifts = await evaluateDistinctBaseDrifts(cwd, lifecycle.contracts.flatMap((model) => {
214
+ const outcome = lookupCurrentContract(lifecycle, model.contract);
215
+ return outcome.status === "healthy" && !terminalById.get(model.contract) ? [model.pins.base] : [];
237
216
  }), defaultBranch);
238
217
  const contracts = [];
239
218
  const commissionPlaces = new Map();
240
- // Readable-ledger commission targets retained for post-assembly filter only.
219
+ // Folded target pins are retained only for post-assembly filtering.
241
220
  const derivedTargetById = new Map();
242
- for (const item of ledgers) {
243
- if ("error" in item) {
244
- // Unreadable ledgers have no derivable target and stay under every filter.
245
- derivedTargetById.set(item.id, undefined);
246
- contracts.push({
247
- id: item.id,
248
- slug: contractSlugFromId(item.id),
249
- title: contractSlugFromId(item.id),
250
- state: { state: "corrupt", at: 0, reason: item.error.message },
251
- queueHeadStalled: false,
252
- arcs: 0,
253
- currentArc: { number: 0, objective: "", state: "closed" },
254
- nextStep: { kind: "inspect-log", detail: "inspect log" },
255
- baseDrift: { state: "unknown", label: "n/a (corrupt ledger)" },
256
- after: [],
257
- corruptReason: item.error.message,
258
- bootstrapDiagnostics: [],
259
- queueDiagnostics: [],
260
- });
221
+ for (const model of lifecycle.contracts) {
222
+ const outcome = lookupCurrentContract(lifecycle, model.contract);
223
+ const diseaseReason = lifecycleDiseaseReason(lifecycle, model.contract);
224
+ const state = outcome.status === "healthy"
225
+ ? currentLifecycleState(model)
226
+ : { state: "corrupt", at: 0, reason: diseaseReason || "isolated lifecycle disease" };
227
+ commissionPlaces.set(model.contract, model.resources.worktree?.place ?? null);
228
+ derivedTargetById.set(model.contract, model.pins.target.ref);
229
+ const base = model.pins.base;
230
+ // Terminal lifecycle facts retire their carrier and worktree projections.
231
+ // Do not interpret a different active workspace-here carrier as terminal
232
+ // replay debt.
233
+ if (model.phase === "claimed" || model.phase === "forfeited")
261
234
  continue;
262
- }
263
- const bind = findBindEntry(item.entries);
264
- commissionPlaces.set(item.contractId, bind?.data.place ?? null);
265
- derivedTargetById.set(item.contractId, commissionTarget(item.entries)?.ref);
266
- const base = latestBase(item.entries);
267
- const state = deriveContractState(item.entries);
268
- if (isTerminalState(state))
269
- continue;
270
- const after = (bind?.data.after ?? []).map((id) => ({ id, terminal: terminalById.get(id) ?? false }));
271
- const currentArc = deriveCurrentArc(item.entries, bind?.data.objective, state);
235
+ const after = model.causal.after.map((id) => ({ id, terminal: terminalById.get(id) ?? false }));
236
+ const currentArc = deriveCurrentArc(model);
272
237
  const baseDrift = base
273
238
  ? baseDrifts.get(base) ?? { state: "unknown", label: "n/a (base drift unavailable)" }
274
239
  : { state: "unknown", label: "n/a (no default branch)" };
275
240
  const nextStep = deriveNextStep({
276
- id: item.contractId,
241
+ id: model.contract,
277
242
  state,
278
243
  currentArc,
279
244
  baseDrift,
280
245
  after,
281
246
  });
282
247
  if (nextStep.kind === "renew" && baseDrift.state === "drift") {
283
- const openArc = findOpenArc(item.entries);
284
- let receiptCount = 0;
285
- if (openArc) {
286
- try {
287
- receiptCount = (await receiptsAfterFence(cwd, item.entries, `keiyaku/${item.contractId}`)).length;
288
- }
289
- catch {
290
- // A ledger-only here contract may not have a delivery ref yet.
291
- }
292
- }
293
- const sealForecast = openArc ? `seals a${openArc.data.arc} (${receiptCount} receipts), ` : "";
294
- nextStep.hint = `run: keiyaku @${item.contractId} renew — ${sealForecast}follows ${baseDrift.defaultBranch} ▲${baseDrift.behindBy}`;
248
+ nextStep.hint = `run: keiyaku @${model.contract} renew — follows ${baseDrift.defaultBranch} ▲${baseDrift.behindBy}`;
295
249
  }
296
- const membership = queueMembership(queueModel, item.contractId);
250
+ const membership = queueMembership(queueModel, model.contract);
297
251
  const queuePosition = membership?.position ?? null;
298
252
  const targetQueueLength = membership?.queueLength ?? 0;
253
+ const replayBlockers = bootstrapReplayBlockers(model);
254
+ const carrierRoot = model.resources.worktree === null
255
+ ? stableRoot
256
+ : await findRegisteredContractWorktreePathFromModel(stableRoot, model);
257
+ if (model.resources.worktree !== null && carrierRoot === undefined) {
258
+ replayBlockers.unshift(replayBlockingFact(model.contract, "worktree"));
259
+ }
260
+ else if (carrierRoot !== undefined) {
261
+ let carrierIdentity = null;
262
+ try {
263
+ carrierIdentity = parseCarrierIdentity(await fs.readFile(path.join(carrierRoot, KEIYAKU_FILE), "utf8"));
264
+ }
265
+ catch {
266
+ carrierIdentity = null;
267
+ }
268
+ if (carrierIdentity?.contractId !== model.contract || carrierIdentity.ledgerHead !== model.observedLedgerHead) {
269
+ const reason = carrierIdentity === null
270
+ ? "missing-or-invalid"
271
+ : carrierIdentity.contractId !== model.contract
272
+ ? "contract-id-mismatch"
273
+ : "ledger-head-mismatch";
274
+ replayBlockers.push(replayBlockingFact(model.contract, "carrier", {
275
+ reason,
276
+ expected: { contractId: model.contract, ledgerHead: model.observedLedgerHead },
277
+ observed: carrierIdentity,
278
+ }));
279
+ }
280
+ }
299
281
  contracts.push({
300
- id: item.contractId,
301
- slug: contractSlugFromId(item.contractId),
302
- title: bind?.data.name ?? contractSlugFromId(item.contractId),
282
+ id: model.contract,
283
+ slug: contractSlugFromId(model.contract),
284
+ title: bindTitle(model),
303
285
  state,
304
286
  queuePosition,
305
287
  queueHeadStalled: isQueueHeadStall({
306
- entries: item.entries,
307
288
  queuePosition,
308
289
  queueLength: targetQueueLength,
309
290
  state,
291
+ hasVerdict: model.petition.state === "approved",
310
292
  }),
311
- arcs: getMaxArc(item.entries),
293
+ arcs: model.intent.arc,
312
294
  currentArc,
313
295
  nextStep,
314
- ...deriveLastActiveAge(item.entries, now),
315
- place: bind?.data.place ?? null,
296
+ ...deriveLastActiveAge(model, now),
297
+ place: model.resources.worktree?.place ?? null,
316
298
  base,
317
299
  baseDrift,
318
300
  after,
319
- bootstrapDiagnostics: deriveBootstrapDiagnostics(item.entries),
301
+ lifecycleDiagnostics: outcome.diagnostics,
302
+ replayBlockers,
320
303
  queueDiagnostics: membership ? [...membership.diagnostics] : [],
321
304
  });
322
305
  }
306
+ for (const contract of lifecycle.isolatedContracts) {
307
+ if (lifecycle.contracts.some((model) => model.contract === contract))
308
+ continue;
309
+ const reason = lifecycleDiseaseReason(lifecycle, contract) || "isolated lifecycle disease";
310
+ derivedTargetById.set(contract, undefined);
311
+ contracts.push({
312
+ id: contract, slug: contractSlugFromId(contract), title: contractSlugFromId(contract),
313
+ state: { state: "corrupt", at: 0, reason }, queueHeadStalled: false, arcs: 0,
314
+ currentArc: { number: 0, objective: "", state: "closed" }, nextStep: { kind: "inspect-log", detail: "inspect log" },
315
+ baseDrift: { state: "unknown", label: "n/a (corrupt ledger)" }, after: [], corruptReason: reason,
316
+ lifecycleDiagnostics: lifecycle.diagnostics.filter((diagnostic) => diagnostic.contract === contract),
317
+ replayBlockers: [], queueDiagnostics: [],
318
+ });
319
+ }
323
320
  const filteredContracts = targetFilter === null
324
321
  ? contracts
325
322
  : contracts.filter((contract) => {
@@ -329,7 +326,6 @@ export async function readKanshiBoard(cwd, now, opts) {
329
326
  const queueLength = targetFilter === null
330
327
  ? boardQueueLength
331
328
  : (queueModel.targetQueues.find((queue) => queue.target === targetFilter)?.ordered.length ?? 0);
332
- const claimReconciliation = await computeClaimReconciliation(cwd, defaultBranch);
333
329
  const exactProjection = opts?.projectionId === undefined
334
330
  ? undefined
335
331
  : await readExactProjectionStatus(cwd, now, opts.projectionId);
@@ -344,7 +340,6 @@ export async function readKanshiBoard(cwd, now, opts) {
344
340
  defaultBranch: defaultBranch?.branch,
345
341
  defaultHead: defaultBranch?.head,
346
342
  diseases: loadedSettings.diseases,
347
- claimReconciliation,
348
343
  targetFilter,
349
344
  ...(tasks ? { tasks } : {}),
350
345
  };
@@ -1,84 +1,11 @@
1
- function transitionFromEntry(current, entry) {
2
- if (current === "claimed" || current === "forfeited" || current === "corrupt") {
3
- return "corrupt";
4
- }
5
- switch (entry.kind) {
6
- case "bind":
7
- return "corrupt";
8
- case "bootstrap":
9
- // Placement legality is owned exclusively by deriveContractState.
10
- return "corrupt";
11
- case "arc":
12
- return current === "bound" || current === "active" ? "active" : "corrupt";
13
- case "seal":
14
- if (current === "active")
15
- return "active";
16
- // Derived default arc a0 may seal from bound (bind/bootstrap → seal0 → petition).
17
- if (current === "bound" && entry.data.arc === 0)
18
- return "active";
19
- return "corrupt";
20
- case "amend":
21
- return current === "petitioned" ? "active" : current;
22
- case "renew":
23
- return current;
24
- case "verdict":
25
- if (current !== "petitioned")
26
- return "corrupt";
27
- return entry.data.result === "rejected" ? "active" : "petitioned";
28
- case "petition":
29
- return current === "bound" || current === "active" ? "petitioned" : "corrupt";
30
- case "claim":
31
- return current === "petitioned" ? "claimed" : "corrupt";
32
- case "forfeit":
33
- return "forfeited";
34
- }
35
- return "corrupt";
36
- }
37
- export function deriveContractState(entries) {
38
- if (entries.length === 0 || entries[0]?.kind !== "bind") {
39
- return { state: "corrupt", at: 0, reason: "ledger must start with bind" };
40
- }
41
- const bind = entries[0];
42
- let state = "bound";
43
- for (let index = 1; index < entries.length; index += 1) {
44
- const entry = entries[index];
45
- if (entry.kind === "bootstrap") {
46
- if (index !== 1 || state !== "bound") {
47
- return { state: "corrupt", at: index + 1, reason: "bootstrap must immediately follow bind" };
48
- }
49
- if (!bind.data.bootstrap) {
50
- return { state: "corrupt", at: index + 1, reason: "bootstrap without bind plan" };
51
- }
52
- // Legal bootstrap is a durable fact and leaves the derived state bound.
53
- continue;
54
- }
55
- const next = transitionFromEntry(state, entry);
56
- if (next === "corrupt") {
57
- return { state: "corrupt", at: index + 1, reason: `invalid ${entry.kind} after ${state}` };
58
- }
59
- state = next;
60
- }
61
- return { state };
62
- }
63
- export function deriveBootstrapDiagnostics(entries) {
64
- const bind = entries.find((entry) => entry.kind === "bind");
65
- if (!bind?.data.bootstrap)
66
- return [];
67
- if (entries.some((entry) => entry.kind === "bootstrap"))
68
- return [];
69
- return [{ kind: "plan-without-result" }];
70
- }
71
1
  export function isTerminalState(state) {
72
2
  return state.state === "claimed" || state.state === "forfeited";
73
3
  }
74
- /** Current-petition verdict predicate used by board queue-head stall logic. */
75
- export function hasCurrentPetitionVerdict(entries) {
76
- for (let index = entries.length - 1; index >= 0; index -= 1) {
77
- const entry = entries[index];
78
- if (entry.kind === "verdict")
79
- return true;
80
- if (entry.kind === "petition")
81
- return false;
82
- }
83
- return false;
4
+ export function replayBlockingFact(contract, surface, carrierMismatch) {
5
+ return {
6
+ kind: "incomplete-replay",
7
+ surface,
8
+ recoveryCommand: `keiyaku bind --contract ${contract}`,
9
+ ...(carrierMismatch ? { carrierMismatch } : {}),
10
+ };
84
11
  }
@@ -0,0 +1,23 @@
1
+ export const STRUCTURED_QUERY_GRAMMAR = "FIELD OP VALUE[,VALUE...] [| FIELD OP VALUE[,VALUE...]]...";
2
+ export const STRUCTURED_QUERY_COMPLETION = "FIELD OP VALUE[,VALUE...]|FIELD OP VALUE[,VALUE...]";
3
+ export const STRUCTURED_QUERY_OPERATORS = ["=", "!=", "~", "<", "<=", ">", ">="];
4
+ export function parseStructuredQuery(input) {
5
+ const parsed = [];
6
+ for (const rawTerm of input.expression.split("|")) {
7
+ if (!rawTerm)
8
+ input.invalid(`'${input.expression}' must use ${STRUCTURED_QUERY_GRAMMAR}`, parsed);
9
+ const match = /^([a-z][a-z0-9_-]*)(!=|<=|>=|~|=|<|>)(.*)$/.exec(rawTerm);
10
+ if (!match)
11
+ input.invalid(`'${input.expression}' must use ${STRUCTURED_QUERY_GRAMMAR}`, parsed);
12
+ const field = match[1];
13
+ const operator = match[2];
14
+ if (!input.fields.has(field))
15
+ input.invalid(`unknown field '${field}'`, parsed);
16
+ const values = match[3].split(",");
17
+ if (values.some((value) => !value))
18
+ input.invalid(`field '${field}' has an empty value`, parsed);
19
+ input.validate(field, operator, values, parsed);
20
+ parsed.push({ field, operator, values });
21
+ }
22
+ return parsed;
23
+ }
@@ -1,5 +1,5 @@
1
1
  import { FlowError } from "../flow-error.js";
2
- import { fullBranchRefSchema } from "./entry.js";
2
+ import { fullBranchRefSchema } from "./repository-ledger/codec.js";
3
3
  /** Prefix bare branch names and validate the full ref. Sole status/bind normalizer. */
4
4
  export function normalizeTargetRef(raw) {
5
5
  const ref = raw.startsWith("refs/heads/") ? raw : `refs/heads/${raw}`;
@@ -1,5 +1,5 @@
1
1
  import { FlowError } from "../../flow-error.js";
2
- import { isTaskId } from "./validation.js";
2
+ import { formatTaskCoordinate, parseTaskCoordinate } from "./coordinate.js";
3
3
  function compareText(left, right) {
4
4
  return left < right ? -1 : left > right ? 1 : 0;
5
5
  }
@@ -14,7 +14,16 @@ function taskSort(left, right) {
14
14
  return leftTerminal - rightTerminal
15
15
  || left.pri - right.pri
16
16
  || compareText(left.createdAt, right.createdAt)
17
- || compareText(left.id, right.id);
17
+ || compareText(left.coordinate ?? left.id, right.coordinate ?? right.id);
18
+ }
19
+ function normalizeDocument(task) {
20
+ const namespace = task.namespace ?? null;
21
+ const coordinate = task.coordinate ?? formatTaskCoordinate(namespace, task.id);
22
+ const parsed = parseTaskCoordinate(coordinate);
23
+ if (parsed.namespace !== namespace || parsed.localId !== task.id) {
24
+ throw new FlowError("INVALID_TARGET", `task coordinate ${coordinate} does not match its namespace and local id`);
25
+ }
26
+ return { ...task, namespace, coordinate };
18
27
  }
19
28
  function cycleComponents(tasks) {
20
29
  let nextIndex = 0;
@@ -59,6 +68,9 @@ function cycleComponents(tasks) {
59
68
  visit(id);
60
69
  return cycles;
61
70
  }
71
+ function supersedesCycleComponents(tasks) {
72
+ return cycleComponents(new Map([...tasks].map(([id, task]) => [id, { ...task, needs: task.supersedes }])));
73
+ }
62
74
  function parentCycleComponents(tasks) {
63
75
  const cycles = [];
64
76
  const visited = new Set();
@@ -85,21 +97,23 @@ function parentCycleComponents(tasks) {
85
97
  }
86
98
  /** Derive the complete storage-neutral board from domain records. */
87
99
  export function buildTaskBoardFromDocuments(documents) {
100
+ documents = documents.map(normalizeDocument);
88
101
  const diseases = [];
89
102
  const counts = new Map();
90
103
  for (const task of documents)
91
- counts.set(task.id, (counts.get(task.id) ?? 0) + 1);
104
+ counts.set(task.coordinate, (counts.get(task.coordinate) ?? 0) + 1);
92
105
  const candidates = new Map();
93
106
  for (const task of documents) {
94
- if (counts.get(task.id) > 1) {
107
+ const coordinate = task.coordinate;
108
+ if (counts.get(coordinate) > 1) {
95
109
  diseases.push({
96
110
  code: "duplicate_id",
97
- message: `duplicate authoritative task id ${task.id}`,
98
- id: task.id,
111
+ message: `duplicate authoritative task coordinate ${coordinate}`,
112
+ id: coordinate,
99
113
  });
100
114
  }
101
115
  else {
102
- candidates.set(task.id, task);
116
+ candidates.set(coordinate, task);
103
117
  }
104
118
  }
105
119
  for (const component of cycleComponents(candidates)) {
@@ -114,6 +128,18 @@ export function buildTaskBoardFromDocuments(documents) {
114
128
  candidates.delete(id);
115
129
  }
116
130
  }
131
+ for (const component of supersedesCycleComponents(candidates)) {
132
+ const relatedIds = [...component].sort();
133
+ for (const id of relatedIds) {
134
+ diseases.push({
135
+ code: "supersedes_cycle",
136
+ message: `task ${id} participates in a supersedes cycle`,
137
+ id,
138
+ relatedIds,
139
+ });
140
+ candidates.delete(id);
141
+ }
142
+ }
117
143
  for (const component of parentCycleComponents(candidates)) {
118
144
  const relatedIds = [...component].sort();
119
145
  for (const id of relatedIds) {
@@ -129,7 +155,7 @@ export function buildTaskBoardFromDocuments(documents) {
129
155
  while (changed) {
130
156
  changed = false;
131
157
  for (const [id, task] of [...candidates].sort(([a], [b]) => compareText(a, b))) {
132
- const missing = [...task.needs, ...(task.parent ? [task.parent] : []), ...task.from]
158
+ const missing = [...task.needs, ...(task.parent ? [task.parent] : []), ...task.from, ...task.supersedes, ...task.relatesTo]
133
159
  .filter((related) => !candidates.has(related));
134
160
  if (missing.length === 0)
135
161
  continue;
@@ -145,65 +171,74 @@ export function buildTaskBoardFromDocuments(documents) {
145
171
  }
146
172
  }
147
173
  const blocks = new Map();
174
+ const supersededBy = new Map();
175
+ const relatedTasks = new Map();
148
176
  for (const task of candidates.values()) {
149
177
  for (const need of task.needs)
150
- blocks.set(need, [...(blocks.get(need) ?? []), task.id]);
178
+ blocks.set(need, [...(blocks.get(need) ?? []), task.coordinate]);
179
+ for (const superseded of task.supersedes)
180
+ supersededBy.set(superseded, [...(supersededBy.get(superseded) ?? []), task.coordinate]);
181
+ for (const related of task.relatesTo)
182
+ relatedTasks.set(related, [...(relatedTasks.get(related) ?? []), task.coordinate]);
151
183
  }
152
184
  const tasks = [...candidates.values()].sort(taskSort).map((task) => {
153
185
  const blockingNeeds = task.needs.filter((id) => {
154
186
  const state = candidates.get(id).state;
155
- return state === "open" || state === "in_progress";
187
+ return state === "open" || state === "in_progress" || state === "on_hold";
156
188
  });
157
189
  return {
158
190
  id: task.id,
191
+ namespace: task.namespace ?? null,
192
+ coordinate: task.coordinate,
159
193
  title: task.title,
160
194
  state: task.state,
161
195
  pri: task.pri,
162
196
  needs: [...task.needs],
163
197
  parent: task.parent,
164
198
  from: [...task.from],
199
+ supersedes: [...task.supersedes],
200
+ relatesTo: [...task.relatesTo],
165
201
  createdAt: task.createdAt,
166
202
  ...(task.updatedAt === undefined ? {} : { updatedAt: task.updatedAt }),
167
203
  creator: task.creator,
168
204
  ...(task.startedAt === undefined ? {} : { startedAt: task.startedAt }),
169
205
  ...(task.contractId === undefined ? {} : { contractId: task.contractId }),
170
206
  body: task.body,
171
- blocks: [...(blocks.get(task.id) ?? [])].sort(),
207
+ blocks: [...(blocks.get(task.coordinate) ?? [])].sort(),
172
208
  blocked: task.state === "open" && blockingNeeds.length > 0,
173
209
  releasedNeeds: task.needs.filter((id) => candidates.get(id).state === "drop").sort(),
210
+ supersededBy: [...(supersededBy.get(task.coordinate) ?? [])].sort(),
211
+ relatedTasks: [...new Set([...task.relatesTo, ...(relatedTasks.get(task.coordinate) ?? [])])].sort(),
174
212
  };
175
213
  });
176
214
  return {
177
215
  tasks,
178
216
  diseases: diseases.sort(diseaseSort),
179
- byId: new Map(tasks.map((task) => [task.id, task])),
217
+ byId: new Map(tasks.map((task) => [task.coordinate, task])),
180
218
  };
181
219
  }
182
220
  export function resolveTaskReadId(board, input) {
183
221
  return resolveTaskId(board, input);
184
222
  }
185
223
  export function resolveTaskId(board, input) {
186
- if (!isTaskId(input)) {
187
- throw new FlowError("INVALID_TARGET", `invalid task id '${input}'`);
188
- }
189
- if (board.byId.has(input))
190
- return input;
224
+ const requested = parseTaskCoordinate(input);
225
+ if (board.byId.has(requested.coordinate))
226
+ return requested.coordinate;
191
227
  const matches = board.tasks
192
- .map((task) => task.id)
193
- .filter((id) => id.startsWith(input))
228
+ .filter((task) => task.namespace === requested.namespace && task.id.startsWith(requested.localId))
229
+ .map((task) => task.coordinate)
194
230
  .sort();
195
231
  if (matches.length === 0)
196
232
  throw new FlowError("INVALID_TARGET", `task '${input}' not found`);
197
233
  if (matches.length > 1) {
198
- throw Object.assign(new FlowError("CONTRACT_AMBIGUOUS", `task prefix '${input}' is ambiguous: ${matches.join(", ")}`), { candidates: matches });
234
+ throw Object.assign(new FlowError("TASK_AMBIGUOUS", `task prefix '${input}' is ambiguous: ${matches.join(", ")}`, {
235
+ facts: { kind: "task_ambiguous", candidates: matches },
236
+ }), { candidates: matches });
199
237
  }
200
238
  return matches[0];
201
239
  }
202
240
  export function requireCanonicalTaskId(input) {
203
- if (!isTaskId(input)) {
204
- throw new FlowError("INVALID_TARGET", "task mutations require a canonical full task id");
205
- }
206
- return input;
241
+ return parseTaskCoordinate(input, "task mutation coordinate").coordinate;
207
242
  }
208
243
  export function requireHealthy(board) {
209
244
  if (board.diseases.length > 0) {