@nextcommerce/campaigns-os 1.37.2 → 1.41.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/AGENTS.md +115 -11
  2. package/CHANGELOG.md +556 -0
  3. package/README.md +43 -36
  4. package/contracts/agent-relevant-change-policy.v1.json +11 -1
  5. package/contracts/effects.v1.json +4794 -0
  6. package/contracts/release-ledger.json +873 -0
  7. package/contracts/supported-surface.json +25 -5
  8. package/docs/activation-and-evidence.md +1 -1
  9. package/docs/build-packet.md +27 -16
  10. package/docs/demo-preview.md +3 -4
  11. package/docs/diagnostics.md +7 -4
  12. package/docs/effects.md +281 -0
  13. package/docs/gateway-login.md +113 -0
  14. package/docs/orientation-contract-reference.md +4 -1
  15. package/docs/progress-snapshots.md +3 -3
  16. package/docs/qa-and-test-orders.md +3 -3
  17. package/docs/readback.md +523 -0
  18. package/docs/runtime-readiness.md +1 -1
  19. package/docs/sdk-storage-compatibility.md +1 -1
  20. package/docs/skills-revision.md +364 -0
  21. package/docs/supported-surface.md +12 -4
  22. package/docs/versioning.md +8 -4
  23. package/package.json +8 -3
  24. package/schemas/campaign-runtime-build-packet.v0.schema.json +5 -0
  25. package/schemas/campaigns-os-effects.v1.schema.json +211 -0
  26. package/schemas/campaigns-os-readback.v2.schema.json +267 -0
  27. package/skills/campaign-lifecycle-orientation/SKILL.md +174 -0
  28. package/skills/campaign-readback-classification/SKILL.md +230 -0
  29. package/skills/campaign-run-evidence/SKILL.md +140 -0
  30. package/skills/contribution-intake/SKILL.md +85 -0
  31. package/skills/next-campaigns-build/SKILL.md +33 -12
  32. package/skills/next-campaigns-os/SKILL.md +45 -21
  33. package/skills/next-campaigns-os-setup/SKILL.md +35 -14
  34. package/skills/next-campaigns-polish/SKILL.md +43 -17
  35. package/skills/next-campaigns-qa/SKILL.md +48 -24
  36. package/skills.json +39 -6
  37. package/src/admin-transport.mjs +123 -0
  38. package/src/cli.mjs +991 -200
  39. package/src/credential-store.mjs +183 -0
  40. package/src/deviation.mjs +3 -2
  41. package/src/diagnostic.mjs +4 -1
  42. package/src/gate-actions.mjs +2 -2
  43. package/src/install-mode.mjs +17 -9
  44. package/src/lifecycle.mjs +95 -0
  45. package/src/login.mjs +152 -0
  46. package/src/package-install-fixture.mjs +3 -2
  47. package/src/qa-node.mjs +56 -19
  48. package/src/qa-publish.mjs +108 -2
  49. package/src/readback.mjs +1936 -0
  50. package/src/remit.mjs +17 -3
@@ -0,0 +1,211 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://nextcommerce.com/schemas/campaigns-os-effects.v1.schema.json",
4
+ "title": "Campaigns OS Declared Command Effects v1",
5
+ "description": "The shape of contracts/effects.v1.json: one row per supported invocation — a command, its subcommand, and the flag combination that changes what the invocation does to the world — stating what it writes, what it sends, the MCP-style annotations a tool face would carry, the effect tier, and the name of the node:test case that proves the row. The claim a row makes is two-sided and both sides are tested: nothing the row does not declare may change, and every declared effect whose `observed_in` names a condition must be seen in it. An effect the offline fixture cannot reach carries an empty `observed_in` and a `not_observed_reason`. Read this with docs/effects.md; scripts/check-effects.mjs enforces the parts a schema cannot (coverage of the supported CLI surface, and the existence of every named test case).",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": ["schema", "contract_version", "vocabulary", "rows"],
9
+ "properties": {
10
+ "schema": {
11
+ "const": "campaigns-os-effects/v1",
12
+ "description": "A consumer should refuse a payload whose schema it does not recognize rather than reading the fields it happens to know."
13
+ },
14
+ "_note": { "type": "string" },
15
+ "contract_version": {
16
+ "type": "string",
17
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$",
18
+ "description": "The contract's own version, independent of the package's surface_version: adding a row is additive, changing a row's declared effect is not."
19
+ },
20
+ "vocabulary": {
21
+ "type": "object",
22
+ "description": "The words the rows use, defined once. A reader must not have to infer what `tier: \"C\"` or `{lifecycle-journal}` means from the rows that happen to use them.",
23
+ "additionalProperties": false,
24
+ "required": ["annotations", "tiers", "locations", "conditions", "effect_changing_flags", "test_scope", "observed_in", "preflight"],
25
+ "properties": {
26
+ "annotations": {
27
+ "type": "object",
28
+ "additionalProperties": false,
29
+ "required": ["readOnlyHint", "destructiveHint", "openWorldHint", "idempotentHint"],
30
+ "properties": {
31
+ "readOnlyHint": { "type": "string" },
32
+ "destructiveHint": { "type": "string" },
33
+ "openWorldHint": { "type": "string" },
34
+ "idempotentHint": { "type": "string" }
35
+ }
36
+ },
37
+ "tiers": {
38
+ "type": "object",
39
+ "additionalProperties": false,
40
+ "required": ["none", "A", "B", "C", "_ranking"],
41
+ "properties": {
42
+ "none": { "type": "string" },
43
+ "A": { "type": "string" },
44
+ "B": { "type": "string" },
45
+ "C": { "type": "string" },
46
+ "_ranking": { "type": "string" }
47
+ }
48
+ },
49
+ "locations": {
50
+ "type": "object",
51
+ "description": "The location tokens a row's write paths may use. A path pattern is relative to the token it opens with.",
52
+ "minProperties": 1,
53
+ "additionalProperties": { "type": "string" }
54
+ },
55
+ "conditions": {
56
+ "type": "object",
57
+ "description": "The five conditions every row is exercised under. The fifth, persisted_consent, is not a variant of the other four: it is the one that does not take the row's word for whether telemetry consent is on.",
58
+ "additionalProperties": false,
59
+ "required": ["no_session", "ambient_session", "stale_session", "lifecycle_log", "persisted_consent"],
60
+ "properties": {
61
+ "no_session": { "type": "string" },
62
+ "ambient_session": { "type": "string" },
63
+ "stale_session": { "type": "string" },
64
+ "lifecycle_log": { "type": "string" },
65
+ "persisted_consent": { "type": "string" }
66
+ }
67
+ },
68
+ "effect_changing_flags": {
69
+ "type": "array",
70
+ "description": "The flags that change what an invocation does to the world. Every one of them that appears on a CLI help usage line owes a row of its own; scripts/check-effects.mjs refuses the file when one does not have it. A flag that only changes the output shape (--json, --report) is deliberately absent.",
71
+ "minItems": 1,
72
+ "uniqueItems": true,
73
+ "items": { "type": "string", "pattern": "^--[a-z0-9-]+$" }
74
+ },
75
+ "test_scope": {
76
+ "type": "object",
77
+ "additionalProperties": false,
78
+ "required": ["full", "preflight"],
79
+ "properties": { "full": { "type": "string" }, "preflight": { "type": "string" } }
80
+ },
81
+ "observed_in": { "type": "string" },
82
+ "preflight": { "type": "string" }
83
+ }
84
+ },
85
+ "rows": {
86
+ "type": "array",
87
+ "minItems": 1,
88
+ "items": { "$ref": "#/$defs/row" }
89
+ }
90
+ },
91
+ "$defs": {
92
+ "condition": {
93
+ "enum": ["no_session", "ambient_session", "stale_session", "lifecycle_log", "persisted_consent"]
94
+ },
95
+ "observed_in": {
96
+ "type": "array",
97
+ "description": "The conditions in which the effect test MUST see this effect. Empty declares an effect the offline fixture cannot reach, and then `not_observed_reason` is required.",
98
+ "items": { "$ref": "#/$defs/condition" },
99
+ "uniqueItems": true
100
+ },
101
+ "row": {
102
+ "type": "object",
103
+ "additionalProperties": false,
104
+ "required": ["command", "subcommand", "flags", "annotations", "tier", "writes", "sends", "effect_test", "test_scope", "notes"],
105
+ "properties": {
106
+ "command": {
107
+ "type": "string",
108
+ "minLength": 1,
109
+ "description": "The top-level command, or the reserved `*refused*` for an invocation refused before its handler runs."
110
+ },
111
+ "subcommand": {
112
+ "description": "The subcommand as the help text spells it (`policy set` is one subcommand, not a subcommand and a flag), or null.",
113
+ "oneOf": [{ "type": "string", "minLength": 1 }, { "type": "null" }]
114
+ },
115
+ "flags": {
116
+ "type": "array",
117
+ "description": "The effect-changing flag combination this row is about. Empty is the base form. Flags that do not change what the invocation does to the world (--json, --report) are deliberately absent.",
118
+ "items": { "type": "string", "pattern": "^--[a-z0-9-]+$" },
119
+ "uniqueItems": true
120
+ },
121
+ "annotations": {
122
+ "type": "object",
123
+ "additionalProperties": false,
124
+ "required": ["readOnlyHint", "destructiveHint", "openWorldHint", "idempotentHint"],
125
+ "properties": {
126
+ "readOnlyHint": { "type": "boolean" },
127
+ "destructiveHint": { "type": "boolean" },
128
+ "openWorldHint": { "type": "boolean" },
129
+ "idempotentHint": { "type": "boolean" }
130
+ }
131
+ },
132
+ "tier": { "enum": ["none", "A", "B", "C"] },
133
+ "writes": { "type": "array", "items": { "$ref": "#/$defs/write" } },
134
+ "sends": { "type": "array", "items": { "$ref": "#/$defs/send" } },
135
+ "effect_test": {
136
+ "type": "string",
137
+ "minLength": 1,
138
+ "description": "The exact name of the node:test case in src/effects.test.mjs that proves this row. check-effects.mjs refuses a row whose case is not there."
139
+ },
140
+ "test_scope": { "enum": ["full", "preflight"] },
141
+ "preflight": { "$ref": "#/$defs/preflight" },
142
+ "notes": {
143
+ "type": "string",
144
+ "minLength": 1,
145
+ "description": "Why the row says what it says, in a consumer's terms. A preflight row states what it cannot reach offline and what the case proves instead."
146
+ }
147
+ },
148
+ "if": { "properties": { "test_scope": { "const": "preflight" } }, "required": ["test_scope"] },
149
+ "then": {
150
+ "description": "A preflight row declares its allowances. The converse — a `full` row that carries them — is reported by scripts/check-effects.mjs, which can say which row it is.",
151
+ "required": ["preflight"],
152
+ "properties": { "preflight": { "$ref": "#/$defs/preflight" } }
153
+ }
154
+ },
155
+ "preflight": {
156
+ "type": "object",
157
+ "description": "What the preflight itself may do, declared independently of the effects the row declares for the invocation that gets past it. Without this, a write declared for the success path ({home}/** for a minted credential) also licensed the refusal to write anywhere under the home directory, and a destination a loopback receiver only stands in for licensed any request path at all.",
158
+ "additionalProperties": false,
159
+ "required": ["may_write", "may_contact"],
160
+ "properties": {
161
+ "may_write": {
162
+ "type": "array",
163
+ "description": "The ONLY paths the preflight may write, each a location token or a path under one. A pattern spanning segments (**) is refused by scripts/check-effects.mjs: an allowance that licenses a subtree licenses the home directory the preflight must not touch.",
164
+ "uniqueItems": true,
165
+ "items": { "type": "string", "minLength": 1 }
166
+ },
167
+ "may_contact": {
168
+ "type": "array",
169
+ "description": "The exact request paths the loopback receiver may see, matched literally against the request path. Empty means the preflight contacts nothing.",
170
+ "uniqueItems": true,
171
+ "items": { "type": "string", "minLength": 1 }
172
+ }
173
+ }
174
+ },
175
+ "write": {
176
+ "type": "object",
177
+ "additionalProperties": false,
178
+ "required": ["path", "when", "observed_in"],
179
+ "properties": {
180
+ "path": {
181
+ "type": "string",
182
+ "minLength": 1,
183
+ "description": "A glob pattern (`*` within one segment, `**` across segments) opening with a location token from vocabulary.locations."
184
+ },
185
+ "when": { "type": "string", "minLength": 1 },
186
+ "observed_in": { "$ref": "#/$defs/observed_in" },
187
+ "not_observed_reason": { "type": "string", "minLength": 1 }
188
+ }
189
+ },
190
+ "send": {
191
+ "type": "object",
192
+ "additionalProperties": false,
193
+ "required": ["destination", "what", "when", "requires_consent", "observed_in"],
194
+ "properties": {
195
+ "destination": {
196
+ "type": "string",
197
+ "minLength": 1,
198
+ "description": "The endpoint, with the same location tokens. A destination of the form `{proxy-base}/<path>` pins the request path down, so the test can assert the declared destination IS the one contacted."
199
+ },
200
+ "what": { "type": "string", "minLength": 1 },
201
+ "when": { "type": "string", "minLength": 1 },
202
+ "requires_consent": {
203
+ "type": "boolean",
204
+ "description": "True when Run Telemetry consent gates the send. The effect test runs such a row with consent ON and a loopback receiver, so 'nothing was sent' can never be an artefact of consent being off."
205
+ },
206
+ "observed_in": { "$ref": "#/$defs/observed_in" },
207
+ "not_observed_reason": { "type": "string", "minLength": 1 }
208
+ }
209
+ }
210
+ }
211
+ }
@@ -0,0 +1,267 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://nextcommerce.com/schemas/campaigns-os-readback.v2.schema.json",
4
+ "title": "Campaigns OS Run-Artifact Readback v2",
5
+ "description": "The machine-readable projection `campaigns-os readback <target> --json` writes to stdout. It serializes exactly what the rendered text view computes over a run's already-emitted artifacts (Build Packet, doctor output, build context, assembly report, QA verdict, findings export) and adds no interpretation the text view does not also carry. The readback is read-only: it writes nothing under the target, starts no process, touches no network, and records no lifecycle entry. v2 differs from v1 in one field's meaning: staleness is assessed PER ARTIFACT, and the aggregate `stale` is true when ANY loaded artifact is stale, where v1 compared only the newest artifact and so reported a set fresh whenever its newest member was. Field semantics and the exact `clean` rule live in docs/readback.md; that document and this schema move together. A consumer that recognizes v2 accepts and preserves unknown additive properties without interpreting them.",
6
+ "type": "object",
7
+ "additionalProperties": true,
8
+ "required": [
9
+ "schema_version",
10
+ "artifacts",
11
+ "packet_selection",
12
+ "staleness",
13
+ "doctor",
14
+ "skip_cascades",
15
+ "divergences",
16
+ "clean"
17
+ ],
18
+ "properties": {
19
+ "schema_version": {
20
+ "const": "campaigns-os-readback/v2",
21
+ "description": "A consumer should refuse a payload whose schema_version it does not recognize rather than reading the fields it happens to know."
22
+ },
23
+ "artifacts": {
24
+ "type": "array",
25
+ "description": "One record per artifact the readback projected, in the fixed render order (packet, doctor, context, report, qa_verdict, findings), restricted to the keys the caller asked for. The artifact's own parsed contents are deliberately absent: a consumer that wants an artifact's payload reads that artifact, so this projection never becomes an unversioned mirror of every upstream schema.",
26
+ "items": { "$ref": "#/$defs/artifact_row" }
27
+ },
28
+ "packet_selection": {
29
+ "description": "How the projected Build Packet was chosen, or null when a programmatic caller built a payload without a selection record. The CLI always supplies one.",
30
+ "oneOf": [{ "$ref": "#/$defs/packet_selection" }, { "type": "null" }]
31
+ },
32
+ "staleness": {
33
+ "description": "The per-artifact freshness assessment, or null when a programmatic caller built a payload without one. The CLI always supplies one.",
34
+ "oneOf": [{ "$ref": "#/$defs/staleness" }, { "type": "null" }]
35
+ },
36
+ "doctor": { "$ref": "#/$defs/doctor_summary" },
37
+ "skip_cascades": {
38
+ "type": "array",
39
+ "description": "Skipped QA assertions grouped by the failure that blocked them, derived from each skipped assertion's evidence.blocked_by. One record per distinct blocker, in first-seen order. Empty when no QA verdict loaded or none of its assertions were skipped. The grouping is the readback's own projection layer.",
40
+ "items": { "$ref": "#/$defs/skip_cascade" }
41
+ },
42
+ "divergences": {
43
+ "type": "array",
44
+ "description": "Where two artifacts record different states for one stage: the assembly report calls a stage completed while the QA verdict fails an assertion whose id is namespaced to that stage. One record per stage, in the order the failures were seen. The readback reports the disagreement and does not adjudicate it. This too is the readback's own layer.",
45
+ "items": { "$ref": "#/$defs/divergence" }
46
+ },
47
+ "clean": {
48
+ "type": "boolean",
49
+ "description": "True only when every artifact the readback found is loaded or absent (never unreadable or unrecognized), staleness is computable and NO loaded artifact is stale, staleness.unparseable_keys is empty (no loaded artifact recorded an age the readback could not read), divergences is empty, and doctor.error_count is zero. A readback-integrity flag, not a verdict: a blocked QA verdict whose artifacts all read cleanly is still clean, because the readback saw exactly what Campaigns OS recorded. docs/readback.md states the rule and its limits."
50
+ }
51
+ },
52
+ "$defs": {
53
+ "artifact_key": {
54
+ "type": "string",
55
+ "enum": ["packet", "doctor", "context", "report", "qa_verdict", "findings"]
56
+ },
57
+ "utc_timestamp": {
58
+ "type": "string",
59
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z$",
60
+ "description": "ISO-8601 UTC at second precision with a Z suffix, rendered by the same formatter the text view uses so both modes name the same instant identically."
61
+ },
62
+ "artifact_row": {
63
+ "type": "object",
64
+ "additionalProperties": true,
65
+ "required": ["key", "path", "state", "detail"],
66
+ "properties": {
67
+ "key": { "$ref": "#/$defs/artifact_key" },
68
+ "path": {
69
+ "type": "string",
70
+ "minLength": 1,
71
+ "description": "The path the readback read, as resolved from the target root, a --<artifact> override, or — for packet — Build Packet discovery. --example reports the bundled sample's paths relative to the package root so the sample's output is identical wherever the package is installed."
72
+ },
73
+ "state": {
74
+ "type": "string",
75
+ "enum": ["loaded", "absent", "unreadable", "unrecognized"],
76
+ "description": "loaded: read and recognized. absent: no file at that path. unreadable: the bytes could not be read or parsed (over the 32 MiB read bound, not UTF-8, not JSON). unrecognized: parsed, but not a shape this readback projects."
77
+ },
78
+ "detail": {
79
+ "type": "string",
80
+ "description": "The readback's explanation for a non-loaded state, or — for a loaded artifact named in staleness.unparseable_keys — the shape of the generated_at value that did not parse, so a consumer reading artifacts alone can see that this artifact's age was never established. The value itself is deliberately not reproduced here. The empty string for every other loaded artifact and for an absent one."
81
+ }
82
+ }
83
+ },
84
+ "packet_selection": {
85
+ "type": "object",
86
+ "additionalProperties": true,
87
+ "required": ["mode", "signal", "selected", "candidates_considered", "rejected"],
88
+ "properties": {
89
+ "mode": {
90
+ "type": "string",
91
+ "enum": ["explicit", "default", "discovered"],
92
+ "description": "explicit: the caller passed --packet. default: the chosen packet is the default-named campaign-runtime.build.json, including a target with no packet at all, where the default path is still the path reported absent. discovered: freshness selected a suffixed packet."
93
+ },
94
+ "signal": {
95
+ "type": "string",
96
+ "enum": ["explicit", "sole_candidate", "generated_at", "none"],
97
+ "description": "What decided it. Freshness is the packet's own recorded generated_at, never the file's modification time: an mtime is rewritten by a clone, a checkout, or a copy without any run having recorded anything, so content-only selection keeps discovery a pure function of the bytes on disk."
98
+ },
99
+ "selected": {
100
+ "description": "The file name discovery chose, or null when no discovery chose one (the explicit and none signals). The full path is the packet row's path in artifacts.",
101
+ "type": ["string", "null"]
102
+ },
103
+ "candidates_considered": {
104
+ "type": "array",
105
+ "items": { "type": "string", "minLength": 1 },
106
+ "description": "The file names of the valid candidates discovery compared, default-first then name order. Empty for explicit mode, where no discovery runs."
107
+ },
108
+ "rejected": {
109
+ "type": "array",
110
+ "items": { "$ref": "#/$defs/rejected_candidate" },
111
+ "description": "Candidates discovery skipped. Root-level files matching the packet naming land here when they did not parse, were not an object, or did not declare a recognized packet schema_version. When no valid root candidate exists, a packet sitting only at .campaign-runtime/campaign-runtime.build.json is recorded here too: that sidecar location is not a discovery candidate, and the reason tells the caller to pass --packet."
112
+ }
113
+ }
114
+ },
115
+ "rejected_candidate": {
116
+ "type": "object",
117
+ "additionalProperties": true,
118
+ "required": ["path", "reason"],
119
+ "properties": {
120
+ "path": { "type": "string", "minLength": 1 },
121
+ "reason": { "type": "string", "minLength": 1 }
122
+ }
123
+ },
124
+ "staleness": {
125
+ "type": "object",
126
+ "additionalProperties": true,
127
+ "required": [
128
+ "computable",
129
+ "stale",
130
+ "stale_keys",
131
+ "unparseable_keys",
132
+ "artifacts",
133
+ "newest_key",
134
+ "head_time",
135
+ "head_detail",
136
+ "artifact_times"
137
+ ],
138
+ "properties": {
139
+ "computable": {
140
+ "type": "boolean",
141
+ "description": "True when at least one loaded artifact carried a parseable generated_at AND the checkout's HEAD reflog was readable."
142
+ },
143
+ "stale": {
144
+ "type": "boolean",
145
+ "description": "True when ANY loaded artifact's generated_at predates the last recorded HEAD movement. This is the v1 -> v2 change of meaning: v1 compared only the newest artifact, so one regenerated artifact reported the whole set fresh. Always false when computable is false; absence of the signal is not evidence of freshness."
146
+ },
147
+ "stale_keys": {
148
+ "type": "array",
149
+ "items": { "$ref": "#/$defs/artifact_key" },
150
+ "description": "The stale artifact keys, in the fixed render order. Empty when nothing is stale or the comparison is not computable."
151
+ },
152
+ "unparseable_keys": {
153
+ "type": "array",
154
+ "items": { "$ref": "#/$defs/artifact_key" },
155
+ "description": "The loaded artifacts that RECORDED a generated_at this readback could not parse, in the fixed render order. Their age was never established — they are neither fresh nor stale, they are absent from artifacts and artifact_times, and clean is false while this list is non-empty, because an unknown age must never read as a fresh one. Each one's artifact row carries the shape of the refused value in its detail. An artifact that carries NO generated_at key at all is a different case: it recorded no age to check, so it is absent from this list and is not by itself unclean (though with no other artifact carrying one, the comparison is not computable). Independent of computable and stale, which keep their meanings: a set whose only comparable artifact is fresh still reports stale false."
156
+ },
157
+ "artifacts": {
158
+ "type": "object",
159
+ "additionalProperties": { "$ref": "#/$defs/artifact_freshness" },
160
+ "description": "Every loaded artifact that carried a parseable generated_at, keyed by artifact key, in render order. An artifact with no parseable generated_at is neither fresh nor stale and is absent from this map; if it is the only artifact, the assessment is not computable. Where it recorded a generated_at that did not parse, unparseable_keys names it."
161
+ },
162
+ "newest_key": {
163
+ "description": "The artifact key holding the newest generated_at, or null when not computable. Information only in v2: it no longer decides the aggregate.",
164
+ "oneOf": [{ "$ref": "#/$defs/artifact_key" }, { "type": "null" }]
165
+ },
166
+ "head_time": {
167
+ "description": "The last recorded HEAD movement, or null when the reflog gave no usable time.",
168
+ "oneOf": [{ "$ref": "#/$defs/utc_timestamp" }, { "type": "null" }]
169
+ },
170
+ "head_detail": {
171
+ "type": "string",
172
+ "description": "Why head_time is null; the empty string when it is not."
173
+ },
174
+ "artifact_times": {
175
+ "type": "object",
176
+ "additionalProperties": { "$ref": "#/$defs/utc_timestamp" },
177
+ "description": "Every loaded artifact's parseable generated_at, keyed by artifact key. Carried unchanged from v1 for consumers that already read it; artifacts carries the same instants plus each one's verdict."
178
+ }
179
+ }
180
+ },
181
+ "artifact_freshness": {
182
+ "type": "object",
183
+ "additionalProperties": true,
184
+ "required": ["generated_at", "stale"],
185
+ "properties": {
186
+ "generated_at": { "$ref": "#/$defs/utc_timestamp" },
187
+ "stale": {
188
+ "type": "boolean",
189
+ "description": "True when the last recorded HEAD movement is later than this artifact's generated_at. False when there is no HEAD signal to compare against."
190
+ }
191
+ }
192
+ },
193
+ "doctor_summary": {
194
+ "type": "object",
195
+ "additionalProperties": true,
196
+ "required": ["present", "status", "error_count", "warning_count", "warning_groups"],
197
+ "properties": {
198
+ "present": {
199
+ "type": "boolean",
200
+ "description": "Whether a doctor output loaded. When false, status is null and both counts are 0: those zeros record an absence, not an observation."
201
+ },
202
+ "status": {
203
+ "type": ["string", "null"],
204
+ "description": "Doctor's own status string, unreinterpreted."
205
+ },
206
+ "error_count": {
207
+ "type": "integer",
208
+ "minimum": 0,
209
+ "description": "The length of doctor's errors list (0 when the field is missing or not a list)."
210
+ },
211
+ "warning_count": {
212
+ "type": "integer",
213
+ "minimum": 0,
214
+ "description": "The total number of object-shaped warnings grouped below."
215
+ },
216
+ "warning_groups": {
217
+ "type": "object",
218
+ "additionalProperties": true,
219
+ "required": ["contract-static", "repo-observed"],
220
+ "description": "The readback's own two-way grouping of doctor warnings, carrying each warning as doctor wrote it. The labels are the readback's projection layer, not doctor's vocabulary.",
221
+ "properties": {
222
+ "contract-static": {
223
+ "type": "array",
224
+ "items": { "type": "object" },
225
+ "description": "Codes under the frontmatter.* prefixes. These restate the template family's shared frontmatter contract and repeat verbatim on every doctor pass while that contract is in force. Their persistence does not mean a flagged value is still unfixed, and their disappearance is not how a fix is confirmed. A gate must not treat these as repository state."
226
+ },
227
+ "repo-observed": {
228
+ "type": "array",
229
+ "items": { "type": "object" },
230
+ "description": "Every other code: not in the contract-static table, so its message reflects this repository, spec, or build as doctor saw it."
231
+ }
232
+ }
233
+ }
234
+ }
235
+ },
236
+ "skip_cascade": {
237
+ "type": "object",
238
+ "additionalProperties": true,
239
+ "required": ["blocked_by", "families"],
240
+ "properties": {
241
+ "blocked_by": {
242
+ "type": "string",
243
+ "minLength": 1,
244
+ "description": "The recorded blocking assertion, or the literal \"(no blocked_by recorded)\" when the verdict named none."
245
+ },
246
+ "families": {
247
+ "type": "array",
248
+ "items": { "type": "string" },
249
+ "description": "The skipped assertions' families, falling back to the assertion id and then to \"(unnamed)\"."
250
+ }
251
+ }
252
+ },
253
+ "divergence": {
254
+ "type": "object",
255
+ "additionalProperties": true,
256
+ "required": ["stage", "assertion_ids"],
257
+ "properties": {
258
+ "stage": { "type": "string", "minLength": 1 },
259
+ "assertion_ids": {
260
+ "type": "array",
261
+ "items": { "type": "string" },
262
+ "description": "The failing QA assertion ids attributed to that stage."
263
+ }
264
+ }
265
+ }
266
+ }
267
+ }
@@ -0,0 +1,174 @@
1
+ ---
2
+ name: campaign-lifecycle-orientation
3
+ version: 1.0.3
4
+ description: Orient a reader to the Campaigns OS lifecycle artifacts a run has already emitted, without advancing any stage or changing any state.
5
+ ---
6
+
7
+ Bundle revision: 1.41.2+skills.1
8
+ Run `npx --no-install campaigns-os tooling status --skills-revision 1.41.2+skills.1`
9
+ from the campaign's Page Kit folder, where it runs the project's pinned copy and
10
+ never installs one, at the start of each task. Start a fresh session if it
11
+ reports `mismatch`: this text is already in your context and is never re-read
12
+ while the CLI on disk can move under it. If the output has no `Skills revision:`
13
+ line (no `revision_check` under `--json`), a campaigns-os older than this check
14
+ answered; follow none of its actions and run the pinned copy.
15
+
16
+ # Campaign lifecycle orientation
17
+
18
+ Use this skill to place Build Packet, Assembly Report and doctor language in the
19
+ pipeline and read what a run already recorded. It teaches interpretation only.
20
+ It advances no stage, and nothing in it is permission to run a command that
21
+ writes.
22
+
23
+ The effect class in each parenthetical below is the declared row of
24
+ `contracts/effects.v1.json` (`none` < `B` writes < `A` sends < `C`
25
+ destructive). Read that file, not this text, when an exact path or endpoint
26
+ matters.
27
+
28
+ ## Cite only the supported surface
29
+
30
+ For Campaigns OS facts, cite `contracts/supported-surface.json` and the entries
31
+ it names — `CONTEXT.md`, `CHANGELOG.md`, `skills.json`, the `contracts/`,
32
+ `schemas/` and `docs/` entries listed there, and the published CLI path. Never
33
+ cite `src/` or `scripts/`: those are implementation and may change without a
34
+ supported-surface bump, so a reader cannot check them and a rename would not
35
+ reach this text. `docs/supported-surface.md` is the prose twin of that list.
36
+
37
+ Identity comes from the tool, not from a file beside the session.
38
+ `campaigns-os tooling status --json` (tier `B`: its only write is the
39
+ command-lifecycle journal) reports the install mode, the version, and a source
40
+ commit when one is derivable. Quote what it printed. Do not derive the answer
41
+ from Git HEAD, from the word "latest", or from prose in a checkout.
42
+
43
+ ## Start with the shared vocabulary
44
+
45
+ `CONTEXT.md` is the glossary kept reconciled against the code, and it separates
46
+ public lifecycle language from internal implementation language. A
47
+ **CampaignSpec** is the JSON campaign contract; current packets use CampaignSpec
48
+ 4.2, whose funnel structure lives in `funnels[]`
49
+ (`schemas/campaign-spec.v4.schema.json`). A **Build Packet** is the assembly
50
+ handoff that wraps the spec without replacing it (`docs/build-packet.md`). An
51
+ **Assembly Report** is the machine-readable record of lifecycle progress.
52
+
53
+ A **Design Source Package** is the normalized source bundle Campaigns OS writes
54
+ when it prepares a build; a material source-reference refresh creates a new one
55
+ rather than editing one in place. A **Readiness Checkpoint** is an
56
+ artifact-backed gate, resumable across runs, passed on evidence or by a
57
+ recorded and attributed waiver. **Partial-source** scope is the documented case
58
+ where only part of a campaign's source is supplied: a declared input state, not
59
+ a degraded run, so treat the absent part as absent evidence rather than
60
+ inferring it.
61
+
62
+ ## Place the artifacts in the pipeline
63
+
64
+ Campaigns OS normalizes a CampaignSpec into `campaign-runtime.build.json`
65
+ (`campaign-runtime-build-packet/v0`). **Page Kit** is the static-site builder
66
+ for campaign funnels. Under the target repository's `.campaign-runtime/`
67
+ Campaigns OS also writes the Build Context, the Assembly Report, the doctor
68
+ output sidecar, theme evidence and normalized inputs
69
+ (`docs/campaigns-os-build-flow.md`). Its `_site/` output is deployed by a
70
+ separate system, and QA then tests a deployed URL
71
+ (`docs/qa-and-test-orders.md`).
72
+
73
+ Keep the two identities apart. The **Map ID** identifies the saved campaign map
74
+ and keys QA evidence storage; the **public route slug** is the shopper-facing
75
+ path segment. Read both from the packet and never substitute one for the other.
76
+
77
+ Campaign pages are typed. The page-type vocabulary is `presell`, `landing`,
78
+ `select`, `checkout`, `upsell`, `downsell` and `thankyou`; `select` is where a
79
+ shopper chooses a package before checkout. Use the spec's own term for a page
80
+ rather than describing it.
81
+
82
+ ## Read doctor as a gate
83
+
84
+ **Doctor** is the lifecycle gate that must pass before the stage ladder
85
+ proceeds. It checks the packet, its CampaignSpec, the artifacts and the built
86
+ output, and its ordered check registry records what ran. When blocking errors
87
+ exist the result is not OK, names the errors, and points the next step at
88
+ collecting or correcting input. `campaigns-os doctor --packet <packet> --json`
89
+ (tier `none`: inspection is the default, and without `--write` it leaves the
90
+ target byte-identical) is the inspection form.
91
+
92
+ Read doctor's recorded result, not a process exit code. Exit codes are knowable
93
+ only from implementation files this skill may not cite, so do not branch on one.
94
+
95
+ ## Read the stage record
96
+
97
+ `campaigns-os next --packet <packet> --json` (tier `A`: it captures a progress
98
+ snapshot under the target and, under Run Telemetry consent, POSTs that
99
+ observation off the machine) reads the recorded state and names the next
100
+ incomplete stage among `setup`, `build`, `polish`, `deploy` and `qa`. That is a
101
+ writing, potentially sending command. If all you need is where the run stands,
102
+ prefer the readback below.
103
+
104
+ The stage called `build` at the CLI is stored under `stages.assembly`. The
105
+ Assembly Report's picker treats statuses beginning `completed` and statuses
106
+ beginning `skipped` as terminal. Build evidence carries `build_fingerprint`, an
107
+ identifier for the exact built state. Polish evidence must classify every
108
+ unresolved issue; the `repair_needed` class means the built output still needs
109
+ work, and the documented polish gate holds deploy and QA until the issue is
110
+ repaired, reclassified or covered by a structured waiver.
111
+
112
+ Where these artifacts already exist for a target, do not open them first.
113
+ `campaigns-os readback <target-repo-root> --json` (tier `none`: it writes
114
+ nothing, not even a lifecycle entry, starts no process and touches no network)
115
+ projects their loaded state, their per-artifact staleness against the
116
+ checkout's HEAD reflog, the doctor warning grouping, the skip cascades and any
117
+ cross-artifact divergence into one `campaigns-os-readback/v2` object.
118
+ `docs/readback.md` and `schemas/campaigns-os-readback.v2.schema.json` say what
119
+ each field means. Cite that projection as the evidence base and open the
120
+ artifacts themselves only to drill into what it names. This orientation
121
+ explains what the artifacts mean; it is not a licence to re-derive the
122
+ projection's staleness or warning grouping by hand.
123
+
124
+ The **Theme Gate** decides whether the generated brand layer is acceptable for
125
+ commerce pages. It is not advice: `next` and QA consume the result and stop
126
+ later stages when it is blocked. An applied layer or a recorded waiver with a
127
+ reason is the supported way through, and downstream evidence keeps the waiver
128
+ visible (`docs/brand-theme-bridge.md`).
129
+
130
+ ## Avoid the two-worlds mistake
131
+
132
+ Store themes and Page Kit campaign funnels are not one build surface, and
133
+ conflating them is the most expensive orientation error available here. This
134
+ repository documents Page Kit as the funnel target and describes the deployment
135
+ of its static output. It does not define the separate store-theme publishing
136
+ stack at all, so that half of the distinction is unverified from here — say so
137
+ rather than filling it in. A claim about store-theme publishing cannot be
138
+ sourced from this surface, and an answer that quietly treats a funnel artifact
139
+ as theme evidence (or the reverse) will read as authoritative while resting on
140
+ nothing.
141
+
142
+ For Page Kit routes, read the packet's per-page target projection — the
143
+ resolved output path for that page — rather than copying producer directories.
144
+ A page needs a **permalink**, an explicit public route assigned to it. Routing
145
+ surprises are not harmless: `prepare-build` can report
146
+ `PAGE_KIT_TARGET_CONFLICT` when two routes project to one terminal filename
147
+ (`docs/build-packet.md`). The machine-readable build summary is the evidence to
148
+ inspect; a build command that exited successfully does not establish correct
149
+ routing.
150
+
151
+ Two further routing error codes an earlier version of this text named were
152
+ dropped rather than re-sourced: they appear only in implementation files, so a
153
+ reader could not verify them. Inspect the build summary and report what it says.
154
+
155
+ ## Name the current skill set
156
+
157
+ This repository publishes its own skill set and names it in `skills.json`. Use
158
+ the id the manifest publishes — the Build Packet setup skill is
159
+ `next-campaigns-os-setup` — and read the manifest rather than remembering a
160
+ name; an older name for the same skill is a stale reference.
161
+
162
+ ## Capability boundary
163
+
164
+ This skill conveys interpretation knowledge only. It authorizes no lifecycle
165
+ command and no change to campaign state. What any supported invocation may do
166
+ is declared per row in `contracts/effects.v1.json` and explained in
167
+ `docs/effects.md`; widening it is a pull request that changes a row of that
168
+ file, never a decision made in a session.
169
+
170
+ An operator-supplied checkout is inspect-only. Do not run `git pull`, `git
171
+ fetch`, checkout, reset, clean or any other Git mutation against it; a baseline
172
+ change is a reviewed change, not a conversational one. Name any suggested
173
+ mutation as work for an authorized human and do not perform it through this
174
+ skill.