@nextcommerce/campaigns-os 1.37.3 → 1.41.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/AGENTS.md +114 -10
  2. package/CHANGELOG.md +530 -0
  3. package/README.md +38 -27
  4. package/contracts/agent-relevant-change-policy.v1.json +11 -1
  5. package/contracts/effects.v1.json +4794 -0
  6. package/contracts/release-ledger.json +789 -0
  7. package/contracts/supported-surface.json +25 -5
  8. package/docs/build-packet.md +27 -16
  9. package/docs/demo-preview.md +1 -1
  10. package/docs/diagnostics.md +7 -4
  11. package/docs/effects.md +281 -0
  12. package/docs/gateway-login.md +113 -0
  13. package/docs/orientation-contract-reference.md +4 -1
  14. package/docs/progress-snapshots.md +3 -3
  15. package/docs/qa-and-test-orders.md +3 -3
  16. package/docs/readback.md +523 -0
  17. package/docs/runtime-readiness.md +1 -1
  18. package/docs/sdk-storage-compatibility.md +1 -1
  19. package/docs/skills-revision.md +364 -0
  20. package/docs/supported-surface.md +11 -3
  21. package/docs/versioning.md +8 -4
  22. package/package.json +8 -3
  23. package/schemas/campaign-runtime-build-packet.v0.schema.json +5 -0
  24. package/schemas/campaigns-os-effects.v1.schema.json +211 -0
  25. package/schemas/campaigns-os-readback.v2.schema.json +267 -0
  26. package/skills/campaign-lifecycle-orientation/SKILL.md +174 -0
  27. package/skills/campaign-readback-classification/SKILL.md +230 -0
  28. package/skills/campaign-run-evidence/SKILL.md +140 -0
  29. package/skills/contribution-intake/SKILL.md +85 -0
  30. package/skills/next-campaigns-build/SKILL.md +33 -12
  31. package/skills/next-campaigns-os/SKILL.md +45 -21
  32. package/skills/next-campaigns-os-setup/SKILL.md +35 -14
  33. package/skills/next-campaigns-polish/SKILL.md +43 -17
  34. package/skills/next-campaigns-qa/SKILL.md +48 -24
  35. package/skills.json +39 -6
  36. package/src/admin-transport.mjs +123 -0
  37. package/src/cli.mjs +991 -200
  38. package/src/credential-store.mjs +183 -0
  39. package/src/deviation.mjs +3 -2
  40. package/src/diagnostic.mjs +4 -1
  41. package/src/gate-actions.mjs +2 -2
  42. package/src/install-mode.mjs +17 -9
  43. package/src/lifecycle.mjs +95 -0
  44. package/src/login.mjs +152 -0
  45. package/src/package-install-fixture.mjs +3 -2
  46. package/src/qa-node.mjs +56 -19
  47. package/src/qa-publish.mjs +108 -2
  48. package/src/readback.mjs +1936 -0
  49. package/src/remit.mjs +17 -3
@@ -1,6 +1,6 @@
1
1
  {
2
- "_note": "The downstream contract manifest. Everything listed here is SUPPORTED SURFACE: consumers (campaigns-agent, campaign-builder, the private ops repo, page-kit campaign repos) may depend on it, and changing it is a deliberate act \u2014 hashed entries require a surface_version bump in the same change (check-supported-surface.mjs --base, mirroring the skills.json bump gate), named entries must keep existing at their path, cli_commands must keep resolving in the CLI dispatch, package_exports must stay exported, and every entry must ship in the npm pack (files[] coverage). Anything NOT listed here \u2014 src/** internals, scripts/** checkers, examples/**, prompts/**, contracts/** other than this file and the entries named[] below (the orientation contract, the release ledger, and the consumer-facing orientation fixtures) \u2014 is implementation: consumers may read it for context but must not build on it, and it can change without notice. Rationale and the compatibility promise: docs/supported-surface.md.",
3
- "surface_version": "1.37.3",
2
+ "_note": "The downstream contract manifest. Everything listed here is SUPPORTED SURFACE: consumers (campaigns-agent, campaign-builder, the private ops repo, page-kit campaign repos) may depend on it, and changing it is a deliberate act — hashed entries require a surface_version bump in the same change (check-supported-surface.mjs --base, mirroring the skills.json bump gate), named entries must keep existing at their path, cli_commands must keep resolving in the CLI dispatch, package_exports must stay exported, and every entry must ship in the npm pack (files[] coverage). Anything NOT listed here — src/** internals, scripts/** checkers, examples/**, prompts/**, contracts/** other than this file and the entries named[] below (the orientation contract, the release ledger, and the consumer-facing orientation fixtures) — is implementation: consumers may read it for context but must not build on it, and it can change without notice. Rationale and the compatibility promise: docs/supported-surface.md.",
3
+ "surface_version": "1.41.2",
4
4
  "package_exports": [
5
5
  "./commercial-journey",
6
6
  "./commercial-parity",
@@ -40,7 +40,10 @@
40
40
  "findings",
41
41
  "run-record",
42
42
  "run",
43
- "demo"
43
+ "demo",
44
+ "login",
45
+ "logout",
46
+ "readback"
44
47
  ],
45
48
  "hashed": {
46
49
  "contracts/migration-sidecar-bundle.v0.json": {
@@ -62,7 +65,7 @@
62
65
  "sha256": "f310c1b7844d9b1994864d15858ff0c6f17efc1e5285ba193d181bdb2dd4be2f"
63
66
  },
64
67
  "schemas/campaign-runtime-build-packet.v0.schema.json": {
65
- "sha256": "91853bbd1b14c080e2fe2ec32f1aa643f53e62df280b543093bf158ae924dd4b"
68
+ "sha256": "886fb59b58787a3fd7a9c43ce4c676aeebbd52fd789269cfac700fcae42106d1"
66
69
  },
67
70
  "schemas/campaign-spec.v4.schema.json": {
68
71
  "sha256": "12b57573739357acb2b007d00a737cd86face441c47f2719fc51b041dc8ea4a5"
@@ -111,6 +114,15 @@
111
114
  },
112
115
  "demo/apollo-v0/provenance.json": {
113
116
  "sha256": "2e440be6513fe06b351c3a72c35424a04f807792621cb01e89803441fd925e06"
117
+ },
118
+ "schemas/campaigns-os-readback.v2.schema.json": {
119
+ "sha256": "dfc9abed38d456969e47a21f606d308a03bdf036f3466f7d6e47a602747dacf4"
120
+ },
121
+ "contracts/effects.v1.json": {
122
+ "sha256": "37c54f75731541f763aedac7b067506b3d0aa98e1083f285de87b9b32b027664"
123
+ },
124
+ "schemas/campaigns-os-effects.v1.schema.json": {
125
+ "sha256": "3eadd22169ab98bc7c2f682af2751267605170581182158d96be035b0cfe44dc"
114
126
  }
115
127
  },
116
128
  "named": [
@@ -123,6 +135,7 @@
123
135
  "docs/supported-surface.md",
124
136
  "docs/campaigns-os-build-flow.md",
125
137
  "docs/build-packet.md",
138
+ "docs/gateway-login.md",
126
139
  "docs/migration-sidecar-bundle.md",
127
140
  "docs/design-source-package.md",
128
141
  "docs/campaign-build-brief.md",
@@ -192,6 +205,13 @@
192
205
  "docs/progress-snapshots.md",
193
206
  "contracts/fixtures/progress/observation.v0.json",
194
207
  "docs/demo-preview.md",
195
- "demo/apollo-v0/NOTICE.txt"
208
+ "demo/apollo-v0/NOTICE.txt",
209
+ "docs/readback.md",
210
+ "docs/effects.md",
211
+ "agents/claude/CLAUDE.md",
212
+ "agents/codex/AGENTS.md",
213
+ "agents/copilot/copilot-instructions.md",
214
+ "agents/cursor/campaigns-os.mdc",
215
+ "docs/skills-revision.md"
196
216
  ]
197
217
  }
@@ -444,17 +444,25 @@ network, which the default run never touches, so it is opt-in:
444
444
  campaigns-os spec derive --packet campaign-runtime.build.json --from-store <subdomain> [--store-token-source env:<VAR>] [--dry-run] [--json]
445
445
  ```
446
446
 
447
- `<subdomain>` is the store's `<store>.29next.store` subdomain (the Admin API
448
- lives at `https://<subdomain>.29next.store/api/admin/`). The read token is
449
- taken from the environment, never from the command line: from
450
- `<SUBDOMAIN>_ADMIN_TOKEN` (upper-cased, dashes as underscores) by default,
451
- or from the variable `--store-token-source env:<VAR>` names. An Admin API
452
- access token with the `store:read` and `content:read` scopes (Settings >
453
- API Access) is enough; the token is sent as a bearer and appears nowhere in
454
- the output, which names the variable instead (a value that is not one line
455
- of printable ASCII is refused unsent, `spec.derive.store_credential_invalid`,
456
- and a transport error that quotes a header is redacted). The store is only
457
- read.
447
+ `<subdomain>` is the store's `<store>.29next.store` subdomain. In the
448
+ 1.38.0 candidate, the default read uses gateway credentials saved by
449
+ `campaigns-os login --store <subdomain>`, through
450
+ `https://mcp.nextcommerce.com/admin/`. This is an admitted owned-store
451
+ private pilot, not general merchant availability. Missing, expired or uncertain
452
+ credentials require login; gateway failure never falls back to an environment
453
+ token. See [gateway login and migration](gateway-login.md).
454
+
455
+ **Migration for existing direct callers:** add
456
+ `--store-token-source env:<VAR>` explicitly, naming your existing environment
457
+ variable (for example `EXAMPLE_ADMIN_TOKEN`). The former implicit
458
+ `<SUBDOMAIN>_ADMIN_TOKEN` lookup is removed. This break-glass path warns that it
459
+ bypasses gateway custody and contacts
460
+ `https://<subdomain>.29next.store/api/admin/` directly. A `store:read` and
461
+ `content:read` Admin token is sufficient. Tokens are never CLI arguments or
462
+ output; invalid bearer values are refused unsent. Both paths only read the store.
463
+ Gateway page pagination is consolidated by custody into a bounded list; the CLI
464
+ does not follow an upstream cursor on this path. The explicit direct path keeps
465
+ its existing bounded cursor traversal.
458
466
 
459
467
  | Spec field | Store authority |
460
468
  |---|---|
@@ -489,14 +497,17 @@ return). A slug that is not one honest path segment (a separator, `.` or
489
497
  spec was stale and the diff is the correction, or `--from-store` names
490
498
  another merchant's store and the spec should be restored.
491
499
 
492
- The result carries a `store` block (`subdomain`, `admin_api`,
493
- `token_source`, `store_read`, `pages_read`, `primary_domain`), and the text
500
+ The result carries a `store` block (gateway reads add `transport: "gateway"`
501
+ and the actual gateway `endpoint`; `admin_api` remains the logical upstream
502
+ source), with `subdomain`, `admin_api`,
503
+ `token_source`, `store_read`, `pages_read`, `primary_domain`, and the text
494
504
  output a `Store:` line. After a write that moved a store field, `next` is
495
505
  `page-kit sync` first (doctor's `page_kit.store_profile` gate now sees the
496
506
  spec ahead of the repo and names sync as its repair), then doctor. A store
497
507
  that cannot be read is a refusal with nothing written, repo fields included,
498
- exit 2: `spec.derive.store_credential_missing` (the variable is unset or
499
- empty), `store_unauthorized` (401/403), `store_not_found` (404: no store at
508
+ exit 2: `spec.derive.store_credential_missing` (no gateway login, or the explicitly
509
+ selected variable is unset or empty), `store_credential_unavailable` (local
510
+ storage is busy or unavailable), `store_unauthorized` (401/403), `store_not_found` (404: no store at
500
511
  that subdomain), `store_unreachable` (transport, timeout, 5xx) or
501
512
  `store_response_invalid`. Local preconditions (packet, spec, target entry, spec boundary, page tree)
502
513
  are checked before the store is contacted, and a packet that names another
@@ -1290,7 +1301,7 @@ doctor read for the locked template family (`required`, `family`, `version`,
1290
1301
  running). This is what `prepare-build` records by default. The catalog
1291
1302
  travels with the toolkit, not with the campaign, so the packet does not
1292
1303
  record where one machine's checkout or package install kept it, and the
1293
- same packet resolves on any machine and under `npx campaigns-os`.
1304
+ same packet resolves on any machine and under `npx --no-install campaigns-os`.
1294
1305
  - A string `path` is an operator-supplied `--commerce-catalog <path>`,
1295
1306
  recorded relative to the packet (keep it inside the campaign repo). Doctor
1296
1307
  resolves it against the packet's directory and blocks on
@@ -6,7 +6,7 @@ release at least 1.37.0 (or a reviewed full-SHA source pin).
6
6
  From the folder containing your exact project-local toolkit installation:
7
7
 
8
8
  ```bash
9
- npx campaigns-os demo --target ./apollo-sample
9
+ npx --no-install campaigns-os demo --target ./apollo-sample
10
10
  ```
11
11
 
12
12
  Open the printed `landing/index.html` file directly. No server or browser
@@ -6,12 +6,15 @@ npm, install a reviewed full-SHA source pin as described in the quickstart.
6
6
  From the campaign folder:
7
7
 
8
8
  ```bash
9
- npx campaigns-os tooling diagnose --platform codex --packet campaign-runtime.build.json
10
- npx campaigns-os tooling diagnose --platform codex --packet campaign-runtime.build.json --json > diagnostic.json
9
+ npx --no-install campaigns-os tooling diagnose --platform codex --packet campaign-runtime.build.json
10
+ npx --no-install campaigns-os tooling diagnose --platform codex --packet campaign-runtime.build.json --json > diagnostic.json
11
11
  ```
12
12
 
13
- Omit `--packet` for installation and skill diagnostics only. Use `--platform
14
- claude` for a Claude-only profile. `--context` and `--report` may select existing
13
+ Omit `--packet` for installation and skill diagnostics only. Without
14
+ `--platform`, only the platforms where Campaigns OS skills are installed are
15
+ checked and the export reports `platform: installed` (or `all` when none are
16
+ installed, so every platform was checked); use `--platform claude` to
17
+ check one profile, or `--platform all` for every one. `--context` and `--report` may select existing
15
18
  local sidecars; `--target` selects a local skills directory. These inputs are
16
19
  never included in the export. The text and JSON forms are suitable for review
17
20
  and copying to a support request. The command itself sends nothing.
@@ -0,0 +1,281 @@
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
+ - The contract: [`contracts/effects.v1.json`](../contracts/effects.v1.json)
13
+ - Its shape: [`schemas/campaigns-os-effects.v1.schema.json`](../schemas/campaigns-os-effects.v1.schema.json)
14
+ - The proof: `src/effects.test.mjs`
15
+ - The gate: `scripts/check-effects.mjs` (`npm run check:effects`)
16
+
17
+ ## The vocabulary
18
+
19
+ ### Annotations
20
+
21
+ Four booleans per row, spelled the way an MCP tool face spells them, so a host
22
+ that already understands those hints needs no translation layer.
23
+
24
+ | Annotation | Meaning |
25
+ | --- | --- |
26
+ | `readOnlyHint` | The invocation changes **nothing**: no file under the target, the working directory or your machine, and no request off the machine. |
27
+ | `destructiveHint` | The invocation can overwrite, clear or discard state that existed before it ran. Only meaningful when `readOnlyHint` is false. |
28
+ | `openWorldHint` | The invocation can contact an endpoint off this machine. True exactly when the row declares at least one send. |
29
+ | `idempotentHint` | Repeating the invocation with the same arguments adds no effect beyond the first run. |
30
+
31
+ **`readOnlyHint` counts the command-lifecycle journal.** A journal append is a
32
+ write like any other, so every `readOnlyHint: true` row is an invocation the
33
+ CLI exempts from lifecycle capture (the converse does not hold: `demo` and the
34
+ `--no-write` forms skip the journal but still write other declared files): `help`, `readback`,
35
+ `run status`, `doctor` inspection, `doctor --no-write`, `sdk storage-check`,
36
+ `tooling diagnose`, a refused invocation, `run-record --no-write`, and every
37
+ `--dry-run` form on the commands that implement the flag. Everything else
38
+ appends an entry when a journal is selected — an active run session,
39
+ `--lifecycle-journal`, or `CAMPAIGNS_OS_LIFECYCLE_LOG` — and is therefore not
40
+ read-only, even when the command writes no artifact of its own. `standardize`
41
+ and `bundle check` are tier `B` for exactly that reason and nothing else; their
42
+ rows say so.
43
+
44
+ ### Tiers
45
+
46
+ | Tier | Meaning |
47
+ | --- | --- |
48
+ | `none` | No effect: nothing written anywhere, nothing sent. |
49
+ | `B` | Writes files under the target, the working directory or your machine. Nothing leaves the machine. |
50
+ | `A` | Can contact an endpoint off this machine (it may write locally too). |
51
+ | `C` | Destructive: overwrites, clears or discards state that was already there (it may send too). |
52
+
53
+ A row carries the **highest** tier it can reach, ranked `none < B < A < C`.
54
+
55
+ ### Location tokens
56
+
57
+ A write path is a glob (`*` within one segment, `**` across segments) that opens
58
+ with one of these:
59
+
60
+ | Token | Resolves to |
61
+ | --- | --- |
62
+ | `{target}` | The target the invocation names: the directory given to `--target`, or the Page Kit target repository the Build Packet points at. |
63
+ | `{cwd}` | The working directory the invocation runs in. |
64
+ | `{spec}` | The CampaignSpec file the Build Packet names (`spec.local_path`), which need not live inside the target repository. |
65
+ | `{home}` | Your machine: the home directory and the config root under it (`XDG_CONFIG_HOME` when set). |
66
+ | `{packet}` | The Build Packet the invocation names (`--packet`), wherever it lives — it need not be the copy inside the target repository. |
67
+ | `{lifecycle-journal}` | The command-lifecycle journal wherever it was selected for this invocation. |
68
+ | `{proxy-base}` | The endpoint `--proxy-base` names, or the canonical NEXT endpoint when it does not. |
69
+ | `{base-url}` | The campaign under test, as `--base-url` names it or as the packet derives it. |
70
+ | `{playwright-download-host}` | Where Playwright fetches browser builds from: `PLAYWRIGHT_DOWNLOAD_HOST` when set, else the Playwright CDN. The one destination in the file that is not a Campaigns OS endpoint — `qa install-browser` is the one supported invocation that downloads from a third party. |
71
+
72
+ The tokens matter because effects are not all under the target. `install-skills`
73
+ writes your **home** directory, not the campaign. `telemetry on` writes your
74
+ **machine** config. `run-record` writes beside the **working directory**, not the
75
+ target repo. A row that said "writes the target" would be wrong about all three.
76
+
77
+ ## How to read a row
78
+
79
+ ```jsonc
80
+ {
81
+ "command": "page-kit",
82
+ "subcommand": "sync",
83
+ "flags": [], // the base form; --dry-run is its own row
84
+ "annotations": { "readOnlyHint": false, "destructiveHint": false,
85
+ "openWorldHint": false, "idempotentHint": true },
86
+ "tier": "B",
87
+ "writes": [
88
+ { "path": "{target}/_data/campaigns.json",
89
+ "when": "one of the ten Store Profile / SDK-pin fields is usable and differs from the entry",
90
+ "observed_in": ["no_session", "ambient_session", "stale_session", "lifecycle_log"] }
91
+ // …
92
+ ],
93
+ "sends": [],
94
+ "effect_test": "effects: page-kit sync",
95
+ "test_scope": "full",
96
+ "notes": "Writes only those ten fields of the packet's route entry…"
97
+ }
98
+ ```
99
+
100
+ There is **one row per command and per effect-changing flag combination**. The
101
+ flags that change what the invocation does to the world are listed once, in
102
+ `vocabulary.effect_changing_flags`: `--browser`, `--built`, `--dry-run`,
103
+ `--emit-packet`, `--example`, `--force`, `--from-store`, `--list`,
104
+ `--no-post-verdict`, `--no-probe`, `--no-remit`, `--no-run-session`,
105
+ `--no-write`, `--republish`, `--test-order`, `--write`, `--write-map`. Flags
106
+ that only change the output shape (`--json`, `--report`) deliberately do not.
107
+
108
+ **"The help text" is every help block the CLI prints**, not one file's.
109
+ `campaigns-os qa` prints its own from `src/qa-node.mjs`, and while the coverage
110
+ scan read only `src/cli.mjs` the three subcommands documented there alone — `qa
111
+ parity`, `qa waive` and `qa install-browser` — owed no row, had none, and the
112
+ gate stayed green. Every module that owns a usage block is listed in
113
+ `HELP_SOURCE_PATHS` and scanned the same way; a test derives that list from the
114
+ source, so a command that grows its own help cannot quietly leave the scan.
115
+
116
+ **Every one of those flags that a help usage line carries owes a row**, and
117
+ `scripts/check-effects.mjs` fails when one does not have it. Coverage by command
118
+ alone was not enough: deleting the `page-kit sync --dry-run` row, or the
119
+ `doctor --write` row, left the gate green while the file lost an effect —
120
+ `doctor --write` writes the doctor sidecar, the assembly report and the packet
121
+ that plain `doctor` does not. A flag that appears in a usage line for a command
122
+ that has only a base row is now the loudest kind of failure this gate has.
123
+
124
+ One row is not a command at all: `{"command": "*refused*"}` is any invocation
125
+ refused before its handler runs — an unknown command, an unknown subcommand, or
126
+ a flag the command rejects up front. It writes nothing, journals nothing, and is
127
+ the row to read when you want to know what a typo costs. The one exception is
128
+ declared on the rows it belongs to: `start`, `prepare-build`, `build`,
129
+ `run start` and `run end` close out a **stale** run session at the root they are
130
+ about to act on *before* argv is refused.
131
+
132
+ ## How a row is proved
133
+
134
+ `src/effects.test.mjs` runs the real CLI in a disposable target seeded from
135
+ `examples/`, under **five conditions**, and snapshots the whole tree (paths plus
136
+ sha256) before and after while a loopback `node:http` receiver counts requests.
137
+
138
+ | Condition | What it sets up |
139
+ | --- | --- |
140
+ | `no_session` | No run session at the target or the working directory. |
141
+ | `ambient_session` | An active ambient run session opened by `run start` at the target. |
142
+ | `stale_session` | A run session idle past the 12 h TTL, at the target and at the working directory. |
143
+ | `lifecycle_log` | `CAMPAIGNS_OS_LIFECYCLE_LOG` names a journal outside the runtime directory. |
144
+ | `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. |
145
+
146
+ Five conditions rather than one, because the CLI's effects are not a function of
147
+ argv alone: an ambient session redirects the journal and is itself touched by
148
+ session resolution, and a stale session is closed out — Run Record assembled —
149
+ before some commands even read argv.
150
+
151
+ ### Why the fifth condition exists
152
+
153
+ Under the first four, consent is `CAMPAIGNS_OS_TELEMETRY=off` unless the row
154
+ declares a consent-gated send it expects to see in that condition; then the row
155
+ runs with consent on and the loopback receiver as its endpoint, so "nothing was
156
+ sent" is not an artefact of consent being off **for a send that is declared**.
157
+
158
+ That took the row's word for which sends exist, and it hid real ones: `next` and
159
+ its five stage forms, and all three `qa run` rows, declared `sends: []` while
160
+ each of them POSTed — to `{proxy-base}/api/progress`, and for `qa run` to
161
+ `{proxy-base}/api/qa/verdicts` as well, on blocked attempts included.
162
+
163
+ `persisted_consent` does not read consent from the row. It persists consent the
164
+ way an operator does — `campaigns-os telemetry on --proxy-base <loopback>`,
165
+ which is a **scoped** record — and runs every row that way. An environment
166
+ override is not equivalent and is the reason the earlier probe found nothing: an
167
+ env grant carries no scope, so the remit refuses it for a non-canonical endpoint
168
+ (`scope_bypassed`) and delivers nothing. Any request the receiver sees that no
169
+ declared send covers fails the row.
170
+
171
+ The same scoping is what keeps the suite off the network. The remit endpoint is
172
+ a hard-coded constant with no environment override, so a command that falls back
173
+ to the canonical endpoint resolves consent **off** (the persisted grant covers
174
+ the loopback scope only) and sends nothing. That is asserted, not assumed: every
175
+ invocation in this condition runs under `NODE_DEBUG=net` and its connection log
176
+ must name no host but `127.0.0.1`, and one case states the claim directly for
177
+ `next` with no `--proxy-base` at all.
178
+
179
+ The assertion runs both ways, and that is what makes the file falsifiable:
180
+
181
+ 1. **Nothing undeclared may change**, in any condition. A `readOnlyHint: true`
182
+ row declares no writes, so any byte that moves fails it.
183
+ 2. **Every declared effect whose `observed_in` names a condition must be seen**
184
+ in it, so a row cannot be padded with effects that never happen.
185
+
186
+ `observed_in` is per effect, not per row: `install-agent-context` writes
187
+ `{target}/.gitignore` only when the target does not already ignore the runtime
188
+ directory, so that entry is observed in three conditions and not under
189
+ `ambient_session`, where `run start` has already added the line.
190
+
191
+ ### Rows proved at the preflight
192
+
193
+ Some invocations cannot execute past their preflight with no network, no
194
+ browser, no renderer and no credentials. Those rows carry
195
+ `test_scope: "preflight"`. Their case proves the preflight refusal writes
196
+ nothing beyond what the row declares and — where a loopback receiver can stand
197
+ in for the destination — that the **declared destination is the one contacted**.
198
+
199
+ | Row | What the offline fixture cannot reach |
200
+ | --- | --- |
201
+ | `login` | A reachable login gateway and a human at a browser. Proved: the failure path writes nothing at all — no credential, no journal entry. |
202
+ | `logout` | A credential minted by a gateway login. Proved: the no-credential path writes nothing. |
203
+ | `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. |
204
+ | `polish capture` | An installed browser and a reachable `--base-url`. Proved: the refusal writes nothing but the journal entry and contacts nothing. |
205
+ | `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. |
206
+ | `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. |
207
+ | `qa resolve` | A resolution that is not blocked before the probe. Proved: the blocked resolution contacts the stand-in zero times. |
208
+ | `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. |
209
+ | `spec derive --from-store` | A live gateway and a real store credential. Proved: the credential refusal writes nothing under the target or the spec. |
210
+ | `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. |
211
+ | `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. |
212
+
213
+ #### What a preflight row is allowed to touch
214
+
215
+ A preflight row declares its allowances **separately from its effects**, and the
216
+ test enforces them independently:
217
+
218
+ ```jsonc
219
+ "preflight": {
220
+ "may_write": ["{lifecycle-journal}"], // the ONLY paths the refusal may write
221
+ "may_contact": ["/api/runs"] // the exact request paths the receiver may see
222
+ }
223
+ ```
224
+
225
+ Both halves close a hole that a declared effect used to open. `logout` declared
226
+ `{home}/**` for the credential a *completed* login writes — and that declaration
227
+ also licensed its refusal to write anywhere under the home directory, so a
228
+ home-directory write injected into the preflight passed. And a destination a
229
+ loopback receiver only stands in for (`{base-url}`, the login gateway) matched
230
+ **any** request path, so an injected endpoint passed too. Now:
231
+
232
+ - `may_write` is the whole permission. It may not name a whole location
233
+ (`{target}`, `{target}/**`) and may not span segments under `{home}` — the
234
+ skills directories, the credential store and the consent file all live there,
235
+ under the temporary `HOME` the test sets, and a preflight that writes one of
236
+ them has to say which.
237
+ - `may_contact` is matched literally against the request path, so an `/api/`
238
+ call nobody declared fails the row even when the row declares a stand-in
239
+ destination. (On a `full` row the same rule holds one step down: a stand-in
240
+ destination never covers an `/api/` path, because every API endpoint in this
241
+ contract is declared as `{proxy-base}/api/…`.)
242
+ - A write the row declares as observed must also be in `may_write`; the gate
243
+ refuses the contradiction rather than letting the test find it.
244
+
245
+ A `full` row may still carry an individual effect the offline fixture cannot
246
+ reach — the Map Builder spec fetch behind `--map-id`, the `codex` and `agents`
247
+ destinations of `install-skills`. Each such entry has an empty `observed_in`
248
+ **and** a `not_observed_reason`, and `check-effects.mjs` refuses one without the
249
+ reason. What it may not be is silent.
250
+
251
+ ## The rule
252
+
253
+ **A row without its test is not published.** `scripts/check-effects.mjs` (in
254
+ `npm run check` and `npm run check:contracts`) fails when:
255
+
256
+ - a command on the supported CLI surface, a subcommand any help block teaches
257
+ (`src/cli.mjs` and `src/qa-node.mjs`), or an effect-changing flag a help usage
258
+ line carries, has no row;
259
+ - a row names no `effect_test`, names one `src/effects.test.mjs` does not
260
+ declare, or names one the per-row generator would not produce (the cases are
261
+ generated from this file, so an unchecked name made the link vacuous);
262
+ - a row has no entry in the test's `INVOCATIONS` table, or the table has an
263
+ entry no row claims — a generated case with no argv proves nothing;
264
+ - an effect declares no `observed_in` and no `not_observed_reason`;
265
+ - a `test_scope: "preflight"` row does not say in its own notes what it cannot
266
+ reach, declares no `preflight` allowances, licenses a whole location or a
267
+ home-directory subtree, names a `may_contact` entry that is not a request
268
+ path, or has an observed write its `may_write` does not allow; a
269
+ `test_scope: "full"` row carries allowances, or has no effect observed
270
+ anywhere;
271
+ - the annotations disagree with the row (`readOnlyHint` with declared effects,
272
+ `openWorldHint` without a send, `destructiveHint` off tier `C`);
273
+ - two rows claim the same invocation, or a write path opens with no known
274
+ location token;
275
+ - `vocabulary.conditions` names a condition `src/effects.test.mjs` does not run.
276
+
277
+ ## When you change a command
278
+
279
+ Change the effect, change the row, in the same PR. The effect test will tell you
280
+ which row is wrong before review does: it names the path that moved and the row
281
+ that failed to declare it.
@@ -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.
@@ -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.41.2`
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
@@ -1692,9 +1692,9 @@ a trusted submission attests the runner, not execution or resource identity.
1692
1692
 
1693
1693
  ### Playwright updates and consumer installs
1694
1694
 
1695
- After installing or updating Campaigns OS, run `npx campaigns-os qa install-browser`
1696
- from the campaign project (or `campaigns-os qa install-browser` for a global
1697
- installation). This resolves the same Playwright package as QA and polish capture.
1695
+ After installing or updating Campaigns OS, run
1696
+ `npx --no-install campaigns-os qa install-browser` from the campaign project
1697
+ (or `campaigns-os qa install-browser` for a global installation). This resolves the same Playwright package as QA and polish capture.
1698
1698
  A project's own `npx playwright install` can resolve a different version and install
1699
1699
  a different Chromium build. Campaigns OS is an optional-dependency owner, not a
1700
1700
  Playwright peer dependency: npm may share a compatible copy or install a nested one.