@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.
- package/CHANGELOG.md +96 -0
- package/README.md +6 -1
- package/SCHEMA.md +22 -4
- package/dist/cli.js +4 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/adopt.js +17 -3
- package/dist/commands/adopt.js.map +1 -1
- package/dist/commands/archive.js +24 -8
- package/dist/commands/archive.js.map +1 -1
- package/dist/commands/delta.js +74 -47
- package/dist/commands/delta.js.map +1 -1
- package/dist/commands/dependencies.js +7 -2
- package/dist/commands/dependencies.js.map +1 -1
- package/dist/commands/docs-repo-gate.js +104 -0
- package/dist/commands/docs-repo-gate.js.map +1 -0
- package/dist/commands/explore.js +195 -0
- package/dist/commands/explore.js.map +1 -0
- package/dist/commands/format.js +17 -0
- package/dist/commands/format.js.map +1 -0
- package/dist/commands/gherkin.js +87 -48
- package/dist/commands/gherkin.js.map +1 -1
- package/dist/commands/init.js +3 -3
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/instructions.js +80 -0
- package/dist/commands/instructions.js.map +1 -0
- package/dist/commands/list.js +10 -76
- package/dist/commands/list.js.map +1 -1
- package/dist/commands/migrate-openspec.js +45 -28
- package/dist/commands/migrate-openspec.js.map +1 -1
- package/dist/commands/new.js +35 -34
- package/dist/commands/new.js.map +1 -1
- package/dist/commands/rebase.js +2 -4
- package/dist/commands/rebase.js.map +1 -1
- package/dist/commands/show.js +26 -31
- package/dist/commands/show.js.map +1 -1
- package/dist/commands/status.js +17 -25
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/unarchive.js +12 -5
- package/dist/commands/unarchive.js.map +1 -1
- package/dist/commands/validate.js +306 -142
- package/dist/commands/validate.js.map +1 -1
- package/dist/commands/verify.js +33 -17
- package/dist/commands/verify.js.map +1 -1
- package/dist/commands/vouch.js +156 -60
- package/dist/commands/vouch.js.map +1 -1
- package/dist/core/agent.js +392 -42
- package/dist/core/agent.js.map +1 -1
- package/dist/core/agents-stamp.js +47 -10
- package/dist/core/agents-stamp.js.map +1 -1
- package/dist/core/arch.js +28 -2
- package/dist/core/arch.js.map +1 -1
- package/dist/core/asyncapi.js +253 -0
- package/dist/core/asyncapi.js.map +1 -0
- package/dist/core/brief.js +28 -6
- package/dist/core/brief.js.map +1 -1
- package/dist/core/coherence.js +37 -13
- package/dist/core/coherence.js.map +1 -1
- package/dist/core/concurrency.js +51 -0
- package/dist/core/concurrency.js.map +1 -0
- package/dist/core/config.js +10 -2
- package/dist/core/config.js.map +1 -1
- package/dist/core/delta.js +21 -2
- package/dist/core/delta.js.map +1 -1
- package/dist/core/dependencies.js +46 -42
- package/dist/core/dependencies.js.map +1 -1
- package/dist/core/docs.js +65 -3
- package/dist/core/docs.js.map +1 -1
- package/dist/core/doctor.js +79 -27
- package/dist/core/doctor.js.map +1 -1
- package/dist/core/document-bytes.js +59 -0
- package/dist/core/document-bytes.js.map +1 -0
- package/dist/core/explore.js +225 -0
- package/dist/core/explore.js.map +1 -0
- package/dist/core/fleet-context.js +29 -63
- package/dist/core/fleet-context.js.map +1 -1
- package/dist/core/frontmatter.js +1 -5
- package/dist/core/frontmatter.js.map +1 -1
- package/dist/core/gherkin.js +3 -61
- package/dist/core/gherkin.js.map +1 -1
- package/dist/core/health.js +1 -5
- package/dist/core/health.js.map +1 -1
- package/dist/core/ids.js +34 -5
- package/dist/core/ids.js.map +1 -1
- package/dist/core/json.js +8 -1
- package/dist/core/json.js.map +1 -1
- package/dist/core/likec4.js +19 -4
- package/dist/core/likec4.js.map +1 -1
- package/dist/core/maturity.js +66 -0
- package/dist/core/maturity.js.map +1 -0
- package/dist/core/openapi-merge.js +39 -6
- package/dist/core/openapi-merge.js.map +1 -1
- package/dist/core/openapi.js +17 -5
- package/dist/core/openapi.js.map +1 -1
- package/dist/core/openspec-inventory.js +64 -27
- package/dist/core/openspec-inventory.js.map +1 -1
- package/dist/core/path-safety.js +59 -0
- package/dist/core/path-safety.js.map +1 -1
- package/dist/core/provenance.js +102 -10
- package/dist/core/provenance.js.map +1 -1
- package/dist/core/records.js +33 -0
- package/dist/core/records.js.map +1 -0
- package/dist/core/repo.js +77 -1
- package/dist/core/repo.js.map +1 -1
- package/dist/core/report.js +9 -0
- package/dist/core/report.js.map +1 -1
- package/dist/core/results.js +1 -3
- package/dist/core/results.js.map +1 -1
- package/dist/core/spec.js +28 -4
- package/dist/core/spec.js.map +1 -1
- package/dist/core/staging.js +124 -17
- package/dist/core/staging.js.map +1 -1
- package/dist/core/status.js +152 -35
- package/dist/core/status.js.map +1 -1
- package/dist/core/steps.js +72 -0
- package/dist/core/steps.js.map +1 -0
- package/dist/core/verify.js +18 -4
- package/dist/core/verify.js.map +1 -1
- package/dist/core/version.js +17 -1
- package/dist/core/version.js.map +1 -1
- package/package.json +1 -1
package/dist/core/agent.js
CHANGED
|
@@ -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,
|
|
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
|
|
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\`,
|
|
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
|
|
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
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
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
|
|
1148
|
-
not parse
|
|
1149
|
-
|
|
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
|
-
*
|
|
1522
|
-
*
|
|
1523
|
-
*
|
|
1524
|
-
*
|
|
1525
|
-
*
|
|
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
|
|
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
|
-
*
|
|
1569
|
-
*
|
|
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
|
-
*
|
|
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
|
|
1841
|
+
const stubbed = (c) => ({
|
|
1583
1842
|
...c,
|
|
1584
|
-
body: `${agentsStampLine(LOAM_VERSION)}\n\n${c
|
|
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
|
-
|
|
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
|
|
1755
|
-
*
|
|
1756
|
-
*
|
|
1757
|
-
*
|
|
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
|
|
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(
|
|
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
|
/**
|