@ybotok/loam 0.0.0 → 0.1.0-beta.2

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 (126) hide show
  1. package/CHANGELOG.md +138 -0
  2. package/COMPARISON.md +63 -0
  3. package/LICENSE +21 -0
  4. package/MIGRATING-from-OpenSpec.md +202 -0
  5. package/README.md +246 -9
  6. package/SCHEMA.md +425 -0
  7. package/SECURITY.md +9 -0
  8. package/dist/cli.js +142 -0
  9. package/dist/cli.js.map +1 -0
  10. package/dist/commands/adopt.js +240 -0
  11. package/dist/commands/adopt.js.map +1 -0
  12. package/dist/commands/archive.js +1313 -0
  13. package/dist/commands/archive.js.map +1 -0
  14. package/dist/commands/delta.js +388 -0
  15. package/dist/commands/delta.js.map +1 -0
  16. package/dist/commands/dependencies.js +83 -0
  17. package/dist/commands/dependencies.js.map +1 -0
  18. package/dist/commands/docs-repo-gate.js +104 -0
  19. package/dist/commands/docs-repo-gate.js.map +1 -0
  20. package/dist/commands/doctor.js +85 -0
  21. package/dist/commands/doctor.js.map +1 -0
  22. package/dist/commands/explore.js +195 -0
  23. package/dist/commands/explore.js.map +1 -0
  24. package/dist/commands/format.js +17 -0
  25. package/dist/commands/format.js.map +1 -0
  26. package/dist/commands/gherkin.js +309 -0
  27. package/dist/commands/gherkin.js.map +1 -0
  28. package/dist/commands/init.js +250 -0
  29. package/dist/commands/init.js.map +1 -0
  30. package/dist/commands/instructions.js +70 -0
  31. package/dist/commands/instructions.js.map +1 -0
  32. package/dist/commands/list.js +291 -0
  33. package/dist/commands/list.js.map +1 -0
  34. package/dist/commands/migrate-openspec.js +950 -0
  35. package/dist/commands/migrate-openspec.js.map +1 -0
  36. package/dist/commands/new.js +413 -0
  37. package/dist/commands/new.js.map +1 -0
  38. package/dist/commands/rebase.js +356 -0
  39. package/dist/commands/rebase.js.map +1 -0
  40. package/dist/commands/show.js +320 -0
  41. package/dist/commands/show.js.map +1 -0
  42. package/dist/commands/status.js +254 -0
  43. package/dist/commands/status.js.map +1 -0
  44. package/dist/commands/unarchive.js +317 -0
  45. package/dist/commands/unarchive.js.map +1 -0
  46. package/dist/commands/validate.js +1591 -0
  47. package/dist/commands/validate.js.map +1 -0
  48. package/dist/commands/verify.js +775 -0
  49. package/dist/commands/verify.js.map +1 -0
  50. package/dist/commands/vouch.js +343 -0
  51. package/dist/commands/vouch.js.map +1 -0
  52. package/dist/core/agent.js +2065 -0
  53. package/dist/core/agent.js.map +1 -0
  54. package/dist/core/agents-stamp.js +68 -0
  55. package/dist/core/agents-stamp.js.map +1 -0
  56. package/dist/core/arch.js +130 -0
  57. package/dist/core/arch.js.map +1 -0
  58. package/dist/core/brief.js +733 -0
  59. package/dist/core/brief.js.map +1 -0
  60. package/dist/core/coherence.js +577 -0
  61. package/dist/core/coherence.js.map +1 -0
  62. package/dist/core/concurrency.js +51 -0
  63. package/dist/core/concurrency.js.map +1 -0
  64. package/dist/core/config.js +180 -0
  65. package/dist/core/config.js.map +1 -0
  66. package/dist/core/delta.js +476 -0
  67. package/dist/core/delta.js.map +1 -0
  68. package/dist/core/dependencies.js +356 -0
  69. package/dist/core/dependencies.js.map +1 -0
  70. package/dist/core/docs.js +144 -0
  71. package/dist/core/docs.js.map +1 -0
  72. package/dist/core/doctor.js +559 -0
  73. package/dist/core/doctor.js.map +1 -0
  74. package/dist/core/document-bytes.js +59 -0
  75. package/dist/core/document-bytes.js.map +1 -0
  76. package/dist/core/explore.js +225 -0
  77. package/dist/core/explore.js.map +1 -0
  78. package/dist/core/fleet-context.js +225 -0
  79. package/dist/core/fleet-context.js.map +1 -0
  80. package/dist/core/frontmatter.js +199 -0
  81. package/dist/core/frontmatter.js.map +1 -0
  82. package/dist/core/gherkin.js +463 -0
  83. package/dist/core/gherkin.js.map +1 -0
  84. package/dist/core/health.js +67 -0
  85. package/dist/core/health.js.map +1 -0
  86. package/dist/core/ids.js +122 -0
  87. package/dist/core/ids.js.map +1 -0
  88. package/dist/core/issue.js +9 -0
  89. package/dist/core/issue.js.map +1 -0
  90. package/dist/core/json.js +74 -0
  91. package/dist/core/json.js.map +1 -0
  92. package/dist/core/likec4.js +479 -0
  93. package/dist/core/likec4.js.map +1 -0
  94. package/dist/core/maturity.js +66 -0
  95. package/dist/core/maturity.js.map +1 -0
  96. package/dist/core/openapi-merge.js +552 -0
  97. package/dist/core/openapi-merge.js.map +1 -0
  98. package/dist/core/openapi.js +255 -0
  99. package/dist/core/openapi.js.map +1 -0
  100. package/dist/core/openspec-inventory.js +1440 -0
  101. package/dist/core/openspec-inventory.js.map +1 -0
  102. package/dist/core/path-safety.js +168 -0
  103. package/dist/core/path-safety.js.map +1 -0
  104. package/dist/core/provenance.js +720 -0
  105. package/dist/core/provenance.js.map +1 -0
  106. package/dist/core/records.js +33 -0
  107. package/dist/core/records.js.map +1 -0
  108. package/dist/core/repo.js +436 -0
  109. package/dist/core/repo.js.map +1 -0
  110. package/dist/core/report.js +47 -0
  111. package/dist/core/report.js.map +1 -0
  112. package/dist/core/results.js +199 -0
  113. package/dist/core/results.js.map +1 -0
  114. package/dist/core/spec.js +481 -0
  115. package/dist/core/spec.js.map +1 -0
  116. package/dist/core/staging.js +890 -0
  117. package/dist/core/staging.js.map +1 -0
  118. package/dist/core/status.js +1104 -0
  119. package/dist/core/status.js.map +1 -0
  120. package/dist/core/steps.js +72 -0
  121. package/dist/core/steps.js.map +1 -0
  122. package/dist/core/verify.js +807 -0
  123. package/dist/core/verify.js.map +1 -0
  124. package/dist/core/version.js +24 -0
  125. package/dist/core/version.js.map +1 -0
  126. package/package.json +63 -4
@@ -0,0 +1,2065 @@
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
+ Then \`loam explore <service> --json\` around the services you believe are
361
+ involved — it writes nothing and answers the one question the next step takes
362
+ as an argument: which services this change actually touches. It reports the
363
+ ring one hop out in the fleet map (\`neighbours\`), how far each service's
364
+ documentation has got (\`maturity\`), what each already exposes, the features
365
+ already in flight over the same ground (\`overlaps\`), and the \`loam new\` line
366
+ the seeds imply (\`scaffold\`). \`--op <operationId>\` seeds from an operation
367
+ when you know the call but not who owns it.
368
+ Never propose a change to a service you have not read.
369
+ 3. **Scaffold** — \`loam new FEAT-101 --title "..." --touches <existing> --new-service <new>\`.
370
+ The scaffold leaves a \`delta.likec4\` whose context elements are commented out —
371
+ a requirements-only feature should DELETE that file rather than ship an empty
372
+ one — and gives every \`--new-service\` a \`specs/<svc>/arch.spec.md\` alongside.
373
+ 4. **Author** the four files the scaffold left as TODO: intent, delta.likec4,
374
+ a spec.md per service, an openapi.yaml per new service. Finish with
375
+ \`loam rebase FEAT-101\`: it pins every MODIFIED/REMOVED requirement and every
376
+ operation in the contract delta to the living version you wrote it against.
377
+ On the contract axis run it even when you changed nothing but one endpoint —
378
+ that is exactly what marks the REST of the document as quotation the merge
379
+ must not write. See "The requirement baseline".
380
+ 5. **Check** — \`loam validate FEAT-101 --json\`. Fix every error before writing code.
381
+ \`loam dependencies --json\` says whether another feature in flight has to
382
+ archive first.
383
+ 6. **Build** — \`loam delta FEAT-101 --service <svc> --json\` is the task for one service:
384
+ intent, its requirement delta with scenarios verbatim, and the edges around it.
385
+ In the service's own repo, \`loam gherkin FEAT-101\` then emits those scenarios as
386
+ \`.feature\` files under \`<gherkinDir>/loam/\` — write step definitions against them
387
+ **first**, outside \`loam/\`, and implement until they pass.
388
+ If \`architecture.errors\` is non-empty the payload keeps \`ok: true\` (the command
389
+ ran) but the exit code is 1: the C4 slice is empty because delta.likec4 did not
390
+ parse, not because the feature changes no architecture — do not build on it.
391
+ 7. **Verify** — \`loam verify FEAT-101 --json\` turns the feature's own promises into a
392
+ checklist. The scenario claims are answered by the cucumber report
393
+ (\`--results report.json\`); answer the rest with evidence and record them
394
+ (\`--record answers.json --service <id>\`), **from each affected service's own
395
+ repo**. See "The done-check".
396
+ 8. **Ship** — \`loam archive FEAT-101\` once the code is merged. \`--dry-run\` shows
397
+ every file the merge would write, and writes none of them.
398
+
399
+ ## Adopting a service
400
+
401
+ \`loam adopt --service <id>\` does not read code and never will. Nothing
402
+ deterministic can look at a legacy service and say what its architecture MEANS,
403
+ and a guessed model is worse than none: everybody downstream has to re-derive it
404
+ to know whether to believe it. So loam takes the half of the job that is
405
+ mechanical — stating the work, and checking the result — and you do the reading.
406
+
407
+ The brief names every file to write, the grammar each must be in, what the
408
+ landscape already says about the service (bind to those elements, do not draw a
409
+ parallel fleet), the frontmatter, and the checks that follow. It also lists what
410
+ **no** check will ever catch. Read that list: everything on it is a way to be
411
+ wrong quietly, and \`loam validate\` passing means nothing about any of it.
412
+
413
+ One of the targets is not under \`services/<id>/\`: \`architecture/landscape.likec4\`,
414
+ the fleet map, carried with \`action: "edit"\` and a \`landscape.instruction\`
415
+ spelling out the block this service owes it. That target appears only while
416
+ nothing in the map resolves to the service. **Add to that file; never rewrite
417
+ it** — every other service's elements and edges are in there. A service that is
418
+ fully documented and undrawn is \`landscape.service-unmodelled\` (error) and is
419
+ invisible to every cross-service check: no edge into it can be graded against
420
+ its openapi.yaml, and no feature can draw a call to it.
421
+
422
+ An adoption ends with two runs, not one: \`loam validate --service <id> --json\`
423
+ for the baseline, then \`loam validate --all --json\` in the docs repo — the
424
+ fleet cross-check the first invocation does not perform is exactly where a
425
+ landscape edit that did not land shows up.
426
+
427
+ Two rules the brief repeats and this file will too. An artifact that already
428
+ exists is reported as \`action: "diff"\` — read it and report what disagrees;
429
+ never replace it, because a document somebody wrote is evidence. And everything
430
+ you write is \`status: draft\`: promotion is \`loam vouch\`, run by a person in
431
+ the service's own repository.
432
+
433
+ ## The done-check
434
+
435
+ \`loam verify <FEAT>\` derives a checklist from the feature's own artifacts —
436
+ one claim per new service, per operation the delta adds, per tagged edge that
437
+ names an operation, and per scenario of every changed requirement, arch.spec.md
438
+ deltas included (those claims name their file, and the test they ask for is an
439
+ integration/ops test, not an acceptance test). Each claim has a stable id, so
440
+ two runs are diffable and an answer cannot drift onto a different question.
441
+
442
+ Two answer channels, and they never overlap. The \`scenario.tested\` claims are
443
+ the TEST RUNNER's to answer: run the generated suite with a cucumber JSON
444
+ report (\`cucumber-js --format json:report.json\`) and pass it back —
445
+ \`loam verify <FEAT> --results report.json\`. A claim is confirmed only when a
446
+ report scenario carrying its \`@loam-digest-<16hex>\` tag ran at least one step
447
+ and every step passed — every occurrence, when the report holds a re-run, and
448
+ every before/after hook with it. A failed, undefined, pending or skipped step,
449
+ a failed hook, or no matching scenario at all, is \`unconfirmed\` with the
450
+ reason. The digest is the only identity: a reworded spec scenario matches
451
+ nothing until the suite is regenerated and re-run. When a report exists,
452
+ ALWAYS pass \`--results\` — under it the runner owns every scenario claim, and
453
+ an answers-file entry for one is refused.
454
+
455
+ \`--record\` without \`--results\` is the fallback for a service with no runnable
456
+ suite yet: there your word answers the scenario claims too. It still works, and
457
+ it costs something you must know about. The record says who answered
458
+ (\`answered_by: agent\`), and that answer changes the VERDICT: a feature whose
459
+ scenario claims rest on an agent's word is **attested**, not verified.
460
+ \`verify --json\` reports \`verdict: "attested"\` with \`verified: false\` and an
461
+ \`attested\` count, \`loam status\` marks it and adds a \`next.verify-attested\` step,
462
+ and the notice rides in \`notices[]\` under \`verify.scenario-attested\` (warn —
463
+ nothing gates on it, because a legacy service with no suite must still be able
464
+ to ship). Answer them mechanically the moment a suite runs:
465
+ \`loam verify <FEAT> --results <report.json>\`.
466
+
467
+ Two more codes belong to the record itself. \`verify.record-miscounted\` (error):
468
+ a \`verification.yaml\` whose \`summary\` block contradicts its own \`claims[]\` — the
469
+ record is refused as unreadable rather than believed in either half, and
470
+ re-recording is the repair, never editing the counts. \`verify.digest-contested\`
471
+ (warn): two or more services word a scenario identically, so they share one
472
+ \`@loam-digest-…\` tag and no single cucumber report can say whose suite ran it —
473
+ those scenario claims are left unconfirmed, and each service records its own
474
+ from its own repository with \`--service\`.
475
+
476
+ A consumed report is written down: \`report\` on the record (or on that
477
+ repository's attestation) carries the path, a sha256 of the bytes, the file's
478
+ mtime and how many tagged scenarios it held. That identifies WHICH file
479
+ answered the claims, and — when the file is committed — that it matches the
480
+ attested commit. It does not prove the file came from executing that commit; no
481
+ digest can.
482
+
483
+ Everything else you answer from the code, and \`--record answers.json\` takes
484
+ those back, writing \`features/<FEAT>/verification.yaml\` — it travels into the
485
+ archive with the feature and reads without loam. With \`--results\`, the answers
486
+ file must answer exactly the non-scenario claims: an entry for a scenario
487
+ claim is refused (\`answers-mismatch\` — the runner owns it), and \`--results\`
488
+ alone is refused while non-scenario claims are outstanding. Every recorded
489
+ verdict says who answered it (\`answered_by: runner | agent\`), so a reviewer
490
+ can tell a green run from somebody's word.
491
+
492
+ **A cross-service feature is verified once per service, and the form matters.**
493
+ The claims of one feature belong to several services, and each service's code
494
+ lives in its own repository — so the recording form is federated:
495
+
496
+ \`\`\`sh
497
+ # in EACH affected service's own repo, not in the docs repo:
498
+ loam verify FEAT-101 --service payment-service --results report.json --record answers.json
499
+ \`\`\`
500
+
501
+ \`--service <id>\` narrows the checklist to that service's claims and binds the
502
+ answers to this repository's commit, producing one **attestation** per service
503
+ inside a single \`schema: 2\` \`verification.yaml\`. Re-running it in the next
504
+ repo adds that repo's attestation and leaves the others standing. Omitting
505
+ \`--service\` in a repo whose \`loam.json\` declares one substitutes it, so the
506
+ short form is safe there; a \`--service\` naming a service this repo does not
507
+ declare is refused (\`service-mismatch\`), and \`--service\` from a repo that
508
+ declares none is refused (\`repository-unavailable\`) — an attestation has to be
509
+ bound to the code it is about.
510
+
511
+ ⚠ **The legacy all-at-once form \`loam verify <FEAT> --record answers.json\` with
512
+ no \`--service\` writes the whole record from one place.** Over a record that
513
+ already carries other services' attestations it is refused outright
514
+ (\`record-federated\`, naming the attestors) rather than merged or migrated — the
515
+ alternative was one repo silently erasing nine others' evidence. Use it only for
516
+ a single-service feature that has never been recorded federated. A
517
+ \`verification.yaml\` that exists but cannot be read is \`record-unreadable\`: it is
518
+ never overwritten and never reported as "no record".
519
+
520
+ Reading is not writing: \`loam verify <FEAT> --service <id>\` **without**
521
+ \`--record\` is a pure lens on the checklist and works from anywhere, docs repo
522
+ included. Every run lists all subjects in \`services\`, and a recorded run reports
523
+ in \`discarded\` every previous answer the new record does not carry.
524
+
525
+ It refuses an answer set that does not answer the current checklist: an id that
526
+ is not on it (\`answers-mismatch\`), a claim with no answer (same), or a
527
+ \`confirmed\` with no evidence (\`answers-unevidenced\`). Evidence is \`file:line\`.
528
+ A claim you cannot show is \`unconfirmed\` with a note saying why — that is a
529
+ successful record, and it is more useful to the next reader than a yes.
530
+
531
+ **Nothing gates on it.** \`loam archive\` will ship an unverified feature. That
532
+ is deliberate: coherence is something loam computed from the documents, and a
533
+ verdict is your word about code loam never read. A gate in front of shipping
534
+ would only teach everyone that the cheapest way past it is to say yes. The
535
+ record is for the reviewer who comes later, so leave it true.
536
+
537
+ ## What you author, what loam derives
538
+
539
+ Authored by hand: \`intent.md\`, \`delta.likec4\`, \`specs/<svc>/spec.md\`,
540
+ \`specs/<svc>/arch.spec.md\`, \`specs/<svc>/openapi.yaml\`, ADRs, runbooks, health.
541
+
542
+ Written by loam: the merge into \`services/<svc>/spec.md\`, \`services/<svc>/arch.spec.md\`,
543
+ \`services/<svc>/openapi.yaml\` and \`architecture/landscape.likec4\` at archive time.
544
+
545
+ **Do not hand-edit the living landscape to add a feature's changes.** Put them in a
546
+ delta and archive it — otherwise the change has no intent, no requirement and no
547
+ history behind it.
548
+
549
+ ## Rules the validator enforces
550
+
551
+ - every requirement has at least one \`#### Scenario:\` — scenarios are the acceptance
552
+ criteria and the source for tests;
553
+ - every operationId a requirement governs exists in that service's OpenAPI;
554
+ - every C4 edge's \`op\` resolves to an operation the target actually exposes;
555
+ - every operation is governed by some requirement (warning);
556
+ - a "Calls" edge with no \`metadata { op }\` is unlinked (warning);
557
+ - **the map is complete**: every \`services/<svc>/\` has an element in the landscape,
558
+ and every element that looks like a service has a directory (warning) or is
559
+ tagged \`#external\`;
560
+ - **the diff applies**: a \`MODIFIED\` or \`REMOVED\` requirement exists in the living
561
+ spec, an \`ADDED\` one does not, and a section heading matches the grammar exactly;
562
+ - a \`Requirement-ID: <id>\` is optional, but when present it is valid and unique;
563
+ archive selects by it, so carry the same ID through \`MODIFIED\` to rename a heading;
564
+ - **every requirement in a delta is under a delta section**: one under a prose heading
565
+ (\`## Behavior\`) is documentation as far as the merge is concerned and is dropped
566
+ (warning). \`## Requirements\` is exempt — quoting the living state is legal.
567
+
568
+ That last pair of rules exists because each of its failures used to be silent.
569
+ \`MODIFIED\` of a requirement that does not exist merged as a creation; \`ADDED\` of one
570
+ that does exist REPLACED it, scenarios and all; a near-miss heading like
571
+ \`## ADDED Requirement\` (singular) parses as plain prose, so archive merged nothing and
572
+ said nothing; and a requirement under \`## Behavior\` vanished the same way.
573
+
574
+ Write the delta section heading on its own line exactly as \`## ADDED Requirements\`.
575
+ The heading is the only thing that gives a requirement its kind: without a matching
576
+ one, archive merges the requirement as nothing.
577
+
578
+ Severity and gating are two different questions. An ERROR means the document is
579
+ invalid — \`loam validate\` fails. A finding with \`gates: true\` means \`loam archive\`
580
+ will refuse — the merge is unsafe. They usually agree (errors gate, warnings do
581
+ not), and where they diverge the \`--json\` output says so: \`delta.requirement-not-merged\`
582
+ is a warning (the shape is legal OpenSpec) that gates (the merge would drop the
583
+ requirement). Leave a warning standing only deliberately.
584
+
585
+ ## Reading loam's output
586
+
587
+ Every command takes \`--json\`, and the envelope holds even when the INVOCATION
588
+ is wrong: with \`--json\` anywhere in the arguments, an unknown flag, unknown
589
+ command or missing argument still yields \`{ ok: false, error: { code:
590
+ "invalid-option" } }\` on stdout with exit 1 (commander's own diagnostic goes
591
+ to stderr; \`--help\` and \`--version\` output pass through untouched). Without
592
+ \`--json\` the same refusal is plain text on stderr with nothing on stdout.
593
+
594
+ Branch on \`findings[].code\`, never on the prose — the wording changes, the codes do
595
+ not. The code-by-code fix table lives in the \`/loam-check\` command \`loam init\` lays
596
+ down. \`init\` writes that body twice for every tool it configures: once as a slash
597
+ command in the tool's own command directory (you type it), and once as an Agent
598
+ Skill at \`<tool-dir>/skills/loam-check/SKILL.md\` (the model loads it by itself when
599
+ the task matches the skill's description). Same body, two ways in.
600
+
601
+ Which tools get them is detected from the repo: \`loam init\` scans for the
602
+ dot-directories of the tools it knows (\`.claude\`, \`.cursor\`, \`.gemini\`, …) and
603
+ writes for the ones it finds, falling back to Claude Code when it finds none.
604
+ \`loam init --tools <ids|all>\` overrides the scan — an unknown id is refused
605
+ (\`invalid-option\`) rather than skipped — and \`--no-commands\` / \`--no-skills\`
606
+ each suppress one delivery. The tools written for are recorded in
607
+ \`loam.json\` as \`agentTools\`, which is how \`loam doctor\` tells a file missing
608
+ because the binary grew a new command from one missing because nobody ever
609
+ selected that tool. The map of which invocation surfaces what:
610
+
611
+ - \`loam status [<FEAT>]\` is the orientation surface — the question you have when
612
+ you join a repository halfway, or come back having lost the session, and the one
613
+ every other command assumed you could already answer. It writes nothing and
614
+ stores nothing: there is no state file, every answer is re-derived from the
615
+ files, so a document someone edited in another window is visible on the next run
616
+ with nothing to invalidate. Artifacts come back as \`missing\` (owed, nothing in
617
+ the way — write it now), \`blocked\` (not written and not writable yet; the entry
618
+ names what comes first), \`draft\` (on disk, and the shared checks report an error
619
+ against it — what exists is wrong), \`ready\` (on disk and clean, but something
620
+ outside the documents — code, a test run, a recording — still has to answer it)
621
+ or \`done\`. The payload's reason to exist is \`next[]\`: ordered, first entry
622
+ first, each carrying a code and the literal command to run. \`next.recover-commit\`
623
+ outranks every other step in both forms and is never elided: a \`.loam-commit\`
624
+ journal says an archive or unarchive was killed mid-commit, so some of the files
625
+ everything below is derived from may be half-written. Its command is that exact
626
+ archive/unarchive re-run, which repairs from the pre-image first, under the lock
627
+ — except when the journal itself cannot be read, where it is \`loam doctor\` and
628
+ the repair is a human's comparison against version control. Fleet-wide the rest are
629
+ \`next.adopt\` (a service with no spec.md — nothing about it is written down, so
630
+ no feature can be graded against it), \`next.complete-service\` (a living spec.md
631
+ with no model.likec4 beside it), \`next.feature\` (something is in flight;
632
+ ask \`loam status <FEAT>\` about it), \`next.archive\` (authored and verified —
633
+ ship it, and everything waiting on it is released), \`next.fleet-clean\`
634
+ (nothing is owed), \`next.elided\` (the fleet list hit its cap and says how many
635
+ steps of the same kinds it left out — it is ordered most-unblocking first, so
636
+ work down it and re-run) and \`next.fleet-gate\` (always last while anything is
637
+ outstanding: \`loam validate --all\` is what CI runs, and the fleet form grades
638
+ nothing itself). On one feature: \`next.author-intent\`, \`next.touch-service\`
639
+ (no per-service delta at all yet), \`next.author-spec\`, \`next.author-openapi\`,
640
+ \`next.author-scenarios\`, \`next.rebase\` (requirements or operations with no
641
+ baseline pin — until they have one the merge cannot tell what it EDITS from what
642
+ it merely quotes), \`next.archive-first\` (another feature in flight has to land
643
+ before this one), \`next.fix-coherence\` (the three axes disagree and archive
644
+ refuses), \`next.generate-tests\` (a per-service delta carries scenarios no test
645
+ run has answered — \`loam gherkin <FEAT> --service <svc>\` in that service's own
646
+ repository), \`next.verify\` (the done-check has not been started — or its record
647
+ will not read, or it answers a checklist the feature has since moved out from
648
+ under, which is not the same as a finished one), \`next.verify-unconfirmed\`
649
+ (started, and claims are still open — close them from each affected service's
650
+ own repository), \`next.attest-service\` (this repo IS one of the services that
651
+ owes an answer, so the step is bound to this commit), \`next.verify-attested\`
652
+ (every claim is confirmed but a scenario rests on an agent's word rather than a
653
+ green run — see "The done-check"), \`next.archive\`, and \`next.archived\` for one
654
+ that shipped.
655
+ It grades nothing of its own: the verdict is the one \`loam validate --feature\`
656
+ and \`loam archive\` compute — status takes the UNION of what both of them refuse,
657
+ so it may be more pessimistic than either and can never be greener than both.
658
+ \`verification\` carries \`verdict\` (\`verified\` | \`attested\` | \`unverified\`)
659
+ and \`attested\` (how many scenario claims rest on an agent's word) beside the
660
+ recounted totals, and \`checks.issues[]\` carries \`gates\` and \`details\` on every
661
+ finding.
662
+ - \`loam validate --service <id>\` grades one service's own axes: \`service.unknown\`,
663
+ \`service.no-model\`, \`service.no-spec\`, \`service.no-openapi\`, \`c4.invalid\`,
664
+ \`requirements.missing-scenarios\`, \`requirements.stepless-scenario\`,
665
+ \`spec.merge-conflict\`, \`spec.duplicate-requirement\`,
666
+ \`spec.no-requirements\`, \`spec.repeated-operations\`, \`spec.repeated-covers\`,
667
+ \`openapi.invalid\`, \`openapi.duplicate-operationid\` (one operationId in two
668
+ (path, method) slots — every join on the id then picks one of them arbitrarily),
669
+ \`api.ungoverned\`, \`api.ops-unlinked\`,
670
+ \`api.requirement-deprecated\`, \`spec-api.op-undefined\` (the living spec's own
671
+ \`Operations:\` lines, not only a delta's), \`spine.landscape-invalid\`, \`spine.op-undefined\`,
672
+ \`spine.op-link-missing\`, \`spine.op-deprecated\`, and
673
+ the architecture spec axis: \`covers.unknown\`, \`health.invalid\`, \`health.uncovered\`. Run inside
674
+ the service's own repo, once a generated suite exists under
675
+ \`<gherkinDir>/loam/\`, it also grades that suite against the living specs:
676
+ \`gherkin.missing\`, \`gherkin.stale\`, \`gherkin.orphaned\` (all warn — the fix is
677
+ always regeneration, never editing a generated file). A service that never ran
678
+ \`loam gherkin\` stays quiet, and a file tagged with a feature still in flight
679
+ answers to that feature, not to the living spec it has not merged into yet.
680
+ - \`loam validate --feature <id>\` grades a change's three axes against each other and
681
+ against the fleet in flight: \`delta.invalid\`, \`delta.nothing-tagged\`,
682
+ \`delta.service-unknown\`,
683
+ \`spec-api.op-undefined\`, \`spec-api.op-pending\`, \`c4-api.op-undefined\`,
684
+ \`c4-api.op-pending\`, \`c4-api.op-deprecated\`, \`c4.op-ungoverned\`, \`c4.op-link-missing\`,
685
+ \`api.op-unconsumed\`, \`service.no-requirement-delta\`, \`archedge.uncovered\`,
686
+ \`spec.repeated-operations\` / \`spec.repeated-covers\` (on the feature's own
687
+ spec deltas — same silent-line-loss check as service scope),
688
+ \`spec.merge-conflict\` / \`requirements.stepless-scenario\` (same codes as
689
+ service scope, graded on the feature's spec.md and arch.spec.md deltas —
690
+ both breaches merge into the living document),
691
+ the architecture spec axis (\`c4.uncovered\`, plus \`covers.unknown\` on the
692
+ feature's arch.spec.md deltas), and
693
+ the delta-shape group: \`delta.unknown-section\`, \`delta.no-delta-sections\`,
694
+ \`delta.requirement-not-merged\`, \`delta.modified-unknown\`, \`delta.removed-unknown\`,
695
+ \`delta.added-duplicate\`, \`delta.added-near-duplicate\`, \`delta.modified-pending\`,
696
+ \`delta.removed-pending\`, \`delta.added-conflict\`, \`delta.modified-conflict\`,
697
+ \`delta.living-duplicate-requirement\`, and the API-removal group:
698
+ \`openapi.remove-marker-anonymous\`, \`openapi.remove-op-consumed\`.
699
+ Where a finding names another feature in flight (\`delta.*-pending\`,
700
+ \`delta.added-conflict\`, \`delta.modified-conflict\`, \`spec-api.op-pending\`,
701
+ \`c4-api.op-pending\`) the ORDER is the answer, and \`loam dependencies --json\`
702
+ is what computes it: \`order\` is a dependency-first sequence, \`conflicts\`
703
+ the pairs no ordering fixes, \`cycles\` the ones that need a human.
704
+ - \`loam validate --all\` runs both of those for everything and adds the fleet
705
+ cross-check: \`landscape.missing\`, \`landscape.invalid\`,
706
+ \`landscape.merge-conflict\`, \`landscape.service-unmodelled\`,
707
+ \`landscape.service-undocumented\`, \`landscape.binding-unknown\`,
708
+ \`landscape.binding-duplicate\`, \`service.id-invalid\` (a \`services/<id>/\`
709
+ directory no loam command can address — fleet scope only, and graded before
710
+ the map is opened, so it stands even when the landscape is missing or
711
+ unreadable), plus a per-service
712
+ \`sources.unverifiable-from-here\` (severity \`ok\`, one per service whose
713
+ \`sources\` only its own repo can check — it is a confirmation, not work, and
714
+ the fleet rollup line is derived from those findings), and one check on this very file — \`agents.stale\`
715
+ (warn) — when the version stamp on line 1 (\`<!-- generated by loam vX.Y.Z -->\`)
716
+ is missing or older than the running binary: the tables here may describe a
717
+ loam that no longer exists, so review this file against the current \`--help\`
718
+ and update the stamp line. A hand-curated file silences it the same way, by
719
+ keeping the stamp current. The file is never refreshed automatically — your
720
+ edits outrank the template, so detection is all loam does.
721
+ - \`loam doctor\` is read-only local/fleet preflight — the first thing to run in a
722
+ repo that behaves as though the fleet were empty, and every finding carries a
723
+ \`fix\`. Its blockers are
724
+ \`doctor.config-missing\`, \`doctor.config-invalid\`, \`doctor.docs-missing\`,
725
+ \`doctor.services-missing\`, and the three that say the fleet map cannot be read
726
+ at all — \`doctor.landscape-merge-conflict\` (it still holds \`<<<<<<<\` markers,
727
+ so it is two halves of two different maps), \`doctor.landscape-invalid\` (it does
728
+ not parse) and \`doctor.landscape-unreadable\` (it is there but could not be
729
+ read). It also grades what a WRITE that did not finish left in the docs repo,
730
+ reported as \`writePath\` beside the findings: \`doctor.docs-locked\` (a held
731
+ \`.loam-lock\` — a warning while its holder is alive, since the answer is to wait
732
+ and re-run, and a BLOCKER once the holder is a process that no longer exists on
733
+ this host, since nothing will ever release it and every archive and unarchive
734
+ refuses \`docs-busy\` until it is deleted), \`doctor.commit-interrupted\` (blocker:
735
+ a \`.loam-commit\` naming an archive or unarchive that was killed mid-commit, so
736
+ the living docs may be half-written — re-run that exact command and it recovers
737
+ first, under the lock), \`doctor.commit-unreadable\` (blocker, and the worst case:
738
+ the record of which files that commit had already written cannot be parsed, so
739
+ nothing can grade it — compare against version control by hand) and
740
+ \`doctor.staging-temps\` (warn: orphaned \`.loam-*.tmp\` scratch that was never
741
+ linked into place — litter, not damage). Accessibility, portability and
742
+ incomplete binding stay warnings:
743
+ \`doctor.docs-unreadable\`, \`doctor.docs-readonly\`, \`doctor.docs-absolute\`
744
+ (\`docsDir\` stored as an absolute path in a committed loam.json — it resolves
745
+ only on the machine that ran \`loam init\`), \`doctor.inventory-unreadable\`,
746
+ \`doctor.landscape-missing\`, \`doctor.service-unbound\`, \`doctor.service-unknown\`,
747
+ and the two about this repo's own generated command and skill files:
748
+ \`doctor.agent-files-missing\` — some of them are absent, because the repo was
749
+ initialized by an older binary or they were deleted — and
750
+ \`doctor.agent-files-stale\` — some that ARE here carry no
751
+ \`<!-- generated by loam vX.Y.Z -->\` stamp, or one older than the running
752
+ binary, so the protocol and code tables they instruct an agent with may
753
+ describe a loam that no longer exists. EXPECT the second one on the first
754
+ \`loam doctor\` after an upgrade, on a repo nobody has touched: stamping is
755
+ newer than the files, so nothing written by an earlier loam carries a stamp
756
+ at all, and every one of them reads as unstamped. That is the intended
757
+ reading — an unstamped file is one nobody has confirmed still describes this
758
+ binary — but it means the finding is not evidence that anything was edited or
759
+ broken.
760
+ For the first, run the command the finding's own \`fix\` field spells: it names
761
+ this repo's \`docsDir\`, its service binding and its tool ids explicitly, and
762
+ \`loam init\` writes only the command and skill files that are absent and never
763
+ touches one that already exists. (It does rewrite \`loam.json\` — but a re-run
764
+ with no \`--docs\` keeps the pointer the file already commits, so it cannot move
765
+ the fleet under you. Reach for \`--create\` only to make a docs repo that does
766
+ not exist yet; beside a working \`docsDir\` it scaffolds a second, empty source
767
+ of truth and \`validate --all\` then goes green over an empty fleet.)
768
+ For the second there is no command: the repair is by hand, one file at a
769
+ time. Read each file the finding names against the body this loam writes,
770
+ then set (or add) its stamp line to \`<!-- generated by loam vX.Y.Z -->\` for
771
+ the running version — or, if you have no local edits worth keeping, delete
772
+ that file and re-run \`loam init\`, which lays the current one down. Neither is
773
+ ever regenerated in place: your edits outrank the template, so detection is
774
+ all loam does, and the stamp is a human's claim that the file still means what
775
+ it says rather than a refresh. Leaving either finding standing costs entry
776
+ points and the accuracy of what the entry points say, nothing else.
777
+ - \`loam rebase <FEAT>\` writes the baseline pins: \`Based-On:\` under every
778
+ MODIFIED/REMOVED requirement in the feature's spec deltas, and
779
+ \`x-loam-based-on\` on every operation in its openapi.yaml, from the living text
780
+ as it stands right now. Run it when you finish authoring a delta and again
781
+ after you resolve a collision — it is the one mechanism standing between two
782
+ features in flight and silently reverting each other's landed work. It refuses
783
+ rather than inventing a pin, and re-pinning is not resolving: see
784
+ "The requirement baseline".
785
+ - \`loam dependencies [<FEAT>]\` derives the ordering of the features in flight
786
+ from the artifacts themselves — requirement identities and OpenAPI
787
+ operationIds, never from validator prose. It is the answer to every
788
+ "another feature in flight does this first" finding, and to
789
+ \`delta.added-conflict\` / \`delta.modified-conflict\`, which no ordering fixes.
790
+ - \`loam explore [<service>...]\` reads the fleet around a change nobody has
791
+ written down yet, and writes nothing. It exists because \`loam new\`'s
792
+ \`--touches\` list is the hardest call in the cycle and the only one nothing
793
+ downstream catches: a list short by one service produces a feature that
794
+ validates, archives and ships with a consumer nobody updated. What it derives
795
+ is mechanical — the one-hop ring, each service's rung, its living operations,
796
+ who already calls whom, and which active features cover the same services. It
797
+ does NOT decide which of those neighbours you actually change; that is a
798
+ judgement about intent, and loam does not make those.
799
+ - \`loam instructions [<workflow>] [args...]\` prints one of the six workflow
800
+ protocols — \`loam-adopt\`, \`loam-feature\`, \`loam-implement\`, \`loam-check\`,
801
+ \`loam-verify\`, \`loam-ship\` — with \`$1\`, \`$2\` filled in from the arguments you
802
+ pass. The protocol ships inside the binary, so it describes the loam you are
803
+ about to run rather than the one that scaffolded the repository; the command
804
+ and skill files \`loam init\` writes are pointers at it. It reads no
805
+ \`loam.json\` and no docs repo, deliberately: \`loam-adopt\`'s own first step is
806
+ to run \`loam init\` when there is no config, so it cannot be the step that
807
+ requires one.
808
+ - \`loam audit-openspec <root>\` is the read-only OpenSpec inventory; an explicit
809
+ \`--write-mapping\` writes a non-overwriting, digest-bound decision skeleton
810
+ outside the source tree. \`loam migrate-openspec <root> --map <file>\` validates
811
+ that mapping and stays dry-run unless both \`--apply\` and an empty \`--target\`
812
+ are supplied. Living/active shape diagnostics are \`openspec.workspace-empty\`
813
+ (the audited root holds no living spec and no active change, so there is
814
+ nothing to migrate and no verdict over it can be \`ready\`),
815
+ \`openspec.specs-missing\`,
816
+ \`openspec.config-invalid\`, \`openspec.external-store-pointer\`,
817
+ \`openspec.living-empty\`, \`openspec.living-requirements-outside-section\`,
818
+ \`openspec.living-delta-section\`, \`openspec.nonstandard-living-spec\` (markdown
819
+ under \`specs/\` named neither spec.md nor design.md, so no capability reads
820
+ it), \`openspec.change-metadata-invalid\`,
821
+ \`openspec.change-schema-unresolved\`, \`openspec.skip-specs-with-specs\`,
822
+ \`openspec.change-no-specs\`, \`openspec.change-empty\`,
823
+ \`openspec.change-without-delta-sections\`,
824
+ \`openspec.change-requirements-outside-delta-sections\`,
825
+ \`openspec.change-quoted-requirements\` (requirements under a \`## Requirements\`
826
+ heading INSIDE a change delta — the shape OpenSpec's own living-spec template
827
+ mandates, and one that stages nothing, so loam refuses it rather than counting
828
+ requirements it cannot route),
829
+ \`openspec.hidden-change-directory\` (a dot-prefixed directory under
830
+ \`changes/\` is not enumerated as a change, so nothing under it migrates —
831
+ rename it or move it out; under \`changes/archive/\` the same code is an archive
832
+ diagnostic and does not gate, because frozen history never blocks),
833
+ \`openspec.nonstandard-change-spec\`, \`openspec.renamed-malformed\`,
834
+ \`openspec.requirement-id-invalid\`, \`openspec.requirement-id-repeated\`,
835
+ \`openspec.requirement-id-duplicate\` (a stable \`Requirement-ID\` that is
836
+ malformed, declared twice, or shared by two requirements — identity is never
837
+ inferred from an invalid declaration, so the RENAMED chain cannot be followed
838
+ through it), \`openspec.symlink-unsupported\`, and \`openspec.non-utf8-artifact\`;
839
+ the same shapes in frozen history are non-blocking archive diagnostics.
840
+ Mapping refuses stale or incomplete decisions under \`mapping.source-missing\`,
841
+ \`mapping.source-root-mismatch\`, \`mapping.source-digest-mismatch\`,
842
+ \`mapping.unknown-capability\`, \`mapping.unknown-requirement\`,
843
+ \`mapping.requirement-allocation-missing\`,
844
+ \`mapping.requirement-service-unknown\`, \`mapping.service-allocation-empty\`,
845
+ \`mapping.unknown-change\`, \`mapping.change-title-missing\`,
846
+ \`mapping.feature-id-invalid\`, \`mapping.feature-id-duplicate\`,
847
+ \`mapping.unknown-rename\`, \`mapping.invalid-requirement-id\`,
848
+ \`mapping.rename-source-missing\`, \`mapping.rename-source-ambiguous\`,
849
+ \`mapping.rename-source-id-invalid\`, \`mapping.rename-target-conflict\`,
850
+ \`mapping.rename-existing-id-conflict\`, \`mapping.rename-id-conflict\`,
851
+ \`mapping.rename-double-source\`, \`mapping.rename-double-target\`,
852
+ \`mapping.rename-chain\`,
853
+ \`mapping.unknown-artifact\`, and \`mapping.invalid-artifact-disposition\`.
854
+ - Repository containment failures are explicit: \`sources.path-outside\` rejects a
855
+ provenance source outside the service repo and \`gherkin.path-outside\` rejects a
856
+ generated-suite target outside it. Federated verify recording refuses under
857
+ \`service-mismatch\`, \`unknown-service\`, or \`repository-unavailable\` when the
858
+ selected service, fleet identity, and repository commit cannot be bound safely,
859
+ and under \`record-federated\` when the legacy all-at-once form would erase other
860
+ repositories' attestations.
861
+ - A file loam cannot read is reported, never skipped: \`service.unreadable\` and
862
+ \`feature.unreadable\` (both errors) name the path and say that nothing about
863
+ that target was checked while the rest of the fleet still was. The whole docs
864
+ repo being unreachable is a refusal instead: \`docs-missing\` (\`docsDir\` points
865
+ at nothing) or \`services-missing\` (it is a directory with no \`services/\` —
866
+ usually the service repo after a typo in \`docsDir\`).
867
+ - Both modes read frontmatter (\`frontmatter.missing\`, \`frontmatter.malformed\`,
868
+ \`frontmatter.field-mismatch\`,
869
+ \`frontmatter.status-unknown\`, \`frontmatter.field-missing\`); a service's spec.md —
870
+ and its arch.spec.md, same conventions — additionally carries the sources chain
871
+ (\`sources.absent\`, \`sources.path-missing\`, \`sources.empty\`,
872
+ \`sources.skipped\`, \`sources.unvouched\`, \`sources.stale\`) and the doc-side freshness check
873
+ (\`content.stale\`) — the one provenance warning that needs no service repo, so
874
+ it is reported from the docs repo too, \`--service\` and \`--all\` alike.
875
+ - \`loam archive\` alone reports the breaches only the merge computation can see:
876
+ \`living.requirement-outside-requirements\` (error), \`openapi.op-modified\` (warn),
877
+ \`openapi.path-item-modified\` (warn), \`openapi.component-modified\` (warn),
878
+ \`openapi.ref-unresolved\` (error), \`openapi.remove-marker-path-level\` (error —
879
+ an \`x-loam-remove: true\` written at PATH level, beside the methods rather than
880
+ inside one: it addresses no operation, so it retires nothing, and it is not a
881
+ contract key either) and \`service.no-model\` (warn — the archive creates
882
+ \`services/<id>/\`, or puts a service in the landscape the fleet has no directory
883
+ for at all, and nothing writes its model.likec4).
884
+
885
+ \`loam validate <target>\` is the positional spelling of the first two: a feature id
886
+ or a service id, tried in that order, so the feature wins when one name could be
887
+ both and \`--service\`/\`--feature\` force the reading. When the name really is both,
888
+ the run says so — \`target.ambiguous\` (warn) names the reading it took and the
889
+ flag that forces the other, and \`--json\` carries \`resolvedKind\` — because a
890
+ silent precedence rule is a report about a target nobody asked for. The
891
+ positional together with \`--all\`, \`--service\` or \`--feature\` is refused
892
+ (\`invalid-option\`).
893
+
894
+ Two rendering levers change nothing else: \`loam validate --errors-only\` drops
895
+ the confirmations from the TEXT view (the \`--json\` payload is byte-identical),
896
+ and \`loam list --needs-work\` narrows the service list to the ones with
897
+ something missing — the adoption worklist.
898
+
899
+ \`--strict\` (every targeting mode, \`--all\` included) exits 1 when any error
900
+ or warning exists — \`ok\`-severity findings are confirmations and never trip
901
+ it. It changes the exit code and nothing else:
902
+ \`valid\` still means "no errors", and the \`--json\` payload stays byte-for-byte
903
+ what it was. The stricter grade is a per-invocation lever, visible in the CI
904
+ pipeline that passes the flag — deliberately not a per-repo profile.
905
+
906
+ \`loam delta <FEAT> --json\` exits 1 when either authored document behind the brief
907
+ failed to parse — \`architecture.errors\` non-empty, or \`openapi.unreadable\` true —
908
+ with \`ok: true\` and the full payload intact: the empty C4 slice means the delta
909
+ did not parse, and an empty \`api\` beside \`openapi.unreadable\` means the contract
910
+ delta did not parse, neither of them "this feature changes nothing there". Branch
911
+ on the exit code before consuming either slice as a task brief.
912
+
913
+ Findings with severity \`ok\` are confirmations, not work: \`c4.valid\`, \`delta.valid\`,
914
+ \`requirements.covered\`, \`api.covered\`, \`spine.resolved\`, \`coherence.ok\`,
915
+ \`landscape.matched\`, \`archedge.covered\`, \`sources.resolved\`, \`sources.current\`,
916
+ \`gherkin.current\`.
917
+
918
+ A finding's \`subject\` names the service it is about. The envelope separates \`ok\` (the
919
+ command ran) from \`valid\` (the docs pass). A refusal is \`ok: false\` with a stable
920
+ \`error.code\`: \`no-config\` / \`config-invalid\` (no loam.json / a corrupt one),
921
+ \`docs-missing\` / \`services-missing\` (a \`docsDir\` that points at nothing, or at a
922
+ directory that is not a docs repo — the fleet is unreadable, not empty),
923
+ \`unknown-target\` (no such service or feature), \`invalid-option\` (flags that contradict
924
+ each other, or a value that cannot be right — a \`loam list\` section that is not
925
+ services or features, or a service id that is not a legal
926
+ \`services/<id>/\` directory name, included), \`already-exists\` (\`loam new\` refusing
927
+ to scaffold over an existing feature), \`sources-absent\` / \`sources-path-missing\` /
928
+ \`vouch-raced\` (\`loam vouch\` refusing to stamp — the last one when the document
929
+ changed between the read and the write, so nothing was written),
930
+ \`not-coherent\` / \`living-outside-requirements\` /
931
+ \`archive-exists\` / \`merge-failed\` / \`rollback-incomplete\` / \`docs-busy\` /
932
+ \`commit-interrupted\`
933
+ (\`loam archive\` — see the
934
+ archive gate below; \`docs-busy\` means another archive or unarchive holds the docs
935
+ repo's lock, nothing was read or written, and re-running once it finishes works),
936
+ \`feature-active\` / \`snapshot-missing\` / \`snapshot-stale\` / \`snapshot-corrupt\` /
937
+ \`restore-failed\` / \`rollback-incomplete\` / \`docs-busy\` / \`commit-interrupted\`
938
+ (\`loam unarchive\` — the
939
+ \`restore-failed\` / \`rollback-incomplete\` pair splits
940
+ exactly as archive's does; see "Taking an archive back"). \`commit-interrupted\` is
941
+ the pair's shared refusal: a previous archive or unarchive was killed mid-commit
942
+ and this run cannot repair it on its own — a half-written file has been edited
943
+ since, the repairing pre-image is gone or altered, or \`.loam-commit\` itself
944
+ cannot be read. \`loam doctor\` names the same state as
945
+ \`doctor.commit-interrupted\`. And \`gherkin-conflict\`
946
+ (\`loam gherkin <FEAT>\` would overwrite a \`.feature\` file owned by another
947
+ feature still in flight — the whole emission refuses and names the owner),
948
+ \`answers-unreadable\` / \`answers-mismatch\` /
949
+ \`answers-unevidenced\` / \`record-federated\` / \`record-unreadable\`
950
+ (\`loam verify --record\` / \`--results\` — an unreadable or
951
+ unrecognizable cucumber report refuses under \`answers-unreadable\` too;
952
+ \`record-federated\` and \`record-unreadable\` are the two records loam will not
953
+ overwrite), and
954
+ \`internal\` — an unexpected throw, the one code with no stable meaning.
955
+
956
+ \`--all\` reports a target per service, a target per feature in flight, and one target
957
+ of kind \`landscape\` for the fleet-level checks that belong to no single service.
958
+
959
+ Three different words for three different questions, and no command conflates them:
960
+ \`ok\` — the command ran; \`valid\` — the documents pass (\`validate\`); \`verified\` —
961
+ somebody says the code was built and showed evidence (\`verify\`). A feature can be
962
+ valid and unverified, or verified and incoherent. Read the one you meant.
963
+
964
+ ## The archive gate
965
+
966
+ The three axes agreeing is called **coherence**, and \`loam validate --feature\` reports
967
+ it as such. \`loam archive\` runs the same coherence check first and refuses a feature
968
+ with GATING issues — every error, plus the rare warning marked \`gates: true\` because
969
+ the merge would silently drop authored content even though the document is legal.
970
+ Advisory warnings never block: archive prints them and proceeds. Each one still names
971
+ something real — usually something the merge will drop or overwrite — so read them
972
+ before the merge runs, not after.
973
+
974
+ \`--approve\` overrides the gating issues — only those, and archive prints exactly which
975
+ ones it overrode. It is a human decision, not an agent's: if archive refuses, fix the
976
+ breach or hand it back.
977
+
978
+ Breaches only the merge computation itself can see are reported at plan time,
979
+ after the gate. \`living.requirement-outside-requirements\` (error): the LIVING spec
980
+ holds a requirement outside \`## Requirements\`, and the merge rewrites only that
981
+ section, so the requirement would land in the file twice — \`--approve\` does not
982
+ override it, because the duplication is mechanical, not a judgment call; re-home the
983
+ requirement first. \`openapi.op-modified\` (warn): the feature redefines an operation
984
+ the living OpenAPI already has, and the merge overwrites the living definition
985
+ wholesale.
986
+
987
+ The OpenAPI merge also carries the merged operations' \`$ref\` closure: every
988
+ \`#/components/<kind>/<name>\` they reference — recursively, a component's own refs
989
+ included — is copied from the feature document into the living one, so an operation
990
+ never lands pointing at a schema that stayed behind. \`openapi.component-modified\`
991
+ (warn): a carried component overwrites a living one that differs, wholesale, same
992
+ discipline as an operation. \`openapi.ref-unresolved\` (error, \`--approve\`
993
+ overrides): a ref reachable from the merged operations resolves in neither
994
+ document, so merging would write a dangling reference. External refs — URLs, file
995
+ paths, anything not starting \`#/\` — are out of scope: left untouched, never gated.
996
+
997
+ ## Taking an archive back
998
+
999
+ \`loam unarchive <FEAT>\` restores the living docs and re-opens the feature. It works
1000
+ by putting bytes back, not by inverting the merge — archive copies every file it is
1001
+ about to overwrite into \`features/archive/<FEAT>/.loam-before/\` first, because the
1002
+ previous text of a \`MODIFIED\` requirement is written down nowhere else. Do not edit
1003
+ or delete that directory, and never reconstruct an old living spec by hand from an
1004
+ archived delta: what the requirement said BEFORE is not in there, and a plausible
1005
+ reconstruction is a lie the next reader has no way to catch.
1006
+
1007
+ Every pre-image is digested when the archive writes it, and re-checked before the
1008
+ restore stages anything: \`snapshot-corrupt\` means a pre-image's bytes no longer
1009
+ match the digest archive recorded for them, so restoring it would write text
1010
+ nobody authored. \`--force\` does NOT override that one — the damage is to the
1011
+ undo itself, not to the living docs — and the fix is version control.
1012
+
1013
+ It refuses rather than guesses, under codes you can branch on: \`feature-active\` (a
1014
+ feature of that id is in flight again), \`snapshot-missing\` (archived before loam
1015
+ recorded this — the docs have to come back from version control), \`snapshot-stale\`
1016
+ (a merged file changed after the archive, so restoring would revert someone else's
1017
+ work). \`--force\` overrides the last one, and like \`--approve\` it is a human's call.
1018
+
1019
+ A restore that fails outright splits the same way archive's does: \`restore-failed\`
1020
+ means nothing was restored or everything was rolled back — the living docs are
1021
+ unchanged, fix the reported cause and re-run; \`rollback-incomplete\` means the
1022
+ restore failed AND some files could not be put back — stop and hand it to a human,
1023
+ the message lists the files to check.
1024
+
1025
+ ## Dropping a feature
1026
+
1027
+ A feature that was never archived is one directory and nothing else: no living doc
1028
+ references it until \`loam archive\` merges it. To abandon it, delete the directory —
1029
+ \`git rm -r features/<FEAT-dir>\` — and version control keeps the record of the
1030
+ attempt. An ARCHIVED feature is the opposite, its content folded into the living
1031
+ docs: run \`loam unarchive <FEAT>\` first, then delete. There is no \`loam abandon\`,
1032
+ deliberately — a removal that computes nothing is what version control is for.
1033
+ `;
1034
+ const COMMANDS = [
1035
+ {
1036
+ name: "loam-adopt",
1037
+ description: "Adopt a service — write its baseline docs from its code, as draft, then validate",
1038
+ argumentHint: "<service-id>",
1039
+ purpose: "Write one service's baseline documentation into the docs repo from its code, as `draft`. You read the code; loam states the work and checks the result — it never reads the service, so anything you cannot show, do not write.",
1040
+ invocation: "loam instructions loam-adopt $1",
1041
+ spine: [
1042
+ "wire this repo (`loam init`, then `loam doctor`) if it has no ./loam.json yet",
1043
+ "`loam adopt` — the brief: every file to write, the grammar of each, and what the fleet map already says",
1044
+ "read the code, keeping the list of every path you open — that list becomes `sources`",
1045
+ "write the artifacts, everything `status: draft`",
1046
+ "validate the service, then validate the whole fleet — a baseline that passes one and fails the other is documented and invisible",
1047
+ "hand back with what you could not determine from the code, and let a human vouch",
1048
+ ],
1049
+ body: `Write one service's baseline documentation into the loam docs repo (its path is
1050
+ \`docsDir\` in ./loam.json). You read the code; loam states the work and checks the
1051
+ result. It never reads the service — so anything you cannot show, do not write.
1052
+
1053
+ 0. If there is no ./loam.json here, wire the repo first:
1054
+ \`loam init --docs <path-to-docs-repo> --service $1\` (add \`--create\` ONLY if the
1055
+ docs repo does not exist yet), then \`loam doctor\` — it resolves \`docsDir\`,
1056
+ checks the docs repo is one, and reports the service binding. Do not go on with
1057
+ \`docs-missing\`, \`services-missing\` or any \`doctor.*\` blocker standing.
1058
+ Every \`doctor\` finding carries a \`fix\` field spelling the exact command —
1059
+ type that, not an approximation of it.
1060
+ If this repo is already wired, \`loam status --json\` in the docs repo is the
1061
+ fastest way to see whether $1 is even the service that owes work next: its
1062
+ \`next.adopt\` entries name the services with nothing written down yet.
1063
+ 1. \`loam adopt --service $1 --json\`. That output IS the brief:
1064
+ - \`warnings[]\` — read these FIRST. A near-miss against an existing service id
1065
+ usually means \`$1\` is a typo, and a brief followed to the letter then produces
1066
+ a complete, validating baseline for a service that does not exist.
1067
+ - \`targets[]\` — every file to write. \`action: "diff"\` means the file ALREADY EXISTS:
1068
+ read it, diff your findings against it, report what disagrees. Do not replace it.
1069
+ \`action: "edit"\` appears on exactly one target — \`architecture/landscape.likec4\`,
1070
+ the WHOLE FLEET's map: add this service's element and edges to it, never rewrite it.
1071
+ - \`targets[].shape\` — the grammar of each artifact, and \`example\` where one is
1072
+ shorter than a description. Every rule there is one a later check depends on;
1073
+ rules nothing checks are in \`unchecked[]\` instead, and are still worth following.
1074
+ - \`landscape\` — the elements and edges the fleet already has for this service.
1075
+ Bind to them; do not draw a second version of the same box. \`landscape.expects\`
1076
+ lists operations other services already call — your openapi.yaml owes them.
1077
+ \`landscape.instruction\` is the write the fleet map still owes this service; it
1078
+ is \`null\` once an element resolves to it, and only then.
1079
+ - \`frontmatter\` — what to put in the header of every markdown artifact.
1080
+ - \`checks[]\` — what \`loam validate\` will run. \`unchecked[]\` — what it will not.
1081
+ 2. Read \`AGENTS.md\` at the docs repo root, then read the code: entry points, HTTP
1082
+ routes and handlers, published events, config, deploy manifests, tests.
1083
+ **Keep a list of every path you actually open.** That list becomes \`sources\` —
1084
+ written as files and directories, never glob patterns — and it is the only line
1085
+ tying the document to the repository.
1086
+ 3. Write the artifacts under \`services/$1/\`, in the order the brief lists them, and
1087
+ make the landscape edit the brief asks for.
1088
+ Everything \`status: draft\`. Never write \`last_verified\`, \`sources_digest\`,
1089
+ \`content_digest\` or \`sources_files\`.
1090
+ 4. \`loam validate --service $1 --json\`. Fix every error. \`sources.unvouched\` is
1091
+ expected on a fresh baseline — it closes when a person vouches, not when you do.
1092
+ 5. \`loam validate --all --json\` in the docs repo. This is the run that grades the
1093
+ fleet map: \`landscape.service-unmodelled\` means the element never landed,
1094
+ \`landscape.binding-unknown\` / \`landscape.binding-duplicate\` mean it landed wrong,
1095
+ and \`landscape.missing\` means there is no map at all. A baseline that passes step 4
1096
+ and fails this one is documented and invisible.
1097
+ 6. Hand back, and say three things: what you could not determine from the code, what
1098
+ the existing artifacts disagreed with, and which parts you are least sure of.
1099
+ Then a human runs \`loam vouch --service $1\` in the service's own repo.
1100
+
1101
+ Where the code does not say, write that it does not say. A confident sentence about
1102
+ behaviour nobody can find is the one failure mode none of loam's checks can catch.
1103
+ `,
1104
+ },
1105
+ {
1106
+ name: "loam-feature",
1107
+ description: "Start a new loam feature — scaffold it, then author the C4 delta and requirement deltas",
1108
+ argumentHint: `<FEAT-id> "<title>"`,
1109
+ purpose: "Start a feature in the docs repo: decide which services it touches, scaffold it, then author the C4 delta and one requirement delta per service. The `--touches` list is the hardest call in the cycle and nothing downstream catches one that is short by a service.",
1110
+ invocation: 'loam instructions loam-feature $1 "$2"',
1111
+ spine: [
1112
+ "read `AGENTS.md` at the docs repo root — it defines the ID spine everything else depends on",
1113
+ "`loam explore` around the services you think are involved: the ring one hop out, and what is already in flight over the same ground",
1114
+ "`loam status` / `loam list` / `loam show` — what exists and what is already owed",
1115
+ "`loam new` with a `--touches` per service it changes and a `--new-service` per service it introduces",
1116
+ "author what the templates left as TODO: intent, the C4 delta, the per-service spec / arch spec / openapi",
1117
+ "`loam rebase` — pin every MODIFIED requirement and every operation to the living version you wrote against",
1118
+ "validate the feature, then read `loam dependencies` before anyone starts building",
1119
+ ],
1120
+ body: `Start a new feature in the loam docs repo (its path is \`docsDir\` in ./loam.json).
1121
+
1122
+ 1. Read \`AGENTS.md\` at the root of the docs repo first — it defines the ID spine
1123
+ everything here depends on.
1124
+ 2. Understand the current state before proposing a change:
1125
+ - \`loam status --json\` — what is already in flight and what it is waiting on.
1126
+ Run this FIRST if you joined this repository halfway or lost the session:
1127
+ \`next[]\` is ordered and each entry carries the literal command to run, so it
1128
+ tells you whether the work is to start $1 at all. \`loam status $1 --json\`
1129
+ narrows it to one feature once $1 exists
1130
+ - \`loam list --json\` — what services exist, and what documentation they are missing
1131
+ - \`loam show <service> --json\` — what a service owns, exposes, and who already calls it
1132
+ 2b. \`loam explore <service> --json --as $1\` around every service you believe this
1133
+ feature touches. This is the step that decides step 3's \`--touches\` list, and it
1134
+ is the only decision in the cycle nothing downstream catches — a list short by one
1135
+ service produces a feature that validates, archives and ships with a consumer
1136
+ nobody updated. Read four fields:
1137
+ - \`neighbours\` — the services one hop out in the fleet map. Weigh each one. loam
1138
+ knows they are connected, not whether you change them, and it deliberately
1139
+ leaves them OUT of the suggested command line rather than guessing for you
1140
+ - \`services[].maturity\` — a service at \`empty\` or \`partial\` has no baseline to
1141
+ write a delta against, so the work starts with \`loam adopt --service <id>\`, not
1142
+ with this feature
1143
+ - \`services[].operations\` — what each already exposes, so an operation you are
1144
+ about to "add" that is already there becomes a MODIFIED requirement instead
1145
+ - \`overlaps\` — active features already carrying a delta for these services.
1146
+ \`loam dependencies --json\` (step 7) then says which must archive first
1147
+ \`--op <operationId>\` seeds from an operation when you know the call but not who
1148
+ owns it. A seed naming no \`services/<id>/\` is reported in \`unknown\` with the
1149
+ closest real ids rather than refused — the feature may be introducing it, and a
1150
+ typo looks identical until you read that list. \`scaffold\` is the literal step-3
1151
+ command line the seeds imply, with \`--new-service\` where the service does not
1152
+ exist yet.
1153
+ 3. Scaffold it: \`loam new $1 --title "$2"\`, adding \`--touches <id>\` for every service the
1154
+ feature touches and \`--new-service <id>\` for every one it introduces. Every id goes
1155
+ through the \`services/<id>/\` grammar, so a typo is refused (\`invalid-option\`)
1156
+ rather than scaffolded; a \`--touches\` that matches nothing existing comes back as
1157
+ a \`note\` with the close ids, not as a failure — read it, because a \`--touches\`
1158
+ naming a service that does not exist is \`delta.service-unknown\` at validate time
1159
+ and a phantom \`services/<typo>/\` directory if it ever archives.
1160
+ 4. Author what the templates left as TODO:
1161
+ - \`intent.md\` — the problem in business terms
1162
+ - \`delta.likec4\` — new elements and edges, each tagged \`#$1\`; every call edge carries
1163
+ \`metadata { op '<operationId>' }\`. For a requirements-only feature DELETE this
1164
+ file: an empty delta is legal, but a scaffold left half-filled is not
1165
+ - \`specs/<svc>/spec.md\` — one behaviour per requirement, SHALL, at least one
1166
+ Given/When/Then scenario; uncomment \`Operations:\` once the operation exists
1167
+ - \`specs/<svc>/arch.spec.md\` — the architectural obligations (outbox, retries,
1168
+ idempotency, alerts), same grammar; a \`Covers:\` line per requirement naming the
1169
+ tagged elements/edges it accounts for, or \`c4.uncovered\` says nothing does
1170
+ - \`specs/<svc>/openapi.yaml\` — define every operationId the edges reference
1171
+ 5. **\`loam rebase $1\`** — the step between authoring and checking, and the one that
1172
+ stops two features in flight from silently deleting each other's work. It pins
1173
+ every MODIFIED/REMOVED requirement (\`Based-On:\`) and every operation in the
1174
+ contract delta (\`x-loam-based-on\`) to the living version you wrote against.
1175
+ Run it on the contract axis even if you changed exactly one endpoint: that is
1176
+ what marks the REST of the document as quotation the merge must not write back.
1177
+ Without the pins, \`delta.baseline-missing\` / \`openapi.baseline-missing\` warn,
1178
+ nothing refuses, and the second feature to archive reverts the first — with
1179
+ \`+0 ~1 -0\`, exit 0, and nobody told.
1180
+ Restamping is not resolving: if rebase reports a requirement as MOVED, re-read
1181
+ the living text and fold in what you still mean BEFORE you ship.
1182
+ 6. \`loam validate --feature $1 --json\`. Do not stop while \`valid\` is false.
1183
+ 7. \`loam dependencies $1 --json\` before anyone starts building: it says which
1184
+ features in flight have to archive first (\`order\`) and which collisions no
1185
+ ordering fixes (\`conflicts\`, \`cycles\`). Every conflict it names is one \`loam rebase\`
1186
+ cannot fix for you — two intentions have to be merged by people.
1187
+
1188
+ The operation's three spellings — the edge's \`op\`, the requirement's \`Operations:\`
1189
+ line, and the OpenAPI \`operationId\` — must match exactly.
1190
+ `,
1191
+ },
1192
+ {
1193
+ name: "loam-implement",
1194
+ description: "Implement a loam feature's slice for one service — generated gherkin first, tests from it, then code",
1195
+ argumentHint: "<FEAT-id> [service]",
1196
+ purpose: "Build one service's part of a feature, in that service's own repository. The generated Gherkin is the acceptance criterion — it comes before the step definitions, and those come before the code.",
1197
+ invocation: "loam instructions loam-implement $1 $2",
1198
+ spine: [
1199
+ "`loam status` — where this feature actually is, and what is owed before you start",
1200
+ "`loam delta` — the task: the intent, every requirement verbatim, the endpoints, and the calls in and out",
1201
+ "`loam gherkin` in the service's repo — the digest-stamped `.feature` files. Never edit them",
1202
+ "write step definitions for the generated scenarios first, outside the generated directory",
1203
+ "implement until the suite passes, running it with a JSON report",
1204
+ "honour the contract in both directions: expose every inbound operation, call every outbound one",
1205
+ "`loam rebase` if building made you change the feature's documents at all, then validate the feature",
1206
+ ],
1207
+ body: `Implement one service's part of a feature. Every step runs in the SERVICE'S own
1208
+ repository, which needs its own committed ./loam.json — if there is none,
1209
+ \`loam init --docs <path-to-docs-repo> --service $2\` then \`loam doctor\` first.
1210
+
1211
+ 0. \`loam status $1 --json\` — where this feature actually is. Run it before
1212
+ anything else if you joined halfway or lost the session: it says which
1213
+ artifacts are still owed, whether another feature has to archive first, and
1214
+ what the next step is, each \`next[]\` entry carrying the literal command. It
1215
+ writes nothing, so it is always safe to re-run.
1216
+ 1. \`loam delta $1 --service $2 --json\` (drop \`--service\` to use the service configured
1217
+ in ./loam.json). That output IS the task:
1218
+ - \`intent\` — why this exists
1219
+ - \`requirements[]\` — what to build, with \`scenarios[].lines\` verbatim
1220
+ - \`archRequirements[]\` — the architectural obligations (outbox, retries, alerts),
1221
+ same shape; their scenarios become integration/ops tests, and \`covers\` names
1222
+ the model objects each exercises
1223
+ - \`architecture\` — whether this service is new, and the calls in and out of it
1224
+ - \`api\` — the endpoints this feature adds or retires for this service: path,
1225
+ method, operationId and summary, with removals spelled \`REMOVE <METHOD> <path>\`
1226
+ - \`openapi\` — whether that contract could be read at all: \`unreadable\`, plus an
1227
+ \`error\` when the parser gave a message. It rides beside \`api\` rather than
1228
+ inside it because \`api\` stays the operations array it has always been, and it
1229
+ is what tells an empty \`api\` apart from a contract delta that did not parse
1230
+ - \`services\` — every service this feature projects onto, so a run without
1231
+ \`--service\` tells you which ones to ask for rather than guessing
1232
+ Exit 1 with \`ok: true\` means one of the two authored documents behind this brief
1233
+ did not parse: \`architecture.errors\` non-empty (delta.likec4), or
1234
+ \`openapi.unreadable\` true (the feature's openapi.yaml for this service). The empty
1235
+ slice is the parse failure itself, not "nothing changes there". Stop and fix that
1236
+ document before building anything — for the delta, \`loam validate $1 --json\` names
1237
+ the error; the contract delta is not graded by validate, so read the parser message
1238
+ the payload carries.
1239
+ 2. In the service's repo, \`loam gherkin $1 --json\` — one \`.feature\` file per changed
1240
+ requirement lands under \`<gherkinDir>/loam/\` (default \`features/loam/\`), scenarios
1241
+ digest-stamped, arch requirements tagged \`@architecture\`. Those files ARE the
1242
+ acceptance criteria. Never edit them: regeneration rewrites the directory, and
1243
+ \`loam validate\` reports the suite stale by digest, not by your intentions.
1244
+ 3. Write step definitions for the generated scenarios FIRST — outside \`loam/\`.
1245
+ Do not paraphrase a scenario into something easier to pass; it is the acceptance
1246
+ criterion someone else reviews against.
1247
+ 4. Implement until the suite passes — run it with a JSON report
1248
+ (\`cucumber-js --format json:report.json\`):
1249
+ \`loam verify $1 --service $2 --results report.json\` consumes that report as the
1250
+ done-check's answer sheet. Record from THIS repo, with \`--service\` — the
1251
+ \`--service\`-less \`--record\` form writes the whole record from one place and is
1252
+ refused (\`record-federated\`) once another service has attested.
1253
+ 5. Honour the contract: every operation in \`architecture.inbound\` must exist under
1254
+ exactly that operationId, and every one in \`architecture.outbound\` must be called.
1255
+ 6. If building made you change the feature's documents at all — a requirement's
1256
+ wording, an operation in its openapi.yaml — **\`loam rebase $1\`** before you
1257
+ validate. Every edit you make in the docs repo while other features are in
1258
+ flight is written against a living document that may have moved since the
1259
+ feature directory was created, and the pins are the only thing that lets the
1260
+ merge tell an EDIT from a QUOTE. \`delta.baseline-missing\` and
1261
+ \`openapi.baseline-missing\` are the warnings that say you skipped it;
1262
+ \`delta.baseline-stale\` / \`openapi.baseline-stale\` are archive refusing because
1263
+ somebody landed a change underneath you — re-read theirs, fold in what you
1264
+ still mean, then rebase again. Re-pinning without re-reading is how you
1265
+ overwrite someone else's requirement with loam's blessing.
1266
+ 7. \`loam validate --feature $1 --json\` before handing back.
1267
+
1268
+ If the requirement is ambiguous, say so and stop — do not invent behaviour and
1269
+ leave the spec disagreeing with the code.
1270
+ `,
1271
+ },
1272
+ {
1273
+ name: "loam-check",
1274
+ description: "Validate the loam docs repo and fix what it reports",
1275
+ argumentHint: "[--all | <FEAT-id> | <service>]",
1276
+ purpose: "Run loam's checks and fix what they find. Branch on `findings[].code`, never on the prose — and remember that a coherence finding marked `gates` stops `loam archive` even when it is only a warning.",
1277
+ // No `$1`, unlike the other five. This body carries no placeholder at all —
1278
+ // `loam-check`'s argument is a target you pass to `loam validate`, not
1279
+ // something the protocol interpolates — and the argument hint's FIRST form
1280
+ // is `--all`, so `loam instructions loam-check $1` expanded to
1281
+ // `loam instructions loam-check --all`, which commander reads as an unknown
1282
+ // option and refuses with exit 1. A pointer file whose one instruction
1283
+ // fails leaves an agent with no protocol at all, which is a state the fat
1284
+ // body could never reach. The command still tolerates a leading-dash
1285
+ // argument (see `allowUnknownOption` in commands/instructions.ts); this is
1286
+ // the half of the fix that stops loam printing the broken line in the first
1287
+ // place.
1288
+ invocation: "loam instructions loam-check",
1289
+ spine: [
1290
+ "`loam validate --all` in the docs repo for the fleet, or `loam validate <target>` for one feature or service",
1291
+ "fix every error, and every finding that gates the archive",
1292
+ "leave an advisory warning standing only if you can say why, and say it",
1293
+ "re-run until the grade is the one you meant",
1294
+ ],
1295
+ body: `Run loam's checks and fix what they find.
1296
+
1297
+ - whole fleet: \`loam validate --all --json\` (run in the docs repo)
1298
+ - one target: \`loam validate <FEAT-id | service-id> --json\` — the feature reading is
1299
+ tried first; \`--feature <id>\` / \`--service <id>\` force one when a name could be both,
1300
+ and \`target.ambiguous\` (warn) says when a name really was both and which reading was taken
1301
+ - \`--strict\` (any mode) exits 1 on any warning too. Exit code only: \`valid\` and the
1302
+ payload do not change, so the stricter grade lives in the CI invocation, not the repo
1303
+ - \`--errors-only\` drops the \`ok\` confirmations from the text view; the \`--json\`
1304
+ payload is byte-identical either way
1305
+ - refusals before any grading: \`docs-missing\` / \`services-missing\` (\`docsDir\` in
1306
+ ./loam.json points at nothing, or at a directory with no \`services/\`) — run
1307
+ \`loam doctor\` and fix the wiring, do not read an empty report as a clean fleet
1308
+
1309
+ Branch on \`findings[].code\`, not the prose. Errors fail validate; a coherence finding
1310
+ with \`gates: true\` will stop \`loam archive\` even when it is a warning. Fix every
1311
+ error and every gating finding. Leave an advisory warning only if you can say why,
1312
+ and say it.
1313
+
1314
+ \`--service <id>\` — one service's own axes (\`--all\` runs these for every service):
1315
+
1316
+ | code | what it means | what to do |
1317
+ |---|---|---|
1318
+ | \`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 |
1319
+ | \`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 |
1320
+ | \`c4.invalid\` | model.likec4 does not parse | fix this first — an unreadable axis makes every other check meaningless |
1321
+ | \`requirements.missing-scenarios\` | a requirement with no scenario | add the acceptance criteria; do not delete the requirement |
1322
+ | \`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 |
1323
+ | \`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 |
1324
+ | \`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 |
1325
+ | \`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 |
1326
+ | \`spec.requirement-id-repeated\` | one requirement declares \`Requirement-ID\` more than once | keep exactly one identity line |
1327
+ | \`spec.requirement-id-duplicate\` | one ID identifies multiple requirements in the same file | give each requirement its own stable ID |
1328
+ | \`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 |
1329
+ | \`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 |
1330
+ | \`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 |
1331
+ | \`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 |
1332
+ | \`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 |
1333
+ | \`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 |
1334
+ | \`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 |
1335
+ | \`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 |
1336
+ | \`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 |
1337
+ | \`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>\` |
1338
+ | \`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 |
1339
+ | \`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 |
1340
+ | \`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 |
1341
+ | \`api.ungoverned\` (warn) | operation(s) no requirement's \`Operations:\` line names | write the requirement, or link an existing one |
1342
+ | \`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 |
1343
+ | \`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 |
1344
+ | \`spine.landscape-invalid\` | the living landscape does not parse, so the C4↔API spine cannot be checked | fix architecture/landscape.likec4 first |
1345
+ | \`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 |
1346
+ | \`spine.op-link-missing\` (warn) | a landscape "Calls" edge into this service with no \`metadata { op }\` | link it to the operationId |
1347
+ | \`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 |
1348
+ | \`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 |
1349
+ | \`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 |
1350
+ | \`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 |
1351
+ | \`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 |
1352
+ | \`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 |
1353
+ | \`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 |
1354
+
1355
+ \`--feature <FEAT-id>\` — a change's three axes against each other. The SAME checks
1356
+ gate \`loam archive\` (errors block it; warnings never do), so a clean run here is
1357
+ what lets a feature ship:
1358
+
1359
+ | code | what it means | what to do |
1360
+ |---|---|---|
1361
+ | \`delta.invalid\` | delta.likec4 does not parse | fix this first — an unreadable axis makes every other check meaningless |
1362
+ | \`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 |
1363
+ | \`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 |
1364
+ | \`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 |
1365
+ | \`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 |
1366
+ | \`spec-api.op-undefined\` | a requirement governs an operation its provider's OpenAPI does not define | define the endpoint, or correct the \`Operations:\` line |
1367
+ | \`spec-api.op-pending\` (warn) | the governed operation is defined by another feature in flight | archive that feature first |
1368
+ | \`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 |
1369
+ | \`c4-api.op-pending\` (warn) | the called operation is defined by another feature in flight | archive that feature first |
1370
+ | \`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 |
1371
+ | \`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 |
1372
+ | \`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 |
1373
+ | \`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 |
1374
+ | \`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 |
1375
+ | \`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 |
1376
+ | \`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 |
1377
+ | \`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 |
1378
+ | \`c4.op-ungoverned\` (warn) | an operation is called but no requirement governs it | write the requirement |
1379
+ | \`c4.op-link-missing\` (warn) | a "Calls" edge in the delta with no \`metadata { op }\` | link it to the operationId |
1380
+ | \`api.op-unconsumed\` (warn) | an added operation no edge consumes | model the caller, or say why it is provider-only |
1381
+ | \`service.no-requirement-delta\` (warn) | a new service with no spec delta | write \`specs/<svc>/spec.md\` |
1382
+ | \`archedge.uncovered\` (warn) | no scenario names a tagged edge (a heuristic) | write the scenario, or say why the edge needs none |
1383
+ | \`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 |
1384
+ | \`delta.unknown-section\` | a heading that nearly matches the delta grammar | fix it — everything under it merges as NOTHING today, silently |
1385
+ | \`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 |
1386
+ | \`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 |
1387
+ | \`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 |
1388
+ | \`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 |
1389
+ | \`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 |
1390
+ | \`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) |
1391
+ | \`delta.removed-unknown\` | REMOVED one that does not exist | drop the section, or fix the name |
1392
+ | \`delta.added-duplicate\` | ADDED a name the living spec already has | use MODIFIED — as written, the merge REPLACES the living requirement |
1393
+ | \`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 |
1394
+ | \`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 |
1395
+ | \`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 |
1396
+ | \`delta.removed-pending\` (warn) | REMOVED something another feature in flight introduces | archive that feature first; \`loam dependencies --json\` says in which order |
1397
+ | \`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\` |
1398
+ | \`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 |
1399
+ | \`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 |
1400
+ | \`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 |
1401
+ | \`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 |
1402
+
1403
+ \`--all\` — everything above for every target, plus the fleet cross-check:
1404
+
1405
+ | code | what it means | what to do |
1406
+ |---|---|---|
1407
+ | \`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 |
1408
+ | \`landscape.invalid\` | the living landscape does not parse | fix it first — the fleet cross-check cannot run against a document nobody can read |
1409
+ | \`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 |
1410
+ | \`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\`) |
1411
+ | \`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 |
1412
+ | \`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 |
1413
+ | \`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 |
1414
+ | \`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\` |
1415
+ | \`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 |
1416
+ | \`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 |
1417
+ | \`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 |
1418
+
1419
+ frontmatter and provenance — services' spec.md and arch.spec.md, and features'
1420
+ intent.md, both modes:
1421
+
1422
+ | code | what it means | what to do |
1423
+ |---|---|---|
1424
+ | \`frontmatter.missing\` (warn) | no frontmatter at all | add owner, status and sources |
1425
+ | \`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 |
1426
+ | \`frontmatter.field-mismatch\` | the doc names a different service/feature than the one it lives under | fix the frontmatter, or move the file |
1427
+ | \`frontmatter.status-unknown\` | a status nobody defined (\`verifed\`) | use the documented vocabulary — a typo here reads as unverified forever |
1428
+ | \`frontmatter.field-missing\` (warn) | owner, status or the identity field is absent | fill them in |
1429
+ | \`sources.absent\` (warn) | the doc names no sources | nothing ties it to the code, so nothing can tell you when it goes stale |
1430
+ | \`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 |
1431
+ | \`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>\` |
1432
+ | \`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 |
1433
+ | \`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 |
1434
+ | \`sources.unvouched\` (warn) | \`sources\` with no \`sources_digest\` — nobody ever stamped it | leave it: vouching is a human's reading, not yours |
1435
+ | \`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 |
1436
+
1437
+ \`loam archive\` alone — breaches only the merge computation can see, reported at
1438
+ plan time (they never appear in \`validate\`):
1439
+
1440
+ | code | what it means | what to do |
1441
+ |---|---|---|
1442
+ | \`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 |
1443
+ | \`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 |
1444
+ | \`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 |
1445
+ | \`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 |
1446
+ | \`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 |
1447
+ | \`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 |
1448
+ | \`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 |
1449
+
1450
+ \`sources.stale\` and \`content.stale\` are the warnings you cannot close by yourself.
1451
+ Fix what the code now says, then hand it back — the stamp is a person's claim to
1452
+ have read it.
1453
+ `,
1454
+ },
1455
+ {
1456
+ name: "loam-verify",
1457
+ description: "Verify a built loam feature against its own promises, claim by claim, with evidence",
1458
+ argumentHint: "<FEAT-id>",
1459
+ purpose: "Check that the code somebody built is the feature that was designed. loam derives the questions; a test runner and you answer them. loam never reads the service, so a verdict is worth exactly what its evidence is worth.",
1460
+ invocation: "loam instructions loam-verify $1",
1461
+ spine: [
1462
+ "`loam verify` — the checklist, derived from the feature's own artifacts",
1463
+ "run the generated suite with a JSON report: the scenario claims are the runner's to answer, not yours",
1464
+ "answer the remaining claims yourself, every one carrying a `file:line` that resolves",
1465
+ "record from each affected service's own repository, bound to that service",
1466
+ "read the verdict honestly — a scenario confirmed on your word is `attested`, not `verified`",
1467
+ ],
1468
+ body: `Check that the code somebody just built is the feature that was designed.
1469
+
1470
+ loam derives the questions; the test runner and you answer them. loam never
1471
+ reads the service, so a verdict is worth exactly what its evidence is worth.
1472
+
1473
+ 1. \`loam verify $1 --json\`. \`claims[]\` is the checklist, derived from the feature's
1474
+ own artifacts — one claim per new service, per operation its openapi delta adds,
1475
+ per tagged edge that names an operation, and per scenario of every changed
1476
+ requirement, arch.spec.md deltas included. Each has a stable \`id\` and a
1477
+ \`subject\` (the service whose code answers it); a claim that says arch.spec.md
1478
+ wants an integration/ops test, not an acceptance test. An ARCHIVED feature returns its record as frozen history instead
1479
+ (\`frozen: true\`) and \`--record\` / \`--results\` refuse it — \`loam unarchive\`
1480
+ first if the answers really must change.
1481
+ 2. The \`scenario.tested\` claims are the runner's to answer, not yours. In the
1482
+ service's repo, run the generated suite with a JSON report —
1483
+ \`npx cucumber-js --format json:report.json\` (cucumber-jvm, behave and
1484
+ SpecFlow emit the same format). The generated scenarios carry
1485
+ \`@loam-digest-…\` tags that ride into the report; that digest is the match,
1486
+ so never retitle or hand-edit a generated scenario to make one pass.
1487
+ If there is no runnable suite yet you MAY answer them yourself, and the record
1488
+ will say so: \`answered_by: agent\` makes the feature **attested**, not verified
1489
+ (\`verified: false\`, \`verdict: "attested"\`, the \`verify.scenario-attested\`
1490
+ notice, and a \`next.verify-attested\` step in \`loam status\`). Nothing gates on
1491
+ it — a legacy service with no suite must still be able to ship — but say so
1492
+ when you hand back, and close it with \`--results\` the moment a suite runs.
1493
+ \`verify.digest-contested\` (warn) is the case where two services word a
1494
+ scenario identically: they share one digest, so no single report can say whose
1495
+ suite ran it, and each service must record its own with \`--service\`.
1496
+ 3. Answer every OTHER claim by finding it in the code. Not by reasoning that it
1497
+ must be there — by opening the file:
1498
+ - \`service.exists\` — the service is deployable: its build, its entry point.
1499
+ - \`api.exposes\` — the route handler serving that operationId, not the spec that
1500
+ declares it.
1501
+ - \`c4.calls\` — the call site in the CALLER.
1502
+ 4. Write those answers as JSON — evidence is \`file:line\`, one entry per place it can
1503
+ be seen:
1504
+ \`\`\`json
1505
+ { "answers": [
1506
+ { "id": "<claim id>", "verdict": "confirmed", "evidence": ["src/split/Api.ts:42"] },
1507
+ { "id": "<claim id>", "verdict": "unconfirmed", "note": "the split service has no entry point yet" }
1508
+ ] }
1509
+ \`\`\`
1510
+ 5. Record, **in each affected service's own repository**, one service at a time:
1511
+
1512
+ \`\`\`sh
1513
+ loam verify $1 --service <id> --results report.json --record answers.json --json
1514
+ \`\`\`
1515
+
1516
+ \`--service <id>\` narrows the checklist to that service's claims and binds the
1517
+ answers to this repository's commit, writing ONE attestation into a shared
1518
+ \`schema: 2\` \`verification.yaml\`. Running it in the next repo adds that repo's
1519
+ attestation and leaves the others standing. In a repo whose ./loam.json
1520
+ declares a \`service\`, \`--service\` may be omitted — it is substituted, and the
1521
+ run still federates. \`--results report.json\` alone is legal when the
1522
+ checklist is all scenarios.
1523
+
1524
+ ⚠ **Never run \`loam verify $1 --record answers.json\` without \`--service\`.**
1525
+ That is the legacy all-at-once form: it writes the WHOLE record from one place,
1526
+ and over a record that already carries other services' attestations it is
1527
+ refused (\`record-federated\`, naming them) precisely because it would erase
1528
+ them. If you meet that refusal, re-run with \`--service\`; do not work around it.
1529
+
1530
+ The other refusals: \`answers-mismatch\` (an id
1531
+ nobody asked about, a claim you left out — re-run step 1, the feature moved —
1532
+ or an answers entry for a \`scenario.tested\` claim: the runner owns those),
1533
+ \`answers-unevidenced\` (a \`confirmed\` with nothing behind it),
1534
+ \`answers-unreadable\` (a report or answers file it cannot read),
1535
+ \`record-unreadable\` (a \`verification.yaml\` that exists but cannot be read as a
1536
+ record — it is never overwritten; a human repairs it, and
1537
+ \`verify.record-miscounted\` is the reason when its \`summary\` block contradicts
1538
+ its own \`claims[]\`: neither half can be believed, so re-record rather than
1539
+ editing the counts),
1540
+ \`service-mismatch\` (\`--service\` names a service this repo does not declare) and
1541
+ \`repository-unavailable\` (\`--service\` from a repo that declares none, or whose
1542
+ commit cannot be read — an attestation must be bound to the code it is about).
1543
+ 6. Report every \`unconfirmed\` claim and what it would take to close it, and read
1544
+ \`discarded\` in the payload: it names every previous answer the new record does
1545
+ not carry, and each one is either evidence that went stale or evidence that was
1546
+ just dropped. A scenario claim the report failed or missed closes by fixing the
1547
+ code and re-running the suite — never by editing the generated .feature.
1548
+ 7. Reading is safe from anywhere: \`loam verify $1 --service <id> --json\` without
1549
+ \`--record\` writes nothing and works from the docs repo too. \`services\` lists
1550
+ every subject of the checklist, so it is how you find out which repos still
1551
+ owe an attestation, and \`loam status $1 --json\` says the same thing in the
1552
+ shape of a next step.
1553
+
1554
+ Anything you cannot show is \`unconfirmed\` with a note. That is a successful record,
1555
+ and it is the useful one: nothing gates on this file, so the only thing it is for is
1556
+ telling the next reader the truth. \`loam archive\` will ship the feature either way —
1557
+ which is exactly why a \`confirmed\` you cannot back up costs nothing now and misleads
1558
+ everyone later.
1559
+ `,
1560
+ },
1561
+ {
1562
+ name: "loam-ship",
1563
+ description: "Archive a finished loam feature — merge its deltas into the living docs",
1564
+ argumentHint: "<FEAT-id>",
1565
+ purpose: "Merge a shipped feature into the living specs, API and landscape. This is the one command that rewrites the source of truth, so every step before it exists to make the merge reviewable before it runs.",
1566
+ invocation: "loam instructions loam-ship $1",
1567
+ spine: [
1568
+ "confirm the code is actually built and merged — archiving early makes the docs claim something that does not exist",
1569
+ "`loam validate --feature $1` must come back valid; if it says nothing pinned the delta, rebase and re-validate first",
1570
+ "`loam archive $1 --dry-run` — read the whole plan and every warning before the merge touches anything",
1571
+ "`loam archive $1` for real, and read what it reports it did",
1572
+ "`loam unarchive $1` is the undo if the plan turns out to have been wrong",
1573
+ ],
1574
+ body: `Archive a shipped feature.
1575
+
1576
+ 1. Confirm the code is actually built and merged. Archiving folds the delta into the
1577
+ living docs; doing it early makes the docs claim something that does not exist.
1578
+ \`loam status $1 --json\` answers "is this actually finished" in one call, and its
1579
+ \`next[]\` names what is outstanding if it is not.
1580
+ 2. \`loam validate --feature $1 --json\` — must come back \`valid: true\`.
1581
+ If it reports \`delta.baseline-missing\` or \`openapi.baseline-missing\`, run
1582
+ \`loam rebase $1\` and re-validate BEFORE going on: those two say nothing has
1583
+ pinned what this delta was written against, so the merge cannot tell the text
1584
+ you EDITED from the text you merely restated, and it upserts all of it —
1585
+ reverting whatever another feature landed in between, silently and with exit 0.
1586
+ 3. \`loam archive $1 --dry-run --json\` first. \`plan[]\` is every file the merge would
1587
+ write (\`create\` / \`update\`, then the final \`move\` into \`features/archive/\`), and
1588
+ none of them are written — read it before letting the merge touch the source of
1589
+ truth, and stop if a file you did not expect is on it. \`warnings[]\` is what the
1590
+ merge will do that is legal but lossy — advisory warnings never block, which is
1591
+ exactly why you read them now: \`openapi.op-modified\` means an operation the living
1592
+ contract already defines gets overwritten wholesale (\`openapi.component-modified\`
1593
+ is the same story for a component the merged operations carry),
1594
+ \`delta.baseline-missing\` / \`openapi.baseline-missing\` mean the pins are still not
1595
+ there and this merge may revert somebody (go back to step 2),
1596
+ \`service.no-model\` means a service arrives with no C4 centre behind it, and
1597
+ \`delta.added-conflict\`
1598
+ means another feature in flight adds the same requirement — the first to archive
1599
+ lands it, and the other's archive is then refused (\`delta.added-duplicate\`: its
1600
+ ADDED now collides with the living spec) unless a human \`--approve\`s the
1601
+ replacement.
1602
+ 4. \`loam archive $1 --json\`. It merges three axes into the living state — requirements
1603
+ into \`services/<svc>/spec.md\`, endpoints into \`services/<svc>/openapi.yaml\`,
1604
+ elements and edges into \`architecture/landscape.likec4\` — then moves the feature
1605
+ under \`features/archive/\`. Success is \`ok: true\`; on \`ok: false\`, branch on
1606
+ \`error.code\`:
1607
+ - \`not-coherent\` — gating coherence issues; \`issues[]\` in the envelope lists them,
1608
+ each with \`gates\` resolved (advisory warnings do not block). Fix the breaches.
1609
+ \`--approve\` overrides the gating issues and is a human's call to make, not
1610
+ yours: report the breach and stop. \`openapi.remove-marker-path-level\` arrives
1611
+ here too: an \`x-loam-remove\` written beside the methods instead of inside the
1612
+ operation retires nothing.
1613
+ - \`living-outside-requirements\` — the LIVING spec holds a requirement outside
1614
+ \`## Requirements\`, and the merge would duplicate it. Re-home it, then re-run.
1615
+ \`--approve\` does not move this one.
1616
+ - \`archive-exists\` — \`features/archive/\` already has that directory; a human
1617
+ decides what it is.
1618
+ - \`merge-failed\` — the merge could not be computed, or failed and was rolled
1619
+ back. Either way the living docs are unchanged; fix the reported cause, re-run.
1620
+ A file loam cannot decode as UTF-8 refuses here by name rather than being
1621
+ rewritten with replacement characters. So does a living document that still
1622
+ holds git conflict markers — the \`spec.merge-conflict\` /
1623
+ \`landscape.merge-conflict\` findings ride in \`findings[]\`, and \`--approve\`
1624
+ does not override them: the loss is mechanical, not a judgment call.
1625
+ - \`docs-busy\` — another \`loam archive\` or \`loam unarchive\` holds the docs
1626
+ repo's lock. Nothing was read or written; wait for it and re-run once.
1627
+ - \`commit-interrupted\` — a previous archive or unarchive was killed mid-commit
1628
+ and this run cannot repair it on its own: a half-written file was edited
1629
+ since, its pre-image is gone or altered, or \`.loam-commit\` cannot be read.
1630
+ Run \`loam doctor\` (\`doctor.commit-interrupted\` / \`doctor.commit-unreadable\`
1631
+ name the same state) and hand it to a human — re-running does not help.
1632
+ - \`rollback-incomplete\` — the merge failed AND some files could not be restored.
1633
+ Stop and hand it to a human; the message lists the files to check.
1634
+
1635
+ Archived by mistake? \`loam unarchive $1\` restores the living docs from the snapshot
1636
+ archive left in \`features/archive/$1*/.loam-before/\` and re-opens the feature. Do not
1637
+ hand-edit the living docs back instead: the previous text of a \`MODIFIED\` requirement
1638
+ is in that snapshot and nowhere else, so anything you write by hand is a guess. It
1639
+ refuses under \`snapshot-corrupt\` when a pre-image's bytes no longer match the digest
1640
+ the archive recorded — \`--force\` does not override that one, because the damage is
1641
+ to the undo itself — and under \`snapshot-missing\` for an archive older than the
1642
+ current snapshot format. Both mean version control is the way back.
1643
+ `,
1644
+ },
1645
+ ];
1646
+ /**
1647
+ * Pre-approved tools in a generated file's frontmatter, so an agent that honors
1648
+ * the field stops asking permission for each `loam` call. It only pre-approves:
1649
+ * everything else a command needs (Read, Write, the service's own test runner)
1650
+ * stays under the user's normal permission settings, and a tool that does not
1651
+ * know the field ignores it.
1652
+ */
1653
+ const LOAM_ALLOWED_TOOLS = "Bash(loam:*)";
1654
+ /**
1655
+ * The Agent Skills convention, which every tool in the registry that reads
1656
+ * skills at all spells the same way: `<tool-dir>/skills/<name>/SKILL.md`, YAML
1657
+ * frontmatter, then the shared body verbatim.
1658
+ *
1659
+ * `description` is load-bearing in a way no command file's is — it is the only
1660
+ * thing the model sees before deciding whether to load the skill — so it comes
1661
+ * from the same `CommandContent.description` the command wrappers use rather
1662
+ * than a second, skill-flavoured copy that could drift into a better pitch for
1663
+ * a worse protocol.
1664
+ */
1665
+ const skillsIn = (dir) => ({
1666
+ path: (name) => [dir, "skills", name, "SKILL.md"],
1667
+ format: (c) => `---\nname: ${c.name}\ndescription: ${c.description}\n` +
1668
+ `allowed-tools: ${LOAM_ALLOWED_TOOLS}\n---\n\n${c.body}`,
1669
+ });
1670
+ /**
1671
+ * TOML basic-string escapes for the one non-markdown format. Today's bodies
1672
+ * carry neither backslashes nor `"""`, but the escape is what keeps a future
1673
+ * body edit from silently corrupting the generated TOML.
1674
+ */
1675
+ const tomlLine = (s) => s.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
1676
+ const tomlBlock = (s) => s.replace(/\\/g, "\\\\").replace(/"""/g, '""\\"');
1677
+ /* The four markdown command dialects the registry actually needs. Shared so a
1678
+ * tool's entry is its PATH and its dialect's name — the part that differs —
1679
+ * instead of a wall of near-identical template literals. `claude` deliberately
1680
+ * spells its own wrapper inline even though `hintedMd` matches it byte for
1681
+ * byte: an edit made here for some other tool must not be able to re-spell the
1682
+ * one format that is frozen. */
1683
+ /** Description-only frontmatter — the most common dialect by a wide margin. */
1684
+ const describedMd = (c) => `---\ndescription: ${c.description}\n---\n\n${c.body}`;
1685
+ /** Description plus Claude's `argument-hint`, for the tools that copied it. */
1686
+ const hintedMd = (c) => `---\ndescription: ${c.description}\nargument-hint: ${c.argumentHint}\n---\n\n${c.body}`;
1687
+ /** Name plus description — the Cascade-style workflow frontmatter. */
1688
+ const namedMd = (c) => `---\nname: ${c.name}\ndescription: ${c.description}\n---\n\n${c.body}`;
1689
+ /** No frontmatter at all: a title line is the whole wrapper. */
1690
+ const titledMd = (c) => `# ${c.name}\n\n${c.body}`;
1691
+ /** The `loam-` prefix a namespace directory carries instead of the file name. */
1692
+ const unprefixed = (name) => name.replace(/^loam-/, "");
1693
+ /**
1694
+ * What a generated file actually contains: the purpose, the spine, and the
1695
+ * command that prints the rest.
1696
+ *
1697
+ * The protocol itself stays in `body` and ships inside the binary, reachable as
1698
+ * `loam instructions <name>`. The file on disk gets a pointer to it, because
1699
+ * these two things go stale in opposite directions and only one of them can be
1700
+ * fixed. A generated file is written once, never regenerated (that is the
1701
+ * never-overwrite contract, and it is deliberate — your edits outrank the
1702
+ * template), so a repo scaffolded a year ago holds a year-old protocol: exact
1703
+ * flags, exact finding codes, exact step order, all of it asserted with total
1704
+ * confidence by a file whose reader has no way to know it is describing a
1705
+ * different release. That is not a hypothetical failure mode. It is the one
1706
+ * every project shipping generated agent instructions has had, and the fix
1707
+ * everybody reaches for — regenerate on upgrade — trades it for silently
1708
+ * overwriting what a human wrote.
1709
+ *
1710
+ * Thinning the file is the third option. What remains is what does not move
1711
+ * between releases: what this workflow is for, and the verbs in order. What
1712
+ * leaves is everything version-shaped. `doctor.agent-files-stale` still reports
1713
+ * a file whose stamp has fallen behind, and now has almost nothing to be right
1714
+ * about — which is the point.
1715
+ *
1716
+ * The trade is real and worth stating: an agent that cannot execute `loam` now
1717
+ * has a spine instead of a protocol. That is the correct failure. Every step
1718
+ * below the spine is a loam invocation, so an agent that cannot run loam was
1719
+ * never going to complete this workflow from the file either — it was going to
1720
+ * try, using a year-old flag.
1721
+ */
1722
+ function stubBody(c) {
1723
+ const steps = c.spine.map((s, i) => ` ${i + 1}. ${s}`).join("\n");
1724
+ return `${c.purpose}
1725
+
1726
+ **Run this first.** It is the protocol, and it ships with the binary you are about
1727
+ to call — so it names this loam's flags, its finding codes and its fix tables,
1728
+ not the ones that were current when this repository was scaffolded:
1729
+
1730
+ ${c.invocation}
1731
+
1732
+ The spine it fills in, so you can tell a run that went sideways from one that did not:
1733
+
1734
+ ${steps}
1735
+
1736
+ Every step above is a \`loam\` invocation, and each command's own \`--json\` output
1737
+ carries what to do next: findings have stable codes, and \`loam status --json\` puts
1738
+ the ordered \`next[]\` — each entry a code and the literal command — in one place.
1739
+ Branch on the codes, never on the prose.
1740
+
1741
+ This file is a pointer, not the protocol. loam wrote it once and will never
1742
+ rewrite it, so your edits here outrank the template and nothing will quietly
1743
+ undo them. Where this file and \`loam instructions\` disagree, the command is right.
1744
+ `;
1745
+ }
1746
+ /**
1747
+ * The command body a file actually gets: the pointer, under the same version
1748
+ * stamp AGENTS.md carries.
1749
+ *
1750
+ * The stamp is applied once, here, rather than in twenty wrappers — it is a
1751
+ * fact about loam, not about a tool's dialect, and a per-wrapper copy is twenty
1752
+ * chances to forget it. It rides at the top of the BODY (not the file) because
1753
+ * every dialect puts something of its own first: YAML frontmatter, a TOML key,
1754
+ * a title line. An HTML comment is invisible in all of them, and
1755
+ * `agentsStampVersion` matches it wherever in the file it lands.
1756
+ *
1757
+ * A repo initialized before stamping existed has unstamped files, which is
1758
+ * exactly what `doctor.agent-files-stale` is for: nobody has confirmed that
1759
+ * those instructions still describe this binary.
1760
+ */
1761
+ const stubbed = (c) => ({
1762
+ ...c,
1763
+ body: `${agentsStampLine(LOAM_VERSION)}\n\n${stubBody(c)}`,
1764
+ });
1765
+ /**
1766
+ * The original target and the default. This WRAPPER must stay byte-identical
1767
+ * to what loam has always written — SLASH_COMMANDS below derives through it —
1768
+ * and an already-initialized repo must still read as fully scaffolded
1769
+ * (`skipped` is existsSync, never content). What the wrapper wraps now carries
1770
+ * a version stamp, which is the one thing about a generated file loam will
1771
+ * report on; see `stubbed`.
1772
+ *
1773
+ * It stands outside the registry object because SLASH_COMMANDS reaches for it
1774
+ * by name. That lookup was `AGENT_TOOLS["claude"]!.format!` — two assertions on
1775
+ * an exported const evaluated at import time, so renaming the key, or letting
1776
+ * this entry lose `format` the way `codex` legitimately has, would have thrown
1777
+ * a TypeError while the CLI entry point was still being loaded: before
1778
+ * `program.exitOverride()` exists, and therefore with no envelope around it.
1779
+ * Named and typed, both mistakes are typecheck failures instead. The registry
1780
+ * still lists it first, and the emitted bytes are unchanged.
1781
+ */
1782
+ const CLAUDE = {
1783
+ path: (name) => [".claude", "commands", `${name}.md`],
1784
+ format: (c) => `---\ndescription: ${c.description}\nargument-hint: ${c.argumentHint}\n---\n\n${c.body}`,
1785
+ skill: skillsIn(".claude"),
1786
+ detect: [".claude"],
1787
+ };
1788
+ export const AGENT_TOOLS = {
1789
+ claude: CLAUDE,
1790
+ // Cursor invokes by flat file name (`/loam-check`); its frontmatter `name`
1791
+ // field spells that invocation form.
1792
+ cursor: {
1793
+ path: (name) => [".cursor", "commands", `${name}.md`],
1794
+ format: (c) => `---\nname: /${c.name}\n---\n\n${c.body}`,
1795
+ skill: skillsIn(".cursor"),
1796
+ detect: [".cursor"],
1797
+ },
1798
+ // Copilot prompt files: `.prompt.md` extension, description-only frontmatter.
1799
+ // Detection cannot use `.github/` — almost every repo has one and almost none
1800
+ // of them means Copilot — so it looks for the files Copilot itself reads.
1801
+ "github-copilot": {
1802
+ path: (name) => [".github", "prompts", `${name}.prompt.md`],
1803
+ format: describedMd,
1804
+ skill: skillsIn(".github"),
1805
+ detect: [
1806
+ ".github/copilot-instructions.md",
1807
+ ".github/instructions",
1808
+ ".github/prompts",
1809
+ ".github/agents",
1810
+ ".github/skills",
1811
+ ".github/workflows/copilot-setup-steps.yml",
1812
+ ".github/.mcp.json",
1813
+ ],
1814
+ },
1815
+ // Gemini: TOML, and a namespace DIRECTORY — `.gemini/commands/loam/check.toml`
1816
+ // is invoked as `/loam:check`, so the file name drops the `loam-` prefix the
1817
+ // flat-named tools carry.
1818
+ gemini: {
1819
+ path: (name) => [".gemini", "commands", "loam", `${unprefixed(name)}.toml`],
1820
+ format: (c) => `description = "${tomlLine(c.description)}"\nprompt = """\n${tomlBlock(c.body)}"""`,
1821
+ skill: skillsIn(".gemini"),
1822
+ detect: [".gemini"],
1823
+ },
1824
+ opencode: {
1825
+ path: (name) => [".opencode", "commands", `${name}.md`],
1826
+ format: describedMd,
1827
+ skill: skillsIn(".opencode"),
1828
+ detect: [".opencode"],
1829
+ },
1830
+ // Cline reads workflows from `.clinerules/` — plain markdown, no frontmatter —
1831
+ // but its skills from `.cline/`. The two directories are unrelated, so both
1832
+ // count as a sighting.
1833
+ cline: {
1834
+ path: (name) => [".clinerules", "workflows", `${name}.md`],
1835
+ format: (c) => `# ${c.name}\n\n${c.body}`,
1836
+ skill: skillsIn(".cline"),
1837
+ detect: [".cline", ".clinerules"],
1838
+ },
1839
+ // Amazon Q surfaces these as its PROMPT library, not as commands: the file is
1840
+ // invoked `@loam-check`, never `/loam-check`. Same file shape regardless.
1841
+ "amazon-q": {
1842
+ path: (name) => [".amazonq", "prompts", `${name}.md`],
1843
+ format: describedMd,
1844
+ skill: skillsIn(".amazonq"),
1845
+ detect: [".amazonq"],
1846
+ },
1847
+ // Antigravity's directory is `.agent/`, which is why the id and the directory
1848
+ // do not match here as they do almost everywhere else.
1849
+ antigravity: {
1850
+ path: (name) => [".agent", "workflows", `${name}.md`],
1851
+ format: describedMd,
1852
+ skill: skillsIn(".agent"),
1853
+ detect: [".agent"],
1854
+ },
1855
+ // Auggie is the Augment CLI: id `auggie`, directory `.augment/`.
1856
+ auggie: {
1857
+ path: (name) => [".augment", "commands", `${name}.md`],
1858
+ format: hintedMd,
1859
+ skill: skillsIn(".augment"),
1860
+ detect: [".augment"],
1861
+ },
1862
+ // Skills-only, deliberately: Codex reads `.codex/skills/` and does not load
1863
+ // custom command files, so it declares no command emitter at all rather than
1864
+ // laying down files nothing will ever open.
1865
+ codex: {
1866
+ skill: skillsIn(".codex"),
1867
+ detect: [".codex"],
1868
+ },
1869
+ // Continue's prompt files take a bare `.prompt` extension, and `invokable`
1870
+ // is what promotes one from a template to something you can call.
1871
+ continue: {
1872
+ path: (name) => [".continue", "prompts", `${name}.prompt`],
1873
+ format: (c) => `---\nname: ${c.name}\ndescription: ${c.description}\ninvokable: true\n---\n\n${c.body}`,
1874
+ skill: skillsIn(".continue"),
1875
+ detect: [".continue"],
1876
+ },
1877
+ // Crush namespaces like Gemini does — `.crush/commands/loam/check.md` is
1878
+ // `/loam:check` — but stays markdown.
1879
+ crush: {
1880
+ path: (name) => [".crush", "commands", "loam", `${unprefixed(name)}.md`],
1881
+ format: namedMd,
1882
+ skill: skillsIn(".crush"),
1883
+ detect: [".crush"],
1884
+ },
1885
+ // Devin Desktop, formerly Windsurf: Cascade-style workflows, and the rename
1886
+ // moved the directory from `.windsurf/` to `.devin/`. New files go to the new
1887
+ // directory; a repo still holding the old one is still a Devin repo, so both
1888
+ // are sightings.
1889
+ devin: {
1890
+ path: (name) => [".devin", "workflows", `${name}.md`],
1891
+ format: namedMd,
1892
+ skill: skillsIn(".devin"),
1893
+ detect: [".devin", ".windsurf"],
1894
+ },
1895
+ factory: {
1896
+ path: (name) => [".factory", "commands", `${name}.md`],
1897
+ format: hintedMd,
1898
+ skill: skillsIn(".factory"),
1899
+ detect: [".factory"],
1900
+ },
1901
+ junie: {
1902
+ path: (name) => [".junie", "commands", `${name}.md`],
1903
+ format: describedMd,
1904
+ skill: skillsIn(".junie"),
1905
+ detect: [".junie"],
1906
+ },
1907
+ // Kilo Code workflows carry no frontmatter — the title line is all the
1908
+ // wrapper there is.
1909
+ kilocode: {
1910
+ path: (name) => [".kilocode", "workflows", `${name}.md`],
1911
+ format: titledMd,
1912
+ skill: skillsIn(".kilocode"),
1913
+ detect: [".kilocode"],
1914
+ },
1915
+ // Kiro uses Copilot's `.prompt.md` extension under its own directory.
1916
+ kiro: {
1917
+ path: (name) => [".kiro", "prompts", `${name}.prompt.md`],
1918
+ format: describedMd,
1919
+ skill: skillsIn(".kiro"),
1920
+ detect: [".kiro"],
1921
+ },
1922
+ // Qwen Code retired its TOML commands in favour of markdown + frontmatter,
1923
+ // so this is markdown despite the Gemini lineage.
1924
+ qwen: {
1925
+ path: (name) => [".qwen", "commands", `${name}.md`],
1926
+ format: describedMd,
1927
+ skill: skillsIn(".qwen"),
1928
+ detect: [".qwen"],
1929
+ },
1930
+ // Roo/Zoo Code: id `roocode`, directory `.roo/`, no frontmatter.
1931
+ roocode: {
1932
+ path: (name) => [".roo", "commands", `${name}.md`],
1933
+ format: titledMd,
1934
+ skill: skillsIn(".roo"),
1935
+ detect: [".roo"],
1936
+ },
1937
+ trae: {
1938
+ path: (name) => [".trae", "commands", `${name}.md`],
1939
+ format: namedMd,
1940
+ skill: skillsIn(".trae"),
1941
+ detect: [".trae"],
1942
+ },
1943
+ };
1944
+ /**
1945
+ * The Claude-format files, keyed by command name: exactly what **this** binary
1946
+ * would write, derived through the same adapter that writes them, so the export
1947
+ * and a freshly scaffolded repo can never disagree.
1948
+ *
1949
+ * Read that scope literally. It used to be described as the on-disk contract of
1950
+ * every repo initialized before `--tools` existed, and that is no longer true
1951
+ * of anything but a fresh scaffold: a file holds the STUB now, every repo
1952
+ * scaffolded before that holds the full protocol, and because loam never
1953
+ * regenerates a generated file the two disagree permanently and on purpose. An
1954
+ * export cannot describe files this binary did not write.
1955
+ *
1956
+ * The protocol those older files carry is `PROTOCOLS`. Anything asserting on
1957
+ * protocol content wants that one — this answers "what would loam write here",
1958
+ * not "what does loam instruct".
1959
+ */
1960
+ export const SLASH_COMMANDS = Object.fromEntries(COMMANDS.map((c) => [c.name, CLAUDE.format(stubbed(c))]));
1961
+ /**
1962
+ * The workflow protocols themselves, keyed by command name — what
1963
+ * `loam instructions <name>` prints, and the corpus every check over loam's
1964
+ * agent-facing prose reads.
1965
+ *
1966
+ * Separate from SLASH_COMMANDS since the file became a pointer: the protocol is
1967
+ * still the thing that must document every stable code (`codes-drift`) and
1968
+ * every `loam …` line it teaches must still parse against the real CLI
1969
+ * (`agent-commands-runnable`). Those two properties are about the INSTRUCTIONS,
1970
+ * and they did not move just because the delivery did.
1971
+ */
1972
+ export const PROTOCOLS = Object.fromEntries(COMMANDS.map((c) => [c.name, c.body]));
1973
+ /** The workflow names, in cycle order — `loam instructions` with no argument lists these. */
1974
+ export const WORKFLOWS = COMMANDS.map(({ name, description, argumentHint }) => ({ name, description, argumentHint }));
1975
+ /**
1976
+ * One workflow's protocol with `$1`, `$2`, … replaced by the arguments given.
1977
+ *
1978
+ * An unsupplied placeholder is LEFT STANDING rather than blanked: the bodies
1979
+ * are written so `$1` reads as "the feature id goes here", and a protocol that
1980
+ * silently dropped it would hand an agent `loam validate --feature --json` — a
1981
+ * command that parses, runs, and answers a different question. Extra arguments
1982
+ * are ignored for the same reason: this substitutes, it does not validate.
1983
+ *
1984
+ * An EMPTY argument counts as unsupplied, and that is not pedantry. Several
1985
+ * tool dialects expand an absent positional to the empty string, and
1986
+ * `loam-feature`'s own invocation quotes its second one (`--title "$2"`), so
1987
+ * `/loam-feature FEAT-101` with no title arrives here as `["FEAT-101", ""]`.
1988
+ * Blanking it produced `loam new FEAT-101 --title ""` — which is exactly the
1989
+ * failure the paragraph above describes, reached by the one path that paragraph
1990
+ * did not cover: the command succeeds, and writes a feature whose title is the
1991
+ * empty string and whose intent.md opens with a bare `#`.
1992
+ */
1993
+ export function protocolFor(name, args = []) {
1994
+ const body = PROTOCOLS[name];
1995
+ if (body === undefined)
1996
+ return null;
1997
+ return body.replace(/\$([1-9])/g, (whole, digit) => {
1998
+ const arg = args[Number(digit) - 1];
1999
+ return arg === undefined || arg === "" ? whole : arg;
2000
+ });
2001
+ }
2002
+ /** The two ways a command body reaches an agent. Both are on by default. */
2003
+ export const DELIVERIES = ["commands", "skills"];
2004
+ /**
2005
+ * Every file the selected tools would lay down, in creation order — the same
2006
+ * list `init` probes for `skipped` BEFORE the scaffold runs, so created +
2007
+ * skipped is the same list on every repo.
2008
+ *
2009
+ * Called with two arguments it returns EVERYTHING the binary would write under
2010
+ * default delivery: commands and skills both. That is the contract callers
2011
+ * outside this module depend on — `loam doctor` asks "what should be here?"
2012
+ * and a half-answer would report a fully-scaffolded repo as fully scaffolded
2013
+ * while ignoring every skill file. The name predates the second delivery and is
2014
+ * kept because it is imported elsewhere; `delivery` narrows it.
2015
+ */
2016
+ export function plannedCommandFiles(cwd, toolIds, delivery = DELIVERIES) {
2017
+ return toolIds.flatMap((id) => {
2018
+ const tool = AGENT_TOOLS[id];
2019
+ if (tool === undefined)
2020
+ throw new Error(`unknown agent tool: ${id}`);
2021
+ const emitters = [];
2022
+ // A tool declares only the deliveries it actually reads, so an absent
2023
+ // emitter is a fact about the tool, never a reason to fall back to some
2024
+ // other tool's convention.
2025
+ const { path, format, skill } = tool;
2026
+ if (delivery.includes("commands") && path !== undefined && format !== undefined) {
2027
+ emitters.push({ path, format });
2028
+ }
2029
+ if (delivery.includes("skills") && skill !== undefined)
2030
+ emitters.push(skill);
2031
+ return emitters.flatMap((e) => COMMANDS.map((c) => ({ path: join(cwd, ...e.path(c.name)), content: e.format(stubbed(c)) })));
2032
+ });
2033
+ }
2034
+ /**
2035
+ * Write the command and skill files for the selected tools into `cwd`. Existing
2036
+ * files are left alone — the never-overwrite contract covers every tool and
2037
+ * every delivery, not just the default one. Returns the paths created.
2038
+ */
2039
+ export async function scaffoldAgentCommands(cwd, toolIds = ["claude"], delivery = DELIVERIES) {
2040
+ const created = [];
2041
+ for (const { path, content } of plannedCommandFiles(cwd, toolIds, delivery)) {
2042
+ if (existsSync(path))
2043
+ continue;
2044
+ await mkdir(dirname(path), { recursive: true });
2045
+ await writeFile(path, content, "utf8");
2046
+ created.push(path);
2047
+ }
2048
+ return created;
2049
+ }
2050
+ /**
2051
+ * The registry ids whose marker paths are present in `cwd` — what `loam init`
2052
+ * writes for when nobody passed `--tools`.
2053
+ *
2054
+ * A repo with `.cursor/` and no Claude Code used to get `.claude/commands/`
2055
+ * and nothing it could actually run. Presence is the whole test, and existence
2056
+ * (not directory-ness) is what is asked, because a marker is as often a file as
2057
+ * a directory — `.github/copilot-instructions.md` is the honest signal for
2058
+ * Copilot where `.github/` itself is noise.
2059
+ */
2060
+ export function detectAgentTools(cwd) {
2061
+ return Object.entries(AGENT_TOOLS)
2062
+ .filter(([, tool]) => tool.detect.some((marker) => existsSync(join(cwd, marker))))
2063
+ .map(([id]) => id);
2064
+ }
2065
+ //# sourceMappingURL=agent.js.map