@nextcommerce/campaigns-os 1.33.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 (310) hide show
  1. package/AGENTS.md +204 -0
  2. package/CHANGELOG.md +5002 -0
  3. package/CONTEXT.md +685 -0
  4. package/LICENSE +202 -0
  5. package/NOTICE +4 -0
  6. package/README.md +368 -0
  7. package/agents/claude/CLAUDE.md +32 -0
  8. package/agents/codex/AGENTS.md +27 -0
  9. package/agents/copilot/copilot-instructions.md +14 -0
  10. package/agents/cursor/campaigns-os.mdc +13 -0
  11. package/bin/campaigns-os.mjs +38 -0
  12. package/campaign-spec/README.md +138 -0
  13. package/campaign-spec/dist/analytics-vocabulary.d.ts +47 -0
  14. package/campaign-spec/dist/analytics-vocabulary.js +74 -0
  15. package/campaign-spec/dist/index.d.ts +40 -0
  16. package/campaign-spec/dist/index.js +77 -0
  17. package/campaign-spec/dist/normalize.d.ts +22 -0
  18. package/campaign-spec/dist/normalize.js +41 -0
  19. package/campaign-spec/dist/routing.d.ts +190 -0
  20. package/campaign-spec/dist/routing.js +263 -0
  21. package/campaign-spec/dist/rules/analytics-contract-shape.d.ts +42 -0
  22. package/campaign-spec/dist/rules/analytics-contract-shape.js +303 -0
  23. package/campaign-spec/dist/rules/assembly-hints-shape.d.ts +42 -0
  24. package/campaign-spec/dist/rules/assembly-hints-shape.js +191 -0
  25. package/campaign-spec/dist/rules/campaign-metadata.d.ts +14 -0
  26. package/campaign-spec/dist/rules/campaign-metadata.js +40 -0
  27. package/campaign-spec/dist/rules/checkout-has-success-url.d.ts +31 -0
  28. package/campaign-spec/dist/rules/checkout-has-success-url.js +66 -0
  29. package/campaign-spec/dist/rules/cycle-detection.d.ts +12 -0
  30. package/campaign-spec/dist/rules/cycle-detection.js +141 -0
  31. package/campaign-spec/dist/rules/design-source-shape.d.ts +29 -0
  32. package/campaign-spec/dist/rules/design-source-shape.js +142 -0
  33. package/campaign-spec/dist/rules/downsell-without-upsell.d.ts +11 -0
  34. package/campaign-spec/dist/rules/downsell-without-upsell.js +40 -0
  35. package/campaign-spec/dist/rules/exit-intent-validation.d.ts +23 -0
  36. package/campaign-spec/dist/rules/exit-intent-validation.js +147 -0
  37. package/campaign-spec/dist/rules/funnel-count.d.ts +9 -0
  38. package/campaign-spec/dist/rules/funnel-count.js +37 -0
  39. package/campaign-spec/dist/rules/funnel-hypothesis-length.d.ts +22 -0
  40. package/campaign-spec/dist/rules/funnel-hypothesis-length.js +60 -0
  41. package/campaign-spec/dist/rules/funnel-identity.d.ts +15 -0
  42. package/campaign-spec/dist/rules/funnel-identity.js +57 -0
  43. package/campaign-spec/dist/rules/funnel-weight-sum.d.ts +20 -0
  44. package/campaign-spec/dist/rules/funnel-weight-sum.js +66 -0
  45. package/campaign-spec/dist/rules/index.d.ts +69 -0
  46. package/campaign-spec/dist/rules/index.js +131 -0
  47. package/campaign-spec/dist/rules/offer-ref-integrity.d.ts +13 -0
  48. package/campaign-spec/dist/rules/offer-ref-integrity.js +59 -0
  49. package/campaign-spec/dist/rules/package-pricing-sanity.d.ts +12 -0
  50. package/campaign-spec/dist/rules/package-pricing-sanity.js +42 -0
  51. package/campaign-spec/dist/rules/page-count.d.ts +11 -0
  52. package/campaign-spec/dist/rules/page-count.js +31 -0
  53. package/campaign-spec/dist/rules/page-id-uniqueness.d.ts +14 -0
  54. package/campaign-spec/dist/rules/page-id-uniqueness.js +47 -0
  55. package/campaign-spec/dist/rules/promo-code-input-validation.d.ts +8 -0
  56. package/campaign-spec/dist/rules/promo-code-input-validation.js +126 -0
  57. package/campaign-spec/dist/rules/promo-codes-shape.d.ts +30 -0
  58. package/campaign-spec/dist/rules/promo-codes-shape.js +187 -0
  59. package/campaign-spec/dist/rules/route-field-ignored-for-page-type.d.ts +33 -0
  60. package/campaign-spec/dist/rules/route-field-ignored-for-page-type.js +81 -0
  61. package/campaign-spec/dist/rules/route-target-resolves.d.ts +32 -0
  62. package/campaign-spec/dist/rules/route-target-resolves.js +112 -0
  63. package/campaign-spec/dist/rules/schema-version.d.ts +21 -0
  64. package/campaign-spec/dist/rules/schema-version.js +53 -0
  65. package/campaign-spec/dist/rules/sdk-version.d.ts +26 -0
  66. package/campaign-spec/dist/rules/sdk-version.js +96 -0
  67. package/campaign-spec/dist/rules/shipping-countries-shape.d.ts +10 -0
  68. package/campaign-spec/dist/rules/shipping-countries-shape.js +30 -0
  69. package/campaign-spec/dist/rules/shipping-methods-present.d.ts +10 -0
  70. package/campaign-spec/dist/rules/shipping-methods-present.js +26 -0
  71. package/campaign-spec/dist/rules/store-profile-shape.d.ts +30 -0
  72. package/campaign-spec/dist/rules/store-profile-shape.js +127 -0
  73. package/campaign-spec/dist/rules/thank-you-requirement.d.ts +16 -0
  74. package/campaign-spec/dist/rules/thank-you-requirement.js +50 -0
  75. package/campaign-spec/dist/rules/unknown-top-level-fields.d.ts +28 -0
  76. package/campaign-spec/dist/rules/unknown-top-level-fields.js +114 -0
  77. package/campaign-spec/dist/rules/upsell-has-packages.d.ts +9 -0
  78. package/campaign-spec/dist/rules/upsell-has-packages.js +35 -0
  79. package/campaign-spec/dist/rules/upsell-routing-complete.d.ts +10 -0
  80. package/campaign-spec/dist/rules/upsell-routing-complete.js +45 -0
  81. package/campaign-spec/dist/rules/upsell-without-checkout.d.ts +11 -0
  82. package/campaign-spec/dist/rules/upsell-without-checkout.js +44 -0
  83. package/campaign-spec/dist/rules/variant-labels-shape.d.ts +28 -0
  84. package/campaign-spec/dist/rules/variant-labels-shape.js +86 -0
  85. package/campaign-spec/dist/sdk-version-parse.d.ts +43 -0
  86. package/campaign-spec/dist/sdk-version-parse.js +62 -0
  87. package/campaign-spec/dist/types.d.ts +674 -0
  88. package/campaign-spec/dist/types.js +40 -0
  89. package/campaign-spec/package.json +12 -0
  90. package/compatibility.json +25 -0
  91. package/contracts/agent-relevant-change-policy.v1.json +111 -0
  92. package/contracts/brand-theme-source-defaults.figma-sections-export.v0.json +32 -0
  93. package/contracts/brand-theme-target-tokens.next-core.v0.json +65 -0
  94. package/contracts/campaign-cart-checkout-field-contract.v0.json +45 -0
  95. package/contracts/campaign-cart-sdk-support-policy.v0.json +11 -0
  96. package/contracts/commerce-surface-catalog.json +2452 -0
  97. package/contracts/fixtures/orientation/canonicalization/v1.json +34 -0
  98. package/contracts/fixtures/orientation/envelope/current.json +95 -0
  99. package/contracts/fixtures/orientation/envelope/freshness_unknown.json +97 -0
  100. package/contracts/fixtures/orientation/envelope/legacy_baseline.json +95 -0
  101. package/contracts/fixtures/orientation/envelope/orientation_available.json +136 -0
  102. package/contracts/fixtures/orientation/envelope/recovered_interrupted_update.json +136 -0
  103. package/contracts/fixtures/orientation/envelope/refused.json +100 -0
  104. package/contracts/fixtures/orientation/envelope/restart_required.json +137 -0
  105. package/contracts/fixtures/orientation/envelope/updated.json +135 -0
  106. package/contracts/fixtures/orientation/hostile-target/README.md +61 -0
  107. package/contracts/fixtures/orientation/hostile-target/manifest.json +82 -0
  108. package/contracts/fixtures/orientation/hostile-target/repo/CHANGELOG.md +18 -0
  109. package/contracts/fixtures/orientation/hostile-target/repo/bin/intended.mjs +12 -0
  110. package/contracts/fixtures/orientation/hostile-target/repo/bin/tripwire.mjs +15 -0
  111. package/contracts/fixtures/orientation/hostile-target/repo/contracts/release-ledger.json +48 -0
  112. package/contracts/fixtures/orientation/hostile-target/repo/contracts/supported-surface.json +17 -0
  113. package/contracts/fixtures/orientation/hostile-target/repo/docs/example-contract.md +13 -0
  114. package/contracts/fixtures/orientation/hostile-target/repo/hooks/post-checkout +5 -0
  115. package/contracts/fixtures/orientation/hostile-target/repo/hooks/post-merge +5 -0
  116. package/contracts/fixtures/orientation/hostile-target/repo/hooks/pre-commit +5 -0
  117. package/contracts/fixtures/orientation/hostile-target/repo/hostile-dependency-tripwire/package.json +15 -0
  118. package/contracts/fixtures/orientation/hostile-target/repo/hostile-dependency-tripwire/tripwire.mjs +9 -0
  119. package/contracts/fixtures/orientation/hostile-target/repo/package.json +20 -0
  120. package/contracts/fixtures/orientation/hostile-target/repo/schemas/example.v0.schema.json +15 -0
  121. package/contracts/fixtures/orientation/release-gate/cases.json +1073 -0
  122. package/contracts/fixtures/runtime-recipe/accept/current.json +299 -0
  123. package/contracts/fixtures/runtime-recipe/accept/minimal.json +294 -0
  124. package/contracts/fixtures/runtime-recipe/dist-states.json +51 -0
  125. package/contracts/fixtures/runtime-recipe/manifest.json +85 -0
  126. package/contracts/fixtures/runtime-recipe/reject/advisory-enforcement.json +299 -0
  127. package/contracts/fixtures/runtime-recipe/reject/allowlist-without-hosts.json +297 -0
  128. package/contracts/fixtures/runtime-recipe/reject/committed-output-claim.json +299 -0
  129. package/contracts/fixtures/runtime-recipe/reject/engines-disagreement-warns.json +299 -0
  130. package/contracts/fixtures/runtime-recipe/reject/lifecycle-scripts-enabled.json +299 -0
  131. package/contracts/fixtures/runtime-recipe/reject/missing-required-field.json +251 -0
  132. package/contracts/fixtures/runtime-recipe/reject/unknown-kind.json +299 -0
  133. package/contracts/fixtures/runtime-recipe/reject/unknown-network-policy.json +299 -0
  134. package/contracts/fixtures/runtime-recipe/reject/unknown-output-check.json +310 -0
  135. package/contracts/fixtures/runtime-recipe/reject/unknown-revision.json +299 -0
  136. package/contracts/fixtures/runtime-recipe/reject/unknown-step-id.json +299 -0
  137. package/contracts/fixtures/runtime-recipe/reject/unperformable-check-skipped.json +299 -0
  138. package/contracts/fixtures/runtime-recipe/reject/unpinned-lockfile.json +299 -0
  139. package/contracts/fixtures/sidecar-bundle/production-shaped/.campaign-runtime/assembly-report.json +180 -0
  140. package/contracts/fixtures/sidecar-bundle/production-shaped/.campaign-runtime/build-context.json +115 -0
  141. package/contracts/fixtures/sidecar-bundle/production-shaped/.campaign-runtime/doctor-output.json +29 -0
  142. package/contracts/fixtures/sidecar-bundle/production-shaped/.campaign-runtime/qa-verdict.json +27 -0
  143. package/contracts/fixtures/sidecar-bundle/production-shaped/campaign-runtime.build.json +141 -0
  144. package/contracts/migration-sidecar-bundle.v0.json +149 -0
  145. package/contracts/orientation-limits.v1.json +41 -0
  146. package/contracts/orientation-reason-codes.v1.json +196 -0
  147. package/contracts/private-template-sources.json +8 -0
  148. package/contracts/release-ledger.json +4598 -0
  149. package/contracts/reserved-skill-names.json +13 -0
  150. package/contracts/runtime-recipe.campaigns-os-node-v1.json +299 -0
  151. package/contracts/supported-surface.json +180 -0
  152. package/contracts/template-brand-contract.apollo-mv-single-step.v0.json +27 -0
  153. package/contracts/template-brand-contract.apollo.v0.json +27 -0
  154. package/contracts/template-brand-contract.demeter.v0.json +27 -0
  155. package/contracts/template-brand-contract.olympus-mv-single-step.v0.json +27 -0
  156. package/contracts/template-brand-contract.olympus-mv-two-step.v0.json +28 -0
  157. package/contracts/template-brand-contract.olympus.v0.json +27 -0
  158. package/contracts/template-brand-contract.shared-commerce.v0.json +190 -0
  159. package/contracts/template-brand-contract.shop-single-step.v0.json +27 -0
  160. package/contracts/template-brand-contract.shop-three-step.v0.json +29 -0
  161. package/contracts/template-slot-manifest.apollo-mv-single-step.v0.json +14 -0
  162. package/contracts/template-slot-manifest.apollo.v0.json +12 -0
  163. package/contracts/template-slot-manifest.demeter.v0.json +30 -0
  164. package/contracts/template-slot-manifest.olympus-mv-single-step.v0.json +14 -0
  165. package/contracts/template-slot-manifest.olympus-mv-two-step.v0.json +15 -0
  166. package/contracts/template-slot-manifest.olympus.v0.json +12 -0
  167. package/contracts/template-slot-manifest.shared-content-core.v0.json +4254 -0
  168. package/contracts/template-slot-manifest.shop-single-step.v0.json +47 -0
  169. package/contracts/template-slot-manifest.shop-three-step.v0.json +33 -0
  170. package/docs/brand-theme-bridge.md +159 -0
  171. package/docs/build-packet.md +1300 -0
  172. package/docs/campaign-build-brief.md +145 -0
  173. package/docs/campaign-standardization-report.md +329 -0
  174. package/docs/campaigns-os-build-flow.md +117 -0
  175. package/docs/design-source-package.md +784 -0
  176. package/docs/legacy-migration.md +58 -0
  177. package/docs/migration-sidecar-bundle.md +139 -0
  178. package/docs/orientation-contract-reference.md +1220 -0
  179. package/docs/polish-evidence.md +502 -0
  180. package/docs/qa-and-test-orders.md +1691 -0
  181. package/docs/release-ledger-authoring-guide.md +274 -0
  182. package/docs/runtime-readiness.md +211 -0
  183. package/docs/supported-surface.md +83 -0
  184. package/docs/versioning.md +55 -0
  185. package/docs/workflow-findings-sidecar.md +588 -0
  186. package/package.json +135 -0
  187. package/prompts/first-build.md +27 -0
  188. package/prompts/friction-log.md +30 -0
  189. package/schemas/campaign-build-brief.v1.schema.json +149 -0
  190. package/schemas/campaign-design-source-package.v0.schema.json +697 -0
  191. package/schemas/campaign-runtime-assembly-report.v0.schema.json +390 -0
  192. package/schemas/campaign-runtime-build-context.v0.schema.json +339 -0
  193. package/schemas/campaign-runtime-build-packet.v0.schema.json +452 -0
  194. package/schemas/campaign-spec.v4.schema.json +582 -0
  195. package/schemas/campaigns-os-doctor-output.v0.schema.json +43 -0
  196. package/schemas/campaigns-os-legacy-migration-inventory.v0.schema.json +112 -0
  197. package/schemas/campaigns-os-legacy-provisioning-plan.v0.schema.json +38 -0
  198. package/schemas/campaigns-os-legacy-provisioning-receipt.v0.schema.json +57 -0
  199. package/schemas/campaigns-os-qa-verdict-sidecar.v0.schema.json +150 -0
  200. package/schemas/campaigns-os-qa-verdict.v0.schema.json +438 -0
  201. package/schemas/campaigns-os-release-ledger.v1.schema.json +162 -0
  202. package/schemas/campaigns-os-run-record.v0.schema.json +343 -0
  203. package/schemas/campaigns-os-runtime-recipe.v1.schema.json +313 -0
  204. package/schemas/campaigns-os-sidecar-bundle-conformance.v0.schema.json +78 -0
  205. package/schemas/campaigns-os-tooling-orientation.v1.schema.json +381 -0
  206. package/schemas/campaigns-os-workflow-finding.v0.schema.json +134 -0
  207. package/schemas/source-html-manifest.v0.schema.json +252 -0
  208. package/skills/next-campaigns-build/SKILL.md +73 -0
  209. package/skills/next-campaigns-os/SKILL.md +101 -0
  210. package/skills/next-campaigns-os/references/session-intake.md +160 -0
  211. package/skills/next-campaigns-os-setup/SKILL.md +22 -0
  212. package/skills/next-campaigns-polish/SKILL.md +145 -0
  213. package/skills/next-campaigns-qa/SKILL.md +92 -0
  214. package/skills.json +56 -0
  215. package/skills.sh +64 -0
  216. package/src/adapter-decision-contract.mjs +333 -0
  217. package/src/brand-theme.mjs +1151 -0
  218. package/src/browser-launch.mjs +79 -0
  219. package/src/build-brief.mjs +781 -0
  220. package/src/built-site-scope.mjs +312 -0
  221. package/src/campaign-ecosystem.mjs +734 -0
  222. package/src/campaign-identity.mjs +405 -0
  223. package/src/campaign-workspace.mjs +127 -0
  224. package/src/checkpoint-waiver.mjs +302 -0
  225. package/src/cli.mjs +13442 -0
  226. package/src/commercial-journey.mjs +1119 -0
  227. package/src/commercial-parity.mjs +965 -0
  228. package/src/consent.mjs +347 -0
  229. package/src/content-residue.mjs +322 -0
  230. package/src/deadline.mjs +81 -0
  231. package/src/design-source-package.mjs +2604 -0
  232. package/src/deviation.mjs +107 -0
  233. package/src/doctor-check-registry.mjs +49 -0
  234. package/src/doctor-sidecar.mjs +106 -0
  235. package/src/finding-cause.mjs +557 -0
  236. package/src/findings.mjs +326 -0
  237. package/src/fs-identity.mjs +68 -0
  238. package/src/gate-actions.mjs +105 -0
  239. package/src/html-scan.mjs +53 -0
  240. package/src/install-mode.mjs +272 -0
  241. package/src/legacy-migration.d.ts +128 -0
  242. package/src/legacy-migration.mjs +510 -0
  243. package/src/lifecycle.mjs +338 -0
  244. package/src/local-proof.mjs +401 -0
  245. package/src/map-pin-writeback.mjs +210 -0
  246. package/src/orchestration-stage-contract.mjs +81 -0
  247. package/src/package-install-fixture.mjs +33 -0
  248. package/src/page-kit-build-summary.mjs +175 -0
  249. package/src/page-kit-campaign-config.mjs +57 -0
  250. package/src/page-kit-sdk-version.mjs +392 -0
  251. package/src/page-kit-store-profile.mjs +369 -0
  252. package/src/page-kit-sync.mjs +162 -0
  253. package/src/polish-browser.mjs +867 -0
  254. package/src/polish-capture.mjs +1094 -0
  255. package/src/polish-deadline.mjs +51 -0
  256. package/src/polish-gate.mjs +739 -0
  257. package/src/polish-node.mjs +639 -0
  258. package/src/polish-page-load.mjs +1100 -0
  259. package/src/private-template-source.mjs +237 -0
  260. package/src/proof-policy.mjs +82 -0
  261. package/src/qa-analytics-correctness.mjs +307 -0
  262. package/src/qa-analytics-errors.mjs +38 -0
  263. package/src/qa-analytics-parity.mjs +699 -0
  264. package/src/qa-binding-evidence.mjs +140 -0
  265. package/src/qa-browser.mjs +6608 -0
  266. package/src/qa-cart-entry.mjs +406 -0
  267. package/src/qa-commercial-parity.mjs +641 -0
  268. package/src/qa-node.mjs +3620 -0
  269. package/src/qa-order-bump.mjs +381 -0
  270. package/src/qa-parity-capture.mjs +428 -0
  271. package/src/qa-parity-fixture.mjs +359 -0
  272. package/src/qa-publish.mjs +362 -0
  273. package/src/qa-purchase-data-layer.mjs +263 -0
  274. package/src/qa-route-probe.mjs +272 -0
  275. package/src/qa-sidecar.mjs +188 -0
  276. package/src/qa-test-order-topology.mjs +207 -0
  277. package/src/qa-url-privacy.mjs +13 -0
  278. package/src/qa-verdict-discovery.mjs +192 -0
  279. package/src/qa-verdict-publish.mjs +105 -0
  280. package/src/qa-verdict.mjs +287 -0
  281. package/src/remit.mjs +388 -0
  282. package/src/repo-scan.mjs +83 -0
  283. package/src/route-identity.mjs +133 -0
  284. package/src/run-record-closeout.mjs +229 -0
  285. package/src/run-record.mjs +839 -0
  286. package/src/run-session.mjs +226 -0
  287. package/src/runtime-state-ignore.mjs +113 -0
  288. package/src/sdk-attribute-index.mjs +212 -0
  289. package/src/sdk-markup.mjs +358 -0
  290. package/src/sdk-meta-tags.mjs +52 -0
  291. package/src/shell-token.mjs +7 -0
  292. package/src/sidecar-bundle.mjs +399 -0
  293. package/src/source-asset-crawl.mjs +469 -0
  294. package/src/source-html-intake.mjs +627 -0
  295. package/src/source-html-manifest.mjs +276 -0
  296. package/src/source-prep.mjs +284 -0
  297. package/src/spec-derive-store.mjs +431 -0
  298. package/src/spec-derive.mjs +514 -0
  299. package/src/spec-fetch.mjs +66 -0
  300. package/src/spec-hash.mjs +48 -0
  301. package/src/spec-identity.mjs +27 -0
  302. package/src/stage-ledger.mjs +509 -0
  303. package/src/standardization-report.mjs +1297 -0
  304. package/src/template-brand-contract.mjs +473 -0
  305. package/src/template-freshness.mjs +196 -0
  306. package/src/template-reference.mjs +81 -0
  307. package/src/template-slot-manifest.mjs +150 -0
  308. package/src/text-safety.mjs +62 -0
  309. package/src/theme-gate.mjs +185 -0
  310. package/src/upsell-selector-scope.mjs +299 -0
@@ -0,0 +1,55 @@
1
+ # Versioning
2
+
3
+ This repo uses independent compatibility versions:
4
+
5
+ - package version: `1.33.0` — equals `surface_version` in
6
+ `contracts/supported-surface.json` (`check:supported-surface` enforces it)
7
+ and is the version published to the npm registry; `+agent.N` changelog
8
+ sections are same-surface changes and are not published on their own
9
+ - Build Packet: `campaign-runtime-build-packet/v0`
10
+ - Build Context: `campaign-runtime-build-context/v0`
11
+ - Assembly Report: `campaign-runtime-assembly-report/v0`
12
+ - Design Source Package: `campaign-design-source-package/v0`
13
+ - Workflow Finding: `campaigns-os-workflow-finding/v0`
14
+ - QA Verdict: `campaigns-os-qa-verdict/v0` (JSON Schema:
15
+ `schemas/campaigns-os-qa-verdict.v0.schema.json`; the emitted
16
+ `schema_version` field is the literal `"1.0"` — it predates the
17
+ slash-versioned naming and the portal receiver validates that same literal,
18
+ so the emitted value cannot change without a breaking shape change)
19
+ - QA Verdict sidecar projection: same `"1.0"` literal, projection guarantees in
20
+ `schemas/campaigns-os-qa-verdict-sidecar.v0.schema.json` (one contract, two
21
+ schema files — the sidecar is an allowlist projection, never a second lineage)
22
+ - CampaignSpec: `4.2`–`4.3` (JSON Schema: `schemas/campaign-spec.v4.schema.json`)
23
+ - starter-template agent contract: `1`
24
+ - commerce surface catalog: `2`
25
+ - Tooling Orientation: `campaigns-os-tooling-orientation/v1`
26
+ - Release Ledger: `campaigns-os-release-ledger/v1`
27
+ - agent-relevant change policy: `1.0.0` (`contracts/agent-relevant-change-policy.v1.json`)
28
+ - orientation reason-code vocabulary: `1.0.0` (`contracts/orientation-reason-codes.v1.json`)
29
+ - orientation limits: `1.0.0` (`contracts/orientation-limits.v1.json`)
30
+ - Runtime Recipe: `campaigns-os-runtime-recipe/v1` (JSON Schema:
31
+ `schemas/campaigns-os-runtime-recipe.v1.schema.json`)
32
+ - runtime recipe kind: `campaigns-os-node-v1`, revision `1.0.0`
33
+ (`contracts/runtime-recipe.campaigns-os-node-v1.json`)
34
+
35
+ The orientation line is separate from the supported-surface line on purpose: a
36
+ consumer needs to know about an agent-relevant change even when
37
+ `surface_version` did not move. `contracts/release-ledger.json` records those,
38
+ `CHANGELOG.md` narrates them, and the two are checked against each other in both
39
+ directions. Reason codes and semantic classes are append-only vocabularies —
40
+ renaming or removing one is a breaking change. Raising a limit advances
41
+ `limits_version` and owes its own ledger entry.
42
+
43
+ The runtime recipe carries two version identifiers because they gate different
44
+ things. The **kind** names what an installed consumer must already understand in
45
+ order to execute the document at all — its commands, its package manager, the
46
+ shape of its network policy, the kinds of output check it declares. A new kind
47
+ needs a consumer release first, and an older consumer must fail closed on it.
48
+ The **revision** re-parameterises fields an existing consumer already
49
+ understands: accepted tool ranges, timeout and output bounds, the enumerated
50
+ input set, the expected output inventory. An older consumer runs a revision
51
+ correctly, with different numbers. Both owe a ledger entry; only a new kind gates
52
+ on a consumer release. The recipe and its schema are hashed surface entries, so
53
+ either changing also advances `surface_version` in the same change.
54
+
55
+ Breaking packet semantics should create a new packet schema version. Non-breaking doctor warnings can ship in package patch/minor releases during developer preview.
@@ -0,0 +1,588 @@
1
+ # Run Telemetry
2
+
3
+ Status: Implemented v0 — Run Records, consent/remit, packet-associated ambient sessions, QA repair-loop closeout, lifecycle timing, and repair-loop aggregation are live.
4
+ Date: 2026-06-08
5
+
6
+ > Supersedes the v0 "Workflow Findings Sidecar" framing. The sidecar was
7
+ > local-only and never remitted; Run Telemetry keeps local capture but adds a
8
+ > consented, opt-out remit so each run can improve the product. Workflow
9
+ > Findings are now one channel inside a per-run Run Record. (Filename retained
10
+ > for now to avoid link churn; the surface is "Run Telemetry".)
11
+
12
+ ## Purpose
13
+
14
+ Every Campaigns OS run produces signal about how the build went — what the
15
+ doctor flagged, which spec rules fired, which adapter decisions were taken, how
16
+ QA resolved, plus anything an operator or agent noticed. Today that signal is
17
+ discarded at the end of the run.
18
+
19
+ Run Telemetry captures that signal as a structured **Run Record** and, when the
20
+ operator has opted in, remits it to Next Commerce so the toolchain can improve
21
+ over time — better skills, tools, templates, and design sources. The goal is a
22
+ loop: a real run surfaces friction, the friction is analyzed, the fix ships, the
23
+ next run is smoother.
24
+
25
+ This is the "share usage data to improve the product" pattern, made explicit and
26
+ asked once, up front.
27
+
28
+ ## What Changed From v0
29
+
30
+ The Workflow Findings Sidecar was deliberately local-only. Run Telemetry keeps
31
+ the local trail but changes the contribution model:
32
+
33
+ - **Capture is always local.** The Run Record is written regardless of consent.
34
+ - **Consent gates remit, not capture.** A machine-level opt-out decides only
35
+ whether records are sent (default ON for the canonical NEXT endpoint,
36
+ announced at remit time). Opt-outs lose nothing locally.
37
+ - **The unit is the Run Record, not a single finding.** Findings (manual and
38
+ harvested) are one channel within it.
39
+
40
+ ## The Run Record (manifest model)
41
+
42
+ The Run Record is a per-run **manifest**, not a giant unified artifact. It is
43
+ keyed by one canonical `run_id` and is written to:
44
+
45
+ ```text
46
+ .campaign-runtime/run-records/<run_id>.json
47
+ ```
48
+
49
+ It does **not** re-embed the full bodies of other artifacts (those have their
50
+ own schemas and evolve independently). Instead it carries:
51
+
52
+ - **Stable envelope** — `schema_version` (`campaigns-os-run-record/v0`),
53
+ `run_id`, package version, the command that ran, an `argv` *shape* (flag names
54
+ present, not raw values), `created_at`, consent state, and remit status.
55
+ - **Run identity** — `map_id`, `campaign_slug`, `template_family`,
56
+ `entry_point_shape`. (Best-effort; missing identity never blocks capture.)
57
+ - **Source artifact refs** — for the Build Packet, Build Context, Assembly
58
+ Report, Page Kit build summary, QA verdict, and findings journal: `{ path,
59
+ schema_version, sha256 }`. References, not copies. This is what survives
60
+ upstream schema drift.
61
+ - **Normalized observation arrays** — the extracted signal: doctor issue codes
62
+ (error/warning/ready), `spec.validation` rule IDs that fired, adapter
63
+ decisions, QA verdict disposition + gap classes, and the **finding IDs** for
64
+ this run.
65
+ - **Findings snapshot** — this run's Workflow Findings (see channel below).
66
+
67
+ ### Run identity
68
+
69
+ A single canonical `campaigns_os_run_id` is minted at the run boundary and
70
+ threaded through the run so every artifact and finding correlates. It is also
71
+ the **idempotency key**, enforced by refusal: the receiver holds one record per
72
+ `run_id` and answers a second POST for an id it already stores with `409
73
+ run_record_conflict`. Retrying a send that never landed is safe; the record that
74
+ did land cannot be revised, so the id must not be spent on an interim record
75
+ before the one that closes the run.
76
+
77
+ Stage timings and repair-loop count are captured from the command lifecycle
78
+ journal when a run session or explicit lifecycle journal is active. They remain
79
+ best-effort signal: telemetry records the commands Campaigns OS can observe, not
80
+ every thought, browser click, or external editor action in an agent session.
81
+
82
+ ### Validation
83
+
84
+ Hand-rolled validator + JSON Schema doc, matching the existing
85
+ `campaigns-os-workflow-finding` pair. **No AJV** (repo convention). The
86
+ validator checks the envelope + observation-array shapes; it does **not**
87
+ re-validate nested artifact bodies (those are referenced by hash, not embedded).
88
+
89
+ ## Improvement-Surface Taxonomy
90
+
91
+ Each observation can map to the surface it should improve. Real signal is rarely
92
+ one surface, so the field is a list, not an enum:
93
+
94
+ - `surfaces: []` — any of `skill | cli | template | design-source | docs |
95
+ spec-rule | platform`
96
+ - `primary_surface` — optional, the best single guess
97
+ - `surface_confidence` — optional
98
+
99
+ A best-effort tag travels with the signal; internal analysis refines and
100
+ clusters. This is the grown-up form of the v0 `suggested_owner` field.
101
+
102
+ ## Consent
103
+
104
+ Consent is a **machine/user-level** setting (consent belongs to the operator,
105
+ not the campaign), resolved through one shared resolver that **every remitting
106
+ command calls** — not a one-time `start` prompt that later commands bypass.
107
+
108
+ - **Stored** at user level (`$XDG_CONFIG_HOME/campaigns-os/config.json`, else
109
+ `~/.config/campaigns-os/config.json`) with its own `schema_version`, the
110
+ package name, the proxy/endpoint scope, a timestamp, and the value source.
111
+ - **Scoped to one endpoint.** A stored grant names the endpoint it was given
112
+ for and applies to remits that go there, not to any other base.
113
+ `campaigns-os telemetry on` grants the canonical NEXT endpoint;
114
+ `campaigns-os telemetry on --proxy-base <url>` grants that receiver instead
115
+ (a loopback or staging receiver; the base must be https or a loopback host,
116
+ the same rule the remit rail applies). The file holds one grant at a time,
117
+ so granting a staging receiver leaves the canonical endpoint OFF until
118
+ `telemetry on` is run again without the flag. A remit whose `--proxy-base`
119
+ does not match the stored scope stays OFF and the warning names the command
120
+ that would grant it. `campaigns-os telemetry status` prints the stored scope
121
+ and checks it against the canonical endpoint, or against `--proxy-base <url>`
122
+ when given (the same https-or-loopback rule applies). `campaigns-os
123
+ telemetry off` takes no `--proxy-base`: an OFF choice applies to every
124
+ endpoint, and the record it writes carries no scope.
125
+ - **Prompted once, up front** — the first interactive command that would remit
126
+ asks plainly: "Campaigns OS can send build telemetry to Next Commerce to
127
+ improve templates, tools, and guidance. Share telemetry from this machine?
128
+ [Y/n] (change any time)."
129
+ - **`campaigns-os telemetry status | on | off`** — explicit control without
130
+ hunting for the config file.
131
+ - **Env override** — `CAMPAIGNS_OS_TELEMETRY` accepts `1|true|on` /
132
+ `0|false|off`; it beats the file (CI/automation). An **unknown** value
133
+ fails closed (no remit) with a warning, never a silent guess. `on` has no
134
+ scope: it applies to whatever endpoint the command names, so a remit to a
135
+ non-canonical `--proxy-base` under it warns that the override bypasses scope
136
+ checking and names the scoped `telemetry on --proxy-base` grant instead.
137
+ - **No file, no env** → **ON for the canonical NEXT endpoint only**, announced
138
+ at remit time with the endpoint and the opt-out command. A non-canonical
139
+ `--proxy-base` (staging, self-hosted) stays **OFF** until explicitly
140
+ consented, and a malformed config file resolves **OFF** — the default never
141
+ overrides an unreadable prior choice.
142
+
143
+ Consent gates **remit only**. With consent off, runs still write the local Run
144
+ Record and `findings`/`export` still work. No run is ever blocked on telemetry,
145
+ and telemetry is never shown to shoppers or merchant-facing approval viewers.
146
+
147
+ ## Data Boundary
148
+
149
+ Run Telemetry carries the run's structure and identity, not raw artifact bodies,
150
+ and applies light minimization to identifying-but-non-essential fields.
151
+
152
+ Included:
153
+
154
+ - run identity (`map_id`, `campaign_slug`, `template_family`) — these are the
155
+ join keys that make the telemetry useful;
156
+ - structural signal (doctor codes, spec-rule IDs, adapter decisions, QA
157
+ disposition, finding IDs);
158
+ - artifact refs (`path`, `schema_version`, `sha256`), counts, classifications.
159
+
160
+ Minimized / excluded:
161
+
162
+ - **Absolute local paths** → relativized or hashed (no contributor filesystem
163
+ layout). **OS username** → omitted.
164
+ - **Raw artifact bodies** → never (full CampaignSpec JSON, source HTML,
165
+ full QA verdict / doctor / report bodies). Excluded for **size and noise** —
166
+ the value is the structured signal, not raw dumps.
167
+
168
+ This is minimization, not a security allowlist: campaigns-os runs use a fixed
169
+ synthetic test customer and a publishable client-side API key, so there is no
170
+ secret/PII exposure to defend against. The path/username scrub is hygiene for a
171
+ public package any agency may run.
172
+
173
+ ## Capture Surfaces
174
+
175
+ The Run Record is assembled from several local inputs, all correlated by
176
+ `run_id`:
177
+
178
+ - **System signal** — extracted from this run's doctor output, Assembly Report,
179
+ and QA verdict (reusing the same artifact readers `findings harvest` uses).
180
+ - **`findings harvest`** — proposes Workflow Findings from doctor blockers,
181
+ selected warnings, and report blockers; `--write` appends them. Under an
182
+ active run session, written findings inherit the session `run_id`; explicit
183
+ `--run-id` still wins. Harvested system findings default to
184
+ `safe_to_share: false` because raw doctor/report messages can contain
185
+ merchant URLs, source-copy snippets, or local artifact references. An operator
186
+ or redaction pass must approve sharing.
187
+ - **`findings add`** — flags-first manual capture for operators and agents.
188
+ Under an active run session, new findings inherit the session `run_id`;
189
+ explicit `--run-id` still wins.
190
+ - **Tiny Prompts** — skippable one-line stage-boundary prompts. Skipped prompts
191
+ record nothing.
192
+
193
+ The findings journal stays `.campaign-runtime/workflow-findings.jsonl`, append-
194
+ only and the **single writer** for findings. New findings carry an optional
195
+ `run_id` (backward-compatible schema addition) so the Run Record's snapshot of
196
+ "this run's findings" is exact rather than inferred from timestamps.
197
+
198
+ ## Remit Channel
199
+
200
+ Remit reuses the QA-verdict publishing rails. Extract one shared helper rather
201
+ than duplicate the fetch/try-catch:
202
+
203
+ ```text
204
+ remit(path, payload, proxyBase) // mirrors qa-node.mjs postVerdict
205
+ ```
206
+
207
+ - **Consent-gated** — only sends when the resolver says yes.
208
+ - **Non-fatal** — a failed POST never blocks or fails the run (mirrors "never
209
+ fail the run if publish is unreachable").
210
+ - **Keyed on `run_id`** — the payload carries it and the receiver stores one
211
+ record per id, rejecting a repeat POST for a stored id with 409. This client
212
+ POSTs only; there is no replace verb. So a failed send may be retried and a
213
+ succeeded one may not be re-sent. Endpoint: `/api/runs` (implemented; receives
214
+ at the canonical remit scope).
215
+ - **Durable status** — the local Run Record records `remit_attempted`,
216
+ `remit_ok`, `remit_error` and `remit_state` so a dropped send is visible,
217
+ not silent. No background retry daemon. The outcome is classified by what
218
+ the receiver answered, not by whether the transport threw:
219
+ - a parsed 2xx is `ok` (`stored`);
220
+ - a **409** is `ok` (`already_stored`): the receiver already holds this
221
+ `run_id`, which is the outcome the send was for — reached by an earlier
222
+ send whose answer was lost, or by a re-run. The body's error token does
223
+ not change this; 409 on this endpoint means exactly one thing;
224
+ - a 2xx whose body is not JSON is `ok` (`ok_unparsed_ack`) with
225
+ `remit_error` set to `Remit POST <status>: acknowledged with a body that
226
+ is not JSON: <excerpt>`, so the anomaly stays on the record;
227
+ - any other non-2xx is `failed` (`refused`) with `remit_error` `Remit POST
228
+ <status>: <statusText> <body>`; a transport failure (refused connection,
229
+ timeout, the proxy-base gate) is `failed` (`transport_error`).
230
+
231
+ The classification and the resolved base — as a kind, `canonical` /
232
+ `loopback` / `proxy`, never the host — are on the record itself since
233
+ surface 1.28.0 as `remit_result` (one of the five outcomes above, or null
234
+ when no send was attempted) and `remit_base_kind` (null when no send was
235
+ attempted); a prior outcome carried forward keeps both. They also travel,
236
+ with the HTTP status, in the `run-record --json` summary under `remit`
237
+ (`result`, `http_status`, `base_kind`, `sent`, `preserved`) and in the text
238
+ `Remit:` line. The summary's `result` is additionally `not_contacted` when
239
+ the record on disk was already `ok` and the receiver was not asked (below),
240
+ or null when nothing was sent and nothing is known (`--no-remit`, consent
241
+ off).
242
+ - **The QA verdict publish is recorded beside the remit** — since surface
243
+ 1.33.0 a record carries an optional `qa_verdict_publish` block: the
244
+ verdict's own `run_id` (the publish idempotency key — distinct from the
245
+ record's), the `publisher` (`qa run` for the run's own post, `qa publish`
246
+ for a later post of the stored verdict), `attempted` / `ok` / `error` /
247
+ `endpoint` (`/api/qa/verdicts`), a `state` (`skipped` when the run's
248
+ publish was off, `ok`, `failed`), the `result` in the same vocabulary as
249
+ `remit_result`, the `base_kind`, and `published_at`. `qa run` hands the
250
+ block to the session through its QA attempt, so `run end` and the auto-end
251
+ stamp it; `qa publish` stamps the record whose `qa_verdict` artifact
252
+ references the verdict, reads `state: "ok"` as already published, and
253
+ refuses without `--republish`. A stored `ok` is never downgraded by a later
254
+ failed send or by a reassembly. Absent on records written before the field
255
+ existed and on runs that produced no verdict.
256
+ - **Re-runs never downgrade a durable outcome** — `run-record` is keyed on
257
+ `run_id`, and `run end`, the QA auto-end and the recovery action `next`
258
+ prints all go through it. Before writing, it reads the record already under
259
+ that id. A record whose remit is `ok` is final: it is neither re-sent (the
260
+ receiver would refuse it) nor rewritten (a reassembly is at best thinner
261
+ than what the session wrote, and would then disagree with the stored copy);
262
+ the command reports `written: false`, `remit.result: "not_contacted"`
263
+ (distinct from `already_stored`, which is a 409 the receiver answered),
264
+ `remit.sent: false`, and the text line `Remit: ok (already stored at the
265
+ receiver for this run id; not re-sent)`. A prior counts only when it is a
266
+ valid Run Record — the same validator that gates `writeRunRecord` — so a
267
+ file that merely says `remit_state: "ok"` is replaced like a corrupt one. A prior `failed` or `pending`
268
+ send is retried when the run may send, and carried forward unchanged
269
+ (`remit.preserved: true`) when it may not — `--no-remit` or consent off
270
+ over a failed remit does not file it as `skipped`. Only a `--no-write` run
271
+ reads nothing, because it writes and sends nothing.
272
+ - **Which `run_id` a run-record is keyed on** — `--run-id` names it; without
273
+ one the active run session's id is used; without a session, the most recent
274
+ Run Record on disk for this packet's campaign (same `identity.map_id` and
275
+ `identity.campaign_slug` as the packet — the closeout-recognition match
276
+ below, newest `created_at` first) is re-emitted under **its** id; only when
277
+ no such record exists is a fresh id minted. `run end` clears the session,
278
+ so before this every run-record after close minted, and the closeout
279
+ action `next` prints or a re-emit after a sidecar fix filed a second Run
280
+ Record for a run that already had one. The summary says which happened:
281
+ `run_id_source` is `explicit`, `session`, `latest_record` or `minted`, in
282
+ `--json` and on the text `Run ID: <id> (<source>)` line, and a
283
+ `latest_record` run first prints `Run ID <id> is the most recent Run Record
284
+ for this campaign; re-emitting it in place. Pass --new-run to start a new
285
+ run under a fresh id, or --list to see every record for this packet.` The
286
+ source is on the command's envelope only, never on the record. `--new-run`
287
+ mints regardless of what is on disk (it is refused beside `--run-id`).
288
+ `--list` is inspection: it prints, newest first, every record for this
289
+ packet's campaign (`run_id`, `created_at`, `remit_state`, `remit_result`,
290
+ `remit_endpoint`, `record_path`), then the id the next plain run would use
291
+ and its source, and — like `--no-write` — assembles, writes and sends
292
+ nothing (`list: true`, `written: false`, `remit.sent: false` in `--json`).
293
+ - **The stored copy states its outcome** — the record the receiver holds is,
294
+ by construction, one whose send landed, so the body sent carries
295
+ `remit_state: "ok"`, `remit_attempted: true`, `remit_ok: true`, the
296
+ endpoint, `remit_result: "stored"` and the `remit_base_kind` the send
297
+ resolved to. The local file carries the `pending` sentinel only between its
298
+ first write and the answer.
299
+ - **Tenant-scoped** — the remit sends the packet's Campaigns API key (packet,
300
+ then the packet-local CampaignSpec, then the declared `env:` source) as the
301
+ `X-Campaign-Key` header. The receiver hashes it server-side into
302
+ `campaign_key_hash`, which its tenant-scoped `GET /api/runs` joins on. A
303
+ record remitted without the header is stored but reachable only through the
304
+ cross-tenant admin listing or by known `run_id` — every record this CLI
305
+ remitted before 2026-09-10 is in that state. The key never enters the record.
306
+ - **Readable back** — `campaigns-os telemetry list --packet <json>` lists the
307
+ tenant scope; `campaigns-os telemetry list` with `CAMPAIGN_OPS_ADMIN_KEY` set
308
+ (or `--admin-key-env <VAR>`) lists cross-tenant, unscoped records included.
309
+ A 2xx is a listing only when its body carries `runs[]`: any other body (a
310
+ maintenance page, an intermediary's HTML) is an error naming the status and
311
+ an excerpt, and exits non-zero, rather than "showing 0 of 0 returned".
312
+ - **Shape-checked before it leaves the machine** — a credential is validated,
313
+ and its destination vetted, before a socket is opened:
314
+ - The resolved campaign key must look like a campaign key: 8-256 characters
315
+ of letters, digits, dot, dash, and underscore, one line, no whitespace.
316
+ A value that is *present but malformed* (a quoted key, a pasted JSON blob,
317
+ a URL) is refused, and the refusal names its **source** — the env var, the
318
+ packet field, or the CampaignSpec — and never its value. `telemetry list
319
+ --packet` fails fast on such a value and sends nothing; the remit rail,
320
+ which is non-fatal by contract, warns on stderr and sends without a tenant
321
+ scope. That is now distinguishable in the output from "no key was
322
+ configured". Consent gates the whole thing: with no send attempted
323
+ (consent off, or `--no-remit`) the key is never read and nothing is said.
324
+ `api_key_source` must additionally name a variable matching
325
+ `^(?=[A-Z])[A-Z0-9_]*CAMPAIGN[A-Z0-9_]*$` — upper-case, starting with a
326
+ letter, containing `CAMPAIGN` anywhere, so the documented default
327
+ `CAMPAIGNS_API_KEY` qualifies — so a packet cannot route an arbitrary
328
+ secret into the header; a variable outside that shape is refused by name
329
+ and its value is never read. `campaigns-os doctor` reads the key through
330
+ the same resolver: a refused value is its `campaign.api_key_rejected`
331
+ warning, naming the source, while a key that is simply not configured
332
+ stays `campaign.api_key_source`.
333
+ - `--proxy-base` must be `https:`. A loopback host (`localhost`,
334
+ `127.0.0.1`, `[::1]`) may be plain http for a local receiver, and each
335
+ such request prints one stderr warning that the credential travels in
336
+ clear. Any other plain-http base is refused before the request — so a
337
+ remit or verdict publish aimed at a plain-http remote proxy now fails
338
+ (non-fatally, recorded in `remit_error`) instead of sending the key in the
339
+ clear. The ops admin key keeps its stricter rule on top of this: it goes
340
+ only to the canonical scope, a loopback receiver, or a base the operator
341
+ vouched for with `--trust-proxy-base`.
342
+
343
+ The public package only emits and remits; it does not cluster, route, summarize
344
+ across runs, or create issues.
345
+
346
+ ## Public / Internal Boundary
347
+
348
+ - **Public `campaigns-os`** owns: the Run Record schema, local capture, the
349
+ consent resolver, and the remit channel. Capturing or opting out must never
350
+ require internal Next Commerce access.
351
+ - **Internal tooling** owns: ingestion, clustering, surface-mapping, trend
352
+ analysis, and turning the backlog into improvement candidates. The loop closes
353
+ through normal development — the system does not edit itself.
354
+
355
+ ## Non-Goals
356
+
357
+ - Do not replace the Build Packet, Assembly Report, doctor output, or QA
358
+ Verdicts — the Run Record references the proof trail, it is not the proof
359
+ trail.
360
+ - Do not auto-run QA or typed-card test orders.
361
+ - Do not edit skills, templates, or rules automatically (no auto-codegen).
362
+ - Do not ship raw artifact bodies, absolute local paths, or OS usernames.
363
+ - Do not block a build on telemetry, and do not expose telemetry to shoppers or
364
+ merchant-facing approval viewers.
365
+ - Do not record skipped Tiny Prompts.
366
+ - Do not add a background retry daemon for failed remit.
367
+
368
+ ## Implementation Sequence
369
+
370
+ The core implementation is landed. This sequence is retained as an orientation
371
+ map for the code paths and tests that own each slice.
372
+
373
+ 1. **Run Record schema** (`campaigns-os-run-record/v0`): envelope + canonical
374
+ `run_id` + artifact-ref shape + normalized observation arrays + `surfaces[]`
375
+ taxonomy. Add optional `run_id` to the Workflow Finding schema.
376
+ 2. **Run identity + local capture**: mint/thread `run_id`; assemble the manifest
377
+ in `src/run-record.mjs` from existing artifact readers + `readJournal`;
378
+ write `.campaign-runtime/run-records/<run_id>.json`. cli.mjs stays thin
379
+ dispatch.
380
+ 3. **Consent resolver + `telemetry` command**: user-level config, env override
381
+ with fail-closed parsing, shared resolver called by every remitting command.
382
+ 4. **Remit**: shared `remit()` helper, consent-gated, non-fatal, idempotent on
383
+ `run_id`, with local remit status.
384
+
385
+ `findings add` / `harvest` / `export` remain local-first and become the findings
386
+ channel of the Run Record.
387
+
388
+ ## Run Sessions (ambient capture)
389
+
390
+ Operators (and the agents driving them) should not have to thread `--run-id` /
391
+ `--lifecycle-journal` on every command. A **run session** makes capture ambient:
392
+
393
+ - `campaigns-os run start [--packet <p>]` mints one `run_id`, picks the
394
+ lifecycle journal, and writes `.campaign-runtime/run-session.json`. With
395
+ `--packet` the session (and the managed `.gitignore` block) lands in the
396
+ packet's target repo — `assembly.target_repo` resolved from the packet's
397
+ directory, else that directory — whatever the cwd, the same root the
398
+ auto-opener behind `start` / `prepare-build` uses; without it, at cwd. A
399
+ packet that exists but does not parse is refused (no session is opened on a
400
+ guessed root); one not written yet roots on its own directory with a warning.
401
+ - Every command then auto-discovers that session (walking up from cwd, or
402
+ from the `--packet` it was handed) and shares its `run_id` + journal **with
403
+ no per-command flags**. `start` / `prepare-build` / `build` take a
404
+ `--target`, not a packet: the first opens the target's session, and a
405
+ repeated one against the same target joins it from any cwd, so every intake
406
+ attempt lands in the same journal (a session bound to a different packet is
407
+ a conflict to end, not one to write into). Findings commands
408
+ also inherit the active `run_id` when writing findings. Explicit `--run-id` /
409
+ `--lifecycle-journal` still wins; `CAMPAIGNS_OS_TELEMETRY` consent still gates
410
+ remit.
411
+ - Each `campaigns-os qa run` records its full local verdict path on the active
412
+ session. A blocked verdict keeps that session open for repair and another QA
413
+ attempt. A ready or ready-with-exceptions verdict auto-assembles the
414
+ aggregated Run Record with references to every attempt, then clears the
415
+ session. Pass `--no-remit` to skip remit for that local Run Record.
416
+ Because the session's close is what remits the session's `run_id`, the
417
+ `run-record` closeout command a QA run prints carries `--no-remit` whenever
418
+ the attempt does not end the session — a **blocked** verdict, or a disposition
419
+ the toolkit does not recognise: the session stays open, so that command would
420
+ share its id, and assembling an interim record is useful while spending the
421
+ session's one accepted POST on it is not. A session-ending verdict auto-ends
422
+ in the same process, before the printed command can run, so there the command
423
+ finds no session and resolves the `run_id` the way any sessionless run does
424
+ (below): to the record the auto-end just wrote, which it re-emits in place —
425
+ and leaves as written when its remit landed. One exported set decides which
426
+ dispositions end a session, read by both
427
+ the auto-end and the closeout, so the two cannot disagree about who owns the
428
+ `run_id`. When
429
+ an auto-end's own remit does not close, the auto-end says so and names the
430
+ local record to keep. It does not print a re-send command: `run-record
431
+ --run-id` reassembles rather than reloads (see below), and the session whose
432
+ attempt references the record carries is already cleared.
433
+ - An explicit `--packet` associates commands, `run status`, and `run end` with
434
+ the target campaign session even from the toolkit or another project
435
+ directory, and `run start --packet` opens it there.
436
+ If cwd and packet resolve to different active sessions, the command fails
437
+ with both run IDs instead of silently cross-writing lifecycle evidence.
438
+ - `campaigns-os run end` remains the manual close path for non-QA or interrupted
439
+ sessions. `run status` reports the active session.
440
+ - Sessions older than 12 hours are treated as stale and are not auto-discovered,
441
+ so a later work session does not inherit an old `run_id` or lifecycle journal.
442
+ A stale session is closed out, not abandoned: the next `start`,
443
+ `prepare-build`, or `build` at that `--target`, or `run start` / `run end`
444
+ (at the `--packet`'s target repo, else at cwd), assembles its Run Record
445
+ from the lifecycle journal (remit under the
446
+ usual consent) and removes the file before opening a new session. A stale
447
+ session whose packet is gone is cleared with a stderr note and no record.
448
+ `run status` reports a stale file but never sweeps it.
449
+
450
+ The session file is transient, machine-local, and lives under the
451
+ scrubber-ignored `.campaign-runtime/`.
452
+
453
+ ## Closeout recognition (`next` reads the records it demands)
454
+
455
+ Nothing in the CLI used to read `.campaign-runtime/run-records/`, so `next` at
456
+ stage `done` demanded a Run Record unconditionally — including for runs that had
457
+ already assembled, closed, and remitted one. `next` now reads that directory and
458
+ decides whether a **matching, current, successfully closed** record exists for
459
+ the packet it was called with.
460
+
461
+ The reading is deliberately conservative. Records are machine-local (they are in
462
+ the managed `.gitignore` block), so an absent directory is the normal case and
463
+ never an error; the scan is bounded and wrapped, and a slow, unreadable, or
464
+ corrupt records directory can never fail or stall orchestration. **Any doubt
465
+ emits the closeout.** A false demand costs one idempotent command; false silence
466
+ loses the run's durable record.
467
+
468
+ A record satisfies closeout only when all of these hold:
469
+
470
+ 1. **Identity** — its `identity.map_id` and `identity.campaign_slug` equal the
471
+ packet's `spec.map_id` and `campaign.public_route_slug`. Both sides must
472
+ assert an identity; a record that names neither is not evidence about this
473
+ campaign.
474
+ 2. **Currency** — its `created_at` is not earlier than the newest `checked_at` /
475
+ `completed_at` on the report's `doctor` and `qa` stages. An older report that
476
+ carries no such timestamps contributes no floor rather than a fabricated one.
477
+ 3. **Artifacts** — when the report's `qa` stage points at a QA verdict, one of
478
+ the record's `qa_verdict` artifact references must carry that verdict's
479
+ current SHA-256. (A record may reference several verdicts: a session retains
480
+ each blocked repair attempt alongside the one that passed.) Verdict identity
481
+ only — the assembly report's own hash drifts the instant a producer writes a
482
+ stage, so including it would make every record instantly outdated.
483
+ 4. **Closure** — `remit_state` is `ok`, or `skipped`. **`skipped` counts as
484
+ closed**: it is the consent-off / `--no-remit` / local-only path, a deliberate
485
+ non-remit rather than a failure.
486
+
487
+ The newest matching record decides, so an older good record can never mask a
488
+ newer broken one.
489
+
490
+ ### Reason codes
491
+
492
+ | Code | `next` emits |
493
+ |---|---|
494
+ | `satisfied` | a non-required `run_record_present` action naming the record and its path |
495
+ | `no_record` | the required `run_record_closeout` (the plain command; with no record for this campaign, `run-record` mints) |
496
+ | `foreign_campaign` | the required `run_record_closeout` (plain; the records on disk belong to other campaigns and are never reused) |
497
+ | `stale_predates_evidence` | the required `run_record_closeout` carrying `--new-run`: the superseded record is the newest one for the campaign, so the plain command would re-emit it in place |
498
+ | `outdated_artifacts` | the required `run_record_closeout` carrying `--new-run`, for the same reason |
499
+ | `remit_failed` | the required `run_record_remit_recovery` |
500
+ | `remit_incomplete` | the required `run_record_remit_recovery` |
501
+
502
+ A failed or never-finished remit is **not** a missing record, and must not be
503
+ answered by minting a second one — that would fork the run's identity. Recovery
504
+ re-runs `run-record` against the record already on disk:
505
+
506
+ ```bash
507
+ campaigns-os run-record --packet <packet> --run-id <existing-run-id> --json
508
+ ```
509
+
510
+ The receiver may or may not hold the id already (a send whose answer was lost
511
+ after the store, say). Either answer closes the record: a 2xx stores it, and a
512
+ 409 is read as `already_stored` — `remit_state: ok` — so the recovery converges
513
+ instead of stamping `failed` over the record and being demanded again. A record
514
+ whose remit is already `ok` on disk is never re-sent and never rewritten by this
515
+ command; it reports `not_contacted` and leaves the file as written.
516
+
517
+ Read the command for what it is on a record that is not yet stored: it
518
+ **reassembles** the record under that `run_id`, it does not reload and re-send
519
+ the file already written. Anything the record held that came only from the run
520
+ session — the QA attempt references a repaired run collects across several
521
+ attempts — is gone once the session is cleared, so on a multi-attempt run this
522
+ replaces the unsent record with a thinner one and sends that. Re-sending the
523
+ persisted record is not implemented. Until it is, treat the local file as the
524
+ durable artifact and recover the remit only for a run whose record the current
525
+ disk state can still reproduce.
526
+
527
+ The plain closeout command (no `--run-id`) is not a way around this: with no
528
+ session it resolves to the same newest record and re-emits it in place, so it
529
+ reaches a new id only through `--new-run` or when no record for the campaign
530
+ exists. `run-record --list` shows which ids exist before choosing.
531
+
532
+ An active run session still wins: with an ambient session open, `done` emits the
533
+ required `run end` exactly as before, satisfied or not.
534
+
535
+ `campaigns-os qa run`'s own closeout action is unchanged. It fires while the
536
+ record for that verdict cannot exist yet, so it is correctly unconditional.
537
+
538
+ ### Latest QA identity cannot disagree with latest QA status
539
+
540
+ The QA producer owns `stages.qa.verdict_run_id`, `stages.qa.evidence`, and
541
+ `stages.qa.purchase_proof`. Before this, only the canonical fields
542
+ (status/outputs/timestamps) refreshed, and hand-authored extension fields
543
+ survived untouched — so a stage could carry a passing status and today's output
544
+ links beside a previous run's id and an `evidence.remaining_blocker` describing
545
+ a bug that had since been fixed.
546
+
547
+ Prior evidence is **preserved, not deleted**: the previous `verdict_run_id` /
548
+ `evidence` pair moves into a bounded `history[]` on the same stage, oldest first,
549
+ carrying its **own original status and `checked_at`**. A stage that had no
550
+ `checked_at` yields a history entry with no `checked_at` — an absent timestamp
551
+ stays absent rather than being stamped with now, because manufactured provenance
552
+ is worse than the stale field it replaces. Re-recording the same verdict does not
553
+ grow history. `evidence` has two schema-legal shapes, object and array, and both
554
+ archive — an array of operator notes is preserved as history rather than dropped
555
+ on the next producer write. Every other extension field on the stage (`waivers`,
556
+ and anything an out-of-repo consumer writes) passes through a producer write
557
+ verbatim.
558
+
559
+ These fields are additive under the assembly-report stage definition, which
560
+ already permits additional properties; no schema and no surface version moved.
561
+
562
+ ## Deferred (not v0)
563
+
564
+ - Command-lifecycle instrumentation — **landed (T6).** A `withCommandLifecycle`
565
+ wrapper times every command and captures its command name, argv shape, and
566
+ exit status. Persistence is active when an explicit `--lifecycle-journal` /
567
+ env `CAMPAIGNS_OS_LIFECYCLE_LOG`, or an ambient run session, is present;
568
+ entries append to `.campaign-runtime/command-lifecycle.jsonl`.
569
+ - Stage timings and repair-loop count — **landed.** `run-record` aggregates the
570
+ whole lifecycle journal for a `run_id` (Tier 1): each command invocation
571
+ becomes a `lifecycle.stages[]` entry (with per-stage `exit_status`),
572
+ `repair_loop_count` counts command re-runs, and run-level `duration_ms` sums
573
+ active command time instead of idle wall-clock gaps between invocations;
574
+ `wall_clock_duration_ms` reports that full outer span separately.
575
+ `started_at` / `completed_at` preserve the observed bounds. Heavy
576
+ commands mark their own sub-phases (Tier 2), which aggregate into
577
+ `command:phase` stages. The cross-command `run_id` is threaded automatically
578
+ by the run session (Tier 3), so these fields populate with real data from a
579
+ normal "talk to your agent and build" flow — no manual flag bookkeeping.
580
+ - Internal ingestion / clustering / surface-mapping — internal tooling
581
+ (ADR-019), not this package.
582
+
583
+ ## Open Questions
584
+
585
+ - Final envelope field list + exact observation-array shapes (resolved when the
586
+ schema is authored against current packet / report / verdict artifacts).
587
+ - `/api/runs` payload envelope + upsert semantics (aligned with the QA verdict
588
+ publishing rails).