@nextcommerce/campaigns-os 1.54.0 → 1.55.0

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 (53) hide show
  1. package/CHANGELOG.md +84 -0
  2. package/CONTEXT.md +8 -5
  3. package/README.md +26 -12
  4. package/compatibility.json +1 -1
  5. package/contracts/release-ledger.json +441 -0
  6. package/contracts/supported-surface.json +2 -2
  7. package/docs/brand-theme-bridge.md +10 -0
  8. package/docs/build-packet.md +35 -19
  9. package/docs/campaigns-os-build-flow.md +1 -1
  10. package/docs/effects.md +2 -5
  11. package/docs/local-setup.md +1 -1
  12. package/docs/orientation-contract-reference.md +1 -1
  13. package/docs/qa-and-test-orders.md +55 -11
  14. package/docs/runtime-readiness.md +1 -1
  15. package/docs/skills-revision.md +10 -10
  16. package/docs/supported-surface.md +1 -1
  17. package/docs/workflow-findings-sidecar.md +57 -2
  18. package/package.json +3 -1
  19. package/schemas/campaigns-os-run-record.v0.schema.json +20 -15
  20. package/skills/campaign-lifecycle-orientation/SKILL.md +3 -3
  21. package/skills/campaign-readback-classification/SKILL.md +3 -3
  22. package/skills/campaign-run-evidence/SKILL.md +3 -3
  23. package/skills/contribution-intake/SKILL.md +3 -3
  24. package/skills/next-campaigns-build/SKILL.md +3 -3
  25. package/skills/next-campaigns-os/SKILL.md +3 -3
  26. package/skills/next-campaigns-os-setup/SKILL.md +3 -3
  27. package/skills/next-campaigns-polish/SKILL.md +3 -3
  28. package/skills/next-campaigns-qa/SKILL.md +4 -4
  29. package/skills.json +10 -10
  30. package/src/adapter-decision-contract.mjs +26 -12
  31. package/src/brand-theme.mjs +44 -8
  32. package/src/campaign-identity.mjs +33 -4
  33. package/src/cli.mjs +36 -59
  34. package/src/consent.mjs +3 -2
  35. package/src/design-source-publication.mjs +1 -1
  36. package/src/deviation.mjs +2 -1
  37. package/src/doctor/checks.mjs +16 -6
  38. package/src/doctor/next-step.mjs +16 -6
  39. package/src/invocation.mjs +4 -3
  40. package/src/lifecycle.mjs +88 -6
  41. package/src/local-preview-policy.mjs +6 -0
  42. package/src/login.mjs +2 -1
  43. package/src/polish-browser.mjs +8 -2
  44. package/src/polish-node.mjs +6 -1
  45. package/src/polish-readability.mjs +2 -2
  46. package/src/qa-browser.mjs +521 -92
  47. package/src/qa-cart-entry.mjs +29 -6
  48. package/src/qa-node.mjs +144 -39
  49. package/src/qa-verdict-publish.mjs +2 -1
  50. package/src/run-record.mjs +46 -34
  51. package/src/sdk-markup.mjs +41 -1
  52. package/src/stage-ledger.mjs +14 -2
  53. package/src/stage-record.mjs +50 -13
@@ -341,6 +341,24 @@ npm run campaigns-os -- qa run \
341
341
 
342
342
  The runner fetches deployed pages, checks route availability, verifies CampaignSpec `sdk_hints.meta_tags` (a key the Campaign Cart SDK does not read, `next-currency` or `next-predictive-address` from `src/sdk-meta-tags.mjs`, is a `warn` row at severity `warn` carrying the shared note, never `manual_review` and never a blocker, whether or not the tag rendered; doctor reports the same key as `sdk_hints.meta_tags.ignored_by_sdk`), writes a local verdict JSON under `<target-repo>/qa-output/<qa-storage-key>/<run-id>.json` (the packet's `assembly.target_repo`, else the packet's directory; `--output-dir` overrides it, and a packet-less run uses `qa-output/` under the current directory), and returns exit code `4` when the verdict is blocked. The storage key is the Map ID for saved-Map QA and `local-spec-<local_spec_id>` for local-spec packet QA. The target's managed ignore block lists `qa-output/`, because full verdicts carry live storefront URLs; the committed form is the `.campaign-runtime/qa-verdict.json` projection.
343
343
 
344
+ `--currency <code>` proves one currency per run: the runner adds
345
+ `?currency=<CODE>` to each funnel's entry URL (the HTTP fetch, the browser load
346
+ and `entry_urls`) and to the first page load of each test-order path (the
347
+ checkout probe, or the entry page or checkout when that load comes first),
348
+ keeping any query or tracking params already there. Campaign Cart keeps the URL
349
+ currency for the rest of that tab's session, so the runner adds it to no other
350
+ load (upsell pages and the receipt reload in recovery go without it); a later
351
+ load of the entry URL still carries it, because that URL is tagged. The flag takes one code per run (the
352
+ shared parser keeps the last one if it is given twice). The code must be three
353
+ letters (upper-cased: `gbp` runs as `GBP`); anything else is refused before any
354
+ request. A campaign set up for three currencies takes three runs. `qa parity`
355
+ refuses `--currency`, and so does a run whose orders come from
356
+ `--legacy-api-test-order`, since a direct API order would be placed in the
357
+ default currency. The persisted verdict redacts every URL query, so the
358
+ currency is recorded as `evidence.currency` on the entry page's
359
+ `http:<page_id>` assertion; an entry URL that does not parse gets no currency,
360
+ and its row records `evidence.currency_not_applied` with the reason instead.
361
+
344
362
  ### Automatic commercial parity
345
363
 
346
364
  When the CampaignSpec has enabled pages with package rows, the same `qa run`
@@ -804,7 +822,7 @@ prints the portal link.
804
822
  The outcome lands on the run's Run Record as the `qa_verdict_publish` block —
805
823
  `verdict_run_id`, `publisher` (`qa run` or `qa publish`), `state`
806
824
  (`skipped` / `ok` / `failed`), `result` in the remit vocabulary, `base_kind`,
807
- `published_at` — on the record whose `qa_verdict` artifact references the
825
+ `published_at`, and the nullable publish decision `reason` — on the record whose `qa_verdict` artifact references the
808
826
  verdict under the packet's campaign. `qa run` records its own publish (or
809
827
  its `--no-post-verdict` skip) the same way when the run session closes, which
810
828
  is what `already_published` reads. A stored `ok` is never downgraded: a
@@ -1183,8 +1201,8 @@ Fixture essentials:
1183
1201
 
1184
1202
  Test Orders use **global test cards** that work on any live store and integration.
1185
1203
  They **bypass the payment gateway and create no transactions** (and no fulfillment),
1186
- so they are safe to run any time and need **no permission flags, packet policy,
1187
- merchant sandbox routing, or test-order approval** — you just pick a mode. They leave a small,
1204
+ but each creates a real order record on the store. Unless the operator has already said test orders are fine for this campaign, ask once, up front, before placing test orders; then proceed. No permission flag,
1205
+ packet policy, or merchant sandbox routing is required. They leave a small,
1188
1206
  easy-to-clean footprint: Test orders are deletable in bulk, and the resulting
1189
1207
  Customer record is reused (see the test email note below) rather than multiplied.
1190
1208
 
@@ -1566,17 +1584,43 @@ Two flags target funnels the default-tier drive cannot prove:
1566
1584
 
1567
1585
  - `--select-package <ref[:qty],...>` — **strict** package/bundle card selection.
1568
1586
  Each ref is matched against rendered selector/bundle cards
1569
- (`[data-next-package-id]`, `[data-next-bundle-card][data-next-bundle-id]`) and
1570
- clicked. An explicit quantity requires one unambiguous rendered card whose
1587
+ (`[data-next-package-id]`, `[data-next-bundle-card][data-next-bundle-id]`, and
1588
+ the package a bundle card's `data-next-bundle-items` declares, so
1589
+ `--select-package 1:2` finds the card declaring
1590
+ `[{"packageId":1,"quantity":2}]` whether or not an inner node repeats the
1591
+ package id) and clicked. An explicit quantity requires one unambiguous rendered card whose
1571
1592
  `data-next-bundle-items` composition contains exactly that package and
1572
- quantity; package identity alone is not enough. The card must then expose a
1593
+ quantity; package identity alone is not enough. A card declaring several
1594
+ items matches by any one of them (`4:1` finds a kit declaring
1595
+ `[{"packageId":1,"quantity":2},{"packageId":4,"quantity":1}]`); a card
1596
+ declaring the item alone is preferred over one declaring it among others. The card must then expose a
1573
1597
  selected-state marker (`data-next-selected="true"` / `.next-selected`) after
1574
1598
  the click. A missing card, ambiguous composition, wrong quantity, or
1575
1599
  unverifiable/refused selection **fails the `selected_bundle` step** instead
1576
1600
  of silently driving the pre-selected default tier. A bundle ref remains an
1577
1601
  authoritative identity when exactly one rendered card declares it. Use this
1578
1602
  flag to traverse non-default tiers of a multi-tier selector. `--cart` remains
1579
- the best-effort variant.
1603
+ the best-effort variant. For a package that bundle cards declare, `--cart
1604
+ <ref:qty>` clicks the card whose items carry that package at that quantity,
1605
+ and clicks nothing when the selected card already does; refs no bundle card
1606
+ declares (an order bump) are clicked as before. The cards are read again
1607
+ before each ref, so a selector that re-renders after a click is clicked as
1608
+ it now stands. Beside `--select-package`,
1609
+ `--cart` never clicks another card in the chosen card's selector group, and
1610
+ a card in no recognised selector container
1611
+ (`[data-next-bundle-selector]`, `[data-next-selector-id]`,
1612
+ `[data-next-cart-selector]`) counts as in that group: a
1613
+ ref the chosen card carries is left as it is, a ref an order bump or a card
1614
+ in another selector group carries is applied through that control, and any
1615
+ other ref is left unapplied and named in the `selected_bundle` detail
1616
+ (`--cart 3:1 not applied: ...`). The display-parity row reconciles the order
1617
+ against the packages and quantities every selected bundle card declares, not
1618
+ its `data-next-bundle-id`. Without `--select-package` it also fails when an
1619
+ explicit `--cart <ref:qty>` is not in the order at that quantity: a
1620
+ different quantity (`--cart 1:2` that ordered one unit) or no line for the
1621
+ package at all (`--cart 1:2,3:1` on a swap-mode selector that kept only the
1622
+ `3:1` card); beside `--select-package` it judges the order against the card
1623
+ that flag chose.
1580
1624
  - `--apply-coupon <code>` — types the code into the rendered coupon/promo input
1581
1625
  (the SDK's `[data-next-checkout-field="coupon"]` or
1582
1626
  `input[data-next-coupon="input"]`, then hand-rolled `coupon`/`voucher`/`promo`
@@ -1709,7 +1753,7 @@ browser launch, prints the planned count, lists the planned paths (up to 40 ids;
1709
1753
  past that the remainder is counted, never cut silently, and `--select-package
1710
1754
  <ref[:qty]>` lists one tier's paths), and names the exact `--max-test-orders <count>` raise. For example, a linear three-offer graph has
1711
1755
  eight terminal paths plus the checkout baseline, so it requires
1712
- `--max-test-orders 9`. No approval step is involved.
1756
+ `--max-test-orders 9`. Unless the operator has already said test orders are fine for this campaign, ask once, up front, before placing these real store order records; then proceed. The command has no permission flag.
1713
1757
 
1714
1758
  `--max-test-orders` bounds **planned paths**, which is not the same as real
1715
1759
  purchases. `--max-order-creations` bounds **actual order creations**, defaults to
@@ -1849,7 +1893,7 @@ and hardcoded phone numbers that differ from CampaignSpec `campaign.store_phone`
1849
1893
  If a static claim is intentionally preserved, wrap it in an element with
1850
1894
  `data-skip-market-lint="true"` and record why in the assembly report.
1851
1895
 
1852
- Test orders themselves need no allowlist or approval. A separate concern is the
1896
+ Test orders need no allowlist and no permission flag; unless the operator has already said test orders are fine for this campaign, ask once, up front, before the first one (each is a real order record). A separate concern is the
1853
1897
  **SDK origin allowlist**: the Campaign Cart SDK must be allowed to load on the
1854
1898
  tested origin for the campaign API key, or runtime checks (and the live page
1855
1899
  itself) may not initialize. Localhost on any port is globally available as a
@@ -1864,8 +1908,8 @@ npm run campaigns-os -- qa policy set \
1864
1908
  --allowed-domains-confirmed true
1865
1909
  ```
1866
1910
 
1867
- There is no permission flag for test orders — they run from `--test-order
1868
- <mode>` alone. The former `--test-orders-allowed` /
1911
+ There is no permission flag for test orders; unless the operator has already said test orders are fine for this campaign, ask once, up front, before placing these real store order records, then run `--test-order
1912
+ <mode>`. The former `--test-orders-allowed` /
1869
1913
  `--sandbox-test-card-confirmed` flags and the `qa.test_orders_allowed` /
1870
1914
  `qa.sandbox_test_card_confirmed` packet fields they set were removed in
1871
1915
  supported surface 1.28.0: nothing had read their values since the gate itself
@@ -8,7 +8,7 @@
8
8
 
9
9
  How a checkout of this repository at one commit becomes a usable installed runtime, and how a consumer decides whether a prepared one is still trustworthy. Everything below is generated from `contracts/runtime-recipe.campaigns-os-node-v1.json`, which is the only authority for these values.
10
10
 
11
- Recipe kind `campaigns-os-node-v1`, revision `1.0.2`, validated by `schemas/campaigns-os-runtime-recipe.v1.schema.json` (`Campaigns OS Runtime Recipe v1`). Supported surface at generation time: `1.54.0`.
11
+ Recipe kind `campaigns-os-node-v1`, revision `1.0.2`, validated by `schemas/campaigns-os-runtime-recipe.v1.schema.json` (`Campaigns OS Runtime Recipe v1`). Supported surface at generation time: `1.55.0`.
12
12
 
13
13
  ## What this is
14
14
 
@@ -16,7 +16,7 @@ that the copy on disk moved.
16
16
  `skills.json` carries one top-level field:
17
17
 
18
18
  ```json
19
- "bundle_revision": "1.54.0+skills.1"
19
+ "bundle_revision": "1.55.0+skills.1"
20
20
  ```
21
21
 
22
22
  The spelling is `<package version>+skills.<n>`:
@@ -25,7 +25,7 @@ The spelling is `<package version>+skills.<n>`:
25
25
  skills ship with (`check-skill-versions.mjs` fails if the two disagree);
26
26
  - `<n>` is a plain counter, not a semver component. It says "this is the *n*th
27
27
  skill-text revision published against that package version" and it **resets
28
- with the prefix**. `1.54.0+skills.1` is therefore ahead of `1.40.0+skills.7`.
28
+ with the prefix**. `1.55.0+skills.1` is therefore ahead of `1.40.0+skills.7`.
29
29
 
30
30
  It is one identity for the bundle as a whole, on purpose. Per-skill versions
31
31
  still exist and still gate per-skill changes, but an agent that loaded one skill
@@ -37,7 +37,7 @@ The first body line of every bundled `SKILL.md`, immediately after the
37
37
  frontmatter, is exactly:
38
38
 
39
39
  ```
40
- Bundle revision: 1.54.0+skills.1
40
+ Bundle revision: 1.55.0+skills.1
41
41
  ```
42
42
 
43
43
  followed by a short paragraph telling the agent to run the check below at the
@@ -48,7 +48,7 @@ text the agent is actually reading, not from a file it would have to go and open
48
48
  ## The check
49
49
 
50
50
  ```bash
51
- npx --no-install campaigns-os tooling status --skills-revision 1.54.0+skills.1
51
+ npx --no-install campaigns-os tooling status --skills-revision 1.55.0+skills.1
52
52
  ```
53
53
 
54
54
  The value is compared against the bundle revision of the **CLI the command runs
@@ -90,20 +90,20 @@ reports the choice as `skills.scope` (`requested`, `installed_platforms`, or
90
90
  "revision_check": "match",
91
91
  "skills_revision": {
92
92
  "status": "match",
93
- "requested": "1.54.0+skills.1",
93
+ "requested": "1.55.0+skills.1",
94
94
  "spelling": "bundle",
95
- "on_disk": "1.54.0+skills.1",
95
+ "on_disk": "1.55.0+skills.1",
96
96
  "on_disk_skill": null,
97
- "message": "match (1.54.0+skills.1)"
97
+ "message": "match (1.55.0+skills.1)"
98
98
  }
99
99
  ```
100
100
 
101
101
  The text view prints one named line, as a header above the rest of the status:
102
102
 
103
103
  ```
104
- Skills revision: match (1.54.0+skills.1)
105
- Skills revision: mismatch: loaded 1.39.0+skills.1, on disk 1.54.0+skills.1 — start a fresh session
106
- Skills revision: unchecked (on disk 1.54.0+skills.1)
104
+ Skills revision: match (1.55.0+skills.1)
105
+ Skills revision: mismatch: loaded 1.39.0+skills.1, on disk 1.55.0+skills.1 — start a fresh session
106
+ Skills revision: unchecked (on disk 1.55.0+skills.1)
107
107
  ```
108
108
 
109
109
  `unchecked` is the state when the flag is absent. It is not an error — an
@@ -13,7 +13,7 @@ implementation detail, however stable it looks.
13
13
 
14
14
  | Surface | Contract | Change discipline |
15
15
  |---|---|---|
16
- | `schemas/*.schema.json` (all of them) | The portable contract catalog: CampaignSpec, Design Source Package, Build Packet, Build Context, Assembly Report, Doctor Output, sidecar-bundle conformance, Run Record, Workflow Finding, Build Brief, Source-HTML Manifest, Tooling Orientation, Release Ledger, QA Verdict, the QA Verdict sidecar projection, Runtime Recipe, and the legacy-migration inventory/plan/receipt trio. | Hashed. Any content change requires updating the recorded hash **and** bumping `surface_version` in the same PR. A shape change that alters meaning gets a new schema-version const — one version identifier must never cover two shapes (the 2026-08 assembly-report drift is the incident this rule encodes). Additions to an open `v0` schema are expected and consumers must tolerate unknown fields; the security-sensitive legacy-migration schemas are closed, so additions there require a new lineage. 1.28.0 (RL entry `surface_version: 1.28.0`, breaking) removed the two required Build Packet booleans `qa.test_orders_allowed` and `qa.sandbox_test_card_confirmed` (nothing read them; test orders run from `--test-order <mode>` alone), added `local-serve` to the `deploy.target` enum, and added the optional `remit_result` / `remit_base_kind` fields to the Run Record. 1.30.0 (additive) added the optional `data_layer` record to the QA Verdict's `test_orders[]` entries — the order's `dl_purchase` reading (#325). 1.33.0 (additive) added the optional `qa_verdict_publish` block to the Run Record — which verdict was posted to the QA portal, by `qa run` or `qa publish`, and what the portal answered, in the `remit_result` vocabulary (#328). |
16
+ | `schemas/*.schema.json` (all of them) | The portable contract catalog: CampaignSpec, Design Source Package, Build Packet, Build Context, Assembly Report, Doctor Output, sidecar-bundle conformance, Run Record, Workflow Finding, Build Brief, Source-HTML Manifest, Tooling Orientation, Release Ledger, QA Verdict, the QA Verdict sidecar projection, Runtime Recipe, and the legacy-migration inventory/plan/receipt trio. | Hashed. Any content change requires updating the recorded hash **and** bumping `surface_version` in the same PR. A shape change that alters meaning gets a new schema-version const — one version identifier must never cover two shapes (the 2026-08 assembly-report drift is the incident this rule encodes). Additions to an open `v0` schema are expected and consumers must tolerate unknown fields; the security-sensitive legacy-migration schemas are closed, so additions there require a new lineage. 1.28.0 (RL entry `surface_version: 1.28.0`, breaking) removed the two required Build Packet booleans `qa.test_orders_allowed` and `qa.sandbox_test_card_confirmed` (nothing read them; test orders run from `--test-order <mode>` alone), added `local-serve` to the `deploy.target` enum, and added the optional `remit_result` / `remit_base_kind` fields to the Run Record. 1.30.0 (additive) added the optional `data_layer` record to the QA Verdict's `test_orders[]` entries — the order's `dl_purchase` reading (#325). 1.33.0 (additive) added the optional `qa_verdict_publish` block to the Run Record — which verdict was posted to the QA portal, by `qa run` or `qa publish`, and what the portal answered, in the `remit_result` vocabulary (#328). 1.55.0 (breaking) adds the optional publish decision `reason`, record `closed_by` closer, QA observation `verdict_run_id`, `build_fingerprint`, and `stale`, lifecycle `stages[].wait_ms` and `stages[].finding_codes`, and run-level `needs_input_count` and `failure_count` while removing the unused `agent_usage` field from the same v0 Run Record shape. |
17
17
  | CLI commands: `start`, `prepare-build`, `build`, `polish`, `checkpoint`, `page-kit`, `spec`, `doctor`, `bundle`, `next`, `theme`, `tooling`, `install-skills`, `install-agent-context`, `validate-assembly-report`, `telemetry`, `standardize`, `qa`, `findings`, `run-record`, `run`, `sdk`, `demo`, `login`, `logout` | Scriptable entry points. The founding list (1.0.0) was the argv surface Campaigns Agent's remit fixture pins; 1.25.0 adds the partner entry path the public guides instruct — `install-skills` (skill install), `tooling status` (preflight), `next --packet` (the runtime cursor) — plus `theme`, `install-agent-context`, `validate-assembly-report`, and `telemetry`, since consent is part of the partner contract. `validate-build-packet`, formerly an undocumented and unsupported alias of `doctor`, was removed in 1.25.0+agent.7 (RL-0036) and now gets the unknown-command error; use `doctor`. `standardization-report`, a supported but redundant second spelling of `standardize` — same flags, same output, same exit codes — was removed in 1.27.0 and now gets the same unknown-command error; use `standardize`. Removing a supported command is a breaking change and is why that release moved the minor. `bundle check` validates the canonical JSON readback set and makes QA required only under `--require-qa`. `polish capture` owns page-load evidence production. `checkpoint waive` currently accepts five registered gates: `page_kit.store_profile`, `page_kit.sdk_version`, `polish.hidden_eager_media`, `built_output.upsell_selector_scope`, and `source_html.producer_provenance` (per page, with `--page <page_id>`). `page-kit sync` (added in 1.29.0) writes the CampaignSpec's Store Profile fields and SDK pin into the target's `_data/campaigns.json` entry — the repair the first two gates name — and is the only `page-kit` subcommand. `spec derive` (added in 1.31.0) is the reverse write for the repo-derived field class (#432): the target's SDK pin, page routes and analytics ids into the packet's local CampaignSpec; it is the only `spec` subcommand, and the `page_kit.sdk_version.repo_newer` advisory names it; its `--write-map` flag (1.33.0+agent.2) also records the derived pin in the saved Map's Build hints field through the proxy Worker, never moving a Map pin backwards. `qa publish` (added in 1.33.0) posts an already-stored verdict to the QA portal without a re-run or an order, refusing a stale `spec_hash` or a verdict its Run Record already records as published (#328). Within Polish, only the broader Source Freshness waiver remains on its existing report lane; theme and QA decisions also retain their existing lanes. | Any change to this list — adding, renaming, or removing a command — bumps `surface_version` in the same PR, and since 1.25.0 `check-supported-surface.mjs --base` enforces that (before, only hashed and named entries owed a bump, so `checkpoint` landed unbumped). Subcommands, registered gates, and flags may grow freely beneath a listed command. Removing a listed command from dispatch fails the gate outright. Do not infer support for an unregistered checkpoint from the top-level command. |
18
18
  | `sdk storage-check` and `docs/sdk-storage-compatibility.md` | Read-only AST compatibility report for explicitly scoped tracked campaign HTML/JS before an SDK bump; consumes the supplied SDK-owned manifest and records its SHA-256 and verified or unverified local Git provenance. Incompatible and unknown findings exit 2; a clean scan is static source evidence only. | Additive supported CLI and named documentation; no automatic merchant integration repair. |
19
19
  | `sdk repin` | Rewrites the version segment of semver-pinned Campaign Cart loader.js / campaign-cart.css URLs in a static repo (no `_data/campaigns.json`) to a target version, default the SDK support policy's `preferred_minimum`. It previews by default. `--apply` writes the files and the change record `.campaign-runtime/sdk-repin.json` (`campaigns-os-sdk-repin/v0`). `@latest`, `@main`, commit and prerelease refs, and pins at or above the target, are reported and never rewritten. A page-kit repo is refused with exit 2. Usage is in `docs/sdk-storage-compatibility.md`. | Additive supported subcommand under the listed `sdk` command. It never changes anything in a URL except the version segment. |
@@ -51,7 +51,8 @@ own schemas and evolve independently). Instead it carries:
51
51
 
52
52
  - **Stable envelope** — `schema_version` (`campaigns-os-run-record/v0`),
53
53
  `run_id`, package version, the command that ran, an `argv` *shape* (flag names
54
- present, not raw values), `created_at`, consent state, and remit status.
54
+ present, not raw values), `created_at`, consent state, remit status, and
55
+ optional `closed_by` (`run_end`, `qa_auto_end`, `stale_sweep`, or `manual`).
55
56
  - **Run identity** — `map_id`, `campaign_slug`, `template_family`,
56
57
  `entry_point_shape`. (Best-effort; missing identity never blocks capture.)
57
58
  - **Source artifact refs** — for the Build Packet, Build Context, Assembly
@@ -64,6 +65,17 @@ own schemas and evolve independently). Instead it carries:
64
65
  this run.
65
66
  - **Findings snapshot** — this run's Workflow Findings (see channel below).
66
67
 
68
+ When a QA verdict is present, `observations.qa.verdict_run_id` names the same
69
+ verdict that supplies `disposition`. `build_fingerprint` is the QA stage's
70
+ recorded build fingerprint only when the Assembly Report binds that stage to
71
+ the same verdict; otherwise it is `null`. `stale` compares that fingerprint
72
+ with the Assembly Report's recorded build fingerprint using the same comparison
73
+ as `next`: `true` when they differ, `false` when they match, and `null` when
74
+ either fingerprint is unknown. A rebuild without `record build` therefore reads
75
+ `stale: false` when the recorded fingerprints still match; doctor's
76
+ `built_output.fingerprint_stale` checks the output on disk. All three fields
77
+ are optional for older records; without a verdict, `observations.qa` is absent.
78
+
67
79
  ### Run identity
68
80
 
69
81
  A single canonical `campaigns_os_run_id` is minted at the run boundary and
@@ -78,6 +90,45 @@ Stage timings and repair-loop count are captured from the command lifecycle
78
90
  journal when a run session or explicit lifecycle journal is active. They remain
79
91
  best-effort signal: telemetry records the commands Campaigns OS can observe, not
80
92
  every thought, browser click, or external editor action in an agent session.
93
+ Each new journal entry records `wait_ms` as non-negative whole milliseconds
94
+ blocked on terminal input; an invocation without a prompt records 0. Legacy
95
+ entries may omit it or hold null, and Run Record stages preserve unknown wait
96
+ as absent or null rather than inventing 0. `duration_ms` still includes prompt
97
+ wait, so observed command work is `duration_ms - wait_ms` when wait is known. Tier-2 phases carry their
98
+ own measured wait when the prompt occurs while that phase is active; the journal
99
+ also retains the invocation total. Nested prompt hooks count their outer wait
100
+ interval once. Authentication prompts are outside lifecycle capture by policy.
101
+
102
+ On a non-zero doctor or QA exit, optional `finding_codes` is an array of at most
103
+ five distinct string codes in first-seen order; null is invalid. Codes belong
104
+ to the phase active when they were recorded. Codes recorded outside any phase,
105
+ and codes in a legacy entry, are invocation-level only; aggregation assigns them
106
+ to the entry's last phase. A successful stage has no finding codes. The optional run-level
107
+ `needs_input_count` and `failure_count` are non-negative integer counts; null is
108
+ invalid and absence means a legacy record did not report the count.
109
+ `needs_input_count` counts journal entries with exit 2 and `failure_count`
110
+ counts entries with any other non-zero exit. Current command
111
+ paths use 0 for success, 1 for operational errors, 2 for missing or blocked
112
+ inputs, and 4 for blocked QA; the other-non-zero rule also covers a future
113
+ status without reclassifying exit 2. Both counts are per command invocation,
114
+ including a command that emits multiple Tier-2 stages. `repair_loop_count`
115
+ continues to count repeated commands plus explicit repair-loop hooks.
116
+
117
+ | Terminal input site | Wait hook | Capture context |
118
+ |---|---|---|
119
+ | Consent question (`src/consent.mjs`) | Around the injectable `ask` call | Active command, including `run-record` |
120
+ | Store question (`src/login.mjs`) | Around readline `question` | Authentication runs outside lifecycle capture |
121
+ | Finding stage, kind, summary, details (`src/cli.mjs`) | Around each readline `question` | Active `findings add` command; omitted fields do not prompt |
122
+
123
+ | Non-zero finding source | Codes recorded |
124
+ |---|---|
125
+ | `doctor` | `result.errors[].code` before rendering and exit 2 |
126
+ | `start` and `build` | Their embedded doctor `errors[].code` before the prepare result sets exit 2 |
127
+ | `qa run` and `qa parity` | Blocked verdict assertions: `evidence.code`, or assertion `id` when no code is present, before returning from QA dispatch with exit 4 |
128
+
129
+ Spec resolution exceptions and prepare-build without a doctor result do not
130
+ expose doctor or QA codes. They keep their existing exit and timing evidence,
131
+ with no invented finding code.
81
132
 
82
133
  ### Validation
83
134
 
@@ -246,7 +297,11 @@ remit(path, payload, proxyBase) // mirrors qa-node.mjs postVerdict
246
297
  for a later post of the stored verdict), `attempted` / `ok` / `error` /
247
298
  `endpoint` (`/api/qa/verdicts`), a `state` (`skipped` when the run's
248
299
  publish was off, `ok`, `failed`), the `result` in the same vocabulary as
249
- `remit_result`, the `base_kind`, and `published_at`. `qa run` hands the
300
+ `remit_result`, the `base_kind`, `published_at`, and optional `reason`.
301
+ The reason records the actual QA publish decision: `loopback_base_url`,
302
+ `portal_managed_default`, `consent_off`, `flag_opt_out`, `flag_opt_in`,
303
+ `default`, or `local_spec`. A later `qa publish` has no QA run decision and
304
+ records a null reason. `qa run` hands the
250
305
  block to the session through its QA attempt, so `run end` and the auto-end
251
306
  stamp it; `qa publish` stamps the record whose `qa_verdict` artifact
252
307
  references the verdict, reads `state: "ok"` as already published, and
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextcommerce/campaigns-os",
3
- "version": "1.54.0",
3
+ "version": "1.55.0",
4
4
  "description": "Toolkit for agent-assisted NEXT campaign builds.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -66,6 +66,8 @@
66
66
  "check:spec": "node --test \"campaign-spec/test/**/*.test.ts\"",
67
67
  "check": "npm run check:provenance && npm run check:workflows && npm run check:changelog-structure && npm run build:spec && npm run check:tests && npm run check:spec && npm run check:fixtures && npm run check:legacy-migration && npm run check:spec-conformance && npm run check:private-strings && npm run check:template-doctrine && npm run check:slot-manifest && npm run check:skill-versions && npm run check:cart-readiness && npm run check:sidecar-bundle && npm run check:supported-surface && npm run check:effects && npm run check:release-ledger && npm run check:runtime-recipe && npm run check:orientation-docs && npm run check:runtime-docs && npm run check:pack -- --skip-prepare",
68
68
  "check:tests": "node ./scripts/check-tests.mjs",
69
+ "check:fast": "node ./scripts/check-fast.mjs",
70
+ "hooks:install": "node ./scripts/install-git-hooks.mjs",
69
71
  "check:legacy-migration": "node ./scripts/check-legacy-migration.mjs",
70
72
  "check:fixtures": "node ./scripts/check-fixtures.mjs",
71
73
  "check:spec-conformance": "node ./scripts/check-campaign-spec-conformance.mjs",
@@ -134,6 +134,11 @@
134
134
  "enum": ["skipped", "ok", "failed"],
135
135
  "description": "skipped: publish was off for the run (no send). ok: the receiver's answer counts as stored. failed: refused or never answered."
136
136
  },
137
+ "reason": {
138
+ "type": ["string", "null"],
139
+ "enum": ["loopback_base_url", "portal_managed_default", "consent_off", "flag_opt_out", "flag_opt_in", "default", "local_spec", null],
140
+ "description": "The QA publish decision's reason. null when no decision was recorded, such as a later qa publish of a stored verdict."
141
+ },
137
142
  "result": {
138
143
  "type": ["string", "null"],
139
144
  "enum": ["stored", "already_stored", "ok_unparsed_ack", "refused", "transport_error", null],
@@ -226,6 +231,9 @@
226
231
  "additionalProperties": false,
227
232
  "properties": {
228
233
  "disposition": { "type": ["string", "null"] },
234
+ "verdict_run_id": { "type": ["string", "null"], "minLength": 1, "description": "Run ID of the verdict supplying disposition." },
235
+ "build_fingerprint": { "type": ["string", "null"], "minLength": 1, "description": "QA build fingerprint from the Assembly Report stage bound to that verdict." },
236
+ "stale": { "type": ["boolean", "null"], "description": "Whether that QA fingerprint differs from the current recorded build; null when either fingerprint is unknown." },
229
237
  "gap_classes": {
230
238
  "type": "array",
231
239
  "items": { "type": "string" },
@@ -286,19 +294,10 @@
286
294
  "type": ["string", "null"],
287
295
  "description": "Optional free-form confidence hint for primary_surface, e.g. \"low\"."
288
296
  },
289
- "agent_usage": {
290
- "type": "object",
291
- "additionalProperties": false,
292
- "description": "Optional host-supplied agent runtime usage. Campaigns OS does not infer these values; outer runners provide them when available.",
293
- "properties": {
294
- "input_tokens": { "type": "integer", "minimum": 0 },
295
- "output_tokens": { "type": "integer", "minimum": 0 },
296
- "tool_output_tokens": { "type": "integer", "minimum": 0 },
297
- "total_tokens": { "type": "integer", "minimum": 0 },
298
- "elapsed_ms": { "type": "integer", "minimum": 0 },
299
- "model": { "type": "string" },
300
- "source": { "type": "string" }
301
- }
297
+ "closed_by": {
298
+ "type": "string",
299
+ "enum": ["run_end", "qa_auto_end", "stale_sweep", "manual"],
300
+ "description": "The command path that closed this Run Record. Absent on records written before this field existed."
302
301
  },
303
302
  "lifecycle": {
304
303
  "type": ["object", "null"],
@@ -327,17 +326,23 @@
327
326
  "type": "object",
328
327
  "additionalProperties": false,
329
328
  "required": ["name"],
329
+ "if": { "required": ["finding_codes"], "properties": { "finding_codes": {} } },
330
+ "then": { "required": ["exit_status"], "properties": { "exit_status": { "type": "integer", "not": { "const": 0 } } } },
330
331
  "properties": {
331
332
  "name": { "type": "string" },
332
333
  "duration_ms": { "type": ["number", "null"] },
333
- "exit_status": { "type": ["integer", "null"] }
334
+ "exit_status": { "type": ["integer", "null"] },
335
+ "wait_ms": { "type": ["integer", "null"], "minimum": 0, "description": "Whole milliseconds blocked on operator input. Absent or null means unknown for legacy entries; duration_ms includes it." },
336
+ "finding_codes": { "type": "array", "maxItems": 5, "uniqueItems": true, "items": { "type": "string" }, "description": "First five distinct doctor or QA finding codes, only on a non-zero exit." }
334
337
  }
335
338
  }
336
339
  },
337
340
  "repair_loop_count": {
338
341
  "type": ["integer", "null"],
339
342
  "description": "Re-runs of any command within the run (a re-run is a repair loop: doctor -> fix -> doctor). 0 when each command ran once."
340
- }
343
+ },
344
+ "needs_input_count": { "type": "integer", "minimum": 0, "description": "Number of command invocations with exit_status 2." },
345
+ "failure_count": { "type": "integer", "minimum": 0, "description": "Number of command invocations with a non-zero exit_status other than 2." }
341
346
  }
342
347
  }
343
348
  }
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: campaign-lifecycle-orientation
3
- version: 1.0.30
3
+ version: 1.0.32
4
4
  description: Orient a reader to the Campaigns OS lifecycle artifacts a run has already emitted, without advancing any stage or changing any state.
5
5
  ---
6
6
 
7
- Bundle revision: 1.54.0+skills.1
8
- Run `npx --no-install campaigns-os tooling status --skills-revision 1.54.0+skills.1`
7
+ Bundle revision: 1.55.0+skills.1
8
+ Run `npx --no-install campaigns-os tooling status --skills-revision 1.55.0+skills.1`
9
9
  from the campaign's Page Kit folder, where it runs the project's pinned copy and
10
10
  never installs one, at the start of each task. Start a fresh session if it
11
11
  reports `mismatch`: this text is already in your context and is never re-read
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: campaign-readback-classification
3
- version: 1.0.30
3
+ version: 1.0.32
4
4
  description: Classify a selected campaign from the readback projection's v2 fields and write a read-only handoff without turning diagnosis into permission.
5
5
  ---
6
6
 
7
- Bundle revision: 1.54.0+skills.1
8
- Run `npx --no-install campaigns-os tooling status --skills-revision 1.54.0+skills.1`
7
+ Bundle revision: 1.55.0+skills.1
8
+ Run `npx --no-install campaigns-os tooling status --skills-revision 1.55.0+skills.1`
9
9
  from the campaign's Page Kit folder, where it runs the project's pinned copy and
10
10
  never installs one, at the start of each task. Start a fresh session if it
11
11
  reports `mismatch`: this text is already in your context and is never re-read
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: campaign-run-evidence
3
- version: 1.0.30
3
+ version: 1.0.32
4
4
  description: Interpret existing Campaigns OS doctor, QA and proof-depth evidence without claiming more proof than the artifacts contain.
5
5
  ---
6
6
 
7
- Bundle revision: 1.54.0+skills.1
8
- Run `npx --no-install campaigns-os tooling status --skills-revision 1.54.0+skills.1`
7
+ Bundle revision: 1.55.0+skills.1
8
+ Run `npx --no-install campaigns-os tooling status --skills-revision 1.55.0+skills.1`
9
9
  from the campaign's Page Kit folder, where it runs the project's pinned copy and
10
10
  never installs one, at the start of each task. Start a fresh session if it
11
11
  reports `mismatch`: this text is already in your context and is never re-read
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: contribution-intake
3
- version: 1.0.30
3
+ version: 1.0.32
4
4
  description: Turn a suggestion about the agent surface into a classified, evidence-checked proposal and, only with attended approval, one issue on this repository's tracker.
5
5
  ---
6
6
 
7
- Bundle revision: 1.54.0+skills.1
8
- Run `npx --no-install campaigns-os tooling status --skills-revision 1.54.0+skills.1`
7
+ Bundle revision: 1.55.0+skills.1
8
+ Run `npx --no-install campaigns-os tooling status --skills-revision 1.55.0+skills.1`
9
9
  from the campaign's Page Kit folder, where it runs the project's pinned copy and
10
10
  never installs one, at the start of each task. Start a fresh session if it
11
11
  reports `mismatch`: this text is already in your context and is never re-read
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: next-campaigns-build
3
- version: 1.0.35
3
+ version: 1.0.37
4
4
  description: Assemble a NEXT campaign from a doctor-cleared Build Packet, CampaignSpec/API values, prepared HTML/assets, page-kit, and starter-template contracts.
5
5
  ---
6
6
 
7
- Bundle revision: 1.54.0+skills.1
8
- Run `npx --no-install campaigns-os tooling status --skills-revision 1.54.0+skills.1`
7
+ Bundle revision: 1.55.0+skills.1
8
+ Run `npx --no-install campaigns-os tooling status --skills-revision 1.55.0+skills.1`
9
9
  from the campaign's Page Kit folder, where it runs the project's pinned copy and
10
10
  never installs one, at the start of each task. Start a fresh session if it
11
11
  reports `mismatch`: this text is already in your context and is never re-read
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: next-campaigns-os
3
- version: 1.0.50
3
+ version: 1.0.52
4
4
  description: Coordinate Campaigns OS lifecycle workflows from CampaignSpec, Build Packet, starter-template contracts, stage reports, deploy evidence, and QA proof depth.
5
5
  ---
6
6
 
7
- Bundle revision: 1.54.0+skills.1
8
- Run `npx --no-install campaigns-os tooling status --skills-revision 1.54.0+skills.1`
7
+ Bundle revision: 1.55.0+skills.1
8
+ Run `npx --no-install campaigns-os tooling status --skills-revision 1.55.0+skills.1`
9
9
  from the campaign's Page Kit folder, where it runs the project's pinned copy and
10
10
  never installs one, at the start of each task. Start a fresh session if it
11
11
  reports `mismatch`: this text is already in your context and is never re-read
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: next-campaigns-os-setup
3
- version: 2.0.33
3
+ version: 2.0.35
4
4
  description: Bootstrap or prepare a target page-kit campaign repo from a doctor-cleared Campaigns OS Build Packet before full build wiring. Formerly installed as next-campaigns-setup; renamed 2026-08 to stop colliding with the published NextCommerceCo/skills scaffolder of that name.
5
5
  ---
6
6
 
7
- Bundle revision: 1.54.0+skills.1
8
- Run `npx --no-install campaigns-os tooling status --skills-revision 1.54.0+skills.1`
7
+ Bundle revision: 1.55.0+skills.1
8
+ Run `npx --no-install campaigns-os tooling status --skills-revision 1.55.0+skills.1`
9
9
  from the campaign's Page Kit folder, where it runs the project's pinned copy and
10
10
  never installs one, at the start of each task. Start a fresh session if it
11
11
  reports `mismatch`: this text is already in your context and is never re-read
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: next-campaigns-polish
3
- version: 1.1.34
3
+ version: 1.1.36
4
4
  description: Run the visual/runtime polish pass after build and before QA for a Campaigns OS campaign.
5
5
  ---
6
6
 
7
- Bundle revision: 1.54.0+skills.1
8
- Run `npx --no-install campaigns-os tooling status --skills-revision 1.54.0+skills.1`
7
+ Bundle revision: 1.55.0+skills.1
8
+ Run `npx --no-install campaigns-os tooling status --skills-revision 1.55.0+skills.1`
9
9
  from the campaign's Page Kit folder, where it runs the project's pinned copy and
10
10
  never installs one, at the start of each task. Start a fresh session if it
11
11
  reports `mismatch`: this text is already in your context and is never re-read
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: next-campaigns-qa
3
- version: 1.3.34
3
+ version: 1.3.36
4
4
  description: Run spec-aware QA from a saved Map or local-spec Build Packet and tested campaign URL after build, polish, and deploy/local evidence exist, including Playwright typed-card test-order proof.
5
5
  ---
6
6
 
7
- Bundle revision: 1.54.0+skills.1
8
- Run `npx --no-install campaigns-os tooling status --skills-revision 1.54.0+skills.1`
7
+ Bundle revision: 1.55.0+skills.1
8
+ Run `npx --no-install campaigns-os tooling status --skills-revision 1.55.0+skills.1`
9
9
  from the campaign's Page Kit folder, where it runs the project's pinned copy and
10
10
  never installs one, at the start of each task. Start a fresh session if it
11
11
  reports `mismatch`: this text is already in your context and is never re-read
@@ -80,7 +80,7 @@ Rules:
80
80
  - Parity capture blocking proof is the voucher-adjusted persisted line from typed-card order readback. Browser totals and client state do not replace the persisted-line voucher guard.
81
81
  - Read client purchase values per event. A whole-cart `dl_purchase` must not mask or supply an offer-level upsell purchase expectation.
82
82
  - Live `qa parity` runs publish to the QA portal by default like other QA runs. Pass `--no-post-verdict` for dev, replay, negative-control, and other local proof runs.
83
- - Theme gate: `qa run` refuses to run when a generatable brand theme is not applied to commerce pages and no waiver exists. Apply the brand layer or record a waiver (`campaigns-os theme waive`, tier `C`: it overwrites the assembly report and doctor output / `qa run --theme-waive "<reason>"`); do not bypass the gate another way. A waived run still reports template-residue findings at warn severity.
83
+ - Theme gate: `qa run` refuses to run when a generatable brand theme is not applied to commerce pages and no waiver exists. Apply the brand layer or use the recorded waiver `campaigns-os theme waive --packet <p> --reason "<why>" --waived-by "<named human>"` (tier `C`: it overwrites the assembly report and doctor output). `qa run --theme-waive "<reason>"` is the one-run ephemeral form. Do not bypass the gate another way. A waived run still reports template-residue findings at warn severity.
84
84
  - Template residue is a QA dimension, not advice: promoted starter families must have a brand/residue/pricing contract (`contracts/template-brand-contract.<family>.v0.json`). Browser QA inspects computed styles on commerce surfaces and fails pages that still render starter defaults (`#3c7dff`/`#0a265c`, starter `next-logo.png`, paypal/klarna chrome absent from the spec).
85
85
  - Pricing visibility is a blocker: an upsell/downsell offer with zero visible price rows fails QA. Pricing surfaces render via template pricing modes (`full_price`, `compare_at_current`, `unit_price_plus_total`, `savings_badge_amount`, `code_discounted_post_checkout`), never via campaign CSS `display:none` on price wrappers.
86
86
  - The upsell and checkout bundle price checks count the SDK's `data-next-bundle-display` price as a price row when it is visible and not empty.