@nextcommerce/campaigns-os 1.37.3 → 1.43.1

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 (76) hide show
  1. package/AGENTS.md +114 -10
  2. package/CHANGELOG.md +708 -0
  3. package/README.md +44 -31
  4. package/agents/claude/CLAUDE.md +5 -1
  5. package/campaign-spec/dist/types.d.ts +2 -0
  6. package/contracts/agent-relevant-change-policy.v1.json +11 -1
  7. package/contracts/effects.v1.json +4887 -0
  8. package/contracts/migration-sidecar-bundle.v0.json +9 -0
  9. package/contracts/release-ledger.json +1541 -0
  10. package/contracts/supported-surface.json +33 -12
  11. package/docs/build-packet.md +83 -22
  12. package/docs/campaigns-os-build-flow.md +2 -2
  13. package/docs/demo-preview.md +1 -1
  14. package/docs/diagnostics.md +7 -4
  15. package/docs/effects.md +350 -0
  16. package/docs/gateway-login.md +113 -0
  17. package/docs/local-setup.md +51 -0
  18. package/docs/migration-sidecar-bundle.md +6 -1
  19. package/docs/orientation-contract-reference.md +4 -1
  20. package/docs/progress-snapshots.md +9 -3
  21. package/docs/qa-and-test-orders.md +29 -13
  22. package/docs/readback.md +523 -0
  23. package/docs/runtime-readiness.md +1 -1
  24. package/docs/sdk-storage-compatibility.md +1 -1
  25. package/docs/skills-revision.md +364 -0
  26. package/docs/supported-surface.md +11 -3
  27. package/docs/versioning.md +8 -4
  28. package/package.json +10 -4
  29. package/schemas/campaign-runtime-assembly-report.v0.schema.json +6 -1
  30. package/schemas/campaign-runtime-build-packet.v0.schema.json +11 -1
  31. package/schemas/campaign-spec.v4.schema.json +4 -0
  32. package/schemas/campaigns-os-effects.v1.schema.json +211 -0
  33. package/schemas/campaigns-os-progress-snapshot.v0.schema.json +1 -0
  34. package/schemas/campaigns-os-qa-verdict-sidecar.v0.schema.json +1 -0
  35. package/schemas/campaigns-os-qa-verdict.v0.schema.json +1 -0
  36. package/schemas/campaigns-os-readback.v2.schema.json +267 -0
  37. package/schemas/campaigns-os-run-record.v0.schema.json +1 -0
  38. package/skills/campaign-lifecycle-orientation/SKILL.md +179 -0
  39. package/skills/campaign-readback-classification/SKILL.md +230 -0
  40. package/skills/campaign-run-evidence/SKILL.md +142 -0
  41. package/skills/contribution-intake/SKILL.md +85 -0
  42. package/skills/next-campaigns-build/SKILL.md +33 -12
  43. package/skills/next-campaigns-os/SKILL.md +59 -22
  44. package/skills/next-campaigns-os/references/session-intake.md +4 -4
  45. package/skills/next-campaigns-os-setup/SKILL.md +35 -14
  46. package/skills/next-campaigns-polish/SKILL.md +43 -17
  47. package/skills/next-campaigns-qa/SKILL.md +53 -28
  48. package/skills.json +40 -7
  49. package/src/admin-transport.mjs +123 -0
  50. package/src/cli.mjs +1178 -270
  51. package/src/credential-store.mjs +183 -0
  52. package/src/deviation.mjs +3 -2
  53. package/src/diagnostic.mjs +4 -1
  54. package/src/finding-cause.mjs +14 -10
  55. package/src/gate-actions.mjs +2 -2
  56. package/src/install-mode.mjs +17 -9
  57. package/src/lifecycle.mjs +96 -0
  58. package/src/login.mjs +152 -0
  59. package/src/package-install-fixture.mjs +3 -2
  60. package/src/polish-node.mjs +5 -2
  61. package/src/progress-node.mjs +3 -2
  62. package/src/progress.mjs +5 -3
  63. package/src/qa-node.mjs +105 -36
  64. package/src/qa-publish.mjs +112 -2
  65. package/src/qa-sidecar.mjs +2 -0
  66. package/src/qa-verdict-discovery.mjs +11 -0
  67. package/src/qa-verdict-publish.mjs +1 -0
  68. package/src/qa-verdict.mjs +8 -1
  69. package/src/readback.mjs +1937 -0
  70. package/src/remit.mjs +17 -3
  71. package/src/run-record-closeout.mjs +3 -4
  72. package/src/run-record.mjs +4 -0
  73. package/src/sidecar-bundle.mjs +21 -0
  74. package/src/spec-source-identity.mjs +44 -0
  75. package/src/stage-ledger.mjs +4 -1
  76. package/src/tooling-setup.mjs +160 -0
@@ -0,0 +1,350 @@
1
+ # Declared command effects
2
+
3
+ `contracts/effects.v1.json` states, for every supported invocation of this
4
+ toolkit, what it **writes** and what it **sends**. It is the file to read before
5
+ you let an agent run a command it has not run before, and it is the file a tool
6
+ face would read to decide whether an invocation needs a human in the loop.
7
+
8
+ The point of the file is not the prose. It is that **every row is proved by a
9
+ test** (`src/effects.test.mjs`), and a row without its test cannot be published:
10
+ `npm run check:effects` refuses it.
11
+
12
+ `tooling setup` composes the existing skill/context/browser installers after a
13
+ project-pin and preservation preflight. It also appends a project `CLAUDE.md`
14
+ import. It bypasses session recovery, gateway credential reads and lifecycle
15
+ capture; `--dry-run` is read-only. Like `qa install-browser`, its browser download
16
+ has preflight-only effects proof offline; setup's preservation and recovery
17
+ behavior has focused tests.
18
+
19
+ - The contract: [`contracts/effects.v1.json`](../contracts/effects.v1.json)
20
+ - Its shape: [`schemas/campaigns-os-effects.v1.schema.json`](../schemas/campaigns-os-effects.v1.schema.json)
21
+ - The proof: `src/effects.test.mjs`
22
+ - The gate: `scripts/check-effects.mjs` (`npm run check:effects`)
23
+
24
+ ## The vocabulary
25
+
26
+ ### Annotations
27
+
28
+ Four booleans per row, spelled the way an MCP tool face spells them, so a host
29
+ that already understands those hints needs no translation layer.
30
+
31
+ | Annotation | Meaning |
32
+ | --- | --- |
33
+ | `readOnlyHint` | The invocation changes **nothing**: no file under the target, the working directory or your machine, and no request off the machine. |
34
+ | `destructiveHint` | The invocation can overwrite, clear or discard state that existed before it ran. Only meaningful when `readOnlyHint` is false. |
35
+ | `openWorldHint` | The invocation can contact an endpoint off this machine. True exactly when the row declares at least one send. |
36
+ | `idempotentHint` | Repeating the invocation with the same arguments adds no effect beyond the first run. |
37
+
38
+ **`readOnlyHint` counts the command-lifecycle journal.** A journal append is a
39
+ write like any other, so every `readOnlyHint: true` row is an invocation the
40
+ CLI exempts from lifecycle capture (the converse does not hold: `demo` and the
41
+ `--no-write` forms skip the journal but still write other declared files): `help`, `readback`,
42
+ `run status`, `doctor` inspection, `doctor --no-write`, `sdk storage-check`,
43
+ `tooling diagnose`, a refused invocation, `run-record --no-write`, and every
44
+ `--dry-run` form on the commands that implement the flag. Everything else
45
+ appends an entry when a journal is selected — an active run session,
46
+ `--lifecycle-journal`, or `CAMPAIGNS_OS_LIFECYCLE_LOG` — and is therefore not
47
+ read-only, even when the command writes no artifact of its own. `standardize`
48
+ and `bundle check` are tier `B` for exactly that reason and nothing else; their
49
+ rows say so.
50
+
51
+ ### Tiers
52
+
53
+ | Tier | Meaning |
54
+ | --- | --- |
55
+ | `none` | No effect: nothing written anywhere, nothing sent. |
56
+ | `B` | Writes files under the target, the working directory or your machine. Nothing leaves the machine. |
57
+ | `A` | Can contact an endpoint off this machine (it may write locally too). |
58
+ | `C` | Destructive: overwrites, clears or discards state that was already there (it may send too). |
59
+
60
+ A row carries the **highest** tier it can reach, ranked `none < B < A < C`.
61
+
62
+ ### Location tokens
63
+
64
+ A write path is a glob (`*` within one segment, `**` across segments) that opens
65
+ with one of these:
66
+
67
+ | Token | Resolves to |
68
+ | --- | --- |
69
+ | `{target}` | The target the invocation names: the directory given to `--target`, or the Page Kit target repository the Build Packet points at. |
70
+ | `{cwd}` | The working directory the invocation runs in. |
71
+ | `{spec}` | The CampaignSpec file the Build Packet names (`spec.local_path`), which need not live inside the target repository. |
72
+ | `{home}` | Your machine: the home directory and the config root under it (`XDG_CONFIG_HOME` when set). |
73
+ | `{packet}` | The Build Packet the invocation names (`--packet`), wherever it lives — it need not be the copy inside the target repository. |
74
+ | `{lifecycle-journal}` | The command-lifecycle journal wherever it was selected for this invocation. |
75
+ | `{proxy-base}` | The endpoint `--proxy-base` names, or the canonical NEXT endpoint when it does not. |
76
+ | `{base-url}` | The campaign under test, as `--base-url` names it or as the packet derives it. |
77
+ | `{playwright-download-host}` | Where Playwright fetches browser builds from: `PLAYWRIGHT_DOWNLOAD_HOST` when set, else the Playwright CDN. The third-party browser download used by `qa install-browser` and `tooling setup`. |
78
+
79
+ The tokens matter because effects are not all under the target. `install-skills`
80
+ writes your **home** directory, not the campaign. `telemetry on` writes your
81
+ **machine** config. `run-record` writes beside the **working directory**, not the
82
+ target repo. A row that said "writes the target" would be wrong about all three.
83
+
84
+ ## How to read a row
85
+
86
+ ```jsonc
87
+ {
88
+ "command": "page-kit",
89
+ "subcommand": "sync",
90
+ "flags": [], // the base form; --dry-run is its own row
91
+ "annotations": { "readOnlyHint": false, "destructiveHint": false,
92
+ "openWorldHint": false, "idempotentHint": true },
93
+ "tier": "B",
94
+ "writes": [
95
+ { "path": "{target}/_data/campaigns.json",
96
+ "when": "one of the ten Store Profile / SDK-pin fields is usable and differs from the entry",
97
+ "observed_in": ["no_session", "ambient_session", "stale_session", "lifecycle_log"] }
98
+ // …
99
+ ],
100
+ "sends": [],
101
+ "effect_test": "effects: page-kit sync",
102
+ "test_scope": "full",
103
+ "notes": "Writes only those ten fields of the packet's route entry…"
104
+ }
105
+ ```
106
+
107
+ There is **one row per command and per effect-changing flag combination**. The
108
+ flags that change what the invocation does to the world are listed once, in
109
+ `vocabulary.effect_changing_flags`: `--browser`, `--built`, `--dry-run`,
110
+ `--emit-packet`, `--example`, `--force`, `--from-store`, `--list`,
111
+ `--no-post-verdict`, `--no-probe`, `--no-remit`, `--no-run-session`,
112
+ `--no-write`, `--republish`, `--test-order`, `--write`, `--write-map`. Flags
113
+ that only change the output shape (`--json`, `--report`) deliberately do not.
114
+
115
+ **"The help text" is every help block the CLI prints**, not one file's.
116
+ `campaigns-os qa` prints its own from `src/qa-node.mjs`, and while the coverage
117
+ scan read only `src/cli.mjs` the three subcommands documented there alone — `qa
118
+ parity`, `qa waive` and `qa install-browser` — owed no row, had none, and the
119
+ gate stayed green. Every module that owns a usage block is listed in
120
+ `HELP_SOURCE_PATHS` and scanned the same way; a test derives that list from the
121
+ source, so a command that grows its own help cannot quietly leave the scan.
122
+
123
+ **Every one of those flags that a help usage line carries owes a row**, and
124
+ `scripts/check-effects.mjs` fails when one does not have it. Coverage by command
125
+ alone was not enough: deleting the `page-kit sync --dry-run` row, or the
126
+ `doctor --write` row, left the gate green while the file lost an effect —
127
+ `doctor --write` writes the doctor sidecar, the assembly report and the packet
128
+ that plain `doctor` does not. A flag that appears in a usage line for a command
129
+ that has only a base row is now the loudest kind of failure this gate has.
130
+
131
+ One row is not a command at all: `{"command": "*refused*"}` is any invocation
132
+ refused before its handler runs — an unknown command, an unknown subcommand, or
133
+ a flag the command rejects up front. It writes nothing, journals nothing, and is
134
+ the row to read when you want to know what a typo costs. The one exception is
135
+ declared on the rows it belongs to: `start`, `prepare-build`, `build`,
136
+ `run start` and `run end` close out a **stale** run session at the root they are
137
+ about to act on *before* argv is refused.
138
+
139
+ A refusal is decided by argv alone. When file content or state on disk decides
140
+ the outcome, the command has reached a handler failure and journals it.
141
+
142
+ For intake, run-record, built-site QA, and `next`, argv-only checks run before
143
+ their handler reads the target; invalid values are refused without a journal
144
+ entry. For `start`, `prepare-build`, and `build`, bare, empty, and whitespace-only
145
+ values of `--spec`, `--map-id`, `--source`, `--target`, `--source-kind`,
146
+ `--proxy-base`, `--wrapper-policy`, `--design-manifest`, and
147
+ `--order-path-depth` are refused before local spec reads, Map fetches, or cache
148
+ writes on the `--spec`, `--map-id`, and `--map-id --cached-spec` paths.
149
+ The operator-facing `run-record` and `run end` commands refuse bare, empty, or
150
+ whitespace-only values for every value-taking inherited run-record flag before
151
+ packet work. The five agent
152
+ token and elapsed-time flags retain their non-negative-integer diagnostics;
153
+ `--surfaces` rejects unknown values, and `--dry-run` rejects a value. The
154
+ inherited boolean flags (`--no-remit`, `--no-write`, `--dry-run`, and `--json`)
155
+ retain their bare-flag behavior. `run end` also rejects `--new-run` and
156
+ `--run-id` because the saved session fixes its run ID. `run-record` also
157
+ rejects bare, empty, or whitespace-only `--run-id` and valued `--new-run`.
158
+ Internal stale-session and QA closeouts retain the previous handling of values
159
+ inherited from their invoking commands. A bare, empty, or whitespace-only
160
+ `--proxy-base` on a sweeping command still writes the stale Run Record.
161
+ Terminal QA auto-end tolerates whitespace-only inherited `--context`,
162
+ `--report`, or `--proxy-base`. A whitespace-only `--context` resolves as a
163
+ literal relative path, so the default context file is not read. Bare or empty
164
+ `--context` or `--report` still makes QA auto-end fail and leaves the session
165
+ open; bare or empty `--qa-verdict` fails a Run Record closeout when inherited,
166
+ though QA auto-end supplies its own verdict path. The underlying run-record
167
+ handler still rejects invalid agent
168
+ integers, unknown `--surfaces`, and any valued `--dry-run` that reaches it. QA
169
+ auto-end drops `--dry-run` from inherited flags; if another inherited value
170
+ fails in the handler, auto-end is skipped and the session stays open. QA's own
171
+ journal entry is unaffected because auto-end runs after QA persistence. A named
172
+ `--design-manifest` that is missing or is not a file is checked
173
+ against the filesystem after intake has begun, so that failure is journaled.
174
+ An invalid manifest's contents are likewise a handler failure. A `next` stage
175
+ must be one of the stages in the orchestration stage contract; an unknown name
176
+ is refused before the `next` handler reads the packet, runs doctor, or writes
177
+ doctor output. The ambient run-session lookup in `main()` may read a named
178
+ `--packet` before the handler runs.
179
+
180
+ `polish capture --packet` would report "polish capture requires
181
+ packet.assembly.target_repo to resolve to a local target repo" as a journaled
182
+ handler failure because packet content would decide it. Today the workspace
183
+ resolver always yields a local path, so this check does not fire through the
184
+ CLI. `run end` reports "run end needs a build packet" as a journaled handler failure
185
+ when the saved session has no packet and argv names none. For `qa run` and `qa
186
+ resolve`, "QA requires a Map ID" is a refusal when argv carries no non-empty
187
+ `--packet`, `--site`, `--built`, positional Map ID, or `--map-id` value. A selector
188
+ flag without a value is refused with "Missing value for --<flag>". If a named
189
+ packet yields neither a Map ID nor a valid local-spec identity after checkpoint
190
+ preflight reads the packet, spec, and report, the requirement is a journaled
191
+ handler failure. A conflicting local/Map identity is also a handler failure.
192
+ The nested run-record refusal scope in session closeout guards against future
193
+ changes. No internal closeout can currently create a refusal before its
194
+ invoking command journals.
195
+
196
+ ## How a row is proved
197
+
198
+ `src/effects.test.mjs` runs the real CLI in a disposable target seeded from
199
+ `examples/`, under **five conditions**, and snapshots the whole tree (paths plus
200
+ sha256) before and after while a loopback `node:http` receiver counts requests.
201
+
202
+ | Condition | What it sets up |
203
+ | --- | --- |
204
+ | `no_session` | No run session at the target or the working directory. |
205
+ | `ambient_session` | An active ambient run session opened by `run start` at the target. |
206
+ | `stale_session` | A run session idle past the 12 h TTL, at the target and at the working directory. |
207
+ | `lifecycle_log` | `CAMPAIGNS_OS_LIFECYCLE_LOG` names a journal outside the runtime directory. |
208
+ | `persisted_consent` | Run Telemetry consent **persisted on the machine for the loopback receiver's scope**, a synthetic campaign key in the environment, no run session, and `--proxy-base <loopback>` wherever the command takes it. |
209
+
210
+ Five conditions rather than one, because the CLI's effects are not a function of
211
+ argv alone: an ambient session redirects the journal and is itself touched by
212
+ session resolution, and a stale session is closed out — Run Record assembled —
213
+ before some commands even read argv.
214
+
215
+ ### Why the fifth condition exists
216
+
217
+ Under the first four, consent is `CAMPAIGNS_OS_TELEMETRY=off` unless the row
218
+ declares a consent-gated send it expects to see in that condition; then the row
219
+ runs with consent on and the loopback receiver as its endpoint, so "nothing was
220
+ sent" is not an artefact of consent being off **for a send that is declared**.
221
+
222
+ That took the row's word for which sends exist, and it hid real ones: `next` and
223
+ its five stage forms, and all three `qa run` rows, declared `sends: []` while
224
+ each of them POSTed — to `{proxy-base}/api/progress`, and for `qa run` to
225
+ `{proxy-base}/api/qa/verdicts` as well, on blocked attempts included.
226
+
227
+ `persisted_consent` does not read consent from the row. It persists consent the
228
+ way an operator does — `campaigns-os telemetry on --proxy-base <loopback>`,
229
+ which is a **scoped** record — and runs every row that way. An environment
230
+ override is not equivalent and is the reason the earlier probe found nothing: an
231
+ env grant carries no scope, so the remit refuses it for a non-canonical endpoint
232
+ (`scope_bypassed`) and delivers nothing. Any request the receiver sees that no
233
+ declared send covers fails the row.
234
+
235
+ The same scoping is what keeps the suite off the network. The remit endpoint is
236
+ a hard-coded constant with no environment override, so a command that falls back
237
+ to the canonical endpoint resolves consent **off** (the persisted grant covers
238
+ the loopback scope only) and sends nothing. That is asserted, not assumed: every
239
+ invocation in this condition runs under `NODE_DEBUG=net` and its connection log
240
+ must name no host but `127.0.0.1`, and one case states the claim directly for
241
+ `next` with no `--proxy-base` at all.
242
+
243
+ The assertion runs both ways, and that is what makes the file falsifiable:
244
+
245
+ 1. **Nothing undeclared may change**, in any condition. A `readOnlyHint: true`
246
+ row declares no writes, so any byte that moves fails it.
247
+ 2. **Every declared effect whose `observed_in` names a condition must be seen**
248
+ in it, so a row cannot be padded with effects that never happen.
249
+
250
+ `observed_in` is per effect, not per row: `install-agent-context` writes
251
+ `{target}/.gitignore` only when the target does not already ignore the runtime
252
+ directory, so that entry is observed in three conditions and not under
253
+ `ambient_session`, where `run start` has already added the line.
254
+
255
+ ### Rows proved at the preflight
256
+
257
+ Some invocations cannot execute past their preflight with no network, no
258
+ browser, no renderer and no credentials. Those rows carry
259
+ `test_scope: "preflight"`. Their case proves the preflight refusal writes
260
+ nothing beyond what the row declares and — where a loopback receiver can stand
261
+ in for the destination — that the **declared destination is the one contacted**.
262
+
263
+ | Row | What the offline fixture cannot reach |
264
+ | --- | --- |
265
+ | `login` | A reachable login gateway and a human at a browser. Proved: the failure path writes nothing at all — no credential, no journal entry. |
266
+ | `logout` | A credential minted by a gateway login. Proved: the no-credential path writes nothing. |
267
+ | `page-kit parity` | A `local-serve` deploy target and a page-kit renderer to build the two renders with. Proved: the refusal writes nothing but the journal entry. |
268
+ | `polish capture` | An installed browser and a reachable `--base-url`. Proved: the refusal writes nothing but the journal entry and contacts nothing. |
269
+ | `qa install-browser` | The Playwright CDN, and the ~150 MB Chromium archive it serves. Proved: the failed download writes exactly one path under your machine — the registry's link entry — and nothing else anywhere, and leaves the machine zero times. |
270
+ | `qa parity` (and `--no-post-verdict`) | An installed Chromium and a reachable candidate funnel. Proved: the refusal writes nothing but the journal entry and contacts the stand-in for `--base-url` zero times. |
271
+ | `qa resolve` | A resolution that is not blocked before the probe. Proved: the blocked resolution contacts the stand-in zero times. |
272
+ | `qa run --browser` | An installed Chromium and a reachable campaign. Proved: the attempt is blocked at the same gate as the node run and writes exactly the blocked-attempt evidence. |
273
+ | `spec derive --from-store` | A live gateway and a real store credential. Proved: the credential refusal writes nothing under the target or the spec. |
274
+ | `spec derive --write-map` | A Map whose `spec_hash` precondition a loopback stand-in can satisfy, so the `PUT` is never reached. Proved: the declared destination **is** the one contacted (the receiver sees the Map read), and the refusal adds no report evidence. |
275
+ | `telemetry list` | A real ops admin key and a real endpoint. Proved: the receiver sees the declared `GET /api/runs`, and the refusal writes nothing but the journal entry. |
276
+
277
+ #### What a preflight row is allowed to touch
278
+
279
+ A preflight row declares its allowances **separately from its effects**, and the
280
+ test enforces them independently:
281
+
282
+ ```jsonc
283
+ "preflight": {
284
+ "may_write": ["{lifecycle-journal}"], // the ONLY paths the refusal may write
285
+ "may_contact": ["/api/runs"] // the exact request paths the receiver may see
286
+ }
287
+ ```
288
+
289
+ Both halves close a hole that a declared effect used to open. `logout` declared
290
+ `{home}/**` for the credential a *completed* login writes — and that declaration
291
+ also licensed its refusal to write anywhere under the home directory, so a
292
+ home-directory write injected into the preflight passed. And a destination a
293
+ loopback receiver only stands in for (`{base-url}`, the login gateway) matched
294
+ **any** request path, so an injected endpoint passed too. Now:
295
+
296
+ - `may_write` is the whole permission. It may not name a whole location
297
+ (`{target}`, `{target}/**`) and may not span segments under `{home}` — the
298
+ skills directories, the credential store and the consent file all live there,
299
+ under the temporary `HOME` the test sets, and a preflight that writes one of
300
+ them has to say which.
301
+ - `may_contact` is matched literally against the request path, so an `/api/`
302
+ call nobody declared fails the row even when the row declares a stand-in
303
+ destination. (On a `full` row the same rule holds one step down: a stand-in
304
+ destination never covers an `/api/` path, because every API endpoint in this
305
+ contract is declared as `{proxy-base}/api/…`.)
306
+ - A write the row declares as observed must also be in `may_write`; the gate
307
+ refuses the contradiction rather than letting the test find it.
308
+
309
+ A `full` row may still carry an individual effect the offline fixture cannot
310
+ reach — the Map Builder spec fetch behind `--map-id`, the `codex` and `agents`
311
+ destinations of `install-skills`. Each such entry has an empty `observed_in`
312
+ **and** a `not_observed_reason`, and `check-effects.mjs` refuses one without the
313
+ reason. What it may not be is silent.
314
+
315
+ ## The rule
316
+
317
+ **A row without its test is not published.** `scripts/check-effects.mjs` (in
318
+ `npm run check` and `npm run check:contracts`) fails when:
319
+
320
+ - a command on the supported CLI surface, a subcommand any help block teaches
321
+ (`src/cli.mjs` and `src/qa-node.mjs`), or an effect-changing flag a help usage
322
+ line carries, has no row;
323
+ - a row names no `effect_test`, names one `src/effects.test.mjs` does not
324
+ declare, or names one the per-row generator would not produce (the cases are
325
+ generated from this file, so an unchecked name made the link vacuous);
326
+ - a row has no entry in the test's `INVOCATIONS` table, or the table has an
327
+ entry no row claims — a generated case with no argv proves nothing;
328
+ - an effect declares no `observed_in` and no `not_observed_reason`;
329
+ - a `test_scope: "preflight"` row does not say in its own notes what it cannot
330
+ reach, declares no `preflight` allowances, licenses a whole location or a
331
+ home-directory subtree, names a `may_contact` entry that is not a request
332
+ path, or has an observed write its `may_write` does not allow; a
333
+ `test_scope: "full"` row carries allowances, or has no effect observed
334
+ anywhere;
335
+ - the annotations disagree with the row (`readOnlyHint` with declared effects,
336
+ `openWorldHint` without a send, `destructiveHint` off tier `C`);
337
+ - two rows claim the same invocation, or a write path opens with no known
338
+ location token;
339
+ - `vocabulary.conditions` names a condition `src/effects.test.mjs` does not run.
340
+
341
+ ## When you change a command
342
+
343
+ Change the effect, change the row, in the same PR. The effect test will tell you
344
+ which row is wrong before review does: it names the path that moved and the row
345
+ that failed to declare it.
346
+
347
+ Local-spec QA retains its artifacts locally. It never sends a verdict or progress
348
+ to the Map portal, even when `--post-verdict` is supplied; `qa publish` refuses
349
+ local-spec packets. Commerce API reads, served-page probes and requested typed-card
350
+ orders keep their existing effects. Run Telemetry still follows its consent controls.
@@ -0,0 +1,113 @@
1
+ # Gateway login and direct-token migration
2
+
3
+ The 1.38.0 candidate supports the admitted owned-store private gateway pilot
4
+ with the registered Campaigns OS CLI client. It is not general merchant
5
+ availability. Publication, external trials and additional clients are separately
6
+ gated. A successful owned-store drill does not prove automatic uninstall
7
+ handling or authorize other stores.
8
+
9
+ ## Sign in
10
+
11
+ ```sh
12
+ campaigns-os login --store example
13
+ # The equivalent canonical host is example.29next.store.
14
+ ```
15
+
16
+ `--store` is optional in an interactive terminal: one prompt asks for the store.
17
+ There is no discovery or guess from the current project. Noninteractive calls
18
+ must supply `--store`. URLs, paths and unrelated hosts are refused before any
19
+ request. Login uses the fixed `https://mcp.nextcommerce.com` gateway.
20
+
21
+ Open the displayed device page in one browser tab and enter the displayed code.
22
+ Keep that tab: if installation is needed, follow its Install Campaigns link,
23
+ sign in to the store dashboard and launch Campaigns. Match the code and explicitly
24
+ allow reads. Return to the CLI. Pilot admission is operator controlled; knowing
25
+ the store host or device-page URL is not an invitation. Never paste a dashboard
26
+ or Admin token into the CLI. Login waits for browser consent within the device
27
+ code's expiry; denial, timeout and failed persistence preserve the prior login.
28
+
29
+ Gateway access and refresh credentials are stored outside the project. On macOS,
30
+ the CLI prefers the user keychain; when unavailable it uses private user files
31
+ under `~/.campaigns-os/credentials` (directory `0700`, files `0600`). Keychain
32
+ selection metadata lives there too. The parent must be owned by the user and
33
+ not group/other writable. Credential paths reject symlinks; a symlinked home is
34
+ not supported in this pilot. Do not copy these files into a repository, support
35
+ export or CI secret bundle. These are gateway credentials, not platform Admin
36
+ tokens; platform OAuth custody stays server side.
37
+
38
+ ## Migrate store reads
39
+
40
+ The default changed. A command that formerly read `EXAMPLE_ADMIN_TOKEN`
41
+ automatically now requires a gateway login for the selected store:
42
+
43
+ ```sh
44
+ campaigns-os login --store example
45
+ campaigns-os spec derive --packet campaign-runtime.build.json --from-store example --dry-run
46
+ ```
47
+
48
+ Existing direct callers, including callers outside the admitted pilot, can retain
49
+ the direct path by explicitly naming their existing environment variable:
50
+
51
+ ```sh
52
+ campaigns-os spec derive --packet campaign-runtime.build.json --from-store example --store-token-source env:EXAMPLE_ADMIN_TOKEN --dry-run
53
+ ```
54
+
55
+ This break-glass path emits a warning and bypasses gateway custody. Its Admin
56
+ token needs `store:read` and `content:read`. Do not put its value in argv.
57
+ The default never checks that variable or falls back to it after a gateway error.
58
+ An unavailable gateway fails closed. Gateway reads use `/admin/store/` and
59
+ `/admin/pages/`; custody consolidates bounded upstream page results. Derivation
60
+ still uses the same nine Store Profile fields and leaves missing or ambiguous
61
+ values unchanged. The output distinguishes the actual gateway endpoint from
62
+ the logical store Admin API source. See [Store Profile derivation](build-packet.md).
63
+
64
+ Refresh is serialized per store binding. The CLI records a pending state before
65
+ sending a refresh, then atomically saves the confirmed winning pair. A lost
66
+ response, interrupted process or uncertain save requires a new login; the next
67
+ invocation must not replay an old refresh. An expired absolute grant also requires
68
+ login. A refresh may happen before access expires, or once after an unauthorized
69
+ read; it is not an unlimited retry loop.
70
+
71
+ ## Inspect and recover
72
+
73
+ `campaigns-os tooling status --json` includes `gateway_login` metadata for saved
74
+ bindings, without `--store` or project inference. It shows store, local access
75
+ expiry/remaining time and the gateway version reported when credentials were
76
+ issued. It makes no gateway validity request and exports no credential values.
77
+ `logged_in` means the saved access expiry is in the future, not that the remote
78
+ grant is still valid. `access_expired` can still refresh on use; `login_required`
79
+ means reauthorize. A reported version such as `a3-offline` is metadata, not proof
80
+ of deployed source identity. `tooling diagnose` remains a separate redacted
81
+ support export and omits gateway login/store metadata.
82
+
83
+ Storage contention waits up to three seconds, then reports unavailable/busy.
84
+ Retry after the other CLI finishes; check user-directory permissions and keychain
85
+ access. One malformed or unreadable record makes the whole gateway status
86
+ unavailable in this pilot; it does not prove that all stores are logged out.
87
+ After a crash, confirm no Campaigns OS process is running before removing the
88
+ stale binding's `.lock` directory under `~/.campaigns-os/credentials`. Never
89
+ remove another live process's lock. If selection metadata is damaged, preserve
90
+ it privately and repair or move aside only that binding's broken selection file
91
+ before logging in again; this does not remotely revoke an old grant. Do not
92
+ bypass ownership or symlink checks by making the directory world writable.
93
+
94
+ ## Sign out
95
+
96
+ ```sh
97
+ campaigns-os logout --store example
98
+ ```
99
+
100
+ Logout uses the same optional interactive store prompt. It attempts gateway
101
+ revocation and clears the local selected login. Its message distinguishes
102
+ confirmed remote revocation from an unrecognized grant, failed request or
103
+ unreadable local record. Local cleanup alone is not proof of remote revocation;
104
+ a failed keychain-item cleanup is reported separately. A pending/uncertain
105
+ refresh is never replayed during logout. If remote revocation is unconfirmed,
106
+ use the pilot operator's grant-revocation procedure; do not assume uninstall or
107
+ local file deletion revoked it.
108
+
109
+ Login, logout and the offline demo bypass lifecycle capture; login/logout do not
110
+ accept general lifecycle flags. `tooling diagnose` also bypasses lifecycle
111
+ capture. Gateway login does not grant telemetry administration:
112
+ `CAMPAIGN_OPS_ADMIN_KEY` remains a separate cross-tenant `/api/runs` credential,
113
+ with its existing trusted-origin safeguards and explicit warning.
@@ -0,0 +1,51 @@
1
+ # Local campaign setup
2
+
3
+ For a new campaign, choose its working folder and run this from that folder:
4
+
5
+ ```sh
6
+ npm install --save-dev --save-exact @nextcommerce/campaigns-os@1.43.1 next-campaign-page-kit@0.2.0 && npx --no-install campaigns-os tooling setup --target . --platform claude
7
+ ```
8
+
9
+ Review the release source/provenance before installation as described in
10
+ `AGENTS.md`. npm installs the dependencies first; `--no-install` then runs only
11
+ the project's installed CLI. Keep `package.json` and `package-lock.json` in
12
+ Git. For an existing project, preserve its reviewed pin: run `npm ci`, then
13
+ `npx --no-install campaigns-os tooling setup --target . --platform claude`
14
+ on a release that supports setup. Changing the pin is a separate update.
15
+
16
+ Setup checks the exact toolkit pin, its lockfile version and the installed
17
+ page-kit dependency before it changes files. It composes the existing
18
+ installers to:
19
+
20
+ 1. Install the QA browser through this toolkit's own Playwright package.
21
+ 2. Install the bundled skills into `~/.claude/skills` (same-name skills are
22
+ refreshed just as with `install-skills`).
23
+ 3. Install the four context files under `.campaign-runtime/agent-context`
24
+ and the managed runtime ignore block.
25
+ 4. Append one import to the project's `CLAUDE.md`, preserving existing text.
26
+
27
+ The import uses Claude Code's documented
28
+ [`@path` syntax](https://code.claude.com/docs/en/memory#import-additional-files).
29
+ Existing context that differs from the bundle and symlink destinations require
30
+ reconciliation before setup; setup does not overwrite them. A repeated run
31
+ preserves campaign pages, authored decisions and project instructions. If the
32
+ browser download fails, fix that error and rerun setup; shared skills and
33
+ project files have not been changed. If the runtime ignore block cannot be
34
+ written, setup reports `context_install_failed`; fix `.gitignore` and rerun.
35
+ `--dry-run --json` previews setup without any writes or browser download.
36
+
37
+ Restart Claude Code in the campaign folder. Use the `next-campaigns-os` skill
38
+ and provide the configured campaign details, HTML/assets and brief. The agent
39
+ authors a local CampaignSpec if there is no saved Map export; follow the
40
+ [local-spec entry](build-packet.md#local-spec-entry). The skill checks its
41
+ loaded bundle revision against the project copy.
42
+ `restart_required` means the files are installed; it does not prove that the
43
+ running agent has loaded them. Check Claude's `/context` view if the project
44
+ instructions are missing.
45
+
46
+ Setup does not scaffold template pages, create a CampaignSpec, connect the
47
+ gateway, change a saved Map, run a campaign session, remit telemetry, or prove
48
+ checkout. The agent performs intake and chooses the template before assembly.
49
+ A local spec uses `spec_identity.local_spec_id` and keeps its evidence in the
50
+ repository. Existing doctor/QA gates still apply. This entry is Claude Code first; other agents retain their existing
51
+ manual installation path.
@@ -55,12 +55,17 @@ contract. A packet found only at
55
55
  remedy; conformance does not silently widen discovery.
56
56
 
57
57
  The checker validates canonical paths, declared schema versions, strict UTC
58
- timestamps, cross-artifact Map ID, public slug, campaign directory, live URL
58
+ timestamps, cross-artifact Map ID or local-spec ID, public slug, campaign directory, live URL
59
59
  path, template family, and spec identity, doctor freshness, and the URL/order-
60
60
  free QA projection. Safe repository-relative spellings such as
61
61
  `campaign-runtime.build.json` and `./campaign-runtime.build.json` are
62
62
  equivalent; absolute paths, URIs, backslashes, and parent traversal are not.
63
63
 
64
+ Local-spec bundles compare `local_spec_id` across the packet, report, doctor
65
+ output and QA sidecar. Their Map IDs remain null; the QA verdict's
66
+ `campaign_slug` is the storage key `local-spec-<local_spec_id>`. Mixing local
67
+ and saved-Map identities fails conformance; a shared public route is not enough.
68
+
64
69
  Spec identity has two deliberately separate meanings. Build Context
65
70
  `spec.hash` and Assembly Report `identity.spec_hash` retain exact raw-byte
66
71
  integrity. Build Context `spec.material_hash`, Assembly Report
@@ -25,7 +25,7 @@ Ledger schema id: `campaigns-os-release-ledger/v1`
25
25
  Change policy version: `1.0.0`
26
26
  Reason-code vocabulary version: `1.0.0`
27
27
  Limits version: `1.0.0`
28
- Supported surface at generation time: `1.37.3`
28
+ Supported surface at generation time: `1.43.1`
29
29
 
30
30
  ## Forward compatibility
31
31
 
@@ -244,6 +244,9 @@ so a renamed command fails here as well as at the supported-surface gate.
244
244
  - `campaigns-os run-record`
245
245
  - `campaigns-os run`
246
246
  - `campaigns-os demo`
247
+ - `campaigns-os login`
248
+ - `campaigns-os logout`
249
+ - `campaigns-os readback`
247
250
 
248
251
  ## Terminal outcome examples
249
252
 
@@ -1,8 +1,8 @@
1
1
  # Minimal progress observations
2
2
 
3
- Candidate release **1.36.0** adds the portable `@nextcommerce/campaigns-os/progress`
4
- export and `schemas/campaigns-os-progress-snapshot.v0.schema.json`. The currently
5
- published install example does not include this feature. Progress is a compact
3
+ Release **1.36.0** adds the portable `@nextcommerce/campaigns-os/progress`
4
+ export and `schemas/campaigns-os-progress-snapshot.v0.schema.json`; it first
5
+ shipped in 1.37.1 and is in every later release. Progress is a compact
6
6
  observation of the existing lifecycle, not a second workflow or proof of readiness.
7
7
 
8
8
  `next --packet <packet>` records the canonical picker result after the same doctor
@@ -140,3 +140,9 @@ The planned immutable receiver key is
140
140
  revision. A key match identifies scope; it is not authentication or trust. The
141
141
  receiver must verify the digest and authorized Map scope and stamp its own trust.
142
142
  Unknown, incomplete or conflicted histories must never yield a ready workspace.
143
+
144
+ Local-spec packets add optional `identity.local_spec_id`. Report binding compares
145
+ that ID and the local material hash, so local stages can be observed without a
146
+ saved Map. `map_id` and `map_revision_hash` remain null and
147
+ `saved_revision_alignment` remains `unconfirmed`; these observations stay on disk
148
+ with `map_id_missing` and have no portal storage key.