@nextcommerce/campaigns-os 1.41.2 → 1.43.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/AGENTS.md +4 -2
  2. package/CHANGELOG.md +629 -0
  3. package/README.md +8 -6
  4. package/agents/claude/CLAUDE.md +5 -1
  5. package/campaign-spec/dist/types.d.ts +2 -0
  6. package/contracts/agent-relevant-change-policy.v1.json +5 -0
  7. package/contracts/effects.v1.json +118 -25
  8. package/contracts/migration-sidecar-bundle.v0.json +9 -0
  9. package/contracts/release-ledger.json +1424 -0
  10. package/contracts/supported-surface.json +12 -11
  11. package/docs/build-packet.md +120 -8
  12. package/docs/campaigns-os-build-flow.md +3 -2
  13. package/docs/design-source-package.md +89 -15
  14. package/docs/effects.md +83 -2
  15. package/docs/local-setup.md +51 -0
  16. package/docs/migration-sidecar-bundle.md +6 -1
  17. package/docs/orientation-contract-reference.md +1 -1
  18. package/docs/progress-snapshots.md +16 -6
  19. package/docs/qa-and-test-orders.md +157 -17
  20. package/docs/release-ledger-authoring-guide.md +6 -4
  21. package/docs/runtime-readiness.md +1 -1
  22. package/docs/skills-revision.md +10 -10
  23. package/package.json +3 -2
  24. package/schemas/campaign-runtime-assembly-report.v0.schema.json +6 -1
  25. package/schemas/campaign-runtime-build-packet.v0.schema.json +6 -1
  26. package/schemas/campaign-spec.v4.schema.json +4 -0
  27. package/schemas/campaigns-os-progress-snapshot.v0.schema.json +1 -0
  28. package/schemas/campaigns-os-qa-verdict-sidecar.v0.schema.json +1 -0
  29. package/schemas/campaigns-os-qa-verdict.v0.schema.json +1 -0
  30. package/schemas/campaigns-os-run-record.v0.schema.json +1 -0
  31. package/skills/campaign-lifecycle-orientation/SKILL.md +13 -8
  32. package/skills/campaign-readback-classification/SKILL.md +3 -3
  33. package/skills/campaign-run-evidence/SKILL.md +8 -6
  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 +17 -4
  37. package/skills/next-campaigns-os/references/session-intake.md +4 -4
  38. package/skills/next-campaigns-os-setup/SKILL.md +3 -3
  39. package/skills/next-campaigns-polish/SKILL.md +3 -3
  40. package/skills/next-campaigns-qa/SKILL.md +10 -9
  41. package/skills.json +11 -11
  42. package/src/build-brief.mjs +6 -4
  43. package/src/built-script-syntax.mjs +480 -0
  44. package/src/campaigns-api-key.mjs +99 -0
  45. package/src/cli-helpers.mjs +118 -0
  46. package/src/cli.mjs +796 -6963
  47. package/src/design-source-package.mjs +1 -1
  48. package/src/design-source-publication.mjs +898 -0
  49. package/src/diagnostic.mjs +2 -1
  50. package/src/directory-lock.mjs +270 -0
  51. package/src/doctor/checks.mjs +4415 -0
  52. package/src/doctor/inspect.mjs +636 -0
  53. package/src/doctor/next-step.mjs +731 -0
  54. package/src/finding-cause.mjs +14 -10
  55. package/src/install-invocation.mjs +29 -0
  56. package/src/invocation.mjs +179 -0
  57. package/src/lifecycle.mjs +5 -4
  58. package/src/polish-node.mjs +5 -2
  59. package/src/private-template-source.mjs +1 -1
  60. package/src/progress-node.mjs +9 -37
  61. package/src/progress.mjs +5 -3
  62. package/src/proof-policy.mjs +1 -1
  63. package/src/qa-analytics-correctness.mjs +3 -0
  64. package/src/qa-binding-evidence.mjs +76 -11
  65. package/src/qa-browser.mjs +778 -77
  66. package/src/qa-build-scope.mjs +47 -0
  67. package/src/qa-node.mjs +276 -39
  68. package/src/qa-publish.mjs +4 -0
  69. package/src/qa-sidecar.mjs +2 -0
  70. package/src/qa-verdict-discovery.mjs +11 -0
  71. package/src/qa-verdict-publish.mjs +1 -0
  72. package/src/qa-verdict.mjs +8 -1
  73. package/src/readback.mjs +2 -1
  74. package/src/run-record-closeout.mjs +3 -4
  75. package/src/run-record.mjs +4 -0
  76. package/src/sidecar-bundle.mjs +21 -0
  77. package/src/source-html-intake.mjs +1 -1
  78. package/src/source-html-manifest.mjs +9 -2
  79. package/src/spec-source-identity.mjs +44 -0
  80. package/src/stage-ledger.mjs +32 -1
  81. package/src/target-lock.mjs +54 -0
  82. package/src/template-brand-contract.mjs +17 -1
  83. package/src/tooling-setup.mjs +160 -0
package/README.md CHANGED
@@ -5,8 +5,8 @@ Campaigns OS is the developer toolkit for agent-assisted campaign builds on [Nex
5
5
  This toolkit gives campaign developers and AI coding tools a clear path for assembling one from prepared page files:
6
6
 
7
7
  1. Configure the campaign in the Next Commerce dashboard (Campaigns App).
8
- 2. Create or review the Campaign Map in [Campaign Map Builder](https://campaign-map.nextcommerce.com).
9
- 3. Export a local CampaignSpec JSON.
8
+ 2. Use the current saved Map in [Campaign Map Builder](https://campaign-map.nextcommerce.com), or have the coding agent author a [local CampaignSpec](docs/build-packet.md#local-spec-entry) from the brief and verified campaign values.
9
+ 3. Keep the CampaignSpec JSON with its saved Map ID or stable `local_spec_id`, plus its public route slug.
10
10
  4. Bring prepared HTML/CSS/assets for the campaign pages.
11
11
  5. Provide or generate a [Campaign Build Brief](./docs/campaign-build-brief.md) for merchandising/design presentation decisions.
12
12
  6. Create and doctor a Build Packet.
@@ -119,7 +119,9 @@ npx --no-install campaigns-os next --packet ./campaign-runtime.build.json --json
119
119
 
120
120
  `--map-id <id>` starts from a map saved in Campaign Map Builder (add
121
121
  `--proxy-base <origin>` when the map was saved on a non-production map store);
122
- `--spec <campaignspec.json>` starts from a local export instead. `--source` is
122
+ `--spec <campaignspec.json>` starts from a local export or an agent-authored
123
+ [local spec](docs/build-packet.md#local-spec-entry) instead. Local-spec identity
124
+ requires a reviewed 1.43.0-or-later release. `--source` is
123
125
  always required: the folder of prepared HTML/CSS/assets for the pages you are
124
126
  building, with a source manifest that carries desktop and mobile screenshot
125
127
  proof for each designed page
@@ -196,8 +198,8 @@ are template stock instead — no bespoke design, the starter family *is* the
196
198
  design — there is no screenshot to honestly supply. Declare those pages out of
197
199
  source scope (a manifest `skip_reason` entry, or CampaignSpec
198
200
  `build_scope.mode: "partial"`): intake records them as template stock, demands
199
- no design source for them, and the build stage materialises each from the
200
- locked family's own page. A family that publishes Template Reference proof
201
+ no design source for them, and leaves them unbuilt unless the operator opts in
202
+ per page to materializing the locked family's stock. A family that publishes Template Reference proof
201
203
  (today `apollo`) covers them with `template_baseline`; every other family
202
204
  records an accepted Source Gap and intake lands at `ready_with_gaps`. See
203
205
  [Template-stock pages: the family decides](docs/design-source-package.md#template-stock-pages-the-family-decides).
@@ -228,7 +230,7 @@ Then ask your AI tool to continue from the emitted handoff. Fresh target repos u
228
230
 
229
231
  ## Source Files
230
232
 
231
- The current source adapter is `html_funnel`: bring prepared HTML/CSS/assets for the campaign pages, plus a local exported CampaignSpec from Campaign Map Builder.
233
+ The current source adapter is `html_funnel`: bring prepared HTML/CSS/assets for the campaign pages, plus a CampaignSpec exported from Campaign Map Builder or authored by the coding agent through the [local-spec entry](docs/build-packet.md#local-spec-entry).
232
234
 
233
235
  For raw AI-generated or exported static HTML, "prepared" means page-kit-ready
234
236
  source, not a browser document dropped in unchanged and not a wholesale Liquid
@@ -1,6 +1,10 @@
1
1
  # Campaigns OS Agent Context
2
2
 
3
- You are helping assemble a NEXT campaign through Campaigns OS. Start from the Build Packet, not from private runtime source.
3
+ You are helping assemble a NEXT campaign through Campaigns OS. Use the
4
+ `next-campaigns-os` skill from this project's pinned toolkit. When a Build
5
+ Packet exists, read it and follow `next`. Check the loaded skill's bundle
6
+ revision and restart the session if it differs from the
7
+ project copy. Do not use private runtime source as the campaign's starting point.
4
8
 
5
9
  Core rules:
6
10
 
@@ -593,6 +593,8 @@ export interface AnalyticsContract {
593
593
  * local-experimental.
594
594
  */
595
595
  export interface SpecIdentity {
596
+ /** Stable agent-authored identity when there is no saved Map. */
597
+ local_spec_id?: string;
596
598
  map_id?: string;
597
599
  source?: string;
598
600
  id?: string;
@@ -67,6 +67,11 @@
67
67
  "class": "cli_surface",
68
68
  "_note": "The agent-facing entry points (skill install, agent context, the tooling-status revision check) are reachable from the supported argv surface, so a change under src/agent/ is a CLI-surface change even though the rest of src/ is unsupported implementation. This rule sits ahead of the broad src/ ignore on purpose: the ignore's reason is 'implementation reachable only through declared package exports', which is exactly what this subtree is not."
69
69
  },
70
+ {
71
+ "match": { "kind": "prefix", "value": "src/doctor/" },
72
+ "class": "cli_surface",
73
+ "_note": "Doctor checks are kernel-declared behavior reachable from the supported argv surface (`doctor`, `next`, `start`, `prepare-build` and `qa run` read them), so a change under src/doctor/ is a CLI-surface change even though the rest of src/ is unsupported implementation. This rule sits ahead of the broad src/ ignore on purpose, as the src/agent/ rule does. The shared helper modules beside it under src/ (src/install-invocation.mjs, src/cli-helpers.mjs, src/campaigns-api-key.mjs) remain implementation under the broad src/ ignore."
74
+ },
70
75
  {
71
76
  "match": { "kind": "prefix", "value": "agents/" },
72
77
  "class": "documentation",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schema": "campaigns-os-effects/v1",
3
3
  "_note": "The declared effect of every supported invocation: what it writes, what it sends, and the test case that proves it. One row per command, per subcommand and per effect-changing flag (vocabulary.effect_changing_flags). A row is not publishable without its effect test — scripts/check-effects.mjs refuses one, and refuses a flag from that list that a help usage line carries and no row declares. Read this with docs/effects.md.",
4
- "contract_version": "1.1.0",
4
+ "contract_version": "1.2.0",
5
5
  "vocabulary": {
6
6
  "annotations": {
7
7
  "readOnlyHint": "The invocation changes nothing: no file under the target, the working directory or the user's machine, and no request off the machine. The command-lifecycle journal counts — a journal append is a write like any other, so every readOnlyHint: true row is an invocation the CLI exempts from lifecycle capture (the converse does not hold: some journal-exempt invocations, such as demo and the --no-write forms, still write other declared files).",
@@ -26,7 +26,7 @@
26
26
  "{proxy-base}": "The endpoint --proxy-base names, or the canonical NEXT endpoint when it does not.",
27
27
  "{base-url}": "The campaign under test, as --base-url names it or as the packet derives it.",
28
28
  "{map-id}": "The saved Map the CampaignSpec identifies.",
29
- "{playwright-download-host}": "Where Playwright fetches browser builds from: PLAYWRIGHT_DOWNLOAD_HOST when set, else the Playwright CDN (cdn.playwright.dev). The one destination in this file that is not a Campaigns OS endpoint — `qa install-browser` is the one supported invocation that downloads from a third party."
29
+ "{playwright-download-host}": "Where Playwright fetches browser builds from: PLAYWRIGHT_DOWNLOAD_HOST when set, else the Playwright CDN (cdn.playwright.dev). The third-party browser download used by `qa install-browser` and `tooling setup`."
30
30
  },
31
31
  "conditions": {
32
32
  "no_session": "No run session at the target or the working directory.",
@@ -236,7 +236,7 @@
236
236
  "sends": [],
237
237
  "effect_test": "effects: page-kit sync --dry-run",
238
238
  "test_scope": "full",
239
- "notes": "page-kit sync --dry-run runs every check the real invocation runs and prints what it would do; it writes nothing under the target, appends no lifecycle entry (DRY_RUN_COMMANDS) and runs no stale-session closeout."
239
+ "notes": "page-kit sync --dry-run runs every check the real invocation runs and prints what it would do; it writes nothing under the target, appends no lifecycle entry (the command is declared `dryRun` in src/invocation.mjs) and runs no stale-session closeout."
240
240
  },
241
241
  {
242
242
  "command": "spec",
@@ -255,7 +255,7 @@
255
255
  "sends": [],
256
256
  "effect_test": "effects: spec derive --dry-run",
257
257
  "test_scope": "full",
258
- "notes": "spec derive --dry-run runs every check the real invocation runs and prints what it would do; it writes nothing under the target, appends no lifecycle entry (DRY_RUN_COMMANDS) and runs no stale-session closeout."
258
+ "notes": "spec derive --dry-run runs every check the real invocation runs and prints what it would do; it writes nothing under the target, appends no lifecycle entry (the command is declared `dryRun` in src/invocation.mjs) and runs no stale-session closeout."
259
259
  },
260
260
  {
261
261
  "command": "install-skills",
@@ -274,7 +274,7 @@
274
274
  "sends": [],
275
275
  "effect_test": "effects: install-skills --dry-run",
276
276
  "test_scope": "full",
277
- "notes": "install-skills --dry-run runs every check the real invocation runs and prints what it would do; it writes nothing under the target, appends no lifecycle entry (DRY_RUN_COMMANDS) and runs no stale-session closeout."
277
+ "notes": "install-skills --dry-run runs every check the real invocation runs and prints what it would do; it writes nothing under the target, appends no lifecycle entry (the command is declared `dryRun` in src/invocation.mjs) and runs no stale-session closeout."
278
278
  },
279
279
  {
280
280
  "command": "install-agent-context",
@@ -293,7 +293,7 @@
293
293
  "sends": [],
294
294
  "effect_test": "effects: install-agent-context --dry-run",
295
295
  "test_scope": "full",
296
- "notes": "install-agent-context --dry-run runs every check the real invocation runs and prints what it would do; it writes nothing under the target, appends no lifecycle entry (DRY_RUN_COMMANDS) and runs no stale-session closeout."
296
+ "notes": "install-agent-context --dry-run runs every check the real invocation runs and prints what it would do; it writes nothing under the target, appends no lifecycle entry (the command is declared `dryRun` in src/invocation.mjs) and runs no stale-session closeout."
297
297
  },
298
298
  {
299
299
  "command": "qa",
@@ -312,7 +312,7 @@
312
312
  "sends": [],
313
313
  "effect_test": "effects: qa publish --dry-run",
314
314
  "test_scope": "full",
315
- "notes": "qa publish --dry-run runs every check the real invocation runs and prints what it would do; it writes nothing under the target, appends no lifecycle entry (DRY_RUN_COMMANDS) and runs no stale-session closeout."
315
+ "notes": "qa publish --dry-run runs every check the real invocation runs and prints what it would do; it writes nothing under the target, appends no lifecycle entry (the command is declared `dryRun` in src/invocation.mjs) and runs no stale-session closeout. Local-spec packets are refused before publication because they have no saved Map destination."
316
316
  },
317
317
  {
318
318
  "command": "checkpoint",
@@ -331,7 +331,7 @@
331
331
  "sends": [],
332
332
  "effect_test": "effects: checkpoint waive --dry-run",
333
333
  "test_scope": "full",
334
- "notes": "checkpoint waive --dry-run runs every check the real invocation runs and prints what it would do; it writes nothing under the target, appends no lifecycle entry (DRY_RUN_COMMANDS) and runs no stale-session closeout."
334
+ "notes": "checkpoint waive --dry-run runs every check the real invocation runs and prints what it would do; it writes nothing under the target, appends no lifecycle entry (the command is declared `dryRun` in src/invocation.mjs) and runs no stale-session closeout."
335
335
  },
336
336
  {
337
337
  "command": "theme",
@@ -350,7 +350,7 @@
350
350
  "sends": [],
351
351
  "effect_test": "effects: theme waive --dry-run",
352
352
  "test_scope": "full",
353
- "notes": "theme waive --dry-run runs every check the real invocation runs and prints what it would do; it writes nothing under the target, appends no lifecycle entry (DRY_RUN_COMMANDS) and runs no stale-session closeout."
353
+ "notes": "theme waive --dry-run runs every check the real invocation runs and prints what it would do; it writes nothing under the target, appends no lifecycle entry (the command is declared `dryRun` in src/invocation.mjs) and runs no stale-session closeout."
354
354
  },
355
355
  {
356
356
  "command": "run-record",
@@ -369,7 +369,7 @@
369
369
  "sends": [],
370
370
  "effect_test": "effects: run-record --dry-run",
371
371
  "test_scope": "full",
372
- "notes": "run-record --dry-run runs every check the real invocation runs and prints what it would do; it writes nothing under the target, appends no lifecycle entry (DRY_RUN_COMMANDS) and runs no stale-session closeout."
372
+ "notes": "run-record --dry-run runs every check the real invocation runs and prints what it would do; it writes nothing under the target, appends no lifecycle entry (the command is declared `dryRun` in src/invocation.mjs) and runs no stale-session closeout."
373
373
  },
374
374
  {
375
375
  "command": "run-record",
@@ -2066,7 +2066,7 @@
2066
2066
  {
2067
2067
  "destination": "{proxy-base}/api/qa/verdicts",
2068
2068
  "what": "the QA verdict for the attempt, published to the QA portal",
2069
- "when": "publish is not opted out (--no-post-verdict) and the consent seam allows it — Run Telemetry consent on for the endpoint, or a portal-managed campaign",
2069
+ "when": "publish is not opted out (--no-post-verdict) and the consent seam allows it — Run Telemetry consent on for the endpoint, or a portal-managed campaign; never for a local-spec packet",
2070
2070
  "requires_consent": true,
2071
2071
  "observed_in": [
2072
2072
  "persisted_consent"
@@ -2091,7 +2091,7 @@
2091
2091
  {
2092
2092
  "destination": "{proxy-base}/api/progress",
2093
2093
  "what": "the stage progress observation — the snapshot just captured locally, POSTed under X-Campaign-Key",
2094
- "when": "Run Telemetry consent is on for the endpoint, --no-remit was not given, and the packet's campaign key resolves",
2094
+ "when": "Run Telemetry consent is on for the endpoint, --no-remit was not given, and the packet's campaign key resolves; never for a local-spec packet",
2095
2095
  "requires_consent": true,
2096
2096
  "observed_in": [
2097
2097
  "persisted_consent"
@@ -2100,7 +2100,7 @@
2100
2100
  ],
2101
2101
  "effect_test": "effects: qa run",
2102
2102
  "test_scope": "full",
2103
- "notes": "Destructive and not idempotent: each attempt overwrites the committed verdict sidecar and the report's QA stage, and a ready verdict clears the ambient run session. It SENDS on a blocked attempt too: the verdict is published to {proxy-base}/api/qa/verdicts and the QA stage progress observation to {proxy-base}/api/progress, both under persisted consent for the endpoint. Publishing rides the telemetry consent seam rather than a flag of its own, so the four consent-off conditions saw neither request."
2103
+ "notes": "Destructive and not idempotent: each attempt overwrites the committed verdict sidecar and the report's QA stage, and a ready verdict clears the ambient run session. It SENDS on a blocked attempt too: the verdict is published to {proxy-base}/api/qa/verdicts and the QA stage progress observation to {proxy-base}/api/progress, both under persisted consent for the endpoint. Publishing rides the telemetry consent seam rather than a flag of its own, so the four consent-off conditions saw neither request. Local-spec packets retain verdicts in the repository and never send a QA verdict or progress to the Map portal, even with --post-verdict."
2104
2104
  },
2105
2105
  {
2106
2106
  "command": "qa",
@@ -2191,7 +2191,7 @@
2191
2191
  {
2192
2192
  "destination": "{proxy-base}/api/qa/verdicts",
2193
2193
  "what": "the QA verdict for the attempt, published to the QA portal",
2194
- "when": "publish is not opted out (--no-post-verdict) and the consent seam allows it — Run Telemetry consent on for the endpoint, or a portal-managed campaign",
2194
+ "when": "publish is not opted out (--no-post-verdict) and the consent seam allows it — Run Telemetry consent on for the endpoint, or a portal-managed campaign; never for a local-spec packet",
2195
2195
  "requires_consent": true,
2196
2196
  "observed_in": [
2197
2197
  "persisted_consent"
@@ -2216,7 +2216,7 @@
2216
2216
  ],
2217
2217
  "effect_test": "effects: qa run --no-remit",
2218
2218
  "test_scope": "full",
2219
- "notes": "The base `qa run` row's writes, without any remit: --no-remit stops the progress observation and the Run Record remit. The verdict publish is NOT a remit and is not gated by the flag — it still posts to the QA portal under the consent seam, which --no-post-verdict is the opt-out from."
2219
+ "notes": "The base `qa run` row's writes, without any remit: --no-remit stops the progress observation and the Run Record remit. The verdict publish is NOT a remit and is not gated by the flag — it still posts to the QA portal under the consent seam, which --no-post-verdict is the opt-out from. Local-spec packets retain verdicts in the repository and never send a QA verdict or progress to the Map portal, even with --post-verdict."
2220
2220
  },
2221
2221
  {
2222
2222
  "command": "qa",
@@ -2323,7 +2323,7 @@
2323
2323
  {
2324
2324
  "destination": "{proxy-base}/api/progress",
2325
2325
  "what": "the stage progress observation — the snapshot just captured locally, POSTed under X-Campaign-Key",
2326
- "when": "Run Telemetry consent is on for the endpoint, --no-remit was not given, and the packet's campaign key resolves",
2326
+ "when": "Run Telemetry consent is on for the endpoint, --no-remit was not given, and the packet's campaign key resolves; never for a local-spec packet",
2327
2327
  "requires_consent": true,
2328
2328
  "observed_in": [
2329
2329
  "persisted_consent"
@@ -2332,7 +2332,7 @@
2332
2332
  ],
2333
2333
  "effect_test": "effects: qa run --no-post-verdict",
2334
2334
  "test_scope": "full",
2335
- "notes": "The base `qa run` row's writes, without the verdict publish: --no-post-verdict is the opt-out from posting the verdict to the QA portal. The progress observation is a separate delivery and still leaves under persisted consent; --no-remit is the flag that stops that one."
2335
+ "notes": "The base `qa run` row's writes, without the verdict publish: --no-post-verdict is the opt-out from posting the verdict to the QA portal. The progress observation is a separate delivery and still leaves under persisted consent; --no-remit is the flag that stops that one. Local-spec packets retain verdicts in the repository and never send a QA verdict or progress to the Map portal, even with --post-verdict."
2336
2336
  },
2337
2337
  {
2338
2338
  "command": "qa",
@@ -2423,7 +2423,7 @@
2423
2423
  {
2424
2424
  "destination": "{proxy-base}/api/qa/verdicts",
2425
2425
  "what": "the QA verdict for the attempt, published to the QA portal",
2426
- "when": "publish is not opted out (--no-post-verdict) and the consent seam allows it — Run Telemetry consent on for the endpoint, or a portal-managed campaign",
2426
+ "when": "publish is not opted out (--no-post-verdict) and the consent seam allows it — Run Telemetry consent on for the endpoint, or a portal-managed campaign; never for a local-spec packet",
2427
2427
  "requires_consent": true,
2428
2428
  "observed_in": [
2429
2429
  "persisted_consent"
@@ -2448,7 +2448,7 @@
2448
2448
  {
2449
2449
  "destination": "{proxy-base}/api/progress",
2450
2450
  "what": "the stage progress observation — the snapshot just captured locally, POSTed under X-Campaign-Key",
2451
- "when": "Run Telemetry consent is on for the endpoint, --no-remit was not given, and the packet's campaign key resolves",
2451
+ "when": "Run Telemetry consent is on for the endpoint, --no-remit was not given, and the packet's campaign key resolves; never for a local-spec packet",
2452
2452
  "requires_consent": true,
2453
2453
  "observed_in": [
2454
2454
  "persisted_consent"
@@ -2457,7 +2457,7 @@
2457
2457
  ],
2458
2458
  "effect_test": "effects: qa run --test-order",
2459
2459
  "test_scope": "full",
2460
- "notes": "The most consequential row in the file: --test-order places REAL orders against the store under test. Destructive, open-world, not idempotent. It SENDS on a blocked attempt too: the verdict is published to {proxy-base}/api/qa/verdicts and the QA stage progress observation to {proxy-base}/api/progress, both under persisted consent for the endpoint. Publishing rides the telemetry consent seam rather than a flag of its own, so the four consent-off conditions saw neither request."
2460
+ "notes": "The most consequential row in the file: --test-order places REAL orders against the store under test. Destructive, open-world, not idempotent. It SENDS on a blocked attempt too: the verdict is published to {proxy-base}/api/qa/verdicts and the QA stage progress observation to {proxy-base}/api/progress, both under persisted consent for the endpoint. Publishing rides the telemetry consent seam rather than a flag of its own, so the four consent-off conditions saw neither request. Local-spec packets retain verdicts in the repository and never send a QA verdict or progress to the Map portal, even with --post-verdict."
2461
2461
  },
2462
2462
  {
2463
2463
  "command": "qa",
@@ -2548,7 +2548,7 @@
2548
2548
  {
2549
2549
  "destination": "{proxy-base}/api/qa/verdicts",
2550
2550
  "what": "the QA verdict for the attempt, published to the QA portal",
2551
- "when": "publish is not opted out (--no-post-verdict) and the consent seam allows it — Run Telemetry consent on for the endpoint, or a portal-managed campaign",
2551
+ "when": "publish is not opted out (--no-post-verdict) and the consent seam allows it — Run Telemetry consent on for the endpoint, or a portal-managed campaign; never for a local-spec packet",
2552
2552
  "requires_consent": true,
2553
2553
  "observed_in": [
2554
2554
  "persisted_consent"
@@ -2573,7 +2573,7 @@
2573
2573
  {
2574
2574
  "destination": "{proxy-base}/api/progress",
2575
2575
  "what": "the stage progress observation — the snapshot just captured locally, POSTed under X-Campaign-Key",
2576
- "when": "Run Telemetry consent is on for the endpoint, --no-remit was not given, and the packet's campaign key resolves",
2576
+ "when": "Run Telemetry consent is on for the endpoint, --no-remit was not given, and the packet's campaign key resolves; never for a local-spec packet",
2577
2577
  "requires_consent": true,
2578
2578
  "observed_in": [
2579
2579
  "persisted_consent"
@@ -2582,7 +2582,7 @@
2582
2582
  ],
2583
2583
  "effect_test": "effects: qa run --browser (preflight)",
2584
2584
  "test_scope": "preflight",
2585
- "notes": "PREFLIGHT-PROVED: --browser needs an installed Chromium and a reachable campaign, neither of which exists offline. The case proves the attempt is blocked at the same gate as the node run and writes exactly the blocked-attempt evidence. It SENDS on a blocked attempt too: the verdict is published to {proxy-base}/api/qa/verdicts and the QA stage progress observation to {proxy-base}/api/progress, both under persisted consent for the endpoint. Publishing rides the telemetry consent seam rather than a flag of its own, so the four consent-off conditions saw neither request.",
2585
+ "notes": "PREFLIGHT-PROVED: --browser needs an installed Chromium and a reachable campaign, neither of which exists offline. The case proves the attempt is blocked at the same gate as the node run and writes exactly the blocked-attempt evidence. It SENDS on a blocked attempt too: the verdict is published to {proxy-base}/api/qa/verdicts and the QA stage progress observation to {proxy-base}/api/progress, both under persisted consent for the endpoint. Publishing rides the telemetry consent seam rather than a flag of its own, so the four consent-off conditions saw neither request. Local-spec packets retain verdicts in the repository and never send a QA verdict or progress to the Map portal, even with --post-verdict.",
2586
2586
  "preflight": {
2587
2587
  "may_write": [
2588
2588
  "{target}/qa-output/**",
@@ -2637,7 +2637,7 @@
2637
2637
  ],
2638
2638
  "effect_test": "effects: qa publish",
2639
2639
  "test_scope": "full",
2640
- "notes": "Posts an already-stored verdict without a re-run and without an order. It is NOT gated by Run Telemetry consent — the publish is the operator's explicit act."
2640
+ "notes": "Posts an already-stored verdict without a re-run and without an order. It is NOT gated by Run Telemetry consent — the publish is the operator's explicit act. Local-spec packets are refused before publication because they have no saved Map destination."
2641
2641
  },
2642
2642
  {
2643
2643
  "command": "qa",
@@ -2679,7 +2679,7 @@
2679
2679
  ],
2680
2680
  "effect_test": "effects: qa publish --republish",
2681
2681
  "test_scope": "full",
2682
- "notes": "--republish lifts the already-published refusal: the same POST to the QA portal for a verdict that was already delivered. Not destructive locally — nothing under the target is overwritten — and not gated by Run Telemetry consent."
2682
+ "notes": "--republish lifts the already-published refusal: the same POST to the QA portal for a verdict that was already delivered. Not destructive locally — nothing under the target is overwritten — and not gated by Run Telemetry consent. Local-spec packets are refused before publication because they have no saved Map destination."
2683
2683
  },
2684
2684
  {
2685
2685
  "command": "qa",
@@ -4789,6 +4789,99 @@
4789
4789
  "may_write": [],
4790
4790
  "may_contact": []
4791
4791
  }
4792
+ },
4793
+ {
4794
+ "command": "tooling",
4795
+ "subcommand": "setup",
4796
+ "flags": [],
4797
+ "annotations": {
4798
+ "readOnlyHint": false,
4799
+ "destructiveHint": false,
4800
+ "openWorldHint": true,
4801
+ "idempotentHint": true
4802
+ },
4803
+ "tier": "A",
4804
+ "writes": [
4805
+ {
4806
+ "path": "{target}/.campaign-runtime/agent-context/*",
4807
+ "when": "the four bundled context files are installed after preservation preflight",
4808
+ "observed_in": [],
4809
+ "not_observed_reason": "The offline preflight fails the browser download before shared skills or project files are changed. Existing installer effects are proved in their own rows; focused setup tests cover composition and preservation."
4810
+ },
4811
+ {
4812
+ "path": "{target}/.gitignore",
4813
+ "when": "the managed runtime ignore block is absent",
4814
+ "observed_in": [],
4815
+ "not_observed_reason": "The offline preflight fails the browser download before shared skills or project files are changed. Existing installer effects are proved in their own rows; focused setup tests cover composition and preservation."
4816
+ },
4817
+ {
4818
+ "path": "{target}/CLAUDE.md",
4819
+ "when": "the context import is absent; preserve all existing text and append the import once",
4820
+ "observed_in": [],
4821
+ "not_observed_reason": "The offline preflight fails the browser download before shared skills or project files are changed. Existing installer effects are proved in their own rows; focused setup tests cover composition and preservation."
4822
+ },
4823
+ {
4824
+ "path": "{home}/.claude/skills/**",
4825
+ "when": "the selected Claude skill bundle is installed with the existing installer",
4826
+ "observed_in": [],
4827
+ "not_observed_reason": "The offline preflight fails the browser download before shared skills or project files are changed. Existing installer effects are proved in their own rows; focused setup tests cover composition and preservation."
4828
+ },
4829
+ {
4830
+ "path": "{home}/ms-playwright/.links/*",
4831
+ "when": "always — Playwright records this package's installation in the browser registry as one link entry, named for the installing package's path, before it looks at what is already downloaded",
4832
+ "observed_in": [
4833
+ "no_session",
4834
+ "ambient_session",
4835
+ "stale_session",
4836
+ "lifecycle_log",
4837
+ "persisted_consent"
4838
+ ]
4839
+ },
4840
+ {
4841
+ "path": "{home}/ms-playwright/**",
4842
+ "when": "the download completes — the Chromium build (a ~150 MB archive) is unpacked into a revision directory of the same registry",
4843
+ "observed_in": [],
4844
+ "not_observed_reason": "the effect fixture pins PLAYWRIGHT_DOWNLOAD_HOST at a closed loopback port, so the archive is never fetched and no browser directory is created"
4845
+ }
4846
+ ],
4847
+ "sends": [
4848
+ {
4849
+ "destination": "{playwright-download-host}",
4850
+ "what": "a GET of the Chromium build archive (~150 MB) and of the headless shell beside it",
4851
+ "when": "the registry does not already hold the revision this package's Playwright pins",
4852
+ "requires_consent": false,
4853
+ "observed_in": [],
4854
+ "not_observed_reason": "the effect fixture pins PLAYWRIGHT_DOWNLOAD_HOST at a closed loopback port, so the fetch fails at connect; the NODE_DEBUG=net trace the case asserts on names no host but 127.0.0.1"
4855
+ }
4856
+ ],
4857
+ "effect_test": "effects: tooling setup (preflight)",
4858
+ "test_scope": "preflight",
4859
+ "notes": "PREFLIGHT-PROVED: checks the selected project copy, exact pin, installed page-kit and context preservation before attempting the package-owned browser download. The closed-loopback fixture fails that download before global skills, context or project instructions change. After a successful download, setup composes the existing installers and appends the context import once. Does not read gateway credentials, resolve or close campaign run sessions, append journals, remit telemetry, scaffold pages or create campaign evidence. Focused tests prove rerun preservation, refusal-before-write and browser-failure recovery.",
4860
+ "preflight": {
4861
+ "may_write": [
4862
+ "{home}/ms-playwright/.links/*"
4863
+ ],
4864
+ "may_contact": []
4865
+ }
4866
+ },
4867
+ {
4868
+ "command": "tooling",
4869
+ "subcommand": "setup",
4870
+ "flags": [
4871
+ "--dry-run"
4872
+ ],
4873
+ "annotations": {
4874
+ "readOnlyHint": true,
4875
+ "destructiveHint": false,
4876
+ "openWorldHint": false,
4877
+ "idempotentHint": true
4878
+ },
4879
+ "tier": "none",
4880
+ "writes": [],
4881
+ "sends": [],
4882
+ "effect_test": "effects: tooling setup --dry-run",
4883
+ "test_scope": "full",
4884
+ "notes": "Checks the selected project and previews the existing skill/context installers and instruction import without downloading a browser, touching credentials or recovering/remitting a run session."
4792
4885
  }
4793
4886
  ]
4794
4887
  }
@@ -59,6 +59,15 @@
59
59
  "identity_fields": [
60
60
  {
61
61
  "name": "map_id",
62
+ "local_spec_alternative": {
63
+ "name": "local_spec_id",
64
+ "artifact_paths": {
65
+ "build_packet": "spec.local_spec_id",
66
+ "assembly_report": "identity.local_spec_id",
67
+ "doctor_output": "derived.local_spec_id",
68
+ "qa_verdict": "local_spec_id"
69
+ }
70
+ },
62
71
  "artifact_paths": {
63
72
  "build_packet": "spec.map_id",
64
73
  "assembly_report": "identity.map_id",