@nextcommerce/campaigns-os 1.37.3 → 1.43.1
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 +708 -0
- package/README.md +44 -31
- package/agents/claude/CLAUDE.md +5 -1
- package/campaign-spec/dist/types.d.ts +2 -0
- package/contracts/agent-relevant-change-policy.v1.json +11 -1
- package/contracts/effects.v1.json +4887 -0
- package/contracts/migration-sidecar-bundle.v0.json +9 -0
- package/contracts/release-ledger.json +1541 -0
- package/contracts/supported-surface.json +33 -12
- package/docs/build-packet.md +83 -22
- package/docs/campaigns-os-build-flow.md +2 -2
- package/docs/demo-preview.md +1 -1
- package/docs/diagnostics.md +7 -4
- package/docs/effects.md +350 -0
- package/docs/gateway-login.md +113 -0
- package/docs/local-setup.md +51 -0
- package/docs/migration-sidecar-bundle.md +6 -1
- package/docs/orientation-contract-reference.md +4 -1
- package/docs/progress-snapshots.md +9 -3
- package/docs/qa-and-test-orders.md +29 -13
- 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 +10 -4
- package/schemas/campaign-runtime-assembly-report.v0.schema.json +6 -1
- package/schemas/campaign-runtime-build-packet.v0.schema.json +11 -1
- package/schemas/campaign-spec.v4.schema.json +4 -0
- package/schemas/campaigns-os-effects.v1.schema.json +211 -0
- package/schemas/campaigns-os-progress-snapshot.v0.schema.json +1 -0
- package/schemas/campaigns-os-qa-verdict-sidecar.v0.schema.json +1 -0
- package/schemas/campaigns-os-qa-verdict.v0.schema.json +1 -0
- package/schemas/campaigns-os-readback.v2.schema.json +267 -0
- package/schemas/campaigns-os-run-record.v0.schema.json +1 -0
- package/skills/campaign-lifecycle-orientation/SKILL.md +179 -0
- package/skills/campaign-readback-classification/SKILL.md +230 -0
- package/skills/campaign-run-evidence/SKILL.md +142 -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 +59 -22
- package/skills/next-campaigns-os/references/session-intake.md +4 -4
- 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 +53 -28
- package/skills.json +40 -7
- package/src/admin-transport.mjs +123 -0
- package/src/cli.mjs +1178 -270
- package/src/credential-store.mjs +183 -0
- package/src/deviation.mjs +3 -2
- package/src/diagnostic.mjs +4 -1
- package/src/finding-cause.mjs +14 -10
- package/src/gate-actions.mjs +2 -2
- package/src/install-mode.mjs +17 -9
- package/src/lifecycle.mjs +96 -0
- package/src/login.mjs +152 -0
- package/src/package-install-fixture.mjs +3 -2
- package/src/polish-node.mjs +5 -2
- package/src/progress-node.mjs +3 -2
- package/src/progress.mjs +5 -3
- package/src/qa-node.mjs +105 -36
- package/src/qa-publish.mjs +112 -2
- package/src/qa-sidecar.mjs +2 -0
- package/src/qa-verdict-discovery.mjs +11 -0
- package/src/qa-verdict-publish.mjs +1 -0
- package/src/qa-verdict.mjs +8 -1
- package/src/readback.mjs +1937 -0
- package/src/remit.mjs +17 -3
- package/src/run-record-closeout.mjs +3 -4
- package/src/run-record.mjs +4 -0
- package/src/sidecar-bundle.mjs +21 -0
- package/src/spec-source-identity.mjs +44 -0
- package/src/stage-ledger.mjs +4 -1
- package/src/tooling-setup.mjs +160 -0
package/skills.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "campaigns-os-skills",
|
|
3
|
-
"description": "Skills bundled with Campaigns OS. `skills.sh` installs these into the shared agent skill directories (~/.claude/skills, ~/.codex/skills), so each one is a versioned package: bump the version whenever a package changes.",
|
|
3
|
+
"description": "Skills bundled with Campaigns OS. `skills.sh` installs these into the shared agent skill directories (~/.claude/skills, ~/.codex/skills), so each one is a versioned package: bump the version whenever a package changes. `bundle_revision` identifies the bundle as a whole — `<package version>+skills.<n>`, where the prefix is this package's version and `<n>` counts the skill-text revisions published against it. It advances whenever ANY bundled skill changes, every SKILL.md states it on its first body line, and `campaigns-os tooling status --skills-revision <value>` compares the value an agent read from a skill against the bundle on disk. See docs/skills-revision.md.",
|
|
4
|
+
"bundle_revision": "1.43.1+skills.1",
|
|
4
5
|
"homepage": "https://github.com/NextCommerceCo/campaigns-os",
|
|
5
6
|
"retired_skills": [
|
|
6
7
|
{
|
|
@@ -15,7 +16,7 @@
|
|
|
15
16
|
{
|
|
16
17
|
"id": "next-campaigns-os",
|
|
17
18
|
"name": "Campaigns OS Lifecycle",
|
|
18
|
-
"version": "1.0.
|
|
19
|
+
"version": "1.0.27",
|
|
19
20
|
"path": "skills/next-campaigns-os/SKILL.md",
|
|
20
21
|
"domain": "campaigns",
|
|
21
22
|
"description": "Coordinate Campaigns OS lifecycle workflows from CampaignSpec, Build Packet, starter-template contracts, stage reports, deploy evidence, and QA proof depth."
|
|
@@ -23,7 +24,7 @@
|
|
|
23
24
|
{
|
|
24
25
|
"id": "next-campaigns-os-setup",
|
|
25
26
|
"name": "Campaigns OS Setup",
|
|
26
|
-
"version": "2.0.
|
|
27
|
+
"version": "2.0.10",
|
|
27
28
|
"path": "skills/next-campaigns-os-setup/SKILL.md",
|
|
28
29
|
"domain": "campaigns",
|
|
29
30
|
"description": "Bootstrap or prepare a target page-kit campaign repo from a doctor-cleared Campaigns OS Build Packet before full build wiring. Formerly next-campaigns-setup; renamed to release that name to the published NextCommerceCo/skills scaffolder."
|
|
@@ -31,7 +32,7 @@
|
|
|
31
32
|
{
|
|
32
33
|
"id": "next-campaigns-build",
|
|
33
34
|
"name": "Campaign Build",
|
|
34
|
-
"version": "1.0.
|
|
35
|
+
"version": "1.0.12",
|
|
35
36
|
"path": "skills/next-campaigns-build/SKILL.md",
|
|
36
37
|
"domain": "campaigns",
|
|
37
38
|
"description": "Assemble a NEXT campaign from a doctor-cleared Build Packet, CampaignSpec/API values, prepared HTML/assets, page-kit, and starter-template contracts."
|
|
@@ -39,7 +40,7 @@
|
|
|
39
40
|
{
|
|
40
41
|
"id": "next-campaigns-polish",
|
|
41
42
|
"name": "Campaign Polish",
|
|
42
|
-
"version": "1.1.
|
|
43
|
+
"version": "1.1.11",
|
|
43
44
|
"path": "skills/next-campaigns-polish/SKILL.md",
|
|
44
45
|
"domain": "campaigns",
|
|
45
46
|
"description": "Run the visual/runtime polish pass after build and before QA for a Campaigns OS campaign."
|
|
@@ -47,10 +48,42 @@
|
|
|
47
48
|
{
|
|
48
49
|
"id": "next-campaigns-qa",
|
|
49
50
|
"name": "Campaign QA",
|
|
50
|
-
"version": "1.3.
|
|
51
|
+
"version": "1.3.11",
|
|
51
52
|
"path": "skills/next-campaigns-qa/SKILL.md",
|
|
52
53
|
"domain": "campaigns",
|
|
53
|
-
"description": "Run spec-aware QA from a
|
|
54
|
+
"description": "Run spec-aware QA from a saved Map or local-spec Build Packet and tested campaign URL after build, polish, and deploy/local evidence exist, including Playwright typed-card test-order proof."
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"id": "campaign-lifecycle-orientation",
|
|
58
|
+
"name": "Campaign Lifecycle Orientation",
|
|
59
|
+
"version": "1.0.7",
|
|
60
|
+
"path": "skills/campaign-lifecycle-orientation/SKILL.md",
|
|
61
|
+
"domain": "campaigns",
|
|
62
|
+
"description": "Orient a reader to the Campaigns OS lifecycle artifacts a run has already emitted, without advancing their state: the shared vocabulary, doctor as a gate, the stage record, and the store-theme/Page Kit two-worlds trap."
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"id": "campaign-run-evidence",
|
|
66
|
+
"name": "Campaign Run Evidence",
|
|
67
|
+
"version": "1.0.7",
|
|
68
|
+
"path": "skills/campaign-run-evidence/SKILL.md",
|
|
69
|
+
"domain": "campaigns",
|
|
70
|
+
"description": "Interpret existing doctor, QA verdict and proof-depth evidence without claiming more proof than the artifacts contain, and keep funnel proof apart from merchant launch readiness."
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"id": "campaign-readback-classification",
|
|
74
|
+
"name": "Campaign Readback Classification",
|
|
75
|
+
"version": "1.0.7",
|
|
76
|
+
"path": "skills/campaign-readback-classification/SKILL.md",
|
|
77
|
+
"domain": "campaigns",
|
|
78
|
+
"description": "Classify a selected campaign from the campaigns-os-readback/v2 fields (artifacts, staleness.stale_keys, clean, doctor, divergences, skip_cascades) and write a read-only handoff without turning diagnosis into permission."
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"id": "contribution-intake",
|
|
82
|
+
"name": "Contribution Intake",
|
|
83
|
+
"version": "1.0.7",
|
|
84
|
+
"path": "skills/contribution-intake/SKILL.md",
|
|
85
|
+
"domain": "campaigns",
|
|
86
|
+
"description": "Turn a suggestion about the agent surface into a classified, evidence-checked and redacted proposal, filed on this repository tracker only with attended approval."
|
|
54
87
|
}
|
|
55
88
|
]
|
|
56
89
|
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { createCredentialStore } from './credential-store.mjs';
|
|
2
|
+
import { GATEWAY, CLIENT_ID, RESOURCE, SCOPE, canonicalLoginStore, gatewayRequest, credentialFromResponse } from './login.mjs';
|
|
3
|
+
|
|
4
|
+
const MAX_BYTES = 10 * 1048576; // custody consolidates at most ten bounded pages
|
|
5
|
+
const MAX_ROWS = 2000;
|
|
6
|
+
const REQUEST_MS = 15000, BUDGET_MS = 45000;
|
|
7
|
+
const object = value => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
8
|
+
const opaque = value => typeof value === 'string' && value.length > 0 && value.length <= 8192 && !/[\u0000-\u001f\u007f]/u.test(value);
|
|
9
|
+
class ReadFailure extends Error {
|
|
10
|
+
constructor(status, detail) { super(detail); this.status = status; }
|
|
11
|
+
}
|
|
12
|
+
const storageUnavailable = () => new ReadFailure('credential_unavailable', 'Gateway credential storage is unavailable or busy. Check your user credential directory permissions and keychain access; wait for another campaigns-os process to finish. For an interrupted process, confirm no campaigns-os process is running before removing its stale .lock directory. No environment fallback was attempted.');
|
|
13
|
+
const loginRequired = () => new ReadFailure('unauthorized', 'Gateway login is expired, revoked, or needs recovery. Run campaigns-os login --store <subdomain>.');
|
|
14
|
+
const bindingFor = store => ({ gateway: GATEWAY, client_id: CLIENT_ID, store });
|
|
15
|
+
function validate(record, binding) {
|
|
16
|
+
if (!record) throw new ReadFailure('credential_missing', 'No gateway login for this store. Run campaigns-os login --store <subdomain>.');
|
|
17
|
+
if (record.gateway !== binding.gateway || record.client_id !== binding.client_id || record.store !== binding.store || record.resource !== RESOURCE || record.scope !== SCOPE || record.schema_version !== 1 || record.token_type !== 'Bearer' || !opaque(record.access_token) || !opaque(record.refresh_token) || !opaque(record.grant_id) || !Number.isFinite(record.access_expires_at) || record.refresh_pending) throw loginRequired();
|
|
18
|
+
return record;
|
|
19
|
+
}
|
|
20
|
+
async function readJson(route, token, { fetchImpl, timeoutMs }) {
|
|
21
|
+
const controller = new AbortController(); let timer, reader;
|
|
22
|
+
const run = async () => {
|
|
23
|
+
const response = await fetchImpl(GATEWAY + route, { method: 'GET', headers: { Accept: 'application/json', Authorization: `Bearer ${token}` }, redirect: 'error', signal: controller.signal });
|
|
24
|
+
if (response.redirected || (response.url && response.url !== GATEWAY + route)) throw new ReadFailure('invalid', 'Gateway returned an unexpected redirect.');
|
|
25
|
+
if (response.status === 401) throw Object.assign(loginRequired(), { httpStatus: 401 });
|
|
26
|
+
if (response.status === 403) throw loginRequired();
|
|
27
|
+
if (response.status === 404) throw new ReadFailure('not_found', 'Gateway read route or store is unavailable.');
|
|
28
|
+
if (!response.ok) throw new ReadFailure('unreachable', 'Gateway read failed. No environment credential fallback was attempted.');
|
|
29
|
+
if (response.headers.get('content-type')?.split(';')[0] !== 'application/json') throw new ReadFailure('invalid', 'Gateway did not return a JSON profile.');
|
|
30
|
+
reader = response.body?.getReader(); if (!reader) throw new ReadFailure('invalid', 'Gateway returned an empty profile.');
|
|
31
|
+
const chunks = []; let size = 0;
|
|
32
|
+
for (;;) { const { value, done } = await reader.read(); if (done) break; size += value.byteLength; if (size > MAX_BYTES) throw new ReadFailure('invalid', 'Gateway response exceeded the read bound.'); chunks.push(value); }
|
|
33
|
+
let body; try { body = JSON.parse(Buffer.concat(chunks).toString('utf8')); } catch { throw new ReadFailure('invalid', 'Gateway response was not valid JSON.'); }
|
|
34
|
+
if (!object(body)) throw new ReadFailure('invalid', 'Gateway response was not the documented object.');
|
|
35
|
+
return body;
|
|
36
|
+
};
|
|
37
|
+
try {
|
|
38
|
+
if (timeoutMs <= 0) throw new ReadFailure('unreachable', 'Gateway read budget expired.');
|
|
39
|
+
return await Promise.race([run(), new Promise((_, reject) => { timer = setTimeout(() => { controller.abort(); reject(new ReadFailure('unreachable', 'Gateway read timed out.')); }, Math.min(REQUEST_MS, timeoutMs)); })]);
|
|
40
|
+
} catch (error) { throw error instanceof ReadFailure ? error : new ReadFailure('unreachable', 'Gateway is unreachable. No environment credential fallback was attempted.'); }
|
|
41
|
+
finally { clearTimeout(timer); controller.abort(); void reader?.cancel().catch(() => {}); }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Caller owns the exclusive credential transaction. Logout uses this same
|
|
45
|
+
// durable consumption boundary, including when interrupted before cleanup.
|
|
46
|
+
export async function rotateLockedCredential(storage, current, { fetchImpl = globalThis.fetch, now = Date.now, timeoutMs = REQUEST_MS } = {}) {
|
|
47
|
+
if (current.refresh_pending) throw loginRequired();
|
|
48
|
+
storage.write({ ...current, refresh_pending: true });
|
|
49
|
+
try {
|
|
50
|
+
const started = now();
|
|
51
|
+
const data = await gatewayRequest('/token', { fetchImpl, timeoutMs, form: { grant_type: 'refresh_token', client_id: CLIENT_ID, resource: RESOURCE, refresh_token: current.refresh_token } });
|
|
52
|
+
if (data.expires_in === 0) throw new ReadFailure('unauthorized', 'Gateway grant has expired. Run campaigns-os login --store <subdomain>.');
|
|
53
|
+
if (data.grant_id !== current.grant_id) throw loginRequired();
|
|
54
|
+
const next = credentialFromResponse(data, current.store, started);
|
|
55
|
+
storage.write(next);
|
|
56
|
+
return next;
|
|
57
|
+
} catch (error) {
|
|
58
|
+
if (error instanceof ReadFailure) throw error;
|
|
59
|
+
const failure = loginRequired(); failure.httpStatus = error?.status; throw failure;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function readGatewayStoreProfile({ subdomain, credentials = createCredentialStore(), fetchImpl = globalThis.fetch, now = Date.now, budgetMs = BUDGET_MS, timeoutMs = REQUEST_MS } = {}) {
|
|
64
|
+
let binding;
|
|
65
|
+
try { binding = bindingFor(canonicalLoginStore(subdomain)); } catch { return { status: 'invalid', detail: 'Invalid store for gateway login.' }; }
|
|
66
|
+
const deadline = now() + Math.min(BUDGET_MS, budgetMs);
|
|
67
|
+
const remaining = () => Math.min(timeoutMs, deadline - now());
|
|
68
|
+
let record, refreshUsed = false;
|
|
69
|
+
const refresh = async loaded => {
|
|
70
|
+
if (refreshUsed) throw loginRequired();
|
|
71
|
+
refreshUsed = true;
|
|
72
|
+
const outcome = await credentials.transaction(binding, async storage => {
|
|
73
|
+
try {
|
|
74
|
+
const current = validate(storage.read(), binding);
|
|
75
|
+
if (current.refresh_token !== loaded.refresh_token || current.access_token !== loaded.access_token) return { record: current };
|
|
76
|
+
if (remaining() <= 0) throw new ReadFailure('unreachable', 'Gateway read budget expired before refresh.');
|
|
77
|
+
return { record: await rotateLockedCredential(storage, current, { fetchImpl, now, timeoutMs: remaining() }) };
|
|
78
|
+
} catch (error) { return { failure: error instanceof ReadFailure ? error : storageUnavailable() }; }
|
|
79
|
+
});
|
|
80
|
+
if (outcome.failure) throw outcome.failure;
|
|
81
|
+
return outcome.record;
|
|
82
|
+
};
|
|
83
|
+
const request = async route => {
|
|
84
|
+
if (record.access_expires_at <= now() + 60000) record = await refresh(record);
|
|
85
|
+
try { return await readJson(route, record.access_token, { fetchImpl, timeoutMs: remaining() }); }
|
|
86
|
+
catch (error) {
|
|
87
|
+
if (error.httpStatus !== 401) throw error;
|
|
88
|
+
record = await refresh(record);
|
|
89
|
+
return readJson(route, record.access_token, { fetchImpl, timeoutMs: remaining() });
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
try {
|
|
93
|
+
record = validate(await credentials.read(binding), binding);
|
|
94
|
+
const store = await request('/admin/store/');
|
|
95
|
+
// A projected sparse store is legitimate: pure derivation explains every
|
|
96
|
+
// missing field. Do not invent required profile fields here.
|
|
97
|
+
try {
|
|
98
|
+
const body = await request('/admin/pages/');
|
|
99
|
+
if (!Array.isArray(body.results) || body.results.length > MAX_ROWS || Object.hasOwn(body, 'next') || body.results.some(row => !object(row) || (row.slug != null && typeof row.slug !== 'string') || (row.title != null && typeof row.title !== 'string'))) throw new ReadFailure('invalid', 'Gateway returned an invalid consolidated page list.');
|
|
100
|
+
return { status: 'ok', store, pages: body.results.filter(row => typeof row.slug === 'string' && row.slug.trim()).map(({ slug, title }) => ({ slug, title: typeof title === 'string' ? title : '' })), pages_status: 'ok', pages_detail: null };
|
|
101
|
+
} catch (error) {
|
|
102
|
+
return { status: 'ok', store, pages: null, pages_status: 'unavailable', pages_detail: error instanceof ReadFailure ? error.message : storageUnavailable().message };
|
|
103
|
+
}
|
|
104
|
+
} catch (error) {
|
|
105
|
+
const failure = error instanceof ReadFailure ? error : storageUnavailable();
|
|
106
|
+
return { status: failure.status, detail: failure.message };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export async function gatewayLoginStatus({ credentials = createCredentialStore(), now = Date.now } = {}) {
|
|
111
|
+
try {
|
|
112
|
+
const bindings = await credentials.listBindings();
|
|
113
|
+
const accounts = [];
|
|
114
|
+
for (const binding of bindings) {
|
|
115
|
+
if (binding.gateway !== GATEWAY || binding.client_id !== CLIENT_ID) continue;
|
|
116
|
+
const record = await credentials.read(binding);
|
|
117
|
+
try { validate(record, binding); }
|
|
118
|
+
catch { accounts.push({ store: canonicalLoginStore(binding.store), state: 'login_required', access_expires_at: null, remaining_seconds: 0, gateway_version: null }); continue; }
|
|
119
|
+
accounts.push({ store: binding.store, state: record.access_expires_at > now() ? 'logged_in' : 'access_expired', access_expires_at: record.access_expires_at, remaining_seconds: Math.max(0, Math.floor((record.access_expires_at - now()) / 1000)), gateway_version: typeof record.gateway_version === 'string' && /^[a-zA-Z0-9][a-zA-Z0-9._+-]{0,127}$/.test(record.gateway_version) && ![record.access_token, record.refresh_token].some(token => record.gateway_version.includes(token)) ? record.gateway_version : null });
|
|
120
|
+
}
|
|
121
|
+
return { gateway: GATEWAY, checked: 'local_only', state: accounts.length ? 'available' : 'logged_out', accounts };
|
|
122
|
+
} catch { return { gateway: GATEWAY, checked: 'local_only', state: 'unavailable', accounts: [] }; }
|
|
123
|
+
}
|