@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.
- package/CHANGELOG.md +138 -0
- package/COMPARISON.md +63 -0
- package/LICENSE +21 -0
- package/MIGRATING-from-OpenSpec.md +202 -0
- package/README.md +246 -9
- package/SCHEMA.md +425 -0
- package/SECURITY.md +9 -0
- package/dist/cli.js +142 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/adopt.js +240 -0
- package/dist/commands/adopt.js.map +1 -0
- package/dist/commands/archive.js +1313 -0
- package/dist/commands/archive.js.map +1 -0
- package/dist/commands/delta.js +388 -0
- package/dist/commands/delta.js.map +1 -0
- package/dist/commands/dependencies.js +83 -0
- package/dist/commands/dependencies.js.map +1 -0
- package/dist/commands/docs-repo-gate.js +104 -0
- package/dist/commands/docs-repo-gate.js.map +1 -0
- package/dist/commands/doctor.js +85 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/explore.js +195 -0
- package/dist/commands/explore.js.map +1 -0
- package/dist/commands/format.js +17 -0
- package/dist/commands/format.js.map +1 -0
- package/dist/commands/gherkin.js +309 -0
- package/dist/commands/gherkin.js.map +1 -0
- package/dist/commands/init.js +250 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/instructions.js +70 -0
- package/dist/commands/instructions.js.map +1 -0
- package/dist/commands/list.js +291 -0
- package/dist/commands/list.js.map +1 -0
- package/dist/commands/migrate-openspec.js +950 -0
- package/dist/commands/migrate-openspec.js.map +1 -0
- package/dist/commands/new.js +413 -0
- package/dist/commands/new.js.map +1 -0
- package/dist/commands/rebase.js +356 -0
- package/dist/commands/rebase.js.map +1 -0
- package/dist/commands/show.js +320 -0
- package/dist/commands/show.js.map +1 -0
- package/dist/commands/status.js +254 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/unarchive.js +317 -0
- package/dist/commands/unarchive.js.map +1 -0
- package/dist/commands/validate.js +1591 -0
- package/dist/commands/validate.js.map +1 -0
- package/dist/commands/verify.js +775 -0
- package/dist/commands/verify.js.map +1 -0
- package/dist/commands/vouch.js +343 -0
- package/dist/commands/vouch.js.map +1 -0
- package/dist/core/agent.js +2065 -0
- package/dist/core/agent.js.map +1 -0
- package/dist/core/agents-stamp.js +68 -0
- package/dist/core/agents-stamp.js.map +1 -0
- package/dist/core/arch.js +130 -0
- package/dist/core/arch.js.map +1 -0
- package/dist/core/brief.js +733 -0
- package/dist/core/brief.js.map +1 -0
- package/dist/core/coherence.js +577 -0
- package/dist/core/coherence.js.map +1 -0
- package/dist/core/concurrency.js +51 -0
- package/dist/core/concurrency.js.map +1 -0
- package/dist/core/config.js +180 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/delta.js +476 -0
- package/dist/core/delta.js.map +1 -0
- package/dist/core/dependencies.js +356 -0
- package/dist/core/dependencies.js.map +1 -0
- package/dist/core/docs.js +144 -0
- package/dist/core/docs.js.map +1 -0
- package/dist/core/doctor.js +559 -0
- package/dist/core/doctor.js.map +1 -0
- package/dist/core/document-bytes.js +59 -0
- package/dist/core/document-bytes.js.map +1 -0
- package/dist/core/explore.js +225 -0
- package/dist/core/explore.js.map +1 -0
- package/dist/core/fleet-context.js +225 -0
- package/dist/core/fleet-context.js.map +1 -0
- package/dist/core/frontmatter.js +199 -0
- package/dist/core/frontmatter.js.map +1 -0
- package/dist/core/gherkin.js +463 -0
- package/dist/core/gherkin.js.map +1 -0
- package/dist/core/health.js +67 -0
- package/dist/core/health.js.map +1 -0
- package/dist/core/ids.js +122 -0
- package/dist/core/ids.js.map +1 -0
- package/dist/core/issue.js +9 -0
- package/dist/core/issue.js.map +1 -0
- package/dist/core/json.js +74 -0
- package/dist/core/json.js.map +1 -0
- package/dist/core/likec4.js +479 -0
- package/dist/core/likec4.js.map +1 -0
- package/dist/core/maturity.js +66 -0
- package/dist/core/maturity.js.map +1 -0
- package/dist/core/openapi-merge.js +552 -0
- package/dist/core/openapi-merge.js.map +1 -0
- package/dist/core/openapi.js +255 -0
- package/dist/core/openapi.js.map +1 -0
- package/dist/core/openspec-inventory.js +1440 -0
- package/dist/core/openspec-inventory.js.map +1 -0
- package/dist/core/path-safety.js +168 -0
- package/dist/core/path-safety.js.map +1 -0
- package/dist/core/provenance.js +720 -0
- package/dist/core/provenance.js.map +1 -0
- package/dist/core/records.js +33 -0
- package/dist/core/records.js.map +1 -0
- package/dist/core/repo.js +436 -0
- package/dist/core/repo.js.map +1 -0
- package/dist/core/report.js +47 -0
- package/dist/core/report.js.map +1 -0
- package/dist/core/results.js +199 -0
- package/dist/core/results.js.map +1 -0
- package/dist/core/spec.js +481 -0
- package/dist/core/spec.js.map +1 -0
- package/dist/core/staging.js +890 -0
- package/dist/core/staging.js.map +1 -0
- package/dist/core/status.js +1104 -0
- package/dist/core/status.js.map +1 -0
- package/dist/core/steps.js +72 -0
- package/dist/core/steps.js.map +1 -0
- package/dist/core/verify.js +807 -0
- package/dist/core/verify.js.map +1 -0
- package/dist/core/version.js +24 -0
- package/dist/core/version.js.map +1 -0
- 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
|