@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.
- package/CHANGELOG.md +49 -0
- package/README.md +6 -1
- 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 +70 -0
- package/dist/commands/instructions.js.map +1 -0
- package/dist/commands/list.js +6 -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 +8 -24
- 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 +115 -133
- 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 +77 -59
- package/dist/commands/vouch.js.map +1 -1
- package/dist/core/agent.js +273 -31
- package/dist/core/agent.js.map +1 -1
- package/dist/core/arch.js +28 -2
- package/dist/core/arch.js.map +1 -1
- package/dist/core/brief.js +3 -3
- 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 +2 -1
- package/dist/core/docs.js.map +1 -1
- package/dist/core/doctor.js +41 -26
- 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 +16 -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 +6 -1
- 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 +35 -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 +75 -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 +3 -3
- 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 +122 -33
- 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
|
@@ -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
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
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
|
|
1148
|
-
not parse
|
|
1149
|
-
|
|
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
|
-
*
|
|
1569
|
-
*
|
|
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
|
-
*
|
|
1572
|
-
*
|
|
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
|
|
1761
|
+
const stubbed = (c) => ({
|
|
1583
1762
|
...c,
|
|
1584
|
-
body: `${agentsStampLine(LOAM_VERSION)}\n\n${c
|
|
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
|
-
|
|
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
|
|
1755
|
-
*
|
|
1756
|
-
*
|
|
1757
|
-
*
|
|
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,
|
|
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(
|
|
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
|
/**
|