@tacuchi/agent-workflow-cli 22.4.1 → 23.0.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 (246) hide show
  1. package/README.md +2 -2
  2. package/dist/adapters/git-cli.js +59 -17
  3. package/dist/adapters/git-cli.js.map +1 -1
  4. package/dist/adapters/node-file-system.js +55 -1
  5. package/dist/adapters/node-file-system.js.map +1 -1
  6. package/dist/application/artifacts-service.js +2 -2
  7. package/dist/application/artifacts-service.js.map +1 -1
  8. package/dist/application/capability/dispatcher.js +2 -2
  9. package/dist/application/capability/dispatcher.js.map +1 -1
  10. package/dist/application/capability/installed-inventory.js +2 -2
  11. package/dist/application/capability/installed-inventory.js.map +1 -1
  12. package/dist/application/capability/readiness.js +1 -1
  13. package/dist/application/capability/readiness.js.map +1 -1
  14. package/dist/application/check-branch-service.js +2 -2
  15. package/dist/application/check-branch-service.js.map +1 -1
  16. package/dist/application/checkpoint/files-touched.js +271 -0
  17. package/dist/application/checkpoint/files-touched.js.map +1 -0
  18. package/dist/application/checkpoint/markdown.js +61 -8
  19. package/dist/application/checkpoint/markdown.js.map +1 -1
  20. package/dist/application/checkpoint/state-reader.js +8 -2
  21. package/dist/application/checkpoint/state-reader.js.map +1 -1
  22. package/dist/application/checkpoint-write-service.js +16 -5
  23. package/dist/application/checkpoint-write-service.js.map +1 -1
  24. package/dist/application/claims-ledger.js +330 -0
  25. package/dist/application/claims-ledger.js.map +1 -0
  26. package/dist/application/claims-recovery.js +338 -0
  27. package/dist/application/claims-recovery.js.map +1 -0
  28. package/dist/application/dev-only-services.js +154 -44
  29. package/dist/application/dev-only-services.js.map +1 -1
  30. package/dist/application/elicitation-server.js +382 -0
  31. package/dist/application/elicitation-server.js.map +1 -0
  32. package/dist/application/elicitation-stdio.js +48 -0
  33. package/dist/application/elicitation-stdio.js.map +1 -0
  34. package/dist/application/flow/advance.js +86 -5
  35. package/dist/application/flow/advance.js.map +1 -1
  36. package/dist/application/flow/checkout-observation.js +152 -0
  37. package/dist/application/flow/checkout-observation.js.map +1 -0
  38. package/dist/application/flow/flow-service.js +72 -13
  39. package/dist/application/flow/flow-service.js.map +1 -1
  40. package/dist/application/flow/internal-actions.js +460 -1
  41. package/dist/application/flow/internal-actions.js.map +1 -1
  42. package/dist/application/flow/internal-drive.js +22 -9
  43. package/dist/application/flow/internal-drive.js.map +1 -1
  44. package/dist/application/flow/prove.js +205 -0
  45. package/dist/application/flow/prove.js.map +1 -0
  46. package/dist/application/flow/run-journey.js +15 -0
  47. package/dist/application/flow/run-journey.js.map +1 -0
  48. package/dist/application/flow/run-projection.js +15 -3
  49. package/dist/application/flow/run-projection.js.map +1 -1
  50. package/dist/application/flow/run-state-service.js +19 -4
  51. package/dist/application/flow/run-state-service.js.map +1 -1
  52. package/dist/application/flow/submit.js +250 -72
  53. package/dist/application/flow/submit.js.map +1 -1
  54. package/dist/application/generate-launch-service.js +4 -4
  55. package/dist/application/generate-launch-service.js.map +1 -1
  56. package/dist/application/git-flow-service.js +9 -0
  57. package/dist/application/git-flow-service.js.map +1 -1
  58. package/dist/application/local-proposal.js +9 -0
  59. package/dist/application/local-proposal.js.map +1 -1
  60. package/dist/application/lock-service.js +6 -3
  61. package/dist/application/lock-service.js.map +1 -1
  62. package/dist/application/mcp-doctor-service.js.map +1 -1
  63. package/dist/application/mcp-host-reader.js +30 -13
  64. package/dist/application/mcp-host-reader.js.map +1 -1
  65. package/dist/application/mcp-host-writer.js +136 -112
  66. package/dist/application/mcp-host-writer.js.map +1 -1
  67. package/dist/application/mcp-remove-service.js +7 -3
  68. package/dist/application/mcp-remove-service.js.map +1 -1
  69. package/dist/application/mcp-scope-common.js +1 -3
  70. package/dist/application/mcp-scope-common.js.map +1 -1
  71. package/dist/application/mcp-setup-service.js +7 -3
  72. package/dist/application/mcp-setup-service.js.map +1 -1
  73. package/dist/application/merge-state-service.js +10 -16
  74. package/dist/application/merge-state-service.js.map +1 -1
  75. package/dist/application/multiroot/claude.js +34 -15
  76. package/dist/application/multiroot/claude.js.map +1 -1
  77. package/dist/application/multiroot/codex.js +9 -5
  78. package/dist/application/multiroot/codex.js.map +1 -1
  79. package/dist/application/multiroot-service.js +20 -11
  80. package/dist/application/multiroot-service.js.map +1 -1
  81. package/dist/application/parsers/project-block.js +25 -2
  82. package/dist/application/parsers/project-block.js.map +1 -1
  83. package/dist/application/paths-service.js +30 -31
  84. package/dist/application/paths-service.js.map +1 -1
  85. package/dist/application/plan-exec-batch-service.js +549 -0
  86. package/dist/application/plan-exec-batch-service.js.map +1 -0
  87. package/dist/application/plan-exec-decision-service.js +273 -0
  88. package/dist/application/plan-exec-decision-service.js.map +1 -0
  89. package/dist/application/project-md-service.js +2 -2
  90. package/dist/application/project-md-service.js.map +1 -1
  91. package/dist/application/project-md-upsert-service.js +4 -4
  92. package/dist/application/project-md-upsert-service.js.map +1 -1
  93. package/dist/application/project-tab-data.js +12 -7
  94. package/dist/application/project-tab-data.js.map +1 -1
  95. package/dist/application/release-data-service.js +2 -2
  96. package/dist/application/release-data-service.js.map +1 -1
  97. package/dist/application/resume-service.js +32 -11
  98. package/dist/application/resume-service.js.map +1 -1
  99. package/dist/application/retirement/apply.js +84 -0
  100. package/dist/application/retirement/apply.js.map +1 -1
  101. package/dist/application/retirement/attribution.js +31 -1
  102. package/dist/application/retirement/attribution.js.map +1 -1
  103. package/dist/application/retirement/graph.js +19 -1
  104. package/dist/application/retirement/graph.js.map +1 -1
  105. package/dist/application/retirement/journal.js +11 -0
  106. package/dist/application/retirement/journal.js.map +1 -1
  107. package/dist/application/retirement/prepare.js +89 -3
  108. package/dist/application/retirement/prepare.js.map +1 -1
  109. package/dist/application/retirement/preview.js +16 -0
  110. package/dist/application/retirement/preview.js.map +1 -1
  111. package/dist/application/retirement/resolve.js +65 -0
  112. package/dist/application/retirement/resolve.js.map +1 -1
  113. package/dist/application/self/host-states.js +2 -0
  114. package/dist/application/self/host-states.js.map +1 -1
  115. package/dist/application/self/install-skill.js +53 -19
  116. package/dist/application/self/install-skill.js.map +1 -1
  117. package/dist/application/self/mcp-config.js +28 -30
  118. package/dist/application/self/mcp-config.js.map +1 -1
  119. package/dist/application/self/mcp-offer.js +96 -0
  120. package/dist/application/self/mcp-offer.js.map +1 -0
  121. package/dist/application/self/mcp-via-state.js +59 -0
  122. package/dist/application/self/mcp-via-state.js.map +1 -0
  123. package/dist/application/self/uninstall-skill.js +18 -3
  124. package/dist/application/self/uninstall-skill.js.map +1 -1
  125. package/dist/application/self/uninstall.js +37 -8
  126. package/dist/application/self/uninstall.js.map +1 -1
  127. package/dist/application/session-close-service.js +24 -1
  128. package/dist/application/session-close-service.js.map +1 -1
  129. package/dist/application/session-create-service.js +7 -0
  130. package/dist/application/session-create-service.js.map +1 -1
  131. package/dist/application/session-resume-service.js +2 -2
  132. package/dist/application/session-resume-service.js.map +1 -1
  133. package/dist/application/sessions-service.js +2 -4
  134. package/dist/application/sessions-service.js.map +1 -1
  135. package/dist/application/skills-resolver-service.js +4 -4
  136. package/dist/application/skills-resolver-service.js.map +1 -1
  137. package/dist/application/source-boundary-policy.js +18 -7
  138. package/dist/application/source-boundary-policy.js.map +1 -1
  139. package/dist/application/source-remove-service.js +12 -1
  140. package/dist/application/source-remove-service.js.map +1 -1
  141. package/dist/application/sources-service.js +2 -2
  142. package/dist/application/sources-service.js.map +1 -1
  143. package/dist/application/status-service.js +4 -0
  144. package/dist/application/status-service.js.map +1 -1
  145. package/dist/application/visibility-doctor-service.js +1 -1
  146. package/dist/application/visibility-doctor-service.js.map +1 -1
  147. package/dist/application/workline-index-service.js +182 -27
  148. package/dist/application/workline-index-service.js.map +1 -1
  149. package/dist/application/workspace-init-service.js +85 -265
  150. package/dist/application/workspace-init-service.js.map +1 -1
  151. package/dist/application/workspace-materialization-service.js +300 -0
  152. package/dist/application/workspace-materialization-service.js.map +1 -0
  153. package/dist/application/worktree-service.js +32 -3
  154. package/dist/application/worktree-service.js.map +1 -1
  155. package/dist/cli/commands/capability.js +4 -7
  156. package/dist/cli/commands/capability.js.map +1 -1
  157. package/dist/cli/commands/claims.js +72 -0
  158. package/dist/cli/commands/claims.js.map +1 -0
  159. package/dist/cli/commands/dev-only.js +73 -25
  160. package/dist/cli/commands/dev-only.js.map +1 -1
  161. package/dist/cli/commands/flow.js +145 -42
  162. package/dist/cli/commands/flow.js.map +1 -1
  163. package/dist/cli/commands/index.js +2 -0
  164. package/dist/cli/commands/index.js.map +1 -1
  165. package/dist/cli/commands/mcp.js +124 -35
  166. package/dist/cli/commands/mcp.js.map +1 -1
  167. package/dist/cli/commands/multiroot.js +40 -7
  168. package/dist/cli/commands/multiroot.js.map +1 -1
  169. package/dist/cli/commands/resume.js +9 -3
  170. package/dist/cli/commands/resume.js.map +1 -1
  171. package/dist/cli/commands/retirement.js +9 -0
  172. package/dist/cli/commands/retirement.js.map +1 -1
  173. package/dist/cli/commands/session-create.js +1 -1
  174. package/dist/cli/commands/session-create.js.map +1 -1
  175. package/dist/cli/commands/skills.js +1 -1
  176. package/dist/cli/commands/skills.js.map +1 -1
  177. package/dist/cli/commands/status.js +72 -24
  178. package/dist/cli/commands/status.js.map +1 -1
  179. package/dist/cli/commands/workspace-init.js +5 -5
  180. package/dist/cli/commands/workspace-init.js.map +1 -1
  181. package/dist/cli/help-groups.js +5 -0
  182. package/dist/cli/help-groups.js.map +1 -1
  183. package/dist/cli/main.js +112 -39
  184. package/dist/cli/main.js.map +1 -1
  185. package/dist/cli/parser.js +3 -0
  186. package/dist/cli/parser.js.map +1 -1
  187. package/dist/cli/tui/app.js +3 -2
  188. package/dist/cli/tui/app.js.map +1 -1
  189. package/dist/cli/tui/components/host-admin-section.js +46 -2
  190. package/dist/cli/tui/components/host-admin-section.js.map +1 -1
  191. package/dist/cli/tui/components/workspace-init-form.js +7 -7
  192. package/dist/cli/tui/components/workspace-init-form.js.map +1 -1
  193. package/dist/cli/tui/data/workflow-content.js +2 -2
  194. package/dist/cli/tui/data/workflow-content.js.map +1 -1
  195. package/dist/cli/tui/tabs/config-tab.js +4 -3
  196. package/dist/cli/tui/tabs/config-tab.js.map +1 -1
  197. package/dist/cli/tui/tabs/mcp-tab-helpers.js +1 -1
  198. package/dist/cli/tui/tabs/mcp-tab-helpers.js.map +1 -1
  199. package/dist/cli/tui/tabs/project-tab.js +18 -21
  200. package/dist/cli/tui/tabs/project-tab.js.map +1 -1
  201. package/dist/cli/tui/workspace-root.js +13 -0
  202. package/dist/cli/tui/workspace-root.js.map +1 -0
  203. package/dist/domain/capability/protocol.js +2 -2
  204. package/dist/domain/capability/protocol.js.map +1 -1
  205. package/dist/domain/degradation-notice.js +60 -0
  206. package/dist/domain/degradation-notice.js.map +1 -0
  207. package/dist/domain/elicitation.js +152 -0
  208. package/dist/domain/elicitation.js.map +1 -0
  209. package/dist/domain/flow/answer.js +173 -42
  210. package/dist/domain/flow/answer.js.map +1 -1
  211. package/dist/domain/flow/authority.js +180 -127
  212. package/dist/domain/flow/authority.js.map +1 -1
  213. package/dist/domain/flow/directive.js +47 -1
  214. package/dist/domain/flow/directive.js.map +1 -1
  215. package/dist/domain/flow/rules.js +7 -4
  216. package/dist/domain/flow/rules.js.map +1 -1
  217. package/dist/domain/flow/run-state.js +636 -31
  218. package/dist/domain/flow/run-state.js.map +1 -1
  219. package/dist/domain/harnesses.js +17 -1
  220. package/dist/domain/harnesses.js.map +1 -1
  221. package/dist/domain/mcp-entry.js +36 -14
  222. package/dist/domain/mcp-entry.js.map +1 -1
  223. package/dist/domain/retirement/proposal.js +1 -0
  224. package/dist/domain/retirement/proposal.js.map +1 -1
  225. package/dist/domain/structured-choice-stamp.js +32 -4
  226. package/dist/domain/structured-choice-stamp.js.map +1 -1
  227. package/dist/domain/workline-mcp-entry.js +37 -0
  228. package/dist/domain/workline-mcp-entry.js.map +1 -0
  229. package/dist/runtime/namespace-resolver.js +104 -18
  230. package/dist/runtime/namespace-resolver.js.map +1 -1
  231. package/package.json +1 -1
  232. package/skills/w/README.md +2 -2
  233. package/skills/w/SKILL.md +9 -10
  234. package/skills/w/artifacts/README.md +1 -1
  235. package/skills/w/commands/README.md +3 -3
  236. package/skills/w/commands/persist.md +3 -1
  237. package/skills/w/commands/resume.md +1 -1
  238. package/skills/w/commands/spec-new.md +2 -2
  239. package/skills/w/commands/status.md +1 -1
  240. package/skills/w/commands/workspace-init.md +9 -10
  241. package/skills/w/context/MANIFEST.json +1 -4
  242. package/skills/w/harness/HARNESS.md +1 -1
  243. package/skills/w/loops/quick-loop/LOOP.md +1 -1
  244. package/skills/w/modules/PROMPT-CONTINUITY.md +1 -1
  245. package/skills/w/modules/WORKSPACE-SCAFFOLD.md +17 -20
  246. package/skills/w/schemas/capability-descriptor.schema.json +1 -1
@@ -0,0 +1,330 @@
1
+ /**
2
+ * The durable trace of a reservation's whole life — and the only place it lives.
3
+ *
4
+ * A claim used to leave exactly one artifact: the marker file itself. That made
5
+ * the reservation visible while it existed and completely unaccountable once it
6
+ * did not. Nothing recorded who had claimed a correlative, why it went away, or
7
+ * whether it had ever been published — so a released number was
8
+ * indistinguishable from one that never existed, and a recovery had no evidence
9
+ * to stand on.
10
+ *
11
+ * This ledger is append-only and lives under `.workflow/`, deliberately OUTSIDE
12
+ * `docs/`: the corpus is for documents somebody published, and a reservation's
13
+ * history is not one. Putting it in `docs/` would make the trace itself look like
14
+ * a spec or a plan, which is the confusion the whole change exists to end.
15
+ *
16
+ * It answers three different questions with one file, which is why there is one
17
+ * file and not three:
18
+ * - **what happened** — the audit trail, still readable after the session's
19
+ * folder is gone, because a session is deleted and its history should not be;
20
+ * - **which correlatives came back** — a number released and never published is
21
+ * eligible again, and only this record knows the difference;
22
+ * - **what may no longer be published** — a revocation is a durable fence, and
23
+ * a fence that lives in memory is not one.
24
+ *
25
+ * Append-only is load-bearing for the third: a record that can be rewritten is a
26
+ * fence somebody can lift, and the revocation has to be irrevocable to be worth
27
+ * anything.
28
+ */
29
+ import { join } from "node:path";
30
+ import { compareCorrelatives, isCorrelative } from "../domain/correlative.js";
31
+ /** Lives next to HISTORY.md: workspace state, never workspace corpus. */
32
+ const LEDGER_FILE = "claims.jsonl";
33
+ const LEDGER_VERSION = 1;
34
+ /** The identity as one comparable string. Order is fixed so it is stable. */
35
+ export function claimKey(claim) {
36
+ return `${claim.category}/${claim.correlative}-${claim.name}@${claim.owner}`;
37
+ }
38
+ export function ledgerPath(paths) {
39
+ return join(paths.cwdRoot(), LEDGER_FILE);
40
+ }
41
+ /**
42
+ * Add one record. Append-only by construction: there is no update and no delete.
43
+ *
44
+ * Two callers, two different guarantees, and the difference is worth stating
45
+ * rather than glossing:
46
+ *
47
+ * - the **claim** appends inside the workspace lock that mints the slot, so the
48
+ * reservation and its record cannot separate;
49
+ * - the **release** at session close does NOT hold that lock — the close's
50
+ * reservation sweep is deliberately outside it and non-fatal — so its record
51
+ * is ordered by `O_APPEND` alone.
52
+ *
53
+ * That is sufficient here only because a record is one short line: `O_APPEND`
54
+ * makes a single small write atomic, so two processes appending cannot interleave
55
+ * halves of a record. It would stop being sufficient the day a record grew past a
56
+ * pipe buffer, which is the reason a record is one line and not a pretty-printed
57
+ * object.
58
+ */
59
+ export async function appendClaimEvent(fs, paths, event) {
60
+ const record = { version: LEDGER_VERSION, ...event };
61
+ await fs.appendText(ledgerPath(paths), `${JSON.stringify(record)}\n`);
62
+ }
63
+ /** Every record, oldest first. A missing ledger reads as empty, never as an error. */
64
+ export async function readClaimEvents(fs, paths) {
65
+ const path = ledgerPath(paths);
66
+ if (!(await fs.exists(path)))
67
+ return { events: [], unreadable: 0 };
68
+ const raw = await fs.readText(path);
69
+ const events = [];
70
+ let unreadable = 0;
71
+ for (const line of raw.split("\n")) {
72
+ const trimmed = line.trim();
73
+ if (trimmed.length === 0)
74
+ continue;
75
+ const parsed = parseEvent(trimmed);
76
+ if (parsed === null)
77
+ unreadable += 1;
78
+ else
79
+ events.push(parsed);
80
+ }
81
+ return { events, unreadable };
82
+ }
83
+ function parseEvent(line) {
84
+ let value;
85
+ try {
86
+ value = JSON.parse(line);
87
+ }
88
+ catch {
89
+ return null;
90
+ }
91
+ if (typeof value !== "object" || value === null)
92
+ return null;
93
+ const candidate = value;
94
+ const claim = candidate.claim;
95
+ if (typeof candidate.at !== "string" ||
96
+ typeof candidate.event !== "string" ||
97
+ typeof claim !== "object" ||
98
+ claim === null ||
99
+ typeof claim.category !== "string" ||
100
+ typeof claim.correlative !== "string" ||
101
+ typeof claim.name !== "string" ||
102
+ typeof claim.owner !== "string") {
103
+ return null;
104
+ }
105
+ return value;
106
+ }
107
+ /**
108
+ * The claims this owner still holds: a `claimed` with no terminal record after it.
109
+ *
110
+ * Derived rather than stored, because the ledger is append-only and a "still
111
+ * open" flag would be exactly the kind of mutable state that makes an
112
+ * append-only log pointless. `published`, `released` and `revoked` are all
113
+ * terminal — the first spends the number, the other two end the owner's hold on
114
+ * it — so any of them closes the claim for this reading.
115
+ */
116
+ export function openClaimsOf(events, owner) {
117
+ const open = new Map();
118
+ for (const event of events) {
119
+ if (event.claim.owner !== owner)
120
+ continue;
121
+ const key = claimKey(event.claim);
122
+ if (event.event === "claimed")
123
+ open.set(key, event.claim);
124
+ else
125
+ open.delete(key);
126
+ }
127
+ return [...open.values()];
128
+ }
129
+ /**
130
+ * The claim a workspace-relative `docs/<category>/<NNN>-<name>` path would be.
131
+ *
132
+ * `null` for anything that is not a numbered document inside a category, which
133
+ * is what keeps this from reading a claim out of an unrelated destination.
134
+ *
135
+ * Exported because it is the ONE place that decides how a path splits into a
136
+ * claim identity: the publication reads it to credit a completion and the
137
+ * retirement reads it to name what it is giving back, and two spellings of that
138
+ * split would join to two different histories for the same slot.
139
+ */
140
+ export function claimOfDocsPath(path, owner) {
141
+ const parts = path.split("/").filter((segment) => segment.length > 0);
142
+ const docsAt = parts.indexOf("docs");
143
+ if (docsAt === -1 || parts.length - docsAt !== 3)
144
+ return null;
145
+ const category = parts[docsAt + 1];
146
+ const file = parts[docsAt + 2];
147
+ if (category === undefined || file === undefined)
148
+ return null;
149
+ const match = /^(\d{3,})-(.+)$/.exec(file);
150
+ if (match?.[1] === undefined || match[2] === undefined)
151
+ return null;
152
+ return { category, correlative: match[1], name: match[2], owner };
153
+ }
154
+ /**
155
+ * Which claims of this owner a publication just completed.
156
+ *
157
+ * The whole decision of "did this write finish a reservation of mine?" lives
158
+ * here rather than at the publication site, and getting it wrong is durable in
159
+ * both directions:
160
+ *
161
+ * - **Under-crediting** leaves a claim open about a correlative that is holding a
162
+ * published document, which invites a later recovery to release live bytes.
163
+ * That is why `already_applied` is handled here: the re-entry that finds the
164
+ * document already on disk reports `written: []`, and crediting only `written`
165
+ * left the claim open FOREVER — every retry answers the same way, so nothing
166
+ * could ever correct it. When the apply says already-applied, every destination
167
+ * holds exactly the proposed bytes, so the destinations are the candidates.
168
+ * - **Over-crediting** writes a permanent "spent forever" fence on a correlative
169
+ * the session never reserved. So a candidate only counts when it closes one of
170
+ * THIS owner's open claims, read from the ledger — not from the shape of
171
+ * whatever proposal happened to carry the write. Every flow's publication goes
172
+ * through this same path, not only the one whose single artifact is its own slot.
173
+ *
174
+ * Both directions had a surviving mutant when this logic sat inlined at the call
175
+ * site, which is the other reason it is here: it is testable in isolation.
176
+ */
177
+ export function completedClaimsIn(events, owner, publication) {
178
+ const candidates = publication.already_applied ? publication.destinations : publication.written;
179
+ if (candidates.length === 0)
180
+ return [];
181
+ const open = new Set(openClaimsOf(events, owner).map((claim) => claimKey(claim)));
182
+ if (open.size === 0)
183
+ return [];
184
+ const completed = [];
185
+ for (const path of candidates) {
186
+ const claim = claimOfDocsPath(path, owner);
187
+ if (claim === null || !open.has(claimKey(claim)))
188
+ continue;
189
+ completed.push(claim);
190
+ }
191
+ return completed;
192
+ }
193
+ /**
194
+ * Every claim this ledger has ever revoked.
195
+ *
196
+ * Membership is permanent by construction: one `revoked` record fences the claim
197
+ * forever, and no later record lifts it. That is what "irrevocable" has to mean
198
+ * to be worth anything — a fence somebody can reopen is not a fence, and the
199
+ * whole reason a recovery may free a correlative is that no late publication can
200
+ * still land on it.
201
+ */
202
+ function revokedKeys(events) {
203
+ const revoked = new Set();
204
+ for (const event of events) {
205
+ if (event.event === "revoked")
206
+ revoked.add(claimKey(event.claim));
207
+ }
208
+ return revoked;
209
+ }
210
+ /** Whether this exact claim is fenced. */
211
+ export function isRevoked(events, claim) {
212
+ return revokedKeys(events).has(claimKey(claim));
213
+ }
214
+ /**
215
+ * The destinations of a publication that a revocation forbids.
216
+ *
217
+ * Checked at the publication point rather than at the release: once a recovery
218
+ * has freed a correlative, the slot no longer exists on disk, so a late sealed
219
+ * proposal would read its destination as a plain creation and land a document on
220
+ * a number that may already belong to somebody else. The fence is the only thing
221
+ * standing between "the reservation was recovered" and "two documents share a
222
+ * correlative".
223
+ */
224
+ export function revokedAmong(events, owner, destinations) {
225
+ const revoked = revokedKeys(events);
226
+ if (revoked.size === 0)
227
+ return [];
228
+ const blocked = [];
229
+ for (const path of destinations) {
230
+ const claim = claimOfDocsPath(path, owner);
231
+ if (claim === null || !revoked.has(claimKey(claim)))
232
+ continue;
233
+ blocked.push(claim);
234
+ }
235
+ return blocked;
236
+ }
237
+ /**
238
+ * Destinations that look like a numbered document of this owner's category space.
239
+ *
240
+ * Used only to scope the fail-closed: a ledger with unreadable lines cannot prove
241
+ * the ABSENCE of a revocation, so a publication that could be completing a
242
+ * reservation must refuse rather than guess. A write that is not a numbered
243
+ * document in a category cannot be a reservation, so it is never held up by a
244
+ * ledger it does not depend on.
245
+ */
246
+ export function claimShapedAmong(owner, destinations) {
247
+ const shaped = [];
248
+ for (const path of destinations) {
249
+ const claim = claimOfDocsPath(path, owner);
250
+ if (claim !== null)
251
+ shaped.push(claim);
252
+ }
253
+ return shaped;
254
+ }
255
+ function sameSlot(claim, slot) {
256
+ return (claim.category === slot.category &&
257
+ claim.correlative === slot.correlative &&
258
+ claim.name === slot.name);
259
+ }
260
+ /**
261
+ * Whether this slot was ever published, by anyone.
262
+ *
263
+ * Asked BEFORE the bytes are interpreted, because bytes lie in one direction that
264
+ * matters: a published document whose content happens to be empty looks exactly
265
+ * like the legacy placeholder a recovery is allowed to delete. The record knows
266
+ * the difference and the file does not, so the record decides.
267
+ */
268
+ export function wasPublished(events, slot) {
269
+ return events.some((event) => event.event === "published" && sameSlot(event.claim, slot));
270
+ }
271
+ /**
272
+ * The owner whose claim on this slot is still open, or `null`.
273
+ *
274
+ * Lets a file that is no longer its own intact marker — emptied by an editor, a
275
+ * `> file`, a checkout — still be attributed to the session that reserved it.
276
+ * Without this the same file reads as ownerless, and freeing it would give the
277
+ * correlative back with no fence for the owner that is still holding it.
278
+ */
279
+ export function openOwnerOfSlot(events, slot) {
280
+ let open = null;
281
+ for (const event of events) {
282
+ if (!sameSlot(event.claim, slot))
283
+ continue;
284
+ open = event.event === "claimed" ? event.claim : null;
285
+ }
286
+ return open;
287
+ }
288
+ /**
289
+ * Correlatives of this category that were released and never published, ascending.
290
+ *
291
+ * The second reason this ledger exists. Minting used to compute `max + 1` and
292
+ * probe forward only, so a correlative given back in the middle of the range was
293
+ * lost forever — this workspace's own `docs/plans` has a permanent hole at `033`
294
+ * from exactly that. Only the record can tell a number that came back from one
295
+ * that never existed, because the disk looks identical either way.
296
+ *
297
+ * Judged per (category, correlative) and by the LAST terminal record, not by the
298
+ * presence of any: a number can be released, re-claimed and then published, and
299
+ * after that it is spent. `published` is the one state that never becomes eligible
300
+ * again — a correlative holding a document is not a free number, whatever else
301
+ * the history says about it.
302
+ *
303
+ * Ascending because the rule has to be deterministic and reproducible, and
304
+ * lowest-first also fills the holes rather than growing the range. The caller is
305
+ * still responsible for skipping anything taken on disk: this answers "did the
306
+ * record give it back", never "is the name free right now".
307
+ */
308
+ export function eligibleCorrelatives(events, category) {
309
+ const state = new Map();
310
+ for (const event of events) {
311
+ if (event.claim.category !== category)
312
+ continue;
313
+ if (event.event === "claimed")
314
+ continue;
315
+ state.set(event.claim.correlative, event.event);
316
+ }
317
+ const eligible = [];
318
+ for (const [correlative, last] of state) {
319
+ // Validated before it can be handed out. A single semi-valid ledger line
320
+ // would otherwise put a non-correlative into the mint, where it becomes an
321
+ // unrecognizable filename or a throw on the comparator.
322
+ if (last === "released" && isCorrelative(correlative))
323
+ eligible.push(correlative);
324
+ }
325
+ // `compareCorrelatives` and not a hand-rolled numeric sort: it is bigint-based,
326
+ // so it stays correct past the width where `parseInt` loses precision — and a
327
+ // second ordering rule for the same domain type is a second thing to keep true.
328
+ return eligible.sort(compareCorrelatives);
329
+ }
330
+ //# sourceMappingURL=claims-ledger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"claims-ledger.js","sourceRoot":"","sources":["../../src/application/claims-ledger.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,338 @@
1
+ /**
2
+ * Recovering a reservation whose owner never came back.
3
+ *
4
+ * The normal cycle needs none of this: completing, closing and cancelling all
5
+ * resolve a reservation through its owner. What is left is the run that simply
6
+ * died — no publication, no close, nothing terminal — and left a correlative held
7
+ * by a session that is never going to finish it. There is deliberately NO clock
8
+ * here: a reservation does not expire, because "it has been a while" is not
9
+ * evidence that nobody is coming, and a timer that frees a slot somebody is still
10
+ * writing into would be worse than the stranded number it fixes.
11
+ *
12
+ * So the release is explicit, authorized, and ORDERED. The order is the whole
13
+ * safety argument and it runs one way only:
14
+ *
15
+ * 1. the reservation is still exactly its own intact marker — otherwise the
16
+ * bytes are somebody's work and this is not a recovery;
17
+ * 2. a revocation is SEALED, durably and irrevocably, scoped to that one claim;
18
+ * 3. and only then is the slot released.
19
+ *
20
+ * Step 2 before step 3 is not a preference. Once the file is gone the correlative
21
+ * is eligible again, so a late sealed proposal from the dead owner — one that was
22
+ * approved before the recovery and lands after it — would write a document onto a
23
+ * number somebody else may already hold. The fence is what makes that publication
24
+ * fail instead of collide, and a fence written after the release is a window. If
25
+ * the seal cannot be written, the recovery FAILS and releases nothing: a released
26
+ * slot with no fence is the one outcome this must never produce.
27
+ *
28
+ * The revocation belongs to the CLAIM, never to the session. The owner may be
29
+ * alive and working on other things, and revoking its whole session to reclaim
30
+ * one number would destroy work to tidy up a correlative.
31
+ */
32
+ import { join } from "node:path";
33
+ import { leadingCorrelative } from "../domain/correlative.js";
34
+ import { reservationOwnerOf } from "../domain/reservation.js";
35
+ import { appendClaimEvent, claimKey, isRevoked, openOwnerOfSlot, readClaimEvents, wasPublished, } from "./claims-ledger.js";
36
+ import { withCwdLock } from "./lock-service.js";
37
+ import { semanticDigest } from "./semantic-operation/protocol.js";
38
+ import { CLOSED_MARKER, listSessionFolders } from "./session-resolver.js";
39
+ /** The identity a reservation's slot maps to, or `null` for an unattributable one. */
40
+ function claimOfSlot(slot) {
41
+ if (slot.owner === null)
42
+ return null;
43
+ return {
44
+ category: slot.category,
45
+ correlative: slot.correlative,
46
+ name: slot.name,
47
+ owner: slot.owner,
48
+ };
49
+ }
50
+ /**
51
+ * The slot a numbered file is, or `null` when it is a document.
52
+ *
53
+ * The LEDGER decides before the bytes do, and the order is the fix for a real
54
+ * defect: classifying from bytes alone made every empty numbered file an
55
+ * ownerless legacy placeholder — releasable with no fence — including a published
56
+ * document whose content happens to be empty, and including a reservation whose
57
+ * marker somebody emptied. In the first case a recovery deleted a published
58
+ * document; in the second it gave a correlative back while its real owner still
59
+ * held an open claim on it. Both times the record held the answer and only the
60
+ * bytes were consulted.
61
+ */
62
+ function slotOf(category, fileName, text, events, activeOwners) {
63
+ const correlative = leadingCorrelative(fileName);
64
+ if (correlative === null)
65
+ return null;
66
+ const name = fileName.slice(correlative.length + 1);
67
+ const slot = { category, correlative, name };
68
+ // Published is terminal and terminal means "a document lives here": whatever
69
+ // its bytes look like, it is not a slot anybody may free.
70
+ if (wasPublished(events, slot))
71
+ return null;
72
+ const markerOwner = reservationOwnerOf(text);
73
+ const base = { path: `docs/${category}/${fileName}`, category, correlative, name };
74
+ if (markerOwner !== null) {
75
+ return {
76
+ ...base,
77
+ kind: "reservation",
78
+ owner: markerOwner,
79
+ ownerActive: activeOwners.has(markerOwner),
80
+ revoked: false,
81
+ intact: true,
82
+ };
83
+ }
84
+ // Real content and no marker: a document, by its bytes this time.
85
+ if (text.trim().length > 0)
86
+ return null;
87
+ // Empty. If the ledger still holds an open claim on this slot, it is that
88
+ // owner's reservation with damaged bytes — not nobody's placeholder.
89
+ const open = openOwnerOfSlot(events, slot);
90
+ if (open !== null) {
91
+ return {
92
+ ...base,
93
+ kind: "reservation",
94
+ owner: open.owner,
95
+ ownerActive: activeOwners.has(open.owner),
96
+ revoked: false,
97
+ intact: false,
98
+ };
99
+ }
100
+ return {
101
+ ...base,
102
+ kind: "legacy-placeholder",
103
+ owner: null,
104
+ ownerActive: null,
105
+ revoked: false,
106
+ intact: false,
107
+ };
108
+ }
109
+ /**
110
+ * Walk `docs/` into `into`, throwing on the first unreadable thing.
111
+ *
112
+ * The accumulator is the caller's so a failure still reports what it managed to
113
+ * see: an unreadable category is a different fact from an empty one, and folding
114
+ * the two would let the board say "no reservations" about a directory nobody
115
+ * could open.
116
+ */
117
+ async function walkSlots(fs, docs, events, activeOwners, into) {
118
+ for (const category of await fs.list(docs)) {
119
+ if (category.type !== "dir")
120
+ continue;
121
+ for (const entry of await fs.list(category.path)) {
122
+ // The filename decides whether the bytes are worth reading at all. Reading
123
+ // every file in every docs/ subdirectory to then discard most of them on the
124
+ // first line of `slotOf` made a scan that runs on EVERY board projection pay
125
+ // for the whole corpus.
126
+ if (entry.type !== "file" || leadingCorrelative(entry.name) === null)
127
+ continue;
128
+ const slot = slotOf(category.name, entry.name, await fs.readText(entry.path), events, activeOwners);
129
+ if (slot === null)
130
+ continue;
131
+ const claim = claimOfSlot(slot);
132
+ slot.revoked = claim !== null && isRevoked(events, claim);
133
+ into.push(slot);
134
+ }
135
+ }
136
+ }
137
+ /**
138
+ * The session folders that are still open.
139
+ *
140
+ * Read once per scan, because the sanctioned action for a slot depends on it: a
141
+ * reservation of a live session is that session's to finish or close, and only a
142
+ * slot nobody is finishing may be recovered.
143
+ */
144
+ async function activeSessionFolders(fs, paths) {
145
+ const active = new Set();
146
+ try {
147
+ for (const folder of await listSessionFolders(fs, paths.cwdSessionsDir())) {
148
+ if (await fs.exists(join(folder.path, CLOSED_MARKER)))
149
+ continue;
150
+ active.add(folder.name);
151
+ }
152
+ }
153
+ catch {
154
+ // An unreadable sessions dir means nobody can be proven alive. The fallback is
155
+ // the conservative one: with no owner known to be active, no slot is offered
156
+ // for recovery on the strength of liveness it could not check.
157
+ }
158
+ return active;
159
+ }
160
+ /**
161
+ * Every reservation and legacy placeholder under `docs/`.
162
+ *
163
+ * Walks each immediate subdirectory rather than a list of categories, for the
164
+ * same reason the close's sweep does: the claim mechanism is category-agnostic,
165
+ * and a hardcoded list is a second place to update the day something else claims
166
+ * a number.
167
+ *
168
+ * A failure comes back WITH its reason and with whatever was already seen. An
169
+ * unreadable `docs/` is not an empty one, and swallowing the difference would let
170
+ * the board answer "no reservations" about a directory nobody could open.
171
+ */
172
+ export async function scanSlots(fs, paths) {
173
+ const docs = join(paths.workspaceDir(), "docs");
174
+ const slots = [];
175
+ const ledger = await readClaimEvents(fs, paths);
176
+ const activeOwners = await activeSessionFolders(fs, paths);
177
+ const sorted = () => slots.sort((a, b) => a.path.localeCompare(b.path));
178
+ if (!(await fs.exists(docs)))
179
+ return { slots };
180
+ try {
181
+ await walkSlots(fs, docs, ledger.events, activeOwners, slots);
182
+ }
183
+ catch (error) {
184
+ return {
185
+ slots: sorted(),
186
+ error: `no se pudo revisar docs/ en busca de reservas: ${error instanceof Error ? error.message : String(error)}`,
187
+ };
188
+ }
189
+ return { slots: sorted() };
190
+ }
191
+ function sealRecovery(body) {
192
+ return { ...body, digest: semanticDigest(body) };
193
+ }
194
+ /** The proposal for one slot, or why there is none. */
195
+ export async function previewRecovery(fs, paths, target) {
196
+ const scan = await scanSlots(fs, paths);
197
+ const slot = scan.slots.find((candidate) => candidate.path === target);
198
+ if (slot === undefined) {
199
+ return {
200
+ error: `'${target}' no es una reserva ni un placeholder legacy de este workspace`,
201
+ action: scan.error ??
202
+ "corré 'aw claims' para ver los correlativos recuperables; un documento publicado no se recupera",
203
+ };
204
+ }
205
+ return {
206
+ proposal: sealRecovery({
207
+ version: 1,
208
+ target: slot.path,
209
+ kind: slot.kind,
210
+ claim: claimOfSlot(slot),
211
+ requires_no_producer_confirmation: !slot.intact,
212
+ resuming: slot.revoked,
213
+ }),
214
+ };
215
+ }
216
+ /**
217
+ * Seal the fence, then release — and never the other way round.
218
+ *
219
+ * The whole thing runs under the workspace lock, which is not decoration: the
220
+ * check that the slot is still what the preview saw, the seal and the removal are
221
+ * three separate awaits, and without the lock a sanctioned publication could pass
222
+ * its own fence, take the lock, write its document and have this function delete
223
+ * it a moment later — leaving the correlative recorded as both spent-forever and
224
+ * eligible-again, with the published document silently gone. Its two siblings hold
225
+ * the lock across exactly this span for exactly this reason.
226
+ *
227
+ * It also re-derives its own proposal inside the lock instead of trusting the
228
+ * digest it was handed: the approval proves a person authorized THIS recovery, not
229
+ * that the world still looks the way it did when they read it.
230
+ */
231
+ export async function applyRecovery(fs, paths, input) {
232
+ const outcome = await withCwdLock(fs, paths, () => recoverUnderLock(fs, paths, input));
233
+ if ("error" in outcome && "action" in outcome)
234
+ return outcome;
235
+ if ("error" in outcome) {
236
+ return {
237
+ error: `no se pudo tomar el candado del workspace: ${outcome.error}`,
238
+ action: "esperá a que otro flujo lo libere y volvé a aplicar la recuperación",
239
+ };
240
+ }
241
+ return outcome;
242
+ }
243
+ async function recoverUnderLock(fs, paths, input) {
244
+ const preview = await previewRecovery(fs, paths, input.target);
245
+ if ("error" in preview)
246
+ return preview;
247
+ const proposal = preview.proposal;
248
+ if (proposal.digest !== input.approval) {
249
+ return {
250
+ error: "la aprobación no corresponde a la recuperación vigente",
251
+ action: `volvé a mirarla con 'aw claims recover ${input.target}' y aprobá el digest que devuelve`,
252
+ };
253
+ }
254
+ if (proposal.requires_no_producer_confirmation && input.noProducerConfirmed !== true) {
255
+ return {
256
+ error: proposal.kind === "legacy-placeholder"
257
+ ? `'${input.target}' es un placeholder legacy ambiguo: sus bytes no prueban que nadie vaya a escribirlo`
258
+ : `'${input.target}' ya no tiene el marcador intacto de su dueño: alguien escribió ahí`,
259
+ action: "confirmá explícitamente que no queda productor capaz de escribir ese destino antes de liberar su correlativo",
260
+ };
261
+ }
262
+ const claim = proposal.claim;
263
+ if (claim !== null && !proposal.resuming) {
264
+ // The fence FIRST. If this throws, nothing was released and the slot is
265
+ // exactly as it was: the recovery failed, which is the correct outcome.
266
+ // Releasing first would leave a window where the correlative is eligible and
267
+ // a late sealed publication can still land on it.
268
+ await appendClaimEvent(fs, paths, {
269
+ at: new Date().toISOString(),
270
+ event: "revoked",
271
+ claim,
272
+ cause: `aw claims recover: recuperación autorizada de ${proposal.target}`,
273
+ });
274
+ }
275
+ // Recorded before the file goes, same reason as the close's release: a slot
276
+ // freed with no line saying so is the state this ledger exists to end. The
277
+ // identity is the SLOT's, so it joins to its own history by claimKey — an
278
+ // ownerless placeholder still records the category, correlative and name it
279
+ // gave back, and only its owner field says nobody held it.
280
+ await appendClaimEvent(fs, paths, {
281
+ at: new Date().toISOString(),
282
+ event: "released",
283
+ claim: claim ?? {
284
+ category: slotIdentityOf(proposal.target).category,
285
+ correlative: slotIdentityOf(proposal.target).correlative,
286
+ name: slotIdentityOf(proposal.target).name,
287
+ owner: LEGACY_OWNERLESS,
288
+ },
289
+ cause: claim === null
290
+ ? "aw claims recover: placeholder legacy liberado con confirmación explícita de que no queda productor"
291
+ : `aw claims recover: liberado tras revocar ${claimKey(claim)}`,
292
+ });
293
+ await fs.remove(join(paths.workspaceDir(), proposal.target));
294
+ return {
295
+ applied: {
296
+ target: proposal.target,
297
+ revoked: claim,
298
+ released: true,
299
+ resumed: proposal.resuming,
300
+ digest: proposal.digest,
301
+ },
302
+ };
303
+ }
304
+ /** The owner field of a slot that never had one. Never a session folder. */
305
+ const LEGACY_OWNERLESS = "(placeholder legacy sin dueño)";
306
+ /** `docs/<cat>/<NNN>-<name>` split the same way every ledger record splits it. */
307
+ function slotIdentityOf(target) {
308
+ const parts = target.split("/");
309
+ const category = parts[1] ?? "";
310
+ const file = parts[2] ?? "";
311
+ const correlative = leadingCorrelative(file) ?? "";
312
+ return {
313
+ category,
314
+ correlative,
315
+ name: correlative.length > 0 ? file.slice(correlative.length + 1) : file,
316
+ };
317
+ }
318
+ /**
319
+ * The one action this slot sanctions, named per slot.
320
+ *
321
+ * A reservation of a live session is resumed or closed by its owner; only a slot
322
+ * nobody is finishing gets recovered. And a slot whose bytes are not its own
323
+ * intact marker carries the confirmation flag in the command itself, so the
324
+ * operator sees the extra assertion being asked of them before they type it.
325
+ */
326
+ export function sanctionedActionFor(slot) {
327
+ // A live owner's reservation is NOT a recovery candidate. Naming the recovery
328
+ // here was destructive: the board handed the running session the one command
329
+ // that revokes its own slot irrevocably, and the flow reads this very field as
330
+ // "the sanctioned next command". Closing the owner releases an intact
331
+ // reservation as part of closing, which is the action that actually resolves it.
332
+ if (slot.ownerActive === true && slot.owner !== null) {
333
+ return `aw session-close --code ${slot.owner}`;
334
+ }
335
+ const confirm = slot.intact ? "" : " --confirm-no-producer";
336
+ return `aw claims recover ${slot.path}${confirm}`;
337
+ }
338
+ //# sourceMappingURL=claims-recovery.js.map