@astrosheep/keiyaku 2.9.8 → 2.9.10

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 +50 -182
  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 +10 -7
  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 +242 -79
  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 +65 -29
  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 +13 -35
  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 +45 -69
  87. package/build/cli/subagent-guard.js +1 -1
  88. package/build/cli/types.js +1 -1
  89. package/build/config/env-keys.js +3 -1
  90. package/build/config/env.js +24 -1
  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 +13 -6
  129. package/build/core/projection/index.js +5 -4
  130. package/build/core/projection/projection-activity.js +29 -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 +35 -337
  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 +125 -127
  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,13 +29,12 @@ 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;
29
- const topTasks = [
30
- ...eligible.filter((task) => task.state === "in_progress").sort(compareTaskStatusRows),
31
- ...eligible.filter((task) => task.state === "open" && !task.blocked).sort(compareTaskStatusRows),
32
- ]
33
- .slice(0, 5)
34
+ const onHold = eligible.filter((task) => task.state === "on_hold").length;
35
+ const topTasks = eligible
36
+ .filter((task) => task.state === "in_progress" || task.state === "on_hold")
37
+ .sort(compareTaskStatusRows)
34
38
  .map((task) => {
35
39
  const updatedAt = taskUpdatedAt(task);
36
40
  return {
@@ -47,14 +51,13 @@ export function deriveTaskStatusSummary(board, now) {
47
51
  });
48
52
  const ready = eligible.filter((task) => task.state === "open" && !task.blocked).length;
49
53
  const blocked = eligible.filter((task) => task.state === "open" && task.blocked).length;
50
- if (board.tasks.length === 0 && board.diseases.length === 0)
51
- return undefined;
52
54
  return {
53
- total: board.tasks.length,
55
+ total: eligible.length,
54
56
  inProgress,
57
+ onHold,
55
58
  ready,
56
59
  blocked,
57
- omitted: inProgress + ready - topTasks.length,
60
+ omitted: ready + blocked,
58
61
  topTasks,
59
62
  diseases: board.diseases,
60
63
  };
@@ -68,6 +71,7 @@ async function readTaskStatusSummary(cwd, now) {
68
71
  return {
69
72
  total: 0,
70
73
  inProgress: 0,
74
+ onHold: 0,
71
75
  ready: 0,
72
76
  blocked: 0,
73
77
  omitted: 0,
@@ -81,10 +85,7 @@ function isQueueHeadStall(input) {
81
85
  return (input.queuePosition === 1 &&
82
86
  input.queueLength > 1 &&
83
87
  input.state.state === "petitioned" &&
84
- !hasCurrentPetitionVerdict(input.entries));
85
- }
86
- function findBindEntry(entries) {
87
- return entries.find((entry) => entry.kind === "bind");
88
+ !input.hasVerdict);
88
89
  }
89
90
  function contractSlugFromId(id) {
90
91
  const dot = id.indexOf(".");
@@ -92,44 +93,25 @@ function contractSlugFromId(id) {
92
93
  return id.slice(0, dot);
93
94
  return id;
94
95
  }
95
- function deriveCurrentArc(entries, bindObjective, lifecycle) {
96
- // No explicit arc has ever existed: derive a0 from bind objective.
97
- if (getMaxArc(entries) === 0) {
98
- const a0Open = lifecycle.state === "bound" || lifecycle.state === "active";
99
- return {
100
- number: 0,
101
- objective: bindObjective ?? "",
102
- state: a0Open ? "open" : "closed",
103
- };
104
- }
105
- const arcObjectives = new Map();
106
- let currentArc = null;
107
- for (const entry of entries) {
108
- if (entry.kind === "arc") {
109
- arcObjectives.set(entry.data.arc, entry.data.objective);
110
- currentArc = {
111
- number: entry.data.arc,
112
- objective: entry.data.objective,
113
- state: "open",
114
- };
115
- continue;
116
- }
117
- if (entry.kind === "seal" && entry.data.arc > 0) {
118
- currentArc = {
119
- number: entry.data.arc,
120
- objective: arcObjectives.get(entry.data.arc) ?? bindObjective ?? "",
121
- state: "closed",
122
- };
123
- }
124
- }
125
- if (currentArc)
126
- return currentArc;
96
+ function deriveCurrentArc(model) {
127
97
  return {
128
- number: 0,
129
- objective: bindObjective ?? "",
130
- state: "closed",
98
+ number: model.intent.arc,
99
+ objective: model.intent.objective,
100
+ state: model.phase === "active" ? "open" : "closed",
131
101
  };
132
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
+ }
133
115
  function deriveNextStep(input) {
134
116
  if (input.state.state === "corrupt") {
135
117
  return { kind: "inspect-log", detail: "inspect log" };
@@ -156,8 +138,8 @@ function deriveNextStep(input) {
156
138
  }
157
139
  return { kind: "arc", detail: "arc" };
158
140
  }
159
- function deriveLastActiveAge(entries, now) {
160
- const last = entries.at(-1);
141
+ function deriveLastActiveAge(model, now) {
142
+ const last = model.history.at(-1)?.entry;
161
143
  if (!last)
162
144
  return {};
163
145
  const atMs = Date.parse(last.at);
@@ -199,14 +181,16 @@ export async function readKanshiBoard(cwd, now, opts) {
199
181
  : await readExactProjectionStatus(cwd, now, opts.projectionId);
200
182
  return {
201
183
  contracts: [],
184
+ contractsObservable: false,
185
+ commissionPlaces: new Map(),
202
186
  projections: opts?.projectionId === undefined
203
187
  ? await readOperationalProjectionStatusBoard(cwd, now)
204
188
  : exactProjection ? { rows: [exactProjection] } : null,
205
189
  queueLength: 0,
206
190
  diseases: loadedSettings.diseases,
207
- claimReconciliation: null,
208
191
  targetFilter,
209
- ...(tasks ? { tasks } : {}),
192
+ // Task and contract boards are repository organs; a non-repository cwd
193
+ // cannot truthfully turn their absence into a zero count.
210
194
  };
211
195
  }
212
196
  // Status is a pure reader: use the same resolver, but retain its ability to
@@ -218,107 +202,121 @@ export async function readKanshiBoard(cwd, now, opts) {
218
202
  catch {
219
203
  defaultBranch = null;
220
204
  }
221
- const terminalById = new Map();
222
- const ledgers = await readStatusLedgers(cwd);
223
- const readableLedgers = [];
224
- for (const ledger of ledgers) {
225
- if ("error" in ledger)
226
- continue;
227
- readableLedgers.push(ledger);
228
- terminalById.set(ledger.contractId, isTerminalState(deriveContractState(ledger.entries)));
229
- }
230
- 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);
231
212
  const boardQueueLength = queueModel.targetQueues.reduce((sum, queue) => sum + queue.ordered.length, 0);
232
- const baseDrifts = await evaluateDistinctBaseDrifts(cwd, readableLedgers.flatMap((item) => {
233
- const state = deriveContractState(item.entries);
234
- return isTerminalState(state) ? [] : (() => {
235
- const base = latestBase(item.entries);
236
- return base ? [base] : [];
237
- })();
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] : [];
238
216
  }), defaultBranch);
239
217
  const contracts = [];
240
- // Readable-ledger commission targets retained for post-assembly filter only.
218
+ const commissionPlaces = new Map();
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
- derivedTargetById.set(item.contractId, commissionTarget(item.entries)?.ref);
265
- const base = latestBase(item.entries);
266
- const state = deriveContractState(item.entries);
267
- if (isTerminalState(state))
268
- continue;
269
- const after = (bind?.data.after ?? []).map((id) => ({ id, terminal: terminalById.get(id) ?? false }));
270
- 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);
271
237
  const baseDrift = base
272
238
  ? baseDrifts.get(base) ?? { state: "unknown", label: "n/a (base drift unavailable)" }
273
239
  : { state: "unknown", label: "n/a (no default branch)" };
274
240
  const nextStep = deriveNextStep({
275
- id: item.contractId,
241
+ id: model.contract,
276
242
  state,
277
243
  currentArc,
278
244
  baseDrift,
279
245
  after,
280
246
  });
281
247
  if (nextStep.kind === "renew" && baseDrift.state === "drift") {
282
- const openArc = findOpenArc(item.entries);
283
- let receiptCount = 0;
284
- if (openArc) {
285
- try {
286
- receiptCount = (await receiptsAfterFence(cwd, item.entries, `keiyaku/${item.contractId}`)).length;
287
- }
288
- catch {
289
- // A ledger-only here contract may not have a delivery ref yet.
290
- }
291
- }
292
- const sealForecast = openArc ? `seals a${openArc.data.arc} (${receiptCount} receipts), ` : "";
293
- 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}`;
294
249
  }
295
- const membership = queueMembership(queueModel, item.contractId);
250
+ const membership = queueMembership(queueModel, model.contract);
296
251
  const queuePosition = membership?.position ?? null;
297
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
+ }
298
281
  contracts.push({
299
- id: item.contractId,
300
- slug: contractSlugFromId(item.contractId),
301
- title: bind?.data.name ?? contractSlugFromId(item.contractId),
282
+ id: model.contract,
283
+ slug: contractSlugFromId(model.contract),
284
+ title: bindTitle(model),
302
285
  state,
303
286
  queuePosition,
304
287
  queueHeadStalled: isQueueHeadStall({
305
- entries: item.entries,
306
288
  queuePosition,
307
289
  queueLength: targetQueueLength,
308
290
  state,
291
+ hasVerdict: model.petition.state === "approved",
309
292
  }),
310
- arcs: getMaxArc(item.entries),
293
+ arcs: model.intent.arc,
311
294
  currentArc,
312
295
  nextStep,
313
- ...deriveLastActiveAge(item.entries, now),
314
- place: bind?.data.place ?? null,
296
+ ...deriveLastActiveAge(model, now),
297
+ place: model.resources.worktree?.place ?? null,
315
298
  base,
316
299
  baseDrift,
317
300
  after,
318
- bootstrapDiagnostics: deriveBootstrapDiagnostics(item.entries),
301
+ lifecycleDiagnostics: outcome.diagnostics,
302
+ replayBlockers,
319
303
  queueDiagnostics: membership ? [...membership.diagnostics] : [],
320
304
  });
321
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
+ }
322
320
  const filteredContracts = targetFilter === null
323
321
  ? contracts
324
322
  : contracts.filter((contract) => {
@@ -328,12 +326,13 @@ export async function readKanshiBoard(cwd, now, opts) {
328
326
  const queueLength = targetFilter === null
329
327
  ? boardQueueLength
330
328
  : (queueModel.targetQueues.find((queue) => queue.target === targetFilter)?.ordered.length ?? 0);
331
- const claimReconciliation = await computeClaimReconciliation(cwd, defaultBranch);
332
329
  const exactProjection = opts?.projectionId === undefined
333
330
  ? undefined
334
331
  : await readExactProjectionStatus(cwd, now, opts.projectionId);
335
332
  return {
336
333
  contracts: filteredContracts,
334
+ contractsObservable: true,
335
+ commissionPlaces,
337
336
  projections: opts?.projectionId === undefined
338
337
  ? await readOperationalProjectionStatusBoard(cwd, now)
339
338
  : exactProjection ? { rows: [exactProjection] } : null,
@@ -341,7 +340,6 @@ export async function readKanshiBoard(cwd, now, opts) {
341
340
  defaultBranch: defaultBranch?.branch,
342
341
  defaultHead: defaultBranch?.head,
343
342
  diseases: loadedSettings.diseases,
344
- claimReconciliation,
345
343
  targetFilter,
346
344
  ...(tasks ? { tasks } : {}),
347
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}`;