@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
@@ -30,6 +30,7 @@ import { existsSync } from "node:fs";
30
30
  import { mkdir, writeFile } from "node:fs/promises";
31
31
  import { dirname, join } from "node:path";
32
32
  import { agentsStampLine } from "./agents-stamp.js";
33
+ import { featureIdProblem, serviceIdProblem } from "./ids.js";
33
34
  import { LOAM_VERSION } from "./version.js";
34
35
  export const AGENTS_MD = `${agentsStampLine(LOAM_VERSION)}
35
36
  # Working in this docs repo
@@ -227,15 +228,28 @@ holds of the new words.
227
228
  Write \`sources\` for anything you author from reading code, and leave \`status: draft\`
228
229
  until a human has read it. Promoting draft to verified is their call, not yours, and
229
230
  it has its own command — \`loam vouch --service <id>\`, run inside that service's repo,
230
- which stamps the status, the date, both digests and the per-file index
231
+ which stamps the status, the date, the identity (\`vouched_by\`, from git), both digests
232
+ and the per-file index
231
233
  (\`sources_files\`, which is what lets a later \`sources.stale\` name the paths that
232
234
  moved) together — into the living spec.md
233
235
  and, when the service has one, arch.spec.md, in one all-or-nothing run: a file whose
234
- sources cannot be verified refuses the whole vouch. Never write those five fields by
236
+ sources cannot be verified refuses the whole vouch. Never write those six fields by
235
237
  hand: a status with no digest behind it is a claim with nothing behind it. If the
236
238
  document changed under vouch between the read and the write, it refuses with
237
239
  \`vouch-raced\` and stamps nothing — re-read and re-run.
238
240
 
241
+ **\`loam vouch\` is not yours to run**, and it is built so that it cannot be. It is
242
+ the one command whose output is a claim about a HUMAN act — everything else loam
243
+ checks is internal consistency, which fluent prose satisfies on its own — so it
244
+ refuses \`vouch-unattended\` when stdin is not a terminal (and in \`--json\` mode,
245
+ where a question cannot be asked), answers \`vouch-declined\` when the person says
246
+ no, and refuses \`vouch-unattributable\` when git can name nobody, because a stamp
247
+ with no \`vouched_by\` behind it records only that the word was written. \`--yes\`
248
+ exists for a person's own scripted run; typing it on someone's behalf is
249
+ writing their name. The generated command and skill files pre-approve loam's
250
+ read-only and authoring verbs one by one and deliberately not this one: when
251
+ the work is done, hand back and say a vouch is owed.
252
+
239
253
  ## The ID spine
240
254
 
241
255
  Three artifacts describe the same call in three languages. They are joined by the
@@ -357,6 +371,14 @@ says so once and stops the warning for good.
357
371
  session: it says what is in flight and what the next step is, for the fleet or
358
372
  for one feature (\`loam status FEAT-101 --json\`). Then \`loam list --json\` and
359
373
  \`loam show <service> --json\`.
374
+ Then \`loam explore <service> --json\` around the services you believe are
375
+ involved — it writes nothing and answers the one question the next step takes
376
+ as an argument: which services this change actually touches. It reports the
377
+ ring one hop out in the fleet map (\`neighbours\`), how far each service's
378
+ documentation has got (\`maturity\`), what each already exposes, the features
379
+ already in flight over the same ground (\`overlaps\`), and the \`loam new\` line
380
+ the seeds imply (\`scaffold\`). \`--op <operationId>\` seeds from an operation
381
+ when you know the call but not who owns it.
360
382
  Never propose a change to a service you have not read.
361
383
  3. **Scaffold** — \`loam new FEAT-101 --title "..." --touches <existing> --new-service <new>\`.
362
384
  The scaffold leaves a \`delta.likec4\` whose context elements are commented out —
@@ -618,6 +640,10 @@ selected that tool. The map of which invocation surfaces what:
618
640
  archive/unarchive re-run, which repairs from the pre-image first, under the lock
619
641
  — except when the journal itself cannot be read, where it is \`loam doctor\` and
620
642
  the repair is a human's comparison against version control. Fleet-wide the rest are
643
+ \`next.adopt-bound\` (this repository's own loam.json names a service the docs repo
644
+ has no directory for at all — it outranks every other service's partial adoption,
645
+ because it is the only step that is about the repo you are standing in, and it is
646
+ the same state \`loam doctor\` reports as \`doctor.service-unknown\`),
621
647
  \`next.adopt\` (a service with no spec.md — nothing about it is written down, so
622
648
  no feature can be graded against it), \`next.complete-service\` (a living spec.md
623
649
  with no model.likec4 beside it), \`next.feature\` (something is in flight;
@@ -661,7 +687,11 @@ selected that tool. The map of which invocation surfaces what:
661
687
  \`api.ungoverned\`, \`api.ops-unlinked\`,
662
688
  \`api.requirement-deprecated\`, \`spec-api.op-undefined\` (the living spec's own
663
689
  \`Operations:\` lines, not only a delta's), \`spine.landscape-invalid\`, \`spine.op-undefined\`,
664
- \`spine.op-link-missing\`, \`spine.op-deprecated\`, and
690
+ \`spine.op-link-missing\`, \`spine.op-deprecated\`,
691
+ the async contract axis (AsyncAPI 3): \`service.no-asyncapi\`, \`asyncapi.invalid\`,
692
+ \`asyncapi.duplicate-message\`, \`spine.message-undefined\`,
693
+ \`spec-event.message-undefined\`, \`spine.message-unproduced\`,
694
+ \`asyncapi.message-contested\`, \`event.messages-unlinked\`, \`event.covered\`, and
665
695
  the architecture spec axis: \`covers.unknown\`, \`health.invalid\`, \`health.uncovered\`. Run inside
666
696
  the service's own repo, once a generated suite exists under
667
697
  \`<gherkinDir>/loam/\`, it also grades that suite against the living specs:
@@ -735,7 +765,15 @@ selected that tool. The map of which invocation surfaces what:
735
765
  \`doctor.docs-unreadable\`, \`doctor.docs-readonly\`, \`doctor.docs-absolute\`
736
766
  (\`docsDir\` stored as an absolute path in a committed loam.json — it resolves
737
767
  only on the machine that ran \`loam init\`), \`doctor.inventory-unreadable\`,
738
- \`doctor.landscape-missing\`, \`doctor.service-unbound\`, \`doctor.service-unknown\`,
768
+ \`doctor.landscape-missing\`, \`doctor.service-unbound\` (no \`service\` in loam.json —
769
+ never raised inside the docs repo itself, where having none is the correct state
770
+ and binding one would be meaningless), \`doctor.service-unknown\` (loam.json names
771
+ a service the docs repo has no directory for — the same state \`loam status\`
772
+ reports as \`next.adopt-bound\`), \`doctor.likec4-config-missing\` (the docs repo
773
+ has no \`likec4.config.json\`, so pointing LikeC4's own renderer at it fails:
774
+ loam parses each \`.likec4\` file alone and each declares its own
775
+ \`specification\` block, so a workspace load merges them and every declaration
776
+ reads as a duplicate — the \`fix\` field carries the exact file to write),
739
777
  and the two about this repo's own generated command and skill files:
740
778
  \`doctor.agent-files-missing\` — some of them are absent, because the repo was
741
779
  initialized by an older binary or they were deleted — and
@@ -779,6 +817,24 @@ selected that tool. The map of which invocation surfaces what:
779
817
  operationIds, never from validator prose. It is the answer to every
780
818
  "another feature in flight does this first" finding, and to
781
819
  \`delta.added-conflict\` / \`delta.modified-conflict\`, which no ordering fixes.
820
+ - \`loam explore [<service>...]\` reads the fleet around a change nobody has
821
+ written down yet, and writes nothing. It exists because \`loam new\`'s
822
+ \`--touches\` list is the hardest call in the cycle and the only one nothing
823
+ downstream catches: a list short by one service produces a feature that
824
+ validates, archives and ships with a consumer nobody updated. What it derives
825
+ is mechanical — the one-hop ring, each service's rung, its living operations,
826
+ who already calls whom, and which active features cover the same services. It
827
+ does NOT decide which of those neighbours you actually change; that is a
828
+ judgement about intent, and loam does not make those.
829
+ - \`loam instructions [<workflow>] [args...]\` prints one of the six workflow
830
+ protocols — \`loam-adopt\`, \`loam-feature\`, \`loam-implement\`, \`loam-check\`,
831
+ \`loam-verify\`, \`loam-ship\` — with \`$1\`, \`$2\` filled in from the arguments you
832
+ pass. The protocol ships inside the binary, so it describes the loam you are
833
+ about to run rather than the one that scaffolded the repository; the command
834
+ and skill files \`loam init\` writes are pointers at it. It reads no
835
+ \`loam.json\` and no docs repo, deliberately: \`loam-adopt\`'s own first step is
836
+ to run \`loam init\` when there is no config, so it cannot be the step that
837
+ requires one.
782
838
  - \`loam audit-openspec <root>\` is the read-only OpenSpec inventory; an explicit
783
839
  \`--write-mapping\` writes a non-overwriting, digest-bound decision skeleton
784
840
  outside the source tree. \`loam migrate-openspec <root> --map <file>\` validates
@@ -877,13 +933,15 @@ it. It changes the exit code and nothing else:
877
933
  what it was. The stricter grade is a per-invocation lever, visible in the CI
878
934
  pipeline that passes the flag — deliberately not a per-repo profile.
879
935
 
880
- \`loam delta <FEAT> --json\` exits 1 when \`architecture.errors\` is non-empty, with
881
- \`ok: true\` and the full payload intact: the empty C4 slice means the delta did
882
- not parse, not that the feature changes no architecture. Branch on the exit code
883
- before consuming the slice as a task brief.
936
+ \`loam delta <FEAT> --json\` exits 1 when either authored document behind the brief
937
+ failed to parse — \`architecture.errors\` non-empty, or \`openapi.unreadable\` true —
938
+ with \`ok: true\` and the full payload intact: the empty C4 slice means the delta
939
+ did not parse, and an empty \`api\` beside \`openapi.unreadable\` means the contract
940
+ delta did not parse, neither of them "this feature changes nothing there". Branch
941
+ on the exit code before consuming either slice as a task brief.
884
942
 
885
943
  Findings with severity \`ok\` are confirmations, not work: \`c4.valid\`, \`delta.valid\`,
886
- \`requirements.covered\`, \`api.covered\`, \`spine.resolved\`, \`coherence.ok\`,
944
+ \`requirements.covered\`, \`api.covered\`, \`spine.resolved\`, \`event.covered\`, \`coherence.ok\`,
887
945
  \`landscape.matched\`, \`archedge.covered\`, \`sources.resolved\`, \`sources.current\`,
888
946
  \`gherkin.current\`.
889
947
 
@@ -1008,6 +1066,17 @@ const COMMANDS = [
1008
1066
  name: "loam-adopt",
1009
1067
  description: "Adopt a service — write its baseline docs from its code, as draft, then validate",
1010
1068
  argumentHint: "<service-id>",
1069
+ purpose: "Write one service's baseline documentation into the docs repo from its code, as `draft`. You read the code; loam states the work and checks the result — it never reads the service, so anything you cannot show, do not write.",
1070
+ invocation: "loam instructions loam-adopt $1",
1071
+ placeholders: ["service"],
1072
+ spine: [
1073
+ "wire this repo (`loam init`, then `loam doctor`) if it has no ./loam.json yet",
1074
+ "`loam adopt` — the brief: every file to write, the grammar of each, and what the fleet map already says",
1075
+ "read the code, keeping the list of every path you open — that list becomes `sources`",
1076
+ "write the artifacts, everything `status: draft`",
1077
+ "validate the service, then validate the whole fleet — a baseline that passes one and fails the other is documented and invisible",
1078
+ "hand back with what you could not determine from the code, and let a human vouch",
1079
+ ],
1011
1080
  body: `Write one service's baseline documentation into the loam docs repo (its path is
1012
1081
  \`docsDir\` in ./loam.json). You read the code; loam states the work and checks the
1013
1082
  result. It never reads the service — so anything you cannot show, do not write.
@@ -1068,6 +1137,19 @@ behaviour nobody can find is the one failure mode none of loam's checks can catc
1068
1137
  name: "loam-feature",
1069
1138
  description: "Start a new loam feature — scaffold it, then author the C4 delta and requirement deltas",
1070
1139
  argumentHint: `<FEAT-id> "<title>"`,
1140
+ purpose: "Start a feature in the docs repo: decide which services it touches, scaffold it, then author the C4 delta and one requirement delta per service. The `--touches` list is the hardest call in the cycle and nothing downstream catches one that is short by a service.",
1141
+ invocation: 'loam instructions loam-feature $1 "$2"',
1142
+ // $2 is the feature's human title: any string is a legal one.
1143
+ placeholders: ["feature", "free"],
1144
+ spine: [
1145
+ "read `AGENTS.md` at the docs repo root — it defines the ID spine everything else depends on",
1146
+ "`loam explore` around the services you think are involved: the ring one hop out, and what is already in flight over the same ground",
1147
+ "`loam status` / `loam list` / `loam show` — what exists and what is already owed",
1148
+ "`loam new` with a `--touches` per service it changes and a `--new-service` per service it introduces",
1149
+ "author what the templates left as TODO: intent, the C4 delta, the per-service spec / arch spec / openapi",
1150
+ "`loam rebase` — pin every MODIFIED requirement and every operation to the living version you wrote against",
1151
+ "validate the feature, then read `loam dependencies` before anyone starts building",
1152
+ ],
1071
1153
  body: `Start a new feature in the loam docs repo (its path is \`docsDir\` in ./loam.json).
1072
1154
 
1073
1155
  1. Read \`AGENTS.md\` at the root of the docs repo first — it defines the ID spine
@@ -1080,6 +1162,27 @@ behaviour nobody can find is the one failure mode none of loam's checks can catc
1080
1162
  narrows it to one feature once $1 exists
1081
1163
  - \`loam list --json\` — what services exist, and what documentation they are missing
1082
1164
  - \`loam show <service> --json\` — what a service owns, exposes, and who already calls it
1165
+ 2b. \`loam explore <service> --json --as $1\` around every service you believe this
1166
+ feature touches. This is the step that decides step 3's \`--touches\` list, and it
1167
+ is the only decision in the cycle nothing downstream catches — a list short by one
1168
+ service produces a feature that validates, archives and ships with a consumer
1169
+ nobody updated. Read four fields:
1170
+ - \`neighbours\` — the services one hop out in the fleet map. Weigh each one. loam
1171
+ knows they are connected, not whether you change them, and it deliberately
1172
+ leaves them OUT of the suggested command line rather than guessing for you
1173
+ - \`services[].maturity\` — a service at \`empty\` or \`partial\` has no baseline to
1174
+ write a delta against, so the work starts with \`loam adopt --service <id>\`, not
1175
+ with this feature
1176
+ - \`services[].operations\` — what each already exposes, so an operation you are
1177
+ about to "add" that is already there becomes a MODIFIED requirement instead
1178
+ - \`overlaps\` — active features already carrying a delta for these services.
1179
+ \`loam dependencies --json\` (step 7) then says which must archive first
1180
+ \`--op <operationId>\` seeds from an operation when you know the call but not who
1181
+ owns it. A seed naming no \`services/<id>/\` is reported in \`unknown\` with the
1182
+ closest real ids rather than refused — the feature may be introducing it, and a
1183
+ typo looks identical until you read that list. \`scaffold\` is the literal step-3
1184
+ command line the seeds imply, with \`--new-service\` where the service does not
1185
+ exist yet.
1083
1186
  3. Scaffold it: \`loam new $1 --title "$2"\`, adding \`--touches <id>\` for every service the
1084
1187
  feature touches and \`--new-service <id>\` for every one it introduces. Every id goes
1085
1188
  through the \`services/<id>/\` grammar, so a typo is refused (\`invalid-option\`)
@@ -1123,6 +1226,18 @@ line, and the OpenAPI \`operationId\` — must match exactly.
1123
1226
  name: "loam-implement",
1124
1227
  description: "Implement a loam feature's slice for one service — generated gherkin first, tests from it, then code",
1125
1228
  argumentHint: "<FEAT-id> [service]",
1229
+ purpose: "Build one service's part of a feature, in that service's own repository. The generated Gherkin is the acceptance criterion — it comes before the step definitions, and those come before the code.",
1230
+ invocation: "loam instructions loam-implement $1 $2",
1231
+ placeholders: ["feature", "service"],
1232
+ spine: [
1233
+ "`loam status` — where this feature actually is, and what is owed before you start",
1234
+ "`loam delta` — the task: the intent, every requirement verbatim, the endpoints, and the calls in and out",
1235
+ "`loam gherkin` in the service's repo — the digest-stamped `.feature` files. Never edit them",
1236
+ "write step definitions for the generated scenarios first, outside the generated directory",
1237
+ "implement until the suite passes, running it with a JSON report",
1238
+ "honour the contract in both directions: expose every inbound operation, call every outbound one",
1239
+ "`loam rebase` if building made you change the feature's documents at all, then validate the feature",
1240
+ ],
1126
1241
  body: `Implement one service's part of a feature. Every step runs in the SERVICE'S own
1127
1242
  repository, which needs its own committed ./loam.json — if there is none,
1128
1243
  \`loam init --docs <path-to-docs-repo> --service $2\` then \`loam doctor\` first.
@@ -1142,11 +1257,19 @@ repository, which needs its own committed ./loam.json — if there is none,
1142
1257
  - \`architecture\` — whether this service is new, and the calls in and out of it
1143
1258
  - \`api\` — the endpoints this feature adds or retires for this service: path,
1144
1259
  method, operationId and summary, with removals spelled \`REMOVE <METHOD> <path>\`
1260
+ - \`openapi\` — whether that contract could be read at all: \`unreadable\`, plus an
1261
+ \`error\` when the parser gave a message. It rides beside \`api\` rather than
1262
+ inside it because \`api\` stays the operations array it has always been, and it
1263
+ is what tells an empty \`api\` apart from a contract delta that did not parse
1145
1264
  - \`services\` — every service this feature projects onto, so a run without
1146
1265
  \`--service\` tells you which ones to ask for rather than guessing
1147
- Exit 1 with \`ok: true\` means \`architecture.errors\` is non-empty: delta.likec4 did
1148
- not parse, and the empty C4 slice is a parse failure, not "no architecture change".
1149
- Stop and fix the delta (\`loam validate $1 --json\`) before building anything.
1266
+ Exit 1 with \`ok: true\` means one of the two authored documents behind this brief
1267
+ did not parse: \`architecture.errors\` non-empty (delta.likec4), or
1268
+ \`openapi.unreadable\` true (the feature's openapi.yaml for this service). The empty
1269
+ slice is the parse failure itself, not "nothing changes there". Stop and fix that
1270
+ document before building anything — for the delta, \`loam validate $1 --json\` names
1271
+ the error; the contract delta is not graded by validate, so read the parser message
1272
+ the payload carries.
1150
1273
  2. In the service's repo, \`loam gherkin $1 --json\` — one \`.feature\` file per changed
1151
1274
  requirement lands under \`<gherkinDir>/loam/\` (default \`features/loam/\`), scenarios
1152
1275
  digest-stamped, arch requirements tagged \`@architecture\`. Those files ARE the
@@ -1184,6 +1307,29 @@ leave the spec disagreeing with the code.
1184
1307
  name: "loam-check",
1185
1308
  description: "Validate the loam docs repo and fix what it reports",
1186
1309
  argumentHint: "[--all | <FEAT-id> | <service>]",
1310
+ purpose: "Run loam's checks and fix what they find. Branch on `findings[].code`, never on the prose — and remember that a coherence finding marked `gates` stops `loam archive` even when it is only a warning.",
1311
+ // No `$1`, unlike the other five. This body carries no placeholder at all —
1312
+ // `loam-check`'s argument is a target you pass to `loam validate`, not
1313
+ // something the protocol interpolates — and the argument hint's FIRST form
1314
+ // is `--all`, so `loam instructions loam-check $1` expanded to
1315
+ // `loam instructions loam-check --all`, which commander reads as an unknown
1316
+ // option and refuses with exit 1. A pointer file whose one instruction
1317
+ // fails leaves an agent with no protocol at all, which is a state the fat
1318
+ // body could never reach. The command still tolerates a leading-dash
1319
+ // argument (see `allowUnknownOption` in commands/instructions.ts); this is
1320
+ // the half of the fix that stops loam printing the broken line in the first
1321
+ // place.
1322
+ invocation: "loam instructions loam-check",
1323
+ // The one protocol whose body names no placeholder: its target (`--all`, a
1324
+ // feature id, a service id) is spelled inside the `loam validate` line the
1325
+ // agent chooses, not substituted into the page.
1326
+ placeholders: [],
1327
+ spine: [
1328
+ "`loam validate --all` in the docs repo for the fleet, or `loam validate <target>` for one feature or service",
1329
+ "fix every error, and every finding that gates the archive",
1330
+ "leave an advisory warning standing only if you can say why, and say it",
1331
+ "re-run until the grade is the one you meant",
1332
+ ],
1187
1333
  body: `Run loam's checks and fix what they find.
1188
1334
 
1189
1335
  - whole fleet: \`loam validate --all --json\` (run in the docs repo)
@@ -1237,6 +1383,14 @@ and say it.
1237
1383
  | \`spine.op-undefined\` | a landscape edge calls an operation this service's OpenAPI does not define | a broken contract between services — fix the edge or add the endpoint |
1238
1384
  | \`spine.op-link-missing\` (warn) | a landscape "Calls" edge into this service with no \`metadata { op }\` | link it to the operationId |
1239
1385
  | \`spine.op-deprecated\` (warn) | a landscape edge calls an operation this service's OpenAPI marks \`deprecated: true\` — the consumer is standing on a contract being retired | migrate the consumer to the replacement operation; deprecation is the first step of retiring an op, and the op stays defined until a human removes it |
1386
+ | \`service.no-asyncapi\` | no asyncapi.yaml, and message link(s) already point into it — a landscape edge's \`metadata { publishes }\` / \`metadata { consumes }\`, or a LIVING requirement's \`Publishes:\` / \`Consumes:\` line. The \`details\` list the stranded message names. Silent when nothing joins into it: an async contract is optional, and most services touch no topic | put the file back — every link the \`details\` name resolves to nothing until it is there |
1387
+ | \`asyncapi.invalid\` | asyncapi.yaml exists but does not parse — an unreadable contract proves nothing, so no \`event.*\` or message-spine finding is graded against it | fix the YAML first — the event axis is unchecked until it reads |
1388
+ | \`asyncapi.duplicate-message\` (warn) | one message name is declared in two slots — every join on the name (an edge's \`metadata { publishes }\`, a requirement's \`Publishes:\` line) then picks one of them arbitrarily | give each declaration its own name, or factor the second into a \`$ref\` at the first |
1389
+ | \`spine.message-undefined\` | a landscape edge \`publishes\`/\`consumes\` a message this service's asyncapi.yaml declares no \`action: send\`/\`receive\` operation for | declare the operation, or correct the edge — the fleet map and the contract disagree about what this service puts on the wire |
1390
+ | \`spec-event.message-undefined\` | a LIVING requirement's \`Publishes:\` / \`Consumes:\` line names a message this service's asyncapi.yaml does not declare in that direction | declare it, or correct the line |
1391
+ | \`spine.message-unproduced\` | this service consumes a message NO service in the fleet declares an \`action: send\` for. The inverse of \`spine.op-undefined\`, and it has no HTTP analog: on the API axis the provider owns the contract and the check is local, while an event's schema lives in the producer's repo | find the producer and have it declare the message, or correct the name — a consumer joined to nothing reads a payload nothing defines |
1392
+ | \`asyncapi.message-contested\` (warn) | two or more services declare they send one message name — every consumer's join picks one of them arbitrarily | namespace the message by its owning domain; one message has one producer, and which contract a consumer reads must not be a coin flip |
1393
+ | \`event.messages-unlinked\` (warn) | messages AND requirements exist but zero \`Publishes:\`/\`Consumes:\` lines join them — the event axis is vacuously green | link each requirement to the messages it governs |
1240
1394
  | \`covers.unknown\` (warn) | a \`Covers:\` entry in arch.spec.md resolves to no element, edge, alert or SLI — living and feature deltas alike | fix the id (the message offers close ones); a mistyped entry silently costs the coverage it was written for |
1241
1395
  | \`health.invalid\` (warn) | health.yaml exists but does not parse — alert/SLI ids are unreadable, so \`Covers: alert:/sli:\` entries and health coverage are unchecked (a missing health.yaml is legal and silent; an unreadable one used to masquerade as \`covers.unknown\` typos) | fix the YAML — the health axis resumes once it reads |
1242
1396
  | \`health.uncovered\` (warn) | health.yaml declares an alert or SLI no arch.spec.md requirement covers | write the arch requirement with \`Covers: alert:<id>\` / \`Covers: sli:<id>\` — a signal nothing tests is dashboard decoration |
@@ -1348,6 +1502,16 @@ have read it.
1348
1502
  name: "loam-verify",
1349
1503
  description: "Verify a built loam feature against its own promises, claim by claim, with evidence",
1350
1504
  argumentHint: "<FEAT-id>",
1505
+ purpose: "Check that the code somebody built is the feature that was designed. loam derives the questions; a test runner and you answer them. loam never reads the service, so a verdict is worth exactly what its evidence is worth.",
1506
+ invocation: "loam instructions loam-verify $1",
1507
+ placeholders: ["feature"],
1508
+ spine: [
1509
+ "`loam verify` — the checklist, derived from the feature's own artifacts",
1510
+ "run the generated suite with a JSON report: the scenario claims are the runner's to answer, not yours",
1511
+ "answer the remaining claims yourself, every one carrying a `file:line` that resolves",
1512
+ "record from each affected service's own repository, bound to that service",
1513
+ "read the verdict honestly — a scenario confirmed on your word is `attested`, not `verified`",
1514
+ ],
1351
1515
  body: `Check that the code somebody just built is the feature that was designed.
1352
1516
 
1353
1517
  loam derives the questions; the test runner and you answer them. loam never
@@ -1445,6 +1609,16 @@ everyone later.
1445
1609
  name: "loam-ship",
1446
1610
  description: "Archive a finished loam feature — merge its deltas into the living docs",
1447
1611
  argumentHint: "<FEAT-id>",
1612
+ purpose: "Merge a shipped feature into the living specs, API and landscape. This is the one command that rewrites the source of truth, so every step before it exists to make the merge reviewable before it runs.",
1613
+ invocation: "loam instructions loam-ship $1",
1614
+ placeholders: ["feature"],
1615
+ spine: [
1616
+ "confirm the code is actually built and merged — archiving early makes the docs claim something that does not exist",
1617
+ "`loam validate --feature $1` must come back valid; if it says nothing pinned the delta, rebase and re-validate first",
1618
+ "`loam archive $1 --dry-run` — read the whole plan and every warning before the merge touches anything",
1619
+ "`loam archive $1` for real, and read what it reports it did",
1620
+ "`loam unarchive $1` is the undo if the plan turns out to have been wrong",
1621
+ ],
1448
1622
  body: `Archive a shipped feature.
1449
1623
 
1450
1624
  1. Confirm the code is actually built and merged. Archiving folds the delta into the
@@ -1518,13 +1692,45 @@ current snapshot format. Both mean version control is the way back.
1518
1692
  },
1519
1693
  ];
1520
1694
  /**
1521
- * Pre-approved tools in a generated file's frontmatter, so an agent that honors
1522
- * the field stops asking permission for each `loam` call. It only pre-approves:
1523
- * everything else a command needs (Read, Write, the service's own test runner)
1524
- * stays under the user's normal permission settings, and a tool that does not
1525
- * know the field ignores it.
1695
+ * The loam verbs a generated file pre-approves, so an agent that honors the
1696
+ * field stops asking permission for each call. Everything else a command needs
1697
+ * (Read, Write, the service's own test runner) stays under the user's normal
1698
+ * permission settings, and a tool that does not know the field ignores it.
1699
+ *
1700
+ * Enumerated rather than spelled `Bash(loam:*)`, for one verb's sake. `vouch`
1701
+ * is the only command in loam whose output is a claim about a HUMAN act — a
1702
+ * person read the code and says the document matches it — and a blanket
1703
+ * allowlist pre-approved it, which handed the agent that wrote a draft the
1704
+ * power to promote its own draft to `verified` without anybody being asked.
1705
+ * That inverts the argument loam makes everywhere else: it holds test evidence
1706
+ * to "an agent must not be able to SAY a scenario is tested", and then let one
1707
+ * say a spec matches the code. `vouch` now refuses without a terminal or
1708
+ * `--yes` (commands/vouch.ts) — this list is the other half, so the refusal is
1709
+ * something a person sees rather than something an agent routes around.
1710
+ *
1711
+ * A verb missing from this list is not forbidden, only unapproved: the agent
1712
+ * asks, and the user says yes. That is the correct cost for this one.
1526
1713
  */
1527
- const LOAM_ALLOWED_TOOLS = "Bash(loam:*)";
1714
+ const LOAM_ALLOWED_VERBS = [
1715
+ "adopt",
1716
+ "archive",
1717
+ "delta",
1718
+ "dependencies",
1719
+ "doctor",
1720
+ "explore",
1721
+ "gherkin",
1722
+ "init",
1723
+ "instructions",
1724
+ "list",
1725
+ "new",
1726
+ "rebase",
1727
+ "show",
1728
+ "status",
1729
+ "unarchive",
1730
+ "validate",
1731
+ "verify",
1732
+ ];
1733
+ const LOAM_ALLOWED_TOOLS = LOAM_ALLOWED_VERBS.map((v) => `Bash(loam ${v}:*)`).join(", ");
1528
1734
  /**
1529
1735
  * The Agent Skills convention, which every tool in the registry that reads
1530
1736
  * skills at all spells the same way: `<tool-dir>/skills/<name>/SKILL.md`, YAML
@@ -1565,11 +1771,64 @@ const titledMd = (c) => `# ${c.name}\n\n${c.body}`;
1565
1771
  /** The `loam-` prefix a namespace directory carries instead of the file name. */
1566
1772
  const unprefixed = (name) => name.replace(/^loam-/, "");
1567
1773
  /**
1568
- * The command body a file actually gets: the shared protocol under the same
1569
- * version stamp AGENTS.md carries.
1774
+ * What a generated file actually contains: the purpose, the spine, and the
1775
+ * command that prints the rest.
1776
+ *
1777
+ * The protocol itself stays in `body` and ships inside the binary, reachable as
1778
+ * `loam instructions <name>`. The file on disk gets a pointer to it, because
1779
+ * these two things go stale in opposite directions and only one of them can be
1780
+ * fixed. A generated file is written once, never regenerated (that is the
1781
+ * never-overwrite contract, and it is deliberate — your edits outrank the
1782
+ * template), so a repo scaffolded a year ago holds a year-old protocol: exact
1783
+ * flags, exact finding codes, exact step order, all of it asserted with total
1784
+ * confidence by a file whose reader has no way to know it is describing a
1785
+ * different release. That is not a hypothetical failure mode. It is the one
1786
+ * every project shipping generated agent instructions has had, and the fix
1787
+ * everybody reaches for — regenerate on upgrade — trades it for silently
1788
+ * overwriting what a human wrote.
1789
+ *
1790
+ * Thinning the file is the third option. What remains is what does not move
1791
+ * between releases: what this workflow is for, and the verbs in order. What
1792
+ * leaves is everything version-shaped. `doctor.agent-files-stale` still reports
1793
+ * a file whose stamp has fallen behind, and now has almost nothing to be right
1794
+ * about — which is the point.
1795
+ *
1796
+ * The trade is real and worth stating: an agent that cannot execute `loam` now
1797
+ * has a spine instead of a protocol. That is the correct failure. Every step
1798
+ * below the spine is a loam invocation, so an agent that cannot run loam was
1799
+ * never going to complete this workflow from the file either — it was going to
1800
+ * try, using a year-old flag.
1801
+ */
1802
+ function stubBody(c) {
1803
+ const steps = c.spine.map((s, i) => ` ${i + 1}. ${s}`).join("\n");
1804
+ return `${c.purpose}
1805
+
1806
+ **Run this first.** It is the protocol, and it ships with the binary you are about
1807
+ to call — so it names this loam's flags, its finding codes and its fix tables,
1808
+ not the ones that were current when this repository was scaffolded:
1809
+
1810
+ ${c.invocation}
1811
+
1812
+ The spine it fills in, so you can tell a run that went sideways from one that did not:
1813
+
1814
+ ${steps}
1815
+
1816
+ Every step above is a \`loam\` invocation, and each command's own \`--json\` output
1817
+ carries what to do next: findings have stable codes, and \`loam status --json\` puts
1818
+ the ordered \`next[]\` — each entry a code and the literal command — in one place.
1819
+ Branch on the codes, never on the prose.
1820
+
1821
+ This file is a pointer, not the protocol. loam wrote it once and will never
1822
+ rewrite it, so your edits here outrank the template and nothing will quietly
1823
+ undo them. Where this file and \`loam instructions\` disagree, the command is right.
1824
+ `;
1825
+ }
1826
+ /**
1827
+ * The command body a file actually gets: the pointer, under the same version
1828
+ * stamp AGENTS.md carries.
1570
1829
  *
1571
- * Applied once, here, rather than in twenty wrappers — the stamp is a fact
1572
- * about loam, not about a tool's dialect, and a per-wrapper copy is twenty
1830
+ * The stamp is applied once, here, rather than in twenty wrappers — it is a
1831
+ * fact about loam, not about a tool's dialect, and a per-wrapper copy is twenty
1573
1832
  * chances to forget it. It rides at the top of the BODY (not the file) because
1574
1833
  * every dialect puts something of its own first: YAML frontmatter, a TOML key,
1575
1834
  * a title line. An HTML comment is invisible in all of them, and
@@ -1579,23 +1838,35 @@ const unprefixed = (name) => name.replace(/^loam-/, "");
1579
1838
  * exactly what `doctor.agent-files-stale` is for: nobody has confirmed that
1580
1839
  * those instructions still describe this binary.
1581
1840
  */
1582
- const stamped = (c) => ({
1841
+ const stubbed = (c) => ({
1583
1842
  ...c,
1584
- body: `${agentsStampLine(LOAM_VERSION)}\n\n${c.body}`,
1843
+ body: `${agentsStampLine(LOAM_VERSION)}\n\n${stubBody(c)}`,
1585
1844
  });
1845
+ /**
1846
+ * The original target and the default. This WRAPPER must stay byte-identical
1847
+ * to what loam has always written — SLASH_COMMANDS below derives through it —
1848
+ * and an already-initialized repo must still read as fully scaffolded
1849
+ * (`skipped` is existsSync, never content). What the wrapper wraps now carries
1850
+ * a version stamp, which is the one thing about a generated file loam will
1851
+ * report on; see `stubbed`.
1852
+ *
1853
+ * It stands outside the registry object because SLASH_COMMANDS reaches for it
1854
+ * by name. That lookup was `AGENT_TOOLS["claude"]!.format!` — two assertions on
1855
+ * an exported const evaluated at import time, so renaming the key, or letting
1856
+ * this entry lose `format` the way `codex` legitimately has, would have thrown
1857
+ * a TypeError while the CLI entry point was still being loaded: before
1858
+ * `program.exitOverride()` exists, and therefore with no envelope around it.
1859
+ * Named and typed, both mistakes are typecheck failures instead. The registry
1860
+ * still lists it first, and the emitted bytes are unchanged.
1861
+ */
1862
+ const CLAUDE = {
1863
+ path: (name) => [".claude", "commands", `${name}.md`],
1864
+ format: (c) => `---\ndescription: ${c.description}\nargument-hint: ${c.argumentHint}\n---\n\n${c.body}`,
1865
+ skill: skillsIn(".claude"),
1866
+ detect: [".claude"],
1867
+ };
1586
1868
  export const AGENT_TOOLS = {
1587
- // The original target and the default. This WRAPPER must stay byte-identical
1588
- // to what loam has always written — SLASH_COMMANDS below derives through it —
1589
- // and an already-initialized repo must still read as fully scaffolded
1590
- // (`skipped` is existsSync, never content). What the wrapper wraps now
1591
- // carries a version stamp, which is the one thing about a generated file loam
1592
- // will report on; see `stamped`.
1593
- claude: {
1594
- path: (name) => [".claude", "commands", `${name}.md`],
1595
- format: (c) => `---\ndescription: ${c.description}\nargument-hint: ${c.argumentHint}\n---\n\n${c.body}`,
1596
- skill: skillsIn(".claude"),
1597
- detect: [".claude"],
1598
- },
1869
+ claude: CLAUDE,
1599
1870
  // Cursor invokes by flat file name (`/loam-check`); its frontmatter `name`
1600
1871
  // field spells that invocation form.
1601
1872
  cursor: {
@@ -1751,12 +2022,91 @@ export const AGENT_TOOLS = {
1751
2022
  },
1752
2023
  };
1753
2024
  /**
1754
- * The Claude-format files, keyed by command name. Kept as the exported shape
1755
- * because it IS the on-disk contract of every repo initialized before
1756
- * `--tools` existed — and derived through the same adapter that writes the
1757
- * files, so the export and the disk can never disagree.
2025
+ * The Claude-format files, keyed by command name: exactly what **this** binary
2026
+ * would write, derived through the same adapter that writes them, so the export
2027
+ * and a freshly scaffolded repo can never disagree.
2028
+ *
2029
+ * Read that scope literally. It used to be described as the on-disk contract of
2030
+ * every repo initialized before `--tools` existed, and that is no longer true
2031
+ * of anything but a fresh scaffold: a file holds the STUB now, every repo
2032
+ * scaffolded before that holds the full protocol, and because loam never
2033
+ * regenerates a generated file the two disagree permanently and on purpose. An
2034
+ * export cannot describe files this binary did not write.
2035
+ *
2036
+ * The protocol those older files carry is `PROTOCOLS`. Anything asserting on
2037
+ * protocol content wants that one — this answers "what would loam write here",
2038
+ * not "what does loam instruct".
2039
+ */
2040
+ export const SLASH_COMMANDS = Object.fromEntries(COMMANDS.map((c) => [c.name, CLAUDE.format(stubbed(c))]));
2041
+ /**
2042
+ * The workflow protocols themselves, keyed by command name — what
2043
+ * `loam instructions <name>` prints, and the corpus every check over loam's
2044
+ * agent-facing prose reads.
2045
+ *
2046
+ * Separate from SLASH_COMMANDS since the file became a pointer: the protocol is
2047
+ * still the thing that must document every stable code (`codes-drift`) and
2048
+ * every `loam …` line it teaches must still parse against the real CLI
2049
+ * (`agent-commands-runnable`). Those two properties are about the INSTRUCTIONS,
2050
+ * and they did not move just because the delivery did.
2051
+ */
2052
+ export const PROTOCOLS = Object.fromEntries(COMMANDS.map((c) => [c.name, c.body]));
2053
+ /** What each workflow's `$1`, `$2`, … denote — see {@link CommandContent.placeholders}. */
2054
+ export const PLACEHOLDERS = Object.fromEntries(COMMANDS.map((c) => [c.name, c.placeholders]));
2055
+ /**
2056
+ * Why the arguments given to `name` cannot be substituted into its protocol, or
2057
+ * null when they can. One sentence per bad argument, already naming the
2058
+ * placeholder it was meant to fill.
2059
+ *
2060
+ * The grammars are `core/ids.ts`'s, never a second copy: a value this accepts
2061
+ * and `loam adopt` refuses would be worse than no check, because the protocol
2062
+ * would still be wrong and now something had approved it. An argument nobody
2063
+ * supplied is not an error — `protocolFor` leaves that placeholder standing on
2064
+ * purpose, so the page reads as "the service id goes here".
1758
2065
  */
1759
- export const SLASH_COMMANDS = Object.fromEntries(COMMANDS.map((c) => [c.name, AGENT_TOOLS["claude"].format(stamped(c))]));
2066
+ export function placeholderProblems(name, args) {
2067
+ const kinds = PLACEHOLDERS[name];
2068
+ if (kinds === undefined)
2069
+ return null;
2070
+ const problems = kinds.flatMap((kind, i) => {
2071
+ const arg = args[i];
2072
+ if (arg === undefined || arg === "" || kind === "free")
2073
+ return [];
2074
+ const problem = kind === "service"
2075
+ ? serviceIdProblem(arg, `$${i + 1}`)
2076
+ : featureIdProblem(arg, `feature id ($${i + 1})`);
2077
+ return problem === null ? [] : [problem];
2078
+ });
2079
+ return problems.length === 0 ? null : problems;
2080
+ }
2081
+ /** The workflow names, in cycle order — `loam instructions` with no argument lists these. */
2082
+ export const WORKFLOWS = COMMANDS.map(({ name, description, argumentHint }) => ({ name, description, argumentHint }));
2083
+ /**
2084
+ * One workflow's protocol with `$1`, `$2`, … replaced by the arguments given.
2085
+ *
2086
+ * An unsupplied placeholder is LEFT STANDING rather than blanked: the bodies
2087
+ * are written so `$1` reads as "the feature id goes here", and a protocol that
2088
+ * silently dropped it would hand an agent `loam validate --feature --json` — a
2089
+ * command that parses, runs, and answers a different question. Extra arguments
2090
+ * are ignored for the same reason: this substitutes, it does not validate.
2091
+ *
2092
+ * An EMPTY argument counts as unsupplied, and that is not pedantry. Several
2093
+ * tool dialects expand an absent positional to the empty string, and
2094
+ * `loam-feature`'s own invocation quotes its second one (`--title "$2"`), so
2095
+ * `/loam-feature FEAT-101` with no title arrives here as `["FEAT-101", ""]`.
2096
+ * Blanking it produced `loam new FEAT-101 --title ""` — which is exactly the
2097
+ * failure the paragraph above describes, reached by the one path that paragraph
2098
+ * did not cover: the command succeeds, and writes a feature whose title is the
2099
+ * empty string and whose intent.md opens with a bare `#`.
2100
+ */
2101
+ export function protocolFor(name, args = []) {
2102
+ const body = PROTOCOLS[name];
2103
+ if (body === undefined)
2104
+ return null;
2105
+ return body.replace(/\$([1-9])/g, (whole, digit) => {
2106
+ const arg = args[Number(digit) - 1];
2107
+ return arg === undefined || arg === "" ? whole : arg;
2108
+ });
2109
+ }
1760
2110
  /** The two ways a command body reaches an agent. Both are on by default. */
1761
2111
  export const DELIVERIES = ["commands", "skills"];
1762
2112
  /**
@@ -1786,7 +2136,7 @@ export function plannedCommandFiles(cwd, toolIds, delivery = DELIVERIES) {
1786
2136
  }
1787
2137
  if (delivery.includes("skills") && skill !== undefined)
1788
2138
  emitters.push(skill);
1789
- return emitters.flatMap((e) => COMMANDS.map((c) => ({ path: join(cwd, ...e.path(c.name)), content: e.format(stamped(c)) })));
2139
+ return emitters.flatMap((e) => COMMANDS.map((c) => ({ path: join(cwd, ...e.path(c.name)), content: e.format(stubbed(c)) })));
1790
2140
  });
1791
2141
  }
1792
2142
  /**