@nextcommerce/campaigns-os 1.48.0 → 1.52.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 (80) hide show
  1. package/CHANGELOG.md +539 -0
  2. package/agents/claude/CLAUDE.md +6 -5
  3. package/agents/codex/AGENTS.md +6 -5
  4. package/agents/copilot/copilot-instructions.md +3 -3
  5. package/agents/cursor/campaigns-os.mdc +3 -3
  6. package/campaign-spec/dist/rules/campaign-metadata.d.ts +5 -1
  7. package/campaign-spec/dist/rules/campaign-metadata.js +9 -2
  8. package/campaign-spec/dist/rules/design-source-shape.js +13 -3
  9. package/campaign-spec/dist/rules/sdk-version.js +2 -1
  10. package/compatibility.json +1 -1
  11. package/contracts/commerce-surface-catalog.json +26 -46
  12. package/contracts/effects.v1.json +254 -2
  13. package/contracts/release-ledger.json +1239 -0
  14. package/contracts/supported-surface.json +4 -4
  15. package/contracts/template-brand-contract.shared-commerce.v0.json +2 -2
  16. package/contracts/template-slot-manifest.shared-content-core.v0.json +24 -0
  17. package/docs/brand-theme-bridge.md +12 -6
  18. package/docs/build-packet.md +101 -12
  19. package/docs/campaign-build-brief.md +25 -1
  20. package/docs/effects.md +6 -0
  21. package/docs/local-setup.md +1 -1
  22. package/docs/orientation-contract-reference.md +1 -1
  23. package/docs/polish-evidence.md +10 -0
  24. package/docs/qa-and-test-orders.md +66 -11
  25. package/docs/runtime-readiness.md +1 -1
  26. package/docs/sdk-storage-compatibility.md +1 -1
  27. package/docs/skills-revision.md +10 -10
  28. package/package.json +1 -1
  29. package/schemas/campaign-runtime-build-packet.v0.schema.json +4 -0
  30. package/schemas/campaigns-os-qa-verdict.v0.schema.json +8 -3
  31. package/skills/campaign-lifecycle-orientation/SKILL.md +3 -3
  32. package/skills/campaign-readback-classification/SKILL.md +3 -3
  33. package/skills/campaign-run-evidence/SKILL.md +3 -3
  34. package/skills/contribution-intake/SKILL.md +3 -3
  35. package/skills/next-campaigns-build/SKILL.md +4 -4
  36. package/skills/next-campaigns-os/SKILL.md +4 -4
  37. package/skills/next-campaigns-os/references/session-intake.md +7 -3
  38. package/skills/next-campaigns-os-setup/SKILL.md +3 -3
  39. package/skills/next-campaigns-polish/SKILL.md +5 -4
  40. package/skills/next-campaigns-qa/SKILL.md +6 -5
  41. package/skills.json +10 -10
  42. package/src/adapter-decision-contract.mjs +1 -1
  43. package/src/brand-theme.mjs +25 -2
  44. package/src/build-brief.mjs +68 -21
  45. package/src/built-site-scope.mjs +39 -6
  46. package/src/built-smoke-qc.mjs +1117 -0
  47. package/src/campaign-identity.mjs +36 -2
  48. package/src/cart-placeholders.mjs +730 -0
  49. package/src/cli.mjs +320 -42
  50. package/src/commercial-journey.mjs +65 -4
  51. package/src/commercial-parity.mjs +6 -1
  52. package/src/doctor/checks.mjs +291 -24
  53. package/src/doctor/inspect.mjs +53 -2
  54. package/src/doctor/next-step.mjs +1 -1
  55. package/src/install-mode.mjs +0 -8
  56. package/src/invocation.mjs +5 -2
  57. package/src/local-preview-policy.mjs +1 -1
  58. package/src/local-proof.mjs +4 -1
  59. package/src/polish-browser.mjs +218 -1
  60. package/src/polish-capture.mjs +1 -1
  61. package/src/polish-media-weight.mjs +492 -0
  62. package/src/polish-node.mjs +96 -4
  63. package/src/progress-node.mjs +5 -1
  64. package/src/qa-binding-evidence.mjs +21 -0
  65. package/src/qa-browser.mjs +338 -97
  66. package/src/qa-content-params.mjs +889 -0
  67. package/src/qa-node.mjs +114 -14
  68. package/src/qa-order-bump.mjs +22 -1
  69. package/src/qa-policy-links.mjs +1019 -0
  70. package/src/qa-tracking-params.mjs +1389 -0
  71. package/src/qa-url-privacy.mjs +168 -0
  72. package/src/qc-accept.mjs +446 -0
  73. package/src/qc-check-registry.mjs +83 -0
  74. package/src/qc-results.mjs +1049 -0
  75. package/src/sdk-attribute-index.mjs +71 -0
  76. package/src/sdk-markup.mjs +2 -2
  77. package/src/sdk-storage-compatibility.mjs +63 -3
  78. package/src/source-prep.mjs +37 -7
  79. package/src/stage-record.mjs +356 -36
  80. package/src/theme-gate.mjs +3 -3
@@ -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.48.0+skills.1"
19
+ "bundle_revision": "1.52.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.48.0+skills.1` is therefore ahead of `1.40.0+skills.7`.
28
+ with the prefix**. `1.52.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.48.0+skills.1
40
+ Bundle revision: 1.52.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.48.0+skills.1
51
+ npx --no-install campaigns-os tooling status --skills-revision 1.52.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.48.0+skills.1",
93
+ "requested": "1.52.0+skills.1",
94
94
  "spelling": "bundle",
95
- "on_disk": "1.48.0+skills.1",
95
+ "on_disk": "1.52.0+skills.1",
96
96
  "on_disk_skill": null,
97
- "message": "match (1.48.0+skills.1)"
97
+ "message": "match (1.52.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.48.0+skills.1)
105
- Skills revision: mismatch: loaded 1.39.0+skills.1, on disk 1.48.0+skills.1 — start a fresh session
106
- Skills revision: unchecked (on disk 1.48.0+skills.1)
104
+ Skills revision: match (1.52.0+skills.1)
105
+ Skills revision: mismatch: loaded 1.39.0+skills.1, on disk 1.52.0+skills.1 — start a fresh session
106
+ Skills revision: unchecked (on disk 1.52.0+skills.1)
107
107
  ```
108
108
 
109
109
  `unchecked` is the state when the flag is absent. It is not an error — an
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextcommerce/campaigns-os",
3
- "version": "1.48.0",
3
+ "version": "1.52.0",
4
4
  "description": "Toolkit for agent-assisted NEXT campaign builds.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -336,6 +336,8 @@
336
336
  "template_family": {
337
337
  "enum": [
338
338
  "undecided",
339
+ "apollo",
340
+ "apollo-mv-single-step",
339
341
  "olympus",
340
342
  "limos",
341
343
  "demeter",
@@ -343,6 +345,8 @@
343
345
  "olympus-mv-two-step",
344
346
  "shop-single-step",
345
347
  "shop-three-step",
348
+ "arjuna",
349
+ "karna",
346
350
  "custom"
347
351
  ]
348
352
  },
@@ -146,7 +146,11 @@
146
146
  "const": "campaigns-os-page-binding/v0"
147
147
  },
148
148
  "observation": {
149
- "const": "static_declaration"
149
+ "enum": [
150
+ "static_declaration",
151
+ "sdk_request"
152
+ ],
153
+ "description": "static_declaration: the key the page's HTML and same-origin config scripts declare. sdk_request: the key the Campaign Cart SDK sent to the Campaigns API in the browser pass (qa run --browser)."
150
154
  },
151
155
  "outcome": {
152
156
  "enum": [
@@ -174,7 +178,8 @@
174
178
  "enum": [
175
179
  "meta",
176
180
  "inline",
177
- "config_script"
181
+ "config_script",
182
+ "sdk_request"
178
183
  ]
179
184
  }
180
185
  },
@@ -182,7 +187,7 @@
182
187
  "const": "not_verified"
183
188
  }
184
189
  },
185
- "description": "Credential-free static declaration comparison, not observed execution or Campaign App identity. No keys, hashes, fragments, source URLs or inferred resource IDs."
190
+ "description": "Credential-free comparison of the page's key with the expected key: the static declaration, or the key the SDK sent in the browser pass. Not Campaign App identity. No keys, hashes, fragments, source URLs or inferred resource IDs."
186
191
  },
187
192
  "pageUrlRef": {
188
193
  "type": "object",
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: campaign-lifecycle-orientation
3
- version: 1.0.22
3
+ version: 1.0.27
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.48.0+skills.1
8
- Run `npx --no-install campaigns-os tooling status --skills-revision 1.48.0+skills.1`
7
+ Bundle revision: 1.52.0+skills.1
8
+ Run `npx --no-install campaigns-os tooling status --skills-revision 1.52.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.22
3
+ version: 1.0.27
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.48.0+skills.1
8
- Run `npx --no-install campaigns-os tooling status --skills-revision 1.48.0+skills.1`
7
+ Bundle revision: 1.52.0+skills.1
8
+ Run `npx --no-install campaigns-os tooling status --skills-revision 1.52.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.22
3
+ version: 1.0.27
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.48.0+skills.1
8
- Run `npx --no-install campaigns-os tooling status --skills-revision 1.48.0+skills.1`
7
+ Bundle revision: 1.52.0+skills.1
8
+ Run `npx --no-install campaigns-os tooling status --skills-revision 1.52.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.22
3
+ version: 1.0.27
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.48.0+skills.1
8
- Run `npx --no-install campaigns-os tooling status --skills-revision 1.48.0+skills.1`
7
+ Bundle revision: 1.52.0+skills.1
8
+ Run `npx --no-install campaigns-os tooling status --skills-revision 1.52.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.27
3
+ version: 1.0.32
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.48.0+skills.1
8
- Run `npx --no-install campaigns-os tooling status --skills-revision 1.48.0+skills.1`
7
+ Bundle revision: 1.52.0+skills.1
8
+ Run `npx --no-install campaigns-os tooling status --skills-revision 1.52.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
@@ -107,6 +107,6 @@ Build rules:
107
107
  - Run page-kit build and SDK/template lint available in the target repo.
108
108
  - Record build with `campaigns-os record build --packet <packet>` after every page-kit build (tier `C`: it overwrites `stages.assembly` and, when the output changed, resets `stages.polish` to `required` in the assembly report, and stamps the doctor output stale; `--dry-run` is tier `none`). It stamps `stages.assembly.build_fingerprint` with the fingerprint doctor computes from `_site/<slug>/` and the Design Source Package material fingerprint when the report has one. Never type or copy these fields by hand.
109
109
  - Capture the machine-readable build summary as an artifact: `npx campaign-build --json > .campaign-runtime/page-kit-build-summary.json` (requires `next-campaign-page-kit` >= 0.1.4). Doctor's `built_output.build_summary` check verifies per-page build status and Page Kit shape warnings (`NESTED_NO_PERMALINK`, `DUPLICATE_OUTPUT`, `MISSING_FRONTMATTER`, `LAYOUT_NOT_FOUND`, `NO_CAMPAIGN`) from this artifact. If the installed page-kit predates `--json`, record that in the assembly report instead of skipping silently.
110
- - Update the assembly report with commands, evidence, warnings, blockers, and next owner. If a brand theme was applied, record `report.theme.status`, `css_path`, `commerce_pages`, `load_order=after-next-core`, evidence, and any first repair-loop defect.
110
+ - Update the assembly report with commands, evidence, warnings, blockers, and next owner. If a brand theme was applied, run `campaigns-os record theme --packet <p>` (tier `C`) after `record build`: it reads each built commerce page's stylesheet links and records `report.theme` (status applied, `load_order=after-next-core`, `css_path`, `commerce_pages`, evidence). It refuses, writing nothing, when a page that loads `next-core.css` does not load the brand layer after it. Don't hand-edit `report.theme`; a first repair-loop defect goes through `record polish`.
111
111
 
112
112
  Build does not replace polish or QA. Hand off to `next-campaigns-polish` when the campaign is runnable.
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: next-campaigns-os
3
- version: 1.0.42
3
+ version: 1.0.47
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.48.0+skills.1
8
- Run `npx --no-install campaigns-os tooling status --skills-revision 1.48.0+skills.1`
7
+ Bundle revision: 1.52.0+skills.1
8
+ Run `npx --no-install campaigns-os tooling status --skills-revision 1.52.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
@@ -95,7 +95,7 @@ Map and run endpoints) with a local CampaignSpec, prepared HTML/assets source, t
95
95
  retains each attributed exception as `ready_with_exceptions`, and one
96
96
  exception never suppresses another blocker.
97
97
  10. Run the package-owned proof path in sequence: ensure `npx --no-install campaigns-os qa install-browser` has completed, run `campaigns-os qa resolve --packet <packet>` (tier `A`: it fetches `--base-url`; `--no-probe` is tier `B` and local), then `campaigns-os qa run --packet <packet> --base-url <url> --browser --test-order common` (tier `C`: it overwrites the stored verdict and assembly report, places real typed-card test orders against the campaign, and posts the verdict and progress; `--no-post-verdict` drops the verdict POST and `--no-remit` the Run Record remit, but both stay tier `C`).
98
- 11. Treat typed-card proof coverage as the control. Global test cards bypass the gateway and create no transactions, so no permission/approval is needed. `common` runs every actual terminal path when they fit under the flood cap (`--max-test-orders`, 6 by default); above the cap it runs checkout, first-offer accept and decline, and a deduplicated shortest real receipt path, then adds one decline path per offer or downsell page not yet declined, up to the cap, and names any page left out. A page counts as covered only when an order clicks its decline; the `browser-test-order:upsell-action-coverage` verdict row warns naming each page whose decline no order clicked. `full` walks every actual terminal path in the selected checkout topology; cycles, missing routes, and reachable nonterminals block exhaustive proof before browser launch. The accidental-flood cap remains `6`, and an overflow names the exact explicit `--max-test-orders` raise. Localhost on any port is a Campaigns App Development domain for SDK QA with analytics suppressed; non-localhost preview/production origins still need SDK origin allowlist confirmation.
98
+ 11. Treat typed-card proof coverage as the control: `qa run` has no permission flag. Test orders still land in the store as real orders (global test cards: no charge, no transaction) that someone may have to cancel. Unless the operator has already said test orders are fine for this campaign, ask once, up front in your first turn with your other setup questions, so the answer covers the whole build and QA and neither stops for it later. `common` runs every actual terminal path when they fit under the flood cap (`--max-test-orders`, 6 by default); above the cap it runs checkout, first-offer accept and decline, and a deduplicated shortest real receipt path, then adds one decline path per offer or downsell page not yet declined, up to the cap, and names any page left out. A page counts as covered only when an order clicks its decline; the `browser-test-order:upsell-action-coverage` verdict row warns naming each page whose decline no order clicked. `full` walks every actual terminal path in the selected checkout topology; cycles, missing routes, and reachable nonterminals block exhaustive proof before browser launch. The accidental-flood cap remains `6`, and an overflow names the exact explicit `--max-test-orders` raise. Localhost on any port is a Campaigns App Development domain for SDK QA with analytics suppressed; non-localhost preview/production origins still need SDK origin allowlist confirmation.
99
99
  12. Discuss launch only from recorded build, polish, deploy, browser QA, and test-order evidence, or from explicit blockers.
100
100
 
101
101
  ## Session Intake
@@ -141,9 +141,13 @@ hand-edit deployed routing config as the primary promotion path.
141
141
 
142
142
  ## Test-Order Proof Policy
143
143
 
144
- Treat test orders as cheap, repeatable proof: global test cards bypass the
145
- gateway and create no transactions, so they need no permission or approval. The
146
- only real choice is coverage. Record:
144
+ Treat test orders as repeatable proof. `qa run` has no permission flag:
145
+ coverage is its only control. Test orders still land in the store as real
146
+ orders (global test cards: no charge, no transaction) that someone may have to
147
+ cancel. Unless the operator has already said test orders are fine for this
148
+ campaign, ask once, up front in your first turn with your other setup questions,
149
+ so the answer covers the whole build and QA and neither stops for it later.
150
+ Record:
147
151
 
148
152
  - Coverage: `common` (every actual terminal path when they fit under the flood cap; above it, checkout, first-offer accept/decline, a deduplicated shortest real receipt path, and one decline path per offer or downsell page not yet declined, up to the cap), `off`, `checkout`, `decline`, `accept`, `both`, `full`, or explicit paths such as `decline-decline-accept`.
149
153
  - Cart matrix: base cart, base plus bump, specific package refs/quantities.
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: next-campaigns-os-setup
3
- version: 2.0.25
3
+ version: 2.0.30
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.48.0+skills.1
8
- Run `npx --no-install campaigns-os tooling status --skills-revision 1.48.0+skills.1`
7
+ Bundle revision: 1.52.0+skills.1
8
+ Run `npx --no-install campaigns-os tooling status --skills-revision 1.52.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.26
3
+ version: 1.1.31
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.48.0+skills.1
8
- Run `npx --no-install campaigns-os tooling status --skills-revision 1.48.0+skills.1`
7
+ Bundle revision: 1.52.0+skills.1
8
+ Run `npx --no-install campaigns-os tooling status --skills-revision 1.52.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
@@ -48,7 +48,8 @@ Use this after build has produced a runnable page-kit campaign.
48
48
  Theme gate: `campaigns-os next polish` (tier `A`, like every `next` form: additive writes under `.campaign-runtime/` plus a stage-progress POST under Run Telemetry consent; `--no-write` and `--no-remit` are each tier `B`) blocks when theme inspect found a
49
49
  generatable brand theme that is not yet applied to commerce pages. Do not work
50
50
  around the gate — apply the brand layer (`theme generate`, tier `B`; copy into
51
- campaign assets, load after `next-core.css`, record `report.theme`) or record an
51
+ campaign assets, load after `next-core.css`, rebuild, `campaigns-os record build`,
52
+ then `campaigns-os record theme --packet <p>`, tier `C`) or record an
52
53
  explicit waiver (`campaigns-os theme waive --packet <p> --reason "<why>" --waived-by "<named human>"` — tier `C`, because the waiver overwrites the assembly report and doctor output; placeholders are refused).
53
54
 
54
55
  Responsibilities:
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: next-campaigns-qa
3
- version: 1.3.26
3
+ version: 1.3.31
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.48.0+skills.1
8
- Run `npx --no-install campaigns-os tooling status --skills-revision 1.48.0+skills.1`
7
+ Bundle revision: 1.52.0+skills.1
8
+ Run `npx --no-install campaigns-os tooling status --skills-revision 1.52.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
@@ -89,7 +89,7 @@ Rules:
89
89
  - A typed-card path that fails is classified by **what it did to the store** before the runner decides what to do about it. A failure the runner can prove happened before submit (`not_created`) is **re-run once, if the creation budget has a slot no still-unrun planned path needs** — so a transient miss is not reported as a defect in the build, without an early path eating budget the last planned paths need. Under the default budget a path whose submit was *rejected* has already spent its own slot, so it is not re-run and records `evidence.order_creation.rerun_skipped` instead. When the re-run does happen, both attempts appear in `test_orders[]` and `evidence.retry` names the first attempt's error and ref id. A failure that happened **after** the order was created (`created` — most often a receipt that did not render) is **never resubmitted**: the runner reloads that order's receipt and re-runs only the read-only checks, and `evidence.recovery` carries the original failure, the checks re-run, and whether it cleared. Read `evidence.order_creation` for two separate counts: `submissions_reserved` (platform-side creation slots charged to this path — reserved before a submit click, or charged for a hosted-checkout redirect where no submit click happens — which stand even when the create then failed) and `orders_confirmed_created` (creates the platform was observed to accept) — a spent slot with no confirmed order is the ambiguous case, not an order to reconcile. Recovery clears only on persisted evidence it re-read on that pass: a failed or absent order read-back stops it honestly rather than re-deciding against the original attempt's numbers. An outcome it cannot prove either way (`ambiguous` — an unusable read-back, a lost create response, a network-failed create, a 4xx after an earlier 2xx) stops the path and names the operator check instead of buying again. A pass that only came back after recovery is never indistinguishable from a first-attempt pass, and a failure that survives recovery still blocks.
90
90
  - Analytics correctness is two-phase in the same run: the campaign-root visit inventories declared providers/tags only, then the one canonical typed-card run proves Purchase for each topology-recognized receipt from the signals emitted across every page the path loaded after checkout, read after the full `--analytics-settle` window. The receipt qualifies the order; the journey is measured, because the SDK fires `dl_purchase` (and the outbound Purchase) on the first `?ref_id=` page — the upsell page when the funnel has one — and dedupes it on the receipt. It never places a second analytics order. The receipt document's own reading stays in evidence (`receipt_signals`, `fired_on`) as the diagnostic of which document fired.
91
91
  - A missing or topology-unrecognized receipt is `MANUAL_REVIEW`/`WARN`; a recognized receipt with no dataLayer, outbound Meta, or outbound GA4 Purchase is `FAIL`/`BLOCKER`. Capture, unreadable-page, and settle-deadline errors on a recognized receipt are explicit non-waivable blockers. The `analytics-correctness:purchase-fires` waiver applies only to a genuine recognized-receipt/no-signal failure, and is recorded with `campaigns-os qa waive --assertion analytics-correctness:purchase-fires --reason "<why>"` (tier `C`: it overwrites the assembly report and doctor output; the lane is scoped to that one assertion and every other is refused).
92
- - Keep QA in a tight sequence: install the Playwright browser, resolve topology, run browser QA plus typed-card proof with `--test-order common` by default. Test orders need no permission step. Pause only for missing inputs, out-of-scope runtime pages that block checkout proof, or merchant-specific uncertainty.
92
+ - Keep QA in a tight sequence: install the Playwright browser, resolve topology, run browser QA plus typed-card proof with `--test-order common` by default. `qa run` has no permission flag: coverage is its only control. Test orders still land in the store as real orders that someone may have to cancel, so unless the operator has already said test orders are fine for this campaign, ask once, up front in your first turn with your other setup questions. When the operator has answered, at the start of this session or earlier in the build, follow that answer and do not pause QA to ask again. When nobody asked, ask once before the first test order rather than skip the question. Otherwise pause only for missing inputs, out-of-scope runtime pages that block checkout proof, or merchant-specific uncertainty.
93
93
  - Use `--browser` for rendered browser evidence. Browser QA must use the package-owned Playwright flow, not external agent/browser skills.
94
94
  - Saved-Map QA publishes to the QA portal under the existing consent and flag controls; report the portal link only when publication succeeds. Pass `--no-post-verdict` (or `--local-only`) to keep that verdict local. This stays tier `C`: served-page probes, requested orders, Run Telemetry and progress retain their own controls. See `docs/qa-and-test-orders.md` for the saved-Map publication policy.
95
95
  - Local-spec QA always keeps verdicts and progress local. `qa run` suppresses portal publication even with `--post-verdict`; `qa publish` refuses local-spec packets with `local_spec`. Report the local verdict/sidecar as evidence, never a dashboard link. A matching route cannot replace a matching `local_spec_id`, and QA refuses a foreign or stale local Assembly Report. Run Telemetry still follows its consent controls.
@@ -116,11 +116,12 @@ Rules:
116
116
  - A path with remaining actions may stop cleanly only at a terminal recognized in that selected topology. Missing accept/decline controls on any other page are blockers. A cross-origin handoff is a valid terminal navigation but is not receipt-rendering or persisted-receipt proof.
117
117
  - Keep multi-funnel and `tiers:common` / `tiers:full` plans isolated to each selected checkout's own funnel graph, tiers, and recognized terminals; never borrow an unrelated funnel's receipt page.
118
118
  - Accepted-upsell proof is valid only when the browser observes the order upsell API mutation and the final order evidence contains the selected upsell package. A checkout bump line marked `is_upsell` is not accepted-upsell proof.
119
- - Test orders are safe to fire any time: global test cards bypass the gateway, create no transactions, and need no merchant-specific sandbox routing confirmation. Localhost on any port is globally available as a Campaigns App Development domain and suppresses Campaigns analytics; non-localhost preview/production origins must be allowlisted for the campaign API key so the SDK loads — that is about SDK initialization, not test-order permission.
119
+ - Test orders are real orders in the store, but global test cards bypass the gateway: no charge, no transaction, and no merchant-specific sandbox routing to confirm. Localhost on any port is globally available as a Campaigns App Development domain and suppresses Campaigns analytics; non-localhost preview/production origins must be allowlisted for the campaign API key so the SDK loads — that is about SDK initialization, not test-order permission.
120
120
  - Launch readiness is separate from Campaigns OS proof. If QA passes on local/preview, still surface production storefront URL, live payment methods, shipping markets, legal/support URLs, analytics expectations, and merchant-side configuration as real-shopper readiness items before launch.
121
121
  - For multi-market campaigns, verify at least one non-default currency/country path: currency display, shipping method names/prices, available payment methods, and market-specific copy.
122
122
  - Treat missing deploy URL, missing polish status, or unresolved doctor blockers as launch blockers.
123
123
  - Report blockers, warnings, and residual risks.
124
+ - At the end of QA, run `next` and present the QC handoff once. Ask the operator which open warnings to accept and why. Run `checkpoint accept` only with the refs, reason, and name the operator gave you in this conversation. Never write `qc_accepts`, `qc_results`, or QA verdict files by hand. The up-front test-order permission does not cover accepts.
124
125
  - QA follows build and polish; it does not edit campaign code.
125
126
 
126
127
  Canonical test-order flow:
package/skills.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "campaigns-os-skills",
3
3
  "description": "Skills bundled with Campaigns OS. `skills.sh` installs these into the shared agent skill directories (~/.claude/skills, ~/.codex/skills), so each one is a versioned package: bump the version whenever a package changes. `bundle_revision` identifies the bundle as a whole — `<package version>+skills.<n>`, where the prefix is this package's version and `<n>` counts the skill-text revisions published against it. It advances whenever ANY bundled skill changes, every SKILL.md states it on its first body line, and `campaigns-os tooling status --skills-revision <value>` compares the value an agent read from a skill against the bundle on disk. See docs/skills-revision.md.",
4
- "bundle_revision": "1.48.0+skills.1",
4
+ "bundle_revision": "1.52.0+skills.1",
5
5
  "homepage": "https://github.com/NextCommerceCo/campaigns-os",
6
6
  "retired_skills": [
7
7
  {
@@ -16,7 +16,7 @@
16
16
  {
17
17
  "id": "next-campaigns-os",
18
18
  "name": "Campaigns OS Lifecycle",
19
- "version": "1.0.42",
19
+ "version": "1.0.47",
20
20
  "path": "skills/next-campaigns-os/SKILL.md",
21
21
  "domain": "campaigns",
22
22
  "description": "Coordinate Campaigns OS lifecycle workflows from CampaignSpec, Build Packet, starter-template contracts, stage reports, deploy evidence, and QA proof depth."
@@ -24,7 +24,7 @@
24
24
  {
25
25
  "id": "next-campaigns-os-setup",
26
26
  "name": "Campaigns OS Setup",
27
- "version": "2.0.25",
27
+ "version": "2.0.30",
28
28
  "path": "skills/next-campaigns-os-setup/SKILL.md",
29
29
  "domain": "campaigns",
30
30
  "description": "Bootstrap or prepare a target page-kit campaign repo from a doctor-cleared Campaigns OS Build Packet before full build wiring. Formerly next-campaigns-setup; renamed to release that name to the published NextCommerceCo/skills scaffolder."
@@ -32,7 +32,7 @@
32
32
  {
33
33
  "id": "next-campaigns-build",
34
34
  "name": "Campaign Build",
35
- "version": "1.0.27",
35
+ "version": "1.0.32",
36
36
  "path": "skills/next-campaigns-build/SKILL.md",
37
37
  "domain": "campaigns",
38
38
  "description": "Assemble a NEXT campaign from a doctor-cleared Build Packet, CampaignSpec/API values, prepared HTML/assets, page-kit, and starter-template contracts."
@@ -40,7 +40,7 @@
40
40
  {
41
41
  "id": "next-campaigns-polish",
42
42
  "name": "Campaign Polish",
43
- "version": "1.1.26",
43
+ "version": "1.1.31",
44
44
  "path": "skills/next-campaigns-polish/SKILL.md",
45
45
  "domain": "campaigns",
46
46
  "description": "Run the visual/runtime polish pass after build and before QA for a Campaigns OS campaign."
@@ -48,7 +48,7 @@
48
48
  {
49
49
  "id": "next-campaigns-qa",
50
50
  "name": "Campaign QA",
51
- "version": "1.3.26",
51
+ "version": "1.3.31",
52
52
  "path": "skills/next-campaigns-qa/SKILL.md",
53
53
  "domain": "campaigns",
54
54
  "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."
@@ -56,7 +56,7 @@
56
56
  {
57
57
  "id": "campaign-lifecycle-orientation",
58
58
  "name": "Campaign Lifecycle Orientation",
59
- "version": "1.0.22",
59
+ "version": "1.0.27",
60
60
  "path": "skills/campaign-lifecycle-orientation/SKILL.md",
61
61
  "domain": "campaigns",
62
62
  "description": "Orient a reader to the Campaigns OS lifecycle artifacts a run has already emitted, without advancing their state: the shared vocabulary, doctor as a gate, the stage record, and the store-theme/Page Kit two-worlds trap."
@@ -64,7 +64,7 @@
64
64
  {
65
65
  "id": "campaign-run-evidence",
66
66
  "name": "Campaign Run Evidence",
67
- "version": "1.0.22",
67
+ "version": "1.0.27",
68
68
  "path": "skills/campaign-run-evidence/SKILL.md",
69
69
  "domain": "campaigns",
70
70
  "description": "Interpret existing doctor, QA verdict and proof-depth evidence without claiming more proof than the artifacts contain, and keep funnel proof apart from merchant launch readiness."
@@ -72,7 +72,7 @@
72
72
  {
73
73
  "id": "campaign-readback-classification",
74
74
  "name": "Campaign Readback Classification",
75
- "version": "1.0.22",
75
+ "version": "1.0.27",
76
76
  "path": "skills/campaign-readback-classification/SKILL.md",
77
77
  "domain": "campaigns",
78
78
  "description": "Classify a selected campaign from the campaigns-os-readback/v2 fields (artifacts, staleness.stale_keys, clean, doctor, divergences, skip_cascades) and write a read-only handoff without turning diagnosis into permission."
@@ -80,7 +80,7 @@
80
80
  {
81
81
  "id": "contribution-intake",
82
82
  "name": "Contribution Intake",
83
- "version": "1.0.22",
83
+ "version": "1.0.27",
84
84
  "path": "skills/contribution-intake/SKILL.md",
85
85
  "domain": "campaigns",
86
86
  "description": "Turn a suggestion about the agent surface into a classified, evidence-checked and redacted proposal, filed on this repository tracker only with attended approval."
@@ -317,7 +317,7 @@ function validateTemplateSlicePaths(copied, location, targetRepo, warnings, read
317
317
 
318
318
  function checkEnum(value, allowed, code, warnings, addIssue) {
319
319
  if (value == null) return;
320
- if (!allowed.has(value)) addIssue(warnings, code, `${code} has unknown value "${value}".`);
320
+ if (!allowed.has(value)) addIssue(warnings, code, `${code} has unknown value ${JSON.stringify(value)}; allowed values: ${[...allowed].join(", ")}.`);
321
321
  }
322
322
 
323
323
  function isObject(value) {
@@ -25,7 +25,7 @@ export const THEME_REPORT_STATUSES = new Set(["applied", "skipped", "blocked", "
25
25
  export const THEME_CONFIDENCES = new Set(["high", "medium", "low", "none"]);
26
26
 
27
27
  const SKIP_DIRS = new Set([".git", "node_modules", "_site", "dist", "build", ".next", "coverage", "qa-output"]);
28
- const BRAND_LAYER_FILENAMES = new Set(["brand-theme.css", "checkout-brand.css"]);
28
+ export const BRAND_LAYER_FILENAMES = new Set(["brand-theme.css", "checkout-brand.css"]);
29
29
 
30
30
  function isObject(value) {
31
31
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
@@ -88,6 +88,18 @@ function loadContracts(repoRoot = ROOT) {
88
88
  };
89
89
  }
90
90
 
91
+ // A theme write error as the Assembly Report's theme.warnings[] carries it
92
+ // (schema $defs.themeIssue): detail, when present, must be an object, so an
93
+ // issue without one omits the key rather than writing null.
94
+ export function themeIssueForReport(error) {
95
+ const detail = error?.detail;
96
+ return {
97
+ code: error?.code,
98
+ message: error?.message,
99
+ ...(detail && typeof detail === "object" && !Array.isArray(detail) ? { detail } : {}),
100
+ };
101
+ }
102
+
91
103
  function issue(code, message, detail = null) {
92
104
  return detail ? { code, message, detail } : { code, message };
93
105
  }
@@ -574,6 +586,17 @@ function discoverThemeCandidates({ sourceRoot, pageMappings = [], manifest = nul
574
586
  candidates.push(candidateFromFile(path, role, source, referencedBy));
575
587
  }
576
588
 
589
+ // A stylesheet that pages of different roles link is the funnel's shared
590
+ // layer, not the first linking page's: it ranks as shared.
591
+ function addPageReference(path, role, referencedBy) {
592
+ const existing = seenFiles.has(resolve(path))
593
+ ? candidates.find((candidate) => candidate.source === "mapped_html_reference" && resolve(candidate.path) === resolve(path))
594
+ : null;
595
+ if (!existing) return addFile(path, role, "mapped_html_reference", referencedBy);
596
+ existing.referenced_by.push(...referencedBy);
597
+ if (existing.role !== role) existing.role = "shared";
598
+ }
599
+
577
600
  for (const conventional of [
578
601
  "assets/css/tokens.css",
579
602
  "assets/css/landing/tokens.css",
@@ -595,7 +618,7 @@ function discoverThemeCandidates({ sourceRoot, pageMappings = [], manifest = nul
595
618
  const content = readFileSync(htmlPath, "utf8");
596
619
  for (const ref of extractCssRefs(content)) {
597
620
  for (const cssPath of candidateCssPathsForRef(sourceRoot, htmlPath, ref)) {
598
- addFile(cssPath, candidateRoleFromPath(ref, role), "mapped_html_reference", [{ page_id: mapping.page_id || null, path: mapping.path, ref }]);
621
+ addPageReference(cssPath, candidateRoleFromPath(ref, role), [{ page_id: mapping.page_id || null, path: mapping.path, ref }]);
599
622
  }
600
623
  }
601
624
  }