@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.
- package/AGENTS.md +204 -0
- package/CHANGELOG.md +5002 -0
- package/CONTEXT.md +685 -0
- package/LICENSE +202 -0
- package/NOTICE +4 -0
- package/README.md +368 -0
- package/agents/claude/CLAUDE.md +32 -0
- package/agents/codex/AGENTS.md +27 -0
- package/agents/copilot/copilot-instructions.md +14 -0
- package/agents/cursor/campaigns-os.mdc +13 -0
- package/bin/campaigns-os.mjs +38 -0
- package/campaign-spec/README.md +138 -0
- package/campaign-spec/dist/analytics-vocabulary.d.ts +47 -0
- package/campaign-spec/dist/analytics-vocabulary.js +74 -0
- package/campaign-spec/dist/index.d.ts +40 -0
- package/campaign-spec/dist/index.js +77 -0
- package/campaign-spec/dist/normalize.d.ts +22 -0
- package/campaign-spec/dist/normalize.js +41 -0
- package/campaign-spec/dist/routing.d.ts +190 -0
- package/campaign-spec/dist/routing.js +263 -0
- package/campaign-spec/dist/rules/analytics-contract-shape.d.ts +42 -0
- package/campaign-spec/dist/rules/analytics-contract-shape.js +303 -0
- package/campaign-spec/dist/rules/assembly-hints-shape.d.ts +42 -0
- package/campaign-spec/dist/rules/assembly-hints-shape.js +191 -0
- package/campaign-spec/dist/rules/campaign-metadata.d.ts +14 -0
- package/campaign-spec/dist/rules/campaign-metadata.js +40 -0
- package/campaign-spec/dist/rules/checkout-has-success-url.d.ts +31 -0
- package/campaign-spec/dist/rules/checkout-has-success-url.js +66 -0
- package/campaign-spec/dist/rules/cycle-detection.d.ts +12 -0
- package/campaign-spec/dist/rules/cycle-detection.js +141 -0
- package/campaign-spec/dist/rules/design-source-shape.d.ts +29 -0
- package/campaign-spec/dist/rules/design-source-shape.js +142 -0
- package/campaign-spec/dist/rules/downsell-without-upsell.d.ts +11 -0
- package/campaign-spec/dist/rules/downsell-without-upsell.js +40 -0
- package/campaign-spec/dist/rules/exit-intent-validation.d.ts +23 -0
- package/campaign-spec/dist/rules/exit-intent-validation.js +147 -0
- package/campaign-spec/dist/rules/funnel-count.d.ts +9 -0
- package/campaign-spec/dist/rules/funnel-count.js +37 -0
- package/campaign-spec/dist/rules/funnel-hypothesis-length.d.ts +22 -0
- package/campaign-spec/dist/rules/funnel-hypothesis-length.js +60 -0
- package/campaign-spec/dist/rules/funnel-identity.d.ts +15 -0
- package/campaign-spec/dist/rules/funnel-identity.js +57 -0
- package/campaign-spec/dist/rules/funnel-weight-sum.d.ts +20 -0
- package/campaign-spec/dist/rules/funnel-weight-sum.js +66 -0
- package/campaign-spec/dist/rules/index.d.ts +69 -0
- package/campaign-spec/dist/rules/index.js +131 -0
- package/campaign-spec/dist/rules/offer-ref-integrity.d.ts +13 -0
- package/campaign-spec/dist/rules/offer-ref-integrity.js +59 -0
- package/campaign-spec/dist/rules/package-pricing-sanity.d.ts +12 -0
- package/campaign-spec/dist/rules/package-pricing-sanity.js +42 -0
- package/campaign-spec/dist/rules/page-count.d.ts +11 -0
- package/campaign-spec/dist/rules/page-count.js +31 -0
- package/campaign-spec/dist/rules/page-id-uniqueness.d.ts +14 -0
- package/campaign-spec/dist/rules/page-id-uniqueness.js +47 -0
- package/campaign-spec/dist/rules/promo-code-input-validation.d.ts +8 -0
- package/campaign-spec/dist/rules/promo-code-input-validation.js +126 -0
- package/campaign-spec/dist/rules/promo-codes-shape.d.ts +30 -0
- package/campaign-spec/dist/rules/promo-codes-shape.js +187 -0
- package/campaign-spec/dist/rules/route-field-ignored-for-page-type.d.ts +33 -0
- package/campaign-spec/dist/rules/route-field-ignored-for-page-type.js +81 -0
- package/campaign-spec/dist/rules/route-target-resolves.d.ts +32 -0
- package/campaign-spec/dist/rules/route-target-resolves.js +112 -0
- package/campaign-spec/dist/rules/schema-version.d.ts +21 -0
- package/campaign-spec/dist/rules/schema-version.js +53 -0
- package/campaign-spec/dist/rules/sdk-version.d.ts +26 -0
- package/campaign-spec/dist/rules/sdk-version.js +96 -0
- package/campaign-spec/dist/rules/shipping-countries-shape.d.ts +10 -0
- package/campaign-spec/dist/rules/shipping-countries-shape.js +30 -0
- package/campaign-spec/dist/rules/shipping-methods-present.d.ts +10 -0
- package/campaign-spec/dist/rules/shipping-methods-present.js +26 -0
- package/campaign-spec/dist/rules/store-profile-shape.d.ts +30 -0
- package/campaign-spec/dist/rules/store-profile-shape.js +127 -0
- package/campaign-spec/dist/rules/thank-you-requirement.d.ts +16 -0
- package/campaign-spec/dist/rules/thank-you-requirement.js +50 -0
- package/campaign-spec/dist/rules/unknown-top-level-fields.d.ts +28 -0
- package/campaign-spec/dist/rules/unknown-top-level-fields.js +114 -0
- package/campaign-spec/dist/rules/upsell-has-packages.d.ts +9 -0
- package/campaign-spec/dist/rules/upsell-has-packages.js +35 -0
- package/campaign-spec/dist/rules/upsell-routing-complete.d.ts +10 -0
- package/campaign-spec/dist/rules/upsell-routing-complete.js +45 -0
- package/campaign-spec/dist/rules/upsell-without-checkout.d.ts +11 -0
- package/campaign-spec/dist/rules/upsell-without-checkout.js +44 -0
- package/campaign-spec/dist/rules/variant-labels-shape.d.ts +28 -0
- package/campaign-spec/dist/rules/variant-labels-shape.js +86 -0
- package/campaign-spec/dist/sdk-version-parse.d.ts +43 -0
- package/campaign-spec/dist/sdk-version-parse.js +62 -0
- package/campaign-spec/dist/types.d.ts +674 -0
- package/campaign-spec/dist/types.js +40 -0
- package/campaign-spec/package.json +12 -0
- package/compatibility.json +25 -0
- package/contracts/agent-relevant-change-policy.v1.json +111 -0
- package/contracts/brand-theme-source-defaults.figma-sections-export.v0.json +32 -0
- package/contracts/brand-theme-target-tokens.next-core.v0.json +65 -0
- package/contracts/campaign-cart-checkout-field-contract.v0.json +45 -0
- package/contracts/campaign-cart-sdk-support-policy.v0.json +11 -0
- package/contracts/commerce-surface-catalog.json +2452 -0
- package/contracts/fixtures/orientation/canonicalization/v1.json +34 -0
- package/contracts/fixtures/orientation/envelope/current.json +95 -0
- package/contracts/fixtures/orientation/envelope/freshness_unknown.json +97 -0
- package/contracts/fixtures/orientation/envelope/legacy_baseline.json +95 -0
- package/contracts/fixtures/orientation/envelope/orientation_available.json +136 -0
- package/contracts/fixtures/orientation/envelope/recovered_interrupted_update.json +136 -0
- package/contracts/fixtures/orientation/envelope/refused.json +100 -0
- package/contracts/fixtures/orientation/envelope/restart_required.json +137 -0
- package/contracts/fixtures/orientation/envelope/updated.json +135 -0
- package/contracts/fixtures/orientation/hostile-target/README.md +61 -0
- package/contracts/fixtures/orientation/hostile-target/manifest.json +82 -0
- package/contracts/fixtures/orientation/hostile-target/repo/CHANGELOG.md +18 -0
- package/contracts/fixtures/orientation/hostile-target/repo/bin/intended.mjs +12 -0
- package/contracts/fixtures/orientation/hostile-target/repo/bin/tripwire.mjs +15 -0
- package/contracts/fixtures/orientation/hostile-target/repo/contracts/release-ledger.json +48 -0
- package/contracts/fixtures/orientation/hostile-target/repo/contracts/supported-surface.json +17 -0
- package/contracts/fixtures/orientation/hostile-target/repo/docs/example-contract.md +13 -0
- package/contracts/fixtures/orientation/hostile-target/repo/hooks/post-checkout +5 -0
- package/contracts/fixtures/orientation/hostile-target/repo/hooks/post-merge +5 -0
- package/contracts/fixtures/orientation/hostile-target/repo/hooks/pre-commit +5 -0
- package/contracts/fixtures/orientation/hostile-target/repo/hostile-dependency-tripwire/package.json +15 -0
- package/contracts/fixtures/orientation/hostile-target/repo/hostile-dependency-tripwire/tripwire.mjs +9 -0
- package/contracts/fixtures/orientation/hostile-target/repo/package.json +20 -0
- package/contracts/fixtures/orientation/hostile-target/repo/schemas/example.v0.schema.json +15 -0
- package/contracts/fixtures/orientation/release-gate/cases.json +1073 -0
- package/contracts/fixtures/runtime-recipe/accept/current.json +299 -0
- package/contracts/fixtures/runtime-recipe/accept/minimal.json +294 -0
- package/contracts/fixtures/runtime-recipe/dist-states.json +51 -0
- package/contracts/fixtures/runtime-recipe/manifest.json +85 -0
- package/contracts/fixtures/runtime-recipe/reject/advisory-enforcement.json +299 -0
- package/contracts/fixtures/runtime-recipe/reject/allowlist-without-hosts.json +297 -0
- package/contracts/fixtures/runtime-recipe/reject/committed-output-claim.json +299 -0
- package/contracts/fixtures/runtime-recipe/reject/engines-disagreement-warns.json +299 -0
- package/contracts/fixtures/runtime-recipe/reject/lifecycle-scripts-enabled.json +299 -0
- package/contracts/fixtures/runtime-recipe/reject/missing-required-field.json +251 -0
- package/contracts/fixtures/runtime-recipe/reject/unknown-kind.json +299 -0
- package/contracts/fixtures/runtime-recipe/reject/unknown-network-policy.json +299 -0
- package/contracts/fixtures/runtime-recipe/reject/unknown-output-check.json +310 -0
- package/contracts/fixtures/runtime-recipe/reject/unknown-revision.json +299 -0
- package/contracts/fixtures/runtime-recipe/reject/unknown-step-id.json +299 -0
- package/contracts/fixtures/runtime-recipe/reject/unperformable-check-skipped.json +299 -0
- package/contracts/fixtures/runtime-recipe/reject/unpinned-lockfile.json +299 -0
- package/contracts/fixtures/sidecar-bundle/production-shaped/.campaign-runtime/assembly-report.json +180 -0
- package/contracts/fixtures/sidecar-bundle/production-shaped/.campaign-runtime/build-context.json +115 -0
- package/contracts/fixtures/sidecar-bundle/production-shaped/.campaign-runtime/doctor-output.json +29 -0
- package/contracts/fixtures/sidecar-bundle/production-shaped/.campaign-runtime/qa-verdict.json +27 -0
- package/contracts/fixtures/sidecar-bundle/production-shaped/campaign-runtime.build.json +141 -0
- package/contracts/migration-sidecar-bundle.v0.json +149 -0
- package/contracts/orientation-limits.v1.json +41 -0
- package/contracts/orientation-reason-codes.v1.json +196 -0
- package/contracts/private-template-sources.json +8 -0
- package/contracts/release-ledger.json +4598 -0
- package/contracts/reserved-skill-names.json +13 -0
- package/contracts/runtime-recipe.campaigns-os-node-v1.json +299 -0
- package/contracts/supported-surface.json +180 -0
- package/contracts/template-brand-contract.apollo-mv-single-step.v0.json +27 -0
- package/contracts/template-brand-contract.apollo.v0.json +27 -0
- package/contracts/template-brand-contract.demeter.v0.json +27 -0
- package/contracts/template-brand-contract.olympus-mv-single-step.v0.json +27 -0
- package/contracts/template-brand-contract.olympus-mv-two-step.v0.json +28 -0
- package/contracts/template-brand-contract.olympus.v0.json +27 -0
- package/contracts/template-brand-contract.shared-commerce.v0.json +190 -0
- package/contracts/template-brand-contract.shop-single-step.v0.json +27 -0
- package/contracts/template-brand-contract.shop-three-step.v0.json +29 -0
- package/contracts/template-slot-manifest.apollo-mv-single-step.v0.json +14 -0
- package/contracts/template-slot-manifest.apollo.v0.json +12 -0
- package/contracts/template-slot-manifest.demeter.v0.json +30 -0
- package/contracts/template-slot-manifest.olympus-mv-single-step.v0.json +14 -0
- package/contracts/template-slot-manifest.olympus-mv-two-step.v0.json +15 -0
- package/contracts/template-slot-manifest.olympus.v0.json +12 -0
- package/contracts/template-slot-manifest.shared-content-core.v0.json +4254 -0
- package/contracts/template-slot-manifest.shop-single-step.v0.json +47 -0
- package/contracts/template-slot-manifest.shop-three-step.v0.json +33 -0
- package/docs/brand-theme-bridge.md +159 -0
- package/docs/build-packet.md +1300 -0
- package/docs/campaign-build-brief.md +145 -0
- package/docs/campaign-standardization-report.md +329 -0
- package/docs/campaigns-os-build-flow.md +117 -0
- package/docs/design-source-package.md +784 -0
- package/docs/legacy-migration.md +58 -0
- package/docs/migration-sidecar-bundle.md +139 -0
- package/docs/orientation-contract-reference.md +1220 -0
- package/docs/polish-evidence.md +502 -0
- package/docs/qa-and-test-orders.md +1691 -0
- package/docs/release-ledger-authoring-guide.md +274 -0
- package/docs/runtime-readiness.md +211 -0
- package/docs/supported-surface.md +83 -0
- package/docs/versioning.md +55 -0
- package/docs/workflow-findings-sidecar.md +588 -0
- package/package.json +135 -0
- package/prompts/first-build.md +27 -0
- package/prompts/friction-log.md +30 -0
- package/schemas/campaign-build-brief.v1.schema.json +149 -0
- package/schemas/campaign-design-source-package.v0.schema.json +697 -0
- package/schemas/campaign-runtime-assembly-report.v0.schema.json +390 -0
- package/schemas/campaign-runtime-build-context.v0.schema.json +339 -0
- package/schemas/campaign-runtime-build-packet.v0.schema.json +452 -0
- package/schemas/campaign-spec.v4.schema.json +582 -0
- package/schemas/campaigns-os-doctor-output.v0.schema.json +43 -0
- package/schemas/campaigns-os-legacy-migration-inventory.v0.schema.json +112 -0
- package/schemas/campaigns-os-legacy-provisioning-plan.v0.schema.json +38 -0
- package/schemas/campaigns-os-legacy-provisioning-receipt.v0.schema.json +57 -0
- package/schemas/campaigns-os-qa-verdict-sidecar.v0.schema.json +150 -0
- package/schemas/campaigns-os-qa-verdict.v0.schema.json +438 -0
- package/schemas/campaigns-os-release-ledger.v1.schema.json +162 -0
- package/schemas/campaigns-os-run-record.v0.schema.json +343 -0
- package/schemas/campaigns-os-runtime-recipe.v1.schema.json +313 -0
- package/schemas/campaigns-os-sidecar-bundle-conformance.v0.schema.json +78 -0
- package/schemas/campaigns-os-tooling-orientation.v1.schema.json +381 -0
- package/schemas/campaigns-os-workflow-finding.v0.schema.json +134 -0
- package/schemas/source-html-manifest.v0.schema.json +252 -0
- package/skills/next-campaigns-build/SKILL.md +73 -0
- package/skills/next-campaigns-os/SKILL.md +101 -0
- package/skills/next-campaigns-os/references/session-intake.md +160 -0
- package/skills/next-campaigns-os-setup/SKILL.md +22 -0
- package/skills/next-campaigns-polish/SKILL.md +145 -0
- package/skills/next-campaigns-qa/SKILL.md +92 -0
- package/skills.json +56 -0
- package/skills.sh +64 -0
- package/src/adapter-decision-contract.mjs +333 -0
- package/src/brand-theme.mjs +1151 -0
- package/src/browser-launch.mjs +79 -0
- package/src/build-brief.mjs +781 -0
- package/src/built-site-scope.mjs +312 -0
- package/src/campaign-ecosystem.mjs +734 -0
- package/src/campaign-identity.mjs +405 -0
- package/src/campaign-workspace.mjs +127 -0
- package/src/checkpoint-waiver.mjs +302 -0
- package/src/cli.mjs +13442 -0
- package/src/commercial-journey.mjs +1119 -0
- package/src/commercial-parity.mjs +965 -0
- package/src/consent.mjs +347 -0
- package/src/content-residue.mjs +322 -0
- package/src/deadline.mjs +81 -0
- package/src/design-source-package.mjs +2604 -0
- package/src/deviation.mjs +107 -0
- package/src/doctor-check-registry.mjs +49 -0
- package/src/doctor-sidecar.mjs +106 -0
- package/src/finding-cause.mjs +557 -0
- package/src/findings.mjs +326 -0
- package/src/fs-identity.mjs +68 -0
- package/src/gate-actions.mjs +105 -0
- package/src/html-scan.mjs +53 -0
- package/src/install-mode.mjs +272 -0
- package/src/legacy-migration.d.ts +128 -0
- package/src/legacy-migration.mjs +510 -0
- package/src/lifecycle.mjs +338 -0
- package/src/local-proof.mjs +401 -0
- package/src/map-pin-writeback.mjs +210 -0
- package/src/orchestration-stage-contract.mjs +81 -0
- package/src/package-install-fixture.mjs +33 -0
- package/src/page-kit-build-summary.mjs +175 -0
- package/src/page-kit-campaign-config.mjs +57 -0
- package/src/page-kit-sdk-version.mjs +392 -0
- package/src/page-kit-store-profile.mjs +369 -0
- package/src/page-kit-sync.mjs +162 -0
- package/src/polish-browser.mjs +867 -0
- package/src/polish-capture.mjs +1094 -0
- package/src/polish-deadline.mjs +51 -0
- package/src/polish-gate.mjs +739 -0
- package/src/polish-node.mjs +639 -0
- package/src/polish-page-load.mjs +1100 -0
- package/src/private-template-source.mjs +237 -0
- package/src/proof-policy.mjs +82 -0
- package/src/qa-analytics-correctness.mjs +307 -0
- package/src/qa-analytics-errors.mjs +38 -0
- package/src/qa-analytics-parity.mjs +699 -0
- package/src/qa-binding-evidence.mjs +140 -0
- package/src/qa-browser.mjs +6608 -0
- package/src/qa-cart-entry.mjs +406 -0
- package/src/qa-commercial-parity.mjs +641 -0
- package/src/qa-node.mjs +3620 -0
- package/src/qa-order-bump.mjs +381 -0
- package/src/qa-parity-capture.mjs +428 -0
- package/src/qa-parity-fixture.mjs +359 -0
- package/src/qa-publish.mjs +362 -0
- package/src/qa-purchase-data-layer.mjs +263 -0
- package/src/qa-route-probe.mjs +272 -0
- package/src/qa-sidecar.mjs +188 -0
- package/src/qa-test-order-topology.mjs +207 -0
- package/src/qa-url-privacy.mjs +13 -0
- package/src/qa-verdict-discovery.mjs +192 -0
- package/src/qa-verdict-publish.mjs +105 -0
- package/src/qa-verdict.mjs +287 -0
- package/src/remit.mjs +388 -0
- package/src/repo-scan.mjs +83 -0
- package/src/route-identity.mjs +133 -0
- package/src/run-record-closeout.mjs +229 -0
- package/src/run-record.mjs +839 -0
- package/src/run-session.mjs +226 -0
- package/src/runtime-state-ignore.mjs +113 -0
- package/src/sdk-attribute-index.mjs +212 -0
- package/src/sdk-markup.mjs +358 -0
- package/src/sdk-meta-tags.mjs +52 -0
- package/src/shell-token.mjs +7 -0
- package/src/sidecar-bundle.mjs +399 -0
- package/src/source-asset-crawl.mjs +469 -0
- package/src/source-html-intake.mjs +627 -0
- package/src/source-html-manifest.mjs +276 -0
- package/src/source-prep.mjs +284 -0
- package/src/spec-derive-store.mjs +431 -0
- package/src/spec-derive.mjs +514 -0
- package/src/spec-fetch.mjs +66 -0
- package/src/spec-hash.mjs +48 -0
- package/src/spec-identity.mjs +27 -0
- package/src/stage-ledger.mjs +509 -0
- package/src/standardization-report.mjs +1297 -0
- package/src/template-brand-contract.mjs +473 -0
- package/src/template-freshness.mjs +196 -0
- package/src/template-reference.mjs +81 -0
- package/src/template-slot-manifest.mjs +150 -0
- package/src/text-safety.mjs +62 -0
- package/src/theme-gate.mjs +185 -0
- 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).
|