@nextcommerce/campaigns-os 1.37.3 → 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.
- package/AGENTS.md +114 -10
- package/CHANGELOG.md +530 -0
- package/README.md +38 -27
- package/contracts/agent-relevant-change-policy.v1.json +11 -1
- package/contracts/effects.v1.json +4794 -0
- package/contracts/release-ledger.json +789 -0
- package/contracts/supported-surface.json +25 -5
- package/docs/build-packet.md +27 -16
- package/docs/demo-preview.md +1 -1
- package/docs/diagnostics.md +7 -4
- package/docs/effects.md +281 -0
- package/docs/gateway-login.md +113 -0
- package/docs/orientation-contract-reference.md +4 -1
- package/docs/progress-snapshots.md +3 -3
- package/docs/qa-and-test-orders.md +3 -3
- package/docs/readback.md +523 -0
- package/docs/runtime-readiness.md +1 -1
- package/docs/sdk-storage-compatibility.md +1 -1
- package/docs/skills-revision.md +364 -0
- package/docs/supported-surface.md +11 -3
- package/docs/versioning.md +8 -4
- package/package.json +8 -3
- package/schemas/campaign-runtime-build-packet.v0.schema.json +5 -0
- package/schemas/campaigns-os-effects.v1.schema.json +211 -0
- package/schemas/campaigns-os-readback.v2.schema.json +267 -0
- package/skills/campaign-lifecycle-orientation/SKILL.md +174 -0
- package/skills/campaign-readback-classification/SKILL.md +230 -0
- package/skills/campaign-run-evidence/SKILL.md +140 -0
- package/skills/contribution-intake/SKILL.md +85 -0
- package/skills/next-campaigns-build/SKILL.md +33 -12
- package/skills/next-campaigns-os/SKILL.md +45 -21
- package/skills/next-campaigns-os-setup/SKILL.md +35 -14
- package/skills/next-campaigns-polish/SKILL.md +43 -17
- package/skills/next-campaigns-qa/SKILL.md +48 -24
- package/skills.json +39 -6
- package/src/admin-transport.mjs +123 -0
- package/src/cli.mjs +991 -200
- package/src/credential-store.mjs +183 -0
- package/src/deviation.mjs +3 -2
- package/src/diagnostic.mjs +4 -1
- package/src/gate-actions.mjs +2 -2
- package/src/install-mode.mjs +17 -9
- package/src/lifecycle.mjs +95 -0
- package/src/login.mjs +152 -0
- package/src/package-install-fixture.mjs +3 -2
- package/src/qa-node.mjs +56 -19
- package/src/qa-publish.mjs +108 -2
- package/src/readback.mjs +1936 -0
- 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.
|