@astrosheep/keiyaku 2.8.4 → 2.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (229) hide show
  1. package/README.md +35 -15
  2. package/build/.tsbuildinfo +1 -1
  3. package/build/agents/{call-terms_v2.js → call-terms.js} +12 -8
  4. package/build/agents/harness/control-types.js +1 -0
  5. package/build/agents/harness/execution-handle.js +23 -3
  6. package/build/agents/harness/index.js +6 -607
  7. package/build/agents/harness/outcome.js +141 -0
  8. package/build/agents/harness/projection.js +310 -0
  9. package/build/agents/harness/router.js +201 -0
  10. package/build/agents/harness/runtime.js +2 -1
  11. package/build/agents/{launch-snapshot_v2.js → launch-snapshot/model.js} +69 -74
  12. package/build/agents/launch-snapshot/resolve.js +72 -0
  13. package/build/agents/opencode-sdk.js +24 -3
  14. package/build/agents/providers/{claude-agent-sdk.js → claude-agent-sdk/adapter.js} +46 -368
  15. package/build/agents/providers/claude-agent-sdk/events.js +218 -0
  16. package/build/agents/providers/claude-agent-sdk/session.js +158 -0
  17. package/build/agents/providers/codex-app-server/adapter.js +347 -0
  18. package/build/agents/providers/codex-app-server/events.js +352 -0
  19. package/build/agents/providers/codex-app-server/session.js +266 -0
  20. package/build/agents/providers/codex-sdk.js +23 -5
  21. package/build/agents/providers/opencode-sdk/adapter.js +344 -0
  22. package/build/agents/providers/opencode-sdk/events.js +354 -0
  23. package/build/agents/providers/opencode-sdk/session.js +180 -0
  24. package/build/agents/providers/pi/adapter.js +269 -0
  25. package/build/agents/providers/pi/checkpoint.js +41 -0
  26. package/build/agents/providers/pi/events.js +282 -0
  27. package/build/agents/{selector_v2.js → selector.js} +8 -6
  28. package/build/cli/commands/akuma/akuma/handler.js +4 -0
  29. package/build/cli/commands/akuma/akuma/meta.js +16 -0
  30. package/build/cli/commands/akuma/catalog.js +8 -0
  31. package/build/cli/commands/akuma/list/handler.js +15 -0
  32. package/build/cli/commands/akuma/list/meta-ls.js +9 -0
  33. package/build/cli/commands/akuma/view/handler.js +34 -0
  34. package/build/cli/commands/akuma/view/meta.js +8 -0
  35. package/build/cli/commands/akuma.js +1 -1
  36. package/build/cli/commands/contract/amend/handler.js +10 -0
  37. package/build/cli/commands/contract/amend/meta.js +12 -0
  38. package/build/cli/commands/contract/arc/handler.js +9 -0
  39. package/build/cli/commands/contract/arc/meta.js +26 -0
  40. package/build/cli/commands/contract/bind/handler.js +23 -0
  41. package/build/cli/commands/contract/bind/meta.js +30 -0
  42. package/build/cli/commands/contract/catalog.js +16 -0
  43. package/build/cli/commands/contract/forfeit/handler.js +13 -0
  44. package/build/cli/commands/contract/forfeit/meta.js +12 -0
  45. package/build/cli/commands/contract/log/handler.js +12 -0
  46. package/build/cli/commands/contract/log/meta.js +12 -0
  47. package/build/cli/commands/contract/petition/handler.js +26 -0
  48. package/build/cli/commands/contract/petition/meta.js +23 -0
  49. package/build/cli/commands/contract/renew/handler.js +10 -0
  50. package/build/cli/commands/contract/renew/meta.js +17 -0
  51. package/build/cli/commands/projection/call/handler.js +39 -0
  52. package/build/cli/commands/projection/call/meta.js +9 -0
  53. package/build/cli/commands/projection/catalog.js +14 -0
  54. package/build/cli/commands/projection/kill/handler.js +35 -0
  55. package/build/cli/commands/projection/kill/meta.js +11 -0
  56. package/build/cli/commands/projection/revive/handler.js +53 -0
  57. package/build/cli/commands/projection/revive/meta.js +9 -0
  58. package/build/cli/commands/projection/status/handler.js +23 -0
  59. package/build/cli/commands/projection/status/meta.js +9 -0
  60. package/build/cli/commands/projection/tell/handler.js +34 -0
  61. package/build/cli/commands/projection/tell/meta.js +9 -0
  62. package/build/cli/commands/projection/wait/handler.js +21 -0
  63. package/build/cli/commands/projection/wait/meta.js +11 -0
  64. package/build/cli/commands/registry.js +100 -0
  65. package/build/cli/commands/shared.js +71 -0
  66. package/build/cli/commands/skills/catalog.js +6 -0
  67. package/build/cli/commands/skills/install/handler.js +10 -0
  68. package/build/cli/commands/skills/install/meta.js +9 -0
  69. package/build/cli/commands/skills/skills/handler.js +4 -0
  70. package/build/cli/commands/skills/skills/meta.js +10 -0
  71. package/build/cli/commands/system/catalog.js +8 -0
  72. package/build/cli/commands/system/completion/handler.js +10 -0
  73. package/build/cli/commands/system/completion/meta.js +9 -0
  74. package/build/cli/commands/system/dump-env/handler.js +3 -0
  75. package/build/cli/commands/system/dump-env/meta.js +10 -0
  76. package/build/cli/commands/system/guide/handler.js +4 -0
  77. package/build/cli/commands/system/guide/meta.js +9 -0
  78. package/build/cli/commands/task/add/handler.js +21 -0
  79. package/build/cli/commands/task/add/meta.js +8 -0
  80. package/build/cli/commands/task/catalog.js +22 -0
  81. package/build/cli/commands/task/doctor/handler.js +4 -0
  82. package/build/cli/commands/task/doctor/meta.js +8 -0
  83. package/build/cli/commands/task/done/handler.js +17 -0
  84. package/build/cli/commands/task/done/meta.js +8 -0
  85. package/build/cli/commands/task/drop/handler.js +6 -0
  86. package/build/cli/commands/task/drop/meta.js +8 -0
  87. package/build/cli/commands/task/ls/handler.js +13 -0
  88. package/build/cli/commands/task/ls/meta.js +8 -0
  89. package/build/cli/commands/task/shared.js +131 -0
  90. package/build/cli/commands/task/start/handler.js +6 -0
  91. package/build/cli/commands/task/start/meta.js +8 -0
  92. package/build/cli/commands/task/stop/handler.js +17 -0
  93. package/build/cli/commands/task/stop/meta.js +8 -0
  94. package/build/cli/commands/task/task/handler.js +5 -0
  95. package/build/cli/commands/task/task/meta.js +11 -0
  96. package/build/cli/commands/task/update/handler.js +18 -0
  97. package/build/cli/commands/task/update/meta.js +8 -0
  98. package/build/cli/commands/task/view/handler.js +14 -0
  99. package/build/cli/commands/task/view/meta.js +8 -0
  100. package/build/cli/completion.js +26 -27
  101. package/build/cli/flags.js +79 -2
  102. package/build/cli/help.js +27 -49
  103. package/build/cli/index.js +121 -374
  104. package/build/cli/output-stream-error-policy.js +17 -0
  105. package/build/cli/parse.js +159 -72
  106. package/build/cli/projection-address.js +5 -0
  107. package/build/cli/render/arc.js +1 -1
  108. package/build/cli/render/errors.js +4 -3
  109. package/build/cli/render/format.js +14 -0
  110. package/build/cli/render/kanshi.js +106 -0
  111. package/build/cli/render/petition.js +24 -1
  112. package/build/cli/render/projection-activity.js +5 -1
  113. package/build/cli/render/shared.js +2 -2
  114. package/build/cli/render/status.js +79 -13
  115. package/build/cli/render/task.js +113 -0
  116. package/build/cli/render/wait.js +23 -3
  117. package/build/cli/types.js +1 -1
  118. package/build/config/{akuma-loader_v2.js → akuma-loader.js} +2 -2
  119. package/build/config/env.js +9 -4
  120. package/build/config/settings/disease.js +26 -0
  121. package/build/config/settings/knobs.js +8 -0
  122. package/build/config/settings/loader.js +244 -0
  123. package/build/config/settings/schema.js +276 -0
  124. package/build/core/addressing.js +1 -1
  125. package/build/core/amend.js +3 -3
  126. package/build/core/arc.js +5 -4
  127. package/build/core/bind-workspace.js +123 -0
  128. package/build/core/bind.js +75 -155
  129. package/build/core/call/call.js +224 -0
  130. package/build/core/call/context.js +238 -0
  131. package/build/core/call/execution.js +217 -0
  132. package/build/core/call/prompt.js +42 -0
  133. package/build/core/call/reference.js +24 -0
  134. package/build/core/call-persist.js +1 -1
  135. package/build/core/claim-delivery.js +247 -0
  136. package/build/core/claim.js +39 -277
  137. package/build/core/context.js +2 -1
  138. package/build/core/{render.js → contract-view.js} +12 -140
  139. package/build/core/draft.js +107 -9
  140. package/build/core/entry.js +9 -1
  141. package/build/core/execution-coordinate.js +4 -20
  142. package/build/core/execution-pact.js +16 -31
  143. package/build/core/forfeit.js +35 -22
  144. package/build/core/hints.js +2 -2
  145. package/build/core/log.js +2 -2
  146. package/build/core/petition-claim-gates.js +8 -6
  147. package/build/core/petition-preview.js +97 -0
  148. package/build/core/petition-run.js +3 -2
  149. package/build/core/petition.js +39 -26
  150. package/build/core/projection/generation/database.js +306 -0
  151. package/build/core/projection/generation/identity.js +84 -0
  152. package/build/core/projection/generation/model.js +201 -0
  153. package/build/core/projection/generation/store.js +73 -0
  154. package/build/core/projection/generation/transitions.js +258 -0
  155. package/build/core/projection/heart.js +0 -307
  156. package/build/core/projection/tell/database.js +109 -0
  157. package/build/core/projection/tell/model.js +97 -0
  158. package/build/core/{projection-tells.js → projection/tell/store.js} +3 -158
  159. package/build/core/projection-activity.js +23 -2
  160. package/build/core/projection-core.js +2 -1
  161. package/build/core/projection-execution-observer.js +259 -0
  162. package/build/core/projection-generation-continuation.js +1 -1
  163. package/build/core/projection-generation-execution.js +90 -20
  164. package/build/core/projection-generation-launcher.js +1 -1
  165. package/build/core/projection-generation-runner.js +198 -4
  166. package/build/core/projection-identity.js +1 -1
  167. package/build/core/projection-kill.js +50 -17
  168. package/build/core/projection-life-observer.js +1 -1
  169. package/build/core/projection-life-protocol.js +10 -0
  170. package/build/core/projection-mint.js +1 -1
  171. package/build/core/projection-status.js +45 -32
  172. package/build/core/projection-wait.js +12 -2
  173. package/build/core/projection-wake.js +5 -2
  174. package/build/core/{queue_v2.js → queue.js} +2 -1
  175. package/build/core/registry.js +1 -1
  176. package/build/core/renew.js +7 -7
  177. package/build/core/response-artifact-catalog-sqlite.js +112 -0
  178. package/build/core/response-artifact-catalog.js +7 -0
  179. package/build/core/seal.js +44 -16
  180. package/build/core/status/board.js +286 -0
  181. package/build/core/status/drift.js +99 -0
  182. package/build/core/status/lifecycle.js +84 -0
  183. package/build/core/status/reconciliation.js +131 -0
  184. package/build/core/stored-agent-event.js +28 -4
  185. package/build/core/target-ref.js +10 -0
  186. package/build/core/task/board.js +175 -0
  187. package/build/core/task/commands.js +211 -0
  188. package/build/core/task/document.js +189 -0
  189. package/build/core/task/model.js +1 -0
  190. package/build/core/task/settlement-git.js +207 -0
  191. package/build/core/task/settlement-policy.js +82 -0
  192. package/build/core/task/source-board.js +84 -0
  193. package/build/core/task/source-model.js +1 -0
  194. package/build/core/task/validation.js +44 -0
  195. package/build/core/task-contract.js +224 -0
  196. package/build/core/task-doctor.js +14 -0
  197. package/build/core/task-git-actor.js +38 -0
  198. package/build/core/task-git-runtime.js +34 -0
  199. package/build/core/task-git-store.js +375 -0
  200. package/build/core/task-repository.js +1 -0
  201. package/build/core/task-store-repository.js +86 -0
  202. package/build/core/task-store.js +1 -0
  203. package/build/core/task-worktree.js +51 -0
  204. package/build/core/task.js +4 -0
  205. package/build/core/transcripts.js +138 -56
  206. package/build/core/validation.js +17 -0
  207. package/build/core/verdict.js +2 -2
  208. package/build/core/worktree-path.js +24 -6
  209. package/build/flow-error.js +3 -2
  210. package/build/generated/version.js +1 -1
  211. package/build/git/commits.js +3 -0
  212. package/build/git/diff/parsers.js +0 -94
  213. package/build/index.js +2 -0
  214. package/build/keiyaku-constants.js +2 -0
  215. package/build/keiyaku.js +4 -3
  216. package/package.json +3 -2
  217. package/skills/keiyaku/SKILL.md +1 -1
  218. package/skills/keiyaku-akuma/SKILL.md +11 -6
  219. package/build/agents/harness/pump.js +0 -158
  220. package/build/agents/providers/codex-app-server.js +0 -864
  221. package/build/agents/providers/opencode-sdk.js +0 -363
  222. package/build/agents/providers/pi.js +0 -557
  223. package/build/config/settings.js +0 -533
  224. package/build/core/call.js +0 -686
  225. package/build/core/projection/generation.js +0 -412
  226. package/build/core/projection/identity.js +0 -81
  227. package/build/core/projection/mint.js +0 -174
  228. package/build/core/projection-generation-store.js +0 -707
  229. package/build/core/status.js +0 -539
@@ -5,9 +5,10 @@ import { createGit, errorContainsAnyPattern, MISSING_HEAD_PATTERNS, wrapGitError
5
5
  import { KEIYAKU_FILE } from "../keiyaku.js";
6
6
  import { activeLedgerRef, appendWithRetry, readLedger } from "./ledger.js";
7
7
  import { readProjectionStatusBoard } from "./projection-status.js";
8
- import { renderContractView } from "./render.js";
9
- import { captureSeal, findOpenArc, receiptsAfterFence, sealOpenArc } from "./seal.js";
10
- import { commissionTarget, deriveContractState, isTerminalState, latestBase } from "./status.js";
8
+ import { renderContractView } from "./contract-view.js";
9
+ import { captureSeal, findOpenArc, getMaxArc, receiptsAfterFence, sealCurrentArc } from "./seal.js";
10
+ import { deriveContractState, isTerminalState } from "./status/lifecycle.js";
11
+ import { commissionTarget, latestBase } from "./status/drift.js";
11
12
  import { findContractWorktreePath } from "./worktree-path.js";
12
13
  import { getCurrentBranch, isGitRepo } from "../git/branches.js";
13
14
  import { resolveContractAddress, withResolvedAddress } from "./addressing.js";
@@ -101,9 +102,8 @@ export async function renewContract(input) {
101
102
  throw new FlowError("INTERNAL_STATE", "ledger must contain bind entry");
102
103
  const branch = `keiyaku/${input.contractId}`;
103
104
  const oldHead = await resolveCommit(input.cwd, branch);
104
- const open = findOpenArc(ledger.entries);
105
- const capture = open ? await captureSeal(input.cwd, ledger.entries, oldHead, input.waivePaths) : null;
106
- if (!open) {
105
+ const capture = await captureSeal(input.cwd, ledger.entries, oldHead, input.waivePaths);
106
+ if (getMaxArc(ledger.entries) > 0 && !findOpenArc(ledger.entries)) {
107
107
  const loose = await receiptsAfterFence(input.cwd, ledger.entries, oldHead);
108
108
  if (loose.length > 0)
109
109
  throw looseCommitError(input.contractId, loose);
@@ -125,7 +125,7 @@ export async function renewContract(input) {
125
125
  assertNoInFlightProjection(rewriteCwd, input.contractId, input.nowMs);
126
126
  await rewriteBranch(rewriteCwd, targetHead, oldBase, worktreePath ? undefined : branch);
127
127
  const newHead = await resolveCommit(input.cwd, branch);
128
- const sealed = await sealOpenArc({
128
+ const sealed = await sealCurrentArc({
129
129
  cwd: input.cwd,
130
130
  renderCwd: rewriteCwd,
131
131
  contractId: input.contractId,
@@ -0,0 +1,112 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { DatabaseSync } from "node:sqlite";
4
+ const CATALOG_FILE = "catalog.db";
5
+ const CREATE_CATALOG_SQL = `
6
+ CREATE TABLE IF NOT EXISTS response_artifacts (
7
+ id TEXT PRIMARY KEY,
8
+ stable_root TEXT NOT NULL,
9
+ registered_at TEXT NOT NULL,
10
+ last_seen_at TEXT NOT NULL
11
+ ) STRICT
12
+ `;
13
+ function recordFromRow(row) {
14
+ return {
15
+ id: row.id,
16
+ stableRoot: row.stable_root,
17
+ registeredAt: row.registered_at,
18
+ lastSeenAt: row.last_seen_at,
19
+ };
20
+ }
21
+ export function createSqliteResponseArtifactCatalog(input) {
22
+ const databasePath = path.join(path.resolve(input.keiyakuHome), CATALOG_FILE);
23
+ const timestamp = () => (input.now?.() ?? new Date()).toISOString();
24
+ const withWritableDatabase = (operation) => {
25
+ fs.mkdirSync(path.dirname(databasePath), { recursive: true });
26
+ const database = new DatabaseSync(databasePath);
27
+ try {
28
+ database.exec("PRAGMA busy_timeout = 5000");
29
+ database.exec(CREATE_CATALOG_SQL);
30
+ return operation(database);
31
+ }
32
+ finally {
33
+ database.close();
34
+ }
35
+ };
36
+ const withExistingDatabase = (fallback, operation) => {
37
+ if (!fs.existsSync(databasePath))
38
+ return fallback;
39
+ const database = new DatabaseSync(databasePath, { readOnly: true });
40
+ try {
41
+ database.exec("PRAGMA busy_timeout = 5000");
42
+ return operation(database);
43
+ }
44
+ catch (error) {
45
+ if (error instanceof Error && /no such table/i.test(error.message))
46
+ return fallback;
47
+ throw error;
48
+ }
49
+ finally {
50
+ database.close();
51
+ }
52
+ };
53
+ const upsert = (coordinate, touchedAt) => {
54
+ withWritableDatabase((database) => {
55
+ database.prepare(`
56
+ INSERT INTO response_artifacts (id, stable_root, registered_at, last_seen_at)
57
+ VALUES (?, ?, ?, ?)
58
+ ON CONFLICT(id) DO UPDATE SET
59
+ registered_at = CASE
60
+ WHEN response_artifacts.stable_root <> excluded.stable_root THEN excluded.registered_at
61
+ ELSE response_artifacts.registered_at
62
+ END,
63
+ stable_root = excluded.stable_root,
64
+ last_seen_at = excluded.last_seen_at
65
+ `).run(coordinate.id, path.resolve(coordinate.stableRoot), touchedAt, touchedAt);
66
+ });
67
+ };
68
+ const records = () => withExistingDatabase([], (database) => {
69
+ const rows = database.prepare(`
70
+ SELECT id, stable_root, registered_at, last_seen_at
71
+ FROM response_artifacts
72
+ ORDER BY id
73
+ `).all();
74
+ return rows.map(recordFromRow);
75
+ });
76
+ return {
77
+ register(coordinate) {
78
+ upsert(coordinate, timestamp());
79
+ },
80
+ touch(coordinate) {
81
+ upsert(coordinate, timestamp());
82
+ },
83
+ lookup(id) {
84
+ return withExistingDatabase(undefined, (database) => {
85
+ const row = database.prepare(`
86
+ SELECT id, stable_root, registered_at, last_seen_at
87
+ FROM response_artifacts
88
+ WHERE id = ?
89
+ `).get(id);
90
+ return row ? recordFromRow(row) : undefined;
91
+ });
92
+ },
93
+ removeDead(coordinate) {
94
+ if (!fs.existsSync(databasePath))
95
+ return;
96
+ withWritableDatabase((database) => {
97
+ database.prepare(`
98
+ DELETE FROM response_artifacts
99
+ WHERE id = ? AND stable_root = ?
100
+ `).run(coordinate.id, path.resolve(coordinate.stableRoot));
101
+ });
102
+ },
103
+ async diagnoseDead(exists) {
104
+ const result = [];
105
+ for (const record of records()) {
106
+ if (!(await exists(record)))
107
+ result.push(record);
108
+ }
109
+ return result;
110
+ },
111
+ };
112
+ }
@@ -0,0 +1,7 @@
1
+ let activeCatalog;
2
+ export function configureResponseArtifactCatalog(catalog) {
3
+ activeCatalog = catalog;
4
+ }
5
+ export function getResponseArtifactCatalog() {
6
+ return activeCatalog;
7
+ }
@@ -4,9 +4,9 @@ import { FlowError } from "../flow-error.js";
4
4
  import { createGit, wrapGitError } from "../git/core.js";
5
5
  import { KEIYAKU_FILE } from "../keiyaku.js";
6
6
  import { activeLedgerRef, appendWithRetry, readLedger } from "./ledger.js";
7
- import { renderContractView } from "./render.js";
7
+ import { renderContractView } from "./contract-view.js";
8
8
  import { normalizeScopePath, pathsMatchingScope } from "./scope.js";
9
- import { deriveContractState, isTerminalState } from "./status.js";
9
+ import { deriveContractState, isTerminalState } from "./status/lifecycle.js";
10
10
  function findBind(entries) {
11
11
  const bind = entries.find((entry) => entry.kind === "bind");
12
12
  if (!bind)
@@ -116,10 +116,28 @@ function assertOperable(contractId, entries) {
116
116
  throw new FlowError("INTERNAL_STATE", `contract ${contractId} cannot seal from state ${state.state}`);
117
117
  }
118
118
  }
119
- export async function captureSeal(cwd, entries, head = "HEAD", waivePaths = []) {
119
+ /** Current seal intent: open explicit arc, else derived a0 while no explicit arc has ever existed. */
120
+ export function currentSealArc(entries) {
120
121
  const open = findOpenArc(entries);
121
- if (!open)
122
- return null;
122
+ if (open)
123
+ return open.data.arc;
124
+ if (getMaxArc(entries) === 0)
125
+ return 0;
126
+ return null;
127
+ }
128
+ /** Stable read-only coordinate naming the receipt source a petition would seal. */
129
+ export function petitionSealSource(entries) {
130
+ const arc = currentSealArc(entries);
131
+ const fence = getFence(entries);
132
+ return arc === null
133
+ ? `no open arc · fence ${fence}`
134
+ : `a${arc} · fence ${fence}▸HEAD`;
135
+ }
136
+ /** Shared read-only owner for the seal capture and its compact changed-path evidence. */
137
+ export async function previewSeal(cwd, entries, head = "HEAD", waivePaths = []) {
138
+ const arc = currentSealArc(entries);
139
+ if (arc === null)
140
+ return { capture: null, paths: [] };
123
141
  const oldFence = getFence(entries);
124
142
  const newFence = await resolveCommit(cwd, head);
125
143
  const [receipts, paths, stat] = await Promise.all([
@@ -132,17 +150,23 @@ export async function captureSeal(cwd, entries, head = "HEAD", waivePaths = [])
132
150
  const waivedPaths = new Set(waivePaths.map(normalizeScopePath));
133
151
  const unwaived = violations.filter((violation) => !waivedPaths.has(violation));
134
152
  if (unwaived.length > 0) {
135
- throw new FlowError("INTERNAL_STATE", `scope audit failed for contract ${open.contract}: paths ${unwaived.join(", ")} outside ${scope.join(", ")}`);
153
+ throw new FlowError("INTERNAL_STATE", `scope audit failed for contract ${findBind(entries).contract}: paths ${unwaived.join(", ")} outside ${scope.join(", ")}`);
136
154
  }
137
155
  return {
138
- arc: open.data.arc,
139
- oldFence,
140
- newFence,
141
- receipts,
142
- diffstat: stat,
143
- scopeAudit: { violations, waived: violations.length > 0 },
156
+ capture: {
157
+ arc,
158
+ oldFence,
159
+ newFence,
160
+ receipts,
161
+ diffstat: stat,
162
+ scopeAudit: { violations, waived: violations.length > 0 },
163
+ },
164
+ paths,
144
165
  };
145
166
  }
167
+ export async function captureSeal(cwd, entries, head = "HEAD", waivePaths = []) {
168
+ return (await previewSeal(cwd, entries, head, waivePaths)).capture;
169
+ }
146
170
  async function renderUpdatedContract(ledgerCwd, renderCwd, contractId) {
147
171
  const ledger = await readLedger(ledgerCwd, contractId);
148
172
  if (!ledger) {
@@ -150,7 +174,7 @@ async function renderUpdatedContract(ledgerCwd, renderCwd, contractId) {
150
174
  }
151
175
  await fs.writeFile(path.join(renderCwd, KEIYAKU_FILE), renderContractView({ id: contractId, entries: ledger.entries }), "utf8");
152
176
  }
153
- export async function sealOpenArc(input) {
177
+ export async function sealCurrentArc(input) {
154
178
  const ledger = await readLedger(input.cwd, input.contractId);
155
179
  if (!ledger) {
156
180
  throw new FlowError("INTERNAL_STATE", `contract ${input.contractId} not found or not active`);
@@ -162,10 +186,14 @@ export async function sealOpenArc(input) {
162
186
  const at = new Date(input.nowMs).toISOString();
163
187
  const result = await appendWithRetry(input.cwd, activeLedgerRef(input.contractId), (_head, entries) => {
164
188
  assertOperable(input.contractId, entries);
165
- const open = findOpenArc(entries);
166
- if (!open)
189
+ const currentArc = currentSealArc(entries);
190
+ if (currentArc === null)
167
191
  return null;
168
- if (open.data.arc !== capture.arc || getFence(entries) !== capture.oldFence) {
192
+ // a0 is legal only while no explicit arc has ever existed; otherwise treat as concurrent change.
193
+ if (capture.arc === 0 && getMaxArc(entries) > 0) {
194
+ throw new FlowError("INTERNAL_STATE", `contract ${input.contractId} changed while sealing arc a${capture.arc}`);
195
+ }
196
+ if (currentArc !== capture.arc || getFence(entries) !== capture.oldFence) {
169
197
  throw new FlowError("INTERNAL_STATE", `contract ${input.contractId} changed while sealing arc a${capture.arc}`);
170
198
  }
171
199
  return {
@@ -0,0 +1,286 @@
1
+ import { loadKeiyakuSettings } from "../../config/settings/loader.js";
2
+ import { isGitRepo } from "../../git/branches.js";
3
+ import { listContractIds, readLedger } from "../ledger.js";
4
+ import { readProjectionStatusBoard } from "../projection-status.js";
5
+ import { deriveQueueReadModel, queueMembership, } from "../queue.js";
6
+ import { findOpenArc, getMaxArc, receiptsAfterFence } from "../seal.js";
7
+ import { readTaskBoard } from "../task.js";
8
+ import { openTaskStore } from "../task-git-store.js";
9
+ import { taskRepositoryFromStore } from "../task-store-repository.js";
10
+ import { deriveBootstrapDiagnostics, deriveContractState, hasCurrentPetitionVerdict, isTerminalState, } from "./lifecycle.js";
11
+ import { commissionTarget, evaluateBaseDrift, latestBase, resolveDefaultBranchFromSettings, } from "./drift.js";
12
+ import { computeClaimReconciliation, } from "./reconciliation.js";
13
+ function compareTaskStatusRows(left, right) {
14
+ if (left.pri !== right.pri)
15
+ return left.pri - right.pri;
16
+ if (left.createdAt !== right.createdAt)
17
+ return left.createdAt < right.createdAt ? -1 : 1;
18
+ return left.id < right.id ? -1 : left.id > right.id ? 1 : 0;
19
+ }
20
+ async function readTaskStatusSummary(cwd) {
21
+ try {
22
+ const board = await readTaskBoard(taskRepositoryFromStore(await openTaskStore(cwd)));
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(({ id, title, pri, createdAt, state, blocked }) => ({ id, title, pri, createdAt, state, blocked }));
31
+ const ready = eligible.filter((task) => task.state === "open" && !task.blocked).length;
32
+ const blocked = eligible.filter((task) => task.state === "open" && task.blocked).length;
33
+ if (board.tasks.length === 0 && board.diseases.length === 0)
34
+ return undefined;
35
+ return { total: board.tasks.length, inProgress, ready, blocked, topTasks, diseases: board.diseases };
36
+ }
37
+ catch (error) {
38
+ return {
39
+ total: 0,
40
+ inProgress: 0,
41
+ ready: 0,
42
+ blocked: 0,
43
+ topTasks: [],
44
+ diseases: [],
45
+ diagnostic: error instanceof Error ? error.message : String(error),
46
+ };
47
+ }
48
+ }
49
+ function isQueueHeadStall(input) {
50
+ return (input.queuePosition === 1 &&
51
+ input.queueLength > 1 &&
52
+ input.state.state === "petitioned" &&
53
+ !hasCurrentPetitionVerdict(input.entries));
54
+ }
55
+ function findBindEntry(entries) {
56
+ return entries.find((entry) => entry.kind === "bind");
57
+ }
58
+ function contractSlugFromId(id) {
59
+ const dot = id.indexOf(".");
60
+ if (dot > 0)
61
+ return id.slice(0, dot);
62
+ return id;
63
+ }
64
+ function deriveCurrentArc(entries, bindObjective, lifecycle) {
65
+ // No explicit arc has ever existed: derive a0 from bind objective.
66
+ if (getMaxArc(entries) === 0) {
67
+ const a0Open = lifecycle.state === "bound" || lifecycle.state === "active";
68
+ return {
69
+ number: 0,
70
+ objective: bindObjective ?? "",
71
+ state: a0Open ? "open" : "closed",
72
+ };
73
+ }
74
+ const arcObjectives = new Map();
75
+ let currentArc = null;
76
+ for (const entry of entries) {
77
+ if (entry.kind === "arc") {
78
+ arcObjectives.set(entry.data.arc, entry.data.objective);
79
+ currentArc = {
80
+ number: entry.data.arc,
81
+ objective: entry.data.objective,
82
+ state: "open",
83
+ };
84
+ continue;
85
+ }
86
+ if (entry.kind === "seal" && entry.data.arc > 0) {
87
+ currentArc = {
88
+ number: entry.data.arc,
89
+ objective: arcObjectives.get(entry.data.arc) ?? bindObjective ?? "",
90
+ state: "closed",
91
+ };
92
+ }
93
+ }
94
+ if (currentArc)
95
+ return currentArc;
96
+ return {
97
+ number: 0,
98
+ objective: bindObjective ?? "",
99
+ state: "closed",
100
+ };
101
+ }
102
+ function deriveNextStep(input) {
103
+ if (input.state.state === "corrupt") {
104
+ return { kind: "inspect-log", detail: "inspect log" };
105
+ }
106
+ const pendingAfter = input.after.filter((item) => !item.terminal).map((item) => contractSlugFromId(item.id));
107
+ if (pendingAfter.length > 0) {
108
+ return { kind: "wait-after", detail: `finish after ${pendingAfter.join(",")}` };
109
+ }
110
+ if (input.state.state === "petitioned") {
111
+ return { kind: "await-claim", detail: "await claim" };
112
+ }
113
+ if (input.baseDrift.state === "drift" || input.baseDrift.state === "orphaned") {
114
+ return { kind: "renew", detail: "renew" };
115
+ }
116
+ // Derived a0 (bound/active, no explicit arc) petitions directly; explicit open arcs still suggest arc work.
117
+ if (input.currentArc.state === "open" && input.currentArc.number === 0) {
118
+ return { kind: "petition", detail: "petition" };
119
+ }
120
+ if (input.currentArc.state === "open") {
121
+ return { kind: "arc", detail: "arc" };
122
+ }
123
+ if (input.state.state === "active" && input.currentArc.number > 0) {
124
+ return { kind: "petition", detail: "petition" };
125
+ }
126
+ return { kind: "arc", detail: "arc" };
127
+ }
128
+ function deriveLastActiveAge(entries, now) {
129
+ const last = entries.at(-1);
130
+ if (!last)
131
+ return {};
132
+ const atMs = Date.parse(last.at);
133
+ if (!Number.isFinite(atMs)) {
134
+ return { lastActivityAt: last.at };
135
+ }
136
+ return {
137
+ lastActivityAt: last.at,
138
+ lastActiveMs: Math.max(0, now - atMs),
139
+ };
140
+ }
141
+ export async function readKanshiBoard(cwd, now, opts) {
142
+ const targetFilter = opts?.target ?? null;
143
+ const loadedSettings = await loadKeiyakuSettings(cwd);
144
+ const tasks = await readTaskStatusSummary(cwd);
145
+ if (!(await isGitRepo(cwd))) {
146
+ return {
147
+ contracts: [],
148
+ projections: null,
149
+ queueLength: 0,
150
+ diseases: loadedSettings.diseases,
151
+ claimReconciliation: null,
152
+ targetFilter,
153
+ ...(tasks ? { tasks } : {}),
154
+ };
155
+ }
156
+ // Status is a pure reader: use the same resolver, but retain its ability to
157
+ // open when the defaultBranch knob itself is diseased.
158
+ let defaultBranch = null;
159
+ try {
160
+ defaultBranch = await resolveDefaultBranchFromSettings(cwd, loadedSettings, false);
161
+ }
162
+ catch {
163
+ defaultBranch = null;
164
+ }
165
+ const contractIds = await listContractIds(cwd);
166
+ const terminalById = new Map();
167
+ const ledgers = [];
168
+ const readableLedgers = [];
169
+ for (const id of contractIds) {
170
+ try {
171
+ const ledger = await readLedger(cwd, id);
172
+ if (ledger) {
173
+ ledgers.push(ledger);
174
+ readableLedgers.push(ledger);
175
+ terminalById.set(id, isTerminalState(deriveContractState(ledger.entries)));
176
+ }
177
+ }
178
+ catch (error) {
179
+ ledgers.push({ id, error: error instanceof Error ? error : new Error(String(error)) });
180
+ }
181
+ }
182
+ const queueModel = deriveQueueReadModel(readableLedgers);
183
+ const boardQueueLength = queueModel.targetQueues.reduce((sum, queue) => sum + queue.ordered.length, 0);
184
+ const contracts = [];
185
+ // Readable-ledger commission targets retained for post-assembly filter only.
186
+ const derivedTargetById = new Map();
187
+ for (const item of ledgers) {
188
+ if ("error" in item) {
189
+ // Unreadable ledgers have no derivable target and stay under every filter.
190
+ derivedTargetById.set(item.id, undefined);
191
+ contracts.push({
192
+ id: item.id,
193
+ slug: contractSlugFromId(item.id),
194
+ state: { state: "corrupt", at: 0, reason: item.error.message },
195
+ queueHeadStalled: false,
196
+ arcs: 0,
197
+ currentArc: { number: 0, objective: "", state: "closed" },
198
+ nextStep: { kind: "inspect-log", detail: "inspect log" },
199
+ baseDrift: { state: "unknown", label: "n/a (corrupt ledger)" },
200
+ after: [],
201
+ corruptReason: item.error.message,
202
+ bootstrapDiagnostics: [],
203
+ queueDiagnostics: [],
204
+ });
205
+ continue;
206
+ }
207
+ const bind = findBindEntry(item.entries);
208
+ derivedTargetById.set(item.contractId, commissionTarget(item.entries)?.ref);
209
+ const base = latestBase(item.entries);
210
+ const state = deriveContractState(item.entries);
211
+ if (isTerminalState(state))
212
+ continue;
213
+ const after = (bind?.data.after ?? []).map((id) => ({ id, terminal: terminalById.get(id) ?? false }));
214
+ const currentArc = deriveCurrentArc(item.entries, bind?.data.objective, state);
215
+ const baseDrift = base
216
+ ? await evaluateBaseDrift(cwd, base, defaultBranch)
217
+ : { state: "unknown", label: "n/a (no default branch)" };
218
+ const nextStep = deriveNextStep({
219
+ id: item.contractId,
220
+ state,
221
+ currentArc,
222
+ baseDrift,
223
+ after,
224
+ });
225
+ if (nextStep.kind === "renew" && baseDrift.state === "drift") {
226
+ const openArc = findOpenArc(item.entries);
227
+ let receiptCount = 0;
228
+ if (openArc) {
229
+ try {
230
+ receiptCount = (await receiptsAfterFence(cwd, item.entries, `keiyaku/${item.contractId}`)).length;
231
+ }
232
+ catch {
233
+ // A ledger-only here contract may not have a delivery ref yet.
234
+ }
235
+ }
236
+ const sealForecast = openArc ? `seals a${openArc.data.arc} (${receiptCount} receipts), ` : "";
237
+ nextStep.hint = `run: keiyaku @${item.contractId} renew — ${sealForecast}follows ${baseDrift.defaultBranch} ▲${baseDrift.behindBy}`;
238
+ }
239
+ const membership = queueMembership(queueModel, item.contractId);
240
+ const queuePosition = membership?.position ?? null;
241
+ const targetQueueLength = membership?.queueLength ?? 0;
242
+ contracts.push({
243
+ id: item.contractId,
244
+ slug: contractSlugFromId(item.contractId),
245
+ state,
246
+ queuePosition,
247
+ queueHeadStalled: isQueueHeadStall({
248
+ entries: item.entries,
249
+ queuePosition,
250
+ queueLength: targetQueueLength,
251
+ state,
252
+ }),
253
+ arcs: getMaxArc(item.entries),
254
+ currentArc,
255
+ nextStep,
256
+ ...deriveLastActiveAge(item.entries, now),
257
+ place: bind?.data.place ?? null,
258
+ base,
259
+ baseDrift,
260
+ after,
261
+ bootstrapDiagnostics: deriveBootstrapDiagnostics(item.entries),
262
+ queueDiagnostics: membership ? [...membership.diagnostics] : [],
263
+ });
264
+ }
265
+ const filteredContracts = targetFilter === null
266
+ ? contracts
267
+ : contracts.filter((contract) => {
268
+ const derived = derivedTargetById.get(contract.id);
269
+ return derived === undefined || derived === targetFilter;
270
+ });
271
+ const queueLength = targetFilter === null
272
+ ? boardQueueLength
273
+ : (queueModel.targetQueues.find((queue) => queue.target === targetFilter)?.ordered.length ?? 0);
274
+ const claimReconciliation = await computeClaimReconciliation(cwd, defaultBranch);
275
+ return {
276
+ contracts: filteredContracts,
277
+ projections: readProjectionStatusBoard(cwd, now, { showAll: opts?.showAllProjections }),
278
+ queueLength,
279
+ defaultBranch: defaultBranch?.branch,
280
+ defaultHead: defaultBranch?.head,
281
+ diseases: loadedSettings.diseases,
282
+ claimReconciliation,
283
+ targetFilter,
284
+ ...(tasks ? { tasks } : {}),
285
+ };
286
+ }
@@ -0,0 +1,99 @@
1
+ import { assertSettingsKnobUsable } from "../../config/settings/disease.js";
2
+ import { loadKeiyakuSettings } from "../../config/settings/loader.js";
3
+ import { FlowError } from "../../flow-error.js";
4
+ import { SETTINGS_FILE } from "../../keiyaku.js";
5
+ import { createGit, errorContainsAnyPattern, MISSING_HEAD_PATTERNS, wrapGitError } from "../../git/core.js";
6
+ function findBindEntry(entries) {
7
+ return entries.find((entry) => entry.kind === "bind");
8
+ }
9
+ export function latestBase(entries) {
10
+ for (let index = entries.length - 1; index >= 0; index -= 1) {
11
+ const entry = entries[index];
12
+ if (entry.kind === "renew")
13
+ return entry.data.newBase;
14
+ }
15
+ return entries.find((entry) => entry.kind === "bind")?.data.base;
16
+ }
17
+ /** Commission settlement target from the bind entry. Schema guarantees refs/heads/<name>. */
18
+ export function commissionTarget(entries) {
19
+ const bind = findBindEntry(entries);
20
+ if (!bind)
21
+ return undefined;
22
+ const ref = bind.data.target;
23
+ return {
24
+ ref,
25
+ branch: ref.slice("refs/heads/".length),
26
+ };
27
+ }
28
+ export async function resolveCommit(cwd, ref) {
29
+ const git = createGit(cwd);
30
+ try {
31
+ return (await git.raw(["rev-parse", "--verify", `${ref}^{commit}`])).trim();
32
+ }
33
+ catch (error) {
34
+ if (errorContainsAnyPattern(error, MISSING_HEAD_PATTERNS))
35
+ return null;
36
+ throw wrapGitError(`rev-parse ${ref}`, error, cwd);
37
+ }
38
+ }
39
+ export async function readSettingsDiseases(cwd) {
40
+ const loaded = await loadKeiyakuSettings(cwd);
41
+ return loaded.diseases;
42
+ }
43
+ export async function readDefaultBranchOverride(cwd) {
44
+ const loaded = await loadKeiyakuSettings(cwd);
45
+ return loaded.knobs?.defaultBranch;
46
+ }
47
+ export async function resolveDefaultBranchFromSettings(cwd, loaded, rejectDiseasedOverride) {
48
+ if (rejectDiseasedOverride)
49
+ assertSettingsKnobUsable(loaded, "defaultBranch");
50
+ const override = loaded.knobs?.defaultBranch;
51
+ if (override) {
52
+ const head = await resolveCommit(cwd, override);
53
+ if (!head)
54
+ throw new FlowError("INVALID_SETTINGS", `${SETTINGS_FILE} defaultBranch '${override}' does not resolve`);
55
+ return { branch: override, head };
56
+ }
57
+ const git = createGit(cwd);
58
+ for (const branch of ["main", "master"]) {
59
+ const head = await resolveCommit(cwd, branch);
60
+ if (head)
61
+ return { branch, head };
62
+ }
63
+ try {
64
+ const originHead = (await git.raw(["symbolic-ref", "--quiet", "--short", "refs/remotes/origin/HEAD"])).trim();
65
+ if (originHead) {
66
+ const head = await resolveCommit(cwd, originHead);
67
+ if (head)
68
+ return { branch: originHead, head };
69
+ }
70
+ }
71
+ catch {
72
+ // Fall through to local branch heuristics.
73
+ }
74
+ return null;
75
+ }
76
+ export async function resolveDefaultBranch(cwd) {
77
+ return resolveDefaultBranchFromSettings(cwd, await loadKeiyakuSettings(cwd), true);
78
+ }
79
+ export async function evaluateBaseDrift(cwd, base, defaultBranch) {
80
+ if (!defaultBranch)
81
+ return { state: "unknown", label: "n/a (no default branch)" };
82
+ if (base === defaultBranch.head) {
83
+ return { state: "green", defaultBranch: defaultBranch.branch, defaultHead: defaultBranch.head };
84
+ }
85
+ const git = createGit(cwd);
86
+ try {
87
+ await git.raw(["merge-base", "--is-ancestor", base, defaultBranch.head]);
88
+ const behindBy = Number.parseInt((await git.raw(["rev-list", "--count", `${base}..${defaultBranch.head}`])).trim(), 10);
89
+ return {
90
+ state: "drift",
91
+ defaultBranch: defaultBranch.branch,
92
+ defaultHead: defaultBranch.head,
93
+ behindBy: Number.isFinite(behindBy) ? behindBy : 0,
94
+ };
95
+ }
96
+ catch {
97
+ return { state: "orphaned", defaultBranch: defaultBranch.branch, defaultHead: defaultBranch.head };
98
+ }
99
+ }