@ybotok/loam 0.0.0 → 0.1.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/CHANGELOG.md +138 -0
  2. package/COMPARISON.md +63 -0
  3. package/LICENSE +21 -0
  4. package/MIGRATING-from-OpenSpec.md +202 -0
  5. package/README.md +246 -9
  6. package/SCHEMA.md +425 -0
  7. package/SECURITY.md +9 -0
  8. package/dist/cli.js +142 -0
  9. package/dist/cli.js.map +1 -0
  10. package/dist/commands/adopt.js +240 -0
  11. package/dist/commands/adopt.js.map +1 -0
  12. package/dist/commands/archive.js +1313 -0
  13. package/dist/commands/archive.js.map +1 -0
  14. package/dist/commands/delta.js +388 -0
  15. package/dist/commands/delta.js.map +1 -0
  16. package/dist/commands/dependencies.js +83 -0
  17. package/dist/commands/dependencies.js.map +1 -0
  18. package/dist/commands/docs-repo-gate.js +104 -0
  19. package/dist/commands/docs-repo-gate.js.map +1 -0
  20. package/dist/commands/doctor.js +85 -0
  21. package/dist/commands/doctor.js.map +1 -0
  22. package/dist/commands/explore.js +195 -0
  23. package/dist/commands/explore.js.map +1 -0
  24. package/dist/commands/format.js +17 -0
  25. package/dist/commands/format.js.map +1 -0
  26. package/dist/commands/gherkin.js +309 -0
  27. package/dist/commands/gherkin.js.map +1 -0
  28. package/dist/commands/init.js +250 -0
  29. package/dist/commands/init.js.map +1 -0
  30. package/dist/commands/instructions.js +70 -0
  31. package/dist/commands/instructions.js.map +1 -0
  32. package/dist/commands/list.js +291 -0
  33. package/dist/commands/list.js.map +1 -0
  34. package/dist/commands/migrate-openspec.js +950 -0
  35. package/dist/commands/migrate-openspec.js.map +1 -0
  36. package/dist/commands/new.js +413 -0
  37. package/dist/commands/new.js.map +1 -0
  38. package/dist/commands/rebase.js +356 -0
  39. package/dist/commands/rebase.js.map +1 -0
  40. package/dist/commands/show.js +320 -0
  41. package/dist/commands/show.js.map +1 -0
  42. package/dist/commands/status.js +254 -0
  43. package/dist/commands/status.js.map +1 -0
  44. package/dist/commands/unarchive.js +317 -0
  45. package/dist/commands/unarchive.js.map +1 -0
  46. package/dist/commands/validate.js +1591 -0
  47. package/dist/commands/validate.js.map +1 -0
  48. package/dist/commands/verify.js +775 -0
  49. package/dist/commands/verify.js.map +1 -0
  50. package/dist/commands/vouch.js +343 -0
  51. package/dist/commands/vouch.js.map +1 -0
  52. package/dist/core/agent.js +2065 -0
  53. package/dist/core/agent.js.map +1 -0
  54. package/dist/core/agents-stamp.js +68 -0
  55. package/dist/core/agents-stamp.js.map +1 -0
  56. package/dist/core/arch.js +130 -0
  57. package/dist/core/arch.js.map +1 -0
  58. package/dist/core/brief.js +733 -0
  59. package/dist/core/brief.js.map +1 -0
  60. package/dist/core/coherence.js +577 -0
  61. package/dist/core/coherence.js.map +1 -0
  62. package/dist/core/concurrency.js +51 -0
  63. package/dist/core/concurrency.js.map +1 -0
  64. package/dist/core/config.js +180 -0
  65. package/dist/core/config.js.map +1 -0
  66. package/dist/core/delta.js +476 -0
  67. package/dist/core/delta.js.map +1 -0
  68. package/dist/core/dependencies.js +356 -0
  69. package/dist/core/dependencies.js.map +1 -0
  70. package/dist/core/docs.js +144 -0
  71. package/dist/core/docs.js.map +1 -0
  72. package/dist/core/doctor.js +559 -0
  73. package/dist/core/doctor.js.map +1 -0
  74. package/dist/core/document-bytes.js +59 -0
  75. package/dist/core/document-bytes.js.map +1 -0
  76. package/dist/core/explore.js +225 -0
  77. package/dist/core/explore.js.map +1 -0
  78. package/dist/core/fleet-context.js +225 -0
  79. package/dist/core/fleet-context.js.map +1 -0
  80. package/dist/core/frontmatter.js +199 -0
  81. package/dist/core/frontmatter.js.map +1 -0
  82. package/dist/core/gherkin.js +463 -0
  83. package/dist/core/gherkin.js.map +1 -0
  84. package/dist/core/health.js +67 -0
  85. package/dist/core/health.js.map +1 -0
  86. package/dist/core/ids.js +122 -0
  87. package/dist/core/ids.js.map +1 -0
  88. package/dist/core/issue.js +9 -0
  89. package/dist/core/issue.js.map +1 -0
  90. package/dist/core/json.js +74 -0
  91. package/dist/core/json.js.map +1 -0
  92. package/dist/core/likec4.js +479 -0
  93. package/dist/core/likec4.js.map +1 -0
  94. package/dist/core/maturity.js +66 -0
  95. package/dist/core/maturity.js.map +1 -0
  96. package/dist/core/openapi-merge.js +552 -0
  97. package/dist/core/openapi-merge.js.map +1 -0
  98. package/dist/core/openapi.js +255 -0
  99. package/dist/core/openapi.js.map +1 -0
  100. package/dist/core/openspec-inventory.js +1440 -0
  101. package/dist/core/openspec-inventory.js.map +1 -0
  102. package/dist/core/path-safety.js +168 -0
  103. package/dist/core/path-safety.js.map +1 -0
  104. package/dist/core/provenance.js +720 -0
  105. package/dist/core/provenance.js.map +1 -0
  106. package/dist/core/records.js +33 -0
  107. package/dist/core/records.js.map +1 -0
  108. package/dist/core/repo.js +436 -0
  109. package/dist/core/repo.js.map +1 -0
  110. package/dist/core/report.js +47 -0
  111. package/dist/core/report.js.map +1 -0
  112. package/dist/core/results.js +199 -0
  113. package/dist/core/results.js.map +1 -0
  114. package/dist/core/spec.js +481 -0
  115. package/dist/core/spec.js.map +1 -0
  116. package/dist/core/staging.js +890 -0
  117. package/dist/core/staging.js.map +1 -0
  118. package/dist/core/status.js +1104 -0
  119. package/dist/core/status.js.map +1 -0
  120. package/dist/core/steps.js +72 -0
  121. package/dist/core/steps.js.map +1 -0
  122. package/dist/core/verify.js +807 -0
  123. package/dist/core/verify.js.map +1 -0
  124. package/dist/core/version.js +24 -0
  125. package/dist/core/version.js.map +1 -0
  126. package/package.json +63 -4
package/CHANGELOG.md ADDED
@@ -0,0 +1,138 @@
1
+ # Changelog
2
+
3
+ All notable project changes are recorded here. The format follows Keep a Changelog, and release versions follow Semantic Versioning.
4
+
5
+ ## [Unreleased]
6
+
7
+ ## [0.1.0-beta.2] - 2026-08-07
8
+
9
+ 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.
10
+
11
+ ### Added
12
+
13
+ - **`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.
14
+ - **`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.
15
+ - **[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.
16
+
17
+ ### Changed — what `loam init` writes into a command or skill file
18
+
19
+ - **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.
20
+ - **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`.
21
+ - Both new commands reuse the existing refusal codes (`invalid-option`, `unknown-target`). No stable code was added, changed or removed.
22
+ - **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.
23
+
24
+ ### Fixed — commands that wrote or attested on evidence they had not checked
25
+
26
+ - **`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`.
27
+ - **`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.
28
+ - **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.
29
+ - **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.
30
+ - **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.
31
+
32
+ ### Fixed — user errors reported as `internal`
33
+
34
+ - **`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.
35
+ - **`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.
36
+ - **`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.
37
+
38
+ ### Fixed — checks that answered differently depending on where they were asked
39
+
40
+ - **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.
41
+ - **`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.
42
+ - **`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).
43
+ - **`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.
44
+ - **`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".
45
+
46
+ ### Fixed — deletion that reached outside the repository
47
+
48
+ - **`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.
49
+
50
+ ### Changed — internal structure
51
+
52
+ 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.
53
+
54
+ ## [0.1.0-beta.1] - 2026-08-06
55
+
56
+ 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.
57
+
58
+ ### Fixed — data loss and silent failure
59
+
60
+ - **A file whose bytes are not UTF-8 is no longer rewritten.** The whole write path moves bytes rather than strings; text is produced only where a parser needs it. Undecodable content refuses the merge (`merge-failed`, naming the file) instead of replacing every such byte with U+FFFD — in the living document *and* in the snapshot meant to undo it, which left nothing to restore from. A non-UTF-8 `openapi.yaml` now grades `openapi.invalid` instead of being handed to the YAML parser with the damage already done.
61
+ - **`unarchive` no longer restores an edited snapshot and certifies it.** The snapshot manifest is version 2: every entry records `before`, a sha256 of the pre-image it will restore, alongside the existing `after`. `unarchive` hashes each pre-image before staging anything and refuses on a mismatch (`snapshot-corrupt`) — and `--force` deliberately does not override it, because `--force` discards later changes to the living docs while the damage here is to the undo itself. A version-1 snapshot is refused as `snapshot-missing`.
62
+ - **A commit killed halfway can now be repaired.** `archive` and `unarchive` fsync an intent journal (`.loam-commit`) before the first swap and recover from it on the next run, under the lock: an archive is undone, an unarchive is finished (the merged text it was replacing is recorded nowhere else). Where the files no longer permit a safe repair the answer is `commit-interrupted`, a refusal. Previously a SIGKILL between two renames left a half-merged repo that `doctor` called healthy, `validate` blamed on the author's delta, and nothing could roll back.
63
+ - **A removal marker can no longer be published into a living contract.** `x-loam-remove` written at path level, beside the methods, names no operation and retires nothing; `archive` gates it (`openapi.remove-marker-path-level`) and the marker is stripped from every merge branch, so it never reaches the fleet's living OpenAPI under any flag.
64
+ - **Symlinked service and feature directories are enumerated.** A `services/<svc>` or `features/<FEAT>` reached through a symlink used to vanish from every listing, which also produced a false `landscape.binding-unknown` for the service that was right there. Dangling links are skipped exactly as an absent directory is.
65
+
66
+ ### Fixed — checks that were blind
67
+
68
+ - **Nested landscape elements are graded.** The landscape cross-check kept only top-level elements, so ordinary grouped C4 — services under an `enterprise`, `group` or `boundary` — reported *every* service as unmodelled. The tree is walked instead: an element is at service level when no ancestor already stands for a service, an element that contains a service is a grouping, and `landscape.binding-unknown` is now graded at any depth.
69
+ - **A missing `openapi.yaml` no longer silences the API axis.** It is an **error** when a living non-`REMOVED` `Operations:` line or an op-linked landscape edge already points into the absent file, with the stranded operationIds in `details`; a warning when nothing does and the landscape cannot prove nobody calls the service; silent when it can. Correspondingly, one absent contract is one finding rather than one `spine.op-undefined` per inbound consumer — a file that is not there proves nothing about an edge.
70
+ - **A feature delta addressed to a nonexistent service is refused** (`delta.service-unknown`, naming close ids). A typo in `--touches` used to validate green and materialise a phantom service directory on archive.
71
+ - **`openapi.duplicate-operationid` fires in service scope**, not only through an unrelated feature's delta — so `validate --all` sees it on a fleet with no feature in flight.
72
+ - **`c4.uncovered` no longer fires on a re-declared edge.** A delta that restates a living edge to hang a requirement on it was told to write a decorative architecture requirement.
73
+ - **`loam doctor` reports what it could not see before**: a held or stale `.loam-lock`, an interrupted commit, orphaned staging temp files, and generated command/skill files that have fallen behind (below).
74
+
75
+ ### Changed — verification says which answers came from a test run
76
+
77
+ `--record` may still confirm a `scenario.tested` claim on an agent's word; a service with no runnable suite yet has to be able to record its answers. What changed is that the record now says so, everywhere:
78
+
79
+ - a three-valued **`verdict`** — `verified`, `attested`, `unverified` — recomputed from `claims[]` on every read and never taken from the record's own `summary:` block. `verified` in `--json` is exactly `verdict === "verified"`, so a record with zero claims no longer reads as verified by arithmetic;
80
+ - `verify.scenario-attested` (warning, gating nothing) naming each claim, on the read view, the recording view and the frozen post-archive view alike; `loam status` re-reports it and offers `next.verify-attested`, and such a feature reaches `stage: ready`, never `done`;
81
+ - a `summary:` block contradicting its own `claims[]` makes the whole record `record-unreadable` (`verify.record-miscounted`) — neither half can be believed, so neither is reported as fact;
82
+ - `--results` writes down **which report it read** (path, sha256, mtime, tagged-scenario count), must resolve inside the attesting repository in federated mode, and refuses a committed report that differs from the attested commit. That says which file was consumed; it does not say the file came from executing that commit, and no digest can;
83
+ - two services wording a scenario identically share one digest, so a single report cannot say whose suite ran it: those claims are left unconfirmed under `verify.digest-contested` rather than confirming both.
84
+
85
+ ### Changed — `loam status` is a projection over the gates
86
+
87
+ `status` used to say "ship it" on trees `archive` refused. It now takes `stage` and `next[]` from the union of what `validate --feature` errors on and what `archive` refuses to merge, so it may be more pessimistic than either and is never greener than both — an invariant with a test behind it. Also: `checks.coherent` runs the same functions `validate` does (coherence, provenance, missing scenarios); `owesContract` keys on the contract *file* rather than the service directory; `next[]` names `loam delta`, `loam gherkin` and `loam rebase`; the fleet form is capped at ten steps plus a `next.elided` notice and always ends on `next.fleet-gate`; and a filesystem failure that carries no path (a directory where a file belongs) is reported rather than crashing the command.
88
+
89
+ ### Changed — generated agent files carry a version stamp
90
+
91
+ Every generated command and skill body now opens with `<!-- generated by loam vX.Y.Z -->`, and `loam doctor` raises `doctor.agent-files-stale` for a file with no stamp or an older one. loam still never rewrites a generated file — the fix is a human's — but drift is now detectable, which it previously was not: a command file mangled to one line left `doctor: healthy: true`. Doctor also stops reporting a repo initialized `--no-skills` as missing its skills.
92
+
93
+ `loam rebase` and `loam status` were added to the shipped workflow bodies, which is where the mechanism that prevents two in-flight features from overwriting each other actually gets run. `doctor`'s fix line now spells `loam adopt --service <id>`; the positional form it used to print is refused by the CLI, and a new test parses every command loam prints against the real CLI so that class of defect cannot return silently.
94
+
95
+ ### Changed — `loam init` keeps the pointer it was given
96
+
97
+ `--docs` wins only when it is actually passed. A re-run in a wired repository keeps the `docsDir` its committed `loam.json` already names, `--create` included, and spreads the rest of the config forward; `--json` reports `docsDirSource` as `flag`, `config` or `default`. Following `doctor`'s advice to re-run `init` used to repoint the repo at an empty decoy over which `validate --all` went green.
98
+
99
+ ### Fixed — the OpenSpec on-ramp
100
+
101
+ - **A Store checkout is audited by its planning shape, not by its marker.** A `.openspec-store/store.yaml` beside real `specs/` used to make audit look for `<checkout>/openspec`, find nothing, and report `ready: true, capabilities: 0` — then apply a target holding no requirements at all. The shape picks the root; the marker picks only the kind; a checkout with planning content in neither place is refused by name.
102
+ - **A workspace nobody could read is never `ready`** (`openspec.workspace-empty`).
103
+ - New blockers for shapes that silently migrated nothing: `openspec.change-quoted-requirements` (requirements under `## Requirements` inside an active delta — the shape OpenSpec's own living template mandates, which stages nothing in a change), `openspec.nonstandard-living-spec` (markdown under `specs/` named neither `spec.md` nor `design.md`), `openspec.hidden-change-directory` (a dot-prefixed directory under `changes/`, which enumeration skips).
104
+ - **The source digest no longer covers archived changes.** Frozen history never gates, so it must not be able to invalidate a completed mapping either; a living or active edit still does.
105
+ - **The staged target is a real docs repo** — its own `loam.json`, `AGENTS.md` and an empty `architecture/landscape.likec4` — so `FOLLOW-UP.md`'s instructions run where they are written, and `FOLLOW-UP.md` now ends with the cutover procedure: `services/` and `features/` move, everything else is review residue.
106
+ - **A target inside a live loam fleet is refused**, instead of producing phantom features in every `loam list`.
107
+ - The living capability tree is copied verbatim under `legacy/openspec/specs/`, so `## Purpose` prose, section prose and capability `design.md` are preserved rather than dropped; an ISO-8601 `created` timestamp is a valid date; fenced FROM/TO examples inside a `## RENAMED Requirements` block are no longer parsed as renames; and a rename's TO name no longer demands a service allocation the router never asks for.
108
+
109
+ ### Performance
110
+
111
+ `loam list` and `loam validate --service` are now flat in landscape edge count: loam reads LikeC4's parsed model and never computes a view, which it had no use for. On a generated 120-service fleet the same commands previously did not finish above roughly 200 edges. Measured at 120 services and 400/800 edges on an 8-core laptop: `list --json` 1.1–1.5 s, `validate --service <id> --json` 1.4–1.6 s. `loam init` no longer scaffolds a `views { view index { include * } }` block it never reads.
112
+
113
+ `loam validate --all` remains ~30 s on that fleet — it parses a fresh LikeC4 workspace per service — and no speedup is claimed for it. Its target loop is now a bounded pool rather than a serial `await`, which is byte-for-byte output-identical and deterministic in ordering, but measured as a wash on the hardware available; the lever that moved this command was the parsed model, not the loop shape.
114
+
115
+ ### Fixed — release engineering
116
+
117
+ - The CI `package` job could never pass: `release-check.mjs` refused `--fixture-ready` whenever `GITHUB_ACTIONS` was set. The guard now keys on a tag ref, which is what it was actually protecting against. Any claim that CI had validated release readiness was false before this.
118
+ - `npm run test:package` failed against the current tarball — the smoke test called `loam init --docs docs` without `--create`.
119
+ - Five release scripts spawned `npm.cmd`/`loam.cmd` without a shell, which Node ≥ 20.12 rejects on Windows; they now resolve npm's CLI and run it through `process.execPath`. Windows itself is unexercised here — there is no Windows host — so this is verified structurally and by the first real CI run.
120
+ - `pack-release` and `verify-release-artifact` compared an unpeeled `GITHUB_SHA` against `HEAD`, so an annotated or signed tag — the `npm version` and `git tag -a/-s` default — could never pass. Both peel now; `git tag -s v<version>` works.
121
+ - Release artifact retention raised 1 → 14 days, so a candidate awaiting human approval outlives a weekend; CI narrowed to pushes on `main` with a concurrency group, so tags no longer run both workflows in parallel.
122
+
123
+ ### Documentation
124
+
125
+ README, COMPARISON, SCHEMA and MIGRATING were re-checked claim by claim against the code and against upstream OpenSpec live on 2026-08-05. Corrected: the documented migration entry point (`migrate-openspec <root>` returns `invalid-option`; `audit-openspec` is the real first step and appeared nowhere in the README), the corpus and test counts, the `--baseline` flag the reproduction recipe needs, the Executability and agent-surface comparison rows, and the claim that OpenSpec has no multi-repo story — Stores shipped in v1.5.0, and the honest argument is their documented "No sync, ever — by design" plus the absence of any lockfile or commit pin. Removed: the argument that upstream's generated instructions are broken, which upstream fixed inside 48 hours and which loam had in its own surface.
126
+
127
+ ### Added
128
+
129
+ - A tag-driven npm release gate using GitHub OIDC trusted publishing and provenance.
130
+ - A reproducible two-fleet pilot harness and scorecard contract.
131
+
132
+ ### Changed
133
+
134
+ - The supported runtime now matches the direct LikeC4 dependency: Node.js 22.22.3 or newer.
135
+
136
+ ## Upgrading to this release
137
+
138
+ Nothing to upgrade from — this is the first published version. One note for a repository initialized from a pre-release build: the first `loam doctor` after installing this one reports `doctor.agent-files-stale` for **every** generated command and skill file. Earlier builds wrote no version stamp, so "no stamp" and "an old stamp" are indistinguishable, and loam refuses to assume the instructions on disk describe this binary. Read each file, then bump its stamp by hand, or delete it and re-run `loam init` — the drift is reported and never repaired for you.
package/COMPARISON.md ADDED
@@ -0,0 +1,63 @@
1
+ # loam vs OpenSpec
2
+
3
+ loam reimplements part of [OpenSpec](https://github.com/Fission-AI/OpenSpec)'s requirement format from the outside and deliberately makes different workflow choices — [MIGRATING-from-OpenSpec.md](MIGRATING-from-OpenSpec.md) covers the mechanics of moving a repo. Released behavior below is pinned to **OpenSpec v1.7.0**, exact commit `4e16790d90d8f54d4773ad9a5e71a57cd9f1e86b` — still the latest release and the latest tag as of 2026-08-05. The separate **main canary** is pinned to `45cca5db6137ed209117cc70510eb3e057fb981b`; it is not mislabeled as the release, and it is deliberately a *historical* pin rather than a moving target — upstream merged 21 commits to `main` in the five days after that SHA was taken, so a pin that chased `main` would be stale before anyone read it. What the canary buys is a second reproducible corpus, and its totals have not moved since. Routine CI runs [`test/openspec-compat.test.ts`](test/openspec-compat.test.ts) over seven representative, verbatim fixtures. A scheduled/manual matrix clones both exact commits and checks the full living + active + archived spec corpus: release 207 files / 739 requirements / 2273 scenarios, canary 209 / 742 / 2284. Reproduce either with `npm run test:openspec-corpus -- --baseline release|canary /path/to/OpenSpec` — the flag is not optional in practice, because it defaults to `canary` and the sweep refuses a checkout at any other HEAD.
4
+
5
+ **Every upstream statement below was re-checked against the live repository and the GitHub API on 2026-08-05.** Where a comparison loam used to make no longer holds, it has been removed rather than softened; those are noted where they mattered.
6
+
7
+ Primary upstream references: [v1.7.0 release](https://github.com/Fission-AI/OpenSpec/releases/tag/v1.7.0), [workflow](https://github.com/Fission-AI/OpenSpec/blob/v1.7.0/docs/workflows.md), [concepts](https://github.com/Fission-AI/OpenSpec/blob/v1.7.0/docs/concepts.md), [default schema](https://github.com/Fission-AI/OpenSpec/blob/v1.7.0/schemas/spec-driven/schema.yaml), [Stores](https://github.com/Fission-AI/OpenSpec/blob/v1.7.0/docs/stores-beta/user-guide.md), and the [derived artifact-state implementation](https://github.com/Fission-AI/OpenSpec/blob/45cca5db6137ed209117cc70510eb3e057fb981b/src/core/artifact-graph/state.ts).
8
+
9
+ ## The one-paragraph verdict
10
+
11
+ OpenSpec v1.7.0 is the lower-friction choice for planning and evolving behavior in a repository. Its action-oriented workflow includes explore, propose, apply, update, early spec sync, verify and archive; nested capability folders and `skip_specs: true` cover domain hierarchies and changes with no behavior delta. It also has beta Stores for Git-owned planning roots and read-only references, configurable artifact graphs, broad agent-tool integrations, and `RENAMED` requirements. loam targets a narrower fleet problem: it persists a **trust chain** (`sources` → content digests → human `vouch`), puts a **C4 service topology at the center**, joins behavior, architecture, and OpenAPI through one ID spine, and can accept scenario evidence from a real Cucumber run. That additional structure costs setup and concepts, and loam's ecosystem and release history are much younger.
12
+
13
+ ## Seven axes
14
+
15
+ | Axis | OpenSpec | loam |
16
+ |---|---|---|
17
+ | **System model** | Capability-oriented specs. Beta Stores place shared planning in a separate Git repo, but do not define service elements, relationships, or deployment views. | C4 landscape ([LikeC4](https://likec4.dev)) at the center; behavior spec, architecture spec, and OpenAPI joined by `FEAT-*`, service id, C4 element id, and `operationId`. |
18
+ | **Change workflow** | Fluid actions rather than fixed phases: explore/propose/apply/update/sync/verify/archive. Artifact status is derived from generated-file existence in a configurable dependency graph. Archive prepares and validates all target specs before sequential writes and supports already-synced no-ops, `RENAMED`, nested specs and spec-less changes. | Three-axis merge across requirements, OpenAPI, and landscape. The plan is computed first, gated for cross-axis coherence, staged, snapshotted, and rolled back if a write fails; `unarchive` restores recorded bytes. |
19
+ | **Implementation evidence** | `/opsx:verify` instructs the active coding agent to search code and tests for completeness, correctness, and coherence. It is useful but advisory and its conclusions depend on that run. The direction of travel is deeper *deliberative* verification: open PR [#1373](https://github.com/Fission-AI/OpenSpec/pull/1373) adds a fourth verify dimension whose own description says it reports evidence-backed warnings for human review, and does not block. | `loam verify` derives stable claims. Agent answers carry `file:line` evidence. A scenario claim is confirmed **mechanically** by a digest-matched green Cucumber run, and an agent may confirm one too — but the record then reads `verdict: "attested"`, not `verified`, and says so on every surface (`verify.scenario-attested`). The distinction is the point: loam does not claim agent answers are impossible, it claims they are *distinguishable*. |
20
+ | **Drift control** | CLI validation checks planning structure. Agent-led verify can find implementation drift on demand. There is no persisted attestation of any kind: `git grep -lE 'createHash\|sha256\|checksum' -- src` returns nothing at either baseline, so nothing written today can be compared mechanically against anything tomorrow. | `sources` name the code a doc was written from; `vouch` stamps source and content digests; later validation reports code movement (`sources.stale`) and doc movement (`content.stale`). **Scope, plainly:** `content.stale` is checkable from anywhere, but `sources.stale` resolves only inside the service's own repo — the docs-repo run counts what it could not check (`sources.unverifiable-from-here`). Both are warnings, so `validate --all` exits 0 on a fully stale fleet unless CI passes `--strict`. |
21
+ | **Multi-repo & topology** | Stores are real and shipped (v1.5.0, beta): standalone Git planning roots with root selection and read-only references. What they do not do is reconcile: the guide states it as a limitation — *"No sync, ever — by design. OpenSpec never clones, pulls, or pushes. A stale checkout shows stale specs until you pull"*, plus one checkout per store id per machine — and there is no lockfile or commit pin ([#1273](https://github.com/Fission-AI/OpenSpec/issues/1273), open since June). Two engineers can therefore read contradicting requirements for one spec id with no signal that they differ. Service topology and contract edges are outside the requirement model entirely. | One docs repo is one checkout, so "which version" is a git question with one answer; cross-service calls are checked against provider operationIds, and the fleet map is a first-class artifact rather than an inference. |
22
+ | **AI integration & entry curve** | Broader tool coverage, and growing: 34 tools at v1.7.0, selected interactively from a scan, with profiles, delivery modes and schemas. Plain Markdown makes the first change quick. | Narrower coverage: 20 tools, written for whichever ones the repo scan finds, plus `--json` on every command and `loam status` for what-to-do-next. LikeC4, frontmatter, and the operationId spine are still to learn before the fleet checks pay off. The question worth asking is not the count but whether your tool is on the list — and if it is not, `AGENTS.md` plus a binary on `PATH` is the whole integration. |
23
+ | **Who owns a generated file** | The tool does. `openspec update` rewrites its generated instructions; a file you edited is replaced, and the version stamp is what lets it skip the ones already current. | You do. loam stamps the same way — `<!-- generated by loam vX.Y.Z -->` in every command and skill body — but the stamp only makes drift *reportable* (`doctor.agent-files-stale`, `agents.stale`). loam never rewrites the file; the fix is a human reading it. That is a trade, not a win: upstream's upgrade path is one command, loam's is a review per repo. |
24
+
25
+ ## When OpenSpec is enough
26
+
27
+ Honesty cuts both ways. If you have **a single repository**, a small team, and **no cross-service API contracts**, most of what loam adds is weight you do not need: the C4 axis, two kinds of digest, an architecture requirement file. OpenSpec's "markdown spec and go" is simpler and, for that shape of project, sufficient. Use it.
28
+
29
+ ## When loam
30
+
31
+ - A **fleet** of services where a business-simple feature routinely spans several of them, and the interesting failures live on the edges — a consumer calling an operation the provider's contract no longer defines.
32
+ - A **legacy adoption campaign**: docs written largely by agents, where an unverified corpus invites exactly the quiet fictions the trust chain exists to catch.
33
+ - A team that wants the **done-check auditable**: a derived checklist where every answer carries a `file:line` and names who gave it, and where the answers a test runner produced are separated from the answers an agent asserted — mechanically, on every surface, rather than by convention. loam does not forbid the assertion; it refuses to let it read as a run.
34
+
35
+ ## What loam takes from OpenSpec
36
+
37
+ The core requirement shape: `### Requirement:` headings, `#### Scenario:` blocks, and the `## ADDED | MODIFIED | REMOVED Requirements` delta algebra. Seven upstream files are vendored as representative regression fixtures. Separate exact-commit sweeps cover living, active, and archived spec trees at both baselines: release 207 files / 739 requirements / 2273 scenarios and canary 209 / 742 / 2284, including parse/serialize/parse stability of requirement content. This is a Markdown-compatibility corpus, not a claim of full modern workspace compatibility. The multi-tool delivery pattern — one content source plus thin per-tool adapters, emitted both as a slash command and as an Agent Skill — is also borrowed from OpenSpec's integration design, including the per-tool paths its adapters document and the `<tool-dir>/skills/<name>/SKILL.md` skills convention **as it stood at v1.7.0**. That convention is no longer uniform upstream and loam should not claim it is: [#1511](https://github.com/Fission-AI/OpenSpec/pull/1511) (merged 2026-08-05) moved Codex's skills to a shared `.agents/` directory with `.codex/` kept as a legacy location, and the staged v1.8.0 adds a vendor-neutral `agents` target installing to `.agents/skills/`. loam's own layout stays per-tool-dir; the shape was borrowed once, not tracked. What is not borrowed at all is regeneration: `openspec update` rewrites its generated files, loam only reports that they have fallen behind.
38
+
39
+ ## What loam does not adopt wholesale
40
+
41
+ Each of these exists upstream and was considered, not overlooked. The long-form reasoning lives in [SCHEMA.md](SCHEMA.md) under "Considered and rejected".
42
+
43
+ - **Workflow/DAG engine** (artifact-graph, `schema.yaml`): OpenSpec derives artifact completion from generated-file existence; it is not a second mutable state database. loam still avoids a configurable graph because fleet-wide validation benefits from one versioned lifecycle and one meaning of green. The risk is project-to-project schema drift, not “one workflow per service”.
44
+ - **Stores as a second planning root**: OpenSpec's beta Stores solve a real cross-repo ownership problem with standalone Git planning repos, root selection and read-only references. loam currently keeps one shared docs repo because that repo also owns the service topology. A reference index may become useful if several independent fleet-doc roots appear; it is not dismissed as a cache.
45
+ - **`RENAMED` sections**: OpenSpec represents rename as heading FROM/TO. loam's preferred conversion assigns a stable `Requirement-ID` and carries it through a `MODIFIED` requirement, so the heading can change without losing identity. `REMOVED` + `ADDED` is only the legacy fallback when identity cannot be established.
46
+ - **Writing OpenSpec back** (bidirectional compatibility): serializing through loam drops `## Purpose` and the `## Requirements` wrapper, both of which OpenSpec's parser requires. Pretending otherwise would corrupt someone's repo politely. Migration is one-way: migrate, then retire the old tooling.
47
+ - **TUI and OpenSpec profiles/worksets**: profiles select installed workflow commands; worksets are personal folder/editor sets, not validator semantics or curated agent evidence bundles. loam keeps a smaller fixed command surface and derives its model-scoped views. Authored `tasks.md` is not authoritative in loam, but migration preserves it as a legacy checklist until a human explicitly disposes of it.
48
+
49
+ ## Architecture decisions: upstream considered them and chose otherwise
50
+
51
+ This is not a gap in OpenSpec's knowledge, and it should not be presented as one. [PR #566](https://github.com/Fission-AI/OpenSpec/pull/566) proposed ADRs as first-class documents — 1656 added lines across 13 files — and was closed **unmerged** on 2026-08-03. The stated reason is worth reading whole, because every item in it is a question loam had to answer too: the PR proposed a new default-core data model and CLI surface *while ADR ownership, lifecycle, concurrency, retrieval, and mutable-versus-immutable semantics remain unresolved*. The recommendation was to prove the workflow through configurable community schemas first, and that alternative is now documented upstream.
52
+
53
+ loam's answers are narrower and therefore cheaper: `adrs/` is presence-tracked only, ownership follows the directory it sits in (`services/<svc>/adrs/` belongs to that service's team via CODEOWNERS), there is no lifecycle beyond MADR's own and no mutable state, and retrieval is a filesystem walk. That is a *smaller* answer than the one #566 proposed, not a better one — it works because loam already forced a service topology to hang decisions on, and OpenSpec deliberately has no such unit. If your ADRs need a lifecycle, neither tool has one.
54
+
55
+ ## The asymmetry that does not go away
56
+
57
+ OpenSpec at the time of writing: 63,900 stars, 4,407 forks, a release every two to four weeks (v1.4.0 through v1.7.0 all landed inside ten weeks), and 21 commits merged to `main` in the five days before this paragraph was written. loam is one repository with no published package. Nothing in this document changes that, and a comparison that reports feature counts against a project moving that fast is a comparison loam loses on principle — which is why the axes above are about *what is checkable*, not about what exists.
58
+
59
+ Two consequences follow honestly. First, a class of defect upstream fixes inside 48 hours: [#1493](https://github.com/Fission-AI/OpenSpec/issues/1493), a report that generated workflows referenced flags that had moved, was closed completed on 2026-08-04 with its whole fix wave merged the same day. loam has had the same class of defect in its own generated instructions, so this is not an axis loam may cite. Second, anything upstream stages will stale a count here: v1.8.0 is already assembled in `main` and adds a vendor-neutral `agents` target and further tools.
60
+
61
+ ## Migrating
62
+
63
+ [MIGRATING-from-OpenSpec.md](MIGRATING-from-OpenSpec.md) — what carries over mechanically, how active changes and `RENAMED` become explicitly mapped loam features/stable-id `MODIFIED` deltas, what remains only in preserved legacy source (`## Purpose`, custom artifacts, complete-state prose), what must be added (`Operations:`/`Covers:`, OpenAPI, C4), and the disposition contract for every file in a real OpenSpec repo.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 loam contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,202 @@
1
+ # Migrating from OpenSpec
2
+
3
+ loam reads OpenSpec requirement Markdown from the outside: `### Requirement:` headings, `#### Scenario:` blocks, and `## ADDED|MODIFIED|REMOVED Requirements` delta sections. Compatibility is measured, not assumed.
4
+
5
+ Two upstream baselines are named separately:
6
+
7
+ - released behavior: **OpenSpec v1.7.0**, exact commit `4e16790d90d8f54d4773ad9a5e71a57cd9f1e86b`;
8
+ - compatibility canary: post-release `main` commit `45cca5db6137ed209117cc70510eb3e057fb981b` — a fixed historical pin, not a moving one. Upstream `main` advances daily; the canary exists to give the sweep a second reproducible corpus, and its totals are unchanged since it was taken.
9
+
10
+ The seven verbatim fixtures come from the main canary and run in routine CI. A scheduled/manual matrix separately checks the exact release and canary commits across living, active, and archived spec trees: release 207 files / 739 requirements / 2273 scenarios; canary 209 / 742 / 2284. These test requirement/scenario parsing, not every modern workspace feature. Provenance and checksums are in [`test/fixtures/openspec/README.md`](test/fixtures/openspec/README.md).
11
+
12
+ ## Start with a read-only audit
13
+
14
+ ```bash
15
+ loam audit-openspec /path/to/repo
16
+ loam audit-openspec /path/to/repo --json
17
+ ```
18
+
19
+ The input may be a repository containing `openspec/`, the `openspec/` directory itself, or a Store checkout. Modern `config.yaml`, Store metadata, nested capability folders, per-change `.openspec.yaml`, `skip_specs: true`, and project custom schemas are inventoried.
20
+
21
+ For a **Store checkout** the planning shape decides the root and the store marker decides only the kind: the planning content may sit at the checkout root or under `<checkout>/openspec`, and a checkout with a `.openspec-store/store.yaml` but planning content in neither place is refused by name rather than audited as an empty, compatible workspace. Planning content in *both* places is the pre-existing `Ambiguous OpenSpec root` refusal, unchanged. A root that holds no living spec and no active change at all is never `ready` (`openspec.workspace-empty`): a verdict over a corpus nobody read is the one answer this command must not be able to give.
22
+
23
+ Audit separates four different facts:
24
+
25
+ - `readiness.living` — living specs are mechanically readable;
26
+ - `readiness.active` — active deltas will not silently strand content;
27
+ - capability/service mappings, active change/feature mappings, rename identities, and authored artifact dispositions — human decisions still required (`readiness.mappingsResolved`, `changesResolved`, `renamesResolved`, and `dispositionsResolved` stay separate);
28
+ - `archiveDiagnostics` — anomalies in frozen history, reported but never allowed to block migration of living/active truth.
29
+
30
+ An audit that completed exits successfully even when it found blockers. Root, YAML and I/O failures still fail. This makes “the audit ran” distinct from `ready`/`mechanicallyCompatible`.
31
+
32
+ ## Generate and complete the mapping
33
+
34
+ Audit is read-only by default. An explicit output path outside the OpenSpec source writes a non-overwriting skeleton:
35
+
36
+ ```bash
37
+ loam audit-openspec /path/to/repo --write-mapping /tmp/openspec-map.yaml
38
+ ```
39
+
40
+ The mapping is versioned and bound to both the canonical planning root and a SHA-256 inventory digest. That digest covers the **living tree and the active changes only** — not `changes/archive/**`. Frozen history is reported as diagnostics that never gate, so letting a colleague's typo fix inside an archived change invalidate a 900-line mapping somebody spent a day completing was a cost with no safety behind it. An edit to a living spec or an active delta still invalidates it, which is the case the binding exists for.
41
+
42
+ Digest and artifact paths are planning-root relative regardless of whether audit receives the repository container or its `openspec/` directory (`@workspace/` is reserved for Store metadata outside that root):
43
+
44
+ ```yaml
45
+ version: 1
46
+ source:
47
+ root: /path/to/repo/openspec
48
+ inventoryDigest: sha256:...
49
+
50
+ capabilities:
51
+ payments/refunds:
52
+ services:
53
+ - payment-service
54
+ suggestedServices:
55
+ - refunds
56
+ requirementServices:
57
+ Authorize: []
58
+
59
+ changes:
60
+ add-refund:
61
+ feature: FEAT-12
62
+ suggestedFeature: FEAT-1
63
+ title: Add refund
64
+
65
+ renames:
66
+ "changes/rename/specs/payments/spec.md:1:1":
67
+ from: Old name
68
+ to: New name
69
+ existingRequirementId: null
70
+ requirementId: payments.authorize
71
+
72
+ artifacts:
73
+ changes/add-refund/proposal.md:
74
+ kind: proposal
75
+ disposition: convert-to-intent
76
+ suggestedDisposition: convert-to-intent
77
+ changes/add-refund/tasks.md:
78
+ kind: tasks
79
+ disposition: preserve-as-legacy-checklist
80
+ suggestedDisposition: preserve-as-legacy-checklist
81
+ ```
82
+
83
+ `suggestedServices`, `suggestedFeature`, and `suggestedDisposition` are hints, not decisions. The fields that count are `services`, an explicit `changes.<OpenSpec-id>.feature` plus `title`, `requirementId` where needed, and `disposition`. Feature ids use loam's `<word>-<number>` grammar and must be unique across active changes.
84
+
85
+ ### Capability → service is a human decision
86
+
87
+ OpenSpec's unit is a capability; loam's unit is a service bound to the C4 landscape. A nested capability id stays nested (`payments/refunds`) rather than collapsing to `payments`. Mapping scope is the union of living capabilities and capabilities found only in active deltas, so a brand-new nested capability cannot disappear merely because it has not reached `specs/` yet.
88
+
89
+ - With one selected service, every requirement in the capability goes there.
90
+ - With several selected services, `requirementServices` must allocate every living **and active** requirement to one or more of them. Apply refuses an omitted allocation or a service outside the declared list.
91
+ - If two mapped capabilities would create the same heading/`Requirement-ID` in one service, apply refuses instead of guessing which requirement wins.
92
+
93
+ ### Change → feature is explicit
94
+
95
+ Every active OpenSpec change gets one `changes.<id>` entry. Fill both the loam feature id and the human title. The skeleton deliberately leaves `feature: null` next to a deterministic suggestion; suggestions never make a migration ready. Unknown OpenSpec change ids, malformed feature ids, blank titles, and duplicate feature ids block apply.
96
+
97
+ ### RENAMED keeps identity
98
+
99
+ Audit parses every OpenSpec FROM/TO pair and gives it a stable mapping key. FROM must select exactly one living requirement in that delta's capability. If that requirement already has a `Requirement-ID`, the skeleton reuses it and refuses a replacement; otherwise assign a valid id. Apply places that id on the staged living source and emits a `MODIFIED` requirement with the TO heading, the same body/scenarios, and an `OpenSpec-Living-Source` annotation. Conflicting targets, duplicate sources/targets, identity collisions, and multi-change rename chains block instead of being flattened to `REMOVED` + `ADDED`.
100
+
101
+ ### Authored artifacts require explicit disposition
102
+
103
+ Every active `proposal.md`, `design.md`, and `tasks.md` appears in the mapping with `disposition: null` until a human chooses. A suggestion does not make migration ready.
104
+
105
+ Typical choices are:
106
+
107
+ - proposal → `convert-to-intent`;
108
+ - design → `review-as-feature-adr` (proposed decision material, not automatically an accepted ADR);
109
+ - tasks → `preserve-as-legacy-checklist` until progress/order information has been reviewed;
110
+ - `retain-read-only` or `manual-review` when conversion would overstate what is known.
111
+
112
+ ## Dry-run and explicit apply
113
+
114
+ Validate the completed mapping without writing anything:
115
+
116
+ ```bash
117
+ loam migrate-openspec /path/to/repo --map /tmp/openspec-map.yaml
118
+ loam migrate-openspec /path/to/repo --map /tmp/openspec-map.yaml --json
119
+ ```
120
+
121
+ `--mapping` remains a deprecated spelling of `--map`. Dry-run is always the default and never creates the target.
122
+
123
+ Writing requires both flags:
124
+
125
+ ```bash
126
+ loam migrate-openspec /path/to/repo \
127
+ --map /tmp/openspec-map.yaml \
128
+ --apply \
129
+ --target /tmp/loam-migration-review
130
+ ```
131
+
132
+ Before apply, loam repeats the audit and compares the fresh source root/digest with the mapping. Any living or active source edit since review invalidates the mapping. The target must be absent or empty, must not overlap the OpenSpec source, and **must not sit inside a live loam fleet**: a target under the `docsDir` of a governing `loam.json` — or under any directory holding `architecture/landscape.likec4` — is refused, because every staged feature would otherwise be enumerated by `loam list` and `loam validate --all` as a phantom feature of the real fleet. A sibling of `docsDir` is fine. Writes are staged and swapped with rollback; source files and live loam docs are never modified, and every refusal above leaves the target directory absent rather than half-written.
133
+
134
+ The target contains:
135
+
136
+ - `services/<service>/spec.md` with mapped living requirements and `status: draft`;
137
+ - `features/<FEAT>-<slug>/intent.md` for every active change;
138
+ - `features/<FEAT>-<slug>/specs/<service>/spec.md` with routed ADDED/MODIFIED/REMOVED sections for every non-`skip_specs` change;
139
+ - feature ADR/legacy files according to the explicit proposal/design/tasks dispositions, plus an exact read-only copy of the complete source change tree under `legacy/openspec/`, **and the living capability tree verbatim under `legacy/openspec/specs/`** — `## Purpose` prose, section prose between a heading and its first requirement, and capability `design.md` have no loam equivalent, so they are copied rather than converted and nothing is silently lost;
140
+ - `migration-plan.json` with active changes, mappings, archive diagnostics and every artifact disposition;
141
+ - normalized `mapping.yaml`;
142
+ - `FOLLOW-UP.md` naming the work that still blocks a trustworthy fleet;
143
+ - and the target's own `loam.json` (`{"docsDir": "."}`), `AGENTS.md`, and an **empty** `architecture/landscape.likec4`. The target is therefore a standalone docs repo, which is what makes `FOLLOW-UP.md`'s instructions runnable where they are written: `loam validate --all`, `loam status`, `loam rebase <FEAT>` and `loam doctor` all work inside it. The landscape is laid down empty on purpose — OpenSpec carries no topology, and a generated one would be a guess presented as the map — so every staged service surfaces as exactly one `landscape.service-unmodelled`, which is follow-up item one.
144
+
145
+ This is deliberately called **staged migration docs**, not a finished or green loam repository. Apply uses the reviewed feature ids and materializes active changes, but it does not invent C4 topology/deltas, OpenAPI contracts, source provenance, or vouch evidence. `skip_specs: true` changes still receive intent and legacy artifacts but no feature spec delta. Every feature therefore remains review material until the follow-up checklist is complete.
146
+
147
+ ## What carries over mechanically
148
+
149
+ The exact-commit corpus gate checks living, active, and archived spec trees. OpenSpec v1.7.0 release `4e16790` contributes 207 Markdown files, 739 requirements, and 2273 scenarios; main canary `45cca5d` contributes 209, 742, and 2284. Both sweeps check parse/serialize/parse stability of requirement content. Nested bullets, fenced markup, removal prose, CRLF, BOM and non-ASCII names survive. `REMOVED` requirements with no scenarios remain legal.
150
+
151
+ Modern ADDED/MODIFIED/REMOVED deltas are readable, but “readable” is not “ready”: routing to services, loam frontmatter, Operations/Covers links and feature identity still require decisions.
152
+
153
+ ## Shapes that need repair or review
154
+
155
+ - **Mixed legacy complete-state deltas.** Any BASE requirement stranded under `## Behavior`, `## Error Handling` or another prose heading is reported even if the same file also contains a valid ADDED/MODIFIED section. Re-home it before conversion.
156
+ - **`## Requirements` inside an active change delta** is a **blocker** (`openspec.change-quoted-requirements`), which needs saying plainly because it is the shape OpenSpec's own living-spec template mandates and it looks correct. In a *living* spec that heading is the requirements section; in a *change delta* it is the one explicitly non-merging quote section, so requirements sitting under it stage nothing. loam refuses rather than counting requirements it cannot route — which is also what guarantees the per-change requirement counts in `migration-plan.json` equal what apply actually stages. Re-home them under `ADDED`, `MODIFIED` or `REMOVED`.
157
+ - **Markdown under `specs/` named neither `spec.md` nor `design.md`** (`openspec.nonstandard-living-spec`) — a `Spec.md` on a case-sensitive filesystem, a `spec.markdown`, a hand-split `part-2.md`. No capability reads it, so migrating would leave its requirements behind. This is the living twin of the same check on change specs.
158
+ - **Dot-prefixed directories under `changes/`** (`openspec.hidden-change-directory`) — a `.wip-refunds/` convention. They are not enumerated as changes, so nothing under them migrates. loam blocks rather than walking them: rename or move the directory before migrating, and its content is never silently dropped. Under `changes/archive/` the same finding is an archive diagnostic and does not gate, because frozen history never blocks.
159
+ - **An ISO-8601 `created` timestamp** in `.openspec.yaml` is a valid date. It used to fail the whole workspace; a calendar date with an optional time and offset is now accepted.
160
+ - **Spec-less changes.** They are valid only when a present, valid `.openspec.yaml` explicitly sets `skip_specs: true` and its named built-in or project custom schema resolves. A custom artifact graph never implies this opt-out by itself. Otherwise an active zero-delta change is a blocker. Explicit `skip_specs` suppresses generated feature specs while preserving intent, metadata, and authored legacy material.
161
+ - **Malformed RENAMED.** A rename-only delta is not “empty”, but every section must supply a FROM/TO pair and every active pair needs an identity decision.
162
+ - **External Store pointer.** A config-only code repo with `store: <id>` identifies external planning. Audit the registered Store checkout itself; loam does not guess a machine-local registry path.
163
+ - **Frozen archive history.** Legacy shapes are diagnostics only. Keep `changes/archive/` read-only where it is; do not reconstruct it as loam `features/archive/`, whose entries imply loam computed and snapshotted the merge.
164
+
165
+ ## What must be added after staged apply
166
+
167
+ - Truthful `sources:` paths from each service repository, followed by human `loam vouch`; staged specs intentionally remain `status: draft`.
168
+ - `Operations:` links to provider OpenAPI `operationId`s, and `Covers:` links where architecture requirements are created.
169
+ - `architecture/landscape.likec4`, service `model.likec4`, and explicit fleet relationships.
170
+ - Service OpenAPI contracts and provider-before-consumer adoption where inbound edges already name operations.
171
+ - LikeC4 and OpenAPI deltas for the already mapped active feature ids (`FEAT-12`, `BUG-42`, and so on); OpenSpec's prose change id/title remains visible in the staged slug, annotations, plan, and preserved source tree.
172
+ - Human disposition of `config.yaml.context`, per-artifact rules, custom schemas/templates, Stores/references, Purpose prose and generated tool instructions.
173
+
174
+ ## Modern artifact disposition guide
175
+
176
+ | OpenSpec artifact | Migration treatment |
177
+ |---|---|
178
+ | `config.yaml` (`schema`, `context`, `rules`, optional `store`) | Inventory and review. Move durable operating context into the docs contract; do not silently convert workflow rules into different validator semantics. |
179
+ | `specs/<nested/capability>/spec.md` | Map requirements to one or more services. Apply stages combined `services/<svc>/spec.md` files. |
180
+ | `specs/<capability>/design.md` | Review as a service ADR; do not mark accepted merely because the file existed. |
181
+ | `changes/<id>/.openspec.yaml` | Preserve schema/`skip_specs`/created metadata in the plan and exact feature-local legacy tree. |
182
+ | `changes/<id>/proposal.md` | Explicit `convert-to-intent`, or retain/manual-review in feature legacy material; the original is also preserved. |
183
+ | `changes/<id>/specs/**/spec.md` | Validate and route ADDED/MODIFIED/REMOVED requirements into the mapped feature/service delta, preserving delta kinds; keep the original beneath `legacy/openspec/`. |
184
+ | `changes/<id>/tasks.md` | Preserve as a non-authoritative feature-local legacy checklist with its explicit disposition until reviewed. |
185
+ | `changes/<id>/design.md` | Stage as proposed feature ADR material for `review-as-feature-adr`, otherwise retain as legacy; never imply acceptance merely because it existed. |
186
+ | `changes/archive/**` | Keep read-only as OpenSpec history; diagnostics do not gate living/active readiness. |
187
+ | `schemas/<name>/schema.yaml` and templates | Review custom workflow semantics. The schema name must resolve as a direct portable member of `schemas/`; even a workflow with no specs artifact still needs explicit valid `skip_specs: true` for a zero-spec change. |
188
+ | `.openspec-store/store.yaml`, `references`, Worksets | Record planning ownership/references for humans. They do not become a second loam fleet topology automatically. |
189
+ | OpenSpec-generated skills/commands/instruction blocks | Remove after cutover so agents do not receive two live process contracts. |
190
+
191
+ ## Cutting over
192
+
193
+ The staged target has two halves, and only one of them moves.
194
+
195
+ 1. Work `FOLLOW-UP.md` until `loam validate --all` **inside the target** is green. That is the gate; nothing before it means anything, because a staged target is deliberately red on arrival.
196
+ 2. Move `services/` and `features/` into the live fleet.
197
+ 3. Leave the rest behind: `legacy/`, `mapping.yaml`, `migration-plan.json`, `FOLLOW-UP.md`, and the target's own `loam.json` and `AGENTS.md` are review residue. They exist so a human can check the conversion, and the live fleet has its own copies of the last two.
198
+ 4. Remove OpenSpec's generated skills, commands and instruction blocks from every repo, so agents stop receiving two live process contracts.
199
+
200
+ ## Migration is one-way
201
+
202
+ loam reads OpenSpec; it never writes back into the OpenSpec workspace. Loam serialization does not preserve OpenSpec's required `## Purpose` and `## Requirements` framing as an OpenSpec round-trip contract — but the source framing is not lost either: the verbatim living tree under `legacy/openspec/specs/` is what makes "no authored artifact is silently lost" cover living capabilities and not only change trees. Migrate into a separate target, review it, cut over once, and retire the old tooling only after the staged follow-up is complete.