@ybotok/loam 0.1.0-beta.1 → 0.1.0-beta.3

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 (120) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/README.md +6 -1
  3. package/SCHEMA.md +22 -4
  4. package/dist/cli.js +4 -0
  5. package/dist/cli.js.map +1 -1
  6. package/dist/commands/adopt.js +17 -3
  7. package/dist/commands/adopt.js.map +1 -1
  8. package/dist/commands/archive.js +24 -8
  9. package/dist/commands/archive.js.map +1 -1
  10. package/dist/commands/delta.js +74 -47
  11. package/dist/commands/delta.js.map +1 -1
  12. package/dist/commands/dependencies.js +7 -2
  13. package/dist/commands/dependencies.js.map +1 -1
  14. package/dist/commands/docs-repo-gate.js +104 -0
  15. package/dist/commands/docs-repo-gate.js.map +1 -0
  16. package/dist/commands/explore.js +195 -0
  17. package/dist/commands/explore.js.map +1 -0
  18. package/dist/commands/format.js +17 -0
  19. package/dist/commands/format.js.map +1 -0
  20. package/dist/commands/gherkin.js +87 -48
  21. package/dist/commands/gherkin.js.map +1 -1
  22. package/dist/commands/init.js +3 -3
  23. package/dist/commands/init.js.map +1 -1
  24. package/dist/commands/instructions.js +80 -0
  25. package/dist/commands/instructions.js.map +1 -0
  26. package/dist/commands/list.js +10 -76
  27. package/dist/commands/list.js.map +1 -1
  28. package/dist/commands/migrate-openspec.js +45 -28
  29. package/dist/commands/migrate-openspec.js.map +1 -1
  30. package/dist/commands/new.js +35 -34
  31. package/dist/commands/new.js.map +1 -1
  32. package/dist/commands/rebase.js +2 -4
  33. package/dist/commands/rebase.js.map +1 -1
  34. package/dist/commands/show.js +26 -31
  35. package/dist/commands/show.js.map +1 -1
  36. package/dist/commands/status.js +17 -25
  37. package/dist/commands/status.js.map +1 -1
  38. package/dist/commands/unarchive.js +12 -5
  39. package/dist/commands/unarchive.js.map +1 -1
  40. package/dist/commands/validate.js +306 -142
  41. package/dist/commands/validate.js.map +1 -1
  42. package/dist/commands/verify.js +33 -17
  43. package/dist/commands/verify.js.map +1 -1
  44. package/dist/commands/vouch.js +156 -60
  45. package/dist/commands/vouch.js.map +1 -1
  46. package/dist/core/agent.js +392 -42
  47. package/dist/core/agent.js.map +1 -1
  48. package/dist/core/agents-stamp.js +47 -10
  49. package/dist/core/agents-stamp.js.map +1 -1
  50. package/dist/core/arch.js +28 -2
  51. package/dist/core/arch.js.map +1 -1
  52. package/dist/core/asyncapi.js +253 -0
  53. package/dist/core/asyncapi.js.map +1 -0
  54. package/dist/core/brief.js +28 -6
  55. package/dist/core/brief.js.map +1 -1
  56. package/dist/core/coherence.js +37 -13
  57. package/dist/core/coherence.js.map +1 -1
  58. package/dist/core/concurrency.js +51 -0
  59. package/dist/core/concurrency.js.map +1 -0
  60. package/dist/core/config.js +10 -2
  61. package/dist/core/config.js.map +1 -1
  62. package/dist/core/delta.js +21 -2
  63. package/dist/core/delta.js.map +1 -1
  64. package/dist/core/dependencies.js +46 -42
  65. package/dist/core/dependencies.js.map +1 -1
  66. package/dist/core/docs.js +65 -3
  67. package/dist/core/docs.js.map +1 -1
  68. package/dist/core/doctor.js +79 -27
  69. package/dist/core/doctor.js.map +1 -1
  70. package/dist/core/document-bytes.js +59 -0
  71. package/dist/core/document-bytes.js.map +1 -0
  72. package/dist/core/explore.js +225 -0
  73. package/dist/core/explore.js.map +1 -0
  74. package/dist/core/fleet-context.js +29 -63
  75. package/dist/core/fleet-context.js.map +1 -1
  76. package/dist/core/frontmatter.js +1 -5
  77. package/dist/core/frontmatter.js.map +1 -1
  78. package/dist/core/gherkin.js +3 -61
  79. package/dist/core/gherkin.js.map +1 -1
  80. package/dist/core/health.js +1 -5
  81. package/dist/core/health.js.map +1 -1
  82. package/dist/core/ids.js +34 -5
  83. package/dist/core/ids.js.map +1 -1
  84. package/dist/core/json.js +8 -1
  85. package/dist/core/json.js.map +1 -1
  86. package/dist/core/likec4.js +19 -4
  87. package/dist/core/likec4.js.map +1 -1
  88. package/dist/core/maturity.js +66 -0
  89. package/dist/core/maturity.js.map +1 -0
  90. package/dist/core/openapi-merge.js +39 -6
  91. package/dist/core/openapi-merge.js.map +1 -1
  92. package/dist/core/openapi.js +17 -5
  93. package/dist/core/openapi.js.map +1 -1
  94. package/dist/core/openspec-inventory.js +64 -27
  95. package/dist/core/openspec-inventory.js.map +1 -1
  96. package/dist/core/path-safety.js +59 -0
  97. package/dist/core/path-safety.js.map +1 -1
  98. package/dist/core/provenance.js +102 -10
  99. package/dist/core/provenance.js.map +1 -1
  100. package/dist/core/records.js +33 -0
  101. package/dist/core/records.js.map +1 -0
  102. package/dist/core/repo.js +77 -1
  103. package/dist/core/repo.js.map +1 -1
  104. package/dist/core/report.js +9 -0
  105. package/dist/core/report.js.map +1 -1
  106. package/dist/core/results.js +1 -3
  107. package/dist/core/results.js.map +1 -1
  108. package/dist/core/spec.js +28 -4
  109. package/dist/core/spec.js.map +1 -1
  110. package/dist/core/staging.js +124 -17
  111. package/dist/core/staging.js.map +1 -1
  112. package/dist/core/status.js +152 -35
  113. package/dist/core/status.js.map +1 -1
  114. package/dist/core/steps.js +72 -0
  115. package/dist/core/steps.js.map +1 -0
  116. package/dist/core/verify.js +18 -4
  117. package/dist/core/verify.js.map +1 -1
  118. package/dist/core/version.js +17 -1
  119. package/dist/core/version.js.map +1 -1
  120. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,102 @@
2
2
 
3
3
  All notable project changes are recorded here. The format follows Keep a Changelog, and release versions follow Semantic Versioning.
4
4
 
5
+ ## [Unreleased]
6
+
7
+ ## [0.1.0-beta.3] - 2026-08-08
8
+
9
+ Still a prerelease under the `beta` dist-tag; the 5–10 service pilot has not run yet. Everything below came out of the first real fleet adoption — two Java Spring Boot services into a fresh docs repo — and every item was reproduced there, not inferred. One breaking change, in `loam vouch`; read that section before upgrading a CI job.
10
+
11
+ ### Changed — `loam vouch` now records a person, and refuses when it cannot
12
+
13
+ **This is a breaking change for any script or CI job that runs `loam vouch`: add `--yes`.**
14
+
15
+ `vouch` is the one command in loam whose output is a claim about a human act — everything else it checks is internal consistency, which fluent prose satisfies on its own. Its own docblock called it "the one command that records a person", and it recorded none: no identity of any kind, no interactive gate. Run twice, unattended, from a script, it stamped `status: verified` both times. Meanwhile every generated command and skill file pre-approved `Bash(loam:*)`, which handed the agent that wrote a draft the power to promote its own draft. That inverts the argument loam makes about test evidence everywhere else — an agent must not be able to *say* a scenario is tested — by letting one say a spec matches the code.
16
+
17
+ Three changes close it, and none of them is a signature:
18
+
19
+ - **`vouched_by` is stamped beside `status` and `last_verified`**, taken from git — `GIT_COMMITTER_*`/`GIT_AUTHOR_*` first and then `user.name`/`user.email`, which is git's own precedence, so a CI job and a laptop resolve it the same way. It is a name, not a proof: git config is a text file. What it buys is that a reviewer can ask a *specific person* what they read, which is the question `status: verified` was silently answering with nobody. It rides `--json` as `vouched_by`, and a re-vouch refreshes it.
20
+ - **`--yes`, and a confirmation when there is a terminal.** Without `--yes`, `vouch` refuses `vouch-unattended` when stdin is not a TTY, and in `--json` mode regardless (a question cannot be asked on a stream whose contract is one JSON document). On a terminal it states what is about to be claimed and takes only an explicit yes; `vouch-declined` is the answer when the person says no. `vouch-unattributable` is the refusal when git can name nobody at all — better than stamping an anonymous claim.
21
+ - **The generated `allowed-tools` allowlist names loam's verbs one by one** — `Bash(loam adopt:*), Bash(loam validate:*), …` — and deliberately not `vouch`. A verb missing from the list is not forbidden, only unapproved: the agent asks, and a person answers. That is the correct cost for this one, and it is what makes the refusal something a human sees rather than something an agent routes around.
22
+
23
+ New error codes: `vouch-unattended`, `vouch-unattributable`, `vouch-declined`. Existing specs carrying `status: verified` with no `vouched_by` are not re-graded — nothing new fires on them — but the next vouch stamps a name.
24
+
25
+ ### Fixed — a docs repo that could be cloned, and rendered
26
+
27
+ - **`loam init --create` writes `services/.gitkeep` and `features/.gitkeep`.** Both directories were created empty; git tracks files, not directories, so after the first push neither existed for anyone who cloned. A missing `services/` is a *blocker* in `doctor`, so the second person to touch the repo got a red preflight on a repository the first person left green.
28
+ - **`loam init --create` writes `likec4.config.json`, and the docs repo is a loadable LikeC4 workspace again.** loam parses every `.likec4` file *alone*, so the landscape, each `services/<id>/model.likec4` and each `features/<FEAT>/delta.likec4` legitimately declares its own `specification { … }` block and re-declares the elements it names. LikeC4's own loader merges the whole tree into one model, so pointing `npx likec4 start` at the repo root — the command loam's own brief recommends — reported every one of those declarations as a duplicate: on loam's four-file `examples/docs`, 16 errors from the renderer and 0 from `loam validate --all`. The scaffolded config declares the root as one project scoped to `architecture/`; a service model or feature delta renders from its own directory (`npx likec4 start services/<id>`), which is what parsing them in isolation meant all along. `doctor` reports `doctor.likec4-config-missing` (warning) on a docs repo created before this, with the exact file to write in its `fix`.
29
+
30
+ ### Fixed — three commands that answered the wrong question
31
+
32
+ - **`loam status` now sees the repository it is standing in.** With `loam.json` naming a service the docs repo has no directory for — a freshly wired service repo, the most common repo there is — the fleet count made "nothing is in flight and every service is written down" vacuously true, and `next.fleet-clean` came out on top. `doctor` had the right answer (`doctor.service-unknown`) at the same moment, and the documented agent loop reads `status --json` and runs `next[0]`: an agent following it was sent to start a feature instead of adopting the service under its feet. The new step is **`next.adopt-bound`**, first in `next[]` (behind only `next.recover-commit`), and suppressed under an explicit `--service`, which is a question about a different service.
33
+ - **`doctor.service-unbound` is no longer raised inside the docs repo.** Having no service binding there is the correct state — the docs repo is the fleet, not any one service — and the `fix` it printed (`loam init --service <id>` here) would have made the repo wrong. `currentService` still reports `unbound`; only the advice is gone.
34
+ - **`loam instructions` checks the arguments it substitutes.** `loam instructions loam-adopt "$PWD"` rendered a protocol reading `services//Users/someone/work/svc/` — a page of confident instructions built around a value no loam command accepts. Each workflow now declares what its `$1`, `$2`, … stand for, and a service-id or feature-id placeholder is checked against the same grammar `loam adopt` and `loam new` use before anything is printed (`invalid-option`). An *unsupplied* placeholder is still left standing, and a free-text one (a feature title) is still free.
35
+
36
+ ### Fixed — `doctor.agent-files-stale` and `agents.stale` across a prerelease
37
+
38
+ `versionTrails` compared only the numeric triple, so `0.1.0-beta.1` neither trailed nor outranked `0.1.0-beta.2`. But beta.2 is precisely the release that changed the *form* of every generated file — embedded protocol text became a pointer at `loam instructions` — and its own changelog told readers to delete the files and re-run `loam init`. The one upgrade that most needed the warning was the one bump shape that could never raise it. Prerelease identifiers are now compared per semver: numerically where they are numbers, a prerelease behind its own final release, and a longer identifier list ahead of a prefix of itself. A stamp whose version is not valid semver still reads as no stamp at all, unchanged.
39
+
40
+ ### Added — the async contract axis (AsyncAPI 3), phase one
41
+
42
+ Kafka was already drawable in the fleet map and describable in prose, and that was all: a `paymentService -> kafka 'Publishes PaymentAuthorized'` edge read as fully documented while naming no contract anybody could check. The event axis gives it the treatment the HTTP axis has — an artifact, a join token, and findings — in a first slice that reads and grades but does not yet merge.
43
+
44
+ - **`services/<svc>/asyncapi.yaml`** — the async contract, sibling of `openapi.yaml`, optional and adopt-seeded. **AsyncAPI 3.0 only**: its operations are named top-level objects carrying `action: send|receive`, which is the exact analog of an `operationId`, where 2.x spells the same thing as `publish`/`subscribe` under a channel and leaves whose perspective they take ambiguous. A 2.x document declares no `operations` and reads as a contract with no messages rather than as a mis-parsed one. No new runtime dependency: the document is read with the `yaml` parser already present, by the same shallow structural walk `core/openapi.ts` uses.
45
+ - **The join token is a message name**, spelled three times and required to be identical: a landscape edge's `metadata { publishes '...' }` / `metadata { consumes '...' }`, a requirement's `Publishes:` / `Consumes:` line, and the message's `name` in `asyncapi.yaml` (its declaration key when it has no `name`).
46
+ - **loam never reads inside a message's `payload`.** It joins on names and on the `action` of the operation carrying them, and nothing else. Write payloads as JSON Schema; a fleet that later adopts Avro changes a `schemaFormat` line in its documents and nothing in loam. Keep payloads inside the document rather than `$ref`-ing an external `.avsc` — external references are out of scope here exactly as on the OpenAPI axis.
47
+ - **New finding codes**, all from `loam validate --service <id>`: `service.no-asyncapi`, `asyncapi.invalid`, `asyncapi.duplicate-message`, `spine.message-undefined`, `spec-event.message-undefined`, `spine.message-unproduced`, `asyncapi.message-contested`, `event.messages-unlinked`, and the `ok` confirmation `event.covered`. No existing code changed, and no refusal that used to succeed now fails: every one of these fires only on a file or a `metadata` key that did not exist before this release.
48
+ - **`spine.message-unproduced` is the check with no HTTP analog**, and it is why the axis exists. On the API axis the provider owns the contract, so "does this operation exist" is answered inside one service's directory. An event's schema lives in the *producer's* repository, so "does anybody publish what I consume" is a fleet question — and a consumer joined to nothing was previously indistinguishable from a healthy one. `asyncapi.message-contested` is its other half: two services claiming to send one name means every consumer's join picks one arbitrarily.
49
+ - **`service.no-asyncapi` has two grades, not three.** Its `service.no-openapi` sibling warns when the landscape cannot prove nobody calls a service, because most services expose HTTP. An event contract is genuinely optional and most services touch no topic, so the absent file is an **error** when something already joins into it — the stranded message names ride in `details` — and **silence** otherwise.
50
+ - `asyncapi.yaml` presence rides in `loam list`/`loam show` (`has.asyncapi`, and a lowercase `e` in the text flags, beside the arch spec's `a`). It is deliberately **not** part of the maturity ladder: making it a fourth required artifact would demote every already-`documented` service in the fleet without one byte of their files changing.
51
+
52
+ **Not in this slice, and stated so nobody reads the absence as a bug:** a feature cannot yet add a message. There is no `features/<FEAT>/specs/<svc>/asyncapi.yaml`, no merge, no baseline pin, and no `verify` claim on this axis — a message is retired, for now, by editing the living contract in a reviewed PR. Deprecation markers and a removal family are deliberately absent rather than pending: an async contract that only ever grows has nothing to mark, because a consumer reading a topic with lag breaks on a removed field regardless of what the document says about it.
53
+
54
+ ## [0.1.0-beta.2] - 2026-08-07
55
+
56
+ Still a prerelease, still under the `beta` dist-tag: the 5–10 service pilot has not run yet, and this release changes what `loam init` leaves in a repository, which is exactly the kind of thing a pilot is for. Two new commands, that change to the generated files, and a code-quality pass over `src/` with the defects it turned up. The refactoring half is invisible by construction; what follows is only the part a user can observe.
57
+
58
+ ### Added
59
+
60
+ - **`loam explore [<service>...]`** — read the fleet around a change nobody has written down yet, and write nothing. It answers the one question `loam new` takes as an argument: which services a feature touches. For each seed it reports the ring one hop out in the fleet map, each service's maturity rung and living operations, who already calls whom, the active features already covering the same services, and the literal `loam new` line the seeds imply. `--op <operationId>` seeds from an operation when you know the call but not who owns it; `--as <FEAT>` names the feature in the suggested line. A seed naming no `services/<id>/` is reported with its near-misses rather than refused — a feature may be introducing that service, and a typo looks identical until you read the list. The neighbours are deliberately **not** folded into the suggested command: loam knows those services are connected, not whether you change them.
61
+ - **`loam instructions [<workflow>] [args...]`** — print one of the six workflow protocols (`loam-adopt`, `loam-feature`, `loam-implement`, `loam-check`, `loam-verify`, `loam-ship`) with `$1`, `$2` filled in from the arguments given. With no argument it lists them. It is the only command that reads no `loam.json` and no docs repo, deliberately: `loam-adopt`'s own first step is to run `loam init` when there is no config, so it cannot be the step that requires one.
62
+ - **[WORKFLOW.md](WORKFLOW.md)** — the working protocol as a document: the artifact graph and its five derived states, what actually gates and what only advises, the six workflows, how an agent drives the cycle from `--json`, and why there is no task-list artifact.
63
+
64
+ ### Changed — what `loam init` writes into a command or skill file
65
+
66
+ - **A generated file is now a pointer at `loam instructions`, not a copy of the protocol.** It carries the workflow's purpose and its spine — the verbs, in order — and defers this release's flags, finding codes and fix tables to the binary; a fresh `.claude/commands/` drops from about 53 KB to about 11 KB, and the finding-code tables move with it. The protocol itself is unchanged and complete; only its delivery moved. The reason is that the two go stale in opposite directions and only one is fixable: loam never regenerates a generated file (your edits outrank the template, and that is not changing), so a protocol copied into a repository will eventually describe a different loam with total confidence, while a pointer cannot. `doctor.agent-files-stale` still reports a file whose version stamp has fallen behind, and now has almost nothing to be wrong about.
67
+ - **Nothing rewrites an existing file.** A repository scaffolded by an earlier loam keeps exactly the files it was given, full protocol text and all — re-running `loam init` over it leaves them byte-unchanged. The version stamp is how you tell, and `loam doctor` raises `doctor.agent-files-stale` against them only once this binary's stamp is ahead of theirs. To take the new form, delete the files and re-run `loam init`.
68
+ - Both new commands reuse the existing refusal codes (`invalid-option`, `unknown-target`). No stable code was added, changed or removed.
69
+ - **The feature-id grammar has one home.** It was spelled privately in `loam new` and again in the OpenSpec migration; `core/ids.ts` owns it now, so an id one command accepts cannot be an id the other refuses. The only user-visible edge is wording: `migrate-openspec`'s `mapping.feature-id-invalid` message now ends with the same "e.g. FEAT-101 or BUG-42" example every other refusal gives. The code is unchanged.
70
+
71
+ ### Fixed — commands that wrote or attested on evidence they had not checked
72
+
73
+ - **`archive` no longer installs an unreadable feature contract as the living one.** A feature `openapi.yaml` that does not parse as an OpenAPI document was planned verbatim into `services/<svc>/openapi.yaml` and reported `ok: true`; every other command reads `readOpenapi`'s `unreadable` flag, and the one that writes was the one that did not. It now refuses with `merge-failed`, naming the file. The same code now answers when the contract is being created rather than merged — that case used to escape as `internal`.
74
+ - **`verify --record` no longer attests when it could not check the report.** The committed-report binding check tested for git's exit code 1 and nothing else, so a `git diff` killed by a signal — a CI timeout, the OOM killer, a fork failure — skipped the check and wrote the attestation anyway. A run that cannot reach an exit status is now a refusal that says git could not be run to completion.
75
+ - **An interrupted `archive` can be retried on a docs repo composed of symlinks.** Checking whether the snapshot was stale resolved the manifest's paths through a symlink test meant for writes, so a `services/<svc>/` mounted from a sibling checkout — a layout every walk of the repo follows on purpose — turned every retry into a permanent refusal. That check only reads a file and compares a digest; containment is now judged without resolving the mount. `unarchive`, which writes the pre-image back, deliberately keeps the stricter test.
76
+ - **A malformed `.loam-before/manifest.json` or `.loam-lock` is answered, not assumed.** Both were read through a cast: well-formed JSON that was not an object slipped past as "a different loam", and a bare `null` threw a `TypeError` out of the refusal itself. They now answer `commit-interrupted` and `docs-busy` respectively — the codes that case always deserved.
77
+ - **A `spec.md` whose bytes are UTF-16LE is refused on the write path.** The write path's UTF-8 guard was a weaker copy of the read path's and missed exactly the file the read path had been hardened against: such a document parsed as zero requirements, so a merge computed over an empty baseline and wrote that back.
78
+
79
+ ### Fixed — user errors reported as `internal`
80
+
81
+ - **`list`, `show` and `validate` report `repository-unavailable`** where a filesystem failure carries no path — a directory where `spec.md` belongs reports EISDIR with no `path`, which defeated the guard. `status` had this fix; the other three copies of the same catch block did not.
82
+ - **`loam.json` that cannot be read reports `config-invalid`.** The read sat one line above its own `try`, so a config file that was a directory, or carried a restrictive permission bit, crashed every command in the CLI.
83
+ - **`loam new` refuses instead of scaffolding into a directory that is not a docs repo**, and **`loam adopt` refuses (`docs-missing`)** instead of briefing an agent to write a baseline into a docs repo that does not exist. Both previously exited 0.
84
+
85
+ ### Fixed — checks that answered differently depending on where they were asked
86
+
87
+ - **A relocated operation grades the same under `validate`, `status` and `archive`.** `FleetContext` carried a second implementation of `serviceOperationIds` that interleaved removals with upserts, so an operation removed at one path and defined at another answered "undefined" or "defined" depending on which the author spelled first — and `spec-api.op-undefined` is a gating error.
88
+ - **`c4.op-ungoverned` joins per service.** Operation ids are unique only within a contract, so an operation governed on one service suppressed the warning for an unrelated service that shared its name.
89
+ - **`api.covered` no longer counts a REMOVED requirement as governing.** An operation whose only governing requirement was removed now reports `api.ungoverned` (a warning; nothing about gating or exit codes changes).
90
+ - **`loam status` (fleet form) reports `blocked` for a repo holding an interrupted commit journal**, agreeing with the per-feature form, which already did. It previously said `done` and "ship it" over a tree the other form called blocked.
91
+ - **`loam delta` exits 1 when the feature's `openapi.yaml` does not parse**, matching the rule the architecture axis already had, and its `--json` payload carries an `openapi` key saying so. An unparseable contract used to project as "no operations".
92
+
93
+ ### Fixed — deletion that reached outside the repository
94
+
95
+ - **`loam gherkin` orphan cleanup no longer follows a symlinked directory out of the repo.** Enumeration follows symlinks by design, so a planted `<gherkinDir>/loam/sub -> /outside` produced orphan paths that `unlink` resolved and deleted elsewhere. A symlinked `.feature` sitting directly in `loam/` is still removed — the link goes, its target survives — which is the distinction the guard now draws.
96
+
97
+ ### Changed — internal structure
98
+
99
+ No behaviour depends on this, and it is recorded because the next contributor will notice it: the eight runtime import cycles in `src/core/` are gone, five helper modules were extracted from the modules that happened to hold them (`document-bytes`, `records`, `steps`, `concurrency`, and the docs-repo gate), and the duplicated spellings of six shared rules now have one home each. `docs/CODE-STYLE.md` records the conventions and the defect behind each one.
100
+
5
101
  ## [0.1.0-beta.1] - 2026-08-06
6
102
 
7
103
  The first published release, and a prerelease on purpose: it ships under the `beta` dist-tag, not `latest`. This entry covers the pre-publication hardening pass. Most of it is not new capability — it is the difference between a command that reports a problem and one that was quietly blind to it, and in four places it is the difference between an undo that works and one that certifies text nobody wrote. Grouped by what a user would notice.
package/README.md CHANGED
@@ -141,6 +141,8 @@ The same split runs the done-check. `verify` cannot compare a generated model to
141
141
  | `loam status [<FEAT>]` | Where the work stands and what to do next, as a projection over the two gates. Derived every run; writes nothing. |
142
142
  | `loam doctor` | Read-only preflight for runtime, config, docs-repo access, fleet roots, counts, and the current service binding. Blockers exit 1; incomplete optional bindings stay warnings. |
143
143
  | `loam dependencies [<FEAT>]` | Derive the active-feature dependency graph and same-identity conflicts from requirement deltas and OpenAPI operationIds. Optional focus includes transitive prerequisites. |
144
+ | `loam explore [<service>...]` | Read the fleet around a change nobody has written down yet: the ring one hop out, each service's maturity and living operations, the features already in flight over the same ground, and the `loam new` line the seeds imply. `--op <id>` seeds from an operation. Writes nothing. |
145
+ | `loam instructions [<workflow>] [args...]` | Print one of the six workflow protocols, `$1`/`$2` filled in — version-matched to this binary, which is what the generated command and skill files point at. Reads no config. |
144
146
  | `loam new <FEAT> [--title <t>] [--touches <id>] [--new-service <id>]` | Scaffold a feature: intent, C4 delta, and a requirement delta for every service named by `--touches` / `--new-service` (both repeatable). The title is optional and becomes the directory slug. |
145
147
  | `loam show <service\|FEAT>` | Everything loam knows about one service or feature. |
146
148
  | `loam adopt --service <id>` | Brief an agent to write this service's baseline into the docs repo as `draft`. Writes nothing. |
@@ -191,7 +193,9 @@ API retirement is explicit and reviewable: a feature marks the exact path/method
191
193
 
192
194
  Which tools is detected from the repo: `init` scans each of the 20 tools it knows for its own marker paths — usually just its dot-directory, but Copilot's are the files *inside* `.github/`, because almost every repo has a `.github/` and almost none of them means Copilot. The twenty are Claude Code, Cursor, GitHub Copilot, Gemini CLI, opencode, Cline, Amazon Q, Antigravity, Auggie, Codex, Continue, Crush, Devin, Factory, Junie, Kilo Code, Kiro, Qwen, Roo Code and Trae. It writes for the ones it finds, and falls back to Claude Code when it finds none. `--tools <ids>|all` overrides the scan; an unknown id is refused, not skipped. What it wrote for is recorded in `loam.json` as `agentTools`, which is how `loam doctor` reports a command or skill file that a newer loam would lay down (`doctor.agent-files-missing`) without mistaking a tool nobody selected for one that fell behind — and a repo that asked for one delivery only (`--no-skills`, `--no-commands`) is not reported as missing the other.
193
195
 
194
- **Nothing is ever overwritten and nothing is ever refreshed**, and that is the whole difference from `openspec update`: your edits to a generated file outrank the template, so drift is reported and never repaired. To make that reportable at all, every generated command and skill body opens with `<!-- generated by loam vX.Y.Z -->`; `loam doctor` reads **only that line** and raises `doctor.agent-files-stale` for a file carrying no stamp or an older one. It never compares bodies — "your file differs from the template" is one step from offering to rewrite it. The fix is a human's: read the file, then bump the stamp, or delete it and re-run `loam init`. The AGENTS.md version stamp (`agents.stale`) works the same way. Running a tool loam has no adapter for? Point it at `AGENTS.md` — the commands are thin wrappers over the CLI, so any runner that can read a file and exec `loam` has everything.
196
+ **A generated file is a pointer, not a protocol.** It carries what does not move between releases — what the workflow is for, and the verbs in order — and defers this release's flags, finding codes and fix tables to `loam instructions <workflow>`, which ships inside the binary you are about to run. That split exists because the two go stale in opposite directions and only one of them is fixable: a file written once and never regenerated will eventually describe a different loam with total confidence, while the command cannot. `loam instructions` reads no `loam.json` and no docs repo, deliberately — `loam-adopt`'s own first step is to run `loam init` when there is no config, so it cannot be the step that requires one.
197
+
198
+ **Nothing is ever overwritten and nothing is ever refreshed**, and that is the whole difference from `openspec update`: your edits to a generated file outrank the template, so drift is reported and never repaired. To make that reportable at all, every generated command and skill body opens with `<!-- generated by loam vX.Y.Z -->`; `loam doctor` reads **only that line** and raises `doctor.agent-files-stale` for a file carrying no stamp or an older one. It never compares bodies — "your file differs from the template" is one step from offering to rewrite it. The fix is a human's: read the file, then bump the stamp, or delete it and re-run `loam init`. The AGENTS.md version stamp (`agents.stale`) works the same way. A repo scaffolded by an older loam keeps the files it was given, protocol text and all — nothing rewrites them, and the stamp is how you know. Running a tool loam has no adapter for? Point it at `AGENTS.md` — the commands are thin wrappers over the CLI, so any runner that can read a file and exec `loam` has everything.
195
199
 
196
200
  ## Fleet scale
197
201
 
@@ -201,6 +205,7 @@ The shape that has been measured, on a generated 120-service fleet with 400 and
201
205
 
202
206
  ## Docs
203
207
 
208
+ - [WORKFLOW.md](WORKFLOW.md) — the working protocol: the artifact graph and its derived states, what gates and what only advises, the six workflows, and how an agent drives the whole cycle from `--json`.
204
209
  - [SCHEMA.md](SCHEMA.md) — the docs-repo layout, each artifact's grammar, and the decisions behind them: the coherence rules, how `archive` writes and `unarchive` undoes, operating at fleet scale.
205
210
  - [COMPARISON.md](COMPARISON.md) — loam vs OpenSpec, honestly: seven axes, when OpenSpec is enough, what loam refuses to borrow.
206
211
  - [MIGRATING-from-OpenSpec.md](MIGRATING-from-OpenSpec.md) — moving an OpenSpec repo onto loam: what maps, what is lost, what must be added.
package/SCHEMA.md CHANGED
@@ -13,6 +13,7 @@ presentation │ UI page-prototypes ─(consume)─► endpoints spec = sour
13
13
  behavior │ gherkin scenarios ──(govern)──► pages source
14
14
  arch behavior│ arch.spec.md ──(covers)──► C4 · health source
15
15
  contract │ OpenAPI ◄──(detail of C4 "exposes/calls") hybrid
16
+ async │ AsyncAPI ◄──(detail of C4 "publishes/consumes") hybrid [no merge yet]
16
17
  structure │ C4 model — services, relationships source (adopt-seeded)
17
18
  ops / why │ ADR · runbook · health source
18
19
  truth │ code ground truth
@@ -25,6 +26,8 @@ All artifacts are wired by one **ID spine**: `FEAT-<id>`, service id, and C4 ele
25
26
  ```
26
27
  docs/
27
28
  AGENTS.md the process contract, travelling with the docs
29
+ loam.json {"docsDir": "."} — makes the repo self-describing [init --create]
30
+ likec4.config.json one LikeC4 project, scoped to architecture/ [init --create]
28
31
  architecture/
29
32
  landscape.likec4 global C4 (fleet landscape)
30
33
  landscape.health.yaml composed health model [derived, later]
@@ -33,6 +36,7 @@ docs/
33
36
  spec.md living requirements (Requirement/Scenario) [adopt]
34
37
  arch.spec.md living ARCHITECTURE requirements (outbox, retries, alerts; Covers:) [authored]
35
38
  openapi.yaml API contract [adopt / authored]
39
+ asyncapi.yaml async contract (AsyncAPI 3) [adopt / authored]
36
40
  adrs/NNNN-*.md MADR decisions [adopt-seed / authored]
37
41
  runbook.md operational runbook [adopt-draft / authored]
38
42
  health.yaml SLI/SLO, checks, critical deps [authored]
@@ -70,17 +74,17 @@ The docs repo is one repository; each service is another. Every one of them carr
70
74
 
71
75
  ## Conventions
72
76
 
73
- **Frontmatter**: `status`, `owner`, `service` or `feature`, `last_verified`, `sources` (literal files and directories), `sources_digest`, `content_digest`, `sources_files`. `loam validate` reads it in exactly three files — a service's living `spec.md`, its living `arch.spec.md` when present (same conventions, same checks, only the label differs), and a feature's `intent.md`: the documents whose identity and status everything else joins on. `adrs/` and `runbook.md` are presence-tracked only (`loam list` says whether they exist); their frontmatter is read by nothing today, so a field written there is a note to a future reader, not a claim any check will catch. `health.yaml`'s frontmatter is equally unread, but the file itself is not inert: `loam validate` reads its top-level `slis:`/`alerts:` ids for `Covers:` resolution and the `health.uncovered` warning (see the architecture spec axis below). For the two checked files:
77
+ **Frontmatter**: `status`, `owner`, `service` or `feature`, `last_verified`, `vouched_by`, `sources` (literal files and directories), `sources_digest`, `content_digest`, `sources_files`. `loam validate` reads it in exactly three files — a service's living `spec.md`, its living `arch.spec.md` when present (same conventions, same checks, only the label differs), and a feature's `intent.md`: the documents whose identity and status everything else joins on. `adrs/` and `runbook.md` are presence-tracked only (`loam list` says whether they exist); their frontmatter is read by nothing today, so a field written there is a note to a future reader, not a claim any check will catch. `health.yaml`'s frontmatter is equally unread, but the file itself is not inert: `loam validate` reads its top-level `slis:`/`alerts:` ids for `Covers:` resolution and the `health.uncovered` warning (see the architecture spec axis below). For the two checked files:
74
78
 
75
79
  - `status` — services: `draft` -> `verified`; features: `proposed` -> `in_progress` -> `built` -> `done`. An undocumented value is an **error**: a typo (`verifed`) would otherwise read as unverified forever.
76
80
  - a frontmatter block that does not parse as YAML is an **error** of its own (`frontmatter.malformed`), and the only finding that file's header produces: the fields are unreadable, not absent, and the cascade the silent parse used to trigger — "owner missing", "status missing", "names no sources" — sent authors adding lines to a block YAML refuses. The field checks and the sources chain resume once the header parses.
77
81
  - `service` / `feature` — must match the directory the file lives under. A mismatch is an **error**; absence is a warning.
78
82
  - `sources` — paths in the *service's own repo* the artifact was written from: literal files and directories only, a directory meaning everything beneath it. Resolved when `loam` runs inside that repo (`service` in `loam.json`); a path that no longer exists is an **error**, and so is a glob-looking entry (`*`, `?` or `[`) — patterns are not supported, and are refused loudly rather than matched, because a pattern dialect nobody can be sure of digests a different file set than its author intended, corrupting the trust signal silently. This is the only mechanical tie between the docs and the code: everything else loam checks is internal consistency, which a fluent fiction satisfies too.
79
- - `sources_digest` / `last_verified` — written by `loam vouch`, never by hand. The digest is a content hash of the files `sources` names, taken when a human last read them; `loam validate`, run in that repo, recomputes it and reports `sources.current`, `sources.stale` (the code moved since anyone looked), or `sources.unvouched` (`sources` with no digest — nobody has ever stamped it). All warnings: staleness is a signal, and only a person can say whether the doc is still right.
83
+ - `sources_digest` / `last_verified` / `vouched_by` — written by `loam vouch`, never by hand. `vouched_by` is git's identity for the repo the vouch ran in (`Name <email>`): a name, not a signature, and it proves nothing cryptographically — what it buys is that a reviewer can ask a *specific person* what they read, where `status: verified` alone recorded only that some process wrote the word. The digest is a content hash of the files `sources` names, taken when a human last read them; `loam validate`, run in that repo, recomputes it and reports `sources.current`, `sources.stale` (the code moved since anyone looked), or `sources.unvouched` (`sources` with no digest — nobody has ever stamped it). All warnings: staleness is a signal, and only a person can say whether the doc is still right.
80
84
  - `content_digest` — written by `loam vouch`, never by hand, like its siblings: a hash of the document's OWN body, so the stamp covers the words as well as the code. Editing a spec after it was vouched used to leave `status: verified` standing over prose nobody read — the exact forgery an agent-written corpus invites. `loam validate` recomputes it wherever the doc is readable; this check needs **no service repo**, so unlike the `sources.*` chain it runs for `--service` and `--all` alike, from the docs repo included. A `verified` spec whose current body hash mismatches gets `content.stale` (**warning**, the `sources.stale` grading: the doc changed since it was vouched, and only a person can say whether verified still holds). A verified doc with no `content_digest` predates the stamp and stays quiet — the fix is re-vouching.
81
85
  - Missing fields are warnings, and `loam list` reports the fleet's draft/verified/unmarked split.
82
86
 
83
- **Vouching** (`loam vouch --service <id>`, run in the service's own repo) is the human promotion `draft` -> `verified`: it stamps `status`, `last_verified`, `sources_digest`, `content_digest` and `sources_files` together — into the living `spec.md`, and into `arch.spec.md` beside it when the service has one — rewriting only the frontmatter. It refuses what it cannot verify — a frontmatter block that will not parse as YAML (its fields are unreadable, and a stamp would have to rewrite the header wholesale, losing the author's lines), no `sources`, a glob-pattern entry (not supported), a source path that is gone, sources expanding to no files at all, or a repo that is not that service's — checked per file, the refusal naming which, and all-or-nothing per service: one unverifiable file refuses the whole run, so a `verified` status always has a digest behind it and never means half the pair was stamped. The write keeps the same promise: the pair is committed through staged temp-file swaps (archive's machinery), so a failure between the two files rolls the first back (`merge-failed` — nothing stamped, re-run) instead of leaving half a stamp; only a rollback that itself fails leaves the repo to a human (`rollback-incomplete`, the message naming the files). And because the stamp is computed from bytes read a moment earlier, the swap re-checks them: a document that changed under the vouch — a concurrent vouch, a hand edit — refuses with `vouch-raced` and writes nothing, rather than stamping a digest of text that is no longer there.
87
+ **Vouching** (`loam vouch --service <id>`, run in the service's own repo) is the human promotion `draft` -> `verified`: it stamps `status`, `last_verified`, `vouched_by`, `sources_digest`, `content_digest` and `sources_files` together — into the living `spec.md`, and into `arch.spec.md` beside it when the service has one — rewriting only the frontmatter. It refuses what it cannot verify — a frontmatter block that will not parse as YAML (its fields are unreadable, and a stamp would have to rewrite the header wholesale, losing the author's lines), no `sources`, a glob-pattern entry (not supported), a source path that is gone, sources expanding to no files at all, or a repo that is not that service's — checked per file, the refusal naming which, and all-or-nothing per service: one unverifiable file refuses the whole run, so a `verified` status always has a digest behind it and never means half the pair was stamped. The write keeps the same promise: the pair is committed through staged temp-file swaps (archive's machinery), so a failure between the two files rolls the first back (`merge-failed` — nothing stamped, re-run) instead of leaving half a stamp; only a rollback that itself fails leaves the repo to a human (`rollback-incomplete`, the message naming the files). It also refuses to be run BY the thing it is meant to check: without `--yes` it requires a terminal and asks (`vouch-unattended` on a pipe or in `--json`, `vouch-declined` on anything but an explicit yes), and it refuses `vouch-unattributable` when git can name nobody at all. The command and skill files `loam init` writes pre-approve loam's other verbs one by one and deliberately not this one, so an agent that has finished a draft hands back and says a vouch is owed. And because the stamp is computed from bytes read a moment earlier, the swap re-checks them: a document that changed under the vouch — a concurrent vouch, a hand edit — refuses with `vouch-raced` and writes nothing, rather than stamping a digest of text that is no longer there.
84
88
 
85
89
  The digest recipe (stable, and part of the contract): expand `sources` to repo-relative file paths — a file is itself, a directory means everything beneath it, dot-entries are skipped while walking (though a path naming one outright is honoured) — sort them, hash each file's bytes with sha256, feed `<path>\0<sha256-hex>\n` per file into an outer sha256, and keep the first 16 hex characters.
86
90
 
@@ -129,6 +133,20 @@ Two `operationId`s that are the same in one document is a **warning** (`openapi.
129
133
 
130
134
  **Removal is an explicit feature delta.** To retire an operation after deprecation, put the exact living path and HTTP method in the feature's `specs/<svc>/openapi.yaml`, keep its `operationId`, and add `x-loam-remove: true` **inside that operation object**, beside its `operationId`. A marker one level up, at path level beside the methods, names no operation: it retires nothing, and it is not a contract key either. `loam archive` gates it (`openapi.remove-marker-path-level`, error, overridable with `--approve` like the other judgement gates) and the marker never reaches the living document under any flag — `x-loam-remove` and `x-loam-based-on` are stripped at path level and operation level alike, on every merge branch. A path item consisting of nothing but a marker publishes no phantom path. The same feature must carry a `REMOVED` requirement whose `Operations:` line names that id. Coherence gates an absent path+method, an id mismatch, an unjustified marker, a removed requirement with no marker, and a new tagged C4 edge consuming the operation being removed. `serviceOperationIds` evaluates the feature as living upserts minus removals, so other same-feature checks see the post-archive contract. Archive deletes only the matching method, reports removals in text and `openapiRemovals` JSON, never copies the marker into living OpenAPI, and deliberately does not garbage-collect components. A removal marker in a living `openapi.yaml` is `openapi.remove-marker-living` (error). The usual staged-write rollback and `unarchive` snapshot restore apply byte-for-byte.
131
135
 
136
+ ### The async contract axis (`asyncapi.yaml`)
137
+
138
+ A `paymentService -> kafka 'Publishes PaymentAuthorized'` edge used to read as fully documented while naming no contract anybody could check. The event axis is the API axis's mirror, in **AsyncAPI 3.0** — only 3.0, because its operations are named top-level objects carrying `action: send|receive`, the exact analog of an `operationId`, where 2.x spells the same thing as `publish`/`subscribe` under a channel and leaves whose perspective they take ambiguous. A 2.x document declares no `operations` and reads as a contract with no messages rather than as a mis-parsed one. Like OpenAPI it is read with a shallow structural walk over the `yaml` parse, never a real AsyncAPI parser: the slice loam joins on is small enough that the parser's model would be dead weight, and adding it would be a fourth runtime dependency.
139
+
140
+ **The join token is a message name**, spelled three times and required to be identical: a landscape edge's `metadata { publishes '...' }` / `metadata { consumes '...' }`, a requirement's `Publishes:` / `Consumes:` line (both requirement files — an outbox requirement in `arch.spec.md` is the canonical home of a `Publishes:` line, and the same keep-last quirk applies as to `Operations:`), and the message's `name` in `asyncapi.yaml`, or its declaration key when it has none.
141
+
142
+ **The payload is opaque, by decision.** loam reads message names and the `action` of the operation carrying them, and nothing inside `payload`. That is the whole property that keeps the payload format a documentation choice: a fleet writes JSON Schema today, and one that later adopts Avro changes a `schemaFormat` line and nothing in loam. Keep payloads in `components/schemas` in the same document rather than `$ref`-ing an external `.avsc` — external references are out of scope here exactly as on the OpenAPI axis, so a schema in another file will not travel with the message that needs it.
143
+
144
+ **The direction is not decoration, and the check inverts.** On the API axis the PROVIDER owns the contract, so "does this operation exist" is answered inside one service's directory. An event's schema lives in the PRODUCER's repository, so "does anybody publish what I consume" is a fleet question with no local answer — which is why `spine.message-unproduced` has no OpenAPI analog, and why a consumer joined to nothing was previously indistinguishable from a healthy one. Its other half is `asyncapi.message-contested`: two services declaring they send one name means every consumer's join picks one arbitrarily. That one fleet question is also the axis's only cost — a per-service run reads every service's `asyncapi.yaml`, which is N small YAML parses rather than N LikeC4 workspace spins, and it is paid only where something actually consumes.
145
+
146
+ **Absence has two grades, not three.** `service.no-openapi` warns when the landscape cannot prove nobody calls a service, because most services expose HTTP. An event contract is genuinely optional and most services touch no topic, so an absent `asyncapi.yaml` is an **error** when something already joins into it (the stranded message names ride in `details`) and **silence** otherwise. The file is presence-tracked in `loam list` and deliberately kept out of the maturity ladder: a fourth required artifact would demote every already-`documented` service in the fleet without one byte of their files changing.
147
+
148
+ **What this axis does not do yet, stated so the absence is not read as a defect:** there is no `features/<FEAT>/specs/<svc>/asyncapi.yaml`, no merge, no baseline pin and no `verify` claim — a feature cannot add a message, and one is retired by editing the living contract in a reviewed PR. Deprecation markers and a removal family are absent rather than pending: an async contract that only ever grows has nothing to mark, because a consumer reading a topic with lag breaks on a removed field regardless of what the document says about it.
149
+
132
150
  **Page-specs** (`ui/pages/*.page.yaml`) are `[later]`: an example ships under `examples/docs/`, but no command reads pages yet, so the file's format is illustrative rather than contract. What is settled is only the job — a page names the operations it consumes and the scenarios that govern it, joining the same spine as everything else.
133
151
 
134
152
  ## Living spec vs delta (OpenSpec model)
@@ -359,7 +377,7 @@ One docs repo, a hundred services, many teams. The repo scales the way any share
359
377
  - **Ownership is CODEOWNERS.** `services/<svc>/**` belongs to the owning team; `features/**` to whoever drives cross-service change, with the owners of each touched service pulled in through the feature's `specs/<svc>/` paths. loam has no permission model and should not grow one — the forge already has it.
360
378
  - **`vouch` and `archive` land through reviewed PRs.** Both rewrite the source of truth, and the PR supplies exactly what loam deliberately does not record: who approved, when, and the diff they saw. The voucher's identity is the PR author's git identity — no `vouched_by:` field, because git already refuses to forget.
361
379
  - **Provenance runs where the code is.** `sources` resolve only inside a service's own repo, so each service repo's CI runs `loam validate --service <id> --json` and branches on the finding codes (`sources.current`, `sources.stale`, `sources.unvouched`). The docs repo's CI runs `loam validate --all`, which counts what it cannot check from there (`sourcesUnverifiableFromHere`). Aggregating the per-repo results into a fleet view happens outside loam, on purpose: the stable codes are the interface, and any CI system can pivot on them.
362
- - **What it costs, measured.** On a generated 120-service fleet with 400 and 800 op-linked landscape edges (compiled binary, 8-core laptop, wall clock including Node startup): `loam list --json` 1.1–1.5 s, and `loam validate --service <id> --json` — the command above, the one every service repo runs — 1.4–1.6 s at either edge count. Both are flat in edge count because loam reads LikeC4's **parsed** model and never computes a view: it renders nothing, and view computation is superlinear in edges. (If you want diagrams, add a `views` block and render it with LikeC4's own tooling — `npx likec4 start` — and scope the views, because an `include *` over a fleet-sized landscape takes minutes. `loam init` scaffolds the landscape with no `views` block for that reason: loam should not plant a diagram it never draws.) `loam validate --all --json` over the same 120 services is **~30 s**, an order of magnitude worse than the rest, because it parses a fresh LikeC4 workspace per service rather than sharing one. That is a known, unclosed cost. It is the docs repo's own CI job, and it is nobody's inner loop.
380
+ - **What it costs, measured.** On a generated 120-service fleet with 400 and 800 op-linked landscape edges (compiled binary, 8-core laptop, wall clock including Node startup): `loam list --json` 1.1–1.5 s, and `loam validate --service <id> --json` — the command above, the one every service repo runs — 1.4–1.6 s at either edge count. Both are flat in edge count because loam reads LikeC4's **parsed** model and never computes a view: it renders nothing, and view computation is superlinear in edges. (If you want diagrams, add a `views` block and render it with LikeC4's own tooling — `npx likec4 start` in the docs repo for the fleet map, `npx likec4 start services/<id>` for one service model — and scope the views, because an `include *` over a fleet-sized landscape takes minutes. `loam init` scaffolds the landscape with no `views` block for that reason: loam should not plant a diagram it never draws. It also scaffolds a `likec4.config.json` that scopes the root LikeC4 project to `architecture/`: loam parses every `.likec4` file **alone**, so each one declares its own `specification` block, and a renderer given the whole tree at once merges them and reports every declaration as a duplicate.) `loam validate --all --json` over the same 120 services is **~30 s**, an order of magnitude worse than the rest, because it parses a fresh LikeC4 workspace per service rather than sharing one. That is a known, unclosed cost. It is the docs repo's own CI job, and it is nobody's inner loop.
363
381
 
364
382
  Four read-only fleet/transition views make that filesystem contract inspectable without creating or repairing anything:
365
383
 
package/dist/cli.js CHANGED
@@ -18,6 +18,8 @@ import { registerVerify } from "./commands/verify.js";
18
18
  import { registerVouch } from "./commands/vouch.js";
19
19
  import { registerDoctor } from "./commands/doctor.js";
20
20
  import { registerDependencies } from "./commands/dependencies.js";
21
+ import { registerExplore } from "./commands/explore.js";
22
+ import { registerInstructions } from "./commands/instructions.js";
21
23
  import { registerMigrateOpenSpec } from "./commands/migrate-openspec.js";
22
24
  import { emitJsonError } from "./core/json.js";
23
25
  import { LOAM_VERSION } from "./core/version.js";
@@ -46,6 +48,7 @@ export function buildProgram() {
46
48
  registerInit(program);
47
49
  registerAdopt(program);
48
50
  registerList(program);
51
+ registerExplore(program);
49
52
  registerNew(program);
50
53
  registerShow(program);
51
54
  registerStatus(program);
@@ -59,6 +62,7 @@ export function buildProgram() {
59
62
  registerVouch(program);
60
63
  registerDoctor(program);
61
64
  registerDependencies(program);
65
+ registerInstructions(program);
62
66
  registerMigrateOpenSpec(program);
63
67
  return program;
64
68
  }
package/dist/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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","sourcesContent":["#!/usr/bin/env node\nimport { Command, CommanderError } from \"commander\";\nimport { realpathSync } from \"node:fs\";\nimport { fileURLToPath } from \"node:url\";\nimport { registerInit } from \"./commands/init.js\";\nimport { registerAdopt } from \"./commands/adopt.js\";\nimport { registerList } from \"./commands/list.js\";\nimport { registerNew } from \"./commands/new.js\";\nimport { registerShow } from \"./commands/show.js\";\nimport { registerStatus } from \"./commands/status.js\";\nimport { registerDelta } from \"./commands/delta.js\";\nimport { registerGherkin } from \"./commands/gherkin.js\";\nimport { registerRebase } from \"./commands/rebase.js\";\nimport { registerArchive } from \"./commands/archive.js\";\nimport { registerUnarchive } from \"./commands/unarchive.js\";\nimport { registerValidate } from \"./commands/validate.js\";\nimport { registerVerify } from \"./commands/verify.js\";\nimport { registerVouch } from \"./commands/vouch.js\";\nimport { registerDoctor } from \"./commands/doctor.js\";\nimport { registerDependencies } from \"./commands/dependencies.js\";\nimport { registerMigrateOpenSpec } from \"./commands/migrate-openspec.js\";\nimport { emitJsonError } from \"./core/json.js\";\nimport { LOAM_VERSION } from \"./core/version.js\";\n\n/**\n * The whole CLI surface, as a program nobody has parsed yet.\n *\n * Exported because loam PRINTS commands — every `loam …` in AGENTS.md, in the\n * generated command and skill bodies, in a `doctor` finding's `fix` and in a\n * `status` next[] step is an instruction somebody (usually an agent) will type\n * back. The only way to prove one of those parses is to hand it to the program\n * that would receive it; a second copy of the registrations in a test agrees\n * with itself and with nothing else, which is exactly how\n * `Run \\`loam adopt <id>\\`` shipped as the first instruction a bound service\n * repo received. `test/agent-commands-runnable.test.ts` is the consumer.\n */\nexport function buildProgram(): Command {\n const program = new Command();\n\n program\n .name(\"loam\")\n .description(\"Architecture-first spec framework for microservice fleets\")\n .version(LOAM_VERSION);\n\n // Make commander throw a CommanderError instead of process.exit()ing, so a\n // usage error (mistyped flag, unknown command) can still honour the envelope\n // invariant in the catch below instead of dying with an empty stdout.\n program.exitOverride();\n\n registerInit(program);\n registerAdopt(program);\n registerList(program);\n registerNew(program);\n registerShow(program);\n registerStatus(program);\n registerDelta(program);\n registerGherkin(program);\n registerRebase(program);\n registerArchive(program);\n registerUnarchive(program);\n registerValidate(program);\n registerVerify(program);\n registerVouch(program);\n registerDoctor(program);\n registerDependencies(program);\n registerMigrateOpenSpec(program);\n\n return program;\n}\n\n/** CommanderError codes whose output (already printed) is the point, not a failure to wrap. */\nconst PASS_THROUGH = new Set([\"commander.help\", \"commander.helpDisplayed\", \"commander.version\"]);\n\nfunction main(): void {\n const program = buildProgram();\n\n // Set once a command's action actually runs: every subcommand declares its own\n // `--json`, so past that point the parsed option is the authoritative answer.\n let parsedJson: boolean | undefined;\n program.hook(\"preAction\", (_thisCommand, actionCommand) => {\n parsedJson = Boolean((actionCommand.optsWithGlobals() as { json?: boolean }).json);\n });\n\n /**\n * Did this invocation ask for --json? Prefers the parsed option once an action\n * has run; before that (commander usage errors abort mid-parse) falls back to\n * scanning raw argv. The scan is a heuristic and cannot be made exact: a token\n * spelled `--json` that was meant as an option VALUE (e.g. --title \"--json\")\n * is indistinguishable from the flag in raw argv. It gets the common cases\n * right: a `--json` token anywhere before a `--` terminator counts, and tokens\n * after `--` never do.\n */\n const jsonRequested = (): boolean => {\n if (parsedJson !== undefined) return parsedJson;\n const argv = process.argv.slice(2);\n const terminator = argv.indexOf(\"--\");\n return (terminator === -1 ? argv : argv.slice(0, terminator)).includes(\"--json\");\n };\n\n program.parseAsync(process.argv).catch((err: unknown) => {\n if (err instanceof CommanderError) {\n // Help/version output was already written by commander — pass it through\n // untouched with the exit code it always had.\n //\n // Anything else is a usage error (unknown option/command, missing or\n // invalid argument). Commander has already printed its diagnostic to\n // stderr, and that line stays: the invariant below is about stdout only.\n if (!PASS_THROUGH.has(err.code) && jsonRequested()) {\n emitJsonError(\"invalid-option\", err.message.replace(/^error: /, \"\"));\n }\n process.exitCode = err.exitCode;\n return;\n }\n const message = err instanceof Error ? err.message : String(err);\n // The envelope's one hard invariant is that stdout is JSON whenever --json\n // was asked for — an unexpected throw must not be the exception to it.\n if (jsonRequested()) {\n emitJsonError(\"internal\", message);\n } else {\n console.error(message);\n process.exitCode = 1;\n }\n });\n}\n\n/**\n * Was this file run, or merely imported? Realpath on both sides because the\n * installed entry point is reached through a symlink (`node_modules/.bin/loam`)\n * on every platform that has them, and a string compare against the link would\n * make the binary a no-op. A failure to resolve reads as \"not the entry\" only\n * for the import side; `process.argv[1]` that cannot be realpath'd is not a\n * loam invocation either.\n */\nfunction invokedDirectly(): boolean {\n const entry = process.argv[1];\n if (entry === undefined) return false;\n try {\n return realpathSync(entry) === realpathSync(fileURLToPath(import.meta.url));\n } catch {\n return false;\n }\n}\n\nif (invokedDirectly()) main();\n"]}
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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","sourcesContent":["#!/usr/bin/env node\nimport { Command, CommanderError } from \"commander\";\nimport { realpathSync } from \"node:fs\";\nimport { fileURLToPath } from \"node:url\";\nimport { registerInit } from \"./commands/init.js\";\nimport { registerAdopt } from \"./commands/adopt.js\";\nimport { registerList } from \"./commands/list.js\";\nimport { registerNew } from \"./commands/new.js\";\nimport { registerShow } from \"./commands/show.js\";\nimport { registerStatus } from \"./commands/status.js\";\nimport { registerDelta } from \"./commands/delta.js\";\nimport { registerGherkin } from \"./commands/gherkin.js\";\nimport { registerRebase } from \"./commands/rebase.js\";\nimport { registerArchive } from \"./commands/archive.js\";\nimport { registerUnarchive } from \"./commands/unarchive.js\";\nimport { registerValidate } from \"./commands/validate.js\";\nimport { registerVerify } from \"./commands/verify.js\";\nimport { registerVouch } from \"./commands/vouch.js\";\nimport { registerDoctor } from \"./commands/doctor.js\";\nimport { registerDependencies } from \"./commands/dependencies.js\";\nimport { registerExplore } from \"./commands/explore.js\";\nimport { registerInstructions } from \"./commands/instructions.js\";\nimport { registerMigrateOpenSpec } from \"./commands/migrate-openspec.js\";\nimport { emitJsonError } from \"./core/json.js\";\nimport { LOAM_VERSION } from \"./core/version.js\";\n\n/**\n * The whole CLI surface, as a program nobody has parsed yet.\n *\n * Exported because loam PRINTS commands — every `loam …` in AGENTS.md, in the\n * generated command and skill bodies, in a `doctor` finding's `fix` and in a\n * `status` next[] step is an instruction somebody (usually an agent) will type\n * back. The only way to prove one of those parses is to hand it to the program\n * that would receive it; a second copy of the registrations in a test agrees\n * with itself and with nothing else, which is exactly how\n * `Run \\`loam adopt <id>\\`` shipped as the first instruction a bound service\n * repo received. `test/agent-commands-runnable.test.ts` is the consumer.\n */\nexport function buildProgram(): Command {\n const program = new Command();\n\n program\n .name(\"loam\")\n .description(\"Architecture-first spec framework for microservice fleets\")\n .version(LOAM_VERSION);\n\n // Make commander throw a CommanderError instead of process.exit()ing, so a\n // usage error (mistyped flag, unknown command) can still honour the envelope\n // invariant in the catch below instead of dying with an empty stdout.\n program.exitOverride();\n\n registerInit(program);\n registerAdopt(program);\n registerList(program);\n registerExplore(program);\n registerNew(program);\n registerShow(program);\n registerStatus(program);\n registerDelta(program);\n registerGherkin(program);\n registerRebase(program);\n registerArchive(program);\n registerUnarchive(program);\n registerValidate(program);\n registerVerify(program);\n registerVouch(program);\n registerDoctor(program);\n registerDependencies(program);\n registerInstructions(program);\n registerMigrateOpenSpec(program);\n\n return program;\n}\n\n/** CommanderError codes whose output (already printed) is the point, not a failure to wrap. */\nconst PASS_THROUGH = new Set([\"commander.help\", \"commander.helpDisplayed\", \"commander.version\"]);\n\nfunction main(): void {\n const program = buildProgram();\n\n // Set once a command's action actually runs: every subcommand declares its own\n // `--json`, so past that point the parsed option is the authoritative answer.\n let parsedJson: boolean | undefined;\n program.hook(\"preAction\", (_thisCommand, actionCommand) => {\n parsedJson = Boolean((actionCommand.optsWithGlobals() as { json?: boolean }).json);\n });\n\n /**\n * Did this invocation ask for --json? Prefers the parsed option once an action\n * has run; before that (commander usage errors abort mid-parse) falls back to\n * scanning raw argv. The scan is a heuristic and cannot be made exact: a token\n * spelled `--json` that was meant as an option VALUE (e.g. --title \"--json\")\n * is indistinguishable from the flag in raw argv. It gets the common cases\n * right: a `--json` token anywhere before a `--` terminator counts, and tokens\n * after `--` never do.\n */\n const jsonRequested = (): boolean => {\n if (parsedJson !== undefined) return parsedJson;\n const argv = process.argv.slice(2);\n const terminator = argv.indexOf(\"--\");\n return (terminator === -1 ? argv : argv.slice(0, terminator)).includes(\"--json\");\n };\n\n program.parseAsync(process.argv).catch((err: unknown) => {\n if (err instanceof CommanderError) {\n // Help/version output was already written by commander — pass it through\n // untouched with the exit code it always had.\n //\n // Anything else is a usage error (unknown option/command, missing or\n // invalid argument). Commander has already printed its diagnostic to\n // stderr, and that line stays: the invariant below is about stdout only.\n if (!PASS_THROUGH.has(err.code) && jsonRequested()) {\n emitJsonError(\"invalid-option\", err.message.replace(/^error: /, \"\"));\n }\n process.exitCode = err.exitCode;\n return;\n }\n const message = err instanceof Error ? err.message : String(err);\n // The envelope's one hard invariant is that stdout is JSON whenever --json\n // was asked for — an unexpected throw must not be the exception to it.\n if (jsonRequested()) {\n emitJsonError(\"internal\", message);\n } else {\n console.error(message);\n process.exitCode = 1;\n }\n });\n}\n\n/**\n * Was this file run, or merely imported? Realpath on both sides because the\n * installed entry point is reached through a symlink (`node_modules/.bin/loam`)\n * on every platform that has them, and a string compare against the link would\n * make the binary a no-op. A failure to resolve reads as \"not the entry\" only\n * for the import side; `process.argv[1]` that cannot be realpath'd is not a\n * loam invocation either.\n */\nfunction invokedDirectly(): boolean {\n const entry = process.argv[1];\n if (entry === undefined) return false;\n try {\n return realpathSync(entry) === realpathSync(fileURLToPath(import.meta.url));\n } catch {\n return false;\n }\n}\n\nif (invokedDirectly()) main();\n"]}
@@ -2,9 +2,11 @@ import { existsSync } from "node:fs";
2
2
  import { closeIds } from "../core/arch.js";
3
3
  import { loadConfig } from "../core/config.js";
4
4
  import { InvalidIdError, assertServiceId } from "../core/ids.js";
5
- import { emitJson, fail, reportNoConfig } from "../core/json.js";
5
+ import { emitJson, fail, NO_SERVICE_MESSAGE, reportNoConfig } from "../core/json.js";
6
6
  import { DocsRepoUnavailableError, listServices, servicePaths } from "../core/repo.js";
7
+ import { SEVERITY_MARK } from "../core/report.js";
7
8
  import { serviceBrief, VIA_ALL } from "../core/brief.js";
9
+ import { docsRepoReady } from "./docs-repo-gate.js";
8
10
  /**
9
11
  * What is suspicious about this invocation without being wrong.
10
12
  *
@@ -60,9 +62,21 @@ export function registerAdopt(program) {
60
62
  reportNoConfig(json);
61
63
  return;
62
64
  }
65
+ // This command writes nothing, but it briefs an agent to write a whole
66
+ // baseline — so a docsDir that does not exist is not a harmless read: it
67
+ // hands over eight target paths under a directory nobody has, at exit 0,
68
+ // with the near-miss warning below silently switched off (listServices
69
+ // throws, `invocationWarnings` swallows it and returns nothing). The same
70
+ // refusal every enumerating command owes; see docs-repo-gate.ts.
71
+ //
72
+ // `docs`, not `services`: a docs repo that has no services/ yet is exactly
73
+ // the repo adopt exists to fill, and refusing there would refuse the
74
+ // command's own first use.
75
+ if (!docsRepoReady(json, config.docsDir, "docs"))
76
+ return;
63
77
  const service = opts.service ?? config.service;
64
78
  if (service === undefined) {
65
- fail(json, "invalid-option", "No service. Pass --service <id> or set it in loam.json.");
79
+ fail(json, "invalid-option", NO_SERVICE_MESSAGE);
66
80
  return;
67
81
  }
68
82
  // The id becomes `services/<id>/` in a shared repo, so it goes through the
@@ -142,7 +156,7 @@ function render(b, warnings) {
142
156
  console.log(wrap("Leave everything `status: draft`. Vouching is somebody reading the code and saying the document matches it — it is not yours to do, and a status with no digest behind it is a claim with nothing behind it.", " "));
143
157
  }
144
158
  function printCheck(c) {
145
- console.log(` ${c.severity === "error" ? "✗" : "⚠"} ${c.code}`);
159
+ console.log(` ${SEVERITY_MARK[c.severity]} ${c.code}`);
146
160
  console.log(wrap(c.what, " "));
147
161
  }
148
162
  const ACTION_NOTE = {
@@ -1 +1 @@
1
- {"version":3,"file":"adopt.js","sourceRoot":"","sources":["../../src/commands/adopt.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `loam adopt` — the bootstrap flow, as a contract with an agent.\n *\n * It emits a BRIEF, not an extraction: the artifacts one service's baseline is\n * made of, the grammar each has to be written in, what the fleet already says\n * about the service, the checks that follow, and the checks that do not exist.\n * The agent reads the code; loam states the work and then judges the result.\n *\n * It writes nothing. A command that produced the documents it is asking for\n * would be doing the half of the job it exists to hand over.\n */\nimport type { Command } from \"commander\";\nimport { existsSync } from \"node:fs\";\nimport { closeIds } from \"../core/arch.js\";\nimport { loadConfig } from \"../core/config.js\";\nimport { InvalidIdError, assertServiceId } from \"../core/ids.js\";\nimport { emitJson, fail, reportNoConfig } from \"../core/json.js\";\nimport { DocsRepoUnavailableError, listServices, servicePaths } from \"../core/repo.js\";\nimport { serviceBrief, VIA_ALL, type Brief, type BriefCheck, type BriefTarget } from \"../core/brief.js\";\n\ninterface AdoptOptions {\n service?: string;\n json?: boolean;\n}\n\n/**\n * What is suspicious about this invocation without being wrong.\n *\n * Adopting a service that has no `services/<id>/` yet is the NORMAL case — it\n * is the whole point of the command — so neither of these can be a refusal.\n * But both used to be silent, and both produce the same outcome: a complete,\n * validating baseline written under a directory name nobody meant. `adopt\n * --service biling-service` briefed a phantom service beside the real\n * `billing-service`, and an agent following the brief created it.\n */\nasync function invocationWarnings(\n docsDir: string,\n service: string,\n bound: string | undefined,\n): Promise<string[]> {\n const warnings: string[] = [];\n\n if (bound !== undefined && bound !== service) {\n warnings.push(\n `This repository's loam.json declares service '${bound}', but you asked to adopt '${service}'. ` +\n `That is legal — one repo can author another service's baseline — but \\`loam vouch\\`, ` +\n `\\`loam gherkin\\` and \\`loam verify --service\\` all bind to '${bound}' here, so the baseline ` +\n `you write cannot be vouched from this repo. If '${service}' is a typo, fix it; otherwise run ` +\n `adopt from '${service}''s own repository.`,\n );\n }\n\n if (existsSync(servicePaths(docsDir, service).dir)) return warnings;\n\n // A near-miss is only computable when the docs repo can be enumerated at all;\n // a repo that refuses enumeration is a different problem, reported by every\n // other command, and adopt has nothing useful to add to it.\n let known: string[] = [];\n try {\n known = (await listServices(docsDir)).map((s) => s.id);\n } catch (err) {\n if (!(err instanceof DocsRepoUnavailableError)) throw err;\n return warnings;\n }\n const close = closeIds(service, known);\n if (close.length > 0) {\n warnings.push(\n `There is no services/${service}/ yet, but the docs repo already has ${close.join(\", \")}. ` +\n `If '${service}' is a misspelling of one of those, stop now: an agent following this brief ` +\n `writes a complete, validating baseline for a service that does not exist, and nothing ` +\n `downstream can tell it from a real one. If it really is a new service, this is expected.`,\n );\n }\n return warnings;\n}\n\nexport function registerAdopt(program: Command): void {\n program\n .command(\"adopt\")\n .description(\"Brief an agent to write one service's baseline docs from its code, and say what will be checked\")\n .option(\"--service <id>\", \"service to adopt (defaults to the configured service)\")\n .option(\"--json\", \"emit the machine contract instead of the human view\")\n .action(async (opts: AdoptOptions) => {\n const json = opts.json === true;\n\n const config = await loadConfig();\n if (!config) {\n reportNoConfig(json);\n return;\n }\n\n const service = opts.service ?? config.service;\n if (service === undefined) {\n fail(json, \"invalid-option\", \"No service. Pass --service <id> or set it in loam.json.\");\n return;\n }\n // The id becomes `services/<id>/` in a shared repo, so it goes through the\n // one grammar before anything interpolates it into a path.\n try {\n assertServiceId(service, opts.service === undefined ? \"service (loam.json)\" : \"--service\");\n } catch (err) {\n if (!(err instanceof InvalidIdError)) throw err;\n fail(json, \"invalid-option\", err.message);\n return;\n }\n\n const warnings = await invocationWarnings(config.docsDir, service, config.service);\n const brief = await serviceBrief(config.docsDir, service);\n if (json) {\n emitJson({ ...brief, warnings });\n return;\n }\n render(brief, warnings);\n });\n}\n\n/* ------------------------------------------------------------------ */\n/* Text */\n/* ------------------------------------------------------------------ */\n\nfunction render(b: Brief, warnings: string[]): void {\n console.log(`adopt ${b.service} — write the baseline into ${b.path}/\\n`);\n // In the header, before the work: a warning printed after eight artifact\n // shapes is a warning read after the agent has started writing.\n for (const w of warnings) {\n console.log(wrap(`! ${w}`, \" \"));\n console.log(\"\");\n }\n console.log(wrap(b.rule, \" \"));\n\n console.log(\"\\n artifacts\\n\");\n const width = Math.max(...b.targets.map((t) => t.artifact.length));\n for (const t of b.targets) {\n // `edit` is the shared fleet map: the file exists, but \"present\" would read\n // as \"nothing to do here\", which is the opposite of why it is on the list.\n const flag = t.action === \"edit\" ? \"UNDRAWN\" : t.exists ? \"present\" : t.required ? \"MISSING\" : \"missing\";\n console.log(` ${t.artifact.padEnd(width)} ${t.action.padEnd(6)} ${flag.padEnd(7)} ${t.purpose}`);\n }\n\n for (const t of b.targets) printShape(t);\n\n printLandscape(b);\n\n console.log(\"\\n frontmatter — on every markdown artifact\\n\");\n for (const [field, what] of Object.entries(b.frontmatter.fields)) {\n console.log(` ${field}`);\n console.log(wrap(what, \" \"));\n }\n console.log(`\\n never write by hand: ${b.frontmatter.never.join(\", \")}`);\n console.log(`\\n${wrap(b.frontmatter.why, \" \")}`);\n\n // Attribution matters here: the fleet cross-check runs under `--all`, and a\n // header promising it to `--service` sent agents chasing a finding that\n // invocation never reports.\n console.log(`\\n what \\`loam validate --service ${b.service}\\` then checks\\n`);\n for (const c of b.checks) {\n if (c.via !== VIA_ALL) printCheck(c);\n }\n console.log(`\\n and what only \\`loam validate --all\\` surfaces\\n`);\n for (const c of b.checks) {\n if (c.via === VIA_ALL) printCheck(c);\n }\n\n console.log(\"\\n what nothing checks\\n\");\n for (const u of b.unchecked) console.log(bullet(u, \" \"));\n\n console.log(\"\\n when you are done\\n\");\n console.log(` loam validate --service ${b.service} --json # fix every error`);\n // The fleet run is not a nicety: --service never reports the landscape\n // cross-check, so a baseline can pass the line above with the fleet map\n // never edited at all.\n console.log(` loam validate --all --json # in the docs repo: the fleet map`);\n console.log(` loam vouch --service ${b.service} # a HUMAN, in the service's own repo`);\n console.log(\"\");\n console.log(\n wrap(\n \"Leave everything `status: draft`. Vouching is somebody reading the code and saying the document matches it — it is not yours to do, and a status with no digest behind it is a claim with nothing behind it.\",\n \" \",\n ),\n );\n}\n\nfunction printCheck(c: BriefCheck): void {\n console.log(` ${c.severity === \"error\" ? \"✗\" : \"⚠\"} ${c.code}`);\n console.log(wrap(c.what, \" \"));\n}\n\nconst ACTION_NOTE: Record<BriefTarget[\"action\"], string> = {\n create: \"\",\n diff: \" (exists — diff, do not replace)\",\n edit: \" (the whole fleet's file — ADD to it, never rewrite it)\",\n};\n\nfunction printShape(t: BriefTarget): void {\n console.log(`\\n ${t.path}${ACTION_NOTE[t.action]}\\n`);\n for (const rule of t.shape) console.log(bullet(rule, \" \"));\n if (t.example === undefined) return;\n console.log(\"\");\n for (const line of t.example.trimEnd().split(\"\\n\")) console.log(` ${line}`.trimEnd());\n}\n\nfunction printLandscape(b: Brief): void {\n const l = b.landscape;\n console.log(\"\\n what the fleet already says about this service\\n\");\n // The instruction is the same sentence the JSON carries, printed from the\n // same field — the text view and `--json` cannot drift into briefing two\n // different writes.\n if (!l.present) {\n console.log(\" architecture/landscape.likec4 does not exist yet — nothing to bind to.\");\n console.log(\"\");\n console.log(wrap(l.instruction ?? \"\", \" \"));\n return;\n }\n if (!l.parses) {\n console.log(\" architecture/landscape.likec4 does not parse — bind by hand, and fix it first.\");\n console.log(\"\");\n console.log(wrap(l.instruction ?? \"\", \" \"));\n return;\n }\n if (l.elements.length === 0) {\n console.log(` Nothing in the landscape models '${b.service}'.`);\n console.log(\"\");\n console.log(wrap(l.instruction ?? \"\", \" \"));\n return;\n }\n for (const e of l.elements) {\n const bound = e.bound ? \"bound\" : \"matched by title — bind it\";\n console.log(` ${e.id} = ${e.kind} '${e.title}' (${bound})`);\n }\n for (const e of l.inbound) console.log(` ← ${e.from} ${e.op ?? `\"${e.title ?? \"\"}\"`}`);\n for (const e of l.outbound) console.log(` → ${e.to} ${e.op ?? `\"${e.title ?? \"\"}\"`}`);\n if (l.expects.length > 0) {\n console.log(\"\");\n console.log(\n wrap(\n `openapi.yaml must define ${l.expects.map((o) => `'${o}'`).join(\", \")} — the fleet already calls ${l.expects.length === 1 ? \"it\" : \"them\"}, and a contract that omits ${l.expects.length === 1 ? \"it\" : \"them\"} fails spine.op-undefined the moment it lands.`,\n \" \",\n ),\n );\n }\n}\n\n/**\n * Wrap prose so the brief is readable in a terminal. Continuation lines are\n * indented past the marker, so a wrapped bullet cannot be misread as two.\n */\nfunction wrap(text: string, indent: string, width = 88): string {\n const hang = /^\\s*-\\s/.test(text) ? `${indent} ` : indent;\n const out: string[] = [];\n let line = indent;\n let pad = indent;\n for (const word of text.trim().split(/\\s+/)) {\n if (line.length > pad.length && line.length + 1 + word.length > width) {\n out.push(line);\n pad = hang;\n line = hang + word;\n } else {\n line = line.length > pad.length ? `${line} ${word}` : line + word;\n }\n }\n out.push(line);\n return out.join(\"\\n\");\n}\n\n/** A bullet, wrapped with its continuation lines hanging under the text. */\nfunction bullet(text: string, indent: string): string {\n return wrap(`- ${text}`, indent);\n}\n"]}
1
+ {"version":3,"file":"adopt.js","sourceRoot":"","sources":["../../src/commands/adopt.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `loam adopt` — the bootstrap flow, as a contract with an agent.\n *\n * It emits a BRIEF, not an extraction: the artifacts one service's baseline is\n * made of, the grammar each has to be written in, what the fleet already says\n * about the service, the checks that follow, and the checks that do not exist.\n * The agent reads the code; loam states the work and then judges the result.\n *\n * It writes nothing. A command that produced the documents it is asking for\n * would be doing the half of the job it exists to hand over.\n */\nimport type { Command } from \"commander\";\nimport { existsSync } from \"node:fs\";\nimport { closeIds } from \"../core/arch.js\";\nimport { loadConfig } from \"../core/config.js\";\nimport { InvalidIdError, assertServiceId } from \"../core/ids.js\";\nimport { emitJson, fail, NO_SERVICE_MESSAGE, reportNoConfig } from \"../core/json.js\";\nimport { DocsRepoUnavailableError, listServices, servicePaths } from \"../core/repo.js\";\nimport { SEVERITY_MARK } from \"../core/report.js\";\nimport { serviceBrief, VIA_ALL, type Brief, type BriefCheck, type BriefTarget } from \"../core/brief.js\";\nimport { docsRepoReady } from \"./docs-repo-gate.js\";\n\ninterface AdoptOptions {\n service?: string;\n json?: boolean;\n}\n\n/**\n * What is suspicious about this invocation without being wrong.\n *\n * Adopting a service that has no `services/<id>/` yet is the NORMAL case — it\n * is the whole point of the command — so neither of these can be a refusal.\n * But both used to be silent, and both produce the same outcome: a complete,\n * validating baseline written under a directory name nobody meant. `adopt\n * --service biling-service` briefed a phantom service beside the real\n * `billing-service`, and an agent following the brief created it.\n */\nasync function invocationWarnings(\n docsDir: string,\n service: string,\n bound: string | undefined,\n): Promise<string[]> {\n const warnings: string[] = [];\n\n if (bound !== undefined && bound !== service) {\n warnings.push(\n `This repository's loam.json declares service '${bound}', but you asked to adopt '${service}'. ` +\n `That is legal — one repo can author another service's baseline — but \\`loam vouch\\`, ` +\n `\\`loam gherkin\\` and \\`loam verify --service\\` all bind to '${bound}' here, so the baseline ` +\n `you write cannot be vouched from this repo. If '${service}' is a typo, fix it; otherwise run ` +\n `adopt from '${service}''s own repository.`,\n );\n }\n\n if (existsSync(servicePaths(docsDir, service).dir)) return warnings;\n\n // A near-miss is only computable when the docs repo can be enumerated at all;\n // a repo that refuses enumeration is a different problem, reported by every\n // other command, and adopt has nothing useful to add to it.\n let known: string[] = [];\n try {\n known = (await listServices(docsDir)).map((s) => s.id);\n } catch (err) {\n if (!(err instanceof DocsRepoUnavailableError)) throw err;\n return warnings;\n }\n const close = closeIds(service, known);\n if (close.length > 0) {\n warnings.push(\n `There is no services/${service}/ yet, but the docs repo already has ${close.join(\", \")}. ` +\n `If '${service}' is a misspelling of one of those, stop now: an agent following this brief ` +\n `writes a complete, validating baseline for a service that does not exist, and nothing ` +\n `downstream can tell it from a real one. If it really is a new service, this is expected.`,\n );\n }\n return warnings;\n}\n\nexport function registerAdopt(program: Command): void {\n program\n .command(\"adopt\")\n .description(\"Brief an agent to write one service's baseline docs from its code, and say what will be checked\")\n .option(\"--service <id>\", \"service to adopt (defaults to the configured service)\")\n .option(\"--json\", \"emit the machine contract instead of the human view\")\n .action(async (opts: AdoptOptions) => {\n const json = opts.json === true;\n\n const config = await loadConfig();\n if (!config) {\n reportNoConfig(json);\n return;\n }\n // This command writes nothing, but it briefs an agent to write a whole\n // baseline — so a docsDir that does not exist is not a harmless read: it\n // hands over eight target paths under a directory nobody has, at exit 0,\n // with the near-miss warning below silently switched off (listServices\n // throws, `invocationWarnings` swallows it and returns nothing). The same\n // refusal every enumerating command owes; see docs-repo-gate.ts.\n //\n // `docs`, not `services`: a docs repo that has no services/ yet is exactly\n // the repo adopt exists to fill, and refusing there would refuse the\n // command's own first use.\n if (!docsRepoReady(json, config.docsDir, \"docs\")) return;\n\n const service = opts.service ?? config.service;\n if (service === undefined) {\n fail(json, \"invalid-option\", NO_SERVICE_MESSAGE);\n return;\n }\n // The id becomes `services/<id>/` in a shared repo, so it goes through the\n // one grammar before anything interpolates it into a path.\n try {\n assertServiceId(service, opts.service === undefined ? \"service (loam.json)\" : \"--service\");\n } catch (err) {\n if (!(err instanceof InvalidIdError)) throw err;\n fail(json, \"invalid-option\", err.message);\n return;\n }\n\n const warnings = await invocationWarnings(config.docsDir, service, config.service);\n const brief = await serviceBrief(config.docsDir, service);\n if (json) {\n emitJson({ ...brief, warnings });\n return;\n }\n render(brief, warnings);\n });\n}\n\n/* ------------------------------------------------------------------ */\n/* Text */\n/* ------------------------------------------------------------------ */\n\nfunction render(b: Brief, warnings: string[]): void {\n console.log(`adopt ${b.service} — write the baseline into ${b.path}/\\n`);\n // In the header, before the work: a warning printed after eight artifact\n // shapes is a warning read after the agent has started writing.\n for (const w of warnings) {\n console.log(wrap(`! ${w}`, \" \"));\n console.log(\"\");\n }\n console.log(wrap(b.rule, \" \"));\n\n console.log(\"\\n artifacts\\n\");\n const width = Math.max(...b.targets.map((t) => t.artifact.length));\n for (const t of b.targets) {\n // `edit` is the shared fleet map: the file exists, but \"present\" would read\n // as \"nothing to do here\", which is the opposite of why it is on the list.\n const flag = t.action === \"edit\" ? \"UNDRAWN\" : t.exists ? \"present\" : t.required ? \"MISSING\" : \"missing\";\n console.log(` ${t.artifact.padEnd(width)} ${t.action.padEnd(6)} ${flag.padEnd(7)} ${t.purpose}`);\n }\n\n for (const t of b.targets) printShape(t);\n\n printLandscape(b);\n\n console.log(\"\\n frontmatter — on every markdown artifact\\n\");\n for (const [field, what] of Object.entries(b.frontmatter.fields)) {\n console.log(` ${field}`);\n console.log(wrap(what, \" \"));\n }\n console.log(`\\n never write by hand: ${b.frontmatter.never.join(\", \")}`);\n console.log(`\\n${wrap(b.frontmatter.why, \" \")}`);\n\n // Attribution matters here: the fleet cross-check runs under `--all`, and a\n // header promising it to `--service` sent agents chasing a finding that\n // invocation never reports.\n console.log(`\\n what \\`loam validate --service ${b.service}\\` then checks\\n`);\n for (const c of b.checks) {\n if (c.via !== VIA_ALL) printCheck(c);\n }\n console.log(`\\n and what only \\`loam validate --all\\` surfaces\\n`);\n for (const c of b.checks) {\n if (c.via === VIA_ALL) printCheck(c);\n }\n\n console.log(\"\\n what nothing checks\\n\");\n for (const u of b.unchecked) console.log(bullet(u, \" \"));\n\n console.log(\"\\n when you are done\\n\");\n console.log(` loam validate --service ${b.service} --json # fix every error`);\n // The fleet run is not a nicety: --service never reports the landscape\n // cross-check, so a baseline can pass the line above with the fleet map\n // never edited at all.\n console.log(` loam validate --all --json # in the docs repo: the fleet map`);\n console.log(` loam vouch --service ${b.service} # a HUMAN, in the service's own repo`);\n console.log(\"\");\n console.log(\n wrap(\n \"Leave everything `status: draft`. Vouching is somebody reading the code and saying the document matches it — it is not yours to do, and a status with no digest behind it is a claim with nothing behind it.\",\n \" \",\n ),\n );\n}\n\nfunction printCheck(c: BriefCheck): void {\n console.log(` ${SEVERITY_MARK[c.severity]} ${c.code}`);\n console.log(wrap(c.what, \" \"));\n}\n\nconst ACTION_NOTE: Record<BriefTarget[\"action\"], string> = {\n create: \"\",\n diff: \" (exists — diff, do not replace)\",\n edit: \" (the whole fleet's file — ADD to it, never rewrite it)\",\n};\n\nfunction printShape(t: BriefTarget): void {\n console.log(`\\n ${t.path}${ACTION_NOTE[t.action]}\\n`);\n for (const rule of t.shape) console.log(bullet(rule, \" \"));\n if (t.example === undefined) return;\n console.log(\"\");\n for (const line of t.example.trimEnd().split(\"\\n\")) console.log(` ${line}`.trimEnd());\n}\n\nfunction printLandscape(b: Brief): void {\n const l = b.landscape;\n console.log(\"\\n what the fleet already says about this service\\n\");\n // The instruction is the same sentence the JSON carries, printed from the\n // same field — the text view and `--json` cannot drift into briefing two\n // different writes.\n if (!l.present) {\n console.log(\" architecture/landscape.likec4 does not exist yet — nothing to bind to.\");\n console.log(\"\");\n console.log(wrap(l.instruction ?? \"\", \" \"));\n return;\n }\n if (!l.parses) {\n console.log(\" architecture/landscape.likec4 does not parse — bind by hand, and fix it first.\");\n console.log(\"\");\n console.log(wrap(l.instruction ?? \"\", \" \"));\n return;\n }\n if (l.elements.length === 0) {\n console.log(` Nothing in the landscape models '${b.service}'.`);\n console.log(\"\");\n console.log(wrap(l.instruction ?? \"\", \" \"));\n return;\n }\n for (const e of l.elements) {\n const bound = e.bound ? \"bound\" : \"matched by title — bind it\";\n console.log(` ${e.id} = ${e.kind} '${e.title}' (${bound})`);\n }\n for (const e of l.inbound) console.log(` ← ${e.from} ${e.op ?? `\"${e.title ?? \"\"}\"`}`);\n for (const e of l.outbound) console.log(` → ${e.to} ${e.op ?? `\"${e.title ?? \"\"}\"`}`);\n if (l.expects.length > 0) {\n console.log(\"\");\n console.log(\n wrap(\n `openapi.yaml must define ${l.expects.map((o) => `'${o}'`).join(\", \")} — the fleet already calls ${l.expects.length === 1 ? \"it\" : \"them\"}, and a contract that omits ${l.expects.length === 1 ? \"it\" : \"them\"} fails spine.op-undefined the moment it lands.`,\n \" \",\n ),\n );\n }\n}\n\n/**\n * Wrap prose so the brief is readable in a terminal. Continuation lines are\n * indented past the marker, so a wrapped bullet cannot be misread as two.\n */\nfunction wrap(text: string, indent: string, width = 88): string {\n const hang = /^\\s*-\\s/.test(text) ? `${indent} ` : indent;\n const out: string[] = [];\n let line = indent;\n let pad = indent;\n for (const word of text.trim().split(/\\s+/)) {\n if (line.length > pad.length && line.length + 1 + word.length > width) {\n out.push(line);\n pad = hang;\n line = hang + word;\n } else {\n line = line.length > pad.length ? `${line} ${word}` : line + word;\n }\n }\n out.push(line);\n return out.join(\"\\n\");\n}\n\n/** A bullet, wrapped with its continuation lines hanging under the text. */\nfunction bullet(text: string, indent: string): string {\n return wrap(`- ${text}`, indent);\n}\n"]}
@@ -6,11 +6,11 @@ import { emitJson, emitJsonError, fail, repoPath, reportNoConfig } from "../core
6
6
  import { gatesArchive } from "../core/issue.js";
7
7
  import { acquireDocsLock, clearCommitIntent, DocsBusyError, InterruptedCommitError, message, NotUtf8Error, planWrite, quietPruneEmptyParents, quietRm, readUtf8, recoverInterruptedCommit, rollbackError, rollbackStaged, snapshotDir, SnapshotClobberError, stageWrites, swapStaged, writeCommitIntent, writeSnapshot, SNAPSHOT_DIR, } from "../core/staging.js";
8
8
  import { elementService, loadFile, loadSource, maskSource, matchBrace, scanModel, serviceOf, } from "../core/likec4.js";
9
- import { featurePaths, featureSpecPaths, featureSpecServices, featuresDir as featuresRoot, landscapePath as landscapeFile, missingFeatureMessage, resolveFeature, servicePaths, SPEC_AXES, } from "../core/repo.js";
9
+ import { archiveDir as archiveRoot, featurePaths, featureSpecPaths, featureSpecServices, landscapePath as landscapeFile, missingFeatureMessage, resolveFeature, servicePaths, SPEC_AXES, } from "../core/repo.js";
10
10
  import { readOpenapi } from "../core/openapi.js";
11
11
  import { mergeOpenapiPaths, OpenapiMergeError, stripOpenapiRemovalMarkers, } from "../core/openapi-merge.js";
12
12
  import { featureCoherence, livingMergeConflicts, unknownDeltaServices } from "../core/coherence.js";
13
- import { findingJson } from "../core/report.js";
13
+ import { findingJson, SEVERITY_MARK } from "../core/report.js";
14
14
  import { parseRequirements, serializeRequirements, applyRequirementDelta, isRequirementsHeading, sectionHeadings, splitRequirementsSection, } from "../core/spec.js";
15
15
  /**
16
16
  * A refusal or failure with its stable `--json` code attached. Thrown from the
@@ -52,6 +52,14 @@ function archiveErrorCode(err) {
52
52
  return err.code;
53
53
  if (err instanceof NotUtf8Error)
54
54
  return "merge-failed";
55
+ // The merge branch wraps its own call and re-throws as merge-failed; the
56
+ // create branch (`stripOpenapiRemovalMarkers`) does not, so one unreadable
57
+ // feature contract answered merge-failed or `internal` depending on whether
58
+ // the service already had a living openapi.yaml. The code answers "can I
59
+ // trust the repo?", and that answer does not turn on which side of the merge
60
+ // the document sat on.
61
+ if (err instanceof OpenapiMergeError)
62
+ return "merge-failed";
55
63
  if (err instanceof InterruptedCommitError)
56
64
  return "commit-interrupted";
57
65
  return "internal";
@@ -139,7 +147,6 @@ async function archiveLocked(config, featureId, opts) {
139
147
  const recovered = await recoverInterruptedCommit(config.docsDir);
140
148
  if (recovered !== null && !json)
141
149
  sayRecovery(recovered);
142
- const featuresDir = featuresRoot(config.docsDir);
143
150
  const feature = await resolveFeature(config.docsDir, featureId, "exclude");
144
151
  if (!feature) {
145
152
  fail(json, "unknown-target", await missingFeatureMessage(config.docsDir, featureId));
@@ -201,7 +208,7 @@ async function archiveLocked(config, featureId, opts) {
201
208
  }
202
209
  console.error(`${msg}:`);
203
210
  for (const i of issues)
204
- console.error(` ${i.severity === "error" ? "✗" : "⚠"} ${i.message}`);
211
+ console.error(` ${SEVERITY_MARK[i.severity]} ${i.message}`);
205
212
  console.error(`\nFix the gating issues (advisory warnings never block), or re-run with --approve to override them (may corrupt the living docs).`);
206
213
  process.exitCode = 1;
207
214
  return;
@@ -209,7 +216,7 @@ async function archiveLocked(config, featureId, opts) {
209
216
  if (gating.length > 0) {
210
217
  say(`⚠ archiving despite ${gating.length} gating issue(s) (--approve):`);
211
218
  for (const i of gating)
212
- say(` ${i.severity === "error" ? "✗" : "⚠"} ${i.message}`);
219
+ say(` ${SEVERITY_MARK[i.severity]} ${i.message}`);
213
220
  say();
214
221
  }
215
222
  if (advisory.length > 0) {
@@ -297,7 +304,7 @@ async function archiveLocked(config, featureId, opts) {
297
304
  }
298
305
  // Pre-flight: the archive destination must be free, or the final move would fail
299
306
  // after the living docs were already rewritten.
300
- const archiveDir = join(featuresDir, "archive");
307
+ const archiveDir = archiveRoot(config.docsDir);
301
308
  const archiveDest = join(archiveDir, dirName);
302
309
  if (existsSync(archiveDest)) {
303
310
  fail(json, "archive-exists", `archive ${id} — BLOCKED: features/archive/${dirName} already exists. Remove or rename it, then re-run.`);
@@ -368,6 +375,17 @@ async function archiveLocked(config, featureId, opts) {
368
375
  const featText = await readUtf8(featOpenapi);
369
376
  const livingOpenapi = servicePaths(config.docsDir, svc).openapi;
370
377
  const featDoc = await readOpenapi(featOpenapi);
378
+ // Every other reader of this flag suspends its own judgement when it is set
379
+ // — validate grades `openapi.invalid`, show and status print that the file
380
+ // does not parse. Archive was the only one that never looked, and it is the
381
+ // command that WRITES: a feature openapi.yaml holding a sequence instead of
382
+ // a mapping was planned verbatim into services/<svc>/openapi.yaml, `ok:
383
+ // true`, printing `created ()` because the reader saw no operations in it.
384
+ // A document loam cannot read is one it cannot merge, and that is the same
385
+ // answer as any other merge it could not compute: nothing is written.
386
+ if (featDoc.unreadable) {
387
+ throw new ArchiveFailure("merge-failed", `feature openapi ${repoPath(config.docsDir, featOpenapi)} for ${svc} does not read as an OpenAPI document (${featDoc.error ?? "not a YAML mapping"}) — the API axis cannot be merged; fix the file, or delete it if this feature has no contract delta`);
388
+ }
371
389
  const ops = featDoc.ops.filter((op) => !op.remove).map((op) => op.id);
372
390
  // An `x-loam-remove: true` written at PATH level retires nothing: the marker
373
391
  // addresses one operation, and beside the methods there is no operation for
@@ -507,8 +525,6 @@ async function archiveLocked(config, featureId, opts) {
507
525
  // here would make onboarding a new service impossible in one command.
508
526
  const newServices = [...new Set([...deltaServices, ...architectureServices])].filter((svc) => !existsSync(servicePaths(config.docsDir, svc).dir));
509
527
  for (const svc of newServices) {
510
- if (existsSync(servicePaths(config.docsDir, svc).model))
511
- continue;
512
528
  // Two shapes of the same debt: a service with a requirement delta gets its
513
529
  // directory from this merge, one that arrives only in the landscape gets no
514
530
  // directory at all — and `validate --all` fails the second harder