@ybotok/loam 0.0.0 → 0.1.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/CHANGELOG.md +89 -0
  2. package/COMPARISON.md +63 -0
  3. package/LICENSE +21 -0
  4. package/MIGRATING-from-OpenSpec.md +202 -0
  5. package/README.md +241 -9
  6. package/SCHEMA.md +425 -0
  7. package/SECURITY.md +9 -0
  8. package/dist/cli.js +138 -0
  9. package/dist/cli.js.map +1 -0
  10. package/dist/commands/adopt.js +226 -0
  11. package/dist/commands/adopt.js.map +1 -0
  12. package/dist/commands/archive.js +1297 -0
  13. package/dist/commands/archive.js.map +1 -0
  14. package/dist/commands/delta.js +361 -0
  15. package/dist/commands/delta.js.map +1 -0
  16. package/dist/commands/dependencies.js +78 -0
  17. package/dist/commands/dependencies.js.map +1 -0
  18. package/dist/commands/doctor.js +85 -0
  19. package/dist/commands/doctor.js.map +1 -0
  20. package/dist/commands/gherkin.js +270 -0
  21. package/dist/commands/gherkin.js.map +1 -0
  22. package/dist/commands/init.js +250 -0
  23. package/dist/commands/init.js.map +1 -0
  24. package/dist/commands/list.js +361 -0
  25. package/dist/commands/list.js.map +1 -0
  26. package/dist/commands/migrate-openspec.js +933 -0
  27. package/dist/commands/migrate-openspec.js.map +1 -0
  28. package/dist/commands/new.js +412 -0
  29. package/dist/commands/new.js.map +1 -0
  30. package/dist/commands/rebase.js +358 -0
  31. package/dist/commands/rebase.js.map +1 -0
  32. package/dist/commands/show.js +325 -0
  33. package/dist/commands/show.js.map +1 -0
  34. package/dist/commands/status.js +270 -0
  35. package/dist/commands/status.js.map +1 -0
  36. package/dist/commands/unarchive.js +310 -0
  37. package/dist/commands/unarchive.js.map +1 -0
  38. package/dist/commands/validate.js +1609 -0
  39. package/dist/commands/validate.js.map +1 -0
  40. package/dist/commands/verify.js +759 -0
  41. package/dist/commands/verify.js.map +1 -0
  42. package/dist/commands/vouch.js +325 -0
  43. package/dist/commands/vouch.js.map +1 -0
  44. package/dist/core/agent.js +1823 -0
  45. package/dist/core/agent.js.map +1 -0
  46. package/dist/core/agents-stamp.js +68 -0
  47. package/dist/core/agents-stamp.js.map +1 -0
  48. package/dist/core/arch.js +104 -0
  49. package/dist/core/arch.js.map +1 -0
  50. package/dist/core/brief.js +733 -0
  51. package/dist/core/brief.js.map +1 -0
  52. package/dist/core/coherence.js +553 -0
  53. package/dist/core/coherence.js.map +1 -0
  54. package/dist/core/config.js +172 -0
  55. package/dist/core/config.js.map +1 -0
  56. package/dist/core/delta.js +457 -0
  57. package/dist/core/delta.js.map +1 -0
  58. package/dist/core/dependencies.js +352 -0
  59. package/dist/core/dependencies.js.map +1 -0
  60. package/dist/core/docs.js +143 -0
  61. package/dist/core/docs.js.map +1 -0
  62. package/dist/core/doctor.js +544 -0
  63. package/dist/core/doctor.js.map +1 -0
  64. package/dist/core/fleet-context.js +272 -0
  65. package/dist/core/fleet-context.js.map +1 -0
  66. package/dist/core/frontmatter.js +203 -0
  67. package/dist/core/frontmatter.js.map +1 -0
  68. package/dist/core/gherkin.js +521 -0
  69. package/dist/core/gherkin.js.map +1 -0
  70. package/dist/core/health.js +71 -0
  71. package/dist/core/health.js.map +1 -0
  72. package/dist/core/ids.js +93 -0
  73. package/dist/core/ids.js.map +1 -0
  74. package/dist/core/issue.js +9 -0
  75. package/dist/core/issue.js.map +1 -0
  76. package/dist/core/json.js +67 -0
  77. package/dist/core/json.js.map +1 -0
  78. package/dist/core/likec4.js +474 -0
  79. package/dist/core/likec4.js.map +1 -0
  80. package/dist/core/openapi-merge.js +519 -0
  81. package/dist/core/openapi-merge.js.map +1 -0
  82. package/dist/core/openapi.js +243 -0
  83. package/dist/core/openapi.js.map +1 -0
  84. package/dist/core/openspec-inventory.js +1403 -0
  85. package/dist/core/openspec-inventory.js.map +1 -0
  86. package/dist/core/path-safety.js +109 -0
  87. package/dist/core/path-safety.js.map +1 -0
  88. package/dist/core/provenance.js +695 -0
  89. package/dist/core/provenance.js.map +1 -0
  90. package/dist/core/repo.js +362 -0
  91. package/dist/core/repo.js.map +1 -0
  92. package/dist/core/report.js +38 -0
  93. package/dist/core/report.js.map +1 -0
  94. package/dist/core/results.js +201 -0
  95. package/dist/core/results.js.map +1 -0
  96. package/dist/core/spec.js +481 -0
  97. package/dist/core/spec.js.map +1 -0
  98. package/dist/core/staging.js +783 -0
  99. package/dist/core/staging.js.map +1 -0
  100. package/dist/core/status.js +1015 -0
  101. package/dist/core/status.js.map +1 -0
  102. package/dist/core/verify.js +793 -0
  103. package/dist/core/verify.js.map +1 -0
  104. package/dist/core/version.js +8 -0
  105. package/dist/core/version.js.map +1 -0
  106. package/package.json +63 -4
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent.js","sourceRoot":"","sources":["../../src/core/agent.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The agent contract: what `loam init` lays down so a coding agent can run the\n * cycle without being told it each time.\n *\n * AGENTS.md goes into the docs repo — it travels with the thing it describes.\n * The command and skill files go into the repo `init` runs in, because that is\n * where the agent is invoked — for whichever tools the repo shows signs of, via\n * the AGENT_TOOLS registry below: one shared body, a per-tool path and wrapper.\n * Neither is ever overwritten: they are starting points, and a team's edits to\n * them outrank ours.\n *\n * Two deliveries, one body. A slash command has to be TYPED, so it only ever\n * reaches an agent whose operator already knows loam exists. A skill is loaded\n * by the model itself when the task matches its `description`, which is how the\n * protocol reaches an agent that was never told about it. That difference is\n * the whole reason both are written by default.\n *\n * The stamp on the first line is the one concession to that never-refresh\n * contract: it records which loam wrote the file, so `loam validate --all` can\n * say when the tables below describe a binary that no longer exists\n * (`agents.stale` — detection only, never a rewrite; agents-stamp.ts).\n *\n * The per-tool command and skill files carry the SAME stamp, for the same\n * reason and with the same answer: `loam doctor` reports `doctor.agent-files-stale`\n * and never rewrites. Without it, absence was the only drift loam could see —\n * a command file mangled to one line left doctor saying `healthy: true`, which\n * across a hundred repositories is invisible rot with no repair path.\n */\nimport { existsSync } from \"node:fs\";\nimport { mkdir, writeFile } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\nimport { agentsStampLine } from \"./agents-stamp.js\";\nimport { LOAM_VERSION } from \"./version.js\";\n\nexport const AGENTS_MD = `${agentsStampLine(LOAM_VERSION)}\n# Working in this docs repo\n\nThis is a **loam** docs repo: the shared source of truth for a fleet of services —\ntheir architecture, their contracts, and what they are required to do. Everything\nhere is plain files. \\`loam\\` reads and writes them; delete \\`loam\\` and the docs remain.\n\n## Layout\n\n\\`\\`\\`\narchitecture/landscape.likec4 the living C4 model of the whole fleet\nservices/<svc>/\n model.likec4 this service's C4\n spec.md its living requirements (current state)\n arch.spec.md its living ARCHITECTURE requirements (outbox, retries, alerts)\n openapi.yaml its API contract\n adrs/ runbook.md health.yaml why it is like this, how to run it\nfeatures/<FEAT>/ a change in flight\n intent.md why, in business terms\n delta.likec4 the architecture change, tagged #<FEAT>\n specs/<svc>/spec.md the requirement change for one service\n specs/<svc>/arch.spec.md the architectural requirement change, same delta grammar\n specs/<svc>/openapi.yaml the endpoints this feature adds\n verification.yaml what was checked once the code was built\nfeatures/archive/<FEAT>/ shipped changes — the evolution history\n\\`\\`\\`\n\n## \\`loam.json\\` — the wiring, in every repo\n\nA fleet is **N + 1 repositories**: this docs repo, and one per service. Every one\nof them carries its own \\`loam.json\\`, and every one of them commits it — that\nfile is how any agent, human or CI runner standing in a directory finds the rest\nof the fleet. There is no environment variable and no global config; if a\ncommand cannot see a \\`loam.json\\` at or above the working directory, it refuses\nwith \\`no-config\\` rather than guessing.\n\n\\`\\`\\`json\n{ \"docsDir\": \"../docs-repo\", \"service\": \"payment-service\", \"gherkinDir\": \"features\" }\n\\`\\`\\`\n\n- \\`docsDir\\` — where the docs repo is, **stored exactly as it was passed** and\n resolved against the directory holding the \\`loam.json\\`. So \\`../docs-repo\\`\n keeps working on every machine that checks the repos out side by side, which\n is the point of committing the file. An absolute path is a warning\n (\\`doctor.docs-absolute\\`): it resolves only on the machine that ran \\`loam init\\`.\n The docs repo's own \\`loam.json\\` says \\`\"docsDir\": \".\"\\`.\n- \\`service\\` — the canonical id of the service THIS repo contains, i.e. the\n \\`services/<id>/\\` directory it is allowed to speak for. \\`loam vouch\\`,\n \\`loam gherkin\\` and \\`loam verify --service\\` all bind to it; without it they\n refuse (\\`repository-unavailable\\`) rather than write another service's\n documents from a repo that is not that service's.\n- \\`gherkinDir\\` — optional, default \\`features\\`: the directory \\`loam gherkin\\`\n writes its own \\`loam/\\` subtree into.\n- \\`agentTools\\` — optional: the agent tools \\`loam init\\` has written command and\n skill files for here, accumulated across runs. Written by \\`init\\`, read by\n \\`loam doctor\\`; nothing else depends on it, and a config without it loads.\n\n**Step 0 of everything below, once per repo:**\n\n\\`\\`\\`sh\nloam init --docs ../docs-repo --service payment-service # in EACH service repo\nloam doctor # before you trust any of it\n\\`\\`\\`\n\n\\`loam init --docs <dir>\\` **joins** an existing docs repo; creating a new one\ntakes \\`--create\\`, so a mistyped path cannot quietly scaffold a second source of\ntruth beside the real one. \\`loam doctor\\` is the read-only preflight: it reads\nloam.json defensively, resolves \\`docsDir\\`, probes access, checks \\`services/\\` and\nthe fleet map, and reports the service binding — run it first when anything\nbehaves as though the fleet were empty.\n\n**Living vs delta.** \\`services/<svc>/spec.md\\` is the complete current state.\n\\`features/<FEAT>/specs/<svc>/spec.md\\` is a diff against it, reviewed as a diff, with\nrequirements grouped under \\`## ADDED\\`, \\`## MODIFIED\\` or \\`## REMOVED Requirements\\`.\n\\`loam archive\\` folds the delta into the living state.\n\n## The architecture spec axis\n\nThe business spec will never mention the transactional outbox — that is\narchitecture. Retries, idempotency, metrics, alerts: real obligations no business\nscenario states, and exactly where generated code cuts corners unless the\nobligations are derived mechanically. They live in \\`arch.spec.md\\` — living and\ndelta, same grammar, same delta algebra, merged by \\`loam archive\\` through the\nsame code path (the two files are separate requirement namespaces).\n\nWhere a business requirement carries \\`Operations:\\`, an arch requirement carries\n\\`Covers:\\` — the model objects its scenarios exercise, comma-separated:\n\n\\`\\`\\`\nCovers: paymentService.db, paymentService -> kafka, alert:payment_5xx, sli:availability\n\\`\\`\\`\n\na C4 element (id, or the service a bound element stands for), an edge\n(\\`source -> target\\`), or a health signal the service's health.yaml declares.\nEvery entry must resolve — \\`covers.unknown\\` (warn) is the typo guard, because a\nmistyped entry silently costs the coverage it was written for.\n\nCoverage is then derived, never trusted, as two warnings: a feature's NEW tagged\nelements and edges want a covering requirement in its arch.spec.md deltas\n(\\`c4.uncovered\\`), and every alert/SLI health.yaml declares wants one in the\nliving arch spec (\\`health.uncovered\\`). None of this gates \\`loam archive\\`;\n\\`--strict\\` is the CI escalation. An absent arch.spec.md is not a finding —\npartial adoption is supported — but write one for any service with real\narchitecture, or its obligations ship unchecked.\n\n**Test levels, mapped once.** A business scenario is an acceptance test. An arch\nscenario is an integration/ops test — the outbox relay under a dead broker, the\nretry that stays idempotent, the alert rule that actually fires. \\`api.exposes\\`\nis a contract test. Unit tests sit below spec granularity and are your TDD\nconcern, not the specs'.\n\n## The generated Gherkin suite\n\nScenarios are the acceptance criteria, and \\`loam gherkin\\` makes them\nexecutable: real \\`.feature\\` files, written into the SERVICE'S repo — the one\nloam command that writes there, because tests live with the code they gate.\nOutput lands in \\`<gherkinDir>/loam/\\` (\\`gherkinDir\\` is an optional loam.json\nfact, default \\`features\\`), and that \\`loam/\\` subdirectory is loam's own\nderived space: regeneration rewrites its scope and deletes its orphans, so\nnever hand-edit inside it — step definitions and hand-written features belong\noutside it, and loam never touches a byte outside \\`loam/\\`.\n\n- \\`loam gherkin FEAT-101\\` — the feature's ADDED/MODIFIED requirements for this\n service, both spec axes. Files carry \\`@FEAT-101\\`; arch-axis files add\n \\`@architecture\\` and are named \\`arch--<slug>.feature\\` — their scenarios are\n integration/ops tests, not acceptance tests.\n- \\`loam gherkin --service <id>\\` (no feature) — the full living suite from\n spec.md + arch.spec.md: the regression skeleton a legacy service gets at\n adoption.\n\nOne file per requirement (\\`Feature:\\` is the requirement name). Bullet lines\nopening with Given/When/Then/And/But — the \\`- **WHEN** ...\\` convention\nincluded — become steps; every other body line is kept as scenario\ndescription. Each scenario is tagged \\`@loam-digest-<16hex>\\`: the same body\nhash \\`loam verify\\` folds into its claim ids, riding into cucumber's JSON\nreport as a tag, so the suite, the claim and the report cannot quietly\ndisagree about what a scenario says. \\`loam validate\n--service <id>\\`, run in the service repo, grades the suite by those digests\n(\\`gherkin.missing\\` / \\`gherkin.stale\\` / \\`gherkin.orphaned\\`, all warn) — the\nfix is always regeneration, never editing a generated file.\n\nThe flow, closed end to end: \\`loam gherkin FEAT-101\\` → write step definitions\n(outside \\`loam/\\`) → run the suite with a JSON report\n(\\`cucumber-js --format json:report.json\\`) → implement until green →\n\\`loam verify FEAT-101 --service <id> --results report.json [--record rest.json]\\`,\nrun in that same service repo. The digest\ntags ride through the runner into the report, so the \\`scenario.tested\\` claims\nare answered by the green run itself — mechanically, never by anybody's word.\nSee \"The done-check\" for what \\`--record\\` still covers.\n\n## Frontmatter — who vouched for this, and from what\n\nEvery markdown artifact opens with a YAML block:\n\n\\`\\`\\`yaml\n---\nservice: payment-service # or feature: FEAT-101\nstatus: verified # services: draft -> verified\n # features: proposed -> in_progress -> built -> done\nowner: payments-team\nlast_verified: 2026-07-31 # written by \\`loam vouch\\` — do not hand-edit\nsources: # files/directories in the SERVICE'S repo this was written from — no globs\n - src/main/java/com/shop/payment/\nsources_digest: 6f1c0a… # written by \\`loam vouch\\` — do not hand-edit\ncontent_digest: 9b2f41… # written by \\`loam vouch\\` — do not hand-edit\nsources_files: | # written by \\`loam vouch\\` — do not hand-edit\n 9b2f41… src/main/java/com/shop/payment/Api.java\n---\n\\`\\`\\`\n\n\\`sources\\` matters more than it looks. Everything else loam checks is internal\nconsistency — and a corpus can agree with itself perfectly while describing nothing\nthat exists. \\`sources\\` is the one thing tying a document to code: \\`loam validate\\`,\nrun inside that service's repository, checks every listed path is still there.\nEntries are literal files and directories only — a directory covers everything\nbeneath it. Glob patterns are refused loudly (\\`loam vouch\\` will not stamp them,\n\\`loam validate\\` errors on them): a pattern dialect nobody can be sure of digests\na different file set than intended, corrupting the staleness signal silently.\n\n\\`sources_digest\\` is what makes that tie say something over time: a hash of the\nCONTENT of those files, taken when a human last vouched for the document. Every\nlater \\`loam validate\\` re-computes it, so it can tell a document nobody has checked\n(\\`sources.unvouched\\`) from one that still matches the code (\\`sources.current\\`) from\none the code has moved out from under (\\`sources.stale\\`).\n\n\\`content_digest\\` is the same promise about the document itself: a hash of the\nbody below the frontmatter, stamped by the same vouch. It closes the other half of\nthe forgery — editing a spec after it was vouched used to leave \\`status: verified\\`\nstanding over words nobody read. \\`loam validate\\` recomputes it wherever it can\nread the doc (no service repo needed, so it fires from the docs repo too) and\nreports \\`content.stale\\` (warn): only a person can say whether verified still\nholds of the new words.\n\nWrite \\`sources\\` for anything you author from reading code, and leave \\`status: draft\\`\nuntil a human has read it. Promoting draft to verified is their call, not yours, and\nit has its own command — \\`loam vouch --service <id>\\`, run inside that service's repo,\nwhich stamps the status, the date, both digests and the per-file index\n(\\`sources_files\\`, which is what lets a later \\`sources.stale\\` name the paths that\nmoved) together — into the living spec.md\nand, when the service has one, arch.spec.md, in one all-or-nothing run: a file whose\nsources cannot be verified refuses the whole vouch. Never write those five fields by\nhand: a status with no digest behind it is a claim with nothing behind it. If the\ndocument changed under vouch between the read and the write, it refuses with\n\\`vouch-raced\\` and stamps nothing — re-read and re-run.\n\n## The ID spine\n\nThree artifacts describe the same call in three languages. They are joined by the\n**operationId**, and that join is what \\`loam validate\\` checks:\n\n- architecture — a C4 edge names the operation it calls:\n \\`checkoutWeb -> paymentService 'Calls authorizePayment' { metadata { op 'authorizePayment' } }\\`\n- behaviour — a requirement declares the operations it governs:\n \\`Operations: authorizePayment\\`\n- contract — the provider's \\`openapi.yaml\\` defines it:\n \\`operationId: authorizePayment\\`\n\nSpell it identically in all three places. A mismatch is an error, not a warning:\nan edge calling an operation nobody defines is a broken contract between services.\n\nFeatures are joined the same way: a delta's new elements and edges carry the\nfeature id as a LikeC4 tag (\\`#FEAT-101\\`), and that tag is exactly what\n\\`loam archive\\` folds into the living landscape. Untagged elements in a delta are\ncontext for the diagram, not changes.\n\n## The requirement baseline — \\`Based-On:\\`\n\nA MODIFIED requirement carries its FULL new text, not a diff, so archive does not\nmerge your wording into the living one — it REPLACES it. When two features rewrite\nthe same requirement, the loser loses everything they wrote, scenarios and all.\n\n\\`loam validate\\` names the other feature while both are in flight\n(\\`delta.modified-conflict\\`), but that warning cannot see the case that actually\nhappens: the first feature archives, stops being an active feature, the second\nrevalidates GREEN, and its archive lands text written against a document that no\nlonger exists — \\`+0 ~1 -0\\`, exit 0, nobody told.\n\nSo a MODIFIED or REMOVED requirement pins the living version it was written\nagainst, directly under its identity:\n\n\\`\\`\\`markdown\n### Requirement: Cancel an order\nRequirement-ID: REQ-042\nBased-On: 3bcc7a40f2b23ff3\n\\`\\`\\`\n\nYou do not compute that value — \\`loam rebase FEAT-101\\` writes it, for every\nMODIFIED/REMOVED requirement in the feature, from the living text as it stands\nright now. Run it when you finish authoring a delta, and again after you resolve\na collision. Archive refuses on a stale pin (\\`delta.baseline-stale\\`) and says\nboth digests.\n\n**Restamping is not resolving.** A pin claims \"I read this version\". If\n\\`loam rebase\\` reports a requirement as moved, re-read the living requirement and\nfold in what you still mean BEFORE you ship — otherwise you have re-pinned to a\ndocument you never read, and archive will merge your text over theirs with\nloam's blessing. There is no automatic three-way merge and there will not be one:\nrequirement prose does not merge, people do.\n\nA delta adopted from OpenSpec has no such line and gets a warning\n(\\`delta.baseline-missing\\`), never a refusal — an entire migrated corpus that\ncannot archive is not a safety property.\n\n### The same pin on the contract axis — and why it matters more there\n\nA feature's \\`openapi.yaml\\` is a COMPLETE document, not a patch: you restate the\nliving contract around the slot you are changing, and the merge upserts every\noperation the document spells. So the collision on this axis needs no overlap at\nall. Two features touching one service, editing DIFFERENT operations, destroy\neach other — the second to archive quietly pushes its authoring-time copy of the\noperation it never meant to touch back over whatever landed in between.\n\nThe same marker fixes it, as a vendor extension beside \\`x-loam-remove\\`:\n\n\\`\\`\\`yaml\npaths:\n /orders/{id}/cancel:\n post:\n operationId: cancelOrder\n x-loam-based-on: 86feabe621ed887d\n\\`\\`\\`\n\n\\`loam rebase\\` writes one on EVERY operation in the delta, pinned to the living\nversion. That is what lets the merge tell a QUOTE from an EDIT:\n\n- pin equals the operation's own content → you quoted it → **the merge skips it\n entirely** and the living contract keeps whatever it holds. Archive prints\n \\`· quotes …\\` so the plan never silently writes less than your delta spells.\n- pin equals the living operation → you edited it, nobody else did → merged.\n- pin matches neither → you edited it AND somebody landed a change to it →\n \\`openapi.baseline-stale\\`, refused.\n\nNot covered, deliberately, and worth knowing: path-level keys (\\`parameters\\`,\n\\`servers\\`) and \\`components/schemas\\` are still upserted wholesale, so a restated\nschema can still revert another feature's change to it —\n\\`openapi.path-item-modified\\` and \\`openapi.component-modified\\` are your only\nwarning there. Removal markers carry no pin either; \\`openapi.remove-target-mismatch\\`\nis what guards those.\n\n## Which element IS which service\n\nAn element says which service it is with \\`metadata { service 'payment-service' }\\`.\nWithout one, its **title** is used instead — which is what most of this repo relies\non, and also the trap: rename a box in a diagram and every check joining it to\n\\`services/<svc>/\\` silently stops matching. Bind an element whenever its title is\nnot exactly the directory name, and prefer binding over renaming a directory.\n\n\\`services/\\` is the list of services — there is no manifest, and none should be\nadded. \\`loam validate --all\\` compares that list to the landscape both ways: a\ndirectory nothing draws is an error, and an element with no directory is a warning.\nSystems that are not ours — kafka, a payment gateway — carry \\`#external\\`, which\nsays so once and stops the warning for good.\n\n## The cycle\n\n0. **Wire the repo** — \\`loam init --docs <path-to-docs-repo> --service <id>\\` in\n each service repo, then \\`loam doctor\\`. Once per repository, before anything\n else; see \"\\`loam.json\\` — the wiring\" above. Every step below runs in a repo\n that has been through this one.\n1. **Adopt** — a service with no documentation at all starts at\n \\`loam adopt --service <id> --json\\`, which briefs you on the baseline to write.\n See \"Adopting a service\" below. Once per service, not once per feature.\n2. **Understand** — \\`loam status --json\\` first if you joined halfway or lost the\n session: it says what is in flight and what the next step is, for the fleet or\n for one feature (\\`loam status FEAT-101 --json\\`). Then \\`loam list --json\\` and\n \\`loam show <service> --json\\`.\n Never propose a change to a service you have not read.\n3. **Scaffold** — \\`loam new FEAT-101 --title \"...\" --touches <existing> --new-service <new>\\`.\n The scaffold leaves a \\`delta.likec4\\` whose context elements are commented out —\n a requirements-only feature should DELETE that file rather than ship an empty\n one — and gives every \\`--new-service\\` a \\`specs/<svc>/arch.spec.md\\` alongside.\n4. **Author** the four files the scaffold left as TODO: intent, delta.likec4,\n a spec.md per service, an openapi.yaml per new service. Finish with\n \\`loam rebase FEAT-101\\`: it pins every MODIFIED/REMOVED requirement and every\n operation in the contract delta to the living version you wrote it against.\n On the contract axis run it even when you changed nothing but one endpoint —\n that is exactly what marks the REST of the document as quotation the merge\n must not write. See \"The requirement baseline\".\n5. **Check** — \\`loam validate FEAT-101 --json\\`. Fix every error before writing code.\n \\`loam dependencies --json\\` says whether another feature in flight has to\n archive first.\n6. **Build** — \\`loam delta FEAT-101 --service <svc> --json\\` is the task for one service:\n intent, its requirement delta with scenarios verbatim, and the edges around it.\n In the service's own repo, \\`loam gherkin FEAT-101\\` then emits those scenarios as\n \\`.feature\\` files under \\`<gherkinDir>/loam/\\` — write step definitions against them\n **first**, outside \\`loam/\\`, and implement until they pass.\n If \\`architecture.errors\\` is non-empty the payload keeps \\`ok: true\\` (the command\n ran) but the exit code is 1: the C4 slice is empty because delta.likec4 did not\n parse, not because the feature changes no architecture — do not build on it.\n7. **Verify** — \\`loam verify FEAT-101 --json\\` turns the feature's own promises into a\n checklist. The scenario claims are answered by the cucumber report\n (\\`--results report.json\\`); answer the rest with evidence and record them\n (\\`--record answers.json --service <id>\\`), **from each affected service's own\n repo**. See \"The done-check\".\n8. **Ship** — \\`loam archive FEAT-101\\` once the code is merged. \\`--dry-run\\` shows\n every file the merge would write, and writes none of them.\n\n## Adopting a service\n\n\\`loam adopt --service <id>\\` does not read code and never will. Nothing\ndeterministic can look at a legacy service and say what its architecture MEANS,\nand a guessed model is worse than none: everybody downstream has to re-derive it\nto know whether to believe it. So loam takes the half of the job that is\nmechanical — stating the work, and checking the result — and you do the reading.\n\nThe brief names every file to write, the grammar each must be in, what the\nlandscape already says about the service (bind to those elements, do not draw a\nparallel fleet), the frontmatter, and the checks that follow. It also lists what\n**no** check will ever catch. Read that list: everything on it is a way to be\nwrong quietly, and \\`loam validate\\` passing means nothing about any of it.\n\nOne of the targets is not under \\`services/<id>/\\`: \\`architecture/landscape.likec4\\`,\nthe fleet map, carried with \\`action: \"edit\"\\` and a \\`landscape.instruction\\`\nspelling out the block this service owes it. That target appears only while\nnothing in the map resolves to the service. **Add to that file; never rewrite\nit** — every other service's elements and edges are in there. A service that is\nfully documented and undrawn is \\`landscape.service-unmodelled\\` (error) and is\ninvisible to every cross-service check: no edge into it can be graded against\nits openapi.yaml, and no feature can draw a call to it.\n\nAn adoption ends with two runs, not one: \\`loam validate --service <id> --json\\`\nfor the baseline, then \\`loam validate --all --json\\` in the docs repo — the\nfleet cross-check the first invocation does not perform is exactly where a\nlandscape edit that did not land shows up.\n\nTwo rules the brief repeats and this file will too. An artifact that already\nexists is reported as \\`action: \"diff\"\\` — read it and report what disagrees;\nnever replace it, because a document somebody wrote is evidence. And everything\nyou write is \\`status: draft\\`: promotion is \\`loam vouch\\`, run by a person in\nthe service's own repository.\n\n## The done-check\n\n\\`loam verify <FEAT>\\` derives a checklist from the feature's own artifacts —\none claim per new service, per operation the delta adds, per tagged edge that\nnames an operation, and per scenario of every changed requirement, arch.spec.md\ndeltas included (those claims name their file, and the test they ask for is an\nintegration/ops test, not an acceptance test). Each claim has a stable id, so\ntwo runs are diffable and an answer cannot drift onto a different question.\n\nTwo answer channels, and they never overlap. The \\`scenario.tested\\` claims are\nthe TEST RUNNER's to answer: run the generated suite with a cucumber JSON\nreport (\\`cucumber-js --format json:report.json\\`) and pass it back —\n\\`loam verify <FEAT> --results report.json\\`. A claim is confirmed only when a\nreport scenario carrying its \\`@loam-digest-<16hex>\\` tag ran at least one step\nand every step passed — every occurrence, when the report holds a re-run, and\nevery before/after hook with it. A failed, undefined, pending or skipped step,\na failed hook, or no matching scenario at all, is \\`unconfirmed\\` with the\nreason. The digest is the only identity: a reworded spec scenario matches\nnothing until the suite is regenerated and re-run. When a report exists,\nALWAYS pass \\`--results\\` — under it the runner owns every scenario claim, and\nan answers-file entry for one is refused.\n\n\\`--record\\` without \\`--results\\` is the fallback for a service with no runnable\nsuite yet: there your word answers the scenario claims too. It still works, and\nit costs something you must know about. The record says who answered\n(\\`answered_by: agent\\`), and that answer changes the VERDICT: a feature whose\nscenario claims rest on an agent's word is **attested**, not verified.\n\\`verify --json\\` reports \\`verdict: \"attested\"\\` with \\`verified: false\\` and an\n\\`attested\\` count, \\`loam status\\` marks it and adds a \\`next.verify-attested\\` step,\nand the notice rides in \\`notices[]\\` under \\`verify.scenario-attested\\` (warn —\nnothing gates on it, because a legacy service with no suite must still be able\nto ship). Answer them mechanically the moment a suite runs:\n\\`loam verify <FEAT> --results <report.json>\\`.\n\nTwo more codes belong to the record itself. \\`verify.record-miscounted\\` (error):\na \\`verification.yaml\\` whose \\`summary\\` block contradicts its own \\`claims[]\\` — the\nrecord is refused as unreadable rather than believed in either half, and\nre-recording is the repair, never editing the counts. \\`verify.digest-contested\\`\n(warn): two or more services word a scenario identically, so they share one\n\\`@loam-digest-…\\` tag and no single cucumber report can say whose suite ran it —\nthose scenario claims are left unconfirmed, and each service records its own\nfrom its own repository with \\`--service\\`.\n\nA consumed report is written down: \\`report\\` on the record (or on that\nrepository's attestation) carries the path, a sha256 of the bytes, the file's\nmtime and how many tagged scenarios it held. That identifies WHICH file\nanswered the claims, and — when the file is committed — that it matches the\nattested commit. It does not prove the file came from executing that commit; no\ndigest can.\n\nEverything else you answer from the code, and \\`--record answers.json\\` takes\nthose back, writing \\`features/<FEAT>/verification.yaml\\` — it travels into the\narchive with the feature and reads without loam. With \\`--results\\`, the answers\nfile must answer exactly the non-scenario claims: an entry for a scenario\nclaim is refused (\\`answers-mismatch\\` — the runner owns it), and \\`--results\\`\nalone is refused while non-scenario claims are outstanding. Every recorded\nverdict says who answered it (\\`answered_by: runner | agent\\`), so a reviewer\ncan tell a green run from somebody's word.\n\n**A cross-service feature is verified once per service, and the form matters.**\nThe claims of one feature belong to several services, and each service's code\nlives in its own repository — so the recording form is federated:\n\n\\`\\`\\`sh\n# in EACH affected service's own repo, not in the docs repo:\nloam verify FEAT-101 --service payment-service --results report.json --record answers.json\n\\`\\`\\`\n\n\\`--service <id>\\` narrows the checklist to that service's claims and binds the\nanswers to this repository's commit, producing one **attestation** per service\ninside a single \\`schema: 2\\` \\`verification.yaml\\`. Re-running it in the next\nrepo adds that repo's attestation and leaves the others standing. Omitting\n\\`--service\\` in a repo whose \\`loam.json\\` declares one substitutes it, so the\nshort form is safe there; a \\`--service\\` naming a service this repo does not\ndeclare is refused (\\`service-mismatch\\`), and \\`--service\\` from a repo that\ndeclares none is refused (\\`repository-unavailable\\`) — an attestation has to be\nbound to the code it is about.\n\n⚠ **The legacy all-at-once form \\`loam verify <FEAT> --record answers.json\\` with\nno \\`--service\\` writes the whole record from one place.** Over a record that\nalready carries other services' attestations it is refused outright\n(\\`record-federated\\`, naming the attestors) rather than merged or migrated — the\nalternative was one repo silently erasing nine others' evidence. Use it only for\na single-service feature that has never been recorded federated. A\n\\`verification.yaml\\` that exists but cannot be read is \\`record-unreadable\\`: it is\nnever overwritten and never reported as \"no record\".\n\nReading is not writing: \\`loam verify <FEAT> --service <id>\\` **without**\n\\`--record\\` is a pure lens on the checklist and works from anywhere, docs repo\nincluded. Every run lists all subjects in \\`services\\`, and a recorded run reports\nin \\`discarded\\` every previous answer the new record does not carry.\n\nIt refuses an answer set that does not answer the current checklist: an id that\nis not on it (\\`answers-mismatch\\`), a claim with no answer (same), or a\n\\`confirmed\\` with no evidence (\\`answers-unevidenced\\`). Evidence is \\`file:line\\`.\nA claim you cannot show is \\`unconfirmed\\` with a note saying why — that is a\nsuccessful record, and it is more useful to the next reader than a yes.\n\n**Nothing gates on it.** \\`loam archive\\` will ship an unverified feature. That\nis deliberate: coherence is something loam computed from the documents, and a\nverdict is your word about code loam never read. A gate in front of shipping\nwould only teach everyone that the cheapest way past it is to say yes. The\nrecord is for the reviewer who comes later, so leave it true.\n\n## What you author, what loam derives\n\nAuthored by hand: \\`intent.md\\`, \\`delta.likec4\\`, \\`specs/<svc>/spec.md\\`,\n\\`specs/<svc>/arch.spec.md\\`, \\`specs/<svc>/openapi.yaml\\`, ADRs, runbooks, health.\n\nWritten by loam: the merge into \\`services/<svc>/spec.md\\`, \\`services/<svc>/arch.spec.md\\`,\n\\`services/<svc>/openapi.yaml\\` and \\`architecture/landscape.likec4\\` at archive time.\n\n**Do not hand-edit the living landscape to add a feature's changes.** Put them in a\ndelta and archive it — otherwise the change has no intent, no requirement and no\nhistory behind it.\n\n## Rules the validator enforces\n\n- every requirement has at least one \\`#### Scenario:\\` — scenarios are the acceptance\n criteria and the source for tests;\n- every operationId a requirement governs exists in that service's OpenAPI;\n- every C4 edge's \\`op\\` resolves to an operation the target actually exposes;\n- every operation is governed by some requirement (warning);\n- a \"Calls\" edge with no \\`metadata { op }\\` is unlinked (warning);\n- **the map is complete**: every \\`services/<svc>/\\` has an element in the landscape,\n and every element that looks like a service has a directory (warning) or is\n tagged \\`#external\\`;\n- **the diff applies**: a \\`MODIFIED\\` or \\`REMOVED\\` requirement exists in the living\n spec, an \\`ADDED\\` one does not, and a section heading matches the grammar exactly;\n- a \\`Requirement-ID: <id>\\` is optional, but when present it is valid and unique;\n archive selects by it, so carry the same ID through \\`MODIFIED\\` to rename a heading;\n- **every requirement in a delta is under a delta section**: one under a prose heading\n (\\`## Behavior\\`) is documentation as far as the merge is concerned and is dropped\n (warning). \\`## Requirements\\` is exempt — quoting the living state is legal.\n\nThat last pair of rules exists because each of its failures used to be silent.\n\\`MODIFIED\\` of a requirement that does not exist merged as a creation; \\`ADDED\\` of one\nthat does exist REPLACED it, scenarios and all; a near-miss heading like\n\\`## ADDED Requirement\\` (singular) parses as plain prose, so archive merged nothing and\nsaid nothing; and a requirement under \\`## Behavior\\` vanished the same way.\n\nWrite the delta section heading on its own line exactly as \\`## ADDED Requirements\\`.\nThe heading is the only thing that gives a requirement its kind: without a matching\none, archive merges the requirement as nothing.\n\nSeverity and gating are two different questions. An ERROR means the document is\ninvalid — \\`loam validate\\` fails. A finding with \\`gates: true\\` means \\`loam archive\\`\nwill refuse — the merge is unsafe. They usually agree (errors gate, warnings do\nnot), and where they diverge the \\`--json\\` output says so: \\`delta.requirement-not-merged\\`\nis a warning (the shape is legal OpenSpec) that gates (the merge would drop the\nrequirement). Leave a warning standing only deliberately.\n\n## Reading loam's output\n\nEvery command takes \\`--json\\`, and the envelope holds even when the INVOCATION\nis wrong: with \\`--json\\` anywhere in the arguments, an unknown flag, unknown\ncommand or missing argument still yields \\`{ ok: false, error: { code:\n\"invalid-option\" } }\\` on stdout with exit 1 (commander's own diagnostic goes\nto stderr; \\`--help\\` and \\`--version\\` output pass through untouched). Without\n\\`--json\\` the same refusal is plain text on stderr with nothing on stdout.\n\nBranch on \\`findings[].code\\`, never on the prose — the wording changes, the codes do\nnot. The code-by-code fix table lives in the \\`/loam-check\\` command \\`loam init\\` lays\ndown. \\`init\\` writes that body twice for every tool it configures: once as a slash\ncommand in the tool's own command directory (you type it), and once as an Agent\nSkill at \\`<tool-dir>/skills/loam-check/SKILL.md\\` (the model loads it by itself when\nthe task matches the skill's description). Same body, two ways in.\n\nWhich tools get them is detected from the repo: \\`loam init\\` scans for the\ndot-directories of the tools it knows (\\`.claude\\`, \\`.cursor\\`, \\`.gemini\\`, …) and\nwrites for the ones it finds, falling back to Claude Code when it finds none.\n\\`loam init --tools <ids|all>\\` overrides the scan — an unknown id is refused\n(\\`invalid-option\\`) rather than skipped — and \\`--no-commands\\` / \\`--no-skills\\`\neach suppress one delivery. The tools written for are recorded in\n\\`loam.json\\` as \\`agentTools\\`, which is how \\`loam doctor\\` tells a file missing\nbecause the binary grew a new command from one missing because nobody ever\nselected that tool. The map of which invocation surfaces what:\n\n- \\`loam status [<FEAT>]\\` is the orientation surface — the question you have when\n you join a repository halfway, or come back having lost the session, and the one\n every other command assumed you could already answer. It writes nothing and\n stores nothing: there is no state file, every answer is re-derived from the\n files, so a document someone edited in another window is visible on the next run\n with nothing to invalidate. Artifacts come back as \\`missing\\` (owed, nothing in\n the way — write it now), \\`blocked\\` (not written and not writable yet; the entry\n names what comes first), \\`draft\\` (on disk, and the shared checks report an error\n against it — what exists is wrong), \\`ready\\` (on disk and clean, but something\n outside the documents — code, a test run, a recording — still has to answer it)\n or \\`done\\`. The payload's reason to exist is \\`next[]\\`: ordered, first entry\n first, each carrying a code and the literal command to run. \\`next.recover-commit\\`\n outranks every other step in both forms and is never elided: a \\`.loam-commit\\`\n journal says an archive or unarchive was killed mid-commit, so some of the files\n everything below is derived from may be half-written. Its command is that exact\n archive/unarchive re-run, which repairs from the pre-image first, under the lock\n — except when the journal itself cannot be read, where it is \\`loam doctor\\` and\n the repair is a human's comparison against version control. Fleet-wide the rest are\n \\`next.adopt\\` (a service with no spec.md — nothing about it is written down, so\n no feature can be graded against it), \\`next.complete-service\\` (a living spec.md\n with no model.likec4 beside it), \\`next.feature\\` (something is in flight;\n ask \\`loam status <FEAT>\\` about it), \\`next.archive\\` (authored and verified —\n ship it, and everything waiting on it is released), \\`next.fleet-clean\\`\n (nothing is owed), \\`next.elided\\` (the fleet list hit its cap and says how many\n steps of the same kinds it left out — it is ordered most-unblocking first, so\n work down it and re-run) and \\`next.fleet-gate\\` (always last while anything is\n outstanding: \\`loam validate --all\\` is what CI runs, and the fleet form grades\n nothing itself). On one feature: \\`next.author-intent\\`, \\`next.touch-service\\`\n (no per-service delta at all yet), \\`next.author-spec\\`, \\`next.author-openapi\\`,\n \\`next.author-scenarios\\`, \\`next.rebase\\` (requirements or operations with no\n baseline pin — until they have one the merge cannot tell what it EDITS from what\n it merely quotes), \\`next.archive-first\\` (another feature in flight has to land\n before this one), \\`next.fix-coherence\\` (the three axes disagree and archive\n refuses), \\`next.generate-tests\\` (a per-service delta carries scenarios no test\n run has answered — \\`loam gherkin <FEAT> --service <svc>\\` in that service's own\n repository), \\`next.verify\\` (the done-check has not been started — or its record\n will not read, or it answers a checklist the feature has since moved out from\n under, which is not the same as a finished one), \\`next.verify-unconfirmed\\`\n (started, and claims are still open — close them from each affected service's\n own repository), \\`next.attest-service\\` (this repo IS one of the services that\n owes an answer, so the step is bound to this commit), \\`next.verify-attested\\`\n (every claim is confirmed but a scenario rests on an agent's word rather than a\n green run — see \"The done-check\"), \\`next.archive\\`, and \\`next.archived\\` for one\n that shipped.\n It grades nothing of its own: the verdict is the one \\`loam validate --feature\\`\n and \\`loam archive\\` compute — status takes the UNION of what both of them refuse,\n so it may be more pessimistic than either and can never be greener than both.\n \\`verification\\` carries \\`verdict\\` (\\`verified\\` | \\`attested\\` | \\`unverified\\`)\n and \\`attested\\` (how many scenario claims rest on an agent's word) beside the\n recounted totals, and \\`checks.issues[]\\` carries \\`gates\\` and \\`details\\` on every\n finding.\n- \\`loam validate --service <id>\\` grades one service's own axes: \\`service.unknown\\`,\n \\`service.no-model\\`, \\`service.no-spec\\`, \\`service.no-openapi\\`, \\`c4.invalid\\`,\n \\`requirements.missing-scenarios\\`, \\`requirements.stepless-scenario\\`,\n \\`spec.merge-conflict\\`, \\`spec.duplicate-requirement\\`,\n \\`spec.no-requirements\\`, \\`spec.repeated-operations\\`, \\`spec.repeated-covers\\`,\n \\`openapi.invalid\\`, \\`openapi.duplicate-operationid\\` (one operationId in two\n (path, method) slots — every join on the id then picks one of them arbitrarily),\n \\`api.ungoverned\\`, \\`api.ops-unlinked\\`,\n \\`api.requirement-deprecated\\`, \\`spec-api.op-undefined\\` (the living spec's own\n \\`Operations:\\` lines, not only a delta's), \\`spine.landscape-invalid\\`, \\`spine.op-undefined\\`,\n \\`spine.op-link-missing\\`, \\`spine.op-deprecated\\`, and\n the architecture spec axis: \\`covers.unknown\\`, \\`health.invalid\\`, \\`health.uncovered\\`. Run inside\n the service's own repo, once a generated suite exists under\n \\`<gherkinDir>/loam/\\`, it also grades that suite against the living specs:\n \\`gherkin.missing\\`, \\`gherkin.stale\\`, \\`gherkin.orphaned\\` (all warn — the fix is\n always regeneration, never editing a generated file). A service that never ran\n \\`loam gherkin\\` stays quiet, and a file tagged with a feature still in flight\n answers to that feature, not to the living spec it has not merged into yet.\n- \\`loam validate --feature <id>\\` grades a change's three axes against each other and\n against the fleet in flight: \\`delta.invalid\\`, \\`delta.nothing-tagged\\`,\n \\`delta.service-unknown\\`,\n \\`spec-api.op-undefined\\`, \\`spec-api.op-pending\\`, \\`c4-api.op-undefined\\`,\n \\`c4-api.op-pending\\`, \\`c4-api.op-deprecated\\`, \\`c4.op-ungoverned\\`, \\`c4.op-link-missing\\`,\n \\`api.op-unconsumed\\`, \\`service.no-requirement-delta\\`, \\`archedge.uncovered\\`,\n \\`spec.repeated-operations\\` / \\`spec.repeated-covers\\` (on the feature's own\n spec deltas — same silent-line-loss check as service scope),\n \\`spec.merge-conflict\\` / \\`requirements.stepless-scenario\\` (same codes as\n service scope, graded on the feature's spec.md and arch.spec.md deltas —\n both breaches merge into the living document),\n the architecture spec axis (\\`c4.uncovered\\`, plus \\`covers.unknown\\` on the\n feature's arch.spec.md deltas), and\n the delta-shape group: \\`delta.unknown-section\\`, \\`delta.no-delta-sections\\`,\n \\`delta.requirement-not-merged\\`, \\`delta.modified-unknown\\`, \\`delta.removed-unknown\\`,\n \\`delta.added-duplicate\\`, \\`delta.added-near-duplicate\\`, \\`delta.modified-pending\\`,\n \\`delta.removed-pending\\`, \\`delta.added-conflict\\`, \\`delta.modified-conflict\\`,\n \\`delta.living-duplicate-requirement\\`, and the API-removal group:\n \\`openapi.remove-marker-anonymous\\`, \\`openapi.remove-op-consumed\\`.\n Where a finding names another feature in flight (\\`delta.*-pending\\`,\n \\`delta.added-conflict\\`, \\`delta.modified-conflict\\`, \\`spec-api.op-pending\\`,\n \\`c4-api.op-pending\\`) the ORDER is the answer, and \\`loam dependencies --json\\`\n is what computes it: \\`order\\` is a dependency-first sequence, \\`conflicts\\`\n the pairs no ordering fixes, \\`cycles\\` the ones that need a human.\n- \\`loam validate --all\\` runs both of those for everything and adds the fleet\n cross-check: \\`landscape.missing\\`, \\`landscape.invalid\\`,\n \\`landscape.merge-conflict\\`, \\`landscape.service-unmodelled\\`,\n \\`landscape.service-undocumented\\`, \\`landscape.binding-unknown\\`,\n \\`landscape.binding-duplicate\\`, \\`service.id-invalid\\` (a \\`services/<id>/\\`\n directory no loam command can address — fleet scope only, and graded before\n the map is opened, so it stands even when the landscape is missing or\n unreadable), plus a per-service\n \\`sources.unverifiable-from-here\\` (severity \\`ok\\`, one per service whose\n \\`sources\\` only its own repo can check — it is a confirmation, not work, and\n the fleet rollup line is derived from those findings), and one check on this very file — \\`agents.stale\\`\n (warn) — when the version stamp on line 1 (\\`<!-- generated by loam vX.Y.Z -->\\`)\n is missing or older than the running binary: the tables here may describe a\n loam that no longer exists, so review this file against the current \\`--help\\`\n and update the stamp line. A hand-curated file silences it the same way, by\n keeping the stamp current. The file is never refreshed automatically — your\n edits outrank the template, so detection is all loam does.\n- \\`loam doctor\\` is read-only local/fleet preflight — the first thing to run in a\n repo that behaves as though the fleet were empty, and every finding carries a\n \\`fix\\`. Its blockers are\n \\`doctor.config-missing\\`, \\`doctor.config-invalid\\`, \\`doctor.docs-missing\\`,\n \\`doctor.services-missing\\`, and the three that say the fleet map cannot be read\n at all — \\`doctor.landscape-merge-conflict\\` (it still holds \\`<<<<<<<\\` markers,\n so it is two halves of two different maps), \\`doctor.landscape-invalid\\` (it does\n not parse) and \\`doctor.landscape-unreadable\\` (it is there but could not be\n read). It also grades what a WRITE that did not finish left in the docs repo,\n reported as \\`writePath\\` beside the findings: \\`doctor.docs-locked\\` (a held\n \\`.loam-lock\\` — a warning while its holder is alive, since the answer is to wait\n and re-run, and a BLOCKER once the holder is a process that no longer exists on\n this host, since nothing will ever release it and every archive and unarchive\n refuses \\`docs-busy\\` until it is deleted), \\`doctor.commit-interrupted\\` (blocker:\n a \\`.loam-commit\\` naming an archive or unarchive that was killed mid-commit, so\n the living docs may be half-written — re-run that exact command and it recovers\n first, under the lock), \\`doctor.commit-unreadable\\` (blocker, and the worst case:\n the record of which files that commit had already written cannot be parsed, so\n nothing can grade it — compare against version control by hand) and\n \\`doctor.staging-temps\\` (warn: orphaned \\`.loam-*.tmp\\` scratch that was never\n linked into place — litter, not damage). Accessibility, portability and\n incomplete binding stay warnings:\n \\`doctor.docs-unreadable\\`, \\`doctor.docs-readonly\\`, \\`doctor.docs-absolute\\`\n (\\`docsDir\\` stored as an absolute path in a committed loam.json — it resolves\n only on the machine that ran \\`loam init\\`), \\`doctor.inventory-unreadable\\`,\n \\`doctor.landscape-missing\\`, \\`doctor.service-unbound\\`, \\`doctor.service-unknown\\`,\n and the two about this repo's own generated command and skill files:\n \\`doctor.agent-files-missing\\` — some of them are absent, because the repo was\n initialized by an older binary or they were deleted — and\n \\`doctor.agent-files-stale\\` — some that ARE here carry no\n \\`<!-- generated by loam vX.Y.Z -->\\` stamp, or one older than the running\n binary, so the protocol and code tables they instruct an agent with may\n describe a loam that no longer exists. EXPECT the second one on the first\n \\`loam doctor\\` after an upgrade, on a repo nobody has touched: stamping is\n newer than the files, so nothing written by an earlier loam carries a stamp\n at all, and every one of them reads as unstamped. That is the intended\n reading — an unstamped file is one nobody has confirmed still describes this\n binary — but it means the finding is not evidence that anything was edited or\n broken.\n For the first, run the command the finding's own \\`fix\\` field spells: it names\n this repo's \\`docsDir\\`, its service binding and its tool ids explicitly, and\n \\`loam init\\` writes only the command and skill files that are absent and never\n touches one that already exists. (It does rewrite \\`loam.json\\` — but a re-run\n with no \\`--docs\\` keeps the pointer the file already commits, so it cannot move\n the fleet under you. Reach for \\`--create\\` only to make a docs repo that does\n not exist yet; beside a working \\`docsDir\\` it scaffolds a second, empty source\n of truth and \\`validate --all\\` then goes green over an empty fleet.)\n For the second there is no command: the repair is by hand, one file at a\n time. Read each file the finding names against the body this loam writes,\n then set (or add) its stamp line to \\`<!-- generated by loam vX.Y.Z -->\\` for\n the running version — or, if you have no local edits worth keeping, delete\n that file and re-run \\`loam init\\`, which lays the current one down. Neither is\n ever regenerated in place: your edits outrank the template, so detection is\n all loam does, and the stamp is a human's claim that the file still means what\n it says rather than a refresh. Leaving either finding standing costs entry\n points and the accuracy of what the entry points say, nothing else.\n- \\`loam rebase <FEAT>\\` writes the baseline pins: \\`Based-On:\\` under every\n MODIFIED/REMOVED requirement in the feature's spec deltas, and\n \\`x-loam-based-on\\` on every operation in its openapi.yaml, from the living text\n as it stands right now. Run it when you finish authoring a delta and again\n after you resolve a collision — it is the one mechanism standing between two\n features in flight and silently reverting each other's landed work. It refuses\n rather than inventing a pin, and re-pinning is not resolving: see\n \"The requirement baseline\".\n- \\`loam dependencies [<FEAT>]\\` derives the ordering of the features in flight\n from the artifacts themselves — requirement identities and OpenAPI\n operationIds, never from validator prose. It is the answer to every\n \"another feature in flight does this first\" finding, and to\n \\`delta.added-conflict\\` / \\`delta.modified-conflict\\`, which no ordering fixes.\n- \\`loam audit-openspec <root>\\` is the read-only OpenSpec inventory; an explicit\n \\`--write-mapping\\` writes a non-overwriting, digest-bound decision skeleton\n outside the source tree. \\`loam migrate-openspec <root> --map <file>\\` validates\n that mapping and stays dry-run unless both \\`--apply\\` and an empty \\`--target\\`\n are supplied. Living/active shape diagnostics are \\`openspec.workspace-empty\\`\n (the audited root holds no living spec and no active change, so there is\n nothing to migrate and no verdict over it can be \\`ready\\`),\n \\`openspec.specs-missing\\`,\n \\`openspec.config-invalid\\`, \\`openspec.external-store-pointer\\`,\n \\`openspec.living-empty\\`, \\`openspec.living-requirements-outside-section\\`,\n \\`openspec.living-delta-section\\`, \\`openspec.nonstandard-living-spec\\` (markdown\n under \\`specs/\\` named neither spec.md nor design.md, so no capability reads\n it), \\`openspec.change-metadata-invalid\\`,\n \\`openspec.change-schema-unresolved\\`, \\`openspec.skip-specs-with-specs\\`,\n \\`openspec.change-no-specs\\`, \\`openspec.change-empty\\`,\n \\`openspec.change-without-delta-sections\\`,\n \\`openspec.change-requirements-outside-delta-sections\\`,\n \\`openspec.change-quoted-requirements\\` (requirements under a \\`## Requirements\\`\n heading INSIDE a change delta — the shape OpenSpec's own living-spec template\n mandates, and one that stages nothing, so loam refuses it rather than counting\n requirements it cannot route),\n \\`openspec.hidden-change-directory\\` (a dot-prefixed directory under\n \\`changes/\\` is not enumerated as a change, so nothing under it migrates —\n rename it or move it out; under \\`changes/archive/\\` the same code is an archive\n diagnostic and does not gate, because frozen history never blocks),\n \\`openspec.nonstandard-change-spec\\`, \\`openspec.renamed-malformed\\`,\n \\`openspec.requirement-id-invalid\\`, \\`openspec.requirement-id-repeated\\`,\n \\`openspec.requirement-id-duplicate\\` (a stable \\`Requirement-ID\\` that is\n malformed, declared twice, or shared by two requirements — identity is never\n inferred from an invalid declaration, so the RENAMED chain cannot be followed\n through it), \\`openspec.symlink-unsupported\\`, and \\`openspec.non-utf8-artifact\\`;\n the same shapes in frozen history are non-blocking archive diagnostics.\n Mapping refuses stale or incomplete decisions under \\`mapping.source-missing\\`,\n \\`mapping.source-root-mismatch\\`, \\`mapping.source-digest-mismatch\\`,\n \\`mapping.unknown-capability\\`, \\`mapping.unknown-requirement\\`,\n \\`mapping.requirement-allocation-missing\\`,\n \\`mapping.requirement-service-unknown\\`, \\`mapping.service-allocation-empty\\`,\n \\`mapping.unknown-change\\`, \\`mapping.change-title-missing\\`,\n \\`mapping.feature-id-invalid\\`, \\`mapping.feature-id-duplicate\\`,\n \\`mapping.unknown-rename\\`, \\`mapping.invalid-requirement-id\\`,\n \\`mapping.rename-source-missing\\`, \\`mapping.rename-source-ambiguous\\`,\n \\`mapping.rename-source-id-invalid\\`, \\`mapping.rename-target-conflict\\`,\n \\`mapping.rename-existing-id-conflict\\`, \\`mapping.rename-id-conflict\\`,\n \\`mapping.rename-double-source\\`, \\`mapping.rename-double-target\\`,\n \\`mapping.rename-chain\\`,\n \\`mapping.unknown-artifact\\`, and \\`mapping.invalid-artifact-disposition\\`.\n- Repository containment failures are explicit: \\`sources.path-outside\\` rejects a\n provenance source outside the service repo and \\`gherkin.path-outside\\` rejects a\n generated-suite target outside it. Federated verify recording refuses under\n \\`service-mismatch\\`, \\`unknown-service\\`, or \\`repository-unavailable\\` when the\n selected service, fleet identity, and repository commit cannot be bound safely,\n and under \\`record-federated\\` when the legacy all-at-once form would erase other\n repositories' attestations.\n- A file loam cannot read is reported, never skipped: \\`service.unreadable\\` and\n \\`feature.unreadable\\` (both errors) name the path and say that nothing about\n that target was checked while the rest of the fleet still was. The whole docs\n repo being unreachable is a refusal instead: \\`docs-missing\\` (\\`docsDir\\` points\n at nothing) or \\`services-missing\\` (it is a directory with no \\`services/\\` —\n usually the service repo after a typo in \\`docsDir\\`).\n- Both modes read frontmatter (\\`frontmatter.missing\\`, \\`frontmatter.malformed\\`,\n \\`frontmatter.field-mismatch\\`,\n \\`frontmatter.status-unknown\\`, \\`frontmatter.field-missing\\`); a service's spec.md —\n and its arch.spec.md, same conventions — additionally carries the sources chain\n (\\`sources.absent\\`, \\`sources.path-missing\\`, \\`sources.empty\\`,\n \\`sources.skipped\\`, \\`sources.unvouched\\`, \\`sources.stale\\`) and the doc-side freshness check\n (\\`content.stale\\`) — the one provenance warning that needs no service repo, so\n it is reported from the docs repo too, \\`--service\\` and \\`--all\\` alike.\n- \\`loam archive\\` alone reports the breaches only the merge computation can see:\n \\`living.requirement-outside-requirements\\` (error), \\`openapi.op-modified\\` (warn),\n \\`openapi.path-item-modified\\` (warn), \\`openapi.component-modified\\` (warn),\n \\`openapi.ref-unresolved\\` (error), \\`openapi.remove-marker-path-level\\` (error —\n an \\`x-loam-remove: true\\` written at PATH level, beside the methods rather than\n inside one: it addresses no operation, so it retires nothing, and it is not a\n contract key either) and \\`service.no-model\\` (warn — the archive creates\n \\`services/<id>/\\`, or puts a service in the landscape the fleet has no directory\n for at all, and nothing writes its model.likec4).\n\n\\`loam validate <target>\\` is the positional spelling of the first two: a feature id\nor a service id, tried in that order, so the feature wins when one name could be\nboth and \\`--service\\`/\\`--feature\\` force the reading. When the name really is both,\nthe run says so — \\`target.ambiguous\\` (warn) names the reading it took and the\nflag that forces the other, and \\`--json\\` carries \\`resolvedKind\\` — because a\nsilent precedence rule is a report about a target nobody asked for. The\npositional together with \\`--all\\`, \\`--service\\` or \\`--feature\\` is refused\n(\\`invalid-option\\`).\n\nTwo rendering levers change nothing else: \\`loam validate --errors-only\\` drops\nthe confirmations from the TEXT view (the \\`--json\\` payload is byte-identical),\nand \\`loam list --needs-work\\` narrows the service list to the ones with\nsomething missing — the adoption worklist.\n\n\\`--strict\\` (every targeting mode, \\`--all\\` included) exits 1 when any error\nor warning exists — \\`ok\\`-severity findings are confirmations and never trip\nit. It changes the exit code and nothing else:\n\\`valid\\` still means \"no errors\", and the \\`--json\\` payload stays byte-for-byte\nwhat it was. The stricter grade is a per-invocation lever, visible in the CI\npipeline that passes the flag — deliberately not a per-repo profile.\n\n\\`loam delta <FEAT> --json\\` exits 1 when \\`architecture.errors\\` is non-empty, with\n\\`ok: true\\` and the full payload intact: the empty C4 slice means the delta did\nnot parse, not that the feature changes no architecture. Branch on the exit code\nbefore consuming the slice as a task brief.\n\nFindings with severity \\`ok\\` are confirmations, not work: \\`c4.valid\\`, \\`delta.valid\\`,\n\\`requirements.covered\\`, \\`api.covered\\`, \\`spine.resolved\\`, \\`coherence.ok\\`,\n\\`landscape.matched\\`, \\`archedge.covered\\`, \\`sources.resolved\\`, \\`sources.current\\`,\n\\`gherkin.current\\`.\n\nA finding's \\`subject\\` names the service it is about. The envelope separates \\`ok\\` (the\ncommand ran) from \\`valid\\` (the docs pass). A refusal is \\`ok: false\\` with a stable\n\\`error.code\\`: \\`no-config\\` / \\`config-invalid\\` (no loam.json / a corrupt one),\n\\`docs-missing\\` / \\`services-missing\\` (a \\`docsDir\\` that points at nothing, or at a\ndirectory that is not a docs repo — the fleet is unreadable, not empty),\n\\`unknown-target\\` (no such service or feature), \\`invalid-option\\` (flags that contradict\neach other, or a value that cannot be right — a \\`loam list\\` section that is not\nservices or features, or a service id that is not a legal\n\\`services/<id>/\\` directory name, included), \\`already-exists\\` (\\`loam new\\` refusing\nto scaffold over an existing feature), \\`sources-absent\\` / \\`sources-path-missing\\` /\n\\`vouch-raced\\` (\\`loam vouch\\` refusing to stamp — the last one when the document\nchanged between the read and the write, so nothing was written),\n\\`not-coherent\\` / \\`living-outside-requirements\\` /\n\\`archive-exists\\` / \\`merge-failed\\` / \\`rollback-incomplete\\` / \\`docs-busy\\` /\n\\`commit-interrupted\\`\n(\\`loam archive\\` — see the\narchive gate below; \\`docs-busy\\` means another archive or unarchive holds the docs\nrepo's lock, nothing was read or written, and re-running once it finishes works),\n\\`feature-active\\` / \\`snapshot-missing\\` / \\`snapshot-stale\\` / \\`snapshot-corrupt\\` /\n\\`restore-failed\\` / \\`rollback-incomplete\\` / \\`docs-busy\\` / \\`commit-interrupted\\`\n(\\`loam unarchive\\` — the\n\\`restore-failed\\` / \\`rollback-incomplete\\` pair splits\nexactly as archive's does; see \"Taking an archive back\"). \\`commit-interrupted\\` is\nthe pair's shared refusal: a previous archive or unarchive was killed mid-commit\nand this run cannot repair it on its own — a half-written file has been edited\nsince, the repairing pre-image is gone or altered, or \\`.loam-commit\\` itself\ncannot be read. \\`loam doctor\\` names the same state as\n\\`doctor.commit-interrupted\\`. And \\`gherkin-conflict\\`\n(\\`loam gherkin <FEAT>\\` would overwrite a \\`.feature\\` file owned by another\nfeature still in flight — the whole emission refuses and names the owner),\n\\`answers-unreadable\\` / \\`answers-mismatch\\` /\n\\`answers-unevidenced\\` / \\`record-federated\\` / \\`record-unreadable\\`\n(\\`loam verify --record\\` / \\`--results\\` — an unreadable or\nunrecognizable cucumber report refuses under \\`answers-unreadable\\` too;\n\\`record-federated\\` and \\`record-unreadable\\` are the two records loam will not\noverwrite), and\n\\`internal\\` — an unexpected throw, the one code with no stable meaning.\n\n\\`--all\\` reports a target per service, a target per feature in flight, and one target\nof kind \\`landscape\\` for the fleet-level checks that belong to no single service.\n\nThree different words for three different questions, and no command conflates them:\n\\`ok\\` — the command ran; \\`valid\\` — the documents pass (\\`validate\\`); \\`verified\\` —\nsomebody says the code was built and showed evidence (\\`verify\\`). A feature can be\nvalid and unverified, or verified and incoherent. Read the one you meant.\n\n## The archive gate\n\nThe three axes agreeing is called **coherence**, and \\`loam validate --feature\\` reports\nit as such. \\`loam archive\\` runs the same coherence check first and refuses a feature\nwith GATING issues — every error, plus the rare warning marked \\`gates: true\\` because\nthe merge would silently drop authored content even though the document is legal.\nAdvisory warnings never block: archive prints them and proceeds. Each one still names\nsomething real — usually something the merge will drop or overwrite — so read them\nbefore the merge runs, not after.\n\n\\`--approve\\` overrides the gating issues — only those, and archive prints exactly which\nones it overrode. It is a human decision, not an agent's: if archive refuses, fix the\nbreach or hand it back.\n\nBreaches only the merge computation itself can see are reported at plan time,\nafter the gate. \\`living.requirement-outside-requirements\\` (error): the LIVING spec\nholds a requirement outside \\`## Requirements\\`, and the merge rewrites only that\nsection, so the requirement would land in the file twice — \\`--approve\\` does not\noverride it, because the duplication is mechanical, not a judgment call; re-home the\nrequirement first. \\`openapi.op-modified\\` (warn): the feature redefines an operation\nthe living OpenAPI already has, and the merge overwrites the living definition\nwholesale.\n\nThe OpenAPI merge also carries the merged operations' \\`$ref\\` closure: every\n\\`#/components/<kind>/<name>\\` they reference — recursively, a component's own refs\nincluded — is copied from the feature document into the living one, so an operation\nnever lands pointing at a schema that stayed behind. \\`openapi.component-modified\\`\n(warn): a carried component overwrites a living one that differs, wholesale, same\ndiscipline as an operation. \\`openapi.ref-unresolved\\` (error, \\`--approve\\`\noverrides): a ref reachable from the merged operations resolves in neither\ndocument, so merging would write a dangling reference. External refs — URLs, file\npaths, anything not starting \\`#/\\` — are out of scope: left untouched, never gated.\n\n## Taking an archive back\n\n\\`loam unarchive <FEAT>\\` restores the living docs and re-opens the feature. It works\nby putting bytes back, not by inverting the merge — archive copies every file it is\nabout to overwrite into \\`features/archive/<FEAT>/.loam-before/\\` first, because the\nprevious text of a \\`MODIFIED\\` requirement is written down nowhere else. Do not edit\nor delete that directory, and never reconstruct an old living spec by hand from an\narchived delta: what the requirement said BEFORE is not in there, and a plausible\nreconstruction is a lie the next reader has no way to catch.\n\nEvery pre-image is digested when the archive writes it, and re-checked before the\nrestore stages anything: \\`snapshot-corrupt\\` means a pre-image's bytes no longer\nmatch the digest archive recorded for them, so restoring it would write text\nnobody authored. \\`--force\\` does NOT override that one — the damage is to the\nundo itself, not to the living docs — and the fix is version control.\n\nIt refuses rather than guesses, under codes you can branch on: \\`feature-active\\` (a\nfeature of that id is in flight again), \\`snapshot-missing\\` (archived before loam\nrecorded this — the docs have to come back from version control), \\`snapshot-stale\\`\n(a merged file changed after the archive, so restoring would revert someone else's\nwork). \\`--force\\` overrides the last one, and like \\`--approve\\` it is a human's call.\n\nA restore that fails outright splits the same way archive's does: \\`restore-failed\\`\nmeans nothing was restored or everything was rolled back — the living docs are\nunchanged, fix the reported cause and re-run; \\`rollback-incomplete\\` means the\nrestore failed AND some files could not be put back — stop and hand it to a human,\nthe message lists the files to check.\n\n## Dropping a feature\n\nA feature that was never archived is one directory and nothing else: no living doc\nreferences it until \\`loam archive\\` merges it. To abandon it, delete the directory —\n\\`git rm -r features/<FEAT-dir>\\` — and version control keeps the record of the\nattempt. An ARCHIVED feature is the opposite, its content folded into the living\ndocs: run \\`loam unarchive <FEAT>\\` first, then delete. There is no \\`loam abandon\\`,\ndeliberately — a removal that computes nothing is what version control is for.\n`;\n\n/**\n * One slash command, format-free. `body` is the whole protocol — the per-tool\n * emitters in AGENT_TOOLS add only a path and a wrapper (frontmatter fields, a\n * TOML shell), never their own prose: one source of truth, thin spellings.\n * Bodies keep Claude's positional `$1`/`$2` placeholder convention across\n * every tool — dialects differ (Gemini's {{args}}, Copilot's ${input}) and rewriting the\n * text per tool would be a second copy of the protocol in all but name; an\n * agent reading the file still sees which argument goes where.\n */\nexport interface CommandContent {\n /** Stable command name (`loam-check`) — also the flat file name everywhere. */\n name: string;\n description: string;\n /** What the user passes, in Claude's `argument-hint` spelling. */\n argumentHint: string;\n /** The protocol itself: markdown, ends with a newline. */\n body: string;\n}\n\nconst COMMANDS: CommandContent[] = [\n {\n name: \"loam-adopt\",\n description:\n \"Adopt a service — write its baseline docs from its code, as draft, then validate\",\n argumentHint: \"<service-id>\",\n body: `Write one service's baseline documentation into the loam docs repo (its path is\n\\`docsDir\\` in ./loam.json). You read the code; loam states the work and checks the\nresult. It never reads the service — so anything you cannot show, do not write.\n\n0. If there is no ./loam.json here, wire the repo first:\n \\`loam init --docs <path-to-docs-repo> --service $1\\` (add \\`--create\\` ONLY if the\n docs repo does not exist yet), then \\`loam doctor\\` — it resolves \\`docsDir\\`,\n checks the docs repo is one, and reports the service binding. Do not go on with\n \\`docs-missing\\`, \\`services-missing\\` or any \\`doctor.*\\` blocker standing.\n Every \\`doctor\\` finding carries a \\`fix\\` field spelling the exact command —\n type that, not an approximation of it.\n If this repo is already wired, \\`loam status --json\\` in the docs repo is the\n fastest way to see whether $1 is even the service that owes work next: its\n \\`next.adopt\\` entries name the services with nothing written down yet.\n1. \\`loam adopt --service $1 --json\\`. That output IS the brief:\n - \\`warnings[]\\` — read these FIRST. A near-miss against an existing service id\n usually means \\`$1\\` is a typo, and a brief followed to the letter then produces\n a complete, validating baseline for a service that does not exist.\n - \\`targets[]\\` — every file to write. \\`action: \"diff\"\\` means the file ALREADY EXISTS:\n read it, diff your findings against it, report what disagrees. Do not replace it.\n \\`action: \"edit\"\\` appears on exactly one target — \\`architecture/landscape.likec4\\`,\n the WHOLE FLEET's map: add this service's element and edges to it, never rewrite it.\n - \\`targets[].shape\\` — the grammar of each artifact, and \\`example\\` where one is\n shorter than a description. Every rule there is one a later check depends on;\n rules nothing checks are in \\`unchecked[]\\` instead, and are still worth following.\n - \\`landscape\\` — the elements and edges the fleet already has for this service.\n Bind to them; do not draw a second version of the same box. \\`landscape.expects\\`\n lists operations other services already call — your openapi.yaml owes them.\n \\`landscape.instruction\\` is the write the fleet map still owes this service; it\n is \\`null\\` once an element resolves to it, and only then.\n - \\`frontmatter\\` — what to put in the header of every markdown artifact.\n - \\`checks[]\\` — what \\`loam validate\\` will run. \\`unchecked[]\\` — what it will not.\n2. Read \\`AGENTS.md\\` at the docs repo root, then read the code: entry points, HTTP\n routes and handlers, published events, config, deploy manifests, tests.\n **Keep a list of every path you actually open.** That list becomes \\`sources\\` —\n written as files and directories, never glob patterns — and it is the only line\n tying the document to the repository.\n3. Write the artifacts under \\`services/$1/\\`, in the order the brief lists them, and\n make the landscape edit the brief asks for.\n Everything \\`status: draft\\`. Never write \\`last_verified\\`, \\`sources_digest\\`,\n \\`content_digest\\` or \\`sources_files\\`.\n4. \\`loam validate --service $1 --json\\`. Fix every error. \\`sources.unvouched\\` is\n expected on a fresh baseline — it closes when a person vouches, not when you do.\n5. \\`loam validate --all --json\\` in the docs repo. This is the run that grades the\n fleet map: \\`landscape.service-unmodelled\\` means the element never landed,\n \\`landscape.binding-unknown\\` / \\`landscape.binding-duplicate\\` mean it landed wrong,\n and \\`landscape.missing\\` means there is no map at all. A baseline that passes step 4\n and fails this one is documented and invisible.\n6. Hand back, and say three things: what you could not determine from the code, what\n the existing artifacts disagreed with, and which parts you are least sure of.\n Then a human runs \\`loam vouch --service $1\\` in the service's own repo.\n\nWhere the code does not say, write that it does not say. A confident sentence about\nbehaviour nobody can find is the one failure mode none of loam's checks can catch.\n`,\n },\n {\n name: \"loam-feature\",\n description:\n \"Start a new loam feature — scaffold it, then author the C4 delta and requirement deltas\",\n argumentHint: `<FEAT-id> \"<title>\"`,\n body: `Start a new feature in the loam docs repo (its path is \\`docsDir\\` in ./loam.json).\n\n1. Read \\`AGENTS.md\\` at the root of the docs repo first — it defines the ID spine\n everything here depends on.\n2. Understand the current state before proposing a change:\n - \\`loam status --json\\` — what is already in flight and what it is waiting on.\n Run this FIRST if you joined this repository halfway or lost the session:\n \\`next[]\\` is ordered and each entry carries the literal command to run, so it\n tells you whether the work is to start $1 at all. \\`loam status $1 --json\\`\n narrows it to one feature once $1 exists\n - \\`loam list --json\\` — what services exist, and what documentation they are missing\n - \\`loam show <service> --json\\` — what a service owns, exposes, and who already calls it\n3. Scaffold it: \\`loam new $1 --title \"$2\"\\`, adding \\`--touches <id>\\` for every service the\n feature touches and \\`--new-service <id>\\` for every one it introduces. Every id goes\n through the \\`services/<id>/\\` grammar, so a typo is refused (\\`invalid-option\\`)\n rather than scaffolded; a \\`--touches\\` that matches nothing existing comes back as\n a \\`note\\` with the close ids, not as a failure — read it, because a \\`--touches\\`\n naming a service that does not exist is \\`delta.service-unknown\\` at validate time\n and a phantom \\`services/<typo>/\\` directory if it ever archives.\n4. Author what the templates left as TODO:\n - \\`intent.md\\` — the problem in business terms\n - \\`delta.likec4\\` — new elements and edges, each tagged \\`#$1\\`; every call edge carries\n \\`metadata { op '<operationId>' }\\`. For a requirements-only feature DELETE this\n file: an empty delta is legal, but a scaffold left half-filled is not\n - \\`specs/<svc>/spec.md\\` — one behaviour per requirement, SHALL, at least one\n Given/When/Then scenario; uncomment \\`Operations:\\` once the operation exists\n - \\`specs/<svc>/arch.spec.md\\` — the architectural obligations (outbox, retries,\n idempotency, alerts), same grammar; a \\`Covers:\\` line per requirement naming the\n tagged elements/edges it accounts for, or \\`c4.uncovered\\` says nothing does\n - \\`specs/<svc>/openapi.yaml\\` — define every operationId the edges reference\n5. **\\`loam rebase $1\\`** — the step between authoring and checking, and the one that\n stops two features in flight from silently deleting each other's work. It pins\n every MODIFIED/REMOVED requirement (\\`Based-On:\\`) and every operation in the\n contract delta (\\`x-loam-based-on\\`) to the living version you wrote against.\n Run it on the contract axis even if you changed exactly one endpoint: that is\n what marks the REST of the document as quotation the merge must not write back.\n Without the pins, \\`delta.baseline-missing\\` / \\`openapi.baseline-missing\\` warn,\n nothing refuses, and the second feature to archive reverts the first — with\n \\`+0 ~1 -0\\`, exit 0, and nobody told.\n Restamping is not resolving: if rebase reports a requirement as MOVED, re-read\n the living text and fold in what you still mean BEFORE you ship.\n6. \\`loam validate --feature $1 --json\\`. Do not stop while \\`valid\\` is false.\n7. \\`loam dependencies $1 --json\\` before anyone starts building: it says which\n features in flight have to archive first (\\`order\\`) and which collisions no\n ordering fixes (\\`conflicts\\`, \\`cycles\\`). Every conflict it names is one \\`loam rebase\\`\n cannot fix for you — two intentions have to be merged by people.\n\nThe operation's three spellings — the edge's \\`op\\`, the requirement's \\`Operations:\\`\nline, and the OpenAPI \\`operationId\\` — must match exactly.\n`,\n },\n {\n name: \"loam-implement\",\n description:\n \"Implement a loam feature's slice for one service — generated gherkin first, tests from it, then code\",\n argumentHint: \"<FEAT-id> [service]\",\n body: `Implement one service's part of a feature. Every step runs in the SERVICE'S own\nrepository, which needs its own committed ./loam.json — if there is none,\n\\`loam init --docs <path-to-docs-repo> --service $2\\` then \\`loam doctor\\` first.\n\n0. \\`loam status $1 --json\\` — where this feature actually is. Run it before\n anything else if you joined halfway or lost the session: it says which\n artifacts are still owed, whether another feature has to archive first, and\n what the next step is, each \\`next[]\\` entry carrying the literal command. It\n writes nothing, so it is always safe to re-run.\n1. \\`loam delta $1 --service $2 --json\\` (drop \\`--service\\` to use the service configured\n in ./loam.json). That output IS the task:\n - \\`intent\\` — why this exists\n - \\`requirements[]\\` — what to build, with \\`scenarios[].lines\\` verbatim\n - \\`archRequirements[]\\` — the architectural obligations (outbox, retries, alerts),\n same shape; their scenarios become integration/ops tests, and \\`covers\\` names\n the model objects each exercises\n - \\`architecture\\` — whether this service is new, and the calls in and out of it\n - \\`api\\` — the endpoints this feature adds or retires for this service: path,\n method, operationId and summary, with removals spelled \\`REMOVE <METHOD> <path>\\`\n - \\`services\\` — every service this feature projects onto, so a run without\n \\`--service\\` tells you which ones to ask for rather than guessing\n Exit 1 with \\`ok: true\\` means \\`architecture.errors\\` is non-empty: delta.likec4 did\n not parse, and the empty C4 slice is a parse failure, not \"no architecture change\".\n Stop and fix the delta (\\`loam validate $1 --json\\`) before building anything.\n2. In the service's repo, \\`loam gherkin $1 --json\\` — one \\`.feature\\` file per changed\n requirement lands under \\`<gherkinDir>/loam/\\` (default \\`features/loam/\\`), scenarios\n digest-stamped, arch requirements tagged \\`@architecture\\`. Those files ARE the\n acceptance criteria. Never edit them: regeneration rewrites the directory, and\n \\`loam validate\\` reports the suite stale by digest, not by your intentions.\n3. Write step definitions for the generated scenarios FIRST — outside \\`loam/\\`.\n Do not paraphrase a scenario into something easier to pass; it is the acceptance\n criterion someone else reviews against.\n4. Implement until the suite passes — run it with a JSON report\n (\\`cucumber-js --format json:report.json\\`):\n \\`loam verify $1 --service $2 --results report.json\\` consumes that report as the\n done-check's answer sheet. Record from THIS repo, with \\`--service\\` — the\n \\`--service\\`-less \\`--record\\` form writes the whole record from one place and is\n refused (\\`record-federated\\`) once another service has attested.\n5. Honour the contract: every operation in \\`architecture.inbound\\` must exist under\n exactly that operationId, and every one in \\`architecture.outbound\\` must be called.\n6. If building made you change the feature's documents at all — a requirement's\n wording, an operation in its openapi.yaml — **\\`loam rebase $1\\`** before you\n validate. Every edit you make in the docs repo while other features are in\n flight is written against a living document that may have moved since the\n feature directory was created, and the pins are the only thing that lets the\n merge tell an EDIT from a QUOTE. \\`delta.baseline-missing\\` and\n \\`openapi.baseline-missing\\` are the warnings that say you skipped it;\n \\`delta.baseline-stale\\` / \\`openapi.baseline-stale\\` are archive refusing because\n somebody landed a change underneath you — re-read theirs, fold in what you\n still mean, then rebase again. Re-pinning without re-reading is how you\n overwrite someone else's requirement with loam's blessing.\n7. \\`loam validate --feature $1 --json\\` before handing back.\n\nIf the requirement is ambiguous, say so and stop — do not invent behaviour and\nleave the spec disagreeing with the code.\n`,\n },\n {\n name: \"loam-check\",\n description: \"Validate the loam docs repo and fix what it reports\",\n argumentHint: \"[--all | <FEAT-id> | <service>]\",\n body: `Run loam's checks and fix what they find.\n\n- whole fleet: \\`loam validate --all --json\\` (run in the docs repo)\n- one target: \\`loam validate <FEAT-id | service-id> --json\\` — the feature reading is\n tried first; \\`--feature <id>\\` / \\`--service <id>\\` force one when a name could be both,\n and \\`target.ambiguous\\` (warn) says when a name really was both and which reading was taken\n- \\`--strict\\` (any mode) exits 1 on any warning too. Exit code only: \\`valid\\` and the\n payload do not change, so the stricter grade lives in the CI invocation, not the repo\n- \\`--errors-only\\` drops the \\`ok\\` confirmations from the text view; the \\`--json\\`\n payload is byte-identical either way\n- refusals before any grading: \\`docs-missing\\` / \\`services-missing\\` (\\`docsDir\\` in\n ./loam.json points at nothing, or at a directory with no \\`services/\\`) — run\n \\`loam doctor\\` and fix the wiring, do not read an empty report as a clean fleet\n\nBranch on \\`findings[].code\\`, not the prose. Errors fail validate; a coherence finding\nwith \\`gates: true\\` will stop \\`loam archive\\` even when it is a warning. Fix every\nerror and every gating finding. Leave an advisory warning only if you can say why,\nand say it.\n\n\\`--service <id>\\` — one service's own axes (\\`--all\\` runs these for every service):\n\n| code | what it means | what to do |\n|---|---|---|\n| \\`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 |\n| \\`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 |\n| \\`c4.invalid\\` | model.likec4 does not parse | fix this first — an unreadable axis makes every other check meaningless |\n| \\`requirements.missing-scenarios\\` | a requirement with no scenario | add the acceptance criteria; do not delete the requirement |\n| \\`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 |\n| \\`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 |\n| \\`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 |\n| \\`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 |\n| \\`spec.requirement-id-repeated\\` | one requirement declares \\`Requirement-ID\\` more than once | keep exactly one identity line |\n| \\`spec.requirement-id-duplicate\\` | one ID identifies multiple requirements in the same file | give each requirement its own stable ID |\n| \\`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 |\n| \\`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 |\n| \\`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 |\n| \\`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 |\n| \\`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 |\n| \\`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 |\n| \\`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 |\n| \\`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 |\n| \\`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 |\n| \\`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>\\` |\n| \\`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 |\n| \\`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 |\n| \\`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 |\n| \\`api.ungoverned\\` (warn) | operation(s) no requirement's \\`Operations:\\` line names | write the requirement, or link an existing one |\n| \\`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 |\n| \\`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 |\n| \\`spine.landscape-invalid\\` | the living landscape does not parse, so the C4↔API spine cannot be checked | fix architecture/landscape.likec4 first |\n| \\`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 |\n| \\`spine.op-link-missing\\` (warn) | a landscape \"Calls\" edge into this service with no \\`metadata { op }\\` | link it to the operationId |\n| \\`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 |\n| \\`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 |\n| \\`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 |\n| \\`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 |\n| \\`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 |\n| \\`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 |\n| \\`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 |\n\n\\`--feature <FEAT-id>\\` — a change's three axes against each other. The SAME checks\ngate \\`loam archive\\` (errors block it; warnings never do), so a clean run here is\nwhat lets a feature ship:\n\n| code | what it means | what to do |\n|---|---|---|\n| \\`delta.invalid\\` | delta.likec4 does not parse | fix this first — an unreadable axis makes every other check meaningless |\n| \\`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 |\n| \\`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 |\n| \\`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 |\n| \\`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 |\n| \\`spec-api.op-undefined\\` | a requirement governs an operation its provider's OpenAPI does not define | define the endpoint, or correct the \\`Operations:\\` line |\n| \\`spec-api.op-pending\\` (warn) | the governed operation is defined by another feature in flight | archive that feature first |\n| \\`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 |\n| \\`c4-api.op-pending\\` (warn) | the called operation is defined by another feature in flight | archive that feature first |\n| \\`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 |\n| \\`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 |\n| \\`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 |\n| \\`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 |\n| \\`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 |\n| \\`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 |\n| \\`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 |\n| \\`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 |\n| \\`c4.op-ungoverned\\` (warn) | an operation is called but no requirement governs it | write the requirement |\n| \\`c4.op-link-missing\\` (warn) | a \"Calls\" edge in the delta with no \\`metadata { op }\\` | link it to the operationId |\n| \\`api.op-unconsumed\\` (warn) | an added operation no edge consumes | model the caller, or say why it is provider-only |\n| \\`service.no-requirement-delta\\` (warn) | a new service with no spec delta | write \\`specs/<svc>/spec.md\\` |\n| \\`archedge.uncovered\\` (warn) | no scenario names a tagged edge (a heuristic) | write the scenario, or say why the edge needs none |\n| \\`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 |\n| \\`delta.unknown-section\\` | a heading that nearly matches the delta grammar | fix it — everything under it merges as NOTHING today, silently |\n| \\`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 |\n| \\`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 |\n| \\`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 |\n| \\`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 |\n| \\`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 |\n| \\`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) |\n| \\`delta.removed-unknown\\` | REMOVED one that does not exist | drop the section, or fix the name |\n| \\`delta.added-duplicate\\` | ADDED a name the living spec already has | use MODIFIED — as written, the merge REPLACES the living requirement |\n| \\`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 |\n| \\`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 |\n| \\`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 |\n| \\`delta.removed-pending\\` (warn) | REMOVED something another feature in flight introduces | archive that feature first; \\`loam dependencies --json\\` says in which order |\n| \\`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\\` |\n| \\`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 |\n| \\`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 |\n| \\`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 |\n| \\`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 |\n\n\\`--all\\` — everything above for every target, plus the fleet cross-check:\n\n| code | what it means | what to do |\n|---|---|---|\n| \\`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 |\n| \\`landscape.invalid\\` | the living landscape does not parse | fix it first — the fleet cross-check cannot run against a document nobody can read |\n| \\`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 |\n| \\`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\\`) |\n| \\`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 |\n| \\`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 |\n| \\`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 |\n| \\`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\\` |\n| \\`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 |\n| \\`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 |\n| \\`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 |\n\nfrontmatter and provenance — services' spec.md and arch.spec.md, and features'\nintent.md, both modes:\n\n| code | what it means | what to do |\n|---|---|---|\n| \\`frontmatter.missing\\` (warn) | no frontmatter at all | add owner, status and sources |\n| \\`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 |\n| \\`frontmatter.field-mismatch\\` | the doc names a different service/feature than the one it lives under | fix the frontmatter, or move the file |\n| \\`frontmatter.status-unknown\\` | a status nobody defined (\\`verifed\\`) | use the documented vocabulary — a typo here reads as unverified forever |\n| \\`frontmatter.field-missing\\` (warn) | owner, status or the identity field is absent | fill them in |\n| \\`sources.absent\\` (warn) | the doc names no sources | nothing ties it to the code, so nothing can tell you when it goes stale |\n| \\`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 |\n| \\`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>\\` |\n| \\`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 |\n| \\`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 |\n| \\`sources.unvouched\\` (warn) | \\`sources\\` with no \\`sources_digest\\` — nobody ever stamped it | leave it: vouching is a human's reading, not yours |\n| \\`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 |\n\n\\`loam archive\\` alone — breaches only the merge computation can see, reported at\nplan time (they never appear in \\`validate\\`):\n\n| code | what it means | what to do |\n|---|---|---|\n| \\`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 |\n| \\`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 |\n| \\`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 |\n| \\`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 |\n| \\`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 |\n| \\`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 |\n| \\`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 |\n\n\\`sources.stale\\` and \\`content.stale\\` are the warnings you cannot close by yourself.\nFix what the code now says, then hand it back — the stamp is a person's claim to\nhave read it.\n`,\n },\n {\n name: \"loam-verify\",\n description:\n \"Verify a built loam feature against its own promises, claim by claim, with evidence\",\n argumentHint: \"<FEAT-id>\",\n body: `Check that the code somebody just built is the feature that was designed.\n\nloam derives the questions; the test runner and you answer them. loam never\nreads the service, so a verdict is worth exactly what its evidence is worth.\n\n1. \\`loam verify $1 --json\\`. \\`claims[]\\` is the checklist, derived from the feature's\n own artifacts — one claim per new service, per operation its openapi delta adds,\n per tagged edge that names an operation, and per scenario of every changed\n requirement, arch.spec.md deltas included. Each has a stable \\`id\\` and a\n \\`subject\\` (the service whose code answers it); a claim that says arch.spec.md\n wants an integration/ops test, not an acceptance test. An ARCHIVED feature returns its record as frozen history instead\n (\\`frozen: true\\`) and \\`--record\\` / \\`--results\\` refuse it — \\`loam unarchive\\`\n first if the answers really must change.\n2. The \\`scenario.tested\\` claims are the runner's to answer, not yours. In the\n service's repo, run the generated suite with a JSON report —\n \\`npx cucumber-js --format json:report.json\\` (cucumber-jvm, behave and\n SpecFlow emit the same format). The generated scenarios carry\n \\`@loam-digest-…\\` tags that ride into the report; that digest is the match,\n so never retitle or hand-edit a generated scenario to make one pass.\n If there is no runnable suite yet you MAY answer them yourself, and the record\n will say so: \\`answered_by: agent\\` makes the feature **attested**, not verified\n (\\`verified: false\\`, \\`verdict: \"attested\"\\`, the \\`verify.scenario-attested\\`\n notice, and a \\`next.verify-attested\\` step in \\`loam status\\`). Nothing gates on\n it — a legacy service with no suite must still be able to ship — but say so\n when you hand back, and close it with \\`--results\\` the moment a suite runs.\n \\`verify.digest-contested\\` (warn) is the case where two services word a\n scenario identically: they share one digest, so no single report can say whose\n suite ran it, and each service must record its own with \\`--service\\`.\n3. Answer every OTHER claim by finding it in the code. Not by reasoning that it\n must be there — by opening the file:\n - \\`service.exists\\` — the service is deployable: its build, its entry point.\n - \\`api.exposes\\` — the route handler serving that operationId, not the spec that\n declares it.\n - \\`c4.calls\\` — the call site in the CALLER.\n4. Write those answers as JSON — evidence is \\`file:line\\`, one entry per place it can\n be seen:\n \\`\\`\\`json\n { \"answers\": [\n { \"id\": \"<claim id>\", \"verdict\": \"confirmed\", \"evidence\": [\"src/split/Api.ts:42\"] },\n { \"id\": \"<claim id>\", \"verdict\": \"unconfirmed\", \"note\": \"the split service has no entry point yet\" }\n ] }\n \\`\\`\\`\n5. Record, **in each affected service's own repository**, one service at a time:\n\n \\`\\`\\`sh\n loam verify $1 --service <id> --results report.json --record answers.json --json\n \\`\\`\\`\n\n \\`--service <id>\\` narrows the checklist to that service's claims and binds the\n answers to this repository's commit, writing ONE attestation into a shared\n \\`schema: 2\\` \\`verification.yaml\\`. Running it in the next repo adds that repo's\n attestation and leaves the others standing. In a repo whose ./loam.json\n declares a \\`service\\`, \\`--service\\` may be omitted — it is substituted, and the\n run still federates. \\`--results report.json\\` alone is legal when the\n checklist is all scenarios.\n\n ⚠ **Never run \\`loam verify $1 --record answers.json\\` without \\`--service\\`.**\n That is the legacy all-at-once form: it writes the WHOLE record from one place,\n and over a record that already carries other services' attestations it is\n refused (\\`record-federated\\`, naming them) precisely because it would erase\n them. If you meet that refusal, re-run with \\`--service\\`; do not work around it.\n\n The other refusals: \\`answers-mismatch\\` (an id\n nobody asked about, a claim you left out — re-run step 1, the feature moved —\n or an answers entry for a \\`scenario.tested\\` claim: the runner owns those),\n \\`answers-unevidenced\\` (a \\`confirmed\\` with nothing behind it),\n \\`answers-unreadable\\` (a report or answers file it cannot read),\n \\`record-unreadable\\` (a \\`verification.yaml\\` that exists but cannot be read as a\n record — it is never overwritten; a human repairs it, and\n \\`verify.record-miscounted\\` is the reason when its \\`summary\\` block contradicts\n its own \\`claims[]\\`: neither half can be believed, so re-record rather than\n editing the counts),\n \\`service-mismatch\\` (\\`--service\\` names a service this repo does not declare) and\n \\`repository-unavailable\\` (\\`--service\\` from a repo that declares none, or whose\n commit cannot be read — an attestation must be bound to the code it is about).\n6. Report every \\`unconfirmed\\` claim and what it would take to close it, and read\n \\`discarded\\` in the payload: it names every previous answer the new record does\n not carry, and each one is either evidence that went stale or evidence that was\n just dropped. A scenario claim the report failed or missed closes by fixing the\n code and re-running the suite — never by editing the generated .feature.\n7. Reading is safe from anywhere: \\`loam verify $1 --service <id> --json\\` without\n \\`--record\\` writes nothing and works from the docs repo too. \\`services\\` lists\n every subject of the checklist, so it is how you find out which repos still\n owe an attestation, and \\`loam status $1 --json\\` says the same thing in the\n shape of a next step.\n\nAnything you cannot show is \\`unconfirmed\\` with a note. That is a successful record,\nand it is the useful one: nothing gates on this file, so the only thing it is for is\ntelling the next reader the truth. \\`loam archive\\` will ship the feature either way —\nwhich is exactly why a \\`confirmed\\` you cannot back up costs nothing now and misleads\neveryone later.\n`,\n },\n {\n name: \"loam-ship\",\n description: \"Archive a finished loam feature — merge its deltas into the living docs\",\n argumentHint: \"<FEAT-id>\",\n body: `Archive a shipped feature.\n\n1. Confirm the code is actually built and merged. Archiving folds the delta into the\n living docs; doing it early makes the docs claim something that does not exist.\n \\`loam status $1 --json\\` answers \"is this actually finished\" in one call, and its\n \\`next[]\\` names what is outstanding if it is not.\n2. \\`loam validate --feature $1 --json\\` — must come back \\`valid: true\\`.\n If it reports \\`delta.baseline-missing\\` or \\`openapi.baseline-missing\\`, run\n \\`loam rebase $1\\` and re-validate BEFORE going on: those two say nothing has\n pinned what this delta was written against, so the merge cannot tell the text\n you EDITED from the text you merely restated, and it upserts all of it —\n reverting whatever another feature landed in between, silently and with exit 0.\n3. \\`loam archive $1 --dry-run --json\\` first. \\`plan[]\\` is every file the merge would\n write (\\`create\\` / \\`update\\`, then the final \\`move\\` into \\`features/archive/\\`), and\n none of them are written — read it before letting the merge touch the source of\n truth, and stop if a file you did not expect is on it. \\`warnings[]\\` is what the\n merge will do that is legal but lossy — advisory warnings never block, which is\n exactly why you read them now: \\`openapi.op-modified\\` means an operation the living\n contract already defines gets overwritten wholesale (\\`openapi.component-modified\\`\n is the same story for a component the merged operations carry),\n \\`delta.baseline-missing\\` / \\`openapi.baseline-missing\\` mean the pins are still not\n there and this merge may revert somebody (go back to step 2),\n \\`service.no-model\\` means a service arrives with no C4 centre behind it, and\n \\`delta.added-conflict\\`\n means another feature in flight adds the same requirement — the first to archive\n lands it, and the other's archive is then refused (\\`delta.added-duplicate\\`: its\n ADDED now collides with the living spec) unless a human \\`--approve\\`s the\n replacement.\n4. \\`loam archive $1 --json\\`. It merges three axes into the living state — requirements\n into \\`services/<svc>/spec.md\\`, endpoints into \\`services/<svc>/openapi.yaml\\`,\n elements and edges into \\`architecture/landscape.likec4\\` — then moves the feature\n under \\`features/archive/\\`. Success is \\`ok: true\\`; on \\`ok: false\\`, branch on\n \\`error.code\\`:\n - \\`not-coherent\\` — gating coherence issues; \\`issues[]\\` in the envelope lists them,\n each with \\`gates\\` resolved (advisory warnings do not block). Fix the breaches.\n \\`--approve\\` overrides the gating issues and is a human's call to make, not\n yours: report the breach and stop. \\`openapi.remove-marker-path-level\\` arrives\n here too: an \\`x-loam-remove\\` written beside the methods instead of inside the\n operation retires nothing.\n - \\`living-outside-requirements\\` — the LIVING spec holds a requirement outside\n \\`## Requirements\\`, and the merge would duplicate it. Re-home it, then re-run.\n \\`--approve\\` does not move this one.\n - \\`archive-exists\\` — \\`features/archive/\\` already has that directory; a human\n decides what it is.\n - \\`merge-failed\\` — the merge could not be computed, or failed and was rolled\n back. Either way the living docs are unchanged; fix the reported cause, re-run.\n A file loam cannot decode as UTF-8 refuses here by name rather than being\n rewritten with replacement characters. So does a living document that still\n holds git conflict markers — the \\`spec.merge-conflict\\` /\n \\`landscape.merge-conflict\\` findings ride in \\`findings[]\\`, and \\`--approve\\`\n does not override them: the loss is mechanical, not a judgment call.\n - \\`docs-busy\\` — another \\`loam archive\\` or \\`loam unarchive\\` holds the docs\n repo's lock. Nothing was read or written; wait for it and re-run once.\n - \\`commit-interrupted\\` — a previous archive or unarchive was killed mid-commit\n and this run cannot repair it on its own: a half-written file was edited\n since, its pre-image is gone or altered, or \\`.loam-commit\\` cannot be read.\n Run \\`loam doctor\\` (\\`doctor.commit-interrupted\\` / \\`doctor.commit-unreadable\\`\n name the same state) and hand it to a human — re-running does not help.\n - \\`rollback-incomplete\\` — the merge failed AND some files could not be restored.\n Stop and hand it to a human; the message lists the files to check.\n\nArchived by mistake? \\`loam unarchive $1\\` restores the living docs from the snapshot\narchive left in \\`features/archive/$1*/.loam-before/\\` and re-opens the feature. Do not\nhand-edit the living docs back instead: the previous text of a \\`MODIFIED\\` requirement\nis in that snapshot and nowhere else, so anything you write by hand is a guess. It\nrefuses under \\`snapshot-corrupt\\` when a pre-image's bytes no longer match the digest\nthe archive recorded — \\`--force\\` does not override that one, because the damage is\nto the undo itself — and under \\`snapshot-missing\\` for an archive older than the\ncurrent snapshot format. Both mean version control is the way back.\n`,\n },\n];\n\n/**\n * One delivery of one command: where the file goes, and what goes in it. Both\n * halves are thin by construction — the protocol is `CommandContent.body` and\n * nothing here may restate it.\n */\nexport interface AgentFileEmitter {\n /** Repo-relative path segments of the file. */\n path(name: string): string[];\n /** The full file: this tool's wrapper around the shared body. */\n format(cmd: CommandContent): string;\n}\n\n/**\n * A tool the commands can be emitted for — the registry key is the `--tools`\n * id (OpenSpec's ids where the tool has one there). A path and a wrapper are\n * the WHOLE adapter: OpenSpec's per-tool command layer, minus everything loam\n * refuses on principle — no version stamps in command files, no\n * overwrite-on-update, no managed marker blocks in anyone else's file. Paths\n * and wrappers follow each tool's own documented convention, pinned in\n * test/agents.test.ts.\n */\nexport interface AgentTool {\n /**\n * Repo-relative path segments of one command's file. Absent — together with\n * `format` — for a tool that registers no command files at all: Codex reads\n * skills and nothing else, and inventing a command directory for it would\n * scatter files no agent ever loads.\n */\n path?(name: string): string[];\n /** The full command file: this tool's wrapper around the shared body. */\n format?(cmd: CommandContent): string;\n /** The skill delivery. Absent for a tool with no skills convention. */\n skill?: AgentFileEmitter;\n /**\n * Repo-relative paths whose presence means this tool is in use here — what\n * `loam init` scans when no `--tools` says otherwise. Usually just the tool's\n * own dot-directory; spelled per tool because the exceptions matter more than\n * the rule (see `github-copilot`, whose `.github/` says nothing at all).\n */\n detect: string[];\n}\n\n/**\n * Pre-approved tools in a generated file's frontmatter, so an agent that honors\n * the field stops asking permission for each `loam` call. It only pre-approves:\n * everything else a command needs (Read, Write, the service's own test runner)\n * stays under the user's normal permission settings, and a tool that does not\n * know the field ignores it.\n */\nconst LOAM_ALLOWED_TOOLS = \"Bash(loam:*)\";\n\n/**\n * The Agent Skills convention, which every tool in the registry that reads\n * skills at all spells the same way: `<tool-dir>/skills/<name>/SKILL.md`, YAML\n * frontmatter, then the shared body verbatim.\n *\n * `description` is load-bearing in a way no command file's is — it is the only\n * thing the model sees before deciding whether to load the skill — so it comes\n * from the same `CommandContent.description` the command wrappers use rather\n * than a second, skill-flavoured copy that could drift into a better pitch for\n * a worse protocol.\n */\nconst skillsIn = (dir: string): AgentFileEmitter => ({\n path: (name) => [dir, \"skills\", name, \"SKILL.md\"],\n format: (c) =>\n `---\\nname: ${c.name}\\ndescription: ${c.description}\\n` +\n `allowed-tools: ${LOAM_ALLOWED_TOOLS}\\n---\\n\\n${c.body}`,\n});\n\n/**\n * TOML basic-string escapes for the one non-markdown format. Today's bodies\n * carry neither backslashes nor `\"\"\"`, but the escape is what keeps a future\n * body edit from silently corrupting the generated TOML.\n */\nconst tomlLine = (s: string): string => s.replace(/\\\\/g, \"\\\\\\\\\").replace(/\"/g, '\\\\\"');\nconst tomlBlock = (s: string): string => s.replace(/\\\\/g, \"\\\\\\\\\").replace(/\"\"\"/g, '\"\"\\\\\"');\n\n/* The four markdown command dialects the registry actually needs. Shared so a\n * tool's entry is its PATH and its dialect's name — the part that differs —\n * instead of a wall of near-identical template literals. `claude` deliberately\n * spells its own wrapper inline even though `hintedMd` matches it byte for\n * byte: an edit made here for some other tool must not be able to re-spell the\n * one format that is frozen. */\n/** Description-only frontmatter — the most common dialect by a wide margin. */\nconst describedMd = (c: CommandContent): string =>\n `---\\ndescription: ${c.description}\\n---\\n\\n${c.body}`;\n/** Description plus Claude's `argument-hint`, for the tools that copied it. */\nconst hintedMd = (c: CommandContent): string =>\n `---\\ndescription: ${c.description}\\nargument-hint: ${c.argumentHint}\\n---\\n\\n${c.body}`;\n/** Name plus description — the Cascade-style workflow frontmatter. */\nconst namedMd = (c: CommandContent): string =>\n `---\\nname: ${c.name}\\ndescription: ${c.description}\\n---\\n\\n${c.body}`;\n/** No frontmatter at all: a title line is the whole wrapper. */\nconst titledMd = (c: CommandContent): string => `# ${c.name}\\n\\n${c.body}`;\n\n/** The `loam-` prefix a namespace directory carries instead of the file name. */\nconst unprefixed = (name: string): string => name.replace(/^loam-/, \"\");\n\n/**\n * The command body a file actually gets: the shared protocol under the same\n * version stamp AGENTS.md carries.\n *\n * Applied once, here, rather than in twenty wrappers — the stamp is a fact\n * about loam, not about a tool's dialect, and a per-wrapper copy is twenty\n * chances to forget it. It rides at the top of the BODY (not the file) because\n * every dialect puts something of its own first: YAML frontmatter, a TOML key,\n * a title line. An HTML comment is invisible in all of them, and\n * `agentsStampVersion` matches it wherever in the file it lands.\n *\n * A repo initialized before stamping existed has unstamped files, which is\n * exactly what `doctor.agent-files-stale` is for: nobody has confirmed that\n * those instructions still describe this binary.\n */\nconst stamped = (c: CommandContent): CommandContent => ({\n ...c,\n body: `${agentsStampLine(LOAM_VERSION)}\\n\\n${c.body}`,\n});\n\nexport const AGENT_TOOLS: Record<string, AgentTool> = {\n // The original target and the default. This WRAPPER must stay byte-identical\n // to what loam has always written — SLASH_COMMANDS below derives through it —\n // and an already-initialized repo must still read as fully scaffolded\n // (`skipped` is existsSync, never content). What the wrapper wraps now\n // carries a version stamp, which is the one thing about a generated file loam\n // will report on; see `stamped`.\n claude: {\n path: (name) => [\".claude\", \"commands\", `${name}.md`],\n format: (c) =>\n `---\\ndescription: ${c.description}\\nargument-hint: ${c.argumentHint}\\n---\\n\\n${c.body}`,\n skill: skillsIn(\".claude\"),\n detect: [\".claude\"],\n },\n // Cursor invokes by flat file name (`/loam-check`); its frontmatter `name`\n // field spells that invocation form.\n cursor: {\n path: (name) => [\".cursor\", \"commands\", `${name}.md`],\n format: (c) => `---\\nname: /${c.name}\\n---\\n\\n${c.body}`,\n skill: skillsIn(\".cursor\"),\n detect: [\".cursor\"],\n },\n // Copilot prompt files: `.prompt.md` extension, description-only frontmatter.\n // Detection cannot use `.github/` — almost every repo has one and almost none\n // of them means Copilot — so it looks for the files Copilot itself reads.\n \"github-copilot\": {\n path: (name) => [\".github\", \"prompts\", `${name}.prompt.md`],\n format: describedMd,\n skill: skillsIn(\".github\"),\n detect: [\n \".github/copilot-instructions.md\",\n \".github/instructions\",\n \".github/prompts\",\n \".github/agents\",\n \".github/skills\",\n \".github/workflows/copilot-setup-steps.yml\",\n \".github/.mcp.json\",\n ],\n },\n // Gemini: TOML, and a namespace DIRECTORY — `.gemini/commands/loam/check.toml`\n // is invoked as `/loam:check`, so the file name drops the `loam-` prefix the\n // flat-named tools carry.\n gemini: {\n path: (name) => [\".gemini\", \"commands\", \"loam\", `${unprefixed(name)}.toml`],\n format: (c) =>\n `description = \"${tomlLine(c.description)}\"\\nprompt = \"\"\"\\n${tomlBlock(c.body)}\"\"\"`,\n skill: skillsIn(\".gemini\"),\n detect: [\".gemini\"],\n },\n opencode: {\n path: (name) => [\".opencode\", \"commands\", `${name}.md`],\n format: describedMd,\n skill: skillsIn(\".opencode\"),\n detect: [\".opencode\"],\n },\n // Cline reads workflows from `.clinerules/` — plain markdown, no frontmatter —\n // but its skills from `.cline/`. The two directories are unrelated, so both\n // count as a sighting.\n cline: {\n path: (name) => [\".clinerules\", \"workflows\", `${name}.md`],\n format: (c) => `# ${c.name}\\n\\n${c.body}`,\n skill: skillsIn(\".cline\"),\n detect: [\".cline\", \".clinerules\"],\n },\n // Amazon Q surfaces these as its PROMPT library, not as commands: the file is\n // invoked `@loam-check`, never `/loam-check`. Same file shape regardless.\n \"amazon-q\": {\n path: (name) => [\".amazonq\", \"prompts\", `${name}.md`],\n format: describedMd,\n skill: skillsIn(\".amazonq\"),\n detect: [\".amazonq\"],\n },\n // Antigravity's directory is `.agent/`, which is why the id and the directory\n // do not match here as they do almost everywhere else.\n antigravity: {\n path: (name) => [\".agent\", \"workflows\", `${name}.md`],\n format: describedMd,\n skill: skillsIn(\".agent\"),\n detect: [\".agent\"],\n },\n // Auggie is the Augment CLI: id `auggie`, directory `.augment/`.\n auggie: {\n path: (name) => [\".augment\", \"commands\", `${name}.md`],\n format: hintedMd,\n skill: skillsIn(\".augment\"),\n detect: [\".augment\"],\n },\n // Skills-only, deliberately: Codex reads `.codex/skills/` and does not load\n // custom command files, so it declares no command emitter at all rather than\n // laying down files nothing will ever open.\n codex: {\n skill: skillsIn(\".codex\"),\n detect: [\".codex\"],\n },\n // Continue's prompt files take a bare `.prompt` extension, and `invokable`\n // is what promotes one from a template to something you can call.\n continue: {\n path: (name) => [\".continue\", \"prompts\", `${name}.prompt`],\n format: (c) =>\n `---\\nname: ${c.name}\\ndescription: ${c.description}\\ninvokable: true\\n---\\n\\n${c.body}`,\n skill: skillsIn(\".continue\"),\n detect: [\".continue\"],\n },\n // Crush namespaces like Gemini does — `.crush/commands/loam/check.md` is\n // `/loam:check` — but stays markdown.\n crush: {\n path: (name) => [\".crush\", \"commands\", \"loam\", `${unprefixed(name)}.md`],\n format: namedMd,\n skill: skillsIn(\".crush\"),\n detect: [\".crush\"],\n },\n // Devin Desktop, formerly Windsurf: Cascade-style workflows, and the rename\n // moved the directory from `.windsurf/` to `.devin/`. New files go to the new\n // directory; a repo still holding the old one is still a Devin repo, so both\n // are sightings.\n devin: {\n path: (name) => [\".devin\", \"workflows\", `${name}.md`],\n format: namedMd,\n skill: skillsIn(\".devin\"),\n detect: [\".devin\", \".windsurf\"],\n },\n factory: {\n path: (name) => [\".factory\", \"commands\", `${name}.md`],\n format: hintedMd,\n skill: skillsIn(\".factory\"),\n detect: [\".factory\"],\n },\n junie: {\n path: (name) => [\".junie\", \"commands\", `${name}.md`],\n format: describedMd,\n skill: skillsIn(\".junie\"),\n detect: [\".junie\"],\n },\n // Kilo Code workflows carry no frontmatter — the title line is all the\n // wrapper there is.\n kilocode: {\n path: (name) => [\".kilocode\", \"workflows\", `${name}.md`],\n format: titledMd,\n skill: skillsIn(\".kilocode\"),\n detect: [\".kilocode\"],\n },\n // Kiro uses Copilot's `.prompt.md` extension under its own directory.\n kiro: {\n path: (name) => [\".kiro\", \"prompts\", `${name}.prompt.md`],\n format: describedMd,\n skill: skillsIn(\".kiro\"),\n detect: [\".kiro\"],\n },\n // Qwen Code retired its TOML commands in favour of markdown + frontmatter,\n // so this is markdown despite the Gemini lineage.\n qwen: {\n path: (name) => [\".qwen\", \"commands\", `${name}.md`],\n format: describedMd,\n skill: skillsIn(\".qwen\"),\n detect: [\".qwen\"],\n },\n // Roo/Zoo Code: id `roocode`, directory `.roo/`, no frontmatter.\n roocode: {\n path: (name) => [\".roo\", \"commands\", `${name}.md`],\n format: titledMd,\n skill: skillsIn(\".roo\"),\n detect: [\".roo\"],\n },\n trae: {\n path: (name) => [\".trae\", \"commands\", `${name}.md`],\n format: namedMd,\n skill: skillsIn(\".trae\"),\n detect: [\".trae\"],\n },\n};\n\n/**\n * The Claude-format files, keyed by command name. Kept as the exported shape\n * because it IS the on-disk contract of every repo initialized before\n * `--tools` existed — and derived through the same adapter that writes the\n * files, so the export and the disk can never disagree.\n */\nexport const SLASH_COMMANDS: Record<string, string> = Object.fromEntries(\n COMMANDS.map((c) => [c.name, AGENT_TOOLS[\"claude\"]!.format!(stamped(c))]),\n);\n\n/** The two ways a command body reaches an agent. Both are on by default. */\nexport const DELIVERIES = [\"commands\", \"skills\"] as const;\nexport type Delivery = (typeof DELIVERIES)[number];\n\n/**\n * Every file the selected tools would lay down, in creation order — the same\n * list `init` probes for `skipped` BEFORE the scaffold runs, so created +\n * skipped is the same list on every repo.\n *\n * Called with two arguments it returns EVERYTHING the binary would write under\n * default delivery: commands and skills both. That is the contract callers\n * outside this module depend on — `loam doctor` asks \"what should be here?\"\n * and a half-answer would report a fully-scaffolded repo as fully scaffolded\n * while ignoring every skill file. The name predates the second delivery and is\n * kept because it is imported elsewhere; `delivery` narrows it.\n */\nexport function plannedCommandFiles(\n cwd: string,\n toolIds: string[],\n delivery: readonly Delivery[] = DELIVERIES,\n): Array<{ path: string; content: string }> {\n return toolIds.flatMap((id) => {\n const tool = AGENT_TOOLS[id];\n if (tool === undefined) throw new Error(`unknown agent tool: ${id}`);\n const emitters: AgentFileEmitter[] = [];\n // A tool declares only the deliveries it actually reads, so an absent\n // emitter is a fact about the tool, never a reason to fall back to some\n // other tool's convention.\n const { path, format, skill } = tool;\n if (delivery.includes(\"commands\") && path !== undefined && format !== undefined) {\n emitters.push({ path, format });\n }\n if (delivery.includes(\"skills\") && skill !== undefined) emitters.push(skill);\n return emitters.flatMap((e) =>\n COMMANDS.map((c) => ({ path: join(cwd, ...e.path(c.name)), content: e.format(stamped(c)) })),\n );\n });\n}\n\n/**\n * Write the command and skill files for the selected tools into `cwd`. Existing\n * files are left alone — the never-overwrite contract covers every tool and\n * every delivery, not just the default one. Returns the paths created.\n */\nexport async function scaffoldAgentCommands(\n cwd: string,\n toolIds: string[] = [\"claude\"],\n delivery: readonly Delivery[] = DELIVERIES,\n): Promise<string[]> {\n const created: string[] = [];\n for (const { path, content } of plannedCommandFiles(cwd, toolIds, delivery)) {\n if (existsSync(path)) continue;\n await mkdir(dirname(path), { recursive: true });\n await writeFile(path, content, \"utf8\");\n created.push(path);\n }\n return created;\n}\n\n/**\n * The registry ids whose marker paths are present in `cwd` — what `loam init`\n * writes for when nobody passed `--tools`.\n *\n * A repo with `.cursor/` and no Claude Code used to get `.claude/commands/`\n * and nothing it could actually run. Presence is the whole test, and existence\n * (not directory-ness) is what is asked, because a marker is as often a file as\n * a directory — `.github/copilot-instructions.md` is the honest signal for\n * Copilot where `.github/` itself is noise.\n */\nexport function detectAgentTools(cwd: string): string[] {\n return Object.entries(AGENT_TOOLS)\n .filter(([, tool]) => tool.detect.some((marker) => existsSync(join(cwd, marker))))\n .map(([id]) => id);\n}\n"]}
@@ -0,0 +1,68 @@
1
+ /** The stamp `loam init` writes at the top of AGENTS.md. */
2
+ export function agentsStampLine(version) {
3
+ return `<!-- generated by loam v${version} -->`;
4
+ }
5
+ /** Matches a stamp line anywhere in the file; the capture is the version text. */
6
+ const STAMP_RE = /<!--\s*generated by loam v(\S+?)\s*-->/;
7
+ /**
8
+ * The version the stamp names, or null when there is no stamp (or one whose
9
+ * version does not read as numbers — unparseable is indistinguishable from
10
+ * absent, and both get the same advice).
11
+ */
12
+ export function agentsStampVersion(text) {
13
+ const m = STAMP_RE.exec(text);
14
+ return m !== null && parseVersion(m[1]) !== null ? m[1] : null;
15
+ }
16
+ /** The leading numeric triple of `X.Y.Z[-anything]`, or null. */
17
+ function parseVersion(v) {
18
+ const m = /^(\d+)\.(\d+)\.(\d+)(?:[-+]|$)/.exec(v);
19
+ return m === null ? null : [Number(m[1]), Number(m[2]), Number(m[3])];
20
+ }
21
+ /**
22
+ * Does `stamp` name an older version than `binary`? Numeric triples only — a
23
+ * prerelease suffix is ignored, so `1.2.0-beta` neither trails nor outranks
24
+ * `1.2.0`. A stamp NEWER than the binary does not trail either: that is an old
25
+ * binary, not a stale file, and the fix (upgrade loam) is not an AGENTS.md edit.
26
+ */
27
+ export function versionTrails(stamp, binary) {
28
+ const s = parseVersion(stamp);
29
+ const b = parseVersion(binary);
30
+ if (s === null || b === null)
31
+ return false;
32
+ for (let i = 0; i < 3; i += 1) {
33
+ if (s[i] !== b[i])
34
+ return s[i] < b[i];
35
+ }
36
+ return false;
37
+ }
38
+ /**
39
+ * The one finding `loam validate --all` owes the agent contract. `text` is the
40
+ * docs repo's AGENTS.md, or null when there is none — no file means no contract
41
+ * to have drifted, and this check stays silent rather than inventing a second
42
+ * meaning for its code. Returns null when the stamp is current (or newer).
43
+ */
44
+ export function agentsStaleFinding(text, binary) {
45
+ if (text === null)
46
+ return null;
47
+ const stamp = agentsStampVersion(text);
48
+ if (stamp === null) {
49
+ return {
50
+ severity: "warn",
51
+ code: "agents.stale",
52
+ message: "AGENTS.md has no version stamp — it predates stamping or was hand-created, so nothing says " +
53
+ "which loam its flag and code tables describe. Review it against the current `loam --help`, " +
54
+ `then add \`${agentsStampLine(binary)}\` near the top; a hand-curated file keeps the warning ` +
55
+ "off the same way, by keeping the stamp current.",
56
+ };
57
+ }
58
+ if (!versionTrails(stamp, binary))
59
+ return null;
60
+ return {
61
+ severity: "warn",
62
+ code: "agents.stale",
63
+ message: `AGENTS.md was written by loam v${stamp}; this binary is v${binary} — the flag and code tables ` +
64
+ "agents branch on may have moved. Review the file against the current `loam --help`, then " +
65
+ `update the stamp line to v${binary} (a hand-curated file silences this by bumping the stamp itself).`,
66
+ };
67
+ }
68
+ //# sourceMappingURL=agents-stamp.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agents-stamp.js","sourceRoot":"","sources":["../../src/core/agents-stamp.ts"],"names":[],"mappings":"AAgBA,4DAA4D;AAC5D,MAAM,UAAU,eAAe,CAAC,OAAe;IAC7C,OAAO,2BAA2B,OAAO,MAAM,CAAC;AAClD,CAAC;AAED,kFAAkF;AAClF,MAAM,QAAQ,GAAG,wCAAwC,CAAC;AAE1D;;;;GAIG;AACH,MAAM,UAAU,kBAAkB,CAAC,IAAY;IAC7C,MAAM,CAAC,GAAG,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC9B,OAAO,CAAC,KAAK,IAAI,IAAI,YAAY,CAAC,CAAC,CAAC,CAAC,CAAE,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAE,CAAC,CAAC,CAAC,IAAI,CAAC;AACnE,CAAC;AAED,iEAAiE;AACjE,SAAS,YAAY,CAAC,CAAS;IAC7B,MAAM,CAAC,GAAG,gCAAgC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACnD,OAAO,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACxE,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,aAAa,CAAC,KAAa,EAAE,MAAc;IACzD,MAAM,CAAC,GAAG,YAAY,CAAC,KAAK,CAAC,CAAC;IAC9B,MAAM,CAAC,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC;IAC/B,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IAC3C,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;QAC9B,IAAI,CAAC,CAAC,CAAC,CAAE,KAAK,CAAC,CAAC,CAAC,CAAE;YAAE,OAAO,CAAC,CAAC,CAAC,CAAE,GAAG,CAAC,CAAC,CAAC,CAAE,CAAC;IAC5C,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAAC,IAAmB,EAAE,MAAc;IACpE,IAAI,IAAI,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAC/B,MAAM,KAAK,GAAG,kBAAkB,CAAC,IAAI,CAAC,CAAC;IACvC,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;QACnB,OAAO;YACL,QAAQ,EAAE,MAAM;YAChB,IAAI,EAAE,cAAc;YACpB,OAAO,EACL,6FAA6F;gBAC7F,6FAA6F;gBAC7F,cAAc,eAAe,CAAC,MAAM,CAAC,yDAAyD;gBAC9F,iDAAiD;SACpD,CAAC;IACJ,CAAC;IACD,IAAI,CAAC,aAAa,CAAC,KAAK,EAAE,MAAM,CAAC;QAAE,OAAO,IAAI,CAAC;IAC/C,OAAO;QACL,QAAQ,EAAE,MAAM;QAChB,IAAI,EAAE,cAAc;QACpB,OAAO,EACL,kCAAkC,KAAK,qBAAqB,MAAM,8BAA8B;YAChG,2FAA2F;YAC3F,6BAA6B,MAAM,mEAAmE;KACzG,CAAC;AACJ,CAAC","sourcesContent":["/**\n * The AGENTS.md version stamp: detection, never refresh.\n *\n * AGENTS.md is written once by `loam init` and never touched again — that is\n * the ownership contract (agent.ts): it is a starting point, and a team's edits\n * to it outrank ours. But the file tells agents to branch on flag and code\n * tables, and those tables describe the binary that wrote them. OpenSpec hit\n * the same wall and answered with auto-refresh, which teaches everyone the file\n * belongs to the tool after all; loam answers with the smallest honest thing —\n * a version stamp in the generated file, and one warning when the stamp is\n * missing or trails the running binary. Reviewing the file and bumping the\n * stamp is a human act, and a hand-curated file silences the warning the same\n * way: by saying, in the stamp, that someone has looked.\n */\nimport type { Finding } from \"./report.js\";\n\n/** The stamp `loam init` writes at the top of AGENTS.md. */\nexport function agentsStampLine(version: string): string {\n return `<!-- generated by loam v${version} -->`;\n}\n\n/** Matches a stamp line anywhere in the file; the capture is the version text. */\nconst STAMP_RE = /<!--\\s*generated by loam v(\\S+?)\\s*-->/;\n\n/**\n * The version the stamp names, or null when there is no stamp (or one whose\n * version does not read as numbers — unparseable is indistinguishable from\n * absent, and both get the same advice).\n */\nexport function agentsStampVersion(text: string): string | null {\n const m = STAMP_RE.exec(text);\n return m !== null && parseVersion(m[1]!) !== null ? m[1]! : null;\n}\n\n/** The leading numeric triple of `X.Y.Z[-anything]`, or null. */\nfunction parseVersion(v: string): [number, number, number] | null {\n const m = /^(\\d+)\\.(\\d+)\\.(\\d+)(?:[-+]|$)/.exec(v);\n return m === null ? null : [Number(m[1]), Number(m[2]), Number(m[3])];\n}\n\n/**\n * Does `stamp` name an older version than `binary`? Numeric triples only — a\n * prerelease suffix is ignored, so `1.2.0-beta` neither trails nor outranks\n * `1.2.0`. A stamp NEWER than the binary does not trail either: that is an old\n * binary, not a stale file, and the fix (upgrade loam) is not an AGENTS.md edit.\n */\nexport function versionTrails(stamp: string, binary: string): boolean {\n const s = parseVersion(stamp);\n const b = parseVersion(binary);\n if (s === null || b === null) return false;\n for (let i = 0; i < 3; i += 1) {\n if (s[i]! !== b[i]!) return s[i]! < b[i]!;\n }\n return false;\n}\n\n/**\n * The one finding `loam validate --all` owes the agent contract. `text` is the\n * docs repo's AGENTS.md, or null when there is none — no file means no contract\n * to have drifted, and this check stays silent rather than inventing a second\n * meaning for its code. Returns null when the stamp is current (or newer).\n */\nexport function agentsStaleFinding(text: string | null, binary: string): Finding | null {\n if (text === null) return null;\n const stamp = agentsStampVersion(text);\n if (stamp === null) {\n return {\n severity: \"warn\",\n code: \"agents.stale\",\n message:\n \"AGENTS.md has no version stamp — it predates stamping or was hand-created, so nothing says \" +\n \"which loam its flag and code tables describe. Review it against the current `loam --help`, \" +\n `then add \\`${agentsStampLine(binary)}\\` near the top; a hand-curated file keeps the warning ` +\n \"off the same way, by keeping the stamp current.\",\n };\n }\n if (!versionTrails(stamp, binary)) return null;\n return {\n severity: \"warn\",\n code: \"agents.stale\",\n message:\n `AGENTS.md was written by loam v${stamp}; this binary is v${binary} — the flag and code tables ` +\n \"agents branch on may have moved. Review the file against the current `loam --help`, then \" +\n `update the stamp line to v${binary} (a hand-curated file silences this by bumping the stamp itself).`,\n };\n}\n"]}
@@ -0,0 +1,104 @@
1
+ /**
2
+ * The architecture spec axis — the grammar and resolution of `Covers:`.
3
+ *
4
+ * A business spec will never mention the transactional outbox; that is
5
+ * architecture, and it lives in `arch.spec.md` — same requirement/scenario
6
+ * grammar as spec.md, same delta algebra, same merge. What is new is the
7
+ * `Covers:` line: where a business requirement declares the OPERATIONS it
8
+ * governs, an architecture requirement declares the MODEL OBJECTS its
9
+ * scenarios exercise, so coverage can be derived mechanically instead of
10
+ * trusted to an author who was never going to write the outbox down.
11
+ *
12
+ * Three entry forms, resolved against the documents loam already reads:
13
+ *
14
+ * paymentService.db a C4 element id — resolved against the
15
+ * in-scope elements the way every check joins
16
+ * them: the id itself, or the service the
17
+ * element stands for (binding, then title);
18
+ * paymentService -> kafka an edge — each side resolved the same way,
19
+ * against the declared relationships;
20
+ * alert:<id> / sli:<id> a health signal — ids `health.yaml`
21
+ * declares (core/health.ts).
22
+ *
23
+ * Resolution never reads code. An entry that resolves to nothing is the typo
24
+ * guard (`covers.unknown`, warn); the emitters live with the other validate
25
+ * checks, this module owns only the grammar and the matching.
26
+ */
27
+ import { elementService, serviceOf } from "./likec4.js";
28
+ /** Parse one comma-separated `Covers:` entry into its form. Never fails: an
29
+ * unclassifiable string is an element entry that will not resolve, and the
30
+ * covers.unknown message is a better diagnosis than a parse refusal. */
31
+ export function parseCoversEntry(raw) {
32
+ const alert = /^alert:\s*(.+)$/.exec(raw);
33
+ if (alert)
34
+ return { form: "alert", id: alert[1].trim(), raw };
35
+ const sli = /^sli:\s*(.+)$/.exec(raw);
36
+ if (sli)
37
+ return { form: "sli", id: sli[1].trim(), raw };
38
+ const edge = /^(.+?)\s*->\s*(.+)$/.exec(raw);
39
+ if (edge)
40
+ return { form: "edge", source: edge[1].trim(), target: edge[2].trim(), raw };
41
+ return { form: "element", id: raw, raw };
42
+ }
43
+ /** Does an element entry name this element? Its id, or the service it stands for. */
44
+ function namesElement(name, e) {
45
+ return e.id === name || elementService(e) === name;
46
+ }
47
+ /** Does an edge entry's side name this relationship endpoint? */
48
+ function namesEndpoint(name, endpointId, elements) {
49
+ return endpointId === name || serviceOf(elements, endpointId) === name;
50
+ }
51
+ /** Does a Covers entry match this element? (Only element entries can.) */
52
+ export function coversElement(entry, e) {
53
+ return entry.form === "element" && namesElement(entry.id, e);
54
+ }
55
+ /** Does a Covers entry match this relationship? Endpoints resolved within `elements`. */
56
+ export function coversEdge(entry, r, elements) {
57
+ return (entry.form === "edge" &&
58
+ namesEndpoint(entry.source, r.source, elements) &&
59
+ namesEndpoint(entry.target, r.target, elements));
60
+ }
61
+ /** Does the entry resolve to ANYTHING in scope? False is `covers.unknown`. */
62
+ export function entryResolves(entry, scope) {
63
+ switch (entry.form) {
64
+ case "alert":
65
+ return scope.health.alerts.includes(entry.id);
66
+ case "sli":
67
+ return scope.health.slis.includes(entry.id);
68
+ case "edge":
69
+ return scope.relationships.some((r) => coversEdge(entry, r, scope.elements));
70
+ case "element":
71
+ return scope.elements.some((e) => coversElement(entry, e));
72
+ }
73
+ }
74
+ /** Ids a covers.unknown hint may offer for a mistyped entry — always real ones. */
75
+ export function coversCandidates(entry, scope) {
76
+ switch (entry.form) {
77
+ case "alert":
78
+ return closeIds(entry.id, scope.health.alerts).map((id) => `alert:${id}`);
79
+ case "sli":
80
+ return closeIds(entry.id, scope.health.slis).map((id) => `sli:${id}`);
81
+ case "edge":
82
+ // Cheap on purpose: no pairwise edge fuzzing — the sides are element
83
+ // names, so the element hint is the useful one.
84
+ return [];
85
+ case "element":
86
+ return closeIds(entry.id, [...new Set(scope.elements.map((e) => e.id))]);
87
+ }
88
+ }
89
+ /**
90
+ * Existing ids near a misspelling — substring containment either way, else a
91
+ * shared 3-character prefix. Deliberately dumb: no fuzzy library for one hint,
92
+ * and every id offered is real, so the hint can never point at the typo itself.
93
+ * (Moved here from validate.ts once `covers.unknown` became its second user.)
94
+ */
95
+ export function closeIds(typo, ids) {
96
+ const t = typo.toLowerCase();
97
+ return ids
98
+ .filter((id) => {
99
+ const i = id.toLowerCase();
100
+ return i.includes(t) || t.includes(i) || (t.length >= 3 && i.startsWith(t.slice(0, 3)));
101
+ })
102
+ .slice(0, 5);
103
+ }
104
+ //# sourceMappingURL=arch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"arch.js","sourceRoot":"","sources":["../../src/core/arch.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The architecture spec axis — the grammar and resolution of `Covers:`.\n *\n * A business spec will never mention the transactional outbox; that is\n * architecture, and it lives in `arch.spec.md` — same requirement/scenario\n * grammar as spec.md, same delta algebra, same merge. What is new is the\n * `Covers:` line: where a business requirement declares the OPERATIONS it\n * governs, an architecture requirement declares the MODEL OBJECTS its\n * scenarios exercise, so coverage can be derived mechanically instead of\n * trusted to an author who was never going to write the outbox down.\n *\n * Three entry forms, resolved against the documents loam already reads:\n *\n * paymentService.db a C4 element id — resolved against the\n * in-scope elements the way every check joins\n * them: the id itself, or the service the\n * element stands for (binding, then title);\n * paymentService -> kafka an edge — each side resolved the same way,\n * against the declared relationships;\n * alert:<id> / sli:<id> a health signal — ids `health.yaml`\n * declares (core/health.ts).\n *\n * Resolution never reads code. An entry that resolves to nothing is the typo\n * guard (`covers.unknown`, warn); the emitters live with the other validate\n * checks, this module owns only the grammar and the matching.\n */\nimport { elementService, serviceOf, type Elem, type Rel } from \"./likec4.js\";\nimport type { HealthIds } from \"./health.js\";\n\nexport type CoversEntry =\n | { form: \"element\"; id: string; raw: string }\n | { form: \"edge\"; source: string; target: string; raw: string }\n | { form: \"alert\" | \"sli\"; id: string; raw: string };\n\n/** Parse one comma-separated `Covers:` entry into its form. Never fails: an\n * unclassifiable string is an element entry that will not resolve, and the\n * covers.unknown message is a better diagnosis than a parse refusal. */\nexport function parseCoversEntry(raw: string): CoversEntry {\n const alert = /^alert:\\s*(.+)$/.exec(raw);\n if (alert) return { form: \"alert\", id: alert[1]!.trim(), raw };\n const sli = /^sli:\\s*(.+)$/.exec(raw);\n if (sli) return { form: \"sli\", id: sli[1]!.trim(), raw };\n const edge = /^(.+?)\\s*->\\s*(.+)$/.exec(raw);\n if (edge) return { form: \"edge\", source: edge[1]!.trim(), target: edge[2]!.trim(), raw };\n return { form: \"element\", id: raw, raw };\n}\n\n/**\n * What `Covers:` entries resolve against: every element and relationship in\n * view (a service's own model plus the landscape; for a feature, the delta\n * plus both), and the health ids of the service whose spec is being read.\n */\nexport interface CoverageScope {\n elements: Elem[];\n relationships: Rel[];\n health: HealthIds;\n}\n\n/** Does an element entry name this element? Its id, or the service it stands for. */\nfunction namesElement(name: string, e: Elem): boolean {\n return e.id === name || elementService(e) === name;\n}\n\n/** Does an edge entry's side name this relationship endpoint? */\nfunction namesEndpoint(name: string, endpointId: string, elements: Elem[]): boolean {\n return endpointId === name || serviceOf(elements, endpointId) === name;\n}\n\n/** Does a Covers entry match this element? (Only element entries can.) */\nexport function coversElement(entry: CoversEntry, e: Elem): boolean {\n return entry.form === \"element\" && namesElement(entry.id, e);\n}\n\n/** Does a Covers entry match this relationship? Endpoints resolved within `elements`. */\nexport function coversEdge(entry: CoversEntry, r: Rel, elements: Elem[]): boolean {\n return (\n entry.form === \"edge\" &&\n namesEndpoint(entry.source, r.source, elements) &&\n namesEndpoint(entry.target, r.target, elements)\n );\n}\n\n/** Does the entry resolve to ANYTHING in scope? False is `covers.unknown`. */\nexport function entryResolves(entry: CoversEntry, scope: CoverageScope): boolean {\n switch (entry.form) {\n case \"alert\":\n return scope.health.alerts.includes(entry.id);\n case \"sli\":\n return scope.health.slis.includes(entry.id);\n case \"edge\":\n return scope.relationships.some((r) => coversEdge(entry, r, scope.elements));\n case \"element\":\n return scope.elements.some((e) => coversElement(entry, e));\n }\n}\n\n/** Ids a covers.unknown hint may offer for a mistyped entry — always real ones. */\nexport function coversCandidates(entry: CoversEntry, scope: CoverageScope): string[] {\n switch (entry.form) {\n case \"alert\":\n return closeIds(entry.id, scope.health.alerts).map((id) => `alert:${id}`);\n case \"sli\":\n return closeIds(entry.id, scope.health.slis).map((id) => `sli:${id}`);\n case \"edge\":\n // Cheap on purpose: no pairwise edge fuzzing — the sides are element\n // names, so the element hint is the useful one.\n return [];\n case \"element\":\n return closeIds(entry.id, [...new Set(scope.elements.map((e) => e.id))]);\n }\n}\n\n/**\n * Existing ids near a misspelling — substring containment either way, else a\n * shared 3-character prefix. Deliberately dumb: no fuzzy library for one hint,\n * and every id offered is real, so the hint can never point at the typo itself.\n * (Moved here from validate.ts once `covers.unknown` became its second user.)\n */\nexport function closeIds(typo: string, ids: string[]): string[] {\n const t = typo.toLowerCase();\n return ids\n .filter((id) => {\n const i = id.toLowerCase();\n return i.includes(t) || t.includes(i) || (t.length >= 3 && i.startsWith(t.slice(0, 3)));\n })\n .slice(0, 5);\n}\n"]}