@nextcommerce/campaigns-os 1.52.0 → 1.53.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 (58) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/agents/claude/CLAUDE.md +2 -0
  3. package/agents/codex/AGENTS.md +2 -0
  4. package/agents/copilot/copilot-instructions.md +2 -0
  5. package/agents/cursor/campaigns-os.mdc +2 -0
  6. package/compatibility.json +1 -1
  7. package/contracts/effects.v1.json +201 -12
  8. package/contracts/release-ledger.json +327 -0
  9. package/contracts/supported-surface.json +2 -2
  10. package/docs/brand-theme-bridge.md +2 -0
  11. package/docs/build-packet.md +56 -15
  12. package/docs/campaign-build-brief.md +48 -17
  13. package/docs/effects.md +8 -0
  14. package/docs/local-setup.md +1 -1
  15. package/docs/orientation-contract-reference.md +1 -1
  16. package/docs/polish-evidence.md +2 -0
  17. package/docs/progress-snapshots.md +2 -4
  18. package/docs/qa-and-test-orders.md +2 -0
  19. package/docs/readback.md +19 -2
  20. package/docs/runtime-readiness.md +1 -1
  21. package/docs/skills-revision.md +10 -10
  22. package/package.json +1 -1
  23. package/skills/campaign-lifecycle-orientation/SKILL.md +3 -3
  24. package/skills/campaign-readback-classification/SKILL.md +3 -3
  25. package/skills/campaign-run-evidence/SKILL.md +3 -3
  26. package/skills/contribution-intake/SKILL.md +3 -3
  27. package/skills/next-campaigns-build/SKILL.md +5 -3
  28. package/skills/next-campaigns-os/SKILL.md +10 -19
  29. package/skills/next-campaigns-os/references/session-intake.md +10 -6
  30. package/skills/next-campaigns-os-setup/SKILL.md +4 -3
  31. package/skills/next-campaigns-polish/SKILL.md +7 -5
  32. package/skills/next-campaigns-qa/SKILL.md +5 -3
  33. package/skills.json +10 -10
  34. package/src/brand-theme.mjs +21 -20
  35. package/src/brief-summary.mjs +275 -0
  36. package/src/build-brief.mjs +167 -15
  37. package/src/cli.mjs +160 -112
  38. package/src/contrast.mjs +649 -0
  39. package/src/design-source-publication.mjs +110 -3
  40. package/src/doctor/checks.mjs +129 -14
  41. package/src/doctor/next-step.mjs +15 -6
  42. package/src/input-currency.mjs +394 -0
  43. package/src/input-refresh.mjs +708 -0
  44. package/src/invocation.mjs +3 -1
  45. package/src/polish-browser.mjs +503 -201
  46. package/src/polish-deadline.mjs +7 -0
  47. package/src/polish-node.mjs +326 -46
  48. package/src/polish-readability.mjs +828 -0
  49. package/src/progress-node.mjs +15 -3
  50. package/src/qa-browser.mjs +131 -84
  51. package/src/qa-node.mjs +14 -3
  52. package/src/qc-accept.mjs +44 -8
  53. package/src/qc-check-registry.mjs +15 -7
  54. package/src/qc-results.mjs +326 -34
  55. package/src/readback.mjs +58 -10
  56. package/src/stage-ledger.mjs +235 -44
  57. package/src/stage-record.mjs +119 -26
  58. package/src/tooling-setup.mjs +1 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,54 @@
2
2
 
3
3
  Notable supported-surface changes are recorded here.
4
4
 
5
+ ## [1.53.0+agent.2] - 2026-10-05
6
+
7
+ ### Changed
8
+
9
+ `next` now prints a short campaign intent summary, generated from the normalized Campaign Build Brief, in every result (`intent_summary` in JSON) and at the top of the setup, build, Polish and QA prompts. Each value is marked as set in the saved brief file, taken from the source, a generated default, or not stated; a value whose origin was not recorded, or that changed since it was recorded, says so. These marks are tamper evidence, not proof of who wrote a value. Values are shown as quoted data. The summary never shows a purpose or palette source that Campaigns OS inferred from page types or page names: unless the brief file states them, they read "not stated". The summary is output only: nothing reads it, and it cannot change prices or commerce behaviour. Bundled skills and the agent context files now tell agents to read the brief and the recorded assembly decisions.
10
+
11
+ The build stage prompt that `next` returns now tells the agent to save a changed Campaign Build Brief with `record brief` or a changed CampaignSpec with `record spec` before building on the change.
12
+
13
+ Bundled skills are at revision `1.53.0+skills.2`. The lifecycle skill saves brief answers with `record brief` and returns the single compact brief defined in `references/session-intake.md`, whose work-type field is now `Work:` beside a new `Campaign:` field. The build skill passes `--deviation-reason` to `record build` only on the operator's explicit decision, the Polish skill records a skipped Polish only on the operator's decision, and the Polish and QA skills say how to report a readability warning and offer repairs inside the brand palette.
14
+
15
+ ### Migration
16
+
17
+ **Migration.** The agent context files changed, so `tooling setup` refuses until each existing campaign refreshes its installed copy. From the campaign folder:
18
+
19
+ 1. `npm install --save-dev --save-exact @nextcommerce/campaigns-os@1.53.0`
20
+ 2. `npx --no-install campaigns-os install-agent-context --target .` (overwrites all four files under `.campaign-runtime/agent-context/`; local edits there are lost)
21
+ 3. Review and commit `git diff .campaign-runtime/agent-context/`
22
+ 4. `npx --no-install campaigns-os tooling setup --target . --platform claude`
23
+ 5. Restart the agent session so it loads the new skills and context.
24
+
25
+ Codex, Cursor and Copilot users run step 2 and reinstall skills with `install-skills --platform codex` (or `agents`).
26
+
27
+ ## [1.53.0+agent.1] - 2026-10-05
28
+
29
+ ### Added
30
+
31
+ - Polish now measures text readability on every built page at desktop and mobile widths and reports warnings, review and coverage results in the QC handoff. QA's primary-CTA check and the theme generator use the same unrounded WCAG 2.x measurement. Results change on some campaigns: ratios in [4.495, 4.5), translucent text or backgrounds, and child text now warn; large bold text at 3:1 and Tailwind v4 `oklch()` colours are measured correctly; gradients, unreadable colours, and opacity, filters, blend modes or masks on surrounding elements become manual review instead of a contrast result. QA's primary-CTA check no longer skips pages by page type: every page with an expected next route is checked, every route-matching CTA is measured (not only the most readable one), each piece of text inside a CTA is compared with the requirement for its own size, and a CTA measured before its text, the SDK, stylesheets or fonts are ready is reported for manual review, never as a pass. The primary-CTA check also reports manual review, not a failure or a pass, when the page inspection itself errors (earlier releases reported a failure), when every route-matching CTA is disabled, and when a CTA's text includes text generated by CSS `::before` or `::after` (earlier releases reported a contrast pass or failure for both). `polish capture` on a campaign with no mapped pages now measures readability and exits successfully, printing the page-load message as a warning. Polish media weight and readability results now read as stale when the built output changed without a new `record build`.
32
+
33
+ ## [1.53.0] - 2026-10-05
34
+
35
+ ### Added
36
+
37
+ - Adds `record brief`, which saves Campaign Build Brief answers without re-running intake. A brief file is guided unless it sets `"brief_mode": "prepared"` (a campaign whose report already records a prepared brief stays prepared). An unchanged or reformatted save keeps all stage evidence and statuses. A material change marks build, Polish and QA (or QA alone for `qa_policy`) as owed again and keeps the last five superseded records per stage as history. `next` routes to the owed stage. Re-recording an unchanged build after an input change keeps the build owed unless the operator records a reason, including when a CampaignSpec change was never saved with `record spec`; an edit to the brief file is read only once it is saved with `record brief`. Intake now also refuses, without `--force`, to discard recorded waivers, warning accepts, stage history, an applied theme, or a recorded deploy target, preview URL, production URL, order-path depth or allowed-domains confirmation; with `--force`, it keeps stage history and archives the records it clears.
38
+ - Adds `record spec`, a non-destructive refresh after a CampaignSpec change. Superseded build, Polish and QA records move to stage history, and `next`, progress, readback and the Assembly Report status now report the owed stage instead of recommending QA. Spec material changes are detected for saved-Map and gateway packets too (against the fetched copy; remote currency is reported as unconfirmed). `spec derive` no longer rebinds report identity on its own; run `record spec` after it. Re-recording an unchanged build or replaying a Polish record after an input change no longer makes them current, whether or not the change was saved with `record spec`.
39
+
40
+ ### Changed
41
+
42
+ - `spec derive --json` no longer carries the `rebound` field, because `spec derive` no longer rebinds report identity.
43
+ - A stage status outside the recognized list (`pending`, `blocked`, `required`, `completed`, `completed_with_warnings`, `completed_partial`, `skipped`) now reads as unknown and stops `next`, where earlier releases accepted any status beginning `completed` or `skipped`.
44
+ - The effects contract declares the `record brief` and `record spec` rows (tier C) and their `--dry-run` rows (tier none, with no lifecycle entry). It corrects the intake rows: `start`, `prepare-build` and `build` are refused without `--force` when the report carries stage evidence, stage history or operator decisions, and their `--force` forms archive completed build, Polish and QA records into stage history, which is kept. It also states that `record build` writes `input_change` and makes Polish and QA owed when it detects a brief or CampaignSpec change, and that `record polish`, `record theme` and `record deploy` are refused while an earlier stage is owed again.
45
+
46
+ ### Migration
47
+
48
+ **Migration.** Build, Polish and QA records made before this release do not record which brief and CampaignSpec content they used, so they read as unconfirmed. After upgrading, `next` routes each in-flight campaign back to build once: re-record build and Polish, and run QA again for a new verdict. Hand-written brief files without `brief_mode` are now guided for new campaigns; add `"brief_mode": "prepared"` if you want open questions to block.
49
+
50
+ - Re-running `start`, `prepare-build` or `build` over a campaign with recorded waivers, warning accepts, stage history, an applied theme, or a recorded deploy target, preview URL, production URL, order-path depth or allowed-domains confirmation is now refused without `--force`. Save brief answers with `record brief` and a CampaignSpec change with `record spec` instead; pass `--force` only to clear that state on purpose.
51
+ - Scripts that read `rebound` from `spec derive --json` must stop: run `record spec` after `spec derive` to bind the new CampaignSpec content.
52
+
5
53
  ## [1.52.0+agent.1] - 2026-10-05
6
54
 
7
55
  ### Added
@@ -8,6 +8,8 @@ project copy. Do not use private runtime source as the campaign's starting point
8
8
 
9
9
  To pick up an existing campaign, run `npx --no-install campaigns-os readback .` and `npx --no-install campaigns-os next --packet campaign-runtime.build.json` before reading artifacts by hand; `next` blocks polish and QA again whenever the built output changes. The committed `.campaign-runtime/qa-verdict.json` keeps no order records or URLs: its `browser-test-order:<path>` assertions are the typed-card proof, and the full verdict is under `qa-output/`.
10
10
 
11
+ A NEXT campaign is one connected commerce journey: the pages the CampaignSpec declares, in its order, through product choice, checkout, post-purchase offers and receipt. Creative intent — audience, purpose, layout, imagery, approved copy, visual style — belongs to the operator, the Campaign Build Brief and the accepted design source. Commerce behaviour — products, package IDs, prices, offers, shipping, routes, cart, checkout, payment and post-purchase — belongs to CampaignSpec/API values and the SDK. Before changing a campaign, read the normalized brief at `.campaign-runtime/input/campaign-build-brief.normalized.json` and the recorded decisions: `adapter_decisions`, `decisions` and `theme` in `.campaign-runtime/assembly-report.json`, and `assembly.template_decision_notes` in the Build Packet. The setup, build, Polish and QA prompts begin with a campaign intent summary, and every `next` result carries it (`intent_summary` in JSON); treat it as orientation, never as a source of prices or behaviour. When a revision is narrow, keep the recorded intent and ask only about missing choices that affect that revision. Ask the operator before saving brief answers.
12
+
11
13
  Core rules:
12
14
 
13
15
  - Treat CampaignSpec as campaign intent and the Campaigns API as live commerce truth.
@@ -2,6 +2,8 @@
2
2
 
3
3
  Use this context when working in a target campaign repo with Campaigns OS artifacts.
4
4
 
5
+ A NEXT campaign is one connected commerce journey: the pages the CampaignSpec declares, in its order, through product choice, checkout, post-purchase offers and receipt. Creative intent — audience, purpose, layout, imagery, approved copy, visual style — belongs to the operator, the Campaign Build Brief and the accepted design source. Commerce behaviour — products, package IDs, prices, offers, shipping, routes, cart, checkout, payment and post-purchase — belongs to CampaignSpec/API values and the SDK. Before changing a campaign, read the normalized brief at `.campaign-runtime/input/campaign-build-brief.normalized.json` and the recorded decisions: `adapter_decisions`, `decisions` and `theme` in `.campaign-runtime/assembly-report.json`, and `assembly.template_decision_notes` in the Build Packet. The setup, build, Polish and QA prompts begin with a campaign intent summary, and every `next` result carries it (`intent_summary` in JSON); treat it as orientation, never as a source of prices or behaviour. When a revision is narrow, keep the recorded intent and ask only about missing choices that affect that revision. Ask the operator before saving brief answers.
6
+
5
7
  - Read `campaign-runtime.build.json` first.
6
8
  - To pick up an existing campaign, run `npx --no-install campaigns-os readback .` and `npx --no-install campaigns-os next --packet campaign-runtime.build.json` before reading artifacts by hand; `next` blocks polish and QA again whenever the built output changes. The committed `.campaign-runtime/qa-verdict.json` keeps no order records or URLs: its `browser-test-order:<path>` assertions are the typed-card proof, and the full verdict is under `qa-output/`.
7
9
  - If `.campaign-runtime/build-context.json` or `.campaign-runtime/assembly-report.json` exists, read them before editing campaign files.
@@ -8,6 +8,8 @@ When this repository contains Campaigns OS artifacts, use them as the build hand
8
8
  - To pick up an existing campaign, run `npx --no-install campaigns-os readback .` and `npx --no-install campaigns-os next --packet campaign-runtime.build.json` before reading artifacts by hand; `next` blocks polish and QA again whenever the built output changes. The committed `.campaign-runtime/qa-verdict.json` keeps no order records or URLs: its `browser-test-order:<path>` assertions are the typed-card proof, and the full verdict is under `qa-output/`.
9
9
  - `.campaign-runtime/theme/theme-report.json`, when present, is optional brand-theme evidence. Generated `brand-theme.css` must load after `next-core.css`; missing or low-confidence theme is a warning/skipped reason, not permission to edit SDK-owned runtime surfaces.
10
10
 
11
+ A NEXT campaign is one connected commerce journey: the pages the CampaignSpec declares, in its order, through product choice, checkout, post-purchase offers and receipt. Creative intent — audience, purpose, layout, imagery, approved copy, visual style — belongs to the operator, the Campaign Build Brief and the accepted design source. Commerce behaviour — products, package IDs, prices, offers, shipping, routes, cart, checkout, payment and post-purchase — belongs to CampaignSpec/API values and the SDK. Before changing a campaign, read the normalized brief at `.campaign-runtime/input/campaign-build-brief.normalized.json` and the recorded decisions: `adapter_decisions`, `decisions` and `theme` in `.campaign-runtime/assembly-report.json`, and `assembly.template_decision_notes` in the Build Packet. The setup, build, Polish and QA prompts begin with a campaign intent summary, and every `next` result carries it (`intent_summary` in JSON); treat it as orientation, never as a source of prices or behaviour. When a revision is narrow, keep the recorded intent and ask only about missing choices that affect that revision. Ask the operator before saving brief answers.
12
+
11
13
  CampaignSpec validation is owned by the public `@nextcommerce/campaigns-os/campaign-spec` rules surfaced through doctor `spec.validation` findings; use structured rule/path detail when available.
12
14
 
13
15
  Preserve Campaign Cart SDK-owned commerce surfaces. Replace starter demo refs from CampaignSpec/API. Prepared AI/exported HTML must be converted into page-kit-ready source first: keep page-owned body markup, strip document wrappers, add YAML frontmatter, move shared CSS/assets into the campaign structure, and use Liquid helpers only for page-kit links/assets/includes. Landing/presell pages can preserve source design; checkout/upsell/downsell/receipt should use starter-template commerce surfaces as SDK contract references while campaign/source owns visual chrome. Reproduce the source design's own proof and urgency elements as designed (reviews, ratings, stock counters, countdowns, guarantees). They are the merchant's content; Campaigns OS does not review them, so do not remove, soften or flag them. Copy starter template families atomically with dependent pages, `_includes/`, `_layouts/`, `assets/css/`, and `assets/js/`; do not copy only checkout/receipt pages. Do not claim launch readiness until build, polish, deploy, and QA evidence are recorded.
@@ -8,6 +8,8 @@ alwaysApply: false
8
8
 
9
9
  Read Campaigns OS artifacts before editing campaign pages. To pick up an existing campaign, run `npx --no-install campaigns-os readback .` and `npx --no-install campaigns-os next --packet campaign-runtime.build.json` before reading artifacts by hand; `next` blocks polish and QA again whenever the built output changes. The committed `.campaign-runtime/qa-verdict.json` keeps no order records or URLs: its `browser-test-order:<path>` assertions are the typed-card proof, and the full verdict is under `qa-output/`. Treat CampaignSpec/API values as live commerce truth, starter-template contracts as SDK surface truth, and designed HTML/assets as visual/content intent. Treat CampaignSpec validation as owned by the public `@nextcommerce/campaigns-os/campaign-spec` rules surfaced through doctor `spec.validation` findings; use structured rule/path detail when available. If `context.theme` or `.campaign-runtime/theme/theme-report.json` exists, use it as optional brand-theme evidence; generated `brand-theme.css` must load after `next-core.css`, and missing/low-confidence theme is a warning or skipped reason, not permission to edit SDK-owned runtime surfaces.
10
10
 
11
+ A NEXT campaign is one connected commerce journey: the pages the CampaignSpec declares, in its order, through product choice, checkout, post-purchase offers and receipt. Creative intent — audience, purpose, layout, imagery, approved copy, visual style — belongs to the operator, the Campaign Build Brief and the accepted design source. Commerce behaviour — products, package IDs, prices, offers, shipping, routes, cart, checkout, payment and post-purchase — belongs to CampaignSpec/API values and the SDK. Before changing a campaign, read the normalized brief at `.campaign-runtime/input/campaign-build-brief.normalized.json` and the recorded decisions: `adapter_decisions`, `decisions` and `theme` in `.campaign-runtime/assembly-report.json`, and `assembly.template_decision_notes` in the Build Packet. The setup, build, Polish and QA prompts begin with a campaign intent summary, and every `next` result carries it (`intent_summary` in JSON); treat it as orientation, never as a source of prices or behaviour. When a revision is narrow, keep the recorded intent and ask only about missing choices that affect that revision. Ask the operator before saving brief answers.
12
+
11
13
  Do not carry over demo package, shipping, voucher, payment, tracking, footer, or SEO values. Prepared AI/exported HTML must be converted into page-kit-ready source first: keep page-owned body markup, strip document wrappers, add YAML frontmatter, move shared CSS/assets into the campaign structure, and use Liquid helpers only for page-kit links/assets/includes. Preserve prepared landing/presell source HTML when it is a real standalone design. For checkout/upsell/downsell/receipt, use starter-template commerce surfaces as SDK contract references while campaign/source owns visual chrome. Reproduce the source design's own proof and urgency elements as designed (reviews, ratings, stock counters, countdowns, guarantees). They are the merchant's content; Campaigns OS does not review them, so do not remove, soften or flag them. Copy starter template families atomically with dependent pages, `_includes/`, `_layouts/`, `assets/css/`, and `assets/js/`; do not copy only checkout/receipt pages. Emit SDK routing meta tags as campaign-root paths such as `/campaign-slug/upsell/`. Preserve SDK-owned checkout/cart/upsell/receipt surfaces. For `shop-three-step`, shipping is dynamic via `window.next.getShippingMethods()`.
12
14
 
13
15
  Before Polish becomes terminal or hands off to deploy/QA, install the package-owned Playwright browser with `npm run qa:install-browser`, serve the current build, and run `npx --no-install campaigns-os polish capture --packet campaign-runtime.build.json --base-url <served-current-build-url>`. After that checkpoint clears, QA uses the Campaigns OS Node/npm runner: `npx --no-install campaigns-os qa resolve`, then `npx --no-install campaigns-os qa run --browser --test-order common` against the tested URL. Test-order proof uses `npx --no-install campaigns-os qa run --test-order <common|checkout|decline|accept|both|full|explicit-path>` through the tested checkout and rendered upsell controls. `qa run` has no permission flag: coverage is its only control. 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 the checkout baseline, 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. Analytics correctness is two-phase: the campaign-root visit inventories declared providers/tags only, and the same canonical typed-card run proves Purchase only from the topology-recognized final receipt document after `--analytics-settle`. Missing/unrecognized receipts are manual-review warnings; recognized receipt no-signal and capture/settle errors block. Only the genuine recognized-receipt/no-signal failure is waivable; capture/settle errors are not. Checkout/upsell signals remain whole-journey parity evidence and cannot satisfy a silent receipt. Do not use `next.getCartData().cartLines` as cart-populated proof; use typed-card order read-back, `cart:updated` payload `items` / `summary.lines`, and rendered bundle DOM evidence. 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. External browser skills, SDK test-mode events, and direct backend orders are not launch proof. Campaigns OS proof is not merchant launch readiness; before launch, confirm production storefront URL, live payment methods, shipping markets, legal/support URLs, analytics expectations, and merchant-side configuration.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "package": "@nextcommerce/campaigns-os",
3
- "version": "1.52.0",
3
+ "version": "1.53.0",
4
4
  "status": "developer-preview",
5
5
  "contracts": {
6
6
  "campaign_spec": "4.2-4.3",
@@ -4406,7 +4406,7 @@
4406
4406
  ],
4407
4407
  "effect_test": "effects: start",
4408
4408
  "test_scope": "full",
4409
- "notes": "Intake: Build Packet, sidecars, agent context and the auto-opened run session. Not idempotent — a second `start` over an assembly report that carries stage evidence is refused without --force."
4409
+ "notes": "Intake: Build Packet, sidecars, agent context and the auto-opened run session. Not idempotent — a second `start` is refused without --force when the report carries stage evidence, stage history or operator decisions (recorded waivers, warning accepts, an applied theme or report evidence), or when the run would change the packet's recorded deploy target, preview URL, production URL, order-path depth or allowed-domains confirmation. To save brief answers or a CampaignSpec change without discarding anything, run `record brief` or `record spec` instead."
4410
4410
  },
4411
4411
  {
4412
4412
  "command": "start",
@@ -4565,7 +4565,7 @@
4565
4565
  ],
4566
4566
  "effect_test": "effects: start --force",
4567
4567
  "test_scope": "full",
4568
- "notes": "DESTRUCTIVE: --force overwrites an assembly report that carries stage evidence, printing the stage keys it cleared. This is the flag that makes `start` destructive; the base form refuses instead. It also regenerates a stale Design Source Package that this intake synthesized itself: the previous Assembly Report records origin \"synthesized\" and the package bytes still match it. An adopted or hand-edited package is never replaced. The declared writes already cover that path."
4568
+ "notes": "DESTRUCTIVE: --force overwrites an assembly report that carries stage evidence, stage history or operator decisions, and the packet's recorded deploy target, preview URL, production URL, order-path depth and allowed-domains confirmation, printing the stage keys and the operator state it cleared. It archives completed build, Polish and QA records into stage history, which is kept, and keeps each stage's record that its inputs changed. This is the flag that makes `start` destructive; the base form refuses instead. It also regenerates a stale Design Source Package that this intake synthesized itself: the previous Assembly Report records origin \"synthesized\" and the package bytes still match it. An adopted or hand-edited package is never replaced. The declared writes already cover that path."
4569
4569
  },
4570
4570
  {
4571
4571
  "command": "start",
@@ -4831,7 +4831,7 @@
4831
4831
  ],
4832
4832
  "effect_test": "effects: prepare-build",
4833
4833
  "test_scope": "full",
4834
- "notes": "Intake without doctor and without the agent-context install: the packet, the sidecars and the auto-opened session. Same stale-session closeout as `start`."
4834
+ "notes": "Intake without doctor and without the agent-context install: the packet, the sidecars and the auto-opened session. Same stale-session closeout as `start`. A second `prepare-build` is refused without --force when the report carries stage evidence, stage history or operator decisions (recorded waivers, warning accepts, an applied theme or report evidence), or when the run would change the packet's recorded deploy target, preview URL, production URL, order-path depth or allowed-domains confirmation. To save brief answers or a CampaignSpec change without discarding anything, run `record brief` or `record spec` instead."
4835
4835
  },
4836
4836
  {
4837
4837
  "command": "prepare-build",
@@ -5077,7 +5077,7 @@
5077
5077
  ],
5078
5078
  "effect_test": "effects: prepare-build --force",
5079
5079
  "test_scope": "full",
5080
- "notes": "DESTRUCTIVE: --force overwrites an assembly report that carries stage evidence, printing the stage keys it cleared. This is the flag that makes `prepare-build` destructive; the base form refuses instead. It also regenerates a stale Design Source Package that this intake synthesized itself: the previous Assembly Report records origin \"synthesized\" and the package bytes still match it. An adopted or hand-edited package is never replaced. The declared writes already cover that path. Everything else it writes is the base form's intake."
5080
+ "notes": "DESTRUCTIVE: --force overwrites an assembly report that carries stage evidence, stage history or operator decisions, and the packet's recorded deploy target, preview URL, production URL, order-path depth and allowed-domains confirmation, printing the stage keys and the operator state it cleared. It archives completed build, Polish and QA records into stage history, which is kept, and keeps each stage's record that its inputs changed. This is the flag that makes `prepare-build` destructive; the base form refuses instead. It also regenerates a stale Design Source Package that this intake synthesized itself: the previous Assembly Report records origin \"synthesized\" and the package bytes still match it. An adopted or hand-edited package is never replaced. The declared writes already cover that path. Everything else it writes is the base form's intake."
5081
5081
  },
5082
5082
  {
5083
5083
  "command": "build",
@@ -5223,7 +5223,7 @@
5223
5223
  ],
5224
5224
  "effect_test": "effects: build",
5225
5225
  "test_scope": "full",
5226
- "notes": "The intake alias for prepare-build + doctor: the same packet and sidecars, plus the doctor sidecar. It does not install the agent context."
5226
+ "notes": "The intake alias for prepare-build + doctor: the same packet and sidecars, plus the doctor sidecar. It does not install the agent context. A second `build` is refused without --force when the report carries stage evidence, stage history or operator decisions (recorded waivers, warning accepts, an applied theme or report evidence), or when the run would change the packet's recorded deploy target, preview URL, production URL, order-path depth or allowed-domains confirmation. To save brief answers or a CampaignSpec change without discarding anything, run `record brief` or `record spec` instead."
5227
5227
  },
5228
5228
  {
5229
5229
  "command": "build",
@@ -5491,7 +5491,7 @@
5491
5491
  ],
5492
5492
  "effect_test": "effects: build --force",
5493
5493
  "test_scope": "full",
5494
- "notes": "DESTRUCTIVE: --force overwrites an assembly report that carries stage evidence, printing the stage keys it cleared. This is the flag that makes `build` destructive; the base form refuses instead. It also regenerates a stale Design Source Package that this intake synthesized itself: the previous Assembly Report records origin \"synthesized\" and the package bytes still match it. An adopted or hand-edited package is never replaced. The declared writes already cover that path. Everything else it writes is the base form's intake."
5494
+ "notes": "DESTRUCTIVE: --force overwrites an assembly report that carries stage evidence, stage history or operator decisions, and the packet's recorded deploy target, preview URL, production URL, order-path depth and allowed-domains confirmation, printing the stage keys and the operator state it cleared. It archives completed build, Polish and QA records into stage history, which is kept, and keeps each stage's record that its inputs changed. This is the flag that makes `build` destructive; the base form refuses instead. It also regenerates a stale Design Source Package that this intake synthesized itself: the previous Assembly Report records origin \"synthesized\" and the package bytes still match it. An adopted or hand-edited package is never replaced. The declared writes already cover that path. Everything else it writes is the base form's intake."
5495
5495
  },
5496
5496
  {
5497
5497
  "command": "spec",
@@ -5683,6 +5683,195 @@
5683
5683
  "may_contact": []
5684
5684
  }
5685
5685
  },
5686
+ {
5687
+ "command": "record",
5688
+ "subcommand": "brief",
5689
+ "flags": [],
5690
+ "annotations": {
5691
+ "readOnlyHint": false,
5692
+ "destructiveHint": true,
5693
+ "openWorldHint": false,
5694
+ "idempotentHint": true
5695
+ },
5696
+ "tier": "C",
5697
+ "writes": [
5698
+ {
5699
+ "path": "{packet}",
5700
+ "when": "the save changes the brief (outcome saved or saved_with_invalidation) — build_brief's mode, status, input_path, question and gate counts and brief bindings",
5701
+ "observed_in": [
5702
+ "no_session",
5703
+ "ambient_session",
5704
+ "stale_session",
5705
+ "lifecycle_log",
5706
+ "persisted_consent"
5707
+ ]
5708
+ },
5709
+ {
5710
+ "path": "{target}/.campaign-runtime/input/*",
5711
+ "when": "the save changes the brief (outcome saved or saved_with_invalidation) — the normalized brief, re-derived from the saved brief file",
5712
+ "observed_in": [
5713
+ "no_session",
5714
+ "ambient_session",
5715
+ "stale_session",
5716
+ "lifecycle_log",
5717
+ "persisted_consent"
5718
+ ]
5719
+ },
5720
+ {
5721
+ "path": "{target}/.campaign-runtime/build-context.json",
5722
+ "when": "the save changes the brief (outcome saved or saved_with_invalidation) — the brief summary, its bindings and the brief's prompts",
5723
+ "observed_in": [
5724
+ "no_session",
5725
+ "ambient_session",
5726
+ "stale_session",
5727
+ "lifecycle_log",
5728
+ "persisted_consent"
5729
+ ]
5730
+ },
5731
+ {
5732
+ "path": "{target}/.campaign-runtime/assembly-report.json",
5733
+ "when": "the save changes the brief (outcome saved or saved_with_invalidation) — the brief summary and bindings, and stages.prepare_build re-derived from the brief's blockers; on a material change (saved_with_invalidation) build, Polish and QA become required (QA alone when only qa_policy changed), each recording input_change and keeping the record it replaced in its history, and evidence[] gains a line",
5734
+ "observed_in": [
5735
+ "no_session",
5736
+ "ambient_session",
5737
+ "stale_session",
5738
+ "lifecycle_log",
5739
+ "persisted_consent"
5740
+ ]
5741
+ },
5742
+ {
5743
+ "path": "{target}/.campaign-runtime/doctor-output.json",
5744
+ "when": "a save changes the brief (outcome saved or saved_with_invalidation) and a retained doctor sidecar exists — the write postdates the doctor snapshot, so it is stamped stale",
5745
+ "observed_in": [
5746
+ "no_session",
5747
+ "ambient_session",
5748
+ "stale_session",
5749
+ "lifecycle_log",
5750
+ "persisted_consent"
5751
+ ]
5752
+ },
5753
+ {
5754
+ "path": "{lifecycle-journal}",
5755
+ "when": "a lifecycle journal is selected for the invocation — an active run session, --lifecycle-journal, or CAMPAIGNS_OS_LIFECYCLE_LOG — on every outcome, an unchanged or refused save included; only --dry-run, and a flag refused before the command runs (such as an unknown flag), append no entry",
5756
+ "observed_in": [
5757
+ "ambient_session",
5758
+ "lifecycle_log"
5759
+ ]
5760
+ }
5761
+ ],
5762
+ "sends": [],
5763
+ "effect_test": "effects: record brief",
5764
+ "test_scope": "full",
5765
+ "notes": "Destructive: a save whose brief material changed makes build, Polish and QA owed again (QA alone when only qa_policy changed); each superseded record moves to that stage's history, which keeps the last five per stage and is never used as current proof. Setup and deploy records, waivers, warning accepts and the applied theme are kept. Idempotent: a repeat reads unchanged. A save whose brief content, mode, status, open questions, gates and file bytes all match what is bound reads unchanged and writes nothing but the lifecycle entry; a reformatted save (key order, an explicit default, `_meta` only) keeps every stage's evidence and status but may rewrite the brief files. Reads the brief file (--brief, otherwise campaign-build-brief.yaml, .yml or .json in the source root or the target) and the CampaignSpec; sends nothing. Refused, writing nothing but the lifecycle entry, when the brief file is over 1 MiB or names more than 512 design_authority entries, names a file Campaigns OS writes itself (the normalized brief, the Build Packet, the Build Context, the Assembly Report or the doctor sidecar), is missing or not a readable regular file, when the source root and the target hold differing brief files and no --brief names one, when the Build Context lacks the intake products evaluating the brief needs, when the CampaignSpec material differs from the one the Assembly Report binds (run record spec first), or when the CampaignSpec's active pages changed (that needs intake)."
5766
+ },
5767
+ {
5768
+ "command": "record",
5769
+ "subcommand": "brief",
5770
+ "flags": [
5771
+ "--dry-run"
5772
+ ],
5773
+ "annotations": {
5774
+ "readOnlyHint": true,
5775
+ "destructiveHint": false,
5776
+ "openWorldHint": false,
5777
+ "idempotentHint": true
5778
+ },
5779
+ "tier": "none",
5780
+ "writes": [],
5781
+ "sends": [],
5782
+ "effect_test": "effects: record brief --dry-run",
5783
+ "test_scope": "full",
5784
+ "notes": "record brief --dry-run runs every check the real invocation runs and prints the outcome and the files it would write; it writes nothing (no packet, brief, Build Context, report or stale stamp), takes no target lock, appends no lifecycle entry (the command is declared `dryRun` in src/invocation.mjs) and runs no stale-session closeout."
5785
+ },
5786
+ {
5787
+ "command": "record",
5788
+ "subcommand": "spec",
5789
+ "flags": [],
5790
+ "annotations": {
5791
+ "readOnlyHint": false,
5792
+ "destructiveHint": true,
5793
+ "openWorldHint": false,
5794
+ "idempotentHint": true
5795
+ },
5796
+ "tier": "C",
5797
+ "writes": [
5798
+ {
5799
+ "path": "{target}/.campaign-runtime/input/*",
5800
+ "when": "the refresh runs (outcome refreshed) and the brief re-derived from the brief file last saved by intake or `record brief` (or from the generated draft) differs from what is bound, or the normalized brief on disk no longer holds the re-derived brief's material — the normalized brief",
5801
+ "observed_in": [
5802
+ "no_session",
5803
+ "ambient_session",
5804
+ "stale_session",
5805
+ "lifecycle_log",
5806
+ "persisted_consent"
5807
+ ]
5808
+ },
5809
+ {
5810
+ "path": "{target}/.campaign-runtime/build-context.json",
5811
+ "when": "the refresh runs (outcome refreshed) — spec.hash, spec.material_hash and the active pages' labels; the brief summary and bindings when the re-derived brief is saved",
5812
+ "observed_in": [
5813
+ "no_session",
5814
+ "ambient_session",
5815
+ "stale_session",
5816
+ "lifecycle_log",
5817
+ "persisted_consent"
5818
+ ]
5819
+ },
5820
+ {
5821
+ "path": "{target}/.campaign-runtime/assembly-report.json",
5822
+ "when": "the refresh runs (outcome refreshed) — identity.spec_hash and identity.spec_material_hash become the CampaignSpec's; each build, Polish and QA record made against other CampaignSpec or brief content becomes required, recording input_change and keeping the record it replaced in its history; evidence[] gains a line",
5823
+ "observed_in": [
5824
+ "no_session",
5825
+ "ambient_session",
5826
+ "stale_session",
5827
+ "lifecycle_log",
5828
+ "persisted_consent"
5829
+ ]
5830
+ },
5831
+ {
5832
+ "path": "{target}/.campaign-runtime/doctor-output.json",
5833
+ "when": "a refresh runs (outcome refreshed) and a retained doctor sidecar exists — the write postdates the doctor snapshot, so it is stamped stale",
5834
+ "observed_in": [
5835
+ "no_session",
5836
+ "ambient_session",
5837
+ "stale_session",
5838
+ "lifecycle_log",
5839
+ "persisted_consent"
5840
+ ]
5841
+ },
5842
+ {
5843
+ "path": "{lifecycle-journal}",
5844
+ "when": "a lifecycle journal is selected for the invocation — an active run session, --lifecycle-journal, or CAMPAIGNS_OS_LIFECYCLE_LOG — on every outcome, an unchanged or refused refresh included; only --dry-run, and a flag refused before the command runs (such as an unknown flag), append no entry",
5845
+ "observed_in": [
5846
+ "ambient_session",
5847
+ "lifecycle_log"
5848
+ ]
5849
+ }
5850
+ ],
5851
+ "sends": [],
5852
+ "effect_test": "effects: record spec",
5853
+ "test_scope": "full",
5854
+ "notes": "Destructive: build, Polish and QA records made against earlier CampaignSpec or brief content become owed again, each moving to that stage's history, which keeps the last five per stage and is never used as current proof. Waivers, warning accepts, the applied theme and deploy settings are kept. Idempotent: a repeat reads unchanged. When the bound CampaignSpec material is the spec's and no stage's recorded CampaignSpec or brief content differs from the current inputs it reads unchanged and writes nothing but the lifecycle entry. The Build Packet is read, not written; the brief is re-read from the brief file last saved by intake or `record brief`, never rediscovered. For a saved-Map or gateway packet it compares the copy intake fetched and sends nothing. Refused, writing nothing but the lifecycle entry, when the CampaignSpec cannot be read as a JSON object, names another campaign (local_spec_id or map_id) than the Build Packet or the Assembly Report binds, or its active pages (ids, order or routes) changed (that needs intake), or when that brief file is over 1 MiB or names more than 512 design_authority entries, names a file Campaigns OS writes itself, or is missing or not a readable regular file."
5855
+ },
5856
+ {
5857
+ "command": "record",
5858
+ "subcommand": "spec",
5859
+ "flags": [
5860
+ "--dry-run"
5861
+ ],
5862
+ "annotations": {
5863
+ "readOnlyHint": true,
5864
+ "destructiveHint": false,
5865
+ "openWorldHint": false,
5866
+ "idempotentHint": true
5867
+ },
5868
+ "tier": "none",
5869
+ "writes": [],
5870
+ "sends": [],
5871
+ "effect_test": "effects: record spec --dry-run",
5872
+ "test_scope": "full",
5873
+ "notes": "record spec --dry-run runs every check the real invocation runs and prints the outcome and the files it would write; it writes nothing (no brief, Build Context, report or stale stamp), takes no target lock, appends no lifecycle entry (the command is declared `dryRun` in src/invocation.mjs) and runs no stale-session closeout."
5874
+ },
5686
5875
  {
5687
5876
  "command": "record",
5688
5877
  "subcommand": "setup",
@@ -5775,7 +5964,7 @@
5775
5964
  "writes": [
5776
5965
  {
5777
5966
  "path": "{target}/.campaign-runtime/assembly-report.json",
5778
- "when": "the record passes every check — stages.assembly becomes completed with build_fingerprint set to doctor's derived.build_output_fingerprint.value, and stages.polish becomes required unless its evidence is bound to that exact output",
5967
+ "when": "the record passes every check — stages.assembly becomes completed with build_fingerprint set to doctor's derived.build_output_fingerprint.value and the brief and CampaignSpec content it was made against, keeping the record it replaced in its history. When the replaced completed record was made against other brief or CampaignSpec content, the record writes input_change and stages.polish and stages.qa become required; otherwise stages.polish becomes required unless its evidence is bound to that exact output. Output identical to the build that change superseded keeps input_change, unless --deviation-reason records the operator's reason (status completed_with_warnings)",
5779
5968
  "observed_in": [
5780
5969
  "no_session",
5781
5970
  "ambient_session",
@@ -5807,7 +5996,7 @@
5807
5996
  "sends": [],
5808
5997
  "effect_test": "effects: record build",
5809
5998
  "test_scope": "full",
5810
- "notes": "Destructive: it replaces stages.assembly and, when the output changed, resets stages.polish to required. The fingerprint is doctor's, never typed. Refused, writing nothing, when next answers prepare-build, when doctor cannot compute the fingerprint (no built output), when setup is still required or stages.setup is not terminal, or when the report would fail its schema. The Build Context is read, not written."
5999
+ "notes": "Destructive: it replaces stages.assembly (the replaced record is kept in its history) and, when the output changed, resets stages.polish to required. When it detects a brief or CampaignSpec change since the replaced record, it writes input_change and also makes Polish and QA required. Re-recording output identical to the build an input change superseded keeps the build owed unless --deviation-reason records the operator's reason. The fingerprint is doctor's, never typed. Refused, writing nothing but the lifecycle entry, when next answers prepare-build, when doctor cannot compute the fingerprint (no built output), when setup is still required or stages.setup is not terminal, or when the report would fail its schema. The Build Context is read, not written."
5811
6000
  },
5812
6001
  {
5813
6002
  "command": "record",
@@ -5842,7 +6031,7 @@
5842
6031
  "writes": [
5843
6032
  {
5844
6033
  "path": "{target}/.campaign-runtime/assembly-report.json",
5845
- "when": "the record passes every check — stages.polish becomes the file's status (completed, blocked with blockers, or skipped with skip_reason) and evidence, bound to doctor's current fingerprint; theme.repair_loop_defect when the file sets it",
6034
+ "when": "the record passes every check — stages.polish becomes the file's status (completed, blocked with blockers, or skipped with skip_reason) and evidence, bound to doctor's current fingerprint and, for a completed status, the brief and CampaignSpec content it was made against, keeping the record it replaced in its history; theme.repair_loop_defect when the file sets it",
5846
6035
  "observed_in": [
5847
6036
  "no_session",
5848
6037
  "ambient_session",
@@ -5874,7 +6063,7 @@
5874
6063
  "sends": [],
5875
6064
  "effect_test": "effects: record polish",
5876
6065
  "test_scope": "full",
5877
- "notes": "Destructive: it replaces stages.polish (the captured page_load is kept). Refused, writing nothing, when build is not recorded for the current output, when the --evidence file has a shape error (named by field), or, for a completed status, when the polish gate doctor evaluates would not pass on the report it would write. The Build Context is read, not written."
6066
+ "notes": "Destructive: it replaces stages.polish (the captured page_load is kept, and the replaced record is kept in its history). A completed record stamps the brief and CampaignSpec content it was made against; after an input change it stays owed until a new capture postdates that change. Refused, writing nothing but the lifecycle entry, when build is not recorded for the current output or build is owed again after a brief or CampaignSpec change or its record does not say which content it was made against, when the --evidence file has a shape error (named by field), or, for a completed status, when the polish gate doctor evaluates would not pass on the report it would write. The Build Context is read, not written."
5878
6067
  },
5879
6068
  {
5880
6069
  "command": "record",
@@ -5941,7 +6130,7 @@
5941
6130
  "sends": [],
5942
6131
  "effect_test": "effects: record theme",
5943
6132
  "test_scope": "full",
5944
- "notes": "Destructive: it replaces report.theme's status, css_path, load_order, commerce_pages, evidence and waiver. Refused, writing nothing, when build is not recorded for the current output, when the campaign ships no commerce pages, when a built commerce page that loads next-core.css does not load brand-theme.css (or checkout-brand.css) after it, or links one that is not in the built output, or when no built commerce page loads next-core.css. The Build Context is read, not written."
6133
+ "notes": "Destructive: it replaces report.theme's status, css_path, load_order, commerce_pages, evidence and waiver. Refused, writing nothing but the lifecycle entry, when build is not recorded for the current output or build is owed again after a brief or CampaignSpec change or its record does not say which content it was made against, when the campaign ships no commerce pages, when a built commerce page that loads next-core.css does not load brand-theme.css (or checkout-brand.css) after it, or links one that is not in the built output, or when no built commerce page loads next-core.css. The Build Context is read, not written."
5945
6134
  },
5946
6135
  {
5947
6136
  "command": "record",
@@ -6033,7 +6222,7 @@
6033
6222
  ],
6034
6223
  "effect_test": "effects: record deploy",
6035
6224
  "test_scope": "full",
6036
- "notes": "Destructive: it replaces stages.deploy and the packet's deploy.preview_url. The requests stay on this machine: --base-url must be a loopback origin. Refused, writing nothing, when the packet is not local-serve, the URL is not loopback or not the campaign's route root, a built page does not answer 2xx, polish is not recorded, the built output changed since build was recorded, or the theme gate is blocked. The Build Context is read, not written."
6225
+ "notes": "Destructive: it replaces stages.deploy and the packet's deploy.preview_url. The requests stay on this machine: --base-url must be a loopback origin. Refused, writing nothing but the lifecycle entry, when the packet is not local-serve, the URL is not loopback or not the campaign's route root, a built page does not answer 2xx, polish is not recorded, build or Polish is owed again after a brief or CampaignSpec change or its record does not say which content it was made against, the built output changed since build was recorded, or the theme gate is blocked. The Build Context is read, not written."
6037
6226
  },
6038
6227
  {
6039
6228
  "command": "record",