@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
@@ -3,8 +3,9 @@ import { parseToAST } from "./markdown/parser.js";
3
3
  import { extractListItems, renderSectionContent } from "./markdown/render.js";
4
4
  import { normalizeSectionTitle } from "./markdown/titles.js";
5
5
  import { markdownSourceSections, renderScopeFragment, validateScopeFragmentLines } from "./scope.js";
6
- const KNOWN_SECTIONS = new Set(["objective", "scope", "checks"]);
7
- const BIND_DRAFT_SECTION_ORDER = ["name", "objective", "scope", "checks"];
6
+ const KNOWN_SECTIONS = new Set(["context", "objective", "design", "scope", "criteria", "verification"]);
7
+ const RETIRED_OR_MALFORMED_CORE_SECTIONS = new Set(["title", "name", "checks"]);
8
+ const BIND_DRAFT_SECTION_ORDER = ["title", "context", "objective", "design", "scope", "criteria", "verification"];
8
9
  function invalidBindDraft(message) {
9
10
  throw new FlowError("INVALID_KEIYAKU_DRAFT", `INVALID_BIND_DRAFT: ${message}`);
10
11
  }
@@ -50,26 +51,26 @@ function findUnexpectedListOnlyLine(section) {
50
51
  }
51
52
  return null;
52
53
  }
53
- function extractSingleLineChecks(section) {
54
+ function extractSingleLineDeclarations(section, heading) {
54
55
  const unexpected = findUnexpectedListOnlyLine(section);
55
56
  if (unexpected) {
56
- invalidBindDraft(`Checks must be a bullet list; found '${unexpected}'`);
57
+ invalidBindDraft(`${heading} must be a bullet list; found '${unexpected}'`);
57
58
  }
58
59
  const items = extractListItems(section);
59
60
  if (items.length === 0) {
60
61
  const hasListNode = section.children.some((child) => child.type === "list");
61
62
  if (hasListNode) {
62
- invalidBindDraft("empty check in bind draft");
63
+ invalidBindDraft(`empty ${heading.toLowerCase()} declaration in bind draft`);
63
64
  }
64
- invalidBindDraft("Checks must be a bullet list; found empty Checks section");
65
+ invalidBindDraft(`${heading} must be a bullet list; found empty ${heading} section`);
65
66
  }
66
67
  return items.map((item) => {
67
68
  const trimmed = item.trim();
68
69
  if (trimmed.length === 0) {
69
- invalidBindDraft("empty check in bind draft");
70
+ invalidBindDraft(`empty ${heading.toLowerCase()} declaration in bind draft`);
70
71
  }
71
72
  if (item.includes("\n") || item.includes("\r")) {
72
- invalidBindDraft("check items must be a single line");
73
+ invalidBindDraft(`${heading} declarations must be a single line`);
73
74
  }
74
75
  return trimmed;
75
76
  });
@@ -85,7 +86,7 @@ function collectBindDraftNodes(content) {
85
86
  structuralContent = `${structuralContent.slice(0, source.contentStart)}\`\`\`\nscope\n\`\`\`\n${structuralContent.slice(source.end)}`;
86
87
  }
87
88
  const ast = parseToAST(structuralContent);
88
- let name;
89
+ let title;
89
90
  let seenH1 = false;
90
91
  const sections = new Map();
91
92
  const extensions = [];
@@ -97,12 +98,16 @@ function collectBindDraftNodes(content) {
97
98
  invalidBindDraft("bind draft allows exactly one H1 heading");
98
99
  }
99
100
  seenH1 = true;
100
- name = node.title.trim() || undefined;
101
+ title = node.title.trim() || undefined;
101
102
  continue;
102
103
  }
103
104
  if (node.level !== 2)
104
105
  continue;
105
106
  const normalized = normalizeSectionName(node.title);
107
+ if (RETIRED_OR_MALFORMED_CORE_SECTIONS.has(normalized)) {
108
+ const replacement = normalized === "checks" ? "Criteria and Verification" : "the H1 Title";
109
+ invalidBindDraft(`obsolete or malformed core section '${node.title}'; use ${replacement}`);
110
+ }
106
111
  if (KNOWN_SECTIONS.has(normalized)) {
107
112
  if (sections.has(normalized)) {
108
113
  invalidBindDraft(`duplicate section '${node.title}' in bind draft`);
@@ -115,7 +120,7 @@ function collectBindDraftNodes(content) {
115
120
  content: normalizeExtensionContent(renderSectionContent(node)),
116
121
  });
117
122
  }
118
- return { ...(name === undefined ? {} : { name }), sections, extensions };
123
+ return { ...(title === undefined ? {} : { title }), sections, extensions };
119
124
  }
120
125
  function parseScopeSection(content) {
121
126
  const sourceSections = markdownSourceSections(content).filter((section) => section.level === 2 && normalizeSectionTitle(section.title) === "scope");
@@ -141,52 +146,68 @@ function parseScopeSection(content) {
141
146
  export function parsePartialBindDraft(content) {
142
147
  if (content.trim().length === 0)
143
148
  return {};
144
- const { name, sections, extensions } = collectBindDraftNodes(content);
149
+ const { title, sections, extensions } = collectBindDraftNodes(content);
145
150
  const partial = {};
146
- if (name !== undefined)
147
- partial.name = name;
148
- const objective = sections.get("objective");
149
- if (objective) {
150
- const objectiveText = renderSectionContent(objective).trim();
151
- if (!objectiveText)
152
- invalidBindDraft("Objective section cannot be empty");
153
- partial.objective = objectiveText;
151
+ if (title !== undefined)
152
+ partial.title = title;
153
+ for (const sectionName of ["context", "objective", "design"]) {
154
+ const section = sections.get(sectionName);
155
+ if (!section)
156
+ continue;
157
+ const text = renderSectionContent(section).trim();
158
+ const heading = `${sectionName[0].toUpperCase()}${sectionName.slice(1)}`;
159
+ if (!text)
160
+ invalidBindDraft(`${heading} section cannot be empty`);
161
+ partial[sectionName] = text;
154
162
  }
155
163
  const scope = sections.get("scope");
156
164
  if (scope) {
157
165
  partial.scope = parseScopeSection(content);
158
166
  }
159
- const checks = sections.get("checks");
160
- if (checks)
161
- partial.checks = extractSingleLineChecks(checks);
167
+ const criteria = sections.get("criteria");
168
+ if (criteria)
169
+ partial.criteria = extractSingleLineDeclarations(criteria, "Criteria");
170
+ const verification = sections.get("verification");
171
+ if (verification)
172
+ partial.verification = extractSingleLineDeclarations(verification, "Verification");
162
173
  if (extensions.length > 0)
163
174
  partial.extensions = extensions;
164
175
  return partial;
165
176
  }
166
177
  export function mergeBindDraft(base, override) {
167
178
  return {
168
- ...(base.name !== undefined ? { name: base.name } : {}),
179
+ ...(base.title !== undefined ? { title: base.title } : {}),
180
+ ...(base.context !== undefined ? { context: base.context } : {}),
169
181
  ...(base.objective !== undefined ? { objective: base.objective } : {}),
182
+ ...(base.design !== undefined ? { design: base.design } : {}),
170
183
  ...(base.scope !== undefined ? { scope: base.scope } : {}),
171
- ...(base.checks !== undefined ? { checks: base.checks } : {}),
184
+ ...(base.criteria !== undefined ? { criteria: base.criteria } : {}),
185
+ ...(base.verification !== undefined ? { verification: base.verification } : {}),
172
186
  ...(base.extensions !== undefined ? { extensions: base.extensions } : {}),
173
- ...(override.name !== undefined ? { name: override.name } : {}),
187
+ ...(override.title !== undefined ? { title: override.title } : {}),
188
+ ...(override.context !== undefined ? { context: override.context } : {}),
174
189
  ...(override.objective !== undefined ? { objective: override.objective } : {}),
190
+ ...(override.design !== undefined ? { design: override.design } : {}),
175
191
  ...(override.scope !== undefined ? { scope: override.scope } : {}),
176
- ...(override.checks !== undefined ? { checks: override.checks } : {}),
192
+ ...(override.criteria !== undefined ? { criteria: override.criteria } : {}),
193
+ ...(override.verification !== undefined ? { verification: override.verification } : {}),
177
194
  ...(override.extensions !== undefined ? { extensions: override.extensions } : {}),
178
195
  };
179
196
  }
180
197
  export function missingBindDraftSections(partial) {
181
198
  const missing = [];
182
- if (partial.name === undefined || partial.name.trim().length === 0)
183
- missing.push("name");
199
+ if (partial.title === undefined || partial.title.trim().length === 0)
200
+ missing.push("title");
201
+ if (partial.context === undefined || partial.context.trim().length === 0)
202
+ missing.push("context");
184
203
  if (partial.objective === undefined || partial.objective.trim().length === 0)
185
204
  missing.push("objective");
205
+ if (partial.design === undefined || partial.design.trim().length === 0)
206
+ missing.push("design");
186
207
  if (partial.scope === undefined || partial.scope.length === 0)
187
208
  missing.push("scope");
188
- if (partial.checks === undefined || partial.checks.length === 0)
189
- missing.push("checks");
209
+ if (partial.criteria === undefined || partial.criteria.length === 0)
210
+ missing.push("criteria");
190
211
  return missing;
191
212
  }
192
213
  /** Paste-ready partial markdown for only the missing required sections. */
@@ -195,19 +216,31 @@ export function renderMissingBindSectionsTemplate(missing) {
195
216
  for (const section of BIND_DRAFT_SECTION_ORDER) {
196
217
  if (!missing.includes(section))
197
218
  continue;
198
- if (section === "name") {
199
- lines.push("# <name>");
219
+ if (section === "title") {
220
+ lines.push("# <title>");
221
+ continue;
222
+ }
223
+ if (section === "context") {
224
+ lines.push("## Context", "<established facts and constraints>");
200
225
  continue;
201
226
  }
202
227
  if (section === "objective") {
203
228
  lines.push("## Objective", "<what the commission should accomplish>");
204
229
  continue;
205
230
  }
231
+ if (section === "design") {
232
+ lines.push("## Design", "<settled owner, boundaries, flow, state, and failure behavior>");
233
+ continue;
234
+ }
206
235
  if (section === "scope") {
207
236
  lines.push("## Scope", "~~~", "<pattern>", "<another-pattern>", "~~~");
208
237
  continue;
209
238
  }
210
- lines.push("## Checks", "- <verification>");
239
+ if (section === "criteria") {
240
+ lines.push("## Criteria", "- <declarative acceptance criterion>");
241
+ continue;
242
+ }
243
+ lines.push("## Verification", "- <executable command or testimony declaration>");
211
244
  }
212
245
  return lines.join("\n");
213
246
  }
@@ -226,46 +259,70 @@ export function materializeBindDraft(partial) {
226
259
  invalidBindDraft(`missing ${describeMissingBindSections(missing)} in bind draft`);
227
260
  }
228
261
  return {
229
- name: partial.name.trim(),
262
+ title: partial.title.trim(),
263
+ context: partial.context.trim(),
230
264
  objective: partial.objective.trim(),
265
+ design: partial.design.trim(),
231
266
  scope: partial.scope,
232
- checks: partial.checks,
267
+ criteria: partial.criteria,
268
+ verification: partial.verification ?? [],
233
269
  extensions: partial.extensions ?? [],
234
270
  };
235
271
  }
236
272
  export function parseBindDraft(content) {
237
- const { name, sections, extensions } = collectBindDraftNodes(content);
238
- if (!name) {
239
- invalidBindDraft("missing H1 in bind draft; expected '# <name>' followed by '## Objective', '## Scope', and '## Checks'");
273
+ const { title, sections, extensions } = collectBindDraftNodes(content);
274
+ if (!title) {
275
+ invalidBindDraft("missing H1 Title in bind draft; expected '# <title>' followed by Context, Objective, Design, Scope, and Criteria; Verification is optional");
240
276
  }
277
+ const context = sections.get("context");
241
278
  const objective = sections.get("objective");
279
+ const design = sections.get("design");
242
280
  const scope = sections.get("scope");
243
- const checks = sections.get("checks");
281
+ const criteria = sections.get("criteria");
282
+ const verification = sections.get("verification");
283
+ if (!context)
284
+ invalidBindDraft("missing Context section in bind draft");
244
285
  if (!objective)
245
286
  invalidBindDraft("missing Objective section in bind draft");
287
+ if (!design)
288
+ invalidBindDraft("missing Design section in bind draft");
246
289
  if (!scope)
247
290
  invalidBindDraft("missing Scope section in bind draft");
248
291
  const parsedScope = parseScopeSection(content);
249
- if (!checks)
250
- invalidBindDraft("missing Checks section in bind draft");
292
+ if (!criteria)
293
+ invalidBindDraft("missing Criteria section in bind draft");
294
+ const contextText = renderSectionContent(context).trim();
251
295
  const objectiveText = renderSectionContent(objective).trim();
296
+ const designText = renderSectionContent(design).trim();
297
+ if (!contextText)
298
+ invalidBindDraft("Context section cannot be empty");
252
299
  if (!objectiveText)
253
300
  invalidBindDraft("Objective section cannot be empty");
301
+ if (!designText)
302
+ invalidBindDraft("Design section cannot be empty");
254
303
  return {
255
- name,
304
+ title,
305
+ context: contextText,
256
306
  objective: objectiveText,
307
+ design: designText,
257
308
  scope: parsedScope,
258
- checks: extractSingleLineChecks(checks),
309
+ criteria: extractSingleLineDeclarations(criteria, "Criteria"),
310
+ verification: verification ? extractSingleLineDeclarations(verification, "Verification") : [],
259
311
  extensions,
260
312
  };
261
313
  }
262
314
  export function renderBindDraft(draft) {
263
315
  const sections = [
264
- `# ${draft.name}`,
316
+ `# ${draft.title}`,
317
+ `## Context\n${draft.context}`,
265
318
  `## Objective\n${draft.objective}`,
319
+ `## Design\n${draft.design}`,
266
320
  `## Scope\n${renderScopeFragment(draft.scope)}`,
267
- `## Checks\n${draft.checks.map((check) => `- ${check}`).join("\n")}`,
321
+ `## Criteria\n${draft.criteria.map((criterion) => `- ${criterion}`).join("\n")}`,
268
322
  ];
323
+ if (draft.verification.length > 0) {
324
+ sections.push(`## Verification\n${draft.verification.map((declaration) => `- ${declaration}`).join("\n")}`);
325
+ }
269
326
  for (const extension of draft.extensions) {
270
327
  const content = normalizeExtensionContent(extension.content);
271
328
  if (content.length > 0) {
@@ -279,21 +336,32 @@ export function renderBindDraft(draft) {
279
336
  }
280
337
  export function compileBindDraftFlags(input) {
281
338
  for (const [flag, value] of [
282
- ["--place", input.place],
339
+ ["--title", input.title],
340
+ ["--context", input.context],
283
341
  ["--objective", input.objective],
342
+ ["--design", input.design],
284
343
  ["--scope", input.scope],
285
- ["--checks", input.checks],
344
+ ["--criteria", input.criteria],
345
+ ["--verification", input.verification],
286
346
  ]) {
287
- if (value.includes("\n") || value.includes("\r")) {
347
+ if (value !== undefined && (value.includes("\n") || value.includes("\r"))) {
288
348
  throw new FlowError("EMPTY_PARAM", `${flag} must be a single line`);
289
349
  }
290
350
  }
291
- const draft = {
292
- name: input.place,
293
- objective: input.objective,
294
- scope: [input.scope],
295
- checks: [input.checks],
296
- extensions: [],
297
- };
298
- return renderBindDraft(parseBindDraft(renderBindDraft(draft)));
351
+ const sections = [];
352
+ if (input.title !== undefined)
353
+ sections.push(`# ${input.title}`);
354
+ if (input.context !== undefined)
355
+ sections.push(`## Context\n${input.context}`);
356
+ if (input.objective !== undefined)
357
+ sections.push(`## Objective\n${input.objective}`);
358
+ if (input.design !== undefined)
359
+ sections.push(`## Design\n${input.design}`);
360
+ if (input.scope !== undefined)
361
+ sections.push(`## Scope\n~~~\n${input.scope}\n~~~`);
362
+ if (input.criteria !== undefined)
363
+ sections.push(`## Criteria\n- ${input.criteria}`);
364
+ if (input.verification !== undefined)
365
+ sections.push(`## Verification\n- ${input.verification}`);
366
+ return sections.length > 0 ? `${sections.join("\n\n")}\n` : "";
299
367
  }
@@ -1,67 +1,123 @@
1
1
  import { FlowError } from "../flow-error.js";
2
2
  import { assertCleanTrackedWorkingTree } from "../git/worktree.js";
3
- import { activeLedgerRef, appendWithRetry, readLedger } from "./ledger.js";
4
- import { releaseScope } from "./registry.js";
5
- import { deriveContractState, isTerminalState } from "./status/lifecycle.js";
6
- import { reconcileTaskForfeit } from "./task/index.js";
7
- import { cleanupContractWorkspace, findContractWorktreePath, stableRepoRoot } from "./worktree-path.js";
8
- async function cleanupForfeit(cwd, contractId, actor, at) {
9
- const stableRoot = await stableRepoRoot(cwd);
10
- await releaseScope(stableRoot, { contractId, actor, at });
11
- return await cleanupContractWorkspace(stableRoot, contractId);
12
- }
13
- export async function forfeitContract(input, deps = {}) {
3
+ import { runLifecycle } from "./lifecycle-runner.js";
4
+ import { currentLifecycleState } from "./repository-ledger/read-model.js";
5
+ import { isTerminalState } from "./status/lifecycle.js";
6
+ import { terminalCleanupFacts } from "./worktree-path.js";
7
+ import { repositoryDeliveryRef } from "./repository-ledger/codec.js";
8
+ export async function forfeitContract(input) {
14
9
  const at = new Date(input.nowMs).toISOString();
15
- const stableRoot = await stableRepoRoot(input.cwd);
16
- const deliveryWorktree = await findContractWorktreePath(stableRoot, input.contractId);
17
- await assertCleanTrackedWorkingTree(deliveryWorktree ?? stableRoot);
18
- const ledger = await readLedger(stableRoot, input.contractId);
19
- if (ledger) {
20
- const state = deriveContractState(ledger.entries);
21
- if (state.state === "forfeited") {
22
- await reconcileTaskForfeit({
23
- cwd: stableRoot,
24
- contractId: input.contractId,
25
- publishLedger: async () => undefined,
26
- });
27
- const cleanup = await (deps.cleanupForfeit ?? cleanupForfeit)(stableRoot, input.contractId, input.actor, at);
28
- return { contractId: input.contractId, cleanup };
29
- }
30
- }
31
- if (!ledger) {
32
- throw new FlowError("INTERNAL_STATE", `contract ${input.contractId} not found or not active`);
33
- }
34
- const state = deriveContractState(ledger.entries);
35
- if (state.state === "corrupt" || isTerminalState(state)) {
36
- throw new FlowError("INTERNAL_STATE", `contract ${input.contractId} cannot forfeit from state ${state.state}`);
37
- }
38
- await reconcileTaskForfeit({
39
- cwd: stableRoot,
10
+ const requestedNote = input.note?.trim() || undefined;
11
+ const execution = await runLifecycle({
12
+ cwd: input.cwd,
40
13
  contractId: input.contractId,
41
- publishLedger: async () => {
42
- const result = await appendWithRetry(stableRoot, activeLedgerRef(input.contractId), () => {
43
- const entry = {
44
- v: 1,
45
- kind: "forfeit",
46
- contract: input.contractId,
47
- at,
14
+ mutation: {
15
+ verb: "forfeit",
16
+ },
17
+ signal: input.signal,
18
+ ...(input.terminalWorkspaceCleanup ? { terminalWorkspaceCleanup: input.terminalWorkspaceCleanup } : {}),
19
+ admit: async (session, replay) => {
20
+ const state = currentLifecycleState(session.lifecycle);
21
+ if (state.state !== "forfeited" && isTerminalState(state))
22
+ throw new FlowError("INTERNAL_STATE", `contract ${input.contractId} cannot forfeit from state ${state.state}`);
23
+ if (state.state === "forfeited") {
24
+ const transaction = replay.state === "verb-committed" ? replay.transaction : undefined;
25
+ const existing = transaction?.entries.at(-1);
26
+ if (!existing || existing.kind !== "forfeit")
27
+ throw new FlowError("INTERNAL_STATE", `contract ${input.contractId} forfeited without an accepted forfeit transaction`);
28
+ return {
29
+ state,
30
+ delivery: { status: "absent" },
31
+ existing: { transactionObjectId: transaction.transactionObjectId, entry: existing },
32
+ ...(session.target.status === "available" ? { targetRoot: session.target.root } : {}),
33
+ };
34
+ }
35
+ if (session.target.status === "available")
36
+ await assertCleanTrackedWorkingTree(session.target.root);
37
+ if (session.lifecycle.petition.state === "awaiting-verdict") {
38
+ throw new FlowError("INTERNAL_STATE", `contract ${input.contractId} has a pending settlement`);
39
+ }
40
+ if (session.lifecycle.petition.state === "approved" && session.lifecycle.petition.intent !== "forfeit") {
41
+ throw new FlowError("INTERNAL_STATE", `contract ${input.contractId} has an approved non-forfeit settlement`);
42
+ }
43
+ if (!session.pins.delivery.head)
44
+ throw new FlowError("INTERNAL_STATE", `contract ${input.contractId} delivery ref is absent before forfeit`);
45
+ return {
46
+ state,
47
+ delivery: { status: "present", head: session.pins.delivery.head },
48
+ ...(session.target.status === "available" ? { targetRoot: session.target.root } : {}),
49
+ };
50
+ },
51
+ prepare: (admission) => ({
52
+ ...(admission.business.existing ? { existing: admission.business.existing } : {}),
53
+ entry: admission.business.existing?.entry ?? {
54
+ v: 2,
55
+ kind: "forfeit",
56
+ contract: input.contractId,
57
+ at,
58
+ actor: input.actor,
59
+ data: {
60
+ reason: input.reason,
48
61
  actor: input.actor,
49
- data: {
50
- reason: input.reason,
51
- by: input.actor,
52
- ...(input.note?.trim() ? { note: input.note.trim() } : {}),
62
+ finalHead: admission.business.delivery.status === "present"
63
+ ? admission.business.delivery.head
64
+ : admission.business.existing.entry.data.finalHead,
65
+ ...(requestedNote ? { note: requestedNote } : {}),
66
+ },
67
+ },
68
+ }),
69
+ revalidate: async ({ session, admission, prepared }) => {
70
+ const state = currentLifecycleState(session.lifecycle);
71
+ if (prepared.existing) {
72
+ return session.repository.topology.nodes.some((node) => node.objectId === prepared.existing.transactionObjectId)
73
+ ? undefined
74
+ : { code: "FORFEIT_STATE_CHANGED", message: "accepted forfeit entry changed during replay preparation" };
75
+ }
76
+ if (session.target.status === "available")
77
+ await assertCleanTrackedWorkingTree(session.target.root);
78
+ if (admission.business.delivery.status !== "present" || session.pins.delivery.head !== admission.business.delivery.head) {
79
+ return { code: "FORFEIT_DELIVERY_CHANGED", message: "contract delivery changed during forfeit preparation" };
80
+ }
81
+ return state.state === admission.business.state.state
82
+ ? undefined
83
+ : { code: "FORFEIT_STATE_CHANGED", message: "contract lifecycle changed during forfeit preparation" };
84
+ },
85
+ effect: async ({ context, admission, prepared }) => {
86
+ const publication = prepared.existing
87
+ ? await context.replayCommitted({ transactionObjectId: prepared.existing.transactionObjectId, carrier: "retire" })
88
+ : await context.publishAuthoritative({
89
+ verb: "forfeit",
90
+ admittedLedgerHead: admission.ledgerHead,
91
+ acceptedLifecycleTail: context.session.lifecycle.history.at(-1).objectId,
92
+ entries: [prepared.entry],
93
+ delivery: {
94
+ ref: repositoryDeliveryRef(input.contractId),
95
+ expectedOid: admission.business.delivery.status === "present" ? admission.business.delivery.head : null,
53
96
  },
54
- };
55
- return entry;
56
- });
57
- if (!result.ok)
58
- throw new FlowError("INTERNAL_STATE", `failed to forfeit ${input.contractId}: ${result.reason}`);
59
- return result;
97
+ });
98
+ if (publication.status === "indeterminate") {
99
+ return { kind: "precommit-effects-only", authoritativeHead: publication.currentHead ?? admission.ledgerHead };
100
+ }
101
+ if (publication.status !== "committed") {
102
+ return { kind: "refused-before-effects", authoritativeHead: publication.currentHead ?? admission.ledgerHead, refusal: { code: "LEDGER_CAS_CONFLICT", message: `contract ${input.contractId} changed during forfeit` } };
103
+ }
104
+ const published = publication;
105
+ await context.session.completeReplay("retire");
106
+ const cleanupRow = context.journal.all().find((row) => row.effect === "worktree-projection" && row.status === "succeeded");
107
+ const cleanupFacts = cleanupRow?.status === "succeeded" ? cleanupRow.receipt?.facts : undefined;
108
+ const deliveryRefRow = context.journal.all().find((row) => row.effect === "delivery-ref-projection" && row.status === "succeeded");
109
+ const deliveryRefFacts = deliveryRefRow?.status === "succeeded" ? deliveryRefRow.receipt?.facts : undefined;
110
+ const cleanup = terminalCleanupFacts(cleanupFacts, deliveryRefFacts);
111
+ if (!cleanup)
112
+ throw new FlowError("INTERNAL_STATE", "terminal replay completed without typed cleanup facts");
113
+ return { kind: "committed", commit: published, entryKind: "forfeit", value: { cleanup } };
60
114
  },
61
115
  });
62
- // Forfeit entry naturally removes membership; no dequeue record.
63
- const cleanup = await (deps.cleanupForfeit ?? cleanupForfeit)(stableRoot, input.contractId, input.actor, at);
64
- return { contractId: input.contractId, cleanup };
116
+ if (execution.kind === "noncommitted")
117
+ return execution.outcome;
118
+ if (execution.kind === "contained")
119
+ return { ...execution.outcome, contractId: input.contractId };
120
+ return { ...execution.outcome, contractId: input.contractId, cleanup: execution.payload.cleanup, carrier: execution.carrier };
65
121
  }
66
122
  export async function forfeitContractCommand(input) {
67
123
  return await forfeitContract({
@@ -71,5 +127,6 @@ export async function forfeitContractCommand(input) {
71
127
  nowMs: input.nowMs,
72
128
  reason: "manual",
73
129
  note: input.note,
130
+ signal: input.signal,
74
131
  });
75
132
  }
@@ -1,3 +1,4 @@
1
+ import * as crypto from "node:crypto";
1
2
  const SLUG_CHARACTER = /[\p{Letter}\p{Number}\p{Mark}]/u;
2
3
  const SLUG_LETTER_OR_NUMBER = /[\p{Letter}\p{Number}]/u;
3
4
  const WINDOWS_RESERVED_BASENAME = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])$/u;
@@ -6,8 +7,8 @@ const WINDOWS_RESERVED_BASENAME = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])$/u;
6
7
  * collision ordinals, extensions, and temporary-write bookkeeping.
7
8
  */
8
9
  export const IDENTIFIER_SLUG_MAX_UTF8_BYTES = 120;
9
- /** Reserves `-` plus every decimal digit of a safe-integer collision ordinal. */
10
- export const SUFFIXABLE_IDENTIFIER_SLUG_MAX_UTF8_BYTES = IDENTIFIER_SLUG_MAX_UTF8_BYTES - 17;
10
+ export const TASK_IDENTIFIER_MAX_UTF8_BYTES = 48;
11
+ const TASK_IDENTIFIER_DIGEST_HEX_LENGTH = 8;
11
12
  export class IdentifierSlugError extends Error {
12
13
  code = "INVALID_IDENTIFIER_SLUG";
13
14
  constructor(message, options) {
@@ -18,23 +19,14 @@ export class IdentifierSlugError extends Error {
18
19
  function utf8Bytes(value) {
19
20
  return Buffer.byteLength(value, "utf8");
20
21
  }
21
- /**
22
- * Normalize human text into one Unicode-safe, portable identifier slug.
23
- * NFKC makes canonically and compatibly equivalent input deterministic. All
24
- * non-letter/number/mark runs become one ASCII hyphen, then the result is
25
- * truncated at a Unicode code-point boundary to the shared UTF-8 byte bound.
26
- */
27
- export function normalizeIdentifierSlug(value, maxUtf8Bytes = IDENTIFIER_SLUG_MAX_UTF8_BYTES) {
28
- if (!Number.isSafeInteger(maxUtf8Bytes) || maxUtf8Bytes < 1 || maxUtf8Bytes > IDENTIFIER_SLUG_MAX_UTF8_BYTES) {
29
- throw new IdentifierSlugError(`slug byte bound must be from 1 through ${IDENTIFIER_SLUG_MAX_UTF8_BYTES}`);
30
- }
22
+ function normalizedIdentifierSlug(value, maxUtf8Bytes) {
31
23
  let slug = "";
32
24
  let pendingSeparator = false;
33
25
  for (const character of value.normalize("NFKC").toLowerCase()) {
34
26
  if (SLUG_CHARACTER.test(character)) {
35
27
  const addition = `${pendingSeparator && slug ? "-" : ""}${character}`;
36
28
  if (utf8Bytes(slug) + utf8Bytes(addition) > maxUtf8Bytes)
37
- break;
29
+ return { slug, truncated: true };
38
30
  slug += addition;
39
31
  pendingSeparator = false;
40
32
  }
@@ -42,6 +34,18 @@ export function normalizeIdentifierSlug(value, maxUtf8Bytes = IDENTIFIER_SLUG_MA
42
34
  pendingSeparator = true;
43
35
  }
44
36
  }
37
+ return { slug, truncated: false };
38
+ }
39
+ function truncateNormalizedIdentifierSlug(value, maxUtf8Bytes) {
40
+ let prefix = "";
41
+ for (const character of value) {
42
+ if (utf8Bytes(prefix) + utf8Bytes(character) > maxUtf8Bytes)
43
+ break;
44
+ prefix += character;
45
+ }
46
+ return prefix.replace(/-+$/u, "");
47
+ }
48
+ function validatedIdentifierSlug(slug, maxUtf8Bytes) {
45
49
  if (!slug || !SLUG_LETTER_OR_NUMBER.test(slug)) {
46
50
  throw new IdentifierSlugError("text cannot be converted to a portable identifier slug; use at least one Unicode letter or number");
47
51
  }
@@ -54,6 +58,52 @@ export function normalizeIdentifierSlug(value, maxUtf8Bytes = IDENTIFIER_SLUG_MA
54
58
  }
55
59
  return slug;
56
60
  }
61
+ function completeNormalizedIdentifierSlug(value) {
62
+ let slug = "";
63
+ let pendingSeparator = false;
64
+ for (const character of value.normalize("NFKC").toLowerCase()) {
65
+ if (SLUG_CHARACTER.test(character)) {
66
+ slug += `${pendingSeparator && slug ? "-" : ""}${character}`;
67
+ pendingSeparator = false;
68
+ }
69
+ else if (slug) {
70
+ pendingSeparator = true;
71
+ }
72
+ }
73
+ return validatedIdentifierSlug(slug, Number.MAX_SAFE_INTEGER);
74
+ }
75
+ /**
76
+ * Normalize human text into one Unicode-safe, portable identifier slug.
77
+ * NFKC makes canonically and compatibly equivalent input deterministic. All
78
+ * non-letter/number/mark runs become one ASCII hyphen, then the result is
79
+ * truncated at a Unicode code-point boundary to the shared UTF-8 byte bound.
80
+ */
81
+ export function normalizeIdentifierSlug(value, maxUtf8Bytes = IDENTIFIER_SLUG_MAX_UTF8_BYTES) {
82
+ if (!Number.isSafeInteger(maxUtf8Bytes) || maxUtf8Bytes < 1 || maxUtf8Bytes > IDENTIFIER_SLUG_MAX_UTF8_BYTES) {
83
+ throw new IdentifierSlugError(`slug byte bound must be from 1 through ${IDENTIFIER_SLUG_MAX_UTF8_BYTES}`);
84
+ }
85
+ return validatedIdentifierSlug(normalizedIdentifierSlug(value, maxUtf8Bytes).slug, maxUtf8Bytes);
86
+ }
87
+ /**
88
+ * Allocate a newly generated task ID. Persisted task documents remain
89
+ * addressable at their original ID; this limit applies only to new IDs.
90
+ */
91
+ export function taskIdentifierSlug(value, collisionSuffix = "") {
92
+ const suffixBytes = utf8Bytes(collisionSuffix);
93
+ const availableBytes = TASK_IDENTIFIER_MAX_UTF8_BYTES - suffixBytes;
94
+ if (availableBytes < TASK_IDENTIFIER_DIGEST_HEX_LENGTH + 2) {
95
+ throw new IdentifierSlugError("task collision suffix leaves no room for a bounded identifier");
96
+ }
97
+ const normalized = completeNormalizedIdentifierSlug(value);
98
+ if (utf8Bytes(normalized) <= TASK_IDENTIFIER_MAX_UTF8_BYTES) {
99
+ return `${truncateNormalizedIdentifierSlug(normalized, availableBytes)}${collisionSuffix}`;
100
+ }
101
+ const prefixBytes = availableBytes - TASK_IDENTIFIER_DIGEST_HEX_LENGTH - 1;
102
+ const prefix = truncateNormalizedIdentifierSlug(normalized, prefixBytes);
103
+ const digest = crypto.createHash("sha256").update(normalized, "utf8").digest("hex")
104
+ .slice(0, TASK_IDENTIFIER_DIGEST_HEX_LENGTH);
105
+ return `${prefix}-${digest}${collisionSuffix}`;
106
+ }
57
107
  export function isNormalizedIdentifierSlug(value) {
58
108
  if (!value || utf8Bytes(value) > IDENTIFIER_SLUG_MAX_UTF8_BYTES)
59
109
  return false;
package/build/core/ids.js CHANGED
@@ -16,13 +16,13 @@ export class ContractIdError extends Error {
16
16
  this.name = "ContractIdError";
17
17
  }
18
18
  }
19
- export function slugifyContractName(name) {
19
+ export function slugifyContractTitle(title) {
20
20
  try {
21
- return normalizeIdentifierSlug(name);
21
+ return normalizeIdentifierSlug(title);
22
22
  }
23
23
  catch (error) {
24
24
  if (error instanceof IdentifierSlugError) {
25
- throw new ContractIdError(`contract name ${error.message}`, { cause: error });
25
+ throw new ContractIdError(`contract title ${error.message}`, { cause: error });
26
26
  }
27
27
  throw error;
28
28
  }
@@ -77,7 +77,7 @@ export function createUlid(nowMs, randomBytes) {
77
77
  return ulid;
78
78
  }
79
79
  export function buildContractId(input) {
80
- return `${slugifyContractName(input.name)}.${createUlid(input.nowMs, input.randomBytes)}`;
80
+ return `${slugifyContractTitle(input.title)}.${createUlid(input.nowMs, input.randomBytes)}`;
81
81
  }
82
82
  export async function assertValidContractId(cwd, id) {
83
83
  const git = createGit(cwd);