@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.
- package/AGENTS.md +114 -10
- package/CHANGELOG.md +708 -0
- package/README.md +44 -31
- package/agents/claude/CLAUDE.md +5 -1
- package/campaign-spec/dist/types.d.ts +2 -0
- package/contracts/agent-relevant-change-policy.v1.json +11 -1
- package/contracts/effects.v1.json +4887 -0
- package/contracts/migration-sidecar-bundle.v0.json +9 -0
- package/contracts/release-ledger.json +1541 -0
- package/contracts/supported-surface.json +33 -12
- package/docs/build-packet.md +83 -22
- package/docs/campaigns-os-build-flow.md +2 -2
- package/docs/demo-preview.md +1 -1
- package/docs/diagnostics.md +7 -4
- package/docs/effects.md +350 -0
- package/docs/gateway-login.md +113 -0
- package/docs/local-setup.md +51 -0
- package/docs/migration-sidecar-bundle.md +6 -1
- package/docs/orientation-contract-reference.md +4 -1
- package/docs/progress-snapshots.md +9 -3
- package/docs/qa-and-test-orders.md +29 -13
- package/docs/readback.md +523 -0
- package/docs/runtime-readiness.md +1 -1
- package/docs/sdk-storage-compatibility.md +1 -1
- package/docs/skills-revision.md +364 -0
- package/docs/supported-surface.md +11 -3
- package/docs/versioning.md +8 -4
- package/package.json +10 -4
- package/schemas/campaign-runtime-assembly-report.v0.schema.json +6 -1
- package/schemas/campaign-runtime-build-packet.v0.schema.json +11 -1
- package/schemas/campaign-spec.v4.schema.json +4 -0
- package/schemas/campaigns-os-effects.v1.schema.json +211 -0
- package/schemas/campaigns-os-progress-snapshot.v0.schema.json +1 -0
- package/schemas/campaigns-os-qa-verdict-sidecar.v0.schema.json +1 -0
- package/schemas/campaigns-os-qa-verdict.v0.schema.json +1 -0
- package/schemas/campaigns-os-readback.v2.schema.json +267 -0
- package/schemas/campaigns-os-run-record.v0.schema.json +1 -0
- package/skills/campaign-lifecycle-orientation/SKILL.md +179 -0
- package/skills/campaign-readback-classification/SKILL.md +230 -0
- package/skills/campaign-run-evidence/SKILL.md +142 -0
- package/skills/contribution-intake/SKILL.md +85 -0
- package/skills/next-campaigns-build/SKILL.md +33 -12
- package/skills/next-campaigns-os/SKILL.md +59 -22
- package/skills/next-campaigns-os/references/session-intake.md +4 -4
- package/skills/next-campaigns-os-setup/SKILL.md +35 -14
- package/skills/next-campaigns-polish/SKILL.md +43 -17
- package/skills/next-campaigns-qa/SKILL.md +53 -28
- package/skills.json +40 -7
- package/src/admin-transport.mjs +123 -0
- package/src/cli.mjs +1178 -270
- package/src/credential-store.mjs +183 -0
- package/src/deviation.mjs +3 -2
- package/src/diagnostic.mjs +4 -1
- package/src/finding-cause.mjs +14 -10
- package/src/gate-actions.mjs +2 -2
- package/src/install-mode.mjs +17 -9
- package/src/lifecycle.mjs +96 -0
- package/src/login.mjs +152 -0
- package/src/package-install-fixture.mjs +3 -2
- package/src/polish-node.mjs +5 -2
- package/src/progress-node.mjs +3 -2
- package/src/progress.mjs +5 -3
- package/src/qa-node.mjs +105 -36
- package/src/qa-publish.mjs +112 -2
- package/src/qa-sidecar.mjs +2 -0
- package/src/qa-verdict-discovery.mjs +11 -0
- package/src/qa-verdict-publish.mjs +1 -0
- package/src/qa-verdict.mjs +8 -1
- package/src/readback.mjs +1937 -0
- package/src/remit.mjs +17 -3
- package/src/run-record-closeout.mjs +3 -4
- package/src/run-record.mjs +4 -0
- package/src/sidecar-bundle.mjs +21 -0
- package/src/spec-source-identity.mjs +44 -0
- package/src/stage-ledger.mjs +4 -1
- package/src/tooling-setup.mjs +160 -0
package/docs/effects.md
ADDED
|
@@ -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.
|
|
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
|
-
|
|
4
|
-
export and `schemas/campaigns-os-progress-snapshot.v0.schema.json
|
|
5
|
-
|
|
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.
|