@ybotok/loam 0.1.0-beta.1 → 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 (115) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +6 -1
  3. package/dist/cli.js +4 -0
  4. package/dist/cli.js.map +1 -1
  5. package/dist/commands/adopt.js +17 -3
  6. package/dist/commands/adopt.js.map +1 -1
  7. package/dist/commands/archive.js +24 -8
  8. package/dist/commands/archive.js.map +1 -1
  9. package/dist/commands/delta.js +74 -47
  10. package/dist/commands/delta.js.map +1 -1
  11. package/dist/commands/dependencies.js +7 -2
  12. package/dist/commands/dependencies.js.map +1 -1
  13. package/dist/commands/docs-repo-gate.js +104 -0
  14. package/dist/commands/docs-repo-gate.js.map +1 -0
  15. package/dist/commands/explore.js +195 -0
  16. package/dist/commands/explore.js.map +1 -0
  17. package/dist/commands/format.js +17 -0
  18. package/dist/commands/format.js.map +1 -0
  19. package/dist/commands/gherkin.js +87 -48
  20. package/dist/commands/gherkin.js.map +1 -1
  21. package/dist/commands/init.js +3 -3
  22. package/dist/commands/init.js.map +1 -1
  23. package/dist/commands/instructions.js +70 -0
  24. package/dist/commands/instructions.js.map +1 -0
  25. package/dist/commands/list.js +6 -76
  26. package/dist/commands/list.js.map +1 -1
  27. package/dist/commands/migrate-openspec.js +45 -28
  28. package/dist/commands/migrate-openspec.js.map +1 -1
  29. package/dist/commands/new.js +35 -34
  30. package/dist/commands/new.js.map +1 -1
  31. package/dist/commands/rebase.js +2 -4
  32. package/dist/commands/rebase.js.map +1 -1
  33. package/dist/commands/show.js +26 -31
  34. package/dist/commands/show.js.map +1 -1
  35. package/dist/commands/status.js +8 -24
  36. package/dist/commands/status.js.map +1 -1
  37. package/dist/commands/unarchive.js +12 -5
  38. package/dist/commands/unarchive.js.map +1 -1
  39. package/dist/commands/validate.js +115 -133
  40. package/dist/commands/validate.js.map +1 -1
  41. package/dist/commands/verify.js +33 -17
  42. package/dist/commands/verify.js.map +1 -1
  43. package/dist/commands/vouch.js +77 -59
  44. package/dist/commands/vouch.js.map +1 -1
  45. package/dist/core/agent.js +273 -31
  46. package/dist/core/agent.js.map +1 -1
  47. package/dist/core/arch.js +28 -2
  48. package/dist/core/arch.js.map +1 -1
  49. package/dist/core/brief.js +3 -3
  50. package/dist/core/brief.js.map +1 -1
  51. package/dist/core/coherence.js +37 -13
  52. package/dist/core/coherence.js.map +1 -1
  53. package/dist/core/concurrency.js +51 -0
  54. package/dist/core/concurrency.js.map +1 -0
  55. package/dist/core/config.js +10 -2
  56. package/dist/core/config.js.map +1 -1
  57. package/dist/core/delta.js +21 -2
  58. package/dist/core/delta.js.map +1 -1
  59. package/dist/core/dependencies.js +46 -42
  60. package/dist/core/dependencies.js.map +1 -1
  61. package/dist/core/docs.js +2 -1
  62. package/dist/core/docs.js.map +1 -1
  63. package/dist/core/doctor.js +41 -26
  64. package/dist/core/doctor.js.map +1 -1
  65. package/dist/core/document-bytes.js +59 -0
  66. package/dist/core/document-bytes.js.map +1 -0
  67. package/dist/core/explore.js +225 -0
  68. package/dist/core/explore.js.map +1 -0
  69. package/dist/core/fleet-context.js +16 -63
  70. package/dist/core/fleet-context.js.map +1 -1
  71. package/dist/core/frontmatter.js +1 -5
  72. package/dist/core/frontmatter.js.map +1 -1
  73. package/dist/core/gherkin.js +3 -61
  74. package/dist/core/gherkin.js.map +1 -1
  75. package/dist/core/health.js +1 -5
  76. package/dist/core/health.js.map +1 -1
  77. package/dist/core/ids.js +34 -5
  78. package/dist/core/ids.js.map +1 -1
  79. package/dist/core/json.js +8 -1
  80. package/dist/core/json.js.map +1 -1
  81. package/dist/core/likec4.js +6 -1
  82. package/dist/core/likec4.js.map +1 -1
  83. package/dist/core/maturity.js +66 -0
  84. package/dist/core/maturity.js.map +1 -0
  85. package/dist/core/openapi-merge.js +39 -6
  86. package/dist/core/openapi-merge.js.map +1 -1
  87. package/dist/core/openapi.js +17 -5
  88. package/dist/core/openapi.js.map +1 -1
  89. package/dist/core/openspec-inventory.js +64 -27
  90. package/dist/core/openspec-inventory.js.map +1 -1
  91. package/dist/core/path-safety.js +59 -0
  92. package/dist/core/path-safety.js.map +1 -1
  93. package/dist/core/provenance.js +35 -10
  94. package/dist/core/provenance.js.map +1 -1
  95. package/dist/core/records.js +33 -0
  96. package/dist/core/records.js.map +1 -0
  97. package/dist/core/repo.js +75 -1
  98. package/dist/core/repo.js.map +1 -1
  99. package/dist/core/report.js +9 -0
  100. package/dist/core/report.js.map +1 -1
  101. package/dist/core/results.js +1 -3
  102. package/dist/core/results.js.map +1 -1
  103. package/dist/core/spec.js +3 -3
  104. package/dist/core/spec.js.map +1 -1
  105. package/dist/core/staging.js +124 -17
  106. package/dist/core/staging.js.map +1 -1
  107. package/dist/core/status.js +122 -33
  108. package/dist/core/status.js.map +1 -1
  109. package/dist/core/steps.js +72 -0
  110. package/dist/core/steps.js.map +1 -0
  111. package/dist/core/verify.js +18 -4
  112. package/dist/core/verify.js.map +1 -1
  113. package/dist/core/version.js +17 -1
  114. package/dist/core/version.js.map +1 -1
  115. package/package.json +1 -1
@@ -357,6 +357,14 @@ says so once and stops the warning for good.
357
357
  session: it says what is in flight and what the next step is, for the fleet or
358
358
  for one feature (\`loam status FEAT-101 --json\`). Then \`loam list --json\` and
359
359
  \`loam show <service> --json\`.
360
+ Then \`loam explore <service> --json\` around the services you believe are
361
+ involved — it writes nothing and answers the one question the next step takes
362
+ as an argument: which services this change actually touches. It reports the
363
+ ring one hop out in the fleet map (\`neighbours\`), how far each service's
364
+ documentation has got (\`maturity\`), what each already exposes, the features
365
+ already in flight over the same ground (\`overlaps\`), and the \`loam new\` line
366
+ the seeds imply (\`scaffold\`). \`--op <operationId>\` seeds from an operation
367
+ when you know the call but not who owns it.
360
368
  Never propose a change to a service you have not read.
361
369
  3. **Scaffold** — \`loam new FEAT-101 --title "..." --touches <existing> --new-service <new>\`.
362
370
  The scaffold leaves a \`delta.likec4\` whose context elements are commented out —
@@ -779,6 +787,24 @@ selected that tool. The map of which invocation surfaces what:
779
787
  operationIds, never from validator prose. It is the answer to every
780
788
  "another feature in flight does this first" finding, and to
781
789
  \`delta.added-conflict\` / \`delta.modified-conflict\`, which no ordering fixes.
790
+ - \`loam explore [<service>...]\` reads the fleet around a change nobody has
791
+ written down yet, and writes nothing. It exists because \`loam new\`'s
792
+ \`--touches\` list is the hardest call in the cycle and the only one nothing
793
+ downstream catches: a list short by one service produces a feature that
794
+ validates, archives and ships with a consumer nobody updated. What it derives
795
+ is mechanical — the one-hop ring, each service's rung, its living operations,
796
+ who already calls whom, and which active features cover the same services. It
797
+ does NOT decide which of those neighbours you actually change; that is a
798
+ judgement about intent, and loam does not make those.
799
+ - \`loam instructions [<workflow>] [args...]\` prints one of the six workflow
800
+ protocols — \`loam-adopt\`, \`loam-feature\`, \`loam-implement\`, \`loam-check\`,
801
+ \`loam-verify\`, \`loam-ship\` — with \`$1\`, \`$2\` filled in from the arguments you
802
+ pass. The protocol ships inside the binary, so it describes the loam you are
803
+ about to run rather than the one that scaffolded the repository; the command
804
+ and skill files \`loam init\` writes are pointers at it. It reads no
805
+ \`loam.json\` and no docs repo, deliberately: \`loam-adopt\`'s own first step is
806
+ to run \`loam init\` when there is no config, so it cannot be the step that
807
+ requires one.
782
808
  - \`loam audit-openspec <root>\` is the read-only OpenSpec inventory; an explicit
783
809
  \`--write-mapping\` writes a non-overwriting, digest-bound decision skeleton
784
810
  outside the source tree. \`loam migrate-openspec <root> --map <file>\` validates
@@ -877,10 +903,12 @@ it. It changes the exit code and nothing else:
877
903
  what it was. The stricter grade is a per-invocation lever, visible in the CI
878
904
  pipeline that passes the flag — deliberately not a per-repo profile.
879
905
 
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.
906
+ \`loam delta <FEAT> --json\` exits 1 when either authored document behind the brief
907
+ failed to parse — \`architecture.errors\` non-empty, or \`openapi.unreadable\` true —
908
+ with \`ok: true\` and the full payload intact: the empty C4 slice means the delta
909
+ did not parse, and an empty \`api\` beside \`openapi.unreadable\` means the contract
910
+ delta did not parse, neither of them "this feature changes nothing there". Branch
911
+ on the exit code before consuming either slice as a task brief.
884
912
 
885
913
  Findings with severity \`ok\` are confirmations, not work: \`c4.valid\`, \`delta.valid\`,
886
914
  \`requirements.covered\`, \`api.covered\`, \`spine.resolved\`, \`coherence.ok\`,
@@ -1008,6 +1036,16 @@ const COMMANDS = [
1008
1036
  name: "loam-adopt",
1009
1037
  description: "Adopt a service — write its baseline docs from its code, as draft, then validate",
1010
1038
  argumentHint: "<service-id>",
1039
+ purpose: "Write one service's baseline documentation into the docs repo from its code, as `draft`. You read the code; loam states the work and checks the result — it never reads the service, so anything you cannot show, do not write.",
1040
+ invocation: "loam instructions loam-adopt $1",
1041
+ spine: [
1042
+ "wire this repo (`loam init`, then `loam doctor`) if it has no ./loam.json yet",
1043
+ "`loam adopt` — the brief: every file to write, the grammar of each, and what the fleet map already says",
1044
+ "read the code, keeping the list of every path you open — that list becomes `sources`",
1045
+ "write the artifacts, everything `status: draft`",
1046
+ "validate the service, then validate the whole fleet — a baseline that passes one and fails the other is documented and invisible",
1047
+ "hand back with what you could not determine from the code, and let a human vouch",
1048
+ ],
1011
1049
  body: `Write one service's baseline documentation into the loam docs repo (its path is
1012
1050
  \`docsDir\` in ./loam.json). You read the code; loam states the work and checks the
1013
1051
  result. It never reads the service — so anything you cannot show, do not write.
@@ -1068,6 +1106,17 @@ behaviour nobody can find is the one failure mode none of loam's checks can catc
1068
1106
  name: "loam-feature",
1069
1107
  description: "Start a new loam feature — scaffold it, then author the C4 delta and requirement deltas",
1070
1108
  argumentHint: `<FEAT-id> "<title>"`,
1109
+ purpose: "Start a feature in the docs repo: decide which services it touches, scaffold it, then author the C4 delta and one requirement delta per service. The `--touches` list is the hardest call in the cycle and nothing downstream catches one that is short by a service.",
1110
+ invocation: 'loam instructions loam-feature $1 "$2"',
1111
+ spine: [
1112
+ "read `AGENTS.md` at the docs repo root — it defines the ID spine everything else depends on",
1113
+ "`loam explore` around the services you think are involved: the ring one hop out, and what is already in flight over the same ground",
1114
+ "`loam status` / `loam list` / `loam show` — what exists and what is already owed",
1115
+ "`loam new` with a `--touches` per service it changes and a `--new-service` per service it introduces",
1116
+ "author what the templates left as TODO: intent, the C4 delta, the per-service spec / arch spec / openapi",
1117
+ "`loam rebase` — pin every MODIFIED requirement and every operation to the living version you wrote against",
1118
+ "validate the feature, then read `loam dependencies` before anyone starts building",
1119
+ ],
1071
1120
  body: `Start a new feature in the loam docs repo (its path is \`docsDir\` in ./loam.json).
1072
1121
 
1073
1122
  1. Read \`AGENTS.md\` at the root of the docs repo first — it defines the ID spine
@@ -1080,6 +1129,27 @@ behaviour nobody can find is the one failure mode none of loam's checks can catc
1080
1129
  narrows it to one feature once $1 exists
1081
1130
  - \`loam list --json\` — what services exist, and what documentation they are missing
1082
1131
  - \`loam show <service> --json\` — what a service owns, exposes, and who already calls it
1132
+ 2b. \`loam explore <service> --json --as $1\` around every service you believe this
1133
+ feature touches. This is the step that decides step 3's \`--touches\` list, and it
1134
+ is the only decision in the cycle nothing downstream catches — a list short by one
1135
+ service produces a feature that validates, archives and ships with a consumer
1136
+ nobody updated. Read four fields:
1137
+ - \`neighbours\` — the services one hop out in the fleet map. Weigh each one. loam
1138
+ knows they are connected, not whether you change them, and it deliberately
1139
+ leaves them OUT of the suggested command line rather than guessing for you
1140
+ - \`services[].maturity\` — a service at \`empty\` or \`partial\` has no baseline to
1141
+ write a delta against, so the work starts with \`loam adopt --service <id>\`, not
1142
+ with this feature
1143
+ - \`services[].operations\` — what each already exposes, so an operation you are
1144
+ about to "add" that is already there becomes a MODIFIED requirement instead
1145
+ - \`overlaps\` — active features already carrying a delta for these services.
1146
+ \`loam dependencies --json\` (step 7) then says which must archive first
1147
+ \`--op <operationId>\` seeds from an operation when you know the call but not who
1148
+ owns it. A seed naming no \`services/<id>/\` is reported in \`unknown\` with the
1149
+ closest real ids rather than refused — the feature may be introducing it, and a
1150
+ typo looks identical until you read that list. \`scaffold\` is the literal step-3
1151
+ command line the seeds imply, with \`--new-service\` where the service does not
1152
+ exist yet.
1083
1153
  3. Scaffold it: \`loam new $1 --title "$2"\`, adding \`--touches <id>\` for every service the
1084
1154
  feature touches and \`--new-service <id>\` for every one it introduces. Every id goes
1085
1155
  through the \`services/<id>/\` grammar, so a typo is refused (\`invalid-option\`)
@@ -1123,6 +1193,17 @@ line, and the OpenAPI \`operationId\` — must match exactly.
1123
1193
  name: "loam-implement",
1124
1194
  description: "Implement a loam feature's slice for one service — generated gherkin first, tests from it, then code",
1125
1195
  argumentHint: "<FEAT-id> [service]",
1196
+ purpose: "Build one service's part of a feature, in that service's own repository. The generated Gherkin is the acceptance criterion — it comes before the step definitions, and those come before the code.",
1197
+ invocation: "loam instructions loam-implement $1 $2",
1198
+ spine: [
1199
+ "`loam status` — where this feature actually is, and what is owed before you start",
1200
+ "`loam delta` — the task: the intent, every requirement verbatim, the endpoints, and the calls in and out",
1201
+ "`loam gherkin` in the service's repo — the digest-stamped `.feature` files. Never edit them",
1202
+ "write step definitions for the generated scenarios first, outside the generated directory",
1203
+ "implement until the suite passes, running it with a JSON report",
1204
+ "honour the contract in both directions: expose every inbound operation, call every outbound one",
1205
+ "`loam rebase` if building made you change the feature's documents at all, then validate the feature",
1206
+ ],
1126
1207
  body: `Implement one service's part of a feature. Every step runs in the SERVICE'S own
1127
1208
  repository, which needs its own committed ./loam.json — if there is none,
1128
1209
  \`loam init --docs <path-to-docs-repo> --service $2\` then \`loam doctor\` first.
@@ -1142,11 +1223,19 @@ repository, which needs its own committed ./loam.json — if there is none,
1142
1223
  - \`architecture\` — whether this service is new, and the calls in and out of it
1143
1224
  - \`api\` — the endpoints this feature adds or retires for this service: path,
1144
1225
  method, operationId and summary, with removals spelled \`REMOVE <METHOD> <path>\`
1226
+ - \`openapi\` — whether that contract could be read at all: \`unreadable\`, plus an
1227
+ \`error\` when the parser gave a message. It rides beside \`api\` rather than
1228
+ inside it because \`api\` stays the operations array it has always been, and it
1229
+ is what tells an empty \`api\` apart from a contract delta that did not parse
1145
1230
  - \`services\` — every service this feature projects onto, so a run without
1146
1231
  \`--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.
1232
+ Exit 1 with \`ok: true\` means one of the two authored documents behind this brief
1233
+ did not parse: \`architecture.errors\` non-empty (delta.likec4), or
1234
+ \`openapi.unreadable\` true (the feature's openapi.yaml for this service). The empty
1235
+ slice is the parse failure itself, not "nothing changes there". Stop and fix that
1236
+ document before building anything — for the delta, \`loam validate $1 --json\` names
1237
+ the error; the contract delta is not graded by validate, so read the parser message
1238
+ the payload carries.
1150
1239
  2. In the service's repo, \`loam gherkin $1 --json\` — one \`.feature\` file per changed
1151
1240
  requirement lands under \`<gherkinDir>/loam/\` (default \`features/loam/\`), scenarios
1152
1241
  digest-stamped, arch requirements tagged \`@architecture\`. Those files ARE the
@@ -1184,6 +1273,25 @@ leave the spec disagreeing with the code.
1184
1273
  name: "loam-check",
1185
1274
  description: "Validate the loam docs repo and fix what it reports",
1186
1275
  argumentHint: "[--all | <FEAT-id> | <service>]",
1276
+ purpose: "Run loam's checks and fix what they find. Branch on `findings[].code`, never on the prose — and remember that a coherence finding marked `gates` stops `loam archive` even when it is only a warning.",
1277
+ // No `$1`, unlike the other five. This body carries no placeholder at all —
1278
+ // `loam-check`'s argument is a target you pass to `loam validate`, not
1279
+ // something the protocol interpolates — and the argument hint's FIRST form
1280
+ // is `--all`, so `loam instructions loam-check $1` expanded to
1281
+ // `loam instructions loam-check --all`, which commander reads as an unknown
1282
+ // option and refuses with exit 1. A pointer file whose one instruction
1283
+ // fails leaves an agent with no protocol at all, which is a state the fat
1284
+ // body could never reach. The command still tolerates a leading-dash
1285
+ // argument (see `allowUnknownOption` in commands/instructions.ts); this is
1286
+ // the half of the fix that stops loam printing the broken line in the first
1287
+ // place.
1288
+ invocation: "loam instructions loam-check",
1289
+ spine: [
1290
+ "`loam validate --all` in the docs repo for the fleet, or `loam validate <target>` for one feature or service",
1291
+ "fix every error, and every finding that gates the archive",
1292
+ "leave an advisory warning standing only if you can say why, and say it",
1293
+ "re-run until the grade is the one you meant",
1294
+ ],
1187
1295
  body: `Run loam's checks and fix what they find.
1188
1296
 
1189
1297
  - whole fleet: \`loam validate --all --json\` (run in the docs repo)
@@ -1348,6 +1456,15 @@ have read it.
1348
1456
  name: "loam-verify",
1349
1457
  description: "Verify a built loam feature against its own promises, claim by claim, with evidence",
1350
1458
  argumentHint: "<FEAT-id>",
1459
+ purpose: "Check that the code somebody built is the feature that was designed. loam derives the questions; a test runner and you answer them. loam never reads the service, so a verdict is worth exactly what its evidence is worth.",
1460
+ invocation: "loam instructions loam-verify $1",
1461
+ spine: [
1462
+ "`loam verify` — the checklist, derived from the feature's own artifacts",
1463
+ "run the generated suite with a JSON report: the scenario claims are the runner's to answer, not yours",
1464
+ "answer the remaining claims yourself, every one carrying a `file:line` that resolves",
1465
+ "record from each affected service's own repository, bound to that service",
1466
+ "read the verdict honestly — a scenario confirmed on your word is `attested`, not `verified`",
1467
+ ],
1351
1468
  body: `Check that the code somebody just built is the feature that was designed.
1352
1469
 
1353
1470
  loam derives the questions; the test runner and you answer them. loam never
@@ -1445,6 +1562,15 @@ everyone later.
1445
1562
  name: "loam-ship",
1446
1563
  description: "Archive a finished loam feature — merge its deltas into the living docs",
1447
1564
  argumentHint: "<FEAT-id>",
1565
+ purpose: "Merge a shipped feature into the living specs, API and landscape. This is the one command that rewrites the source of truth, so every step before it exists to make the merge reviewable before it runs.",
1566
+ invocation: "loam instructions loam-ship $1",
1567
+ spine: [
1568
+ "confirm the code is actually built and merged — archiving early makes the docs claim something that does not exist",
1569
+ "`loam validate --feature $1` must come back valid; if it says nothing pinned the delta, rebase and re-validate first",
1570
+ "`loam archive $1 --dry-run` — read the whole plan and every warning before the merge touches anything",
1571
+ "`loam archive $1` for real, and read what it reports it did",
1572
+ "`loam unarchive $1` is the undo if the plan turns out to have been wrong",
1573
+ ],
1448
1574
  body: `Archive a shipped feature.
1449
1575
 
1450
1576
  1. Confirm the code is actually built and merged. Archiving folds the delta into the
@@ -1565,11 +1691,64 @@ const titledMd = (c) => `# ${c.name}\n\n${c.body}`;
1565
1691
  /** The `loam-` prefix a namespace directory carries instead of the file name. */
1566
1692
  const unprefixed = (name) => name.replace(/^loam-/, "");
1567
1693
  /**
1568
- * The command body a file actually gets: the shared protocol under the same
1569
- * version stamp AGENTS.md carries.
1694
+ * What a generated file actually contains: the purpose, the spine, and the
1695
+ * command that prints the rest.
1696
+ *
1697
+ * The protocol itself stays in `body` and ships inside the binary, reachable as
1698
+ * `loam instructions <name>`. The file on disk gets a pointer to it, because
1699
+ * these two things go stale in opposite directions and only one of them can be
1700
+ * fixed. A generated file is written once, never regenerated (that is the
1701
+ * never-overwrite contract, and it is deliberate — your edits outrank the
1702
+ * template), so a repo scaffolded a year ago holds a year-old protocol: exact
1703
+ * flags, exact finding codes, exact step order, all of it asserted with total
1704
+ * confidence by a file whose reader has no way to know it is describing a
1705
+ * different release. That is not a hypothetical failure mode. It is the one
1706
+ * every project shipping generated agent instructions has had, and the fix
1707
+ * everybody reaches for — regenerate on upgrade — trades it for silently
1708
+ * overwriting what a human wrote.
1570
1709
  *
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
1710
+ * Thinning the file is the third option. What remains is what does not move
1711
+ * between releases: what this workflow is for, and the verbs in order. What
1712
+ * leaves is everything version-shaped. `doctor.agent-files-stale` still reports
1713
+ * a file whose stamp has fallen behind, and now has almost nothing to be right
1714
+ * about — which is the point.
1715
+ *
1716
+ * The trade is real and worth stating: an agent that cannot execute `loam` now
1717
+ * has a spine instead of a protocol. That is the correct failure. Every step
1718
+ * below the spine is a loam invocation, so an agent that cannot run loam was
1719
+ * never going to complete this workflow from the file either — it was going to
1720
+ * try, using a year-old flag.
1721
+ */
1722
+ function stubBody(c) {
1723
+ const steps = c.spine.map((s, i) => ` ${i + 1}. ${s}`).join("\n");
1724
+ return `${c.purpose}
1725
+
1726
+ **Run this first.** It is the protocol, and it ships with the binary you are about
1727
+ to call — so it names this loam's flags, its finding codes and its fix tables,
1728
+ not the ones that were current when this repository was scaffolded:
1729
+
1730
+ ${c.invocation}
1731
+
1732
+ The spine it fills in, so you can tell a run that went sideways from one that did not:
1733
+
1734
+ ${steps}
1735
+
1736
+ Every step above is a \`loam\` invocation, and each command's own \`--json\` output
1737
+ carries what to do next: findings have stable codes, and \`loam status --json\` puts
1738
+ the ordered \`next[]\` — each entry a code and the literal command — in one place.
1739
+ Branch on the codes, never on the prose.
1740
+
1741
+ This file is a pointer, not the protocol. loam wrote it once and will never
1742
+ rewrite it, so your edits here outrank the template and nothing will quietly
1743
+ undo them. Where this file and \`loam instructions\` disagree, the command is right.
1744
+ `;
1745
+ }
1746
+ /**
1747
+ * The command body a file actually gets: the pointer, under the same version
1748
+ * stamp AGENTS.md carries.
1749
+ *
1750
+ * The stamp is applied once, here, rather than in twenty wrappers — it is a
1751
+ * fact about loam, not about a tool's dialect, and a per-wrapper copy is twenty
1573
1752
  * chances to forget it. It rides at the top of the BODY (not the file) because
1574
1753
  * every dialect puts something of its own first: YAML frontmatter, a TOML key,
1575
1754
  * a title line. An HTML comment is invisible in all of them, and
@@ -1579,23 +1758,35 @@ const unprefixed = (name) => name.replace(/^loam-/, "");
1579
1758
  * exactly what `doctor.agent-files-stale` is for: nobody has confirmed that
1580
1759
  * those instructions still describe this binary.
1581
1760
  */
1582
- const stamped = (c) => ({
1761
+ const stubbed = (c) => ({
1583
1762
  ...c,
1584
- body: `${agentsStampLine(LOAM_VERSION)}\n\n${c.body}`,
1763
+ body: `${agentsStampLine(LOAM_VERSION)}\n\n${stubBody(c)}`,
1585
1764
  });
1765
+ /**
1766
+ * The original target and the default. This WRAPPER must stay byte-identical
1767
+ * to what loam has always written — SLASH_COMMANDS below derives through it —
1768
+ * and an already-initialized repo must still read as fully scaffolded
1769
+ * (`skipped` is existsSync, never content). What the wrapper wraps now carries
1770
+ * a version stamp, which is the one thing about a generated file loam will
1771
+ * report on; see `stubbed`.
1772
+ *
1773
+ * It stands outside the registry object because SLASH_COMMANDS reaches for it
1774
+ * by name. That lookup was `AGENT_TOOLS["claude"]!.format!` — two assertions on
1775
+ * an exported const evaluated at import time, so renaming the key, or letting
1776
+ * this entry lose `format` the way `codex` legitimately has, would have thrown
1777
+ * a TypeError while the CLI entry point was still being loaded: before
1778
+ * `program.exitOverride()` exists, and therefore with no envelope around it.
1779
+ * Named and typed, both mistakes are typecheck failures instead. The registry
1780
+ * still lists it first, and the emitted bytes are unchanged.
1781
+ */
1782
+ const CLAUDE = {
1783
+ path: (name) => [".claude", "commands", `${name}.md`],
1784
+ format: (c) => `---\ndescription: ${c.description}\nargument-hint: ${c.argumentHint}\n---\n\n${c.body}`,
1785
+ skill: skillsIn(".claude"),
1786
+ detect: [".claude"],
1787
+ };
1586
1788
  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
- },
1789
+ claude: CLAUDE,
1599
1790
  // Cursor invokes by flat file name (`/loam-check`); its frontmatter `name`
1600
1791
  // field spells that invocation form.
1601
1792
  cursor: {
@@ -1751,12 +1942,63 @@ export const AGENT_TOOLS = {
1751
1942
  },
1752
1943
  };
1753
1944
  /**
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.
1945
+ * The Claude-format files, keyed by command name: exactly what **this** binary
1946
+ * would write, derived through the same adapter that writes them, so the export
1947
+ * and a freshly scaffolded repo can never disagree.
1948
+ *
1949
+ * Read that scope literally. It used to be described as the on-disk contract of
1950
+ * every repo initialized before `--tools` existed, and that is no longer true
1951
+ * of anything but a fresh scaffold: a file holds the STUB now, every repo
1952
+ * scaffolded before that holds the full protocol, and because loam never
1953
+ * regenerates a generated file the two disagree permanently and on purpose. An
1954
+ * export cannot describe files this binary did not write.
1955
+ *
1956
+ * The protocol those older files carry is `PROTOCOLS`. Anything asserting on
1957
+ * protocol content wants that one — this answers "what would loam write here",
1958
+ * not "what does loam instruct".
1758
1959
  */
1759
- export const SLASH_COMMANDS = Object.fromEntries(COMMANDS.map((c) => [c.name, AGENT_TOOLS["claude"].format(stamped(c))]));
1960
+ export const SLASH_COMMANDS = Object.fromEntries(COMMANDS.map((c) => [c.name, CLAUDE.format(stubbed(c))]));
1961
+ /**
1962
+ * The workflow protocols themselves, keyed by command name — what
1963
+ * `loam instructions <name>` prints, and the corpus every check over loam's
1964
+ * agent-facing prose reads.
1965
+ *
1966
+ * Separate from SLASH_COMMANDS since the file became a pointer: the protocol is
1967
+ * still the thing that must document every stable code (`codes-drift`) and
1968
+ * every `loam …` line it teaches must still parse against the real CLI
1969
+ * (`agent-commands-runnable`). Those two properties are about the INSTRUCTIONS,
1970
+ * and they did not move just because the delivery did.
1971
+ */
1972
+ export const PROTOCOLS = Object.fromEntries(COMMANDS.map((c) => [c.name, c.body]));
1973
+ /** The workflow names, in cycle order — `loam instructions` with no argument lists these. */
1974
+ export const WORKFLOWS = COMMANDS.map(({ name, description, argumentHint }) => ({ name, description, argumentHint }));
1975
+ /**
1976
+ * One workflow's protocol with `$1`, `$2`, … replaced by the arguments given.
1977
+ *
1978
+ * An unsupplied placeholder is LEFT STANDING rather than blanked: the bodies
1979
+ * are written so `$1` reads as "the feature id goes here", and a protocol that
1980
+ * silently dropped it would hand an agent `loam validate --feature --json` — a
1981
+ * command that parses, runs, and answers a different question. Extra arguments
1982
+ * are ignored for the same reason: this substitutes, it does not validate.
1983
+ *
1984
+ * An EMPTY argument counts as unsupplied, and that is not pedantry. Several
1985
+ * tool dialects expand an absent positional to the empty string, and
1986
+ * `loam-feature`'s own invocation quotes its second one (`--title "$2"`), so
1987
+ * `/loam-feature FEAT-101` with no title arrives here as `["FEAT-101", ""]`.
1988
+ * Blanking it produced `loam new FEAT-101 --title ""` — which is exactly the
1989
+ * failure the paragraph above describes, reached by the one path that paragraph
1990
+ * did not cover: the command succeeds, and writes a feature whose title is the
1991
+ * empty string and whose intent.md opens with a bare `#`.
1992
+ */
1993
+ export function protocolFor(name, args = []) {
1994
+ const body = PROTOCOLS[name];
1995
+ if (body === undefined)
1996
+ return null;
1997
+ return body.replace(/\$([1-9])/g, (whole, digit) => {
1998
+ const arg = args[Number(digit) - 1];
1999
+ return arg === undefined || arg === "" ? whole : arg;
2000
+ });
2001
+ }
1760
2002
  /** The two ways a command body reaches an agent. Both are on by default. */
1761
2003
  export const DELIVERIES = ["commands", "skills"];
1762
2004
  /**
@@ -1786,7 +2028,7 @@ export function plannedCommandFiles(cwd, toolIds, delivery = DELIVERIES) {
1786
2028
  }
1787
2029
  if (delivery.includes("skills") && skill !== undefined)
1788
2030
  emitters.push(skill);
1789
- return emitters.flatMap((e) => COMMANDS.map((c) => ({ path: join(cwd, ...e.path(c.name)), content: e.format(stamped(c)) })));
2031
+ return emitters.flatMap((e) => COMMANDS.map((c) => ({ path: join(cwd, ...e.path(c.name)), content: e.format(stubbed(c)) })));
1790
2032
  });
1791
2033
  }
1792
2034
  /**