@ybotok/loam 0.0.0 → 0.1.0-beta.1

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 (106) hide show
  1. package/CHANGELOG.md +89 -0
  2. package/COMPARISON.md +63 -0
  3. package/LICENSE +21 -0
  4. package/MIGRATING-from-OpenSpec.md +202 -0
  5. package/README.md +241 -9
  6. package/SCHEMA.md +425 -0
  7. package/SECURITY.md +9 -0
  8. package/dist/cli.js +138 -0
  9. package/dist/cli.js.map +1 -0
  10. package/dist/commands/adopt.js +226 -0
  11. package/dist/commands/adopt.js.map +1 -0
  12. package/dist/commands/archive.js +1297 -0
  13. package/dist/commands/archive.js.map +1 -0
  14. package/dist/commands/delta.js +361 -0
  15. package/dist/commands/delta.js.map +1 -0
  16. package/dist/commands/dependencies.js +78 -0
  17. package/dist/commands/dependencies.js.map +1 -0
  18. package/dist/commands/doctor.js +85 -0
  19. package/dist/commands/doctor.js.map +1 -0
  20. package/dist/commands/gherkin.js +270 -0
  21. package/dist/commands/gherkin.js.map +1 -0
  22. package/dist/commands/init.js +250 -0
  23. package/dist/commands/init.js.map +1 -0
  24. package/dist/commands/list.js +361 -0
  25. package/dist/commands/list.js.map +1 -0
  26. package/dist/commands/migrate-openspec.js +933 -0
  27. package/dist/commands/migrate-openspec.js.map +1 -0
  28. package/dist/commands/new.js +412 -0
  29. package/dist/commands/new.js.map +1 -0
  30. package/dist/commands/rebase.js +358 -0
  31. package/dist/commands/rebase.js.map +1 -0
  32. package/dist/commands/show.js +325 -0
  33. package/dist/commands/show.js.map +1 -0
  34. package/dist/commands/status.js +270 -0
  35. package/dist/commands/status.js.map +1 -0
  36. package/dist/commands/unarchive.js +310 -0
  37. package/dist/commands/unarchive.js.map +1 -0
  38. package/dist/commands/validate.js +1609 -0
  39. package/dist/commands/validate.js.map +1 -0
  40. package/dist/commands/verify.js +759 -0
  41. package/dist/commands/verify.js.map +1 -0
  42. package/dist/commands/vouch.js +325 -0
  43. package/dist/commands/vouch.js.map +1 -0
  44. package/dist/core/agent.js +1823 -0
  45. package/dist/core/agent.js.map +1 -0
  46. package/dist/core/agents-stamp.js +68 -0
  47. package/dist/core/agents-stamp.js.map +1 -0
  48. package/dist/core/arch.js +104 -0
  49. package/dist/core/arch.js.map +1 -0
  50. package/dist/core/brief.js +733 -0
  51. package/dist/core/brief.js.map +1 -0
  52. package/dist/core/coherence.js +553 -0
  53. package/dist/core/coherence.js.map +1 -0
  54. package/dist/core/config.js +172 -0
  55. package/dist/core/config.js.map +1 -0
  56. package/dist/core/delta.js +457 -0
  57. package/dist/core/delta.js.map +1 -0
  58. package/dist/core/dependencies.js +352 -0
  59. package/dist/core/dependencies.js.map +1 -0
  60. package/dist/core/docs.js +143 -0
  61. package/dist/core/docs.js.map +1 -0
  62. package/dist/core/doctor.js +544 -0
  63. package/dist/core/doctor.js.map +1 -0
  64. package/dist/core/fleet-context.js +272 -0
  65. package/dist/core/fleet-context.js.map +1 -0
  66. package/dist/core/frontmatter.js +203 -0
  67. package/dist/core/frontmatter.js.map +1 -0
  68. package/dist/core/gherkin.js +521 -0
  69. package/dist/core/gherkin.js.map +1 -0
  70. package/dist/core/health.js +71 -0
  71. package/dist/core/health.js.map +1 -0
  72. package/dist/core/ids.js +93 -0
  73. package/dist/core/ids.js.map +1 -0
  74. package/dist/core/issue.js +9 -0
  75. package/dist/core/issue.js.map +1 -0
  76. package/dist/core/json.js +67 -0
  77. package/dist/core/json.js.map +1 -0
  78. package/dist/core/likec4.js +474 -0
  79. package/dist/core/likec4.js.map +1 -0
  80. package/dist/core/openapi-merge.js +519 -0
  81. package/dist/core/openapi-merge.js.map +1 -0
  82. package/dist/core/openapi.js +243 -0
  83. package/dist/core/openapi.js.map +1 -0
  84. package/dist/core/openspec-inventory.js +1403 -0
  85. package/dist/core/openspec-inventory.js.map +1 -0
  86. package/dist/core/path-safety.js +109 -0
  87. package/dist/core/path-safety.js.map +1 -0
  88. package/dist/core/provenance.js +695 -0
  89. package/dist/core/provenance.js.map +1 -0
  90. package/dist/core/repo.js +362 -0
  91. package/dist/core/repo.js.map +1 -0
  92. package/dist/core/report.js +38 -0
  93. package/dist/core/report.js.map +1 -0
  94. package/dist/core/results.js +201 -0
  95. package/dist/core/results.js.map +1 -0
  96. package/dist/core/spec.js +481 -0
  97. package/dist/core/spec.js.map +1 -0
  98. package/dist/core/staging.js +783 -0
  99. package/dist/core/staging.js.map +1 -0
  100. package/dist/core/status.js +1015 -0
  101. package/dist/core/status.js.map +1 -0
  102. package/dist/core/verify.js +793 -0
  103. package/dist/core/verify.js.map +1 -0
  104. package/dist/core/version.js +8 -0
  105. package/dist/core/version.js.map +1 -0
  106. package/package.json +63 -4
@@ -0,0 +1,1823 @@
1
+ /**
2
+ * The agent contract: what `loam init` lays down so a coding agent can run the
3
+ * cycle without being told it each time.
4
+ *
5
+ * AGENTS.md goes into the docs repo — it travels with the thing it describes.
6
+ * The command and skill files go into the repo `init` runs in, because that is
7
+ * where the agent is invoked — for whichever tools the repo shows signs of, via
8
+ * the AGENT_TOOLS registry below: one shared body, a per-tool path and wrapper.
9
+ * Neither is ever overwritten: they are starting points, and a team's edits to
10
+ * them outrank ours.
11
+ *
12
+ * Two deliveries, one body. A slash command has to be TYPED, so it only ever
13
+ * reaches an agent whose operator already knows loam exists. A skill is loaded
14
+ * by the model itself when the task matches its `description`, which is how the
15
+ * protocol reaches an agent that was never told about it. That difference is
16
+ * the whole reason both are written by default.
17
+ *
18
+ * The stamp on the first line is the one concession to that never-refresh
19
+ * contract: it records which loam wrote the file, so `loam validate --all` can
20
+ * say when the tables below describe a binary that no longer exists
21
+ * (`agents.stale` — detection only, never a rewrite; agents-stamp.ts).
22
+ *
23
+ * The per-tool command and skill files carry the SAME stamp, for the same
24
+ * reason and with the same answer: `loam doctor` reports `doctor.agent-files-stale`
25
+ * and never rewrites. Without it, absence was the only drift loam could see —
26
+ * a command file mangled to one line left doctor saying `healthy: true`, which
27
+ * across a hundred repositories is invisible rot with no repair path.
28
+ */
29
+ import { existsSync } from "node:fs";
30
+ import { mkdir, writeFile } from "node:fs/promises";
31
+ import { dirname, join } from "node:path";
32
+ import { agentsStampLine } from "./agents-stamp.js";
33
+ import { LOAM_VERSION } from "./version.js";
34
+ export const AGENTS_MD = `${agentsStampLine(LOAM_VERSION)}
35
+ # Working in this docs repo
36
+
37
+ This is a **loam** docs repo: the shared source of truth for a fleet of services —
38
+ their architecture, their contracts, and what they are required to do. Everything
39
+ here is plain files. \`loam\` reads and writes them; delete \`loam\` and the docs remain.
40
+
41
+ ## Layout
42
+
43
+ \`\`\`
44
+ architecture/landscape.likec4 the living C4 model of the whole fleet
45
+ services/<svc>/
46
+ model.likec4 this service's C4
47
+ spec.md its living requirements (current state)
48
+ arch.spec.md its living ARCHITECTURE requirements (outbox, retries, alerts)
49
+ openapi.yaml its API contract
50
+ adrs/ runbook.md health.yaml why it is like this, how to run it
51
+ features/<FEAT>/ a change in flight
52
+ intent.md why, in business terms
53
+ delta.likec4 the architecture change, tagged #<FEAT>
54
+ specs/<svc>/spec.md the requirement change for one service
55
+ specs/<svc>/arch.spec.md the architectural requirement change, same delta grammar
56
+ specs/<svc>/openapi.yaml the endpoints this feature adds
57
+ verification.yaml what was checked once the code was built
58
+ features/archive/<FEAT>/ shipped changes — the evolution history
59
+ \`\`\`
60
+
61
+ ## \`loam.json\` — the wiring, in every repo
62
+
63
+ A fleet is **N + 1 repositories**: this docs repo, and one per service. Every one
64
+ of them carries its own \`loam.json\`, and every one of them commits it — that
65
+ file is how any agent, human or CI runner standing in a directory finds the rest
66
+ of the fleet. There is no environment variable and no global config; if a
67
+ command cannot see a \`loam.json\` at or above the working directory, it refuses
68
+ with \`no-config\` rather than guessing.
69
+
70
+ \`\`\`json
71
+ { "docsDir": "../docs-repo", "service": "payment-service", "gherkinDir": "features" }
72
+ \`\`\`
73
+
74
+ - \`docsDir\` — where the docs repo is, **stored exactly as it was passed** and
75
+ resolved against the directory holding the \`loam.json\`. So \`../docs-repo\`
76
+ keeps working on every machine that checks the repos out side by side, which
77
+ is the point of committing the file. An absolute path is a warning
78
+ (\`doctor.docs-absolute\`): it resolves only on the machine that ran \`loam init\`.
79
+ The docs repo's own \`loam.json\` says \`"docsDir": "."\`.
80
+ - \`service\` — the canonical id of the service THIS repo contains, i.e. the
81
+ \`services/<id>/\` directory it is allowed to speak for. \`loam vouch\`,
82
+ \`loam gherkin\` and \`loam verify --service\` all bind to it; without it they
83
+ refuse (\`repository-unavailable\`) rather than write another service's
84
+ documents from a repo that is not that service's.
85
+ - \`gherkinDir\` — optional, default \`features\`: the directory \`loam gherkin\`
86
+ writes its own \`loam/\` subtree into.
87
+ - \`agentTools\` — optional: the agent tools \`loam init\` has written command and
88
+ skill files for here, accumulated across runs. Written by \`init\`, read by
89
+ \`loam doctor\`; nothing else depends on it, and a config without it loads.
90
+
91
+ **Step 0 of everything below, once per repo:**
92
+
93
+ \`\`\`sh
94
+ loam init --docs ../docs-repo --service payment-service # in EACH service repo
95
+ loam doctor # before you trust any of it
96
+ \`\`\`
97
+
98
+ \`loam init --docs <dir>\` **joins** an existing docs repo; creating a new one
99
+ takes \`--create\`, so a mistyped path cannot quietly scaffold a second source of
100
+ truth beside the real one. \`loam doctor\` is the read-only preflight: it reads
101
+ loam.json defensively, resolves \`docsDir\`, probes access, checks \`services/\` and
102
+ the fleet map, and reports the service binding — run it first when anything
103
+ behaves as though the fleet were empty.
104
+
105
+ **Living vs delta.** \`services/<svc>/spec.md\` is the complete current state.
106
+ \`features/<FEAT>/specs/<svc>/spec.md\` is a diff against it, reviewed as a diff, with
107
+ requirements grouped under \`## ADDED\`, \`## MODIFIED\` or \`## REMOVED Requirements\`.
108
+ \`loam archive\` folds the delta into the living state.
109
+
110
+ ## The architecture spec axis
111
+
112
+ The business spec will never mention the transactional outbox — that is
113
+ architecture. Retries, idempotency, metrics, alerts: real obligations no business
114
+ scenario states, and exactly where generated code cuts corners unless the
115
+ obligations are derived mechanically. They live in \`arch.spec.md\` — living and
116
+ delta, same grammar, same delta algebra, merged by \`loam archive\` through the
117
+ same code path (the two files are separate requirement namespaces).
118
+
119
+ Where a business requirement carries \`Operations:\`, an arch requirement carries
120
+ \`Covers:\` — the model objects its scenarios exercise, comma-separated:
121
+
122
+ \`\`\`
123
+ Covers: paymentService.db, paymentService -> kafka, alert:payment_5xx, sli:availability
124
+ \`\`\`
125
+
126
+ a C4 element (id, or the service a bound element stands for), an edge
127
+ (\`source -> target\`), or a health signal the service's health.yaml declares.
128
+ Every entry must resolve — \`covers.unknown\` (warn) is the typo guard, because a
129
+ mistyped entry silently costs the coverage it was written for.
130
+
131
+ Coverage is then derived, never trusted, as two warnings: a feature's NEW tagged
132
+ elements and edges want a covering requirement in its arch.spec.md deltas
133
+ (\`c4.uncovered\`), and every alert/SLI health.yaml declares wants one in the
134
+ living arch spec (\`health.uncovered\`). None of this gates \`loam archive\`;
135
+ \`--strict\` is the CI escalation. An absent arch.spec.md is not a finding —
136
+ partial adoption is supported — but write one for any service with real
137
+ architecture, or its obligations ship unchecked.
138
+
139
+ **Test levels, mapped once.** A business scenario is an acceptance test. An arch
140
+ scenario is an integration/ops test — the outbox relay under a dead broker, the
141
+ retry that stays idempotent, the alert rule that actually fires. \`api.exposes\`
142
+ is a contract test. Unit tests sit below spec granularity and are your TDD
143
+ concern, not the specs'.
144
+
145
+ ## The generated Gherkin suite
146
+
147
+ Scenarios are the acceptance criteria, and \`loam gherkin\` makes them
148
+ executable: real \`.feature\` files, written into the SERVICE'S repo — the one
149
+ loam command that writes there, because tests live with the code they gate.
150
+ Output lands in \`<gherkinDir>/loam/\` (\`gherkinDir\` is an optional loam.json
151
+ fact, default \`features\`), and that \`loam/\` subdirectory is loam's own
152
+ derived space: regeneration rewrites its scope and deletes its orphans, so
153
+ never hand-edit inside it — step definitions and hand-written features belong
154
+ outside it, and loam never touches a byte outside \`loam/\`.
155
+
156
+ - \`loam gherkin FEAT-101\` — the feature's ADDED/MODIFIED requirements for this
157
+ service, both spec axes. Files carry \`@FEAT-101\`; arch-axis files add
158
+ \`@architecture\` and are named \`arch--<slug>.feature\` — their scenarios are
159
+ integration/ops tests, not acceptance tests.
160
+ - \`loam gherkin --service <id>\` (no feature) — the full living suite from
161
+ spec.md + arch.spec.md: the regression skeleton a legacy service gets at
162
+ adoption.
163
+
164
+ One file per requirement (\`Feature:\` is the requirement name). Bullet lines
165
+ opening with Given/When/Then/And/But — the \`- **WHEN** ...\` convention
166
+ included — become steps; every other body line is kept as scenario
167
+ description. Each scenario is tagged \`@loam-digest-<16hex>\`: the same body
168
+ hash \`loam verify\` folds into its claim ids, riding into cucumber's JSON
169
+ report as a tag, so the suite, the claim and the report cannot quietly
170
+ disagree about what a scenario says. \`loam validate
171
+ --service <id>\`, run in the service repo, grades the suite by those digests
172
+ (\`gherkin.missing\` / \`gherkin.stale\` / \`gherkin.orphaned\`, all warn) — the
173
+ fix is always regeneration, never editing a generated file.
174
+
175
+ The flow, closed end to end: \`loam gherkin FEAT-101\` → write step definitions
176
+ (outside \`loam/\`) → run the suite with a JSON report
177
+ (\`cucumber-js --format json:report.json\`) → implement until green →
178
+ \`loam verify FEAT-101 --service <id> --results report.json [--record rest.json]\`,
179
+ run in that same service repo. The digest
180
+ tags ride through the runner into the report, so the \`scenario.tested\` claims
181
+ are answered by the green run itself — mechanically, never by anybody's word.
182
+ See "The done-check" for what \`--record\` still covers.
183
+
184
+ ## Frontmatter — who vouched for this, and from what
185
+
186
+ Every markdown artifact opens with a YAML block:
187
+
188
+ \`\`\`yaml
189
+ ---
190
+ service: payment-service # or feature: FEAT-101
191
+ status: verified # services: draft -> verified
192
+ # features: proposed -> in_progress -> built -> done
193
+ owner: payments-team
194
+ last_verified: 2026-07-31 # written by \`loam vouch\` — do not hand-edit
195
+ sources: # files/directories in the SERVICE'S repo this was written from — no globs
196
+ - src/main/java/com/shop/payment/
197
+ sources_digest: 6f1c0a… # written by \`loam vouch\` — do not hand-edit
198
+ content_digest: 9b2f41… # written by \`loam vouch\` — do not hand-edit
199
+ sources_files: | # written by \`loam vouch\` — do not hand-edit
200
+ 9b2f41… src/main/java/com/shop/payment/Api.java
201
+ ---
202
+ \`\`\`
203
+
204
+ \`sources\` matters more than it looks. Everything else loam checks is internal
205
+ consistency — and a corpus can agree with itself perfectly while describing nothing
206
+ that exists. \`sources\` is the one thing tying a document to code: \`loam validate\`,
207
+ run inside that service's repository, checks every listed path is still there.
208
+ Entries are literal files and directories only — a directory covers everything
209
+ beneath it. Glob patterns are refused loudly (\`loam vouch\` will not stamp them,
210
+ \`loam validate\` errors on them): a pattern dialect nobody can be sure of digests
211
+ a different file set than intended, corrupting the staleness signal silently.
212
+
213
+ \`sources_digest\` is what makes that tie say something over time: a hash of the
214
+ CONTENT of those files, taken when a human last vouched for the document. Every
215
+ later \`loam validate\` re-computes it, so it can tell a document nobody has checked
216
+ (\`sources.unvouched\`) from one that still matches the code (\`sources.current\`) from
217
+ one the code has moved out from under (\`sources.stale\`).
218
+
219
+ \`content_digest\` is the same promise about the document itself: a hash of the
220
+ body below the frontmatter, stamped by the same vouch. It closes the other half of
221
+ the forgery — editing a spec after it was vouched used to leave \`status: verified\`
222
+ standing over words nobody read. \`loam validate\` recomputes it wherever it can
223
+ read the doc (no service repo needed, so it fires from the docs repo too) and
224
+ reports \`content.stale\` (warn): only a person can say whether verified still
225
+ holds of the new words.
226
+
227
+ Write \`sources\` for anything you author from reading code, and leave \`status: draft\`
228
+ until a human has read it. Promoting draft to verified is their call, not yours, and
229
+ it has its own command — \`loam vouch --service <id>\`, run inside that service's repo,
230
+ which stamps the status, the date, both digests and the per-file index
231
+ (\`sources_files\`, which is what lets a later \`sources.stale\` name the paths that
232
+ moved) together — into the living spec.md
233
+ and, when the service has one, arch.spec.md, in one all-or-nothing run: a file whose
234
+ sources cannot be verified refuses the whole vouch. Never write those five fields by
235
+ hand: a status with no digest behind it is a claim with nothing behind it. If the
236
+ document changed under vouch between the read and the write, it refuses with
237
+ \`vouch-raced\` and stamps nothing — re-read and re-run.
238
+
239
+ ## The ID spine
240
+
241
+ Three artifacts describe the same call in three languages. They are joined by the
242
+ **operationId**, and that join is what \`loam validate\` checks:
243
+
244
+ - architecture — a C4 edge names the operation it calls:
245
+ \`checkoutWeb -> paymentService 'Calls authorizePayment' { metadata { op 'authorizePayment' } }\`
246
+ - behaviour — a requirement declares the operations it governs:
247
+ \`Operations: authorizePayment\`
248
+ - contract — the provider's \`openapi.yaml\` defines it:
249
+ \`operationId: authorizePayment\`
250
+
251
+ Spell it identically in all three places. A mismatch is an error, not a warning:
252
+ an edge calling an operation nobody defines is a broken contract between services.
253
+
254
+ Features are joined the same way: a delta's new elements and edges carry the
255
+ feature id as a LikeC4 tag (\`#FEAT-101\`), and that tag is exactly what
256
+ \`loam archive\` folds into the living landscape. Untagged elements in a delta are
257
+ context for the diagram, not changes.
258
+
259
+ ## The requirement baseline — \`Based-On:\`
260
+
261
+ A MODIFIED requirement carries its FULL new text, not a diff, so archive does not
262
+ merge your wording into the living one — it REPLACES it. When two features rewrite
263
+ the same requirement, the loser loses everything they wrote, scenarios and all.
264
+
265
+ \`loam validate\` names the other feature while both are in flight
266
+ (\`delta.modified-conflict\`), but that warning cannot see the case that actually
267
+ happens: the first feature archives, stops being an active feature, the second
268
+ revalidates GREEN, and its archive lands text written against a document that no
269
+ longer exists — \`+0 ~1 -0\`, exit 0, nobody told.
270
+
271
+ So a MODIFIED or REMOVED requirement pins the living version it was written
272
+ against, directly under its identity:
273
+
274
+ \`\`\`markdown
275
+ ### Requirement: Cancel an order
276
+ Requirement-ID: REQ-042
277
+ Based-On: 3bcc7a40f2b23ff3
278
+ \`\`\`
279
+
280
+ You do not compute that value — \`loam rebase FEAT-101\` writes it, for every
281
+ MODIFIED/REMOVED requirement in the feature, from the living text as it stands
282
+ right now. Run it when you finish authoring a delta, and again after you resolve
283
+ a collision. Archive refuses on a stale pin (\`delta.baseline-stale\`) and says
284
+ both digests.
285
+
286
+ **Restamping is not resolving.** A pin claims "I read this version". If
287
+ \`loam rebase\` reports a requirement as moved, re-read the living requirement and
288
+ fold in what you still mean BEFORE you ship — otherwise you have re-pinned to a
289
+ document you never read, and archive will merge your text over theirs with
290
+ loam's blessing. There is no automatic three-way merge and there will not be one:
291
+ requirement prose does not merge, people do.
292
+
293
+ A delta adopted from OpenSpec has no such line and gets a warning
294
+ (\`delta.baseline-missing\`), never a refusal — an entire migrated corpus that
295
+ cannot archive is not a safety property.
296
+
297
+ ### The same pin on the contract axis — and why it matters more there
298
+
299
+ A feature's \`openapi.yaml\` is a COMPLETE document, not a patch: you restate the
300
+ living contract around the slot you are changing, and the merge upserts every
301
+ operation the document spells. So the collision on this axis needs no overlap at
302
+ all. Two features touching one service, editing DIFFERENT operations, destroy
303
+ each other — the second to archive quietly pushes its authoring-time copy of the
304
+ operation it never meant to touch back over whatever landed in between.
305
+
306
+ The same marker fixes it, as a vendor extension beside \`x-loam-remove\`:
307
+
308
+ \`\`\`yaml
309
+ paths:
310
+ /orders/{id}/cancel:
311
+ post:
312
+ operationId: cancelOrder
313
+ x-loam-based-on: 86feabe621ed887d
314
+ \`\`\`
315
+
316
+ \`loam rebase\` writes one on EVERY operation in the delta, pinned to the living
317
+ version. That is what lets the merge tell a QUOTE from an EDIT:
318
+
319
+ - pin equals the operation's own content → you quoted it → **the merge skips it
320
+ entirely** and the living contract keeps whatever it holds. Archive prints
321
+ \`· quotes …\` so the plan never silently writes less than your delta spells.
322
+ - pin equals the living operation → you edited it, nobody else did → merged.
323
+ - pin matches neither → you edited it AND somebody landed a change to it →
324
+ \`openapi.baseline-stale\`, refused.
325
+
326
+ Not covered, deliberately, and worth knowing: path-level keys (\`parameters\`,
327
+ \`servers\`) and \`components/schemas\` are still upserted wholesale, so a restated
328
+ schema can still revert another feature's change to it —
329
+ \`openapi.path-item-modified\` and \`openapi.component-modified\` are your only
330
+ warning there. Removal markers carry no pin either; \`openapi.remove-target-mismatch\`
331
+ is what guards those.
332
+
333
+ ## Which element IS which service
334
+
335
+ An element says which service it is with \`metadata { service 'payment-service' }\`.
336
+ Without one, its **title** is used instead — which is what most of this repo relies
337
+ on, and also the trap: rename a box in a diagram and every check joining it to
338
+ \`services/<svc>/\` silently stops matching. Bind an element whenever its title is
339
+ not exactly the directory name, and prefer binding over renaming a directory.
340
+
341
+ \`services/\` is the list of services — there is no manifest, and none should be
342
+ added. \`loam validate --all\` compares that list to the landscape both ways: a
343
+ directory nothing draws is an error, and an element with no directory is a warning.
344
+ Systems that are not ours — kafka, a payment gateway — carry \`#external\`, which
345
+ says so once and stops the warning for good.
346
+
347
+ ## The cycle
348
+
349
+ 0. **Wire the repo** — \`loam init --docs <path-to-docs-repo> --service <id>\` in
350
+ each service repo, then \`loam doctor\`. Once per repository, before anything
351
+ else; see "\`loam.json\` — the wiring" above. Every step below runs in a repo
352
+ that has been through this one.
353
+ 1. **Adopt** — a service with no documentation at all starts at
354
+ \`loam adopt --service <id> --json\`, which briefs you on the baseline to write.
355
+ See "Adopting a service" below. Once per service, not once per feature.
356
+ 2. **Understand** — \`loam status --json\` first if you joined halfway or lost the
357
+ session: it says what is in flight and what the next step is, for the fleet or
358
+ for one feature (\`loam status FEAT-101 --json\`). Then \`loam list --json\` and
359
+ \`loam show <service> --json\`.
360
+ Never propose a change to a service you have not read.
361
+ 3. **Scaffold** — \`loam new FEAT-101 --title "..." --touches <existing> --new-service <new>\`.
362
+ The scaffold leaves a \`delta.likec4\` whose context elements are commented out —
363
+ a requirements-only feature should DELETE that file rather than ship an empty
364
+ one — and gives every \`--new-service\` a \`specs/<svc>/arch.spec.md\` alongside.
365
+ 4. **Author** the four files the scaffold left as TODO: intent, delta.likec4,
366
+ a spec.md per service, an openapi.yaml per new service. Finish with
367
+ \`loam rebase FEAT-101\`: it pins every MODIFIED/REMOVED requirement and every
368
+ operation in the contract delta to the living version you wrote it against.
369
+ On the contract axis run it even when you changed nothing but one endpoint —
370
+ that is exactly what marks the REST of the document as quotation the merge
371
+ must not write. See "The requirement baseline".
372
+ 5. **Check** — \`loam validate FEAT-101 --json\`. Fix every error before writing code.
373
+ \`loam dependencies --json\` says whether another feature in flight has to
374
+ archive first.
375
+ 6. **Build** — \`loam delta FEAT-101 --service <svc> --json\` is the task for one service:
376
+ intent, its requirement delta with scenarios verbatim, and the edges around it.
377
+ In the service's own repo, \`loam gherkin FEAT-101\` then emits those scenarios as
378
+ \`.feature\` files under \`<gherkinDir>/loam/\` — write step definitions against them
379
+ **first**, outside \`loam/\`, and implement until they pass.
380
+ If \`architecture.errors\` is non-empty the payload keeps \`ok: true\` (the command
381
+ ran) but the exit code is 1: the C4 slice is empty because delta.likec4 did not
382
+ parse, not because the feature changes no architecture — do not build on it.
383
+ 7. **Verify** — \`loam verify FEAT-101 --json\` turns the feature's own promises into a
384
+ checklist. The scenario claims are answered by the cucumber report
385
+ (\`--results report.json\`); answer the rest with evidence and record them
386
+ (\`--record answers.json --service <id>\`), **from each affected service's own
387
+ repo**. See "The done-check".
388
+ 8. **Ship** — \`loam archive FEAT-101\` once the code is merged. \`--dry-run\` shows
389
+ every file the merge would write, and writes none of them.
390
+
391
+ ## Adopting a service
392
+
393
+ \`loam adopt --service <id>\` does not read code and never will. Nothing
394
+ deterministic can look at a legacy service and say what its architecture MEANS,
395
+ and a guessed model is worse than none: everybody downstream has to re-derive it
396
+ to know whether to believe it. So loam takes the half of the job that is
397
+ mechanical — stating the work, and checking the result — and you do the reading.
398
+
399
+ The brief names every file to write, the grammar each must be in, what the
400
+ landscape already says about the service (bind to those elements, do not draw a
401
+ parallel fleet), the frontmatter, and the checks that follow. It also lists what
402
+ **no** check will ever catch. Read that list: everything on it is a way to be
403
+ wrong quietly, and \`loam validate\` passing means nothing about any of it.
404
+
405
+ One of the targets is not under \`services/<id>/\`: \`architecture/landscape.likec4\`,
406
+ the fleet map, carried with \`action: "edit"\` and a \`landscape.instruction\`
407
+ spelling out the block this service owes it. That target appears only while
408
+ nothing in the map resolves to the service. **Add to that file; never rewrite
409
+ it** — every other service's elements and edges are in there. A service that is
410
+ fully documented and undrawn is \`landscape.service-unmodelled\` (error) and is
411
+ invisible to every cross-service check: no edge into it can be graded against
412
+ its openapi.yaml, and no feature can draw a call to it.
413
+
414
+ An adoption ends with two runs, not one: \`loam validate --service <id> --json\`
415
+ for the baseline, then \`loam validate --all --json\` in the docs repo — the
416
+ fleet cross-check the first invocation does not perform is exactly where a
417
+ landscape edit that did not land shows up.
418
+
419
+ Two rules the brief repeats and this file will too. An artifact that already
420
+ exists is reported as \`action: "diff"\` — read it and report what disagrees;
421
+ never replace it, because a document somebody wrote is evidence. And everything
422
+ you write is \`status: draft\`: promotion is \`loam vouch\`, run by a person in
423
+ the service's own repository.
424
+
425
+ ## The done-check
426
+
427
+ \`loam verify <FEAT>\` derives a checklist from the feature's own artifacts —
428
+ one claim per new service, per operation the delta adds, per tagged edge that
429
+ names an operation, and per scenario of every changed requirement, arch.spec.md
430
+ deltas included (those claims name their file, and the test they ask for is an
431
+ integration/ops test, not an acceptance test). Each claim has a stable id, so
432
+ two runs are diffable and an answer cannot drift onto a different question.
433
+
434
+ Two answer channels, and they never overlap. The \`scenario.tested\` claims are
435
+ the TEST RUNNER's to answer: run the generated suite with a cucumber JSON
436
+ report (\`cucumber-js --format json:report.json\`) and pass it back —
437
+ \`loam verify <FEAT> --results report.json\`. A claim is confirmed only when a
438
+ report scenario carrying its \`@loam-digest-<16hex>\` tag ran at least one step
439
+ and every step passed — every occurrence, when the report holds a re-run, and
440
+ every before/after hook with it. A failed, undefined, pending or skipped step,
441
+ a failed hook, or no matching scenario at all, is \`unconfirmed\` with the
442
+ reason. The digest is the only identity: a reworded spec scenario matches
443
+ nothing until the suite is regenerated and re-run. When a report exists,
444
+ ALWAYS pass \`--results\` — under it the runner owns every scenario claim, and
445
+ an answers-file entry for one is refused.
446
+
447
+ \`--record\` without \`--results\` is the fallback for a service with no runnable
448
+ suite yet: there your word answers the scenario claims too. It still works, and
449
+ it costs something you must know about. The record says who answered
450
+ (\`answered_by: agent\`), and that answer changes the VERDICT: a feature whose
451
+ scenario claims rest on an agent's word is **attested**, not verified.
452
+ \`verify --json\` reports \`verdict: "attested"\` with \`verified: false\` and an
453
+ \`attested\` count, \`loam status\` marks it and adds a \`next.verify-attested\` step,
454
+ and the notice rides in \`notices[]\` under \`verify.scenario-attested\` (warn —
455
+ nothing gates on it, because a legacy service with no suite must still be able
456
+ to ship). Answer them mechanically the moment a suite runs:
457
+ \`loam verify <FEAT> --results <report.json>\`.
458
+
459
+ Two more codes belong to the record itself. \`verify.record-miscounted\` (error):
460
+ a \`verification.yaml\` whose \`summary\` block contradicts its own \`claims[]\` — the
461
+ record is refused as unreadable rather than believed in either half, and
462
+ re-recording is the repair, never editing the counts. \`verify.digest-contested\`
463
+ (warn): two or more services word a scenario identically, so they share one
464
+ \`@loam-digest-…\` tag and no single cucumber report can say whose suite ran it —
465
+ those scenario claims are left unconfirmed, and each service records its own
466
+ from its own repository with \`--service\`.
467
+
468
+ A consumed report is written down: \`report\` on the record (or on that
469
+ repository's attestation) carries the path, a sha256 of the bytes, the file's
470
+ mtime and how many tagged scenarios it held. That identifies WHICH file
471
+ answered the claims, and — when the file is committed — that it matches the
472
+ attested commit. It does not prove the file came from executing that commit; no
473
+ digest can.
474
+
475
+ Everything else you answer from the code, and \`--record answers.json\` takes
476
+ those back, writing \`features/<FEAT>/verification.yaml\` — it travels into the
477
+ archive with the feature and reads without loam. With \`--results\`, the answers
478
+ file must answer exactly the non-scenario claims: an entry for a scenario
479
+ claim is refused (\`answers-mismatch\` — the runner owns it), and \`--results\`
480
+ alone is refused while non-scenario claims are outstanding. Every recorded
481
+ verdict says who answered it (\`answered_by: runner | agent\`), so a reviewer
482
+ can tell a green run from somebody's word.
483
+
484
+ **A cross-service feature is verified once per service, and the form matters.**
485
+ The claims of one feature belong to several services, and each service's code
486
+ lives in its own repository — so the recording form is federated:
487
+
488
+ \`\`\`sh
489
+ # in EACH affected service's own repo, not in the docs repo:
490
+ loam verify FEAT-101 --service payment-service --results report.json --record answers.json
491
+ \`\`\`
492
+
493
+ \`--service <id>\` narrows the checklist to that service's claims and binds the
494
+ answers to this repository's commit, producing one **attestation** per service
495
+ inside a single \`schema: 2\` \`verification.yaml\`. Re-running it in the next
496
+ repo adds that repo's attestation and leaves the others standing. Omitting
497
+ \`--service\` in a repo whose \`loam.json\` declares one substitutes it, so the
498
+ short form is safe there; a \`--service\` naming a service this repo does not
499
+ declare is refused (\`service-mismatch\`), and \`--service\` from a repo that
500
+ declares none is refused (\`repository-unavailable\`) — an attestation has to be
501
+ bound to the code it is about.
502
+
503
+ ⚠ **The legacy all-at-once form \`loam verify <FEAT> --record answers.json\` with
504
+ no \`--service\` writes the whole record from one place.** Over a record that
505
+ already carries other services' attestations it is refused outright
506
+ (\`record-federated\`, naming the attestors) rather than merged or migrated — the
507
+ alternative was one repo silently erasing nine others' evidence. Use it only for
508
+ a single-service feature that has never been recorded federated. A
509
+ \`verification.yaml\` that exists but cannot be read is \`record-unreadable\`: it is
510
+ never overwritten and never reported as "no record".
511
+
512
+ Reading is not writing: \`loam verify <FEAT> --service <id>\` **without**
513
+ \`--record\` is a pure lens on the checklist and works from anywhere, docs repo
514
+ included. Every run lists all subjects in \`services\`, and a recorded run reports
515
+ in \`discarded\` every previous answer the new record does not carry.
516
+
517
+ It refuses an answer set that does not answer the current checklist: an id that
518
+ is not on it (\`answers-mismatch\`), a claim with no answer (same), or a
519
+ \`confirmed\` with no evidence (\`answers-unevidenced\`). Evidence is \`file:line\`.
520
+ A claim you cannot show is \`unconfirmed\` with a note saying why — that is a
521
+ successful record, and it is more useful to the next reader than a yes.
522
+
523
+ **Nothing gates on it.** \`loam archive\` will ship an unverified feature. That
524
+ is deliberate: coherence is something loam computed from the documents, and a
525
+ verdict is your word about code loam never read. A gate in front of shipping
526
+ would only teach everyone that the cheapest way past it is to say yes. The
527
+ record is for the reviewer who comes later, so leave it true.
528
+
529
+ ## What you author, what loam derives
530
+
531
+ Authored by hand: \`intent.md\`, \`delta.likec4\`, \`specs/<svc>/spec.md\`,
532
+ \`specs/<svc>/arch.spec.md\`, \`specs/<svc>/openapi.yaml\`, ADRs, runbooks, health.
533
+
534
+ Written by loam: the merge into \`services/<svc>/spec.md\`, \`services/<svc>/arch.spec.md\`,
535
+ \`services/<svc>/openapi.yaml\` and \`architecture/landscape.likec4\` at archive time.
536
+
537
+ **Do not hand-edit the living landscape to add a feature's changes.** Put them in a
538
+ delta and archive it — otherwise the change has no intent, no requirement and no
539
+ history behind it.
540
+
541
+ ## Rules the validator enforces
542
+
543
+ - every requirement has at least one \`#### Scenario:\` — scenarios are the acceptance
544
+ criteria and the source for tests;
545
+ - every operationId a requirement governs exists in that service's OpenAPI;
546
+ - every C4 edge's \`op\` resolves to an operation the target actually exposes;
547
+ - every operation is governed by some requirement (warning);
548
+ - a "Calls" edge with no \`metadata { op }\` is unlinked (warning);
549
+ - **the map is complete**: every \`services/<svc>/\` has an element in the landscape,
550
+ and every element that looks like a service has a directory (warning) or is
551
+ tagged \`#external\`;
552
+ - **the diff applies**: a \`MODIFIED\` or \`REMOVED\` requirement exists in the living
553
+ spec, an \`ADDED\` one does not, and a section heading matches the grammar exactly;
554
+ - a \`Requirement-ID: <id>\` is optional, but when present it is valid and unique;
555
+ archive selects by it, so carry the same ID through \`MODIFIED\` to rename a heading;
556
+ - **every requirement in a delta is under a delta section**: one under a prose heading
557
+ (\`## Behavior\`) is documentation as far as the merge is concerned and is dropped
558
+ (warning). \`## Requirements\` is exempt — quoting the living state is legal.
559
+
560
+ That last pair of rules exists because each of its failures used to be silent.
561
+ \`MODIFIED\` of a requirement that does not exist merged as a creation; \`ADDED\` of one
562
+ that does exist REPLACED it, scenarios and all; a near-miss heading like
563
+ \`## ADDED Requirement\` (singular) parses as plain prose, so archive merged nothing and
564
+ said nothing; and a requirement under \`## Behavior\` vanished the same way.
565
+
566
+ Write the delta section heading on its own line exactly as \`## ADDED Requirements\`.
567
+ The heading is the only thing that gives a requirement its kind: without a matching
568
+ one, archive merges the requirement as nothing.
569
+
570
+ Severity and gating are two different questions. An ERROR means the document is
571
+ invalid — \`loam validate\` fails. A finding with \`gates: true\` means \`loam archive\`
572
+ will refuse — the merge is unsafe. They usually agree (errors gate, warnings do
573
+ not), and where they diverge the \`--json\` output says so: \`delta.requirement-not-merged\`
574
+ is a warning (the shape is legal OpenSpec) that gates (the merge would drop the
575
+ requirement). Leave a warning standing only deliberately.
576
+
577
+ ## Reading loam's output
578
+
579
+ Every command takes \`--json\`, and the envelope holds even when the INVOCATION
580
+ is wrong: with \`--json\` anywhere in the arguments, an unknown flag, unknown
581
+ command or missing argument still yields \`{ ok: false, error: { code:
582
+ "invalid-option" } }\` on stdout with exit 1 (commander's own diagnostic goes
583
+ to stderr; \`--help\` and \`--version\` output pass through untouched). Without
584
+ \`--json\` the same refusal is plain text on stderr with nothing on stdout.
585
+
586
+ Branch on \`findings[].code\`, never on the prose — the wording changes, the codes do
587
+ not. The code-by-code fix table lives in the \`/loam-check\` command \`loam init\` lays
588
+ down. \`init\` writes that body twice for every tool it configures: once as a slash
589
+ command in the tool's own command directory (you type it), and once as an Agent
590
+ Skill at \`<tool-dir>/skills/loam-check/SKILL.md\` (the model loads it by itself when
591
+ the task matches the skill's description). Same body, two ways in.
592
+
593
+ Which tools get them is detected from the repo: \`loam init\` scans for the
594
+ dot-directories of the tools it knows (\`.claude\`, \`.cursor\`, \`.gemini\`, …) and
595
+ writes for the ones it finds, falling back to Claude Code when it finds none.
596
+ \`loam init --tools <ids|all>\` overrides the scan — an unknown id is refused
597
+ (\`invalid-option\`) rather than skipped — and \`--no-commands\` / \`--no-skills\`
598
+ each suppress one delivery. The tools written for are recorded in
599
+ \`loam.json\` as \`agentTools\`, which is how \`loam doctor\` tells a file missing
600
+ because the binary grew a new command from one missing because nobody ever
601
+ selected that tool. The map of which invocation surfaces what:
602
+
603
+ - \`loam status [<FEAT>]\` is the orientation surface — the question you have when
604
+ you join a repository halfway, or come back having lost the session, and the one
605
+ every other command assumed you could already answer. It writes nothing and
606
+ stores nothing: there is no state file, every answer is re-derived from the
607
+ files, so a document someone edited in another window is visible on the next run
608
+ with nothing to invalidate. Artifacts come back as \`missing\` (owed, nothing in
609
+ the way — write it now), \`blocked\` (not written and not writable yet; the entry
610
+ names what comes first), \`draft\` (on disk, and the shared checks report an error
611
+ against it — what exists is wrong), \`ready\` (on disk and clean, but something
612
+ outside the documents — code, a test run, a recording — still has to answer it)
613
+ or \`done\`. The payload's reason to exist is \`next[]\`: ordered, first entry
614
+ first, each carrying a code and the literal command to run. \`next.recover-commit\`
615
+ outranks every other step in both forms and is never elided: a \`.loam-commit\`
616
+ journal says an archive or unarchive was killed mid-commit, so some of the files
617
+ everything below is derived from may be half-written. Its command is that exact
618
+ archive/unarchive re-run, which repairs from the pre-image first, under the lock
619
+ — except when the journal itself cannot be read, where it is \`loam doctor\` and
620
+ the repair is a human's comparison against version control. Fleet-wide the rest are
621
+ \`next.adopt\` (a service with no spec.md — nothing about it is written down, so
622
+ no feature can be graded against it), \`next.complete-service\` (a living spec.md
623
+ with no model.likec4 beside it), \`next.feature\` (something is in flight;
624
+ ask \`loam status <FEAT>\` about it), \`next.archive\` (authored and verified —
625
+ ship it, and everything waiting on it is released), \`next.fleet-clean\`
626
+ (nothing is owed), \`next.elided\` (the fleet list hit its cap and says how many
627
+ steps of the same kinds it left out — it is ordered most-unblocking first, so
628
+ work down it and re-run) and \`next.fleet-gate\` (always last while anything is
629
+ outstanding: \`loam validate --all\` is what CI runs, and the fleet form grades
630
+ nothing itself). On one feature: \`next.author-intent\`, \`next.touch-service\`
631
+ (no per-service delta at all yet), \`next.author-spec\`, \`next.author-openapi\`,
632
+ \`next.author-scenarios\`, \`next.rebase\` (requirements or operations with no
633
+ baseline pin — until they have one the merge cannot tell what it EDITS from what
634
+ it merely quotes), \`next.archive-first\` (another feature in flight has to land
635
+ before this one), \`next.fix-coherence\` (the three axes disagree and archive
636
+ refuses), \`next.generate-tests\` (a per-service delta carries scenarios no test
637
+ run has answered — \`loam gherkin <FEAT> --service <svc>\` in that service's own
638
+ repository), \`next.verify\` (the done-check has not been started — or its record
639
+ will not read, or it answers a checklist the feature has since moved out from
640
+ under, which is not the same as a finished one), \`next.verify-unconfirmed\`
641
+ (started, and claims are still open — close them from each affected service's
642
+ own repository), \`next.attest-service\` (this repo IS one of the services that
643
+ owes an answer, so the step is bound to this commit), \`next.verify-attested\`
644
+ (every claim is confirmed but a scenario rests on an agent's word rather than a
645
+ green run — see "The done-check"), \`next.archive\`, and \`next.archived\` for one
646
+ that shipped.
647
+ It grades nothing of its own: the verdict is the one \`loam validate --feature\`
648
+ and \`loam archive\` compute — status takes the UNION of what both of them refuse,
649
+ so it may be more pessimistic than either and can never be greener than both.
650
+ \`verification\` carries \`verdict\` (\`verified\` | \`attested\` | \`unverified\`)
651
+ and \`attested\` (how many scenario claims rest on an agent's word) beside the
652
+ recounted totals, and \`checks.issues[]\` carries \`gates\` and \`details\` on every
653
+ finding.
654
+ - \`loam validate --service <id>\` grades one service's own axes: \`service.unknown\`,
655
+ \`service.no-model\`, \`service.no-spec\`, \`service.no-openapi\`, \`c4.invalid\`,
656
+ \`requirements.missing-scenarios\`, \`requirements.stepless-scenario\`,
657
+ \`spec.merge-conflict\`, \`spec.duplicate-requirement\`,
658
+ \`spec.no-requirements\`, \`spec.repeated-operations\`, \`spec.repeated-covers\`,
659
+ \`openapi.invalid\`, \`openapi.duplicate-operationid\` (one operationId in two
660
+ (path, method) slots — every join on the id then picks one of them arbitrarily),
661
+ \`api.ungoverned\`, \`api.ops-unlinked\`,
662
+ \`api.requirement-deprecated\`, \`spec-api.op-undefined\` (the living spec's own
663
+ \`Operations:\` lines, not only a delta's), \`spine.landscape-invalid\`, \`spine.op-undefined\`,
664
+ \`spine.op-link-missing\`, \`spine.op-deprecated\`, and
665
+ the architecture spec axis: \`covers.unknown\`, \`health.invalid\`, \`health.uncovered\`. Run inside
666
+ the service's own repo, once a generated suite exists under
667
+ \`<gherkinDir>/loam/\`, it also grades that suite against the living specs:
668
+ \`gherkin.missing\`, \`gherkin.stale\`, \`gherkin.orphaned\` (all warn — the fix is
669
+ always regeneration, never editing a generated file). A service that never ran
670
+ \`loam gherkin\` stays quiet, and a file tagged with a feature still in flight
671
+ answers to that feature, not to the living spec it has not merged into yet.
672
+ - \`loam validate --feature <id>\` grades a change's three axes against each other and
673
+ against the fleet in flight: \`delta.invalid\`, \`delta.nothing-tagged\`,
674
+ \`delta.service-unknown\`,
675
+ \`spec-api.op-undefined\`, \`spec-api.op-pending\`, \`c4-api.op-undefined\`,
676
+ \`c4-api.op-pending\`, \`c4-api.op-deprecated\`, \`c4.op-ungoverned\`, \`c4.op-link-missing\`,
677
+ \`api.op-unconsumed\`, \`service.no-requirement-delta\`, \`archedge.uncovered\`,
678
+ \`spec.repeated-operations\` / \`spec.repeated-covers\` (on the feature's own
679
+ spec deltas — same silent-line-loss check as service scope),
680
+ \`spec.merge-conflict\` / \`requirements.stepless-scenario\` (same codes as
681
+ service scope, graded on the feature's spec.md and arch.spec.md deltas —
682
+ both breaches merge into the living document),
683
+ the architecture spec axis (\`c4.uncovered\`, plus \`covers.unknown\` on the
684
+ feature's arch.spec.md deltas), and
685
+ the delta-shape group: \`delta.unknown-section\`, \`delta.no-delta-sections\`,
686
+ \`delta.requirement-not-merged\`, \`delta.modified-unknown\`, \`delta.removed-unknown\`,
687
+ \`delta.added-duplicate\`, \`delta.added-near-duplicate\`, \`delta.modified-pending\`,
688
+ \`delta.removed-pending\`, \`delta.added-conflict\`, \`delta.modified-conflict\`,
689
+ \`delta.living-duplicate-requirement\`, and the API-removal group:
690
+ \`openapi.remove-marker-anonymous\`, \`openapi.remove-op-consumed\`.
691
+ Where a finding names another feature in flight (\`delta.*-pending\`,
692
+ \`delta.added-conflict\`, \`delta.modified-conflict\`, \`spec-api.op-pending\`,
693
+ \`c4-api.op-pending\`) the ORDER is the answer, and \`loam dependencies --json\`
694
+ is what computes it: \`order\` is a dependency-first sequence, \`conflicts\`
695
+ the pairs no ordering fixes, \`cycles\` the ones that need a human.
696
+ - \`loam validate --all\` runs both of those for everything and adds the fleet
697
+ cross-check: \`landscape.missing\`, \`landscape.invalid\`,
698
+ \`landscape.merge-conflict\`, \`landscape.service-unmodelled\`,
699
+ \`landscape.service-undocumented\`, \`landscape.binding-unknown\`,
700
+ \`landscape.binding-duplicate\`, \`service.id-invalid\` (a \`services/<id>/\`
701
+ directory no loam command can address — fleet scope only, and graded before
702
+ the map is opened, so it stands even when the landscape is missing or
703
+ unreadable), plus a per-service
704
+ \`sources.unverifiable-from-here\` (severity \`ok\`, one per service whose
705
+ \`sources\` only its own repo can check — it is a confirmation, not work, and
706
+ the fleet rollup line is derived from those findings), and one check on this very file — \`agents.stale\`
707
+ (warn) — when the version stamp on line 1 (\`<!-- generated by loam vX.Y.Z -->\`)
708
+ is missing or older than the running binary: the tables here may describe a
709
+ loam that no longer exists, so review this file against the current \`--help\`
710
+ and update the stamp line. A hand-curated file silences it the same way, by
711
+ keeping the stamp current. The file is never refreshed automatically — your
712
+ edits outrank the template, so detection is all loam does.
713
+ - \`loam doctor\` is read-only local/fleet preflight — the first thing to run in a
714
+ repo that behaves as though the fleet were empty, and every finding carries a
715
+ \`fix\`. Its blockers are
716
+ \`doctor.config-missing\`, \`doctor.config-invalid\`, \`doctor.docs-missing\`,
717
+ \`doctor.services-missing\`, and the three that say the fleet map cannot be read
718
+ at all — \`doctor.landscape-merge-conflict\` (it still holds \`<<<<<<<\` markers,
719
+ so it is two halves of two different maps), \`doctor.landscape-invalid\` (it does
720
+ not parse) and \`doctor.landscape-unreadable\` (it is there but could not be
721
+ read). It also grades what a WRITE that did not finish left in the docs repo,
722
+ reported as \`writePath\` beside the findings: \`doctor.docs-locked\` (a held
723
+ \`.loam-lock\` — a warning while its holder is alive, since the answer is to wait
724
+ and re-run, and a BLOCKER once the holder is a process that no longer exists on
725
+ this host, since nothing will ever release it and every archive and unarchive
726
+ refuses \`docs-busy\` until it is deleted), \`doctor.commit-interrupted\` (blocker:
727
+ a \`.loam-commit\` naming an archive or unarchive that was killed mid-commit, so
728
+ the living docs may be half-written — re-run that exact command and it recovers
729
+ first, under the lock), \`doctor.commit-unreadable\` (blocker, and the worst case:
730
+ the record of which files that commit had already written cannot be parsed, so
731
+ nothing can grade it — compare against version control by hand) and
732
+ \`doctor.staging-temps\` (warn: orphaned \`.loam-*.tmp\` scratch that was never
733
+ linked into place — litter, not damage). Accessibility, portability and
734
+ incomplete binding stay warnings:
735
+ \`doctor.docs-unreadable\`, \`doctor.docs-readonly\`, \`doctor.docs-absolute\`
736
+ (\`docsDir\` stored as an absolute path in a committed loam.json — it resolves
737
+ only on the machine that ran \`loam init\`), \`doctor.inventory-unreadable\`,
738
+ \`doctor.landscape-missing\`, \`doctor.service-unbound\`, \`doctor.service-unknown\`,
739
+ and the two about this repo's own generated command and skill files:
740
+ \`doctor.agent-files-missing\` — some of them are absent, because the repo was
741
+ initialized by an older binary or they were deleted — and
742
+ \`doctor.agent-files-stale\` — some that ARE here carry no
743
+ \`<!-- generated by loam vX.Y.Z -->\` stamp, or one older than the running
744
+ binary, so the protocol and code tables they instruct an agent with may
745
+ describe a loam that no longer exists. EXPECT the second one on the first
746
+ \`loam doctor\` after an upgrade, on a repo nobody has touched: stamping is
747
+ newer than the files, so nothing written by an earlier loam carries a stamp
748
+ at all, and every one of them reads as unstamped. That is the intended
749
+ reading — an unstamped file is one nobody has confirmed still describes this
750
+ binary — but it means the finding is not evidence that anything was edited or
751
+ broken.
752
+ For the first, run the command the finding's own \`fix\` field spells: it names
753
+ this repo's \`docsDir\`, its service binding and its tool ids explicitly, and
754
+ \`loam init\` writes only the command and skill files that are absent and never
755
+ touches one that already exists. (It does rewrite \`loam.json\` — but a re-run
756
+ with no \`--docs\` keeps the pointer the file already commits, so it cannot move
757
+ the fleet under you. Reach for \`--create\` only to make a docs repo that does
758
+ not exist yet; beside a working \`docsDir\` it scaffolds a second, empty source
759
+ of truth and \`validate --all\` then goes green over an empty fleet.)
760
+ For the second there is no command: the repair is by hand, one file at a
761
+ time. Read each file the finding names against the body this loam writes,
762
+ then set (or add) its stamp line to \`<!-- generated by loam vX.Y.Z -->\` for
763
+ the running version — or, if you have no local edits worth keeping, delete
764
+ that file and re-run \`loam init\`, which lays the current one down. Neither is
765
+ ever regenerated in place: your edits outrank the template, so detection is
766
+ all loam does, and the stamp is a human's claim that the file still means what
767
+ it says rather than a refresh. Leaving either finding standing costs entry
768
+ points and the accuracy of what the entry points say, nothing else.
769
+ - \`loam rebase <FEAT>\` writes the baseline pins: \`Based-On:\` under every
770
+ MODIFIED/REMOVED requirement in the feature's spec deltas, and
771
+ \`x-loam-based-on\` on every operation in its openapi.yaml, from the living text
772
+ as it stands right now. Run it when you finish authoring a delta and again
773
+ after you resolve a collision — it is the one mechanism standing between two
774
+ features in flight and silently reverting each other's landed work. It refuses
775
+ rather than inventing a pin, and re-pinning is not resolving: see
776
+ "The requirement baseline".
777
+ - \`loam dependencies [<FEAT>]\` derives the ordering of the features in flight
778
+ from the artifacts themselves — requirement identities and OpenAPI
779
+ operationIds, never from validator prose. It is the answer to every
780
+ "another feature in flight does this first" finding, and to
781
+ \`delta.added-conflict\` / \`delta.modified-conflict\`, which no ordering fixes.
782
+ - \`loam audit-openspec <root>\` is the read-only OpenSpec inventory; an explicit
783
+ \`--write-mapping\` writes a non-overwriting, digest-bound decision skeleton
784
+ outside the source tree. \`loam migrate-openspec <root> --map <file>\` validates
785
+ that mapping and stays dry-run unless both \`--apply\` and an empty \`--target\`
786
+ are supplied. Living/active shape diagnostics are \`openspec.workspace-empty\`
787
+ (the audited root holds no living spec and no active change, so there is
788
+ nothing to migrate and no verdict over it can be \`ready\`),
789
+ \`openspec.specs-missing\`,
790
+ \`openspec.config-invalid\`, \`openspec.external-store-pointer\`,
791
+ \`openspec.living-empty\`, \`openspec.living-requirements-outside-section\`,
792
+ \`openspec.living-delta-section\`, \`openspec.nonstandard-living-spec\` (markdown
793
+ under \`specs/\` named neither spec.md nor design.md, so no capability reads
794
+ it), \`openspec.change-metadata-invalid\`,
795
+ \`openspec.change-schema-unresolved\`, \`openspec.skip-specs-with-specs\`,
796
+ \`openspec.change-no-specs\`, \`openspec.change-empty\`,
797
+ \`openspec.change-without-delta-sections\`,
798
+ \`openspec.change-requirements-outside-delta-sections\`,
799
+ \`openspec.change-quoted-requirements\` (requirements under a \`## Requirements\`
800
+ heading INSIDE a change delta — the shape OpenSpec's own living-spec template
801
+ mandates, and one that stages nothing, so loam refuses it rather than counting
802
+ requirements it cannot route),
803
+ \`openspec.hidden-change-directory\` (a dot-prefixed directory under
804
+ \`changes/\` is not enumerated as a change, so nothing under it migrates —
805
+ rename it or move it out; under \`changes/archive/\` the same code is an archive
806
+ diagnostic and does not gate, because frozen history never blocks),
807
+ \`openspec.nonstandard-change-spec\`, \`openspec.renamed-malformed\`,
808
+ \`openspec.requirement-id-invalid\`, \`openspec.requirement-id-repeated\`,
809
+ \`openspec.requirement-id-duplicate\` (a stable \`Requirement-ID\` that is
810
+ malformed, declared twice, or shared by two requirements — identity is never
811
+ inferred from an invalid declaration, so the RENAMED chain cannot be followed
812
+ through it), \`openspec.symlink-unsupported\`, and \`openspec.non-utf8-artifact\`;
813
+ the same shapes in frozen history are non-blocking archive diagnostics.
814
+ Mapping refuses stale or incomplete decisions under \`mapping.source-missing\`,
815
+ \`mapping.source-root-mismatch\`, \`mapping.source-digest-mismatch\`,
816
+ \`mapping.unknown-capability\`, \`mapping.unknown-requirement\`,
817
+ \`mapping.requirement-allocation-missing\`,
818
+ \`mapping.requirement-service-unknown\`, \`mapping.service-allocation-empty\`,
819
+ \`mapping.unknown-change\`, \`mapping.change-title-missing\`,
820
+ \`mapping.feature-id-invalid\`, \`mapping.feature-id-duplicate\`,
821
+ \`mapping.unknown-rename\`, \`mapping.invalid-requirement-id\`,
822
+ \`mapping.rename-source-missing\`, \`mapping.rename-source-ambiguous\`,
823
+ \`mapping.rename-source-id-invalid\`, \`mapping.rename-target-conflict\`,
824
+ \`mapping.rename-existing-id-conflict\`, \`mapping.rename-id-conflict\`,
825
+ \`mapping.rename-double-source\`, \`mapping.rename-double-target\`,
826
+ \`mapping.rename-chain\`,
827
+ \`mapping.unknown-artifact\`, and \`mapping.invalid-artifact-disposition\`.
828
+ - Repository containment failures are explicit: \`sources.path-outside\` rejects a
829
+ provenance source outside the service repo and \`gherkin.path-outside\` rejects a
830
+ generated-suite target outside it. Federated verify recording refuses under
831
+ \`service-mismatch\`, \`unknown-service\`, or \`repository-unavailable\` when the
832
+ selected service, fleet identity, and repository commit cannot be bound safely,
833
+ and under \`record-federated\` when the legacy all-at-once form would erase other
834
+ repositories' attestations.
835
+ - A file loam cannot read is reported, never skipped: \`service.unreadable\` and
836
+ \`feature.unreadable\` (both errors) name the path and say that nothing about
837
+ that target was checked while the rest of the fleet still was. The whole docs
838
+ repo being unreachable is a refusal instead: \`docs-missing\` (\`docsDir\` points
839
+ at nothing) or \`services-missing\` (it is a directory with no \`services/\` —
840
+ usually the service repo after a typo in \`docsDir\`).
841
+ - Both modes read frontmatter (\`frontmatter.missing\`, \`frontmatter.malformed\`,
842
+ \`frontmatter.field-mismatch\`,
843
+ \`frontmatter.status-unknown\`, \`frontmatter.field-missing\`); a service's spec.md —
844
+ and its arch.spec.md, same conventions — additionally carries the sources chain
845
+ (\`sources.absent\`, \`sources.path-missing\`, \`sources.empty\`,
846
+ \`sources.skipped\`, \`sources.unvouched\`, \`sources.stale\`) and the doc-side freshness check
847
+ (\`content.stale\`) — the one provenance warning that needs no service repo, so
848
+ it is reported from the docs repo too, \`--service\` and \`--all\` alike.
849
+ - \`loam archive\` alone reports the breaches only the merge computation can see:
850
+ \`living.requirement-outside-requirements\` (error), \`openapi.op-modified\` (warn),
851
+ \`openapi.path-item-modified\` (warn), \`openapi.component-modified\` (warn),
852
+ \`openapi.ref-unresolved\` (error), \`openapi.remove-marker-path-level\` (error —
853
+ an \`x-loam-remove: true\` written at PATH level, beside the methods rather than
854
+ inside one: it addresses no operation, so it retires nothing, and it is not a
855
+ contract key either) and \`service.no-model\` (warn — the archive creates
856
+ \`services/<id>/\`, or puts a service in the landscape the fleet has no directory
857
+ for at all, and nothing writes its model.likec4).
858
+
859
+ \`loam validate <target>\` is the positional spelling of the first two: a feature id
860
+ or a service id, tried in that order, so the feature wins when one name could be
861
+ both and \`--service\`/\`--feature\` force the reading. When the name really is both,
862
+ the run says so — \`target.ambiguous\` (warn) names the reading it took and the
863
+ flag that forces the other, and \`--json\` carries \`resolvedKind\` — because a
864
+ silent precedence rule is a report about a target nobody asked for. The
865
+ positional together with \`--all\`, \`--service\` or \`--feature\` is refused
866
+ (\`invalid-option\`).
867
+
868
+ Two rendering levers change nothing else: \`loam validate --errors-only\` drops
869
+ the confirmations from the TEXT view (the \`--json\` payload is byte-identical),
870
+ and \`loam list --needs-work\` narrows the service list to the ones with
871
+ something missing — the adoption worklist.
872
+
873
+ \`--strict\` (every targeting mode, \`--all\` included) exits 1 when any error
874
+ or warning exists — \`ok\`-severity findings are confirmations and never trip
875
+ it. It changes the exit code and nothing else:
876
+ \`valid\` still means "no errors", and the \`--json\` payload stays byte-for-byte
877
+ what it was. The stricter grade is a per-invocation lever, visible in the CI
878
+ pipeline that passes the flag — deliberately not a per-repo profile.
879
+
880
+ \`loam delta <FEAT> --json\` exits 1 when \`architecture.errors\` is non-empty, with
881
+ \`ok: true\` and the full payload intact: the empty C4 slice means the delta did
882
+ not parse, not that the feature changes no architecture. Branch on the exit code
883
+ before consuming the slice as a task brief.
884
+
885
+ Findings with severity \`ok\` are confirmations, not work: \`c4.valid\`, \`delta.valid\`,
886
+ \`requirements.covered\`, \`api.covered\`, \`spine.resolved\`, \`coherence.ok\`,
887
+ \`landscape.matched\`, \`archedge.covered\`, \`sources.resolved\`, \`sources.current\`,
888
+ \`gherkin.current\`.
889
+
890
+ A finding's \`subject\` names the service it is about. The envelope separates \`ok\` (the
891
+ command ran) from \`valid\` (the docs pass). A refusal is \`ok: false\` with a stable
892
+ \`error.code\`: \`no-config\` / \`config-invalid\` (no loam.json / a corrupt one),
893
+ \`docs-missing\` / \`services-missing\` (a \`docsDir\` that points at nothing, or at a
894
+ directory that is not a docs repo — the fleet is unreadable, not empty),
895
+ \`unknown-target\` (no such service or feature), \`invalid-option\` (flags that contradict
896
+ each other, or a value that cannot be right — a \`loam list\` section that is not
897
+ services or features, or a service id that is not a legal
898
+ \`services/<id>/\` directory name, included), \`already-exists\` (\`loam new\` refusing
899
+ to scaffold over an existing feature), \`sources-absent\` / \`sources-path-missing\` /
900
+ \`vouch-raced\` (\`loam vouch\` refusing to stamp — the last one when the document
901
+ changed between the read and the write, so nothing was written),
902
+ \`not-coherent\` / \`living-outside-requirements\` /
903
+ \`archive-exists\` / \`merge-failed\` / \`rollback-incomplete\` / \`docs-busy\` /
904
+ \`commit-interrupted\`
905
+ (\`loam archive\` — see the
906
+ archive gate below; \`docs-busy\` means another archive or unarchive holds the docs
907
+ repo's lock, nothing was read or written, and re-running once it finishes works),
908
+ \`feature-active\` / \`snapshot-missing\` / \`snapshot-stale\` / \`snapshot-corrupt\` /
909
+ \`restore-failed\` / \`rollback-incomplete\` / \`docs-busy\` / \`commit-interrupted\`
910
+ (\`loam unarchive\` — the
911
+ \`restore-failed\` / \`rollback-incomplete\` pair splits
912
+ exactly as archive's does; see "Taking an archive back"). \`commit-interrupted\` is
913
+ the pair's shared refusal: a previous archive or unarchive was killed mid-commit
914
+ and this run cannot repair it on its own — a half-written file has been edited
915
+ since, the repairing pre-image is gone or altered, or \`.loam-commit\` itself
916
+ cannot be read. \`loam doctor\` names the same state as
917
+ \`doctor.commit-interrupted\`. And \`gherkin-conflict\`
918
+ (\`loam gherkin <FEAT>\` would overwrite a \`.feature\` file owned by another
919
+ feature still in flight — the whole emission refuses and names the owner),
920
+ \`answers-unreadable\` / \`answers-mismatch\` /
921
+ \`answers-unevidenced\` / \`record-federated\` / \`record-unreadable\`
922
+ (\`loam verify --record\` / \`--results\` — an unreadable or
923
+ unrecognizable cucumber report refuses under \`answers-unreadable\` too;
924
+ \`record-federated\` and \`record-unreadable\` are the two records loam will not
925
+ overwrite), and
926
+ \`internal\` — an unexpected throw, the one code with no stable meaning.
927
+
928
+ \`--all\` reports a target per service, a target per feature in flight, and one target
929
+ of kind \`landscape\` for the fleet-level checks that belong to no single service.
930
+
931
+ Three different words for three different questions, and no command conflates them:
932
+ \`ok\` — the command ran; \`valid\` — the documents pass (\`validate\`); \`verified\` —
933
+ somebody says the code was built and showed evidence (\`verify\`). A feature can be
934
+ valid and unverified, or verified and incoherent. Read the one you meant.
935
+
936
+ ## The archive gate
937
+
938
+ The three axes agreeing is called **coherence**, and \`loam validate --feature\` reports
939
+ it as such. \`loam archive\` runs the same coherence check first and refuses a feature
940
+ with GATING issues — every error, plus the rare warning marked \`gates: true\` because
941
+ the merge would silently drop authored content even though the document is legal.
942
+ Advisory warnings never block: archive prints them and proceeds. Each one still names
943
+ something real — usually something the merge will drop or overwrite — so read them
944
+ before the merge runs, not after.
945
+
946
+ \`--approve\` overrides the gating issues — only those, and archive prints exactly which
947
+ ones it overrode. It is a human decision, not an agent's: if archive refuses, fix the
948
+ breach or hand it back.
949
+
950
+ Breaches only the merge computation itself can see are reported at plan time,
951
+ after the gate. \`living.requirement-outside-requirements\` (error): the LIVING spec
952
+ holds a requirement outside \`## Requirements\`, and the merge rewrites only that
953
+ section, so the requirement would land in the file twice — \`--approve\` does not
954
+ override it, because the duplication is mechanical, not a judgment call; re-home the
955
+ requirement first. \`openapi.op-modified\` (warn): the feature redefines an operation
956
+ the living OpenAPI already has, and the merge overwrites the living definition
957
+ wholesale.
958
+
959
+ The OpenAPI merge also carries the merged operations' \`$ref\` closure: every
960
+ \`#/components/<kind>/<name>\` they reference — recursively, a component's own refs
961
+ included — is copied from the feature document into the living one, so an operation
962
+ never lands pointing at a schema that stayed behind. \`openapi.component-modified\`
963
+ (warn): a carried component overwrites a living one that differs, wholesale, same
964
+ discipline as an operation. \`openapi.ref-unresolved\` (error, \`--approve\`
965
+ overrides): a ref reachable from the merged operations resolves in neither
966
+ document, so merging would write a dangling reference. External refs — URLs, file
967
+ paths, anything not starting \`#/\` — are out of scope: left untouched, never gated.
968
+
969
+ ## Taking an archive back
970
+
971
+ \`loam unarchive <FEAT>\` restores the living docs and re-opens the feature. It works
972
+ by putting bytes back, not by inverting the merge — archive copies every file it is
973
+ about to overwrite into \`features/archive/<FEAT>/.loam-before/\` first, because the
974
+ previous text of a \`MODIFIED\` requirement is written down nowhere else. Do not edit
975
+ or delete that directory, and never reconstruct an old living spec by hand from an
976
+ archived delta: what the requirement said BEFORE is not in there, and a plausible
977
+ reconstruction is a lie the next reader has no way to catch.
978
+
979
+ Every pre-image is digested when the archive writes it, and re-checked before the
980
+ restore stages anything: \`snapshot-corrupt\` means a pre-image's bytes no longer
981
+ match the digest archive recorded for them, so restoring it would write text
982
+ nobody authored. \`--force\` does NOT override that one — the damage is to the
983
+ undo itself, not to the living docs — and the fix is version control.
984
+
985
+ It refuses rather than guesses, under codes you can branch on: \`feature-active\` (a
986
+ feature of that id is in flight again), \`snapshot-missing\` (archived before loam
987
+ recorded this — the docs have to come back from version control), \`snapshot-stale\`
988
+ (a merged file changed after the archive, so restoring would revert someone else's
989
+ work). \`--force\` overrides the last one, and like \`--approve\` it is a human's call.
990
+
991
+ A restore that fails outright splits the same way archive's does: \`restore-failed\`
992
+ means nothing was restored or everything was rolled back — the living docs are
993
+ unchanged, fix the reported cause and re-run; \`rollback-incomplete\` means the
994
+ restore failed AND some files could not be put back — stop and hand it to a human,
995
+ the message lists the files to check.
996
+
997
+ ## Dropping a feature
998
+
999
+ A feature that was never archived is one directory and nothing else: no living doc
1000
+ references it until \`loam archive\` merges it. To abandon it, delete the directory —
1001
+ \`git rm -r features/<FEAT-dir>\` — and version control keeps the record of the
1002
+ attempt. An ARCHIVED feature is the opposite, its content folded into the living
1003
+ docs: run \`loam unarchive <FEAT>\` first, then delete. There is no \`loam abandon\`,
1004
+ deliberately — a removal that computes nothing is what version control is for.
1005
+ `;
1006
+ const COMMANDS = [
1007
+ {
1008
+ name: "loam-adopt",
1009
+ description: "Adopt a service — write its baseline docs from its code, as draft, then validate",
1010
+ argumentHint: "<service-id>",
1011
+ body: `Write one service's baseline documentation into the loam docs repo (its path is
1012
+ \`docsDir\` in ./loam.json). You read the code; loam states the work and checks the
1013
+ result. It never reads the service — so anything you cannot show, do not write.
1014
+
1015
+ 0. If there is no ./loam.json here, wire the repo first:
1016
+ \`loam init --docs <path-to-docs-repo> --service $1\` (add \`--create\` ONLY if the
1017
+ docs repo does not exist yet), then \`loam doctor\` — it resolves \`docsDir\`,
1018
+ checks the docs repo is one, and reports the service binding. Do not go on with
1019
+ \`docs-missing\`, \`services-missing\` or any \`doctor.*\` blocker standing.
1020
+ Every \`doctor\` finding carries a \`fix\` field spelling the exact command —
1021
+ type that, not an approximation of it.
1022
+ If this repo is already wired, \`loam status --json\` in the docs repo is the
1023
+ fastest way to see whether $1 is even the service that owes work next: its
1024
+ \`next.adopt\` entries name the services with nothing written down yet.
1025
+ 1. \`loam adopt --service $1 --json\`. That output IS the brief:
1026
+ - \`warnings[]\` — read these FIRST. A near-miss against an existing service id
1027
+ usually means \`$1\` is a typo, and a brief followed to the letter then produces
1028
+ a complete, validating baseline for a service that does not exist.
1029
+ - \`targets[]\` — every file to write. \`action: "diff"\` means the file ALREADY EXISTS:
1030
+ read it, diff your findings against it, report what disagrees. Do not replace it.
1031
+ \`action: "edit"\` appears on exactly one target — \`architecture/landscape.likec4\`,
1032
+ the WHOLE FLEET's map: add this service's element and edges to it, never rewrite it.
1033
+ - \`targets[].shape\` — the grammar of each artifact, and \`example\` where one is
1034
+ shorter than a description. Every rule there is one a later check depends on;
1035
+ rules nothing checks are in \`unchecked[]\` instead, and are still worth following.
1036
+ - \`landscape\` — the elements and edges the fleet already has for this service.
1037
+ Bind to them; do not draw a second version of the same box. \`landscape.expects\`
1038
+ lists operations other services already call — your openapi.yaml owes them.
1039
+ \`landscape.instruction\` is the write the fleet map still owes this service; it
1040
+ is \`null\` once an element resolves to it, and only then.
1041
+ - \`frontmatter\` — what to put in the header of every markdown artifact.
1042
+ - \`checks[]\` — what \`loam validate\` will run. \`unchecked[]\` — what it will not.
1043
+ 2. Read \`AGENTS.md\` at the docs repo root, then read the code: entry points, HTTP
1044
+ routes and handlers, published events, config, deploy manifests, tests.
1045
+ **Keep a list of every path you actually open.** That list becomes \`sources\` —
1046
+ written as files and directories, never glob patterns — and it is the only line
1047
+ tying the document to the repository.
1048
+ 3. Write the artifacts under \`services/$1/\`, in the order the brief lists them, and
1049
+ make the landscape edit the brief asks for.
1050
+ Everything \`status: draft\`. Never write \`last_verified\`, \`sources_digest\`,
1051
+ \`content_digest\` or \`sources_files\`.
1052
+ 4. \`loam validate --service $1 --json\`. Fix every error. \`sources.unvouched\` is
1053
+ expected on a fresh baseline — it closes when a person vouches, not when you do.
1054
+ 5. \`loam validate --all --json\` in the docs repo. This is the run that grades the
1055
+ fleet map: \`landscape.service-unmodelled\` means the element never landed,
1056
+ \`landscape.binding-unknown\` / \`landscape.binding-duplicate\` mean it landed wrong,
1057
+ and \`landscape.missing\` means there is no map at all. A baseline that passes step 4
1058
+ and fails this one is documented and invisible.
1059
+ 6. Hand back, and say three things: what you could not determine from the code, what
1060
+ the existing artifacts disagreed with, and which parts you are least sure of.
1061
+ Then a human runs \`loam vouch --service $1\` in the service's own repo.
1062
+
1063
+ Where the code does not say, write that it does not say. A confident sentence about
1064
+ behaviour nobody can find is the one failure mode none of loam's checks can catch.
1065
+ `,
1066
+ },
1067
+ {
1068
+ name: "loam-feature",
1069
+ description: "Start a new loam feature — scaffold it, then author the C4 delta and requirement deltas",
1070
+ argumentHint: `<FEAT-id> "<title>"`,
1071
+ body: `Start a new feature in the loam docs repo (its path is \`docsDir\` in ./loam.json).
1072
+
1073
+ 1. Read \`AGENTS.md\` at the root of the docs repo first — it defines the ID spine
1074
+ everything here depends on.
1075
+ 2. Understand the current state before proposing a change:
1076
+ - \`loam status --json\` — what is already in flight and what it is waiting on.
1077
+ Run this FIRST if you joined this repository halfway or lost the session:
1078
+ \`next[]\` is ordered and each entry carries the literal command to run, so it
1079
+ tells you whether the work is to start $1 at all. \`loam status $1 --json\`
1080
+ narrows it to one feature once $1 exists
1081
+ - \`loam list --json\` — what services exist, and what documentation they are missing
1082
+ - \`loam show <service> --json\` — what a service owns, exposes, and who already calls it
1083
+ 3. Scaffold it: \`loam new $1 --title "$2"\`, adding \`--touches <id>\` for every service the
1084
+ feature touches and \`--new-service <id>\` for every one it introduces. Every id goes
1085
+ through the \`services/<id>/\` grammar, so a typo is refused (\`invalid-option\`)
1086
+ rather than scaffolded; a \`--touches\` that matches nothing existing comes back as
1087
+ a \`note\` with the close ids, not as a failure — read it, because a \`--touches\`
1088
+ naming a service that does not exist is \`delta.service-unknown\` at validate time
1089
+ and a phantom \`services/<typo>/\` directory if it ever archives.
1090
+ 4. Author what the templates left as TODO:
1091
+ - \`intent.md\` — the problem in business terms
1092
+ - \`delta.likec4\` — new elements and edges, each tagged \`#$1\`; every call edge carries
1093
+ \`metadata { op '<operationId>' }\`. For a requirements-only feature DELETE this
1094
+ file: an empty delta is legal, but a scaffold left half-filled is not
1095
+ - \`specs/<svc>/spec.md\` — one behaviour per requirement, SHALL, at least one
1096
+ Given/When/Then scenario; uncomment \`Operations:\` once the operation exists
1097
+ - \`specs/<svc>/arch.spec.md\` — the architectural obligations (outbox, retries,
1098
+ idempotency, alerts), same grammar; a \`Covers:\` line per requirement naming the
1099
+ tagged elements/edges it accounts for, or \`c4.uncovered\` says nothing does
1100
+ - \`specs/<svc>/openapi.yaml\` — define every operationId the edges reference
1101
+ 5. **\`loam rebase $1\`** — the step between authoring and checking, and the one that
1102
+ stops two features in flight from silently deleting each other's work. It pins
1103
+ every MODIFIED/REMOVED requirement (\`Based-On:\`) and every operation in the
1104
+ contract delta (\`x-loam-based-on\`) to the living version you wrote against.
1105
+ Run it on the contract axis even if you changed exactly one endpoint: that is
1106
+ what marks the REST of the document as quotation the merge must not write back.
1107
+ Without the pins, \`delta.baseline-missing\` / \`openapi.baseline-missing\` warn,
1108
+ nothing refuses, and the second feature to archive reverts the first — with
1109
+ \`+0 ~1 -0\`, exit 0, and nobody told.
1110
+ Restamping is not resolving: if rebase reports a requirement as MOVED, re-read
1111
+ the living text and fold in what you still mean BEFORE you ship.
1112
+ 6. \`loam validate --feature $1 --json\`. Do not stop while \`valid\` is false.
1113
+ 7. \`loam dependencies $1 --json\` before anyone starts building: it says which
1114
+ features in flight have to archive first (\`order\`) and which collisions no
1115
+ ordering fixes (\`conflicts\`, \`cycles\`). Every conflict it names is one \`loam rebase\`
1116
+ cannot fix for you — two intentions have to be merged by people.
1117
+
1118
+ The operation's three spellings — the edge's \`op\`, the requirement's \`Operations:\`
1119
+ line, and the OpenAPI \`operationId\` — must match exactly.
1120
+ `,
1121
+ },
1122
+ {
1123
+ name: "loam-implement",
1124
+ description: "Implement a loam feature's slice for one service — generated gherkin first, tests from it, then code",
1125
+ argumentHint: "<FEAT-id> [service]",
1126
+ body: `Implement one service's part of a feature. Every step runs in the SERVICE'S own
1127
+ repository, which needs its own committed ./loam.json — if there is none,
1128
+ \`loam init --docs <path-to-docs-repo> --service $2\` then \`loam doctor\` first.
1129
+
1130
+ 0. \`loam status $1 --json\` — where this feature actually is. Run it before
1131
+ anything else if you joined halfway or lost the session: it says which
1132
+ artifacts are still owed, whether another feature has to archive first, and
1133
+ what the next step is, each \`next[]\` entry carrying the literal command. It
1134
+ writes nothing, so it is always safe to re-run.
1135
+ 1. \`loam delta $1 --service $2 --json\` (drop \`--service\` to use the service configured
1136
+ in ./loam.json). That output IS the task:
1137
+ - \`intent\` — why this exists
1138
+ - \`requirements[]\` — what to build, with \`scenarios[].lines\` verbatim
1139
+ - \`archRequirements[]\` — the architectural obligations (outbox, retries, alerts),
1140
+ same shape; their scenarios become integration/ops tests, and \`covers\` names
1141
+ the model objects each exercises
1142
+ - \`architecture\` — whether this service is new, and the calls in and out of it
1143
+ - \`api\` — the endpoints this feature adds or retires for this service: path,
1144
+ method, operationId and summary, with removals spelled \`REMOVE <METHOD> <path>\`
1145
+ - \`services\` — every service this feature projects onto, so a run without
1146
+ \`--service\` tells you which ones to ask for rather than guessing
1147
+ Exit 1 with \`ok: true\` means \`architecture.errors\` is non-empty: delta.likec4 did
1148
+ not parse, and the empty C4 slice is a parse failure, not "no architecture change".
1149
+ Stop and fix the delta (\`loam validate $1 --json\`) before building anything.
1150
+ 2. In the service's repo, \`loam gherkin $1 --json\` — one \`.feature\` file per changed
1151
+ requirement lands under \`<gherkinDir>/loam/\` (default \`features/loam/\`), scenarios
1152
+ digest-stamped, arch requirements tagged \`@architecture\`. Those files ARE the
1153
+ acceptance criteria. Never edit them: regeneration rewrites the directory, and
1154
+ \`loam validate\` reports the suite stale by digest, not by your intentions.
1155
+ 3. Write step definitions for the generated scenarios FIRST — outside \`loam/\`.
1156
+ Do not paraphrase a scenario into something easier to pass; it is the acceptance
1157
+ criterion someone else reviews against.
1158
+ 4. Implement until the suite passes — run it with a JSON report
1159
+ (\`cucumber-js --format json:report.json\`):
1160
+ \`loam verify $1 --service $2 --results report.json\` consumes that report as the
1161
+ done-check's answer sheet. Record from THIS repo, with \`--service\` — the
1162
+ \`--service\`-less \`--record\` form writes the whole record from one place and is
1163
+ refused (\`record-federated\`) once another service has attested.
1164
+ 5. Honour the contract: every operation in \`architecture.inbound\` must exist under
1165
+ exactly that operationId, and every one in \`architecture.outbound\` must be called.
1166
+ 6. If building made you change the feature's documents at all — a requirement's
1167
+ wording, an operation in its openapi.yaml — **\`loam rebase $1\`** before you
1168
+ validate. Every edit you make in the docs repo while other features are in
1169
+ flight is written against a living document that may have moved since the
1170
+ feature directory was created, and the pins are the only thing that lets the
1171
+ merge tell an EDIT from a QUOTE. \`delta.baseline-missing\` and
1172
+ \`openapi.baseline-missing\` are the warnings that say you skipped it;
1173
+ \`delta.baseline-stale\` / \`openapi.baseline-stale\` are archive refusing because
1174
+ somebody landed a change underneath you — re-read theirs, fold in what you
1175
+ still mean, then rebase again. Re-pinning without re-reading is how you
1176
+ overwrite someone else's requirement with loam's blessing.
1177
+ 7. \`loam validate --feature $1 --json\` before handing back.
1178
+
1179
+ If the requirement is ambiguous, say so and stop — do not invent behaviour and
1180
+ leave the spec disagreeing with the code.
1181
+ `,
1182
+ },
1183
+ {
1184
+ name: "loam-check",
1185
+ description: "Validate the loam docs repo and fix what it reports",
1186
+ argumentHint: "[--all | <FEAT-id> | <service>]",
1187
+ body: `Run loam's checks and fix what they find.
1188
+
1189
+ - whole fleet: \`loam validate --all --json\` (run in the docs repo)
1190
+ - one target: \`loam validate <FEAT-id | service-id> --json\` — the feature reading is
1191
+ tried first; \`--feature <id>\` / \`--service <id>\` force one when a name could be both,
1192
+ and \`target.ambiguous\` (warn) says when a name really was both and which reading was taken
1193
+ - \`--strict\` (any mode) exits 1 on any warning too. Exit code only: \`valid\` and the
1194
+ payload do not change, so the stricter grade lives in the CI invocation, not the repo
1195
+ - \`--errors-only\` drops the \`ok\` confirmations from the text view; the \`--json\`
1196
+ payload is byte-identical either way
1197
+ - refusals before any grading: \`docs-missing\` / \`services-missing\` (\`docsDir\` in
1198
+ ./loam.json points at nothing, or at a directory with no \`services/\`) — run
1199
+ \`loam doctor\` and fix the wiring, do not read an empty report as a clean fleet
1200
+
1201
+ Branch on \`findings[].code\`, not the prose. Errors fail validate; a coherence finding
1202
+ with \`gates: true\` will stop \`loam archive\` even when it is a warning. Fix every
1203
+ error and every gating finding. Leave an advisory warning only if you can say why,
1204
+ and say it.
1205
+
1206
+ \`--service <id>\` — one service's own axes (\`--all\` runs these for every service):
1207
+
1208
+ | code | what it means | what to do |
1209
+ |---|---|---|
1210
+ | \`service.unknown\` | no \`services/<id>/\` at all — the id is a typo until proven otherwise | use one of the ids the message offers, or \`loam list services\`; never \`loam adopt\` a misspelling |
1211
+ | \`service.no-model\` | the directory is real but there is no model.likec4 | run \`loam adopt\` for it — without the C4 center nothing else has anywhere to hang |
1212
+ | \`c4.invalid\` | model.likec4 does not parse | fix this first — an unreadable axis makes every other check meaningless |
1213
+ | \`requirements.missing-scenarios\` | a requirement with no scenario | add the acceptance criteria; do not delete the requirement |
1214
+ | \`requirements.stepless-scenario\` | a scenario with a heading and no recognizable steps — it satisfies the coverage rule and tests nothing: cucumber runs it vacuously green and \`loam verify --results\` can never confirm it. Graded on spec.md and arch.spec.md alike, in \`--service\` and \`--feature\` scope both, because a stepless scenario in a delta merges into the living spec and is called covered forever after | write the body as \`- **Given/When/Then**\` bullets |
1215
+ | \`spec.merge-conflict\` | a requirement document still holds \`<<<<<<<\` / \`=======\` / \`>>>>>>>\` markers — nobody wrote what it now says. Graded on living spec.md/arch.spec.md and on a feature's deltas of them, in both scopes | resolve the conflict in the file the finding names. \`loam archive\` refuses (\`merge-failed\`) on a conflicted living document it would rewrite, and \`--approve\` does not override it: the merge rewrites the requirements section and takes whichever marker lines fall inside it, so a conflict anyone can see would become a document nobody can tell is wrong |
1216
+ | \`spec.duplicate-requirement\` | one \`### Requirement:\` name defined twice in one living spec.md or arch.spec.md — a merge edits only the first, the rest live on as stale copies | keep exactly one block per name (merge the bodies); the two files are separate namespaces, so a name in both is fine |
1217
+ | \`spec.requirement-id-invalid\` | a \`Requirement-ID\` violates \`[A-Za-z][A-Za-z0-9._-]{0,127}\` | replace it with a portable stable ID; do not remove an established ID merely to silence the check |
1218
+ | \`spec.requirement-id-repeated\` | one requirement declares \`Requirement-ID\` more than once | keep exactly one identity line |
1219
+ | \`spec.requirement-id-duplicate\` | one ID identifies multiple requirements in the same file | give each requirement its own stable ID |
1220
+ | \`spec.repeated-operations\` (warn) | a second \`Operations:\` line in one requirement body — the last line REPLACES the earlier ones, whose list is silently lost (living specs and feature deltas alike) | merge them into one comma-separated \`Operations:\` line |
1221
+ | \`spec.repeated-covers\` (warn) | same for \`Covers:\` — the last line wins, the earlier list is silently lost | merge them into one comma-separated \`Covers:\` line |
1222
+ | \`spec.no-requirements\` (warn) | a LIVING spec.md with no \`### Requirement:\` block at all — every requirement-driven check below it is vacuous, not passing | write the requirements, or say in the file why the service has none yet |
1223
+ | \`service.unreadable\` | an artifact of this service could not be read (permissions, a dangling symlink) — nothing about the service was checked, and the rest of the fleet still was | fix the file the finding names; a service silently skipped would have read as a clean one |
1224
+ | \`service.no-spec\` (warn) | no living spec.md — a part-adopted service, legal but unchecked | write it; until it exists, requirement coverage and API governance are vacuous |
1225
+ | \`service.no-openapi\` (error, or warn) | no openapi.yaml — deleted, renamed, or never written. ERROR when something already written down joins into the absent file: a LIVING non-REMOVED requirement's \`Operations:\` line, or a landscape edge carrying \`metadata { op }\` into this service. The finding's \`details\` list the stranded operationIds. WARN when nothing joins into it but the landscape cannot prove nobody calls this service (it is absent, or it does not parse). Silent when the landscape parses and no edge calls an operation here — a worker, a cron or a UI owes no contract | error: put the file back — every link the \`details\` name resolves to nothing until it is there. If the operations are genuinely gone, retire them through a feature delta (REMOVED requirement + \`x-loam-remove\` marker) instead of deleting the contract out from under them. warn: write the contract, or model the service in the fleet map so it shows no one expects an API |
1226
+ | \`openapi.invalid\` | openapi.yaml exists but does not parse — an unreadable contract proves nothing, so no \`api.*\` or spine finding is graded against it (before this code, the empty parse graded every inbound edge \`spine.op-undefined\`) | fix the YAML first — the API axis is unchecked until it reads |
1227
+ | \`openapi.remove-marker-living\` | a living openapi.yaml contains feature-only \`x-loam-remove: true\` | remove the marker from living; retire the operation through a feature delta with a matching REMOVED requirement |
1228
+ | \`openapi.duplicate-operationid\` (warn) | one operationId occupies two (path, method) slots in a living contract — every join on the id (a requirement's \`Operations:\` line, an edge's \`metadata { op }\`, a removal marker) then picks one of them arbitrarily | give each slot its own id; the identity of an operation is its path and method, and the id is how the other axes name it |
1229
+ | \`openapi.baseline-stale\` | the living operation changed after this delta edited it | somebody landed a change to it in between. Re-read the living operation, fold in what you still mean, then \`loam rebase <FEAT>\` |
1230
+ | \`openapi.baseline-missing\` (warn, one per service) | operations in this feature's openapi.yaml carry no \`x-loam-based-on\` | run \`loam rebase <FEAT>\`. Until you do, the merge cannot tell the operations you EDITED from the ones you merely restated, so it upserts all of them — and every restated one reverts whatever landed on it |
1231
+ | \`openapi.baseline-invalid\` | an \`x-loam-based-on\` that is not a digest, or one on an operation the living contract has no slot for | \`loam rebase\` writes the value; a new operation has no living version to be based on, so drop the marker there |
1232
+ | \`spec-api.op-undefined\` | a LIVING requirement's \`Operations:\` line names an operationId this service's openapi.yaml does not define (the same code fires inside a feature delta) | define the endpoint, or correct the \`Operations:\` line — the two axes disagree about what the service exposes |
1233
+ | \`api.ungoverned\` (warn) | operation(s) no requirement's \`Operations:\` line names | write the requirement, or link an existing one |
1234
+ | \`api.ops-unlinked\` (warn) | operations AND requirements exist but zero \`Operations:\` lines join them — the API axis is vacuously green | link each requirement to the operations it governs |
1235
+ | \`api.requirement-deprecated\` (warn) | a requirement's \`Operations:\` list resolves only to operations the OpenAPI marks \`deprecated: true\` — the behaviour it governs is on its way out | migrate the requirement to the replacement operation, or retire it with the ops it governs |
1236
+ | \`spine.landscape-invalid\` | the living landscape does not parse, so the C4↔API spine cannot be checked | fix architecture/landscape.likec4 first |
1237
+ | \`spine.op-undefined\` | a landscape edge calls an operation this service's OpenAPI does not define | a broken contract between services — fix the edge or add the endpoint |
1238
+ | \`spine.op-link-missing\` (warn) | a landscape "Calls" edge into this service with no \`metadata { op }\` | link it to the operationId |
1239
+ | \`spine.op-deprecated\` (warn) | a landscape edge calls an operation this service's OpenAPI marks \`deprecated: true\` — the consumer is standing on a contract being retired | migrate the consumer to the replacement operation; deprecation is the first step of retiring an op, and the op stays defined until a human removes it |
1240
+ | \`covers.unknown\` (warn) | a \`Covers:\` entry in arch.spec.md resolves to no element, edge, alert or SLI — living and feature deltas alike | fix the id (the message offers close ones); a mistyped entry silently costs the coverage it was written for |
1241
+ | \`health.invalid\` (warn) | health.yaml exists but does not parse — alert/SLI ids are unreadable, so \`Covers: alert:/sli:\` entries and health coverage are unchecked (a missing health.yaml is legal and silent; an unreadable one used to masquerade as \`covers.unknown\` typos) | fix the YAML — the health axis resumes once it reads |
1242
+ | \`health.uncovered\` (warn) | health.yaml declares an alert or SLI no arch.spec.md requirement covers | write the arch requirement with \`Covers: alert:<id>\` / \`Covers: sli:<id>\` — a signal nothing tests is dashboard decoration |
1243
+ | \`gherkin.missing\` (warn) | a living scenario no generated .feature scenario carries a digest for — the suite has no test for these words. Fires only in the service's repo, once \`<gherkinDir>/loam/\` exists | \`loam gherkin --service <id>\` regenerates the suite |
1244
+ | \`gherkin.stale\` (warn) | a generated scenario's digest matches no living scenario while its requirement still exists — the spec moved under the file (a reworded scenario reports stale + missing together) | regenerate; never edit a generated file to catch it up |
1245
+ | \`gherkin.orphaned\` (warn) | a generated file whose requirement no longer exists in the living spec (a file for a feature still in flight is exempt — it answers to its feature until it archives) | regenerate; the orphaned file is deleted and reported |
1246
+
1247
+ \`--feature <FEAT-id>\` — a change's three axes against each other. The SAME checks
1248
+ gate \`loam archive\` (errors block it; warnings never do), so a clean run here is
1249
+ what lets a feature ship:
1250
+
1251
+ | code | what it means | what to do |
1252
+ |---|---|---|
1253
+ | \`delta.invalid\` | delta.likec4 does not parse | fix this first — an unreadable axis makes every other check meaningless |
1254
+ | \`delta.nothing-tagged\` | the delta declares elements/relationships but none carry \`#<FEAT>\` | tag what IS the change — untagged parts are context, and archive merges only tags |
1255
+ | \`delta.service-unknown\` | a \`specs/<svc>/\` directory addresses a service that neither exists as \`services/<svc>/\` nor is introduced by this feature's own tagged delta.likec4 — a typo in \`--touches\`, which archive would otherwise materialise as a phantom service directory | fix the id (the message offers close ones); suspended while \`delta.invalid\` stands, because an unparseable delta cannot be asked what it introduces |
1256
+ | \`spec.merge-conflict\` | a spec delta (\`specs/<svc>/spec.md\` or \`arch.spec.md\`) still holds \`<<<<<<<\` / \`=======\` / \`>>>>>>>\` markers — the same code and the same sentence as service scope, because the markers merge into the living document as prose under somebody's requirement | resolve the conflict before archive; the merge rewrites the requirements section and the marker lines inside it disappear with it |
1257
+ | \`requirements.stepless-scenario\` | a scenario in a spec delta with a heading and no steps — same code as service scope, and it merges into the living spec as a requirement the coverage rule calls covered from then on | write the body as \`- **Given/When/Then**\` bullets |
1258
+ | \`spec-api.op-undefined\` | a requirement governs an operation its provider's OpenAPI does not define | define the endpoint, or correct the \`Operations:\` line |
1259
+ | \`spec-api.op-pending\` (warn) | the governed operation is defined by another feature in flight | archive that feature first |
1260
+ | \`c4-api.op-undefined\` | an edge calls an operation the target does not expose | a broken contract between services — fix the caller or add the endpoint |
1261
+ | \`c4-api.op-pending\` (warn) | the called operation is defined by another feature in flight | archive that feature first |
1262
+ | \`c4-api.op-deprecated\` (warn) | a NEW tagged edge consumes an operation the living provider's OpenAPI marks \`deprecated: true\` — building new consumption on a dying op (quiet when this feature's own openapi delta drops the flag: that IS the un-deprecation) | point the edge at the replacement operation, or say why the deprecated one is right; never gates archive |
1263
+ | \`c4-api.op-removing\` | a NEW tagged edge consumes an operation this feature removes | remove or redirect the new edge; one feature cannot retire and add consumption of the same operation |
1264
+ | \`openapi.remove-target-missing\` | an \`x-loam-remove: true\` marker's exact path+method does not exist in living OpenAPI | update the stale marker to the current slot, or drop it if the operation is already gone |
1265
+ | \`openapi.remove-target-mismatch\` | the marker's operationId differs from the living operation at that path+method | name the living operation exactly; loam never deletes the different operation occupying the slot |
1266
+ | \`openapi.remove-marker-missing\` | a REMOVED requirement governs an operation but the feature has no removal marker | add the exact path/method operation with its operationId and \`x-loam-remove: true\` to the feature openapi.yaml |
1267
+ | \`openapi.remove-marker-unjustified\` | a removal marker is not named by any REMOVED requirement's \`Operations:\` line | remove the marker or retire the governing requirement in the same feature |
1268
+ | \`openapi.remove-marker-anonymous\` | an \`x-loam-remove: true\` marker with no \`operationId\` — loam cannot tell which operation it retires, and the marker itself would be written into the living contract | name the operation the marker retires, exactly as the living contract spells it |
1269
+ | \`openapi.remove-op-consumed\` | the feature retires an operation the LIVING fleet still consumes — a landscape edge's \`metadata { op }\`, or another service's living requirement naming it | fix the consumer in the same feature (redirect the edge, retire its requirement), or archive with \`--approve\` if the consumer is already gone in code |
1270
+ | \`c4.op-ungoverned\` (warn) | an operation is called but no requirement governs it | write the requirement |
1271
+ | \`c4.op-link-missing\` (warn) | a "Calls" edge in the delta with no \`metadata { op }\` | link it to the operationId |
1272
+ | \`api.op-unconsumed\` (warn) | an added operation no edge consumes | model the caller, or say why it is provider-only |
1273
+ | \`service.no-requirement-delta\` (warn) | a new service with no spec delta | write \`specs/<svc>/spec.md\` |
1274
+ | \`archedge.uncovered\` (warn) | no scenario names a tagged edge (a heuristic) | write the scenario, or say why the edge needs none |
1275
+ | \`c4.uncovered\` (warn) | a NEW tagged element or edge in delta.likec4 that no arch requirement covers via \`Covers:\` — the mechanical check, not the heuristic above | add the requirement to \`specs/<svc>/arch.spec.md\` (outbox? retries? alerts?), or the architectural obligations ship unchecked |
1276
+ | \`delta.unknown-section\` | a heading that nearly matches the delta grammar | fix it — everything under it merges as NOTHING today, silently |
1277
+ | \`delta.no-delta-sections\` | requirements, but no \`## ADDED/MODIFIED/REMOVED Requirements\` section anywhere — the whole delta would merge nothing | put every changed requirement under its delta section |
1278
+ | \`delta.requirement-not-merged\` (warn, gates archive) | a requirement under a prose heading (\`## Behavior\`) instead of a delta section | move it under \`## ADDED\`/\`## MODIFIED\`/\`## REMOVED Requirements\` — as written, archive drops it. If it really is documentation, quote it under \`## Requirements\`, which is exempt |
1279
+ | \`delta.requirement-id-invalid\` / \`delta.requirement-id-repeated\` / \`delta.requirement-id-duplicate\` | a stable ID is malformed or ambiguous inside the delta | repair it before archive; identity is never inferred from an invalid declaration |
1280
+ | \`delta.living-requirement-id-invalid\` | the living file's IDs are malformed or ambiguous | repair the living spec first; the delta cannot select it safely |
1281
+ | \`delta.requirement-identity-collision\` | the delta's ID and heading point at different living requirements | fix the ID or heading; archive will not guess which identity should win |
1282
+ | \`delta.modified-unknown\` | MODIFIED a requirement the living spec does not have | use ADDED, or fix the name (a spelling slip reads as a different requirement) |
1283
+ | \`delta.removed-unknown\` | REMOVED one that does not exist | drop the section, or fix the name |
1284
+ | \`delta.added-duplicate\` | ADDED a name the living spec already has | use MODIFIED — as written, the merge REPLACES the living requirement |
1285
+ | \`delta.added-near-duplicate\` (warn) | ADDED a name that differs only in case from a living requirement — the merge matches exactly, so both would coexist | match the living spelling and use MODIFIED, or pick a distinct name |
1286
+ | \`delta.living-duplicate-requirement\` | two requirements in the LIVING document share one heading — MODIFIED rewrites only the first and REMOVED deletes both, so no delta applies to it predictably | fix the living spec first (merge the bodies, or give each a distinct heading and a \`Requirement-ID\`); the delta cannot select safely until you do |
1287
+ | \`delta.modified-pending\` (warn) | the requirement is introduced by another feature in flight | archive that feature first — \`loam dependencies --json\` computes the whole ordering (\`order\`), so you do not have to work it out per finding |
1288
+ | \`delta.removed-pending\` (warn) | REMOVED something another feature in flight introduces | archive that feature first; \`loam dependencies --json\` says in which order |
1289
+ | \`delta.added-conflict\` (warn) | two features in flight add the same requirement | whichever archives first lands it; the other's ADDED then collides with the living spec (\`delta.added-duplicate\`, error) and its archive is refused — coordinate now, or rework the later one as MODIFIED after the first ships. No ordering fixes it, which is why \`loam dependencies\` reports it under \`conflicts\` rather than in \`order\` |
1290
+ | \`delta.modified-conflict\` (warn) | another feature in flight MODIFIES or REMOVES the same living requirement | both deltas apply cleanly, so whichever archives second replaces the other's text wholesale — \`loam dependencies\` lists it under \`conflicts\`; merge the two intentions before either ships |
1291
+ | \`delta.baseline-stale\` | the living requirement changed after this delta was written against it | someone landed a change in between and this MODIFIED would replace it outright. Re-read the living requirement, fold in what you still mean, then \`loam rebase <FEAT>\`. Re-pinning without re-reading is how you overwrite them on purpose |
1292
+ | \`delta.baseline-missing\` (warn) | a MODIFIED/REMOVED requirement with no \`Based-On:\` | run \`loam rebase <FEAT>\` — until then nothing can say whether the living text moved under this delta. Expected on deltas adopted from OpenSpec, which never carried the line |
1293
+ | \`delta.baseline-invalid\` | a \`Based-On:\` that is not a digest, declared twice, or sitting on an ADDED requirement | \`loam rebase\` writes the value; an ADDED requirement has no living version to be based on, so drop the line or make it MODIFIED |
1294
+
1295
+ \`--all\` — everything above for every target, plus the fleet cross-check:
1296
+
1297
+ | code | what it means | what to do |
1298
+ |---|---|---|
1299
+ | \`landscape.missing\` | \`architecture/landscape.likec4\` does not exist — an ERROR as soon as \`services/\` holds one service, a warning only in an empty docs repo | create it: a \`specification { ... }\` block declaring the kinds, then a \`model { ... }\` block with one bound element per service (\`metadata { service '<id>' }\`) and one edge per cross-service call carrying \`metadata { op '<operationId>' }\`. With no fleet map every cross-service check is blind, not passing |
1300
+ | \`landscape.invalid\` | the living landscape does not parse | fix it first — the fleet cross-check cannot run against a document nobody can read |
1301
+ | \`service.id-invalid\` | a \`services/<id>/\` directory whose name is not a legal service id — every authoring command refuses it, so nothing in that directory can be changed through loam. Fleet scope only: the SET of service directories is what makes the id a question, and a \`--service\` run is already refused by its own guard before it gets this far. Graded before the map is opened, so it stands even when the landscape is missing or unreadable | rename the directory, then update its \`service:\` frontmatter, its \`metadata { service }\` binding in the landscape, and any \`features/<FEAT>/specs/<id>/\` naming it |
1302
+ | \`landscape.merge-conflict\` | the fleet map still holds conflict markers — it is two halves of two different maps, and every cross-service check reads it. Checked BEFORE the parse and returns like \`landscape.invalid\`: nothing is concluded from the file, so no \`landscape.service-unmodelled\` fires behind it | resolve it keeping BOTH sides' elements and edges; ten people adopting ten services into one landscape.likec4 in the same week is how it happens. \`loam archive\` refuses on it too (\`merge-failed\`, not overridable with \`--approve\`) |
1303
+ | \`landscape.service-unmodelled\` | a \`services/<svc>/\` no element in the landscape resolves to | draw it, or bind an existing element with \`metadata { service '<svc>' }\` — the fleet map is incomplete until you do |
1304
+ | \`landscape.service-undocumented\` (warn) | a landscape element with no \`services/<id>/\` | document the service, bind the element to the directory it means, or tag it \`#external\` if it is not ours |
1305
+ | \`landscape.binding-unknown\` | an element's \`metadata { service }\` names a directory that does not exist | fix the id or create the service — a binding is a claim, and this one is false |
1306
+ | \`landscape.binding-duplicate\` (warn) | two elements resolve to the same \`services/<id>/\` | every element→service join picks one of them arbitrarily, so the other's edges are filed under a service that does not own them — bind each element to the directory it actually is, or tag the stray one \`#external\` |
1307
+ | \`feature.unreadable\` | an artifact of a feature in flight could not be read — nothing about that feature was graded, and the rest of the fleet still was | fix the file the finding names; a feature silently skipped would have read as a clean one |
1308
+ | \`sources.unverifiable-from-here\` (ok) | one finding PER SERVICE whose \`sources\` only its own repo can check — a confirmation, not work, so it never trips \`--strict\`, and it is present in \`--service\` runs from the docs repo too | run \`loam validate --service <id>\` from inside that service's repo to actually grade its provenance; the fleet rollup line is derived from these findings |
1309
+ | \`agents.stale\` (warn) | the docs repo's AGENTS.md has no version stamp, or one older than the running binary — its flag and code tables may describe a loam that no longer exists | review AGENTS.md against the current \`loam --help\`, then update the \`<!-- generated by loam vX.Y.Z -->\` line; a hand-curated file silences it by bumping the stamp itself. Never regenerate the file — the team's edits outrank the template |
1310
+
1311
+ frontmatter and provenance — services' spec.md and arch.spec.md, and features'
1312
+ intent.md, both modes:
1313
+
1314
+ | code | what it means | what to do |
1315
+ |---|---|---|
1316
+ | \`frontmatter.missing\` (warn) | no frontmatter at all | add owner, status and sources |
1317
+ | \`frontmatter.malformed\` | the frontmatter block does not parse as YAML — owner, status and sources are unreadable, which is not the same fact as absent | fix the header; the field checks and the sources chain resume once it parses — never add fields to a block YAML refuses |
1318
+ | \`frontmatter.field-mismatch\` | the doc names a different service/feature than the one it lives under | fix the frontmatter, or move the file |
1319
+ | \`frontmatter.status-unknown\` | a status nobody defined (\`verifed\`) | use the documented vocabulary — a typo here reads as unverified forever |
1320
+ | \`frontmatter.field-missing\` (warn) | owner, status or the identity field is absent | fill them in |
1321
+ | \`sources.absent\` (warn) | the doc names no sources | nothing ties it to the code, so nothing can tell you when it goes stale |
1322
+ | \`sources.path-missing\` | a listed source no longer exists, or is a glob pattern (no longer supported) | the code moved — re-read it and update the doc, do not just fix the path. For a pattern, name files or directories instead |
1323
+ | \`sources.stale\` (warn) | the source files changed since the doc was vouched for — the finding names which paths were added, changed or removed, from the \`sources_files\` index the stamp carries | re-read the code at exactly those paths, correct the doc, then ask a human to \`loam vouch --service <id>\` |
1324
+ | \`sources.empty\` (warn) | the declared \`sources\` exist but expand to no files at all — an empty directory, or a tree the repository ignores. A digest over nothing reads as current forever | point \`sources\` at the files the document was actually written from; \`loam vouch\` refuses to stamp this state with the same sentence |
1325
+ | \`sources.skipped\` (warn) | a path under a listed source was found but NOT hashed: a symlink loam will not follow (its target is outside the repository, dangling, or not a file or directory) — the digest cannot go stale over bytes it never read | not yours to close by editing the document: either the symlink should not be under a listed source, or \`sources\` should name the real path. Report it |
1326
+ | \`sources.unvouched\` (warn) | \`sources\` with no \`sources_digest\` — nobody ever stamped it | leave it: vouching is a human's reading, not yours |
1327
+ | \`content.stale\` (warn) | the spec's body changed since it was vouched — \`status: verified\` is standing over words nobody has read. Unlike \`sources.*\` it needs no service repo, so it fires from the docs repo too | if you edited the doc, that is the point: report it and ask a human to re-vouch. Never revert the doc or touch the digest just to silence it |
1328
+
1329
+ \`loam archive\` alone — breaches only the merge computation can see, reported at
1330
+ plan time (they never appear in \`validate\`):
1331
+
1332
+ | code | what it means | what to do |
1333
+ |---|---|---|
1334
+ | \`living.requirement-outside-requirements\` | the LIVING spec holds a requirement outside \`## Requirements\`, and the merge rewrites only that section — it would land in the file twice | re-home the requirement under \`## Requirements\`, then re-run; \`--approve\` does not override this |
1335
+ | \`openapi.op-modified\` (warn) | the feature redefines an operation the living OpenAPI already has — the merge overwrites the living definition wholesale | make sure the redefinition is intended; if not, align the feature's openapi.yaml with the living one |
1336
+ | \`openapi.path-item-modified\` (warn) | the delta redefines a PATH-level key (\`parameters\`, \`servers\`, \`summary\`, an \`x-\`) the living OpenAPI already has — the overwrite applies to EVERY operation on that path, including ones this feature never mentions | keep path-level keys out of the delta unless changing them for every operation on the path is the intent |
1337
+ | \`service.no-model\` (warn) | the archive creates \`services/<id>/\` but nothing writes its \`model.likec4\` | run \`loam adopt --service <id>\` after the archive lands, or the new service ships without a C4 center and \`validate --all\` grades it incomplete |
1338
+ | \`openapi.component-modified\` (warn) | a component the merged operations reference already exists in the living OpenAPI with different content — the merge copies the feature's version over it wholesale | make sure the redefinition is intended; if not, align the feature's component with the living one |
1339
+ | \`openapi.ref-unresolved\` | a \`$ref\` reachable from the merged operations resolves in neither the feature's OpenAPI nor the living one — the merge would write a dangling reference | define the missing component or fix the ref; \`--approve\` merges the dangling reference anyway. External refs (not starting \`#/\`) are never checked |
1340
+ | \`openapi.remove-marker-path-level\` | an \`x-loam-remove: true\` written at PATH level, beside the methods instead of inside the operation being retired — it names no operation, so it retires nothing, and it is not a contract key either | move the marker inside the operation (with its \`operationId\`), or delete it. The merge is safe either way — a feature-only key is never published into the living contract — but the removal you asked for will not happen |
1341
+
1342
+ \`sources.stale\` and \`content.stale\` are the warnings you cannot close by yourself.
1343
+ Fix what the code now says, then hand it back — the stamp is a person's claim to
1344
+ have read it.
1345
+ `,
1346
+ },
1347
+ {
1348
+ name: "loam-verify",
1349
+ description: "Verify a built loam feature against its own promises, claim by claim, with evidence",
1350
+ argumentHint: "<FEAT-id>",
1351
+ body: `Check that the code somebody just built is the feature that was designed.
1352
+
1353
+ loam derives the questions; the test runner and you answer them. loam never
1354
+ reads the service, so a verdict is worth exactly what its evidence is worth.
1355
+
1356
+ 1. \`loam verify $1 --json\`. \`claims[]\` is the checklist, derived from the feature's
1357
+ own artifacts — one claim per new service, per operation its openapi delta adds,
1358
+ per tagged edge that names an operation, and per scenario of every changed
1359
+ requirement, arch.spec.md deltas included. Each has a stable \`id\` and a
1360
+ \`subject\` (the service whose code answers it); a claim that says arch.spec.md
1361
+ wants an integration/ops test, not an acceptance test. An ARCHIVED feature returns its record as frozen history instead
1362
+ (\`frozen: true\`) and \`--record\` / \`--results\` refuse it — \`loam unarchive\`
1363
+ first if the answers really must change.
1364
+ 2. The \`scenario.tested\` claims are the runner's to answer, not yours. In the
1365
+ service's repo, run the generated suite with a JSON report —
1366
+ \`npx cucumber-js --format json:report.json\` (cucumber-jvm, behave and
1367
+ SpecFlow emit the same format). The generated scenarios carry
1368
+ \`@loam-digest-…\` tags that ride into the report; that digest is the match,
1369
+ so never retitle or hand-edit a generated scenario to make one pass.
1370
+ If there is no runnable suite yet you MAY answer them yourself, and the record
1371
+ will say so: \`answered_by: agent\` makes the feature **attested**, not verified
1372
+ (\`verified: false\`, \`verdict: "attested"\`, the \`verify.scenario-attested\`
1373
+ notice, and a \`next.verify-attested\` step in \`loam status\`). Nothing gates on
1374
+ it — a legacy service with no suite must still be able to ship — but say so
1375
+ when you hand back, and close it with \`--results\` the moment a suite runs.
1376
+ \`verify.digest-contested\` (warn) is the case where two services word a
1377
+ scenario identically: they share one digest, so no single report can say whose
1378
+ suite ran it, and each service must record its own with \`--service\`.
1379
+ 3. Answer every OTHER claim by finding it in the code. Not by reasoning that it
1380
+ must be there — by opening the file:
1381
+ - \`service.exists\` — the service is deployable: its build, its entry point.
1382
+ - \`api.exposes\` — the route handler serving that operationId, not the spec that
1383
+ declares it.
1384
+ - \`c4.calls\` — the call site in the CALLER.
1385
+ 4. Write those answers as JSON — evidence is \`file:line\`, one entry per place it can
1386
+ be seen:
1387
+ \`\`\`json
1388
+ { "answers": [
1389
+ { "id": "<claim id>", "verdict": "confirmed", "evidence": ["src/split/Api.ts:42"] },
1390
+ { "id": "<claim id>", "verdict": "unconfirmed", "note": "the split service has no entry point yet" }
1391
+ ] }
1392
+ \`\`\`
1393
+ 5. Record, **in each affected service's own repository**, one service at a time:
1394
+
1395
+ \`\`\`sh
1396
+ loam verify $1 --service <id> --results report.json --record answers.json --json
1397
+ \`\`\`
1398
+
1399
+ \`--service <id>\` narrows the checklist to that service's claims and binds the
1400
+ answers to this repository's commit, writing ONE attestation into a shared
1401
+ \`schema: 2\` \`verification.yaml\`. Running it in the next repo adds that repo's
1402
+ attestation and leaves the others standing. In a repo whose ./loam.json
1403
+ declares a \`service\`, \`--service\` may be omitted — it is substituted, and the
1404
+ run still federates. \`--results report.json\` alone is legal when the
1405
+ checklist is all scenarios.
1406
+
1407
+ ⚠ **Never run \`loam verify $1 --record answers.json\` without \`--service\`.**
1408
+ That is the legacy all-at-once form: it writes the WHOLE record from one place,
1409
+ and over a record that already carries other services' attestations it is
1410
+ refused (\`record-federated\`, naming them) precisely because it would erase
1411
+ them. If you meet that refusal, re-run with \`--service\`; do not work around it.
1412
+
1413
+ The other refusals: \`answers-mismatch\` (an id
1414
+ nobody asked about, a claim you left out — re-run step 1, the feature moved —
1415
+ or an answers entry for a \`scenario.tested\` claim: the runner owns those),
1416
+ \`answers-unevidenced\` (a \`confirmed\` with nothing behind it),
1417
+ \`answers-unreadable\` (a report or answers file it cannot read),
1418
+ \`record-unreadable\` (a \`verification.yaml\` that exists but cannot be read as a
1419
+ record — it is never overwritten; a human repairs it, and
1420
+ \`verify.record-miscounted\` is the reason when its \`summary\` block contradicts
1421
+ its own \`claims[]\`: neither half can be believed, so re-record rather than
1422
+ editing the counts),
1423
+ \`service-mismatch\` (\`--service\` names a service this repo does not declare) and
1424
+ \`repository-unavailable\` (\`--service\` from a repo that declares none, or whose
1425
+ commit cannot be read — an attestation must be bound to the code it is about).
1426
+ 6. Report every \`unconfirmed\` claim and what it would take to close it, and read
1427
+ \`discarded\` in the payload: it names every previous answer the new record does
1428
+ not carry, and each one is either evidence that went stale or evidence that was
1429
+ just dropped. A scenario claim the report failed or missed closes by fixing the
1430
+ code and re-running the suite — never by editing the generated .feature.
1431
+ 7. Reading is safe from anywhere: \`loam verify $1 --service <id> --json\` without
1432
+ \`--record\` writes nothing and works from the docs repo too. \`services\` lists
1433
+ every subject of the checklist, so it is how you find out which repos still
1434
+ owe an attestation, and \`loam status $1 --json\` says the same thing in the
1435
+ shape of a next step.
1436
+
1437
+ Anything you cannot show is \`unconfirmed\` with a note. That is a successful record,
1438
+ and it is the useful one: nothing gates on this file, so the only thing it is for is
1439
+ telling the next reader the truth. \`loam archive\` will ship the feature either way —
1440
+ which is exactly why a \`confirmed\` you cannot back up costs nothing now and misleads
1441
+ everyone later.
1442
+ `,
1443
+ },
1444
+ {
1445
+ name: "loam-ship",
1446
+ description: "Archive a finished loam feature — merge its deltas into the living docs",
1447
+ argumentHint: "<FEAT-id>",
1448
+ body: `Archive a shipped feature.
1449
+
1450
+ 1. Confirm the code is actually built and merged. Archiving folds the delta into the
1451
+ living docs; doing it early makes the docs claim something that does not exist.
1452
+ \`loam status $1 --json\` answers "is this actually finished" in one call, and its
1453
+ \`next[]\` names what is outstanding if it is not.
1454
+ 2. \`loam validate --feature $1 --json\` — must come back \`valid: true\`.
1455
+ If it reports \`delta.baseline-missing\` or \`openapi.baseline-missing\`, run
1456
+ \`loam rebase $1\` and re-validate BEFORE going on: those two say nothing has
1457
+ pinned what this delta was written against, so the merge cannot tell the text
1458
+ you EDITED from the text you merely restated, and it upserts all of it —
1459
+ reverting whatever another feature landed in between, silently and with exit 0.
1460
+ 3. \`loam archive $1 --dry-run --json\` first. \`plan[]\` is every file the merge would
1461
+ write (\`create\` / \`update\`, then the final \`move\` into \`features/archive/\`), and
1462
+ none of them are written — read it before letting the merge touch the source of
1463
+ truth, and stop if a file you did not expect is on it. \`warnings[]\` is what the
1464
+ merge will do that is legal but lossy — advisory warnings never block, which is
1465
+ exactly why you read them now: \`openapi.op-modified\` means an operation the living
1466
+ contract already defines gets overwritten wholesale (\`openapi.component-modified\`
1467
+ is the same story for a component the merged operations carry),
1468
+ \`delta.baseline-missing\` / \`openapi.baseline-missing\` mean the pins are still not
1469
+ there and this merge may revert somebody (go back to step 2),
1470
+ \`service.no-model\` means a service arrives with no C4 centre behind it, and
1471
+ \`delta.added-conflict\`
1472
+ means another feature in flight adds the same requirement — the first to archive
1473
+ lands it, and the other's archive is then refused (\`delta.added-duplicate\`: its
1474
+ ADDED now collides with the living spec) unless a human \`--approve\`s the
1475
+ replacement.
1476
+ 4. \`loam archive $1 --json\`. It merges three axes into the living state — requirements
1477
+ into \`services/<svc>/spec.md\`, endpoints into \`services/<svc>/openapi.yaml\`,
1478
+ elements and edges into \`architecture/landscape.likec4\` — then moves the feature
1479
+ under \`features/archive/\`. Success is \`ok: true\`; on \`ok: false\`, branch on
1480
+ \`error.code\`:
1481
+ - \`not-coherent\` — gating coherence issues; \`issues[]\` in the envelope lists them,
1482
+ each with \`gates\` resolved (advisory warnings do not block). Fix the breaches.
1483
+ \`--approve\` overrides the gating issues and is a human's call to make, not
1484
+ yours: report the breach and stop. \`openapi.remove-marker-path-level\` arrives
1485
+ here too: an \`x-loam-remove\` written beside the methods instead of inside the
1486
+ operation retires nothing.
1487
+ - \`living-outside-requirements\` — the LIVING spec holds a requirement outside
1488
+ \`## Requirements\`, and the merge would duplicate it. Re-home it, then re-run.
1489
+ \`--approve\` does not move this one.
1490
+ - \`archive-exists\` — \`features/archive/\` already has that directory; a human
1491
+ decides what it is.
1492
+ - \`merge-failed\` — the merge could not be computed, or failed and was rolled
1493
+ back. Either way the living docs are unchanged; fix the reported cause, re-run.
1494
+ A file loam cannot decode as UTF-8 refuses here by name rather than being
1495
+ rewritten with replacement characters. So does a living document that still
1496
+ holds git conflict markers — the \`spec.merge-conflict\` /
1497
+ \`landscape.merge-conflict\` findings ride in \`findings[]\`, and \`--approve\`
1498
+ does not override them: the loss is mechanical, not a judgment call.
1499
+ - \`docs-busy\` — another \`loam archive\` or \`loam unarchive\` holds the docs
1500
+ repo's lock. Nothing was read or written; wait for it and re-run once.
1501
+ - \`commit-interrupted\` — a previous archive or unarchive was killed mid-commit
1502
+ and this run cannot repair it on its own: a half-written file was edited
1503
+ since, its pre-image is gone or altered, or \`.loam-commit\` cannot be read.
1504
+ Run \`loam doctor\` (\`doctor.commit-interrupted\` / \`doctor.commit-unreadable\`
1505
+ name the same state) and hand it to a human — re-running does not help.
1506
+ - \`rollback-incomplete\` — the merge failed AND some files could not be restored.
1507
+ Stop and hand it to a human; the message lists the files to check.
1508
+
1509
+ Archived by mistake? \`loam unarchive $1\` restores the living docs from the snapshot
1510
+ archive left in \`features/archive/$1*/.loam-before/\` and re-opens the feature. Do not
1511
+ hand-edit the living docs back instead: the previous text of a \`MODIFIED\` requirement
1512
+ is in that snapshot and nowhere else, so anything you write by hand is a guess. It
1513
+ refuses under \`snapshot-corrupt\` when a pre-image's bytes no longer match the digest
1514
+ the archive recorded — \`--force\` does not override that one, because the damage is
1515
+ to the undo itself — and under \`snapshot-missing\` for an archive older than the
1516
+ current snapshot format. Both mean version control is the way back.
1517
+ `,
1518
+ },
1519
+ ];
1520
+ /**
1521
+ * Pre-approved tools in a generated file's frontmatter, so an agent that honors
1522
+ * the field stops asking permission for each `loam` call. It only pre-approves:
1523
+ * everything else a command needs (Read, Write, the service's own test runner)
1524
+ * stays under the user's normal permission settings, and a tool that does not
1525
+ * know the field ignores it.
1526
+ */
1527
+ const LOAM_ALLOWED_TOOLS = "Bash(loam:*)";
1528
+ /**
1529
+ * The Agent Skills convention, which every tool in the registry that reads
1530
+ * skills at all spells the same way: `<tool-dir>/skills/<name>/SKILL.md`, YAML
1531
+ * frontmatter, then the shared body verbatim.
1532
+ *
1533
+ * `description` is load-bearing in a way no command file's is — it is the only
1534
+ * thing the model sees before deciding whether to load the skill — so it comes
1535
+ * from the same `CommandContent.description` the command wrappers use rather
1536
+ * than a second, skill-flavoured copy that could drift into a better pitch for
1537
+ * a worse protocol.
1538
+ */
1539
+ const skillsIn = (dir) => ({
1540
+ path: (name) => [dir, "skills", name, "SKILL.md"],
1541
+ format: (c) => `---\nname: ${c.name}\ndescription: ${c.description}\n` +
1542
+ `allowed-tools: ${LOAM_ALLOWED_TOOLS}\n---\n\n${c.body}`,
1543
+ });
1544
+ /**
1545
+ * TOML basic-string escapes for the one non-markdown format. Today's bodies
1546
+ * carry neither backslashes nor `"""`, but the escape is what keeps a future
1547
+ * body edit from silently corrupting the generated TOML.
1548
+ */
1549
+ const tomlLine = (s) => s.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
1550
+ const tomlBlock = (s) => s.replace(/\\/g, "\\\\").replace(/"""/g, '""\\"');
1551
+ /* The four markdown command dialects the registry actually needs. Shared so a
1552
+ * tool's entry is its PATH and its dialect's name — the part that differs —
1553
+ * instead of a wall of near-identical template literals. `claude` deliberately
1554
+ * spells its own wrapper inline even though `hintedMd` matches it byte for
1555
+ * byte: an edit made here for some other tool must not be able to re-spell the
1556
+ * one format that is frozen. */
1557
+ /** Description-only frontmatter — the most common dialect by a wide margin. */
1558
+ const describedMd = (c) => `---\ndescription: ${c.description}\n---\n\n${c.body}`;
1559
+ /** Description plus Claude's `argument-hint`, for the tools that copied it. */
1560
+ const hintedMd = (c) => `---\ndescription: ${c.description}\nargument-hint: ${c.argumentHint}\n---\n\n${c.body}`;
1561
+ /** Name plus description — the Cascade-style workflow frontmatter. */
1562
+ const namedMd = (c) => `---\nname: ${c.name}\ndescription: ${c.description}\n---\n\n${c.body}`;
1563
+ /** No frontmatter at all: a title line is the whole wrapper. */
1564
+ const titledMd = (c) => `# ${c.name}\n\n${c.body}`;
1565
+ /** The `loam-` prefix a namespace directory carries instead of the file name. */
1566
+ const unprefixed = (name) => name.replace(/^loam-/, "");
1567
+ /**
1568
+ * The command body a file actually gets: the shared protocol under the same
1569
+ * version stamp AGENTS.md carries.
1570
+ *
1571
+ * Applied once, here, rather than in twenty wrappers — the stamp is a fact
1572
+ * about loam, not about a tool's dialect, and a per-wrapper copy is twenty
1573
+ * chances to forget it. It rides at the top of the BODY (not the file) because
1574
+ * every dialect puts something of its own first: YAML frontmatter, a TOML key,
1575
+ * a title line. An HTML comment is invisible in all of them, and
1576
+ * `agentsStampVersion` matches it wherever in the file it lands.
1577
+ *
1578
+ * A repo initialized before stamping existed has unstamped files, which is
1579
+ * exactly what `doctor.agent-files-stale` is for: nobody has confirmed that
1580
+ * those instructions still describe this binary.
1581
+ */
1582
+ const stamped = (c) => ({
1583
+ ...c,
1584
+ body: `${agentsStampLine(LOAM_VERSION)}\n\n${c.body}`,
1585
+ });
1586
+ export const AGENT_TOOLS = {
1587
+ // The original target and the default. This WRAPPER must stay byte-identical
1588
+ // to what loam has always written — SLASH_COMMANDS below derives through it —
1589
+ // and an already-initialized repo must still read as fully scaffolded
1590
+ // (`skipped` is existsSync, never content). What the wrapper wraps now
1591
+ // carries a version stamp, which is the one thing about a generated file loam
1592
+ // will report on; see `stamped`.
1593
+ claude: {
1594
+ path: (name) => [".claude", "commands", `${name}.md`],
1595
+ format: (c) => `---\ndescription: ${c.description}\nargument-hint: ${c.argumentHint}\n---\n\n${c.body}`,
1596
+ skill: skillsIn(".claude"),
1597
+ detect: [".claude"],
1598
+ },
1599
+ // Cursor invokes by flat file name (`/loam-check`); its frontmatter `name`
1600
+ // field spells that invocation form.
1601
+ cursor: {
1602
+ path: (name) => [".cursor", "commands", `${name}.md`],
1603
+ format: (c) => `---\nname: /${c.name}\n---\n\n${c.body}`,
1604
+ skill: skillsIn(".cursor"),
1605
+ detect: [".cursor"],
1606
+ },
1607
+ // Copilot prompt files: `.prompt.md` extension, description-only frontmatter.
1608
+ // Detection cannot use `.github/` — almost every repo has one and almost none
1609
+ // of them means Copilot — so it looks for the files Copilot itself reads.
1610
+ "github-copilot": {
1611
+ path: (name) => [".github", "prompts", `${name}.prompt.md`],
1612
+ format: describedMd,
1613
+ skill: skillsIn(".github"),
1614
+ detect: [
1615
+ ".github/copilot-instructions.md",
1616
+ ".github/instructions",
1617
+ ".github/prompts",
1618
+ ".github/agents",
1619
+ ".github/skills",
1620
+ ".github/workflows/copilot-setup-steps.yml",
1621
+ ".github/.mcp.json",
1622
+ ],
1623
+ },
1624
+ // Gemini: TOML, and a namespace DIRECTORY — `.gemini/commands/loam/check.toml`
1625
+ // is invoked as `/loam:check`, so the file name drops the `loam-` prefix the
1626
+ // flat-named tools carry.
1627
+ gemini: {
1628
+ path: (name) => [".gemini", "commands", "loam", `${unprefixed(name)}.toml`],
1629
+ format: (c) => `description = "${tomlLine(c.description)}"\nprompt = """\n${tomlBlock(c.body)}"""`,
1630
+ skill: skillsIn(".gemini"),
1631
+ detect: [".gemini"],
1632
+ },
1633
+ opencode: {
1634
+ path: (name) => [".opencode", "commands", `${name}.md`],
1635
+ format: describedMd,
1636
+ skill: skillsIn(".opencode"),
1637
+ detect: [".opencode"],
1638
+ },
1639
+ // Cline reads workflows from `.clinerules/` — plain markdown, no frontmatter —
1640
+ // but its skills from `.cline/`. The two directories are unrelated, so both
1641
+ // count as a sighting.
1642
+ cline: {
1643
+ path: (name) => [".clinerules", "workflows", `${name}.md`],
1644
+ format: (c) => `# ${c.name}\n\n${c.body}`,
1645
+ skill: skillsIn(".cline"),
1646
+ detect: [".cline", ".clinerules"],
1647
+ },
1648
+ // Amazon Q surfaces these as its PROMPT library, not as commands: the file is
1649
+ // invoked `@loam-check`, never `/loam-check`. Same file shape regardless.
1650
+ "amazon-q": {
1651
+ path: (name) => [".amazonq", "prompts", `${name}.md`],
1652
+ format: describedMd,
1653
+ skill: skillsIn(".amazonq"),
1654
+ detect: [".amazonq"],
1655
+ },
1656
+ // Antigravity's directory is `.agent/`, which is why the id and the directory
1657
+ // do not match here as they do almost everywhere else.
1658
+ antigravity: {
1659
+ path: (name) => [".agent", "workflows", `${name}.md`],
1660
+ format: describedMd,
1661
+ skill: skillsIn(".agent"),
1662
+ detect: [".agent"],
1663
+ },
1664
+ // Auggie is the Augment CLI: id `auggie`, directory `.augment/`.
1665
+ auggie: {
1666
+ path: (name) => [".augment", "commands", `${name}.md`],
1667
+ format: hintedMd,
1668
+ skill: skillsIn(".augment"),
1669
+ detect: [".augment"],
1670
+ },
1671
+ // Skills-only, deliberately: Codex reads `.codex/skills/` and does not load
1672
+ // custom command files, so it declares no command emitter at all rather than
1673
+ // laying down files nothing will ever open.
1674
+ codex: {
1675
+ skill: skillsIn(".codex"),
1676
+ detect: [".codex"],
1677
+ },
1678
+ // Continue's prompt files take a bare `.prompt` extension, and `invokable`
1679
+ // is what promotes one from a template to something you can call.
1680
+ continue: {
1681
+ path: (name) => [".continue", "prompts", `${name}.prompt`],
1682
+ format: (c) => `---\nname: ${c.name}\ndescription: ${c.description}\ninvokable: true\n---\n\n${c.body}`,
1683
+ skill: skillsIn(".continue"),
1684
+ detect: [".continue"],
1685
+ },
1686
+ // Crush namespaces like Gemini does — `.crush/commands/loam/check.md` is
1687
+ // `/loam:check` — but stays markdown.
1688
+ crush: {
1689
+ path: (name) => [".crush", "commands", "loam", `${unprefixed(name)}.md`],
1690
+ format: namedMd,
1691
+ skill: skillsIn(".crush"),
1692
+ detect: [".crush"],
1693
+ },
1694
+ // Devin Desktop, formerly Windsurf: Cascade-style workflows, and the rename
1695
+ // moved the directory from `.windsurf/` to `.devin/`. New files go to the new
1696
+ // directory; a repo still holding the old one is still a Devin repo, so both
1697
+ // are sightings.
1698
+ devin: {
1699
+ path: (name) => [".devin", "workflows", `${name}.md`],
1700
+ format: namedMd,
1701
+ skill: skillsIn(".devin"),
1702
+ detect: [".devin", ".windsurf"],
1703
+ },
1704
+ factory: {
1705
+ path: (name) => [".factory", "commands", `${name}.md`],
1706
+ format: hintedMd,
1707
+ skill: skillsIn(".factory"),
1708
+ detect: [".factory"],
1709
+ },
1710
+ junie: {
1711
+ path: (name) => [".junie", "commands", `${name}.md`],
1712
+ format: describedMd,
1713
+ skill: skillsIn(".junie"),
1714
+ detect: [".junie"],
1715
+ },
1716
+ // Kilo Code workflows carry no frontmatter — the title line is all the
1717
+ // wrapper there is.
1718
+ kilocode: {
1719
+ path: (name) => [".kilocode", "workflows", `${name}.md`],
1720
+ format: titledMd,
1721
+ skill: skillsIn(".kilocode"),
1722
+ detect: [".kilocode"],
1723
+ },
1724
+ // Kiro uses Copilot's `.prompt.md` extension under its own directory.
1725
+ kiro: {
1726
+ path: (name) => [".kiro", "prompts", `${name}.prompt.md`],
1727
+ format: describedMd,
1728
+ skill: skillsIn(".kiro"),
1729
+ detect: [".kiro"],
1730
+ },
1731
+ // Qwen Code retired its TOML commands in favour of markdown + frontmatter,
1732
+ // so this is markdown despite the Gemini lineage.
1733
+ qwen: {
1734
+ path: (name) => [".qwen", "commands", `${name}.md`],
1735
+ format: describedMd,
1736
+ skill: skillsIn(".qwen"),
1737
+ detect: [".qwen"],
1738
+ },
1739
+ // Roo/Zoo Code: id `roocode`, directory `.roo/`, no frontmatter.
1740
+ roocode: {
1741
+ path: (name) => [".roo", "commands", `${name}.md`],
1742
+ format: titledMd,
1743
+ skill: skillsIn(".roo"),
1744
+ detect: [".roo"],
1745
+ },
1746
+ trae: {
1747
+ path: (name) => [".trae", "commands", `${name}.md`],
1748
+ format: namedMd,
1749
+ skill: skillsIn(".trae"),
1750
+ detect: [".trae"],
1751
+ },
1752
+ };
1753
+ /**
1754
+ * The Claude-format files, keyed by command name. Kept as the exported shape
1755
+ * because it IS the on-disk contract of every repo initialized before
1756
+ * `--tools` existed — and derived through the same adapter that writes the
1757
+ * files, so the export and the disk can never disagree.
1758
+ */
1759
+ export const SLASH_COMMANDS = Object.fromEntries(COMMANDS.map((c) => [c.name, AGENT_TOOLS["claude"].format(stamped(c))]));
1760
+ /** The two ways a command body reaches an agent. Both are on by default. */
1761
+ export const DELIVERIES = ["commands", "skills"];
1762
+ /**
1763
+ * Every file the selected tools would lay down, in creation order — the same
1764
+ * list `init` probes for `skipped` BEFORE the scaffold runs, so created +
1765
+ * skipped is the same list on every repo.
1766
+ *
1767
+ * Called with two arguments it returns EVERYTHING the binary would write under
1768
+ * default delivery: commands and skills both. That is the contract callers
1769
+ * outside this module depend on — `loam doctor` asks "what should be here?"
1770
+ * and a half-answer would report a fully-scaffolded repo as fully scaffolded
1771
+ * while ignoring every skill file. The name predates the second delivery and is
1772
+ * kept because it is imported elsewhere; `delivery` narrows it.
1773
+ */
1774
+ export function plannedCommandFiles(cwd, toolIds, delivery = DELIVERIES) {
1775
+ return toolIds.flatMap((id) => {
1776
+ const tool = AGENT_TOOLS[id];
1777
+ if (tool === undefined)
1778
+ throw new Error(`unknown agent tool: ${id}`);
1779
+ const emitters = [];
1780
+ // A tool declares only the deliveries it actually reads, so an absent
1781
+ // emitter is a fact about the tool, never a reason to fall back to some
1782
+ // other tool's convention.
1783
+ const { path, format, skill } = tool;
1784
+ if (delivery.includes("commands") && path !== undefined && format !== undefined) {
1785
+ emitters.push({ path, format });
1786
+ }
1787
+ if (delivery.includes("skills") && skill !== undefined)
1788
+ emitters.push(skill);
1789
+ return emitters.flatMap((e) => COMMANDS.map((c) => ({ path: join(cwd, ...e.path(c.name)), content: e.format(stamped(c)) })));
1790
+ });
1791
+ }
1792
+ /**
1793
+ * Write the command and skill files for the selected tools into `cwd`. Existing
1794
+ * files are left alone — the never-overwrite contract covers every tool and
1795
+ * every delivery, not just the default one. Returns the paths created.
1796
+ */
1797
+ export async function scaffoldAgentCommands(cwd, toolIds = ["claude"], delivery = DELIVERIES) {
1798
+ const created = [];
1799
+ for (const { path, content } of plannedCommandFiles(cwd, toolIds, delivery)) {
1800
+ if (existsSync(path))
1801
+ continue;
1802
+ await mkdir(dirname(path), { recursive: true });
1803
+ await writeFile(path, content, "utf8");
1804
+ created.push(path);
1805
+ }
1806
+ return created;
1807
+ }
1808
+ /**
1809
+ * The registry ids whose marker paths are present in `cwd` — what `loam init`
1810
+ * writes for when nobody passed `--tools`.
1811
+ *
1812
+ * A repo with `.cursor/` and no Claude Code used to get `.claude/commands/`
1813
+ * and nothing it could actually run. Presence is the whole test, and existence
1814
+ * (not directory-ness) is what is asked, because a marker is as often a file as
1815
+ * a directory — `.github/copilot-instructions.md` is the honest signal for
1816
+ * Copilot where `.github/` itself is noise.
1817
+ */
1818
+ export function detectAgentTools(cwd) {
1819
+ return Object.entries(AGENT_TOOLS)
1820
+ .filter(([, tool]) => tool.detect.some((marker) => existsSync(join(cwd, marker))))
1821
+ .map(([id]) => id);
1822
+ }
1823
+ //# sourceMappingURL=agent.js.map