@nextcommerce/campaigns-os 1.60.0 → 1.62.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +2 -1
- package/CHANGELOG.md +244 -0
- package/README.md +86 -438
- package/agents/claude/CLAUDE.md +20 -0
- package/agents/codex/AGENTS.md +19 -0
- package/agents/copilot/copilot-instructions.md +19 -0
- package/agents/cursor/campaigns-os.mdc +21 -0
- package/campaign-spec/README.md +7 -0
- package/campaign-spec/dist/analytics-vendors.d.ts +40 -0
- package/campaign-spec/dist/analytics-vendors.js +44 -0
- package/campaign-spec/dist/checkout-flow.d.ts +18 -0
- package/campaign-spec/dist/checkout-flow.js +28 -0
- package/campaign-spec/dist/index.d.ts +4 -2
- package/campaign-spec/dist/index.js +5 -1
- package/campaign-spec/dist/rules/analytics-contract-shape.d.ts +8 -0
- package/campaign-spec/dist/rules/analytics-contract-shape.js +39 -0
- package/campaign-spec/dist/rules/campaign-currencies.d.ts +29 -0
- package/campaign-spec/dist/rules/campaign-currencies.js +130 -0
- package/campaign-spec/dist/rules/checkout-path-has-packages.d.ts +31 -0
- package/campaign-spec/dist/rules/checkout-path-has-packages.js +79 -0
- package/campaign-spec/dist/rules/index.d.ts +2 -0
- package/campaign-spec/dist/rules/index.js +6 -0
- package/campaign-spec/dist/types.d.ts +43 -9
- package/compatibility.json +1 -1
- package/contracts/brand-theme-target-tokens.next-core.v0.json +95 -25
- package/contracts/campaign-cart-sdk-support-policy.v0.json +2 -2
- package/contracts/commerce-surface-catalog.json +68 -61
- package/contracts/effects.v1.json +549 -165
- package/contracts/fixtures/runtime-recipe/accept/current.json +3 -0
- package/contracts/fixtures/runtime-recipe/accept/minimal.json +3 -0
- package/contracts/fixtures/runtime-recipe/dist-states.json +1 -1
- package/contracts/fixtures/runtime-recipe/reject/advisory-enforcement.json +3 -0
- package/contracts/fixtures/runtime-recipe/reject/allowlist-without-hosts.json +3 -0
- package/contracts/fixtures/runtime-recipe/reject/committed-output-claim.json +3 -0
- package/contracts/fixtures/runtime-recipe/reject/engines-disagreement-warns.json +3 -0
- package/contracts/fixtures/runtime-recipe/reject/lifecycle-scripts-enabled.json +3 -0
- package/contracts/fixtures/runtime-recipe/reject/unknown-kind.json +3 -0
- package/contracts/fixtures/runtime-recipe/reject/unknown-network-policy.json +3 -0
- package/contracts/fixtures/runtime-recipe/reject/unknown-output-check.json +3 -0
- package/contracts/fixtures/runtime-recipe/reject/unknown-revision.json +3 -0
- package/contracts/fixtures/runtime-recipe/reject/unknown-step-id.json +3 -0
- package/contracts/fixtures/runtime-recipe/reject/unperformable-check-skipped.json +3 -0
- package/contracts/fixtures/runtime-recipe/reject/unpinned-lockfile.json +3 -0
- package/contracts/release-ledger.json +1767 -0
- package/contracts/runtime-recipe.campaigns-os-node-v1.json +3 -0
- package/contracts/supported-surface.json +10 -10
- package/contracts/template-brand-contract.shared-commerce.v0.json +23 -1
- package/docs/access-model.md +20 -0
- package/docs/brand-theme-bridge.md +62 -3
- package/docs/build-packet.md +456 -36
- package/docs/campaign-build-brief.md +16 -0
- package/docs/campaign-standardization-report.md +4 -4
- package/docs/campaigns-os-build-flow.md +112 -2
- package/docs/campaignspec-authoring-examples.md +141 -0
- package/docs/design-source-package.md +64 -8
- package/docs/effects.md +17 -9
- package/docs/entry-points.md +188 -0
- package/docs/local-setup.md +1 -1
- package/docs/orientation-contract-reference.md +1 -1
- package/docs/polish-evidence.md +42 -2
- package/docs/progress-snapshots.md +7 -4
- package/docs/qa-and-test-orders.md +197 -3
- package/docs/quickstart.md +438 -0
- package/docs/readback.md +154 -6
- package/docs/runtime-readiness.md +6 -3
- package/docs/sdk-storage-compatibility.md +4 -2
- package/docs/skills-revision.md +10 -10
- package/docs/source-adapters.md +213 -0
- package/package.json +10 -3
- package/schemas/campaign-design-source-package.v0.schema.json +8 -0
- package/schemas/campaign-runtime-build-context.v0.schema.json +9 -0
- package/schemas/campaign-runtime-build-packet.v0.schema.json +9 -0
- package/schemas/campaign-spec.v4.schema.json +19 -3
- package/schemas/campaign-spec.v5.schema.json +19 -3
- package/schemas/campaigns-os-readback.v2.schema.json +238 -1
- package/schemas/source-html-manifest.v0.schema.json +3 -0
- package/skills/campaign-lifecycle-orientation/SKILL.md +3 -3
- package/skills/campaign-readback-classification/SKILL.md +3 -3
- package/skills/campaign-run-evidence/SKILL.md +3 -3
- package/skills/contribution-intake/SKILL.md +3 -3
- package/skills/next-campaigns-build/SKILL.md +5 -4
- package/skills/next-campaigns-os/SKILL.md +7 -4
- package/skills/next-campaigns-os-setup/SKILL.md +5 -3
- package/skills/next-campaigns-polish/SKILL.md +8 -10
- package/skills/next-campaigns-qa/SKILL.md +7 -4
- package/skills/next-campaigns-sdk-bump/SKILL.md +151 -25
- package/skills.json +11 -11
- package/src/brand-theme.mjs +415 -9
- package/src/brief-summary.mjs +6 -2
- package/src/browser-launch.mjs +2 -1
- package/src/build-brief.mjs +13 -3
- package/src/built-site-scope.mjs +2 -2
- package/src/campaign-workspace.mjs +288 -15
- package/src/change-scope.mjs +284 -0
- package/src/cli.mjs +358 -97
- package/src/commercial-journey.mjs +16 -3
- package/src/commercial-parity.mjs +15 -7
- package/src/content-residue.mjs +4 -2
- package/src/design-file-reference.mjs +93 -0
- package/src/design-source-package.mjs +91 -1
- package/src/design-source-publication.mjs +9 -6
- package/src/directory-lock.mjs +73 -19
- package/src/doctor/checks.mjs +87 -81
- package/src/doctor/ci.mjs +177 -0
- package/src/doctor/inspect.mjs +41 -6
- package/src/doctor/next-step.mjs +7 -0
- package/src/doctor/source-provenance.mjs +74 -0
- package/src/doctor-sidecar.mjs +3 -2
- package/src/evidence-debt.mjs +159 -0
- package/src/finding-cause.mjs +12 -11
- package/src/input-refresh.mjs +339 -32
- package/src/invocation.mjs +7 -1
- package/src/live-campaign-refs.mjs +7 -2
- package/src/local-preview-policy.mjs +35 -2
- package/src/page-kit-analytics-vendors.mjs +132 -0
- package/src/parity/compare.mjs +316 -0
- package/src/parity/design-access.mjs +54 -0
- package/src/parity/element-parity-record.mjs +569 -0
- package/src/parity/probe.mjs +205 -0
- package/src/parity/snapshot.mjs +110 -0
- package/src/png-header.mjs +87 -0
- package/src/polish-browser.mjs +189 -10
- package/src/polish-captures-record.mjs +52 -0
- package/src/polish-deadline.mjs +14 -0
- package/src/polish-gate.mjs +25 -6
- package/src/polish-node.mjs +81 -7
- package/src/polish-readability.mjs +221 -9
- package/src/polish-visual-fixtures.mjs +74 -0
- package/src/polish-visual.mjs +613 -0
- package/src/progress-node.mjs +3 -2
- package/src/qa-analytics-correctness.mjs +14 -2
- package/src/qa-analytics-parity.mjs +111 -6
- package/src/qa-browser.mjs +40 -4
- package/src/qa-commercial-parity.mjs +50 -3
- package/src/qa-node.mjs +235 -22
- package/src/qa-publish.mjs +9 -8
- package/src/qa-sidecar.mjs +16 -4
- package/src/qa-vendor-beacons.mjs +232 -0
- package/src/qa-vendor-endpoints.mjs +306 -0
- package/src/qc-accept.mjs +176 -6
- package/src/qc-check-registry.mjs +42 -2
- package/src/qc-results.mjs +477 -42
- package/src/readback.mjs +113 -21
- package/src/route-identity.mjs +10 -0
- package/src/run-record.mjs +18 -10
- package/src/runtime-state-ignore.mjs +6 -2
- package/src/sdk-attribute-index.mjs +11 -11
- package/src/sdk-markup.mjs +77 -2
- package/src/sdk-storage-compatibility.mjs +115 -6
- package/src/source-html-intake.mjs +15 -1
- package/src/source-html-manifest.mjs +7 -0
- package/src/source-html-server.mjs +129 -0
- package/src/spec-derive-campaign.mjs +216 -0
- package/src/spec-derive.mjs +473 -53
- package/src/spec-identity.mjs +14 -0
- package/src/stage-ledger.mjs +40 -1
- package/src/stage-record.mjs +100 -17
- package/src/target-lock.mjs +11 -1
- package/src/theme-gate.mjs +2 -2
- package/src/visual-diff.mjs +296 -0
- package/src/visual-reference.mjs +126 -0
package/src/cli.mjs
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { campaignSpecIdentity, resolveCampaignIdentity, campaignIdentitiesMatch, localSpecIdentityFields } from "./spec-source-identity.mjs";
|
|
2
|
+
import { withDoctorCiReport } from "./doctor/ci.mjs";
|
|
2
3
|
import { createHash, randomUUID } from "node:crypto";
|
|
4
|
+
import { resolveDesignFileReferences } from "./design-file-reference.mjs";
|
|
3
5
|
import { withDesignSourcePublication } from "./design-source-publication.mjs";
|
|
4
6
|
import { createDemo, demoArguments } from "./demo.mjs";
|
|
5
7
|
import { execFileSync } from "node:child_process";
|
|
@@ -26,11 +28,11 @@ import { diagnosticExport, diagnosticTextLines } from "./diagnostic.mjs";
|
|
|
26
28
|
import { observeProgress, PROGRESS_OBSERVATION } from "./progress-node.mjs";
|
|
27
29
|
import { HIDDEN_EAGER_MEDIA_ACTIONS, qaRunCommand, requiredActionText, substitutePacket } from "./gate-actions.mjs";
|
|
28
30
|
import { orderPathDepthDriftText, orderPathDepthReconcileAction, orderPathDepthsDisagree, parseOrderPathDepthFlag } from "./proof-policy.mjs";
|
|
29
|
-
import { specMaterialHash, specHashesMatch } from "./spec-identity.mjs";
|
|
31
|
+
import { routeMaskedSpecMaterialHash, specMaterialHash, specHashesMatch } from "./spec-identity.mjs";
|
|
30
32
|
// The same predicate stage-ledger.mjs judges a mutator's result with, imported
|
|
31
33
|
// rather than re-stated so the waive preview and the commit agree by identity.
|
|
32
34
|
import { isPlainObject } from "./repo-scan.mjs";
|
|
33
|
-
import { applyDerivedAssemblyReportSummary, commitAssemblyReport, QA_GATE_PLACEHOLDER_TEXT_RESIDUE, recordProducerStageOutcome } from "./stage-ledger.mjs";
|
|
35
|
+
import { applyDerivedAssemblyReportSummary, campaignHandedOff, commitAssemblyReport, QA_GATE_PLACEHOLDER_TEXT_RESIDUE, recordProducerStageOutcome } from "./stage-ledger.mjs";
|
|
34
36
|
import { SESSION_ENDING_DISPOSITIONS, summarizePlaceholderTextGate, summarizePurchaseProof } from "./qa-verdict.mjs";
|
|
35
37
|
import { assessRunRecordCloseout, identityMatches, latestMatchingRunRecord, reasonIsRemitRecovery } from "./run-record-closeout.mjs";
|
|
36
38
|
import {
|
|
@@ -80,7 +82,17 @@ import { markDoctorSidecarStale, writeDoctorSidecar, writeJsonAtomic } from "./d
|
|
|
80
82
|
import { QcAcceptRefusal, buildQcHandoff, parseQcResultRef, planQcAccepts, projectQcAccept, qcAcceptAttribution, qcHandoffTextLines } from "./qc-accept.mjs";
|
|
81
83
|
import { loadQcRederivers } from "./qc-check-registry.mjs";
|
|
82
84
|
import { fingerprint12, readCurrentQcResults } from "./qc-results.mjs";
|
|
83
|
-
import {
|
|
85
|
+
import {
|
|
86
|
+
DEFAULT_PACKET_FILENAME,
|
|
87
|
+
RUNTIME_DIR_REL_PATH,
|
|
88
|
+
campaignSidecarPaths,
|
|
89
|
+
explicitReportPath,
|
|
90
|
+
packetRuntimeRelPath,
|
|
91
|
+
planFunnelWorkspace,
|
|
92
|
+
resolveCampaignWorkspace,
|
|
93
|
+
soleProjectPacket,
|
|
94
|
+
targetRepoFor,
|
|
95
|
+
} from "./campaign-workspace.mjs";
|
|
84
96
|
import { canonicalPath, sameFile } from "./fs-identity.mjs";
|
|
85
97
|
import { DEFAULT_PROXY_BASE, fetchSpecByMapId } from "./spec-fetch.mjs";
|
|
86
98
|
import { writeMapSdkPin } from "./map-pin-writeback.mjs";
|
|
@@ -99,7 +111,7 @@ import {
|
|
|
99
111
|
runWithRefusalScope,
|
|
100
112
|
timeOperatorWait,
|
|
101
113
|
} from "./lifecycle.mjs";
|
|
102
|
-
import { commandNames, optsOutOfRunSession, runInvocation } from "./invocation.mjs";
|
|
114
|
+
import { JOINED_FLAGS, commandNames, optsOutOfRunSession, runInvocation } from "./invocation.mjs";
|
|
103
115
|
import {
|
|
104
116
|
clearRunSession,
|
|
105
117
|
findRunSession,
|
|
@@ -139,6 +151,7 @@ import { derivePackagePin, LOCAL_INVOCATION_PREFIX, localInstallStatus, resolveI
|
|
|
139
151
|
import {
|
|
140
152
|
campaignRouteRoot,
|
|
141
153
|
normalizePublicRouteSlug,
|
|
154
|
+
specPublicRouteSlug,
|
|
142
155
|
} from "./route-identity.mjs";
|
|
143
156
|
import {
|
|
144
157
|
PAGE_KIT_BUILD_SUMMARY_CAPTURE_COMMAND,
|
|
@@ -238,11 +251,13 @@ import {
|
|
|
238
251
|
formatDeriveValue,
|
|
239
252
|
isPageTreeIgnoredDir,
|
|
240
253
|
isPageTreeIgnoredPath,
|
|
241
|
-
|
|
254
|
+
frontmatterScalars,
|
|
242
255
|
pageRouteForFile,
|
|
243
256
|
permalinkRoute,
|
|
244
257
|
planSpecDerive,
|
|
245
258
|
} from "./spec-derive.mjs";
|
|
259
|
+
import { planCampaignMirrorDerive } from "./spec-derive-campaign.mjs";
|
|
260
|
+
import { disabledLiveRead, liveRefsDisabled, readLiveCampaignForPacket } from "./live-campaign-refs.mjs";
|
|
246
261
|
import {
|
|
247
262
|
adminApiBaseForStore,
|
|
248
263
|
normalizeStoreSubdomain,
|
|
@@ -309,7 +324,7 @@ Usage:
|
|
|
309
324
|
[--brief <yaml|json>] [--proxy-base <url>] [--cached-spec] [--theme-policy <inspect_only|auto|off>]
|
|
310
325
|
[--wrapper-policy <strip_document_wrappers|preserve_document_wrappers|not_required|unknown>] [--design-manifest <path>]
|
|
311
326
|
[--allow-uncertified-template "<reason>"] [--order-path-depth <off|common|full>]
|
|
312
|
-
[--deploy-target <target>] [--preview-url <url>] [--production-url <url>] [--no-run-session] [--force] # --force overwrites an assembly report that carries stage evidence (destructive; prints the cleared stage keys) and regenerates a stale Design Source Package an earlier intake synthesized and nobody changed
|
|
327
|
+
[--deploy-target <target>] [--preview-url <url>] [--production-url <url>] [--no-run-session] [--force] # --force overwrites an assembly report that carries stage evidence (destructive; prints the cleared stage keys) and regenerates a stale Design Source Package an earlier intake synthesized and nobody changed; with --out naming another route's packet it replaces that funnel (renaming its route)
|
|
313
328
|
campaigns-os prepare-build (--spec <json> | --map-id <id>) --source <html-dir> --target <page-kit-dir> --template-family <family>
|
|
314
329
|
[--brief <yaml|json>] [--proxy-base <url>] [--cached-spec] [--theme-policy <inspect_only|auto|off>]
|
|
315
330
|
[--wrapper-policy <strip_document_wrappers|preserve_document_wrappers|not_required|unknown>] [--design-manifest <path>]
|
|
@@ -320,7 +335,7 @@ Usage:
|
|
|
320
335
|
[--wrapper-policy <strip_document_wrappers|preserve_document_wrappers|not_required|unknown>] [--design-manifest <path>]
|
|
321
336
|
[--allow-uncertified-template "<reason>"] [--order-path-depth <off|common|full>]
|
|
322
337
|
[--deploy-target <target>] [--preview-url <url>] [--production-url <url>] [--no-run-session] [--force] # intake alias for prepare-build + doctor
|
|
323
|
-
campaigns-os doctor --packet <campaign-runtime.build.json> [--context <json>] [--report <json>] [--strip-paths] [--write] [--no-write] [--doctor-out <path>] [--proxy-base <url>] [--no-live-refs] [--json] # inspection by default; --doctor-out requires --write; --no-write wins. When the packet's built _site/<route>/ exists and a public Campaigns API key resolves (packet, its local CampaignSpec, or the declared campaign-key env var), doctor makes one read-only GET of {proxy-base}/api/campaign under X-Campaign-Key to check each built page's shipping and package refs against the live campaign; --proxy-base overrides the canonical proxy (https, or a loopback host over http). --no-live-refs skips the read and records not_run with reason disabled. No key, no built page, or a failed read records derived.live_campaign_refs as not_run with its reason. Only doctor and qa run make this read; other commands that run doctor record not_read
|
|
338
|
+
campaigns-os doctor --packet <campaign-runtime.build.json> [--context <json>] [--report <json>] [--strip-paths] [--write] [--no-write] [--doctor-out <path>] [--proxy-base <url>] [--no-live-refs] [--ci] [--json] # inspection by default; --doctor-out requires --write; --no-write wins. --ci: Markdown (or JSON ci object), exits 0 current / 3 owed / 4 unknown / 2 packet usage error; other doctor errors do not affect the CI exit. Without --ci: 0 ok / 2 errors. When the packet's built _site/<route>/ exists and a public Campaigns API key resolves (packet, its local CampaignSpec, or the declared campaign-key env var), doctor makes one read-only GET of {proxy-base}/api/campaign under X-Campaign-Key to check each built page's shipping and package refs against the live campaign; --proxy-base overrides the canonical proxy (https, or a loopback host over http). --no-live-refs skips the read and records not_run with reason disabled. No key, no built page, or a failed read records derived.live_campaign_refs as not_run with its reason. Only doctor and qa run make this read; other commands that run doctor record not_read
|
|
324
339
|
campaigns-os doctor --built <page-kit-target-repo> --family <family> [--slug <slug>] [--base-url <url>] [--emit-packet [path]] [--json] # L7: doctor a built _site/ with no Build Packet. Reads the local files only: --base-url is not fetched, it fills deploy.preview_url in the minimal packet (check served pages with qa run --site)
|
|
325
340
|
campaigns-os bundle check --packet <campaign-runtime.build.json> [--require-qa] [--json] # validate the canonical migration/readback JSON bundle; never substitutes markdown
|
|
326
341
|
campaigns-os sdk storage-check --target <git-root> --target-sdk <x.y.z> --manifest <SDK-manifest.json> --scope <dir,file> [--exclude <dir,file>] [--json]
|
|
@@ -332,7 +347,7 @@ Usage:
|
|
|
332
347
|
campaigns-os checkpoint waive --packet <campaign-runtime.build.json> --gate <checkpoint-id> [--page <page_id>] --reason "<why>" --waived-by "<named human>" [--expires-at <ISO>] [--review-condition "<trigger>"] [--report <json>] [--dry-run] [--json] # one bound is required; registered gates: page_kit.store_profile, page_kit.sdk_version, polish.hidden_eager_media, built_output.upsell_selector_scope, source_html.producer_provenance (per page: --page <page_id> is required, for a Figma-typed page whose approved source is hand-written HTML). --dry-run runs every check (named human, bounds, registered and waivable gate) and prints the waiver it would write, without touching the report
|
|
333
348
|
campaigns-os checkpoint accept --packet <campaign-runtime.build.json> --result <result_id>@<fingerprint12> [--result ...] --reason "<the operator's reason>" --accepted-by "<named human>" [--expires-at <ISO>] [--review-condition "<trigger>"] [--report <json>] [--dry-run] [--json] # record the operator's accept of a measured QC warning, as the QC handoff of next prints it; changes no readiness and lapses when the measured state or the build changes. --dry-run runs every check and prints the accepts it would record, without touching the report
|
|
334
349
|
campaigns-os page-kit sync --packet <campaign-runtime.build.json> [--dry-run] [--json] # write the CampaignSpec's Store Profile fields (campaign.store_*) and SDK pin (global_config.sdk_version, runtime.sdk_version alias) into the target's _data/campaigns.json entry for the packet's route, printing a field-by-field diff; the recovery for a doctor blocked on page_kit.store_profile / page_kit.sdk_version after a fresh scaffold. Writes only those ten fields, only from usable spec values (a bad pin, a non-http URL, a non-tel: phone URI or the demo value itself is reported as not synced, status PARTIAL); exit 2 when the entry or the spec is missing, or the spec identifies another campaign.
|
|
335
|
-
campaigns-os spec derive --packet <campaign-runtime.build.json> [--dry-run] [--json] [--report <json>] [--from-store <subdomain> [--store-token-source env:<VAR>]] [--write-map] [--proxy-base <url>] # write the fields the target repo already states into the packet's local CampaignSpec (spec.local_path): the SDK pin from _data/campaigns.json[<route>].sdk_version (global_config.sdk_version, and the runtime.sdk_version alias when declared), each page's page_url from the page tree under src/<route>/ (filename or permalink), and the analytics ids the entry carries (gtm_id -> analytics.providers.gtm.containerId, fb_pixel_id -> analytics.providers.facebook.pixelId); prints a field-by-field before -> after diff and writes nothing else.
|
|
350
|
+
campaigns-os spec derive --packet <campaign-runtime.build.json> [--dry-run] [--json] [--report <json>] [--no-live-refs] [--from-store <subdomain> [--store-token-source env:<VAR>]] [--write-map] [--proxy-base <url>] # write the fields the target repo already states into the packet's local CampaignSpec (spec.local_path): the SDK pin from _data/campaigns.json[<route>].sdk_version (global_config.sdk_version, and the runtime.sdk_version alias when declared), each page's page_url from the page tree under src/<route>/ (filename or permalink), and the analytics ids the entry carries (gtm_id -> analytics.providers.gtm.containerId, fb_pixel_id -> analytics.providers.facebook.pixelId, and each starter-template vendor's id key, such as ga4_id or triplewhale_name -> analytics.vendors.<vendor>.id); prints a field-by-field before -> after diff and writes nothing else. On a handed-off campaign (deploy or QA completed, now or in the stage's history) the page tree also adds pages (a file with a page-kit page_type no spec page binds; id from the file name, appended to the funnel, routing from its next_url/decline_url/next_step frontmatter) and removes pages (one whose built file the packet named is gone), matched by page id; page order is reported as not derived. When the packet's built _site/<route>/ exists and a public Campaigns API key resolves, it also makes the one read-only GET of {proxy-base}/api/campaign doctor makes and refreshes the fields the spec mirrors from the Campaigns App (each page package's name, price, product, variant and availability; the offer records and each page's offer copies; live offers missing from offers[] are added) — a price is never written from the repo; --no-live-refs sends nothing and leaves those fields as they are; --proxy-base names the proxy. Otherwise repo-derived fields only; --from-store <subdomain> (the <store> of <store>.29next.store) also reads through campaigns-os login gateway credentials (--store-token-source env:<VAR> explicitly selects the warned break-glass Admin path; a token never goes on the command line) and writes the nine campaign.store_* Store Profile fields: store_name and store_url (primary domain) and store_phone/store_phone_tel from GET /store/, and store_terms/privacy/contact/returns/shipping as https://<primary domain>/<slug>/ from the one storefront page (GET /pages/) whose slug or title names each policy; an empty store field, no page or several never empties the spec's value. A field the repo or store cannot state (a scaffold's seeded pin, an unbound page, an empty or malformed id, an active page_kit.sdk_version waiver, an empty store field, an unbound policy page) is reported as not derived, status PARTIAL; exit 2 when the packet, the spec or the target entry is missing, the spec identifies another campaign, or the store cannot be read (credential missing, 401/403, no such store, unreachable). --write-map also records the derived pin into the saved Map's Build hints (Campaign Cart SDK version) through the proxy Worker (PUT /api/maps/<spec.map_id> under X-Campaign-Key, the packet's Campaigns API key, with the Map's spec_hash as the X-Spec-Hash precondition): written when the Map declares no pin or one behind the repo, unchanged when equal, refused (warning, exit 0) when the Map pin is ahead or cannot be ordered, failed (error, exit 2) when the key is missing or mismatched, the Map is gone, was saved in between, or the proxy refuses the body; the write is recorded on the Assembly Report evidence[] and in the result's map object. --proxy-base overrides the canonical proxy (https, or a loopback host over http); --dry-run reads the Map and reports would_write without a PUT.
|
|
336
351
|
campaigns-os page-kit parity --packet <campaign-runtime.build.json> [--report <json>] [--json] # local proof mode (deploy.target local-serve): render the current source in development and production through the target's page-kit into temp dirs, assert the served _site/ is the current development render and that production differs from it only in environment-gated output (same page set, same route slugs, same Campaign Cart pin and next-api-key); records stages.assembly.evidence.local_proof.production_parity, which doctor reads as local_proof.production_parity. Exit 2 on a non-gated difference.
|
|
337
352
|
campaigns-os polish capture --packet <campaign-runtime.build.json> --base-url <url> [--report <json>] [--headed] [--auth-cookie <cookie>] [--json]
|
|
338
353
|
campaigns-os record brief --packet <campaign-runtime.build.json> [--brief <yaml|json>] [--context <json>] [--report <json>] [--dry-run] [--json]
|
|
@@ -351,7 +366,7 @@ Usage:
|
|
|
351
366
|
campaigns-os logout [--store <subdomain>]
|
|
352
367
|
campaigns-os tooling status [--platform <claude|codex|agents|all>] [--target <skills-dir>] [--skills-revision <bundle-revision|skill-id@version>] [--packet <campaign-runtime.build.json>] [--force] [--json] # install-mode, git, skill freshness, and local gateway login/store/expiry/reported version. --skills-revision checks the bundle revision the skill you loaded states on its first body line (or that one skill's <skill-id>@<version>) against the bundle THIS CLI ships: revision_check is match, mismatch or unchecked, and a mismatch prints the full status and exits 2 because skill text already in context cannot be refreshed by re-running — start a fresh session. The pin check reports one executable per project: the project pin first — the first exact spec for this package (x.y.z, =x.y.z or vx.y.z) on the walk up from the nearest package.json, devDependencies then dependencies in each, entering a workspace root and stopping there, never peerDependencies or optionalDependencies — then the Build Packet's campaigns_os_version (the project's campaign-runtime.build.json, or --packet <path>); the Pin: line names the key and manifest (or packet) each version came from; pin.status is match, stale_pin (the pin is not the running version), conflicting_pin (the two sources disagree) or unpinned (neither, or only a range; exit 0). stale_pin and conflicting_pin exit 2 with the file to change; --force (bare) overrides them, is reported as pin.forced and lands on the lifecycle journal entry. See docs/skills-revision.md
|
|
353
368
|
campaigns-os tooling diagnose [--packet <packet>] [--platform <claude|codex|agents|all>] [--json] # read-only redacted support summary
|
|
354
|
-
campaigns-os install-agent-context --target <page-kit-dir> [--dry-run]
|
|
369
|
+
campaigns-os install-agent-context --target <page-kit-dir> [--dry-run] [--json]
|
|
355
370
|
campaigns-os next [${NEXT_STAGE_ORDER.join("|")}] --packet <json> [--no-write] [--no-remit] [--proxy-base <url>] [--json] # no stage self-decides; returns gates[] + next_actions[] alongside the prompt
|
|
356
371
|
campaigns-os next setup --packet <json> [--context <json>] [--report <json>] [--json]
|
|
357
372
|
campaigns-os next build --packet <json> [--context <json>] [--report <json>] [--json]
|
|
@@ -359,7 +374,7 @@ Usage:
|
|
|
359
374
|
campaigns-os next deploy --packet <json> --report <json> [--json]
|
|
360
375
|
campaigns-os next qa --packet <json> --report <json> [--json]
|
|
361
376
|
campaigns-os qa resolve --packet <json> [--base-url <url>] [--no-probe] [--probe-timeout-ms <ms>] [--json] # probes the derived entry URLs; a dead route set reports routes_unresolved, an unprobed one ready_unprobed
|
|
362
|
-
campaigns-os qa run --packet <json> [--base-url <url>] [--currency <code>] [--browser] [--test-order <mode>] [--select-package <ref[:qty],...>] [--apply-coupon <code>] [--no-post-verdict] [--no-remit] [--no-live-refs] [--output-dir <dir>] [--json]
|
|
377
|
+
campaigns-os qa run --packet <json> [--base-url <url>] [--currency <code>] [--page <page_id>]... [--browser] [--test-order <mode>] [--select-package <ref[:qty],...>] [--apply-coupon <code>] [--test-email <email>] [--test-country <code> --test-address1 <line> --test-city <city> --test-province <code> --test-postal <code>] [--vendor-beacons live] [--no-post-verdict] [--no-remit] [--no-live-refs] [--output-dir <dir>] [--json]
|
|
363
378
|
campaigns-os qa promote --packet <json> --verdict <full-verdict.json> [--json] # project one explicit qa-output verdict to the committed .campaign-runtime/qa-verdict.json sidecar
|
|
364
379
|
campaigns-os qa publish --packet <json> [--verdict <full-verdict.json>] [--republish] [--proxy-base <url>] [--dry-run] [--json] # post an already-stored verdict (the sidecar's run, or --verdict) to the QA portal without a re-run or an order; refuses a stale spec_hash or an already-published verdict. --dry-run runs every one of those refusal checks and prints what would be posted (endpoint, verdict run id, payload bytes) without the POST
|
|
365
380
|
campaigns-os qa policy set --packet <json> [--allowed-domains-confirmed true|false] [--deploy-target <target>] [--preview-url <url>] [--production-url <url>] [--order-path-depth <off|common|full>] [--json] # --order-path-depth writes qa.proof_policy.order_path_depth and refreshes the assembly report's proof_policy mirror
|
|
@@ -373,7 +388,7 @@ Usage:
|
|
|
373
388
|
|
|
374
389
|
Commands other than login, logout, demo, and tooling diagnose accept [--lifecycle-journal <path>] (or env CAMPAIGNS_OS_LIFECYCLE_LOG) to append a command-lifecycle entry (command, argv shape, exit status, timing) for the run; pair with --run-id so run-record can embed it.
|
|
375
390
|
--no-write suppresses that append for every command, however the journal was selected (flag, env, or the active run session); a refused invocation (unknown command, an unknown subcommand refused before its handler runs, or a flag the command refuses up front) and \`run status\` never append one at all.
|
|
376
|
-
A refused invocation writes no file of its own. One effect still precedes argument refusal: \`start\`, \`prepare-build\`, \`build\`, \`run start\` and \`run end\` close out a STALE run session at the root they are about to act on (Run Record assembled and remitted under the usual consent, session file cleared) before argv is refused — a declared effect of those commands. --no-write suppresses that closeout too, so a --no-write invocation leaves the target byte-identical.
|
|
391
|
+
A refused invocation writes no file of its own. One effect still precedes argument refusal: \`start\`, \`prepare-build\`, \`build\`, \`run start\` and \`run end\` close out a STALE run session at the root they are about to act on (Run Record assembled and remitted under the usual consent, session file cleared) before argv is refused — a declared effect of those commands. --no-write suppresses that closeout too, so a --no-write invocation leaves the target byte-identical; a --flag=value token, which every command refuses, skips it as well.
|
|
377
392
|
campaigns-os telemetry status|on [--proxy-base <url>] [--json] # machine-level Run Telemetry consent (gates remit only; capture is always local). \`on\` records consent for ONE endpoint: the canonical NEXT endpoint by default, or the --proxy-base you name (a loopback or staging receiver); \`status\` reports the stored scope and checks it against the canonical endpoint or the --proxy-base you name
|
|
378
393
|
campaigns-os telemetry off [--json] # turn remit off for every endpoint (takes no --proxy-base)
|
|
379
394
|
campaigns-os telemetry list [--packet <json> | --admin-key-env <VAR>] [--since <ISO>] [--package <v>] [--surface <s>] [--trusted] [--limit <n>] [--proxy-base <url>] [--trust-proxy-base] [--json] # read stored Run Records: tenant scope via the packet's campaign key, or cross-tenant via the ops admin key (default env CAMPAIGN_OPS_ADMIN_KEY). --proxy-base must be https unless it is a loopback host (allowed over http, with a warning that the credential is in clear).
|
|
@@ -484,14 +499,17 @@ export async function main(argv, { authentication, qcStandIns } = {}) {
|
|
|
484
499
|
console.log(packageVersion());
|
|
485
500
|
return;
|
|
486
501
|
}
|
|
487
|
-
return
|
|
488
|
-
dispatch: (command, args, context) =>
|
|
502
|
+
return withDoctorCiReport(parseArgs(argv), (args) => runInvocation(args, {
|
|
503
|
+
dispatch: (command, args, context) => {
|
|
504
|
+
refuseJoinedFlag(args);
|
|
505
|
+
return dispatch(command, args, { ...context, argv, authentication, qcStandIns });
|
|
506
|
+
},
|
|
489
507
|
closeOutStaleRunSessions,
|
|
490
508
|
ambientRunSession,
|
|
491
509
|
lifecycleIdentity,
|
|
492
510
|
persistLifecycle: persistLifecycleIfRequested,
|
|
493
511
|
autoEndAfterQa: autoEndRunSessionAfterTerminalQa,
|
|
494
|
-
});
|
|
512
|
+
}));
|
|
495
513
|
}
|
|
496
514
|
|
|
497
515
|
// The lifecycle entry's identity (T6): its argv shape and its run_id. Ambient
|
|
@@ -728,10 +746,17 @@ export function recordQaStageOutcome(args, result) {
|
|
|
728
746
|
// start and the spec material it judged, never the inputs at write time.
|
|
729
747
|
const qaStamps = inputStamps({ briefMaterial: verdict.source_brief_material, specMaterial: verdict.spec_hash });
|
|
730
748
|
const qaInputs = stageWriteInputs("qa", currentPacketInputs({ packet, packetPath }));
|
|
749
|
+
const pageScope = isObject(result.page_scope) ? result.page_scope : null;
|
|
731
750
|
const committed = commitAssemblyReport(workspace, (report) => {
|
|
732
751
|
if (hasLocalIdentity && !specHashesMatch(verdict.spec_hash, report.identity?.spec_material_hash)) {
|
|
733
752
|
throw new Error("Local-spec QA verdict belongs to a different material revision; report evidence was not changed.");
|
|
734
753
|
}
|
|
754
|
+
// A page-scoped run (#720) is bound to the build it checked; a report
|
|
755
|
+
// rebuilt since then keeps the QA it had.
|
|
756
|
+
if (pageScope && currentBuildFingerprint(report) !== pageScope.build_fingerprint) {
|
|
757
|
+
throw new Error("The build was recorded again during this page-scoped QA run; report evidence was not changed. Re-run qa run --page against the recorded build.");
|
|
758
|
+
}
|
|
759
|
+
const evidence = qaStageGateEvidence(verdict, report, result);
|
|
735
760
|
return recordProducerStageOutcome(report, {
|
|
736
761
|
stage: "qa",
|
|
737
762
|
disposition: verdict.disposition,
|
|
@@ -748,11 +773,19 @@ export function recordQaStageOutcome(args, result) {
|
|
|
748
773
|
// Which build this verdict judged, and the gates whose browser outcome
|
|
749
774
|
// the doctor's static scan defers to (qaGatePassedForCurrentBuild). A
|
|
750
775
|
// gate that never ran is left out, so silence never reads as a pass.
|
|
751
|
-
|
|
776
|
+
// A page-scoped record names its pages, the changed pages it pays
|
|
777
|
+
// for and the build whose full QA it extends.
|
|
778
|
+
evidence: pageScope ? { ...evidence, page_scope: {
|
|
779
|
+
pages: pageScope.pages, owed_pages: pageScope.owed_pages, base_build_fingerprint: pageScope.base_build_fingerprint,
|
|
780
|
+
} } : evidence,
|
|
752
781
|
// Counts-only: never order ids, refs, emails or URLs (see
|
|
753
782
|
// summarizePurchaseProof). This is what lets `next` tell a real purchase
|
|
754
|
-
// path from a `--test-order off` diagnostic.
|
|
755
|
-
|
|
783
|
+
// path from a `--test-order off` diagnostic. A page-scoped run places
|
|
784
|
+
// no orders and the change it pays touches no commerce, so the
|
|
785
|
+
// purchase proof of the full run it extends is kept.
|
|
786
|
+
proof: pageScope && isObject(report.stages?.qa?.purchase_proof)
|
|
787
|
+
? report.stages.qa.purchase_proof
|
|
788
|
+
: summarizePurchaseProof({ verdict, proofPolicy: packet.qa?.proof_policy }),
|
|
756
789
|
stamps: qaStamps,
|
|
757
790
|
inputs: qaInputs,
|
|
758
791
|
});
|
|
@@ -1022,7 +1055,7 @@ async function dispatch(command, args, { recorder = NOOP_RECORDER, ambient = nul
|
|
|
1022
1055
|
// A --map-id fetch runs here, but the fetched spec is written to the
|
|
1023
1056
|
// shared cache file inside prepareBuild, under the per-target lock, so it
|
|
1024
1057
|
// never replaces the spec another run is reading and hashing (#501).
|
|
1025
|
-
const { publishSpec, ...resolved } = await recorder.time("resolve-spec", () => resolveSpecPath(args, { deferCacheWrite: true }));
|
|
1058
|
+
const { publishSpec, fetchedSpec, ...resolved } = await recorder.time("resolve-spec", () => resolveSpecPath(args, { deferCacheWrite: true }));
|
|
1026
1059
|
// The spec as the operator named it, before resolution, so the build
|
|
1027
1060
|
// context can record which input to replay (a local file, or a map id
|
|
1028
1061
|
// fetched from a given store).
|
|
@@ -1033,7 +1066,7 @@ async function dispatch(command, args, { recorder = NOOP_RECORDER, ambient = nul
|
|
|
1033
1066
|
// Doctor runs inside prepare-build, so its codes are recorded while that
|
|
1034
1067
|
// phase is active and attach to it.
|
|
1035
1068
|
const result = await recorder.time("prepare-build", async () => {
|
|
1036
|
-
const prepared = await prepareBuild(args, { ...mode, command, specInput, publishSpec, sourceKind, wrapperPolicyFlag, orderPathDepthFlag });
|
|
1069
|
+
const prepared = await prepareBuild(args, { ...mode, command, specInput, publishSpec, fetchedSpec, sourceKind, wrapperPolicyFlag, orderPathDepthFlag });
|
|
1037
1070
|
if (prepared.doctor && !prepared.doctor.ok) recorder.recordFindingCodes(prepared.doctor.errors?.map((issue) => issue.code));
|
|
1038
1071
|
return prepared;
|
|
1039
1072
|
});
|
|
@@ -1049,8 +1082,15 @@ async function dispatch(command, args, { recorder = NOOP_RECORDER, ambient = nul
|
|
|
1049
1082
|
const liveCampaign = await readDoctorLiveCampaign(args);
|
|
1050
1083
|
const result = doctorCommand(args, { liveCampaign, qcStandIns });
|
|
1051
1084
|
if (!result.ok) recorder.recordFindingCodes(result.errors?.map((issue) => issue.code));
|
|
1052
|
-
|
|
1053
|
-
|
|
1085
|
+
if (args.ci === true) {
|
|
1086
|
+
const { formatDoctorCi, DOCTOR_CI_EXIT_CODES } = await import("./doctor/ci.mjs");
|
|
1087
|
+
if (!args.json) console.log(formatDoctorCi(result));
|
|
1088
|
+
else writeResult(result, args, 0);
|
|
1089
|
+
process.exitCode = result.ci?.exit_code ?? result.exit_code ?? DOCTOR_CI_EXIT_CODES.usage;
|
|
1090
|
+
} else {
|
|
1091
|
+
writeResult(result, args, result.ok ? 0 : 2);
|
|
1092
|
+
printDoctorTinyPrompt(result, args);
|
|
1093
|
+
}
|
|
1054
1094
|
return;
|
|
1055
1095
|
}
|
|
1056
1096
|
|
|
@@ -1172,8 +1212,15 @@ async function dispatch(command, args, { recorder = NOOP_RECORDER, ambient = nul
|
|
|
1172
1212
|
|
|
1173
1213
|
if (command === "install-agent-context") {
|
|
1174
1214
|
const target = requireArg(args, "target");
|
|
1175
|
-
const
|
|
1176
|
-
|
|
1215
|
+
const dryRun = isDryRun(args);
|
|
1216
|
+
const result = installAgentContext(resolve(target), dryRun);
|
|
1217
|
+
if (dryRun && !args.json) {
|
|
1218
|
+
for (const file of result.file_contents) {
|
|
1219
|
+
console.log(`Destination: ${file.destination}\n${file.content}`);
|
|
1220
|
+
}
|
|
1221
|
+
} else {
|
|
1222
|
+
writeResult(result, args, 0);
|
|
1223
|
+
}
|
|
1177
1224
|
return;
|
|
1178
1225
|
}
|
|
1179
1226
|
|
|
@@ -1241,6 +1288,8 @@ async function dispatch(command, args, { recorder = NOOP_RECORDER, ambient = nul
|
|
|
1241
1288
|
// The ambient session is handed over, not re-discovered: the closeout
|
|
1242
1289
|
// command a QA run prints has to agree with the run_id this session will
|
|
1243
1290
|
// later close and remit under.
|
|
1291
|
+
// --page repeats; the shared parser keeps only the last value.
|
|
1292
|
+
if (args._[1] === "run" && Array.isArray(argv) && argv.includes("--page")) args = { ...args, page: repeatedFlagValues(argv, "page") };
|
|
1244
1293
|
const result = await runQaCli(args, { ambient });
|
|
1245
1294
|
if (result?.verdict?.disposition === "blocked") {
|
|
1246
1295
|
recorder.recordFindingCodes(result.verdict.assertions?.filter((item) => item.status === "fail" || item.severity === "blocker")
|
|
@@ -1291,6 +1340,20 @@ async function dispatch(command, args, { recorder = NOOP_RECORDER, ambient = nul
|
|
|
1291
1340
|
);
|
|
1292
1341
|
}
|
|
1293
1342
|
|
|
1343
|
+
// A flag takes its value as the next token. `--flag=value` would otherwise be
|
|
1344
|
+
// an unknown key, silently ignored (`qa run --page=x` widening to a full run),
|
|
1345
|
+
// so it is refused at dispatch, like every other argv refusal; the stale
|
|
1346
|
+
// sweep before dispatch is skipped for it (JOINED_FLAGS). Only the name is
|
|
1347
|
+
// quoted: the value can be a secret, such as an --auth-cookie.
|
|
1348
|
+
function refuseJoinedFlag(args) {
|
|
1349
|
+
const flags = args?.[JOINED_FLAGS];
|
|
1350
|
+
if (!flags?.length) return;
|
|
1351
|
+
const [flag] = flags;
|
|
1352
|
+
if (!flag) throw refused("--=… is not accepted: the flag name is missing before =. Write the flag, then its value as the next argument (--flag value).");
|
|
1353
|
+
const name = `--${flag}`;
|
|
1354
|
+
throw refused(`${name}=… is not accepted: a flag takes its value as the next argument, not --flag=value. Write \`${name}\` alone for an on/off flag, or \`${name} <value>\` for a flag that takes a value.`);
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1294
1357
|
export function parseArgs(argv) {
|
|
1295
1358
|
const args = { _: [] };
|
|
1296
1359
|
for (let index = 0; index < argv.length; index += 1) {
|
|
@@ -1299,6 +1362,12 @@ export function parseArgs(argv) {
|
|
|
1299
1362
|
args._.push(token);
|
|
1300
1363
|
continue;
|
|
1301
1364
|
}
|
|
1365
|
+
const equal = token.indexOf("=");
|
|
1366
|
+
if (equal >= 2) {
|
|
1367
|
+
if (!args[JOINED_FLAGS]) Object.defineProperty(args, JOINED_FLAGS, { value: [] });
|
|
1368
|
+
args[JOINED_FLAGS].push(token.slice(2, equal));
|
|
1369
|
+
continue;
|
|
1370
|
+
}
|
|
1302
1371
|
const key = token.slice(2);
|
|
1303
1372
|
const next = argv[index + 1];
|
|
1304
1373
|
if (!next || next.startsWith("--")) {
|
|
@@ -1387,7 +1456,9 @@ async function resolveSpecPath(args, opts = {}) {
|
|
|
1387
1456
|
return { specPath: cachePath, source: "remote", mapId, proxyBase,
|
|
1388
1457
|
savedMapRevision: { map_id: mapId, hash: spec.spec_identity?.spec_hash || spec.spec_hash || null,
|
|
1389
1458
|
algorithm: "map-store-v1", local_spec_material_hash: specMaterialHash(spec) },
|
|
1390
|
-
|
|
1459
|
+
// The fetched spec itself, for a caller that writes the cache later and
|
|
1460
|
+
// must know the funnel's route first. Not part of the reported source.
|
|
1461
|
+
...(opts.deferCacheWrite ? { publishSpec, fetchedSpec: spec } : {}),
|
|
1391
1462
|
};
|
|
1392
1463
|
}
|
|
1393
1464
|
throw refused(
|
|
@@ -1516,11 +1587,13 @@ function createProofPolicy({ orderPathDepth = null } = {}) {
|
|
|
1516
1587
|
// channel that supplies it. Source screenshot proof is seeded only by the
|
|
1517
1588
|
// source-html manifest's pages[].screenshots[], which is documented nowhere in
|
|
1518
1589
|
// the blocking reason itself.
|
|
1519
|
-
|
|
1590
|
+
// `packagePath` is where this run emitted the package, relative to the target
|
|
1591
|
+
// repo: under the funnel's runtime directory.
|
|
1592
|
+
const designSourcePackageRemedy = (packagePath = DESIGN_SOURCE_PACKAGE_REL_PATH) => [
|
|
1520
1593
|
`Supply the missing source proof through pages[].screenshots[] in ${SOURCE_HTML_MANIFEST_REL_PATH}`,
|
|
1521
1594
|
"under the source root, or in a manifest anywhere else named by --design-manifest <path>",
|
|
1522
1595
|
"(one available desktop record and one available mobile record per renderable page);",
|
|
1523
|
-
`then, if no downstream stage has consumed it, remove the Design Source Package this blocked run emitted at ${
|
|
1596
|
+
`then, if no downstream stage has consumed it, remove the Design Source Package this blocked run emitted at ${packagePath}`,
|
|
1524
1597
|
"and rerun prepare-build/start.",
|
|
1525
1598
|
'See "Clearing DESIGN_SOURCE_PACKAGE_NOT_READY" in docs/design-source-package.md.',
|
|
1526
1599
|
].join(" ");
|
|
@@ -1531,12 +1604,13 @@ const DESIGN_SOURCE_PACKAGE_REMEDY = [
|
|
|
1531
1604
|
// exception reasons keep their own wording.
|
|
1532
1605
|
const DESIGN_SOURCE_PACKAGE_REMEDY_REASON = /source_screenshot proof\.$|^Source TODO "capture-(?![^"]*-template-)[^"]*" is /;
|
|
1533
1606
|
|
|
1534
|
-
function designSourcePackageBlockers(prepared) {
|
|
1607
|
+
function designSourcePackageBlockers(prepared, targetRepo) {
|
|
1535
1608
|
if (!["blocked", "pending"].includes(prepared.value?.readiness?.status)) return [];
|
|
1609
|
+
const remedy = designSourcePackageRemedy(prepared.path && targetRepo ? relFromDir(targetRepo, prepared.path) : undefined);
|
|
1536
1610
|
return (prepared.value.readiness.blocking_reasons || []).map((message, index) => ({
|
|
1537
1611
|
code: "DESIGN_SOURCE_PACKAGE_NOT_READY",
|
|
1538
1612
|
stage: "prepare_build",
|
|
1539
|
-
message: DESIGN_SOURCE_PACKAGE_REMEDY_REASON.test(message) ? `${message} ${
|
|
1613
|
+
message: DESIGN_SOURCE_PACKAGE_REMEDY_REASON.test(message) ? `${message} ${remedy}` : message,
|
|
1540
1614
|
detail: {
|
|
1541
1615
|
blocker_index: index,
|
|
1542
1616
|
readiness_status: prepared.value.readiness.status,
|
|
@@ -1641,18 +1715,44 @@ async function prepareBuild(args, options = {}) {
|
|
|
1641
1715
|
if (publishSpec && !existsSync(targetRepo)) mkdirSync(targetRepo, { recursive: true });
|
|
1642
1716
|
if (!existsSync(targetRepo) || !statSync(targetRepo).isDirectory()) throw new Error(`Target repo is not a directory: ${targetRepo}`);
|
|
1643
1717
|
|
|
1644
|
-
|
|
1645
|
-
|
|
1718
|
+
// Which funnel this is decides where its packet and records go (#719): a
|
|
1719
|
+
// project's first funnel uses the default packet and the project-level
|
|
1720
|
+
// .campaign-runtime/, every later one its own (planFunnelWorkspace). The
|
|
1721
|
+
// route is read here, before the lock, and checked again under it.
|
|
1722
|
+
const plannedRoute = intakeRouteSlug(options.fetchedSpec || readSpecForRoute(specPath), args);
|
|
1723
|
+
const planFunnel = () => plannedRoute
|
|
1724
|
+
? refusing(() => planFunnelWorkspace(targetRepo, plannedRoute, { packetPath: args.out || null, force: args.force === true }))
|
|
1725
|
+
: { packetPath: resolve(args.out || join(targetRepo, DEFAULT_PACKET_FILENAME)), runtimeRelPath: RUNTIME_DIR_REL_PATH, scoped: false, addedBeside: null };
|
|
1726
|
+
const funnel = planFunnel();
|
|
1727
|
+
const sidecars = campaignSidecarPaths(targetRepo, funnel.runtimeRelPath);
|
|
1728
|
+
const packetPath = funnel.packetPath;
|
|
1646
1729
|
const contextPath = resolve(args["context-out"] || sidecars.contextPath);
|
|
1647
1730
|
const reportPath = resolve(args["report-out"] || sidecars.reportPath);
|
|
1648
1731
|
const doctorOutPath = resolve(args["doctor-out"] || sidecars.doctorOutPath);
|
|
1649
|
-
const briefPath = resolve(args["brief-out"] || join(
|
|
1732
|
+
const briefPath = resolve(args["brief-out"] || join(sidecars.runtimeDir, relative(RUNTIME_DIR_REL_PATH, BUILD_BRIEF_NORMALIZED_REL_PATH)));
|
|
1650
1733
|
return withDesignSourcePublication({
|
|
1651
1734
|
targetRepo,
|
|
1735
|
+
runtimeDir: sidecars.runtimeDir,
|
|
1652
1736
|
outputs: { packetPath, contextPath, reportPath, doctorOutPath, briefPath },
|
|
1653
1737
|
force: args.force === true,
|
|
1654
1738
|
publishSpec,
|
|
1655
|
-
}, (publication) => prepareBuildUnderLock({ args, options, specPath, sourceRoot, targetRepo, publication }));
|
|
1739
|
+
}, (publication) => prepareBuildUnderLock({ args, options, specPath, sourceRoot, targetRepo, publication, funnel: { ...funnel, route: plannedRoute }, planFunnel }));
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
// A spec that cannot be read here is read again, and refused, under the lock.
|
|
1743
|
+
function readSpecForRoute(specPath) {
|
|
1744
|
+
try {
|
|
1745
|
+
return readJson(specPath);
|
|
1746
|
+
} catch {
|
|
1747
|
+
return null;
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
// The public route slug intake will use, from the flag or the spec; null when
|
|
1752
|
+
// neither names one (prepare-build then refuses under the lock).
|
|
1753
|
+
function intakeRouteSlug(spec, args) {
|
|
1754
|
+
if (!isObject(spec)) return optionalString(args["public-route-slug"]) || null;
|
|
1755
|
+
return normalizePublicRouteSlug(campaignIdentity(spec, args).publicRouteSlug) || null;
|
|
1656
1756
|
}
|
|
1657
1757
|
|
|
1658
1758
|
function prepareBuildUnderLock({
|
|
@@ -1662,6 +1762,8 @@ function prepareBuildUnderLock({
|
|
|
1662
1762
|
sourceRoot,
|
|
1663
1763
|
targetRepo,
|
|
1664
1764
|
publication,
|
|
1765
|
+
funnel,
|
|
1766
|
+
planFunnel,
|
|
1665
1767
|
}) {
|
|
1666
1768
|
const { packetPath, contextPath, reportPath, doctorOutPath, briefPath, designSourcePackagePath } = publication.paths;
|
|
1667
1769
|
// #531: a host-prefixed route ("shop.example.com/route/upsell/") in a Map
|
|
@@ -1685,6 +1787,22 @@ function prepareBuildUnderLock({
|
|
|
1685
1787
|
throw new Error("CampaignSpec requires exactly one identity: a saved spec_identity.map_id, or an agent-authored spec_identity.local_spec_id (1–64 letters, digits, underscores or hyphens). Keep the local ID stable across revisions; do not invent a Map ID.");
|
|
1686
1788
|
}
|
|
1687
1789
|
if (!publicRouteSlug) throw new Error("CampaignSpec has no public route slug. Set spec_identity.public_route_slug or campaign.slug.");
|
|
1790
|
+
// The packet and runtime directory were chosen for the route read before
|
|
1791
|
+
// the lock; a spec that changed meanwhile would file this funnel's records
|
|
1792
|
+
// under another route's.
|
|
1793
|
+
if (normalizePublicRouteSlug(publicRouteSlug) !== funnel.route) {
|
|
1794
|
+
throw new Error(`The CampaignSpec's public route slug changed while prepare-build waited for the target lock (${JSON.stringify(funnel.route)} became ${JSON.stringify(publicRouteSlug)}). Re-run prepare-build.`);
|
|
1795
|
+
}
|
|
1796
|
+
// Another funnel set up while this run waited can change where this one
|
|
1797
|
+
// belongs (two first funnels started at once in an empty project). The
|
|
1798
|
+
// paths this run writes were fixed before the lock, so it stops instead.
|
|
1799
|
+
const replanned = planFunnel();
|
|
1800
|
+
if (replanned.packetPath !== funnel.packetPath || replanned.runtimeRelPath !== funnel.runtimeRelPath) {
|
|
1801
|
+
throw new Error(
|
|
1802
|
+
`Another funnel was set up in ${targetRepo} while prepare-build waited for the target lock, so route ${JSON.stringify(funnel.route)} now belongs at `
|
|
1803
|
+
+ `${replanned.packetPath} with its records under ${replanned.runtimeRelPath}/, not ${funnel.packetPath} and ${funnel.runtimeRelPath}/. Re-run prepare-build.`,
|
|
1804
|
+
);
|
|
1805
|
+
}
|
|
1688
1806
|
|
|
1689
1807
|
// Dispatch validated the argv-only flags before spec resolution, including
|
|
1690
1808
|
// the value forms of --template-family, --allow-uncertified-template,
|
|
@@ -1845,6 +1963,7 @@ function prepareBuildUnderLock({
|
|
|
1845
1963
|
// campaign's report), and the previous brief carries field provenance.
|
|
1846
1964
|
const priorBriefInputs = priorBriefInputsForIntake({ reportPath, briefPath, identity: { spec: { map_id: mapId, ...(localSpecId ? { local_spec_id: localSpecId } : {}) }, campaign: { public_route_slug: publicRouteSlug } } });
|
|
1847
1965
|
const buildBrief = createCampaignBuildBriefArtifact({
|
|
1966
|
+
designFiles: resolveDesignFileReferences({ activePages, mappings: matched.mappings, manifest: manifestResult.manifest }).pages,
|
|
1848
1967
|
inputPath: briefDiscovery?.path || null,
|
|
1849
1968
|
inputSource: briefDiscovery?.source || null,
|
|
1850
1969
|
spec,
|
|
@@ -1915,7 +2034,7 @@ function prepareBuildUnderLock({
|
|
|
1915
2034
|
mapId,
|
|
1916
2035
|
publicRouteSlug,
|
|
1917
2036
|
});
|
|
1918
|
-
const designSourceBlockers = designSourcePackageBlockers(designSourcePackage);
|
|
2037
|
+
const designSourceBlockers = designSourcePackageBlockers(designSourcePackage, targetRepo);
|
|
1919
2038
|
const blockers = [...sourceBlockers, ...briefBlockers, ...briefQuestionBlockers, ...designSourceBlockers];
|
|
1920
2039
|
const adapterDecisions = createAdapterDecisions({ commerceZoneFindings, wrapperPolicy: wrapperPolicy.value });
|
|
1921
2040
|
const proofPolicy = createProofPolicy({ orderPathDepth: orderPathDepthFlag });
|
|
@@ -1960,6 +2079,9 @@ function prepareBuildUnderLock({
|
|
|
1960
2079
|
assembly: {
|
|
1961
2080
|
implementation: "next-campaigns-build",
|
|
1962
2081
|
target_repo: relFromFile(packetPath, targetRepo),
|
|
2082
|
+
// Recorded only for a funnel that is not the project's first; absent,
|
|
2083
|
+
// the records are under the project-level .campaign-runtime/.
|
|
2084
|
+
...(funnel.scoped ? { runtime_dir: funnel.runtimeRelPath } : {}),
|
|
1963
2085
|
output_dir: outputDir,
|
|
1964
2086
|
template_family: isUnresolvedTemplateFamily(templateFamily) ? "undecided" : templateFamily,
|
|
1965
2087
|
template_decision_notes: templateLocked
|
|
@@ -2033,6 +2155,7 @@ function prepareBuildUnderLock({
|
|
|
2033
2155
|
path: portable(specPath),
|
|
2034
2156
|
hash: specFileHash,
|
|
2035
2157
|
material_hash: specMaterialHash(spec),
|
|
2158
|
+
route_masked_material_hash: routeMaskedSpecMaterialHash(spec),
|
|
2036
2159
|
active_pages: activePages.map((page) => ({
|
|
2037
2160
|
id: page.id,
|
|
2038
2161
|
type: page.type || null,
|
|
@@ -2085,7 +2208,7 @@ function prepareBuildUnderLock({
|
|
|
2085
2208
|
target_repo: ".",
|
|
2086
2209
|
output_dir: portable(resolve(targetRepo, outputDir)),
|
|
2087
2210
|
handoff_skill: existsSync(resolve(targetRepo, outputDir)) ? "next-campaigns-build" : "next-campaigns-os-setup",
|
|
2088
|
-
handoff_artifact:
|
|
2211
|
+
handoff_artifact: `${funnel.runtimeRelPath}/setup-handoff.json`,
|
|
2089
2212
|
reason: existsSync(resolve(targetRepo, outputDir))
|
|
2090
2213
|
? "Target campaign output directory already exists."
|
|
2091
2214
|
: "Target campaign output directory is missing; scaffold before build.",
|
|
@@ -2205,6 +2328,7 @@ function prepareBuildUnderLock({
|
|
|
2205
2328
|
context,
|
|
2206
2329
|
report,
|
|
2207
2330
|
doctor,
|
|
2331
|
+
...(funnel.addedBeside ? { funnelAddedBeside: funnel.addedBeside } : {}),
|
|
2208
2332
|
};
|
|
2209
2333
|
}
|
|
2210
2334
|
|
|
@@ -2285,7 +2409,7 @@ function assemblyThemeFromContext(theme) {
|
|
|
2285
2409
|
load_order: "unknown",
|
|
2286
2410
|
commerce_pages: [],
|
|
2287
2411
|
evidence: wroteCss
|
|
2288
|
-
? ["prepare-build auto-generated .campaign-runtime/theme/brand-theme.css; build should copy that existing artifact into campaign assets and load it after next-core.css on commerce pages."]
|
|
2412
|
+
? ["prepare-build auto-generated .campaign-runtime/theme/brand-theme.css; build should copy that existing artifact (and brand-fonts.css, when written) into campaign assets and load it after next-core.css on commerce pages."]
|
|
2289
2413
|
: theme.generated?.can_generate
|
|
2290
2414
|
? ["theme inspect found a generatable brand theme; run theme generate or explicit auto policy before applying."]
|
|
2291
2415
|
: ["theme inspect completed; no generated brand theme was applied during prepare-build."],
|
|
@@ -2488,11 +2612,8 @@ function rejectUnknownStandardizeFlags(args) {
|
|
|
2488
2612
|
const known = new Set(STANDARDIZE_FLAGS);
|
|
2489
2613
|
const unknown = Object.keys(args).filter((key) => key !== "_" && !known.has(key));
|
|
2490
2614
|
if (!unknown.length) return;
|
|
2491
|
-
const valueHint = unknown.some((key) => key.includes("="))
|
|
2492
|
-
? " A flag takes its value as the next argument (--flag value), not --flag=value."
|
|
2493
|
-
: "";
|
|
2494
2615
|
throw refused(
|
|
2495
|
-
`Unknown flag${unknown.length > 1 ? "s" : ""} for standardize: ${unknown.map((key) => `--${key}`).join(", ")}
|
|
2616
|
+
`Unknown flag${unknown.length > 1 ? "s" : ""} for standardize: ${unknown.map((key) => `--${key}`).join(", ")}. Known flags: ${STANDARDIZE_FLAGS.map((key) => `--${key}`).join(", ")}.`,
|
|
2496
2617
|
);
|
|
2497
2618
|
}
|
|
2498
2619
|
|
|
@@ -2584,7 +2705,9 @@ function themeCommand(args) {
|
|
|
2584
2705
|
// (blocked, already-current CSS) leaves doctor's inputs untouched (Kilo
|
|
2585
2706
|
// review, PR #176).
|
|
2586
2707
|
if (written.wrote?.report || written.wrote?.css) {
|
|
2587
|
-
|
|
2708
|
+
const staleTarget = resolveFromFile(packetPath, packet?.assembly?.target_repo) || dirname(packetPath);
|
|
2709
|
+
markDoctorSidecarStale(staleTarget, {
|
|
2710
|
+
path: campaignSidecarPaths(staleTarget, packetRuntimeRelPath(packet)).doctorOutPath,
|
|
2588
2711
|
command: "theme generate",
|
|
2589
2712
|
reason: `Theme artifacts were generated after this doctor snapshot. Re-run ${cmd("doctor")} (or next) for current state.`,
|
|
2590
2713
|
});
|
|
@@ -2805,6 +2928,8 @@ export async function polishCaptureCommand(args, options = {}) {
|
|
|
2805
2928
|
adapterCloseDeadlineMs: options.adapterCloseDeadlineMs,
|
|
2806
2929
|
probeClock: options.probeClock,
|
|
2807
2930
|
targetRepo,
|
|
2931
|
+
visual: { packetPath, targetRepo, ...options.visual },
|
|
2932
|
+
onFrozen: options.onFrozen,
|
|
2808
2933
|
});
|
|
2809
2934
|
// Nothing was measured: the held plan error stands.
|
|
2810
2935
|
if (heldPlanError && !capture.readability) throw heldPlanError;
|
|
@@ -2834,6 +2959,8 @@ export async function polishCaptureCommand(args, options = {}) {
|
|
|
2834
2959
|
pageLoad: capture.page_load,
|
|
2835
2960
|
mediaWeight: capture.media_weight,
|
|
2836
2961
|
readability: capture.readability,
|
|
2962
|
+
captures: capture.captures,
|
|
2963
|
+
elementParity: capture.element_parity,
|
|
2837
2964
|
now: options.now ?? new Date(),
|
|
2838
2965
|
});
|
|
2839
2966
|
checkpoint = evaluateRecordedHiddenEagerMediaCheckpoint({
|
|
@@ -2879,6 +3006,15 @@ export async function polishCaptureCommand(args, options = {}) {
|
|
|
2879
3006
|
route_scope: capture.plan?.route_scope ?? null,
|
|
2880
3007
|
routes: (capture.plan?.routes ?? []).map((route) => route.requested_route),
|
|
2881
3008
|
viewports: (capture.plan?.viewports ?? []).map((viewport) => viewport.key),
|
|
3009
|
+
visual: capture.captures ? { cells: capture.captures.cells.length,
|
|
3010
|
+
rendered: capture.captures.cells.filter((cell) => cell.status === "rendered").length,
|
|
3011
|
+
comparisons_compared: capture.captures.comparisons.filter((comparison) => comparison.status === "compared").length,
|
|
3012
|
+
comparisons_unsupported: capture.captures.comparisons.filter((comparison) => comparison.status === "unsupported").length,
|
|
3013
|
+
dependencies: capture.captures.dependencies, adapters: capture.captures.adapters,
|
|
3014
|
+
// A run budget spent: a cell never rendered, or a decode or diff never ran (§2.4).
|
|
3015
|
+
budget_exhausted: capture.captures.cells.some((cell) => cell.status === "run_budget_exhausted")
|
|
3016
|
+
|| capture.captures.references.some((reference) => reference.verification?.reason === "verify_budget_exhausted")
|
|
3017
|
+
|| capture.captures.comparisons.some((comparison) => comparison.reason === "diff_budget_exhausted") } : null,
|
|
2882
3018
|
},
|
|
2883
3019
|
};
|
|
2884
3020
|
}
|
|
@@ -3256,8 +3392,7 @@ export function pageKitSyncCommand(args) {
|
|
|
3256
3392
|
// Assembly Report whose waivers are honoured below.
|
|
3257
3393
|
const unknown = Object.keys(args).filter((key) => key !== "_" && !PAGE_KIT_SYNC_FLAGS.includes(key));
|
|
3258
3394
|
if (unknown.length) {
|
|
3259
|
-
|
|
3260
|
-
throw refused(`Unknown flag${unknown.length > 1 ? "s" : ""} for page-kit sync: ${unknown.map((key) => `--${key}`).join(", ")}.${valueHint} Known flags: ${PAGE_KIT_SYNC_FLAGS.map((key) => `--${key}`).join(", ")}.`);
|
|
3395
|
+
throw refused(`Unknown flag${unknown.length > 1 ? "s" : ""} for page-kit sync: ${unknown.map((key) => `--${key}`).join(", ")}. Known flags: ${PAGE_KIT_SYNC_FLAGS.map((key) => `--${key}`).join(", ")}.`);
|
|
3261
3396
|
}
|
|
3262
3397
|
const packetPath = resolve(requireArg(args, "packet"));
|
|
3263
3398
|
const dryRun = isDryRun(args);
|
|
@@ -3334,11 +3469,7 @@ export function pageKitSyncCommand(args) {
|
|
|
3334
3469
|
// spec.local_path would write another campaign's store profile into this
|
|
3335
3470
|
// route with a plausible-looking diff.
|
|
3336
3471
|
if (spec && publicRouteSlug) {
|
|
3337
|
-
const specSlug =
|
|
3338
|
-
optionalString(spec.spec_identity?.public_route_slug)
|
|
3339
|
-
|| optionalString(spec.campaign?.slug)
|
|
3340
|
-
|| optionalString(spec.campaign?.id),
|
|
3341
|
-
);
|
|
3472
|
+
const specSlug = specPublicRouteSlug(spec);
|
|
3342
3473
|
const specMapId = optionalString(spec.spec_identity?.map_id) || optionalString(spec.map_id);
|
|
3343
3474
|
const packetMapId = optionalString(packet.spec?.map_id);
|
|
3344
3475
|
if (specSlug && specSlug !== publicRouteSlug) {
|
|
@@ -3464,6 +3595,7 @@ export function pageKitSyncCommand(args) {
|
|
|
3464
3595
|
// result, never a thrown error that hides the write.
|
|
3465
3596
|
try {
|
|
3466
3597
|
markDoctorSidecarStale(targetRepo, {
|
|
3598
|
+
path: campaignSidecarPaths(targetRepo, packetRuntimeRelPath(packet)).doctorOutPath,
|
|
3467
3599
|
command: "page-kit sync",
|
|
3468
3600
|
reason: `page-kit sync rewrote ${PAGE_KIT_CAMPAIGNS_REL_PATH}[${publicRouteSlug}] after this doctor snapshot. Re-run ${cmd("doctor")} (or next) for current state.`,
|
|
3469
3601
|
});
|
|
@@ -3494,7 +3626,7 @@ export function pageKitSyncCommand(args) {
|
|
|
3494
3626
|
// the default run stays offline. --dry-run prints the same diff and writes
|
|
3495
3627
|
// nothing. Exit 2 when the packet, the spec or the target entry is missing,
|
|
3496
3628
|
// or the store cannot be read.
|
|
3497
|
-
const SPEC_DERIVE_FLAGS = Object.freeze(["packet", "dry-run", "json", "report", "from-store", "store-token-source"]);
|
|
3629
|
+
const SPEC_DERIVE_FLAGS = Object.freeze(["packet", "dry-run", "json", "report", "from-store", "store-token-source", "no-live-refs"]);
|
|
3498
3630
|
|
|
3499
3631
|
// The store flags, parsed once for both the async reader and the sync
|
|
3500
3632
|
// command: `{ subdomain, token_env }` when --from-store is given, null when
|
|
@@ -3522,9 +3654,9 @@ function parseSpecDeriveStoreFlags(args) {
|
|
|
3522
3654
|
// run the same command with the store read in hand. The only network the
|
|
3523
3655
|
// command ever does happens here, and the token never leaves this function:
|
|
3524
3656
|
// the result names the credential source, never its value.
|
|
3525
|
-
export async function specDeriveFromStoreCommand(args, { fetchImpl = globalThis.fetch, env = process.env, credentials, warn = console.warn } = {}) {
|
|
3657
|
+
export async function specDeriveFromStoreCommand(args, { fetchImpl = globalThis.fetch, env = process.env, credentials, warn = console.warn, liveCampaign = undefined } = {}) {
|
|
3526
3658
|
const store = parseSpecDeriveStoreFlags(args);
|
|
3527
|
-
if (!store) return specDeriveCommand(args);
|
|
3659
|
+
if (!store) return specDeriveCommand(args, { liveCampaign });
|
|
3528
3660
|
const preflight = specDeriveCommand(args, { store: { ...store, status: "preflight" } });
|
|
3529
3661
|
if (preflight.errors.length) return preflight;
|
|
3530
3662
|
let read;
|
|
@@ -3537,7 +3669,36 @@ export async function specDeriveFromStoreCommand(args, { fetchImpl = globalThis.
|
|
|
3537
3669
|
read = await readGatewayStoreProfile({ subdomain: store.subdomain, credentials, fetchImpl });
|
|
3538
3670
|
}
|
|
3539
3671
|
// Recheck the original packet/spec identity after the network operation.
|
|
3540
|
-
return specDeriveCommand(args, { store: { ...store, ...read, expected: { spec_path: preflight.spec_path, public_route_slug: preflight.public_route_slug } } });
|
|
3672
|
+
return specDeriveCommand(args, { liveCampaign, store: { ...store, ...read, expected: { spec_path: preflight.spec_path, public_route_slug: preflight.public_route_slug } } });
|
|
3673
|
+
}
|
|
3674
|
+
|
|
3675
|
+
// The live campaign read `spec derive` makes (#721): the one read-only GET of
|
|
3676
|
+
// {proxy-base}/api/campaign doctor makes, under the same rule (packet mode,
|
|
3677
|
+
// the packet's built _site/<route>/ exists, a public Campaigns API key
|
|
3678
|
+
// resolves), with the campaign body kept so the mirrored offer and package
|
|
3679
|
+
// fields can be refreshed from it. --no-live-refs sends nothing. `undefined`
|
|
3680
|
+
// means no read was due.
|
|
3681
|
+
export async function readSpecDeriveLiveCampaign(args, { fetchImpl = globalThis.fetch, env = process.env, warn = undefined } = {}) {
|
|
3682
|
+
if (liveRefsDisabled(args)) return disabledLiveRead();
|
|
3683
|
+
if (!isNonEmptyString(args.packet)) return undefined;
|
|
3684
|
+
let workspace;
|
|
3685
|
+
try {
|
|
3686
|
+
workspace = resolveCampaignWorkspace(resolve(args.packet), { followContextPointer: false });
|
|
3687
|
+
} catch {
|
|
3688
|
+
// An unreadable packet is the derive's own error; there is nothing to read for.
|
|
3689
|
+
return undefined;
|
|
3690
|
+
}
|
|
3691
|
+
const slug = normalizePublicRouteSlug(workspace.packet?.campaign?.public_route_slug);
|
|
3692
|
+
if (!workspace.targetRepo || !slug || !existsSync(join(workspace.targetRepo, "_site", slug))) return undefined;
|
|
3693
|
+
return readLiveCampaignForPacket({
|
|
3694
|
+
packet: workspace.packet,
|
|
3695
|
+
packetPath: workspace.packetPath,
|
|
3696
|
+
env,
|
|
3697
|
+
fetchImpl,
|
|
3698
|
+
proxyBase: optionalString(args["proxy-base"]) || DEFAULT_PROXY_BASE,
|
|
3699
|
+
includeCampaign: true,
|
|
3700
|
+
...(warn ? { warn } : {}),
|
|
3701
|
+
});
|
|
3541
3702
|
}
|
|
3542
3703
|
|
|
3543
3704
|
// The page files page-kit renders under the campaign's source directory, with
|
|
@@ -3565,30 +3726,32 @@ function listPageKitPageFiles(outputDir, publicRouteSlug, { assetsHtmlIgnored =
|
|
|
3565
3726
|
// page-kit honours a permalink before its filename rule, so the
|
|
3566
3727
|
// frontmatter is read first (quoting, comments, BOM and CRLF as
|
|
3567
3728
|
// page-kit's reader takes them); only a file with no usable permalink
|
|
3568
|
-
// falls back to the filename route.
|
|
3569
|
-
|
|
3729
|
+
// falls back to the filename route. The page_type and the links to
|
|
3730
|
+
// other pages are read too: after handoff they say which files are
|
|
3731
|
+
// funnel pages and how they link (#721).
|
|
3732
|
+
const { permalink, page_type: pageType, next_url: nextUrl, decline_url: declineUrl, next_step: nextStep, step_number: stepNumber } = frontmatterScalars(readFileSync(fullPath, "utf8"));
|
|
3733
|
+
const facts = { page_type: pageType, next_url: nextUrl, decline_url: declineUrl, next_step: nextStep, step_number: stepNumber };
|
|
3570
3734
|
if (permalink === null) {
|
|
3571
3735
|
const filenameRoute = pageRouteForFile(path);
|
|
3572
3736
|
if (filenameRoute === null) continue;
|
|
3573
|
-
files.push({ path, basename: basename(entry.name, ".html"), route: filenameRoute, permalink: null, problem: null });
|
|
3737
|
+
files.push({ path, basename: basename(entry.name, ".html"), route: filenameRoute, permalink: null, problem: null, ...facts });
|
|
3574
3738
|
continue;
|
|
3575
3739
|
}
|
|
3576
3740
|
const resolved = permalinkRoute(permalink, publicRouteSlug);
|
|
3577
|
-
files.push({ path, basename: basename(entry.name, ".html"), route: resolved.route ?? null, permalink, problem: resolved.problem ?? null });
|
|
3741
|
+
files.push({ path, basename: basename(entry.name, ".html"), route: resolved.route ?? null, permalink, problem: resolved.problem ?? null, ...facts });
|
|
3578
3742
|
}
|
|
3579
3743
|
};
|
|
3580
3744
|
walk(outputDir);
|
|
3581
3745
|
return files.sort((a, b) => a.path.localeCompare(b.path));
|
|
3582
3746
|
}
|
|
3583
3747
|
|
|
3584
|
-
export function specDeriveCommand(args, { store: storeRead = null } = {}) {
|
|
3748
|
+
export function specDeriveCommand(args, { store: storeRead = null, liveCampaign = undefined } = {}) {
|
|
3585
3749
|
// A command that rewrites the spec rejects flags it does not know: a
|
|
3586
3750
|
// mistyped --dryrun must not fall through to a real write. --report names
|
|
3587
3751
|
// the Assembly Report whose waivers are honoured below.
|
|
3588
3752
|
const unknown = Object.keys(args).filter((key) => key !== "_" && !SPEC_DERIVE_FLAGS.includes(key));
|
|
3589
3753
|
if (unknown.length) {
|
|
3590
|
-
|
|
3591
|
-
throw refused(`Unknown flag${unknown.length > 1 ? "s" : ""} for spec derive: ${unknown.map((key) => `--${key}`).join(", ")}.${valueHint} Known flags: ${SPEC_DERIVE_FLAGS.map((key) => `--${key}`).join(", ")}.`);
|
|
3754
|
+
throw refused(`Unknown flag${unknown.length > 1 ? "s" : ""} for spec derive: ${unknown.map((key) => `--${key}`).join(", ")}. Known flags: ${SPEC_DERIVE_FLAGS.map((key) => `--${key}`).join(", ")}.`);
|
|
3592
3755
|
}
|
|
3593
3756
|
// The store read is supplied by specDeriveFromStoreCommand; this function
|
|
3594
3757
|
// never touches the network itself, so a --from-store call that reaches it
|
|
@@ -3616,6 +3779,8 @@ export function specDeriveCommand(args, { store: storeRead = null } = {}) {
|
|
|
3616
3779
|
not_derived: [],
|
|
3617
3780
|
not_in_target: [],
|
|
3618
3781
|
stale_hints: [],
|
|
3782
|
+
page_scope: null,
|
|
3783
|
+
live_campaign: null,
|
|
3619
3784
|
store: storeFlags
|
|
3620
3785
|
? { subdomain: storeFlags.subdomain, admin_api: adminApiBaseForStore(storeFlags.subdomain), ...(storeFlags.token_env ? {} : { transport: "gateway", endpoint: "https://mcp.nextcommerce.com/admin/" }), token_source: storeFlags.token_env ? `env:${storeFlags.token_env}` : "gateway:login", store_read: null, pages_read: null, primary_domain: null }
|
|
3621
3786
|
: null,
|
|
@@ -3679,11 +3844,7 @@ export function specDeriveCommand(args, { store: storeRead = null } = {}) {
|
|
|
3679
3844
|
// (campaign.route_slug_identity); without it a stale or copy-pasted
|
|
3680
3845
|
// spec.local_path would receive another campaign's routes and pin.
|
|
3681
3846
|
if (spec && publicRouteSlug) {
|
|
3682
|
-
const specSlug =
|
|
3683
|
-
optionalString(spec.spec_identity?.public_route_slug)
|
|
3684
|
-
|| optionalString(spec.campaign?.slug)
|
|
3685
|
-
|| optionalString(spec.campaign?.id),
|
|
3686
|
-
);
|
|
3847
|
+
const specSlug = specPublicRouteSlug(spec);
|
|
3687
3848
|
const specMapId = optionalString(spec.spec_identity?.map_id) || optionalString(spec.map_id);
|
|
3688
3849
|
const packetMapId = optionalString(packet.spec?.map_id);
|
|
3689
3850
|
if (specSlug && specSlug !== publicRouteSlug) {
|
|
@@ -3806,7 +3967,24 @@ export function specDeriveCommand(args, { store: storeRead = null } = {}) {
|
|
|
3806
3967
|
addIssue(result.errors, `spec.derive.${codes[storeRead.status] || "store_unreachable"}`, `${storeRead.detail} Nothing was written.`, { subdomain: storeRead.subdomain, token_source: storeRead.token_env ? `env:${storeRead.token_env}` : "gateway:login" });
|
|
3807
3968
|
return result;
|
|
3808
3969
|
}
|
|
3809
|
-
|
|
3970
|
+
// After handoff the page tree also says which pages exist (#721).
|
|
3971
|
+
const handedOff = campaignHandedOff(report);
|
|
3972
|
+
const plan = planSpecDerive({ spec, entry, pageFiles, packetBindings, waivedGates, waiversUnknown, publicRouteSlug, handedOff });
|
|
3973
|
+
result.page_scope = plan.page_scope;
|
|
3974
|
+
// The mirrored offer and package fields, from the live campaign read
|
|
3975
|
+
// (#721). A read that did not happen leaves them as they are and says why.
|
|
3976
|
+
if (liveCampaign?.status === "read" && isObject(liveCampaign.campaign)) {
|
|
3977
|
+
const mirror = planCampaignMirrorDerive({ spec, campaign: liveCampaign.campaign, campaignRef: liveCampaign.campaign_ref_id || null });
|
|
3978
|
+
plan.changes.push(...mirror.changes);
|
|
3979
|
+
plan.unchanged.push(...mirror.unchanged);
|
|
3980
|
+
plan.not_derived.push(...mirror.not_derived);
|
|
3981
|
+
result.live_campaign = { status: "read", key_source: liveCampaign.key_source || null, campaign_ref_id: liveCampaign.campaign_ref_id || null, ...mirror.summary };
|
|
3982
|
+
} else if (liveCampaign) {
|
|
3983
|
+
result.live_campaign = { status: "not_run", reason_code: liveCampaign.reason_code || null, reason: liveCampaign.reason || null, key_source: liveCampaign.key_source || null };
|
|
3984
|
+
if (!["disabled", "no_key", "not_read"].includes(liveCampaign.reason_code)) {
|
|
3985
|
+
addIssue(result.warnings, "spec.derive.live_campaign_not_read", `The live campaign was not read (${singleLineDetail(liveCampaign.reason || liveCampaign.reason_code || "unknown reason")}); the spec's offer and package fields were left as they are.`, { reason_code: liveCampaign.reason_code || null });
|
|
3986
|
+
}
|
|
3987
|
+
}
|
|
3810
3988
|
let storeChanged = false;
|
|
3811
3989
|
if (storeRead) {
|
|
3812
3990
|
const storePlan = planStoreProfileDerive({ spec, store: storeRead.store, pages: storeRead.pages, pagesStatus: storeRead.pages_status, pagesDetail: storeRead.pages_detail, subdomain: storeRead.subdomain });
|
|
@@ -3830,11 +4008,18 @@ export function specDeriveCommand(args, { store: storeRead = null } = {}) {
|
|
|
3830
4008
|
addIssue(result.warnings, `spec.derive.${row.reason}`, `${row.field} was not derived: ${row.detail}`, { field: row.field, reason: row.reason, ...(row.page_id ? { page_id: row.page_id } : {}) });
|
|
3831
4009
|
}
|
|
3832
4010
|
for (const hint of plan.stale_hints) {
|
|
3833
|
-
addIssue(result.warnings, "spec.derive.routing_hint_stale", `page "${hint.page_id}" carries sdk_hints.meta_tags.${hint.tag} ${JSON.stringify(hint.value)}, but page "${hint.target_page_id}" now derives to ${JSON.stringify(hint.derived_route)}.
|
|
4011
|
+
addIssue(result.warnings, "spec.derive.routing_hint_stale", `page "${hint.page_id}" carries sdk_hints.meta_tags.${hint.tag} ${JSON.stringify(hint.value)}, but page "${hint.target_page_id}" now derives to ${JSON.stringify(hint.derived_route)}. Derive rewrites a hint only on an edge it re-points after handoff; edit the hint to that route in the spec (or, while the Map is still the spec's source, re-save the Map) so the built meta tag and doctor's expectation agree. Doctor blocks and record spec refuses until then.`, hint);
|
|
3834
4012
|
}
|
|
3835
4013
|
for (const block of plan.created_blocks || []) {
|
|
3836
4014
|
addIssue(result.warnings, "spec.derive.analytics_block_created", `${block} ${dryRun ? "would be" : "is"} created from the repo's id: the spec ${dryRun ? "would then declare" : "now declares"} an analytics contract, so QA expects that tag to fire instead of treating analytics as advisory.`);
|
|
3837
4015
|
}
|
|
4016
|
+
// A vendor block changes what doctor reads; it turns on QA's analytics leg
|
|
4017
|
+
// only when it is the first analytics the spec carries (a provider block
|
|
4018
|
+
// created beside it already says so).
|
|
4019
|
+
const vendorOpensAnalytics = plan.analytics_created === true && !(plan.created_blocks || []).length;
|
|
4020
|
+
for (const block of plan.created_vendor_blocks || []) {
|
|
4021
|
+
addIssue(result.warnings, "spec.derive.analytics_vendor_created", `${block} ${dryRun ? "would be" : "is"} created from the repo's id: doctor ${dryRun ? "would then read" : "now reads"} that vendor's campaigns.json key against the spec${vendorOpensAnalytics ? `, and the spec ${dryRun ? "would then carry" : "now carries"} an analytics block, so QA runs its analytics leg and expects the typed-card order to fire a Purchase` : ""}.`);
|
|
4022
|
+
}
|
|
3838
4023
|
|
|
3839
4024
|
// What a write entails is said in both modes, so a dry run previews the
|
|
3840
4025
|
// warnings a real run would carry: a lossy round trip, a build now stale,
|
|
@@ -3848,8 +4033,9 @@ export function specDeriveCommand(args, { store: storeRead = null } = {}) {
|
|
|
3848
4033
|
if (serialize(spec) !== text) {
|
|
3849
4034
|
addIssue(result.warnings, "spec.derive.file_reformatted", `${result.spec_path} ${would} re-serialized with ${indent === "\t" ? "tab" : `${indent.length}-space`} indentation; formatting outside the derived fields (key order, whitespace, number spelling) may differ from the original. Review the file diff before committing.`);
|
|
3850
4035
|
}
|
|
3851
|
-
if (plan.changes.some((row) => row.page_id)) {
|
|
3852
|
-
|
|
4036
|
+
if (plan.changes.some((row) => row.page_id && (row.op || /\.page_url$/.test(row.field)))) {
|
|
4037
|
+
const pageSetMoved = plan.changes.some((row) => row.op && row.page_id);
|
|
4038
|
+
addIssue(result.warnings, "spec.derive.projection_stale", `${pageSetMoved ? "A page added or removed" : "A page route moved"} ${dryRun ? "would leave" : "leaves"} the packet's page-kit projection (source_html.pages[].page_kit) and the Build Context describing the old pages. Bind it with ${cmd("record")} spec, which re-projects them for a routes-only change and, on a handed-off campaign, for pages added or removed; any other page-scope change needs ${cmd("prepare-build")}.`);
|
|
3853
4039
|
}
|
|
3854
4040
|
if (stageIsTerminal(report?.stages?.assembly?.status)) {
|
|
3855
4041
|
addIssue(result.warnings, "spec.derive.build_stale", `The Assembly Report records a terminal build (stages.assembly.status ${report.stages.assembly.status}) rendered from the spec ${dryRun ? "this would rewrite" : "just rewritten"}. Bind the rewritten spec with ${cmd("record")} spec (it keeps stage history); next then routes back to the build, and Polish and QA are owed again.`);
|
|
@@ -3901,6 +4087,7 @@ export function specDeriveCommand(args, { store: storeRead = null } = {}) {
|
|
|
3901
4087
|
// The retained doctor snapshot (if any) now predates the spec it judged.
|
|
3902
4088
|
try {
|
|
3903
4089
|
markDoctorSidecarStale(targetRepo, {
|
|
4090
|
+
path: campaignSidecarPaths(targetRepo, packetRuntimeRelPath(packet)).doctorOutPath,
|
|
3904
4091
|
command: "spec derive",
|
|
3905
4092
|
reason: `spec derive rewrote the CampaignSpec after this doctor snapshot. Re-run ${cmd("doctor")} (or next) for current state.`,
|
|
3906
4093
|
});
|
|
@@ -3951,6 +4138,14 @@ export function specDeriveTextLines(result) {
|
|
|
3951
4138
|
}
|
|
3952
4139
|
if (result.unchanged?.length) lines.push(`Unchanged: ${result.unchanged.map((row) => row.field).join(", ")}`);
|
|
3953
4140
|
if (result.not_in_target?.length) lines.push(`Not in target (left as they are): ${result.not_in_target.join(", ")}`);
|
|
4141
|
+
if (result.page_scope?.added?.length) lines.push(`Pages added from the page tree: ${result.page_scope.added.map((page) => `${singleLineField(page.page_id)} (${singleLineField(page.file)})`).join(", ")}`);
|
|
4142
|
+
if (result.page_scope?.removed?.length) lines.push(`Pages removed (their built file is gone): ${result.page_scope.removed.map((page) => `${singleLineField(page.page_id)} (${singleLineField(page.file)})`).join(", ")}`);
|
|
4143
|
+
if (result.page_scope?.unclaimed?.length) lines.push(`Page files no spec page binds (pages are added from the tree only after handoff): ${result.page_scope.unclaimed.map(singleLineField).join(", ")}`);
|
|
4144
|
+
if (result.live_campaign) {
|
|
4145
|
+
lines.push(result.live_campaign.status === "read"
|
|
4146
|
+
? `Live campaign: read${result.live_campaign.campaign_ref_id ? ` (campaign ${singleLineField(result.live_campaign.campaign_ref_id)})` : ""}; offer and package fields refreshed from it`
|
|
4147
|
+
: `Live campaign: not read (${singleLineField(result.live_campaign.reason_code || "unknown")}); offer and package fields left as they are`);
|
|
4148
|
+
}
|
|
3954
4149
|
if (result.warnings?.length) {
|
|
3955
4150
|
lines.push("Warnings:");
|
|
3956
4151
|
for (const issue of result.warnings) lines.push(`- ${formatIssueSummary(issue)}`);
|
|
@@ -4349,7 +4544,7 @@ export function nextStage(stage, args, ambient = null, { qcStandIns = null, qcRe
|
|
|
4349
4544
|
let runRecordCloseout = null;
|
|
4350
4545
|
try {
|
|
4351
4546
|
runRecordCloseout = assessRunRecordCloseout({
|
|
4352
|
-
records: readRunRecordsForTarget(dirname(packetPath)),
|
|
4547
|
+
records: readRunRecordsForTarget(dirname(packetPath), packetRuntimeRelPath(packet)),
|
|
4353
4548
|
packet,
|
|
4354
4549
|
report,
|
|
4355
4550
|
currentQaVerdictDigests: currentQaVerdictDigestsForReport(report, report ? reportPath : null, targetRepo),
|
|
@@ -4375,7 +4570,7 @@ export function nextStage(stage, args, ambient = null, { qcStandIns = null, qcRe
|
|
|
4375
4570
|
// command names the report this run read when that is not the default
|
|
4376
4571
|
// one (from --report or the Build Context pointer), so it records there.
|
|
4377
4572
|
const qc = readCurrentQcResults({ report, doctor, spec, targetRepo, packetPath, reportPath, qcStandIns, rederivers: qcRederivers });
|
|
4378
|
-
result.qc_handoff = buildQcHandoff({ results: qc.results, coverage: qc.coverage, accepts: report?.qc_accepts, packetPath, reportPath: explicitReportPath(reportPath, targetRepo) });
|
|
4573
|
+
result.qc_handoff = buildQcHandoff({ results: qc.results, coverage: qc.coverage, accepts: report?.qc_accepts, packetPath, reportPath: explicitReportPath(reportPath, targetRepo, packetRuntimeRelPath(packet)) });
|
|
4379
4574
|
// One note per operator decision that kept the build output unchanged
|
|
4380
4575
|
// after an input change, quoting the operator's reason.
|
|
4381
4576
|
for (const issue of doctor.warnings) if (issue.code === "assembly.output_unchanged_by_operator_decision") (result.qc_handoff.notes ||= []).push(`Build output kept unchanged after an input change by the operator's decision: "${issue.detail.reason}"`);
|
|
@@ -5053,14 +5248,15 @@ function templateStockPromptLine(packet, derived = {}) {
|
|
|
5053
5248
|
|
|
5054
5249
|
function buildPrompt(packetPath, contextPath, reportPath, packet, derived = {}, intent) {
|
|
5055
5250
|
const briefPath = packet.build_brief?.normalized_path || "(missing; generate or confirm Campaign Build Brief before business-sensitive assembly)";
|
|
5251
|
+
const runtime = runtimeRelPathOf(packet);
|
|
5056
5252
|
return `${campaignIntentPromptHeader(intent)}Use next-campaigns-build for this Campaigns OS handoff.
|
|
5057
5253
|
|
|
5058
5254
|
Read first:
|
|
5059
5255
|
- Build Packet: ${packetPath}
|
|
5060
|
-
- Build Context: ${contextPath ||
|
|
5061
|
-
- Assembly Report: ${reportPath ||
|
|
5256
|
+
- Build Context: ${contextPath || `(use packet-adjacent ${runtime}/build-context.json if present)`}
|
|
5257
|
+
- Assembly Report: ${reportPath || `(use packet-adjacent ${runtime}/assembly-report.json if present)`}
|
|
5062
5258
|
- Campaign Build Brief: ${briefPath}
|
|
5063
|
-
- Design Source Package:
|
|
5259
|
+
- Design Source Package: ${runtime}/input/design-source-package.json when present; use report.design_source_package.material_fingerprint as the source context fingerprint.
|
|
5064
5260
|
- Template family: ${packet.assembly.template_family}${templateStockPromptLine(packet, derived)}
|
|
5065
5261
|
|
|
5066
5262
|
Rules:
|
|
@@ -5071,7 +5267,7 @@ Rules:
|
|
|
5071
5267
|
- Prepared AI/exported HTML must be converted into page-kit-ready source first: keep page-owned body markup, strip document wrappers, add YAML frontmatter, move shared CSS/assets into the campaign structure, and use Liquid helpers only for page-kit links/assets/includes. Page Kit publishes src/<slug>/assets/config.js as /<slug>/config.js and src/<slug>/assets/products/foo.png as /<slug>/products/foo.png; do not leave raw /assets/... or /<slug>/assets/... references in rendered pages.
|
|
5072
5268
|
- Preserve prepared source HTML for landing/presell pages when it is a real standalone design.
|
|
5073
5269
|
- For checkout/upsell/downsell/receipt, use the starter template as the SDK contract reference only: preserve required data-next controls and runtime wiring, but let the campaign/source own visual chrome, copy hierarchy, imagery, and brand layer.
|
|
5074
|
-
- Read context.theme and
|
|
5270
|
+
- Read context.theme and ${runtime}/theme/theme-report.json when present. If a fresh brand-theme.css exists, copy it into the campaign assets/css folder and list it after next-core.css in checkout/upsell/downsell/receipt frontmatter styles; when theme generate also wrote brand-fonts.css (the design's own faces), copy it beside brand-theme.css and list it on the same pages; if policy is inspect_only, generate or skip explicitly before applying a new brand layer.
|
|
5075
5271
|
- Generated brand-theme.css v0 is root-variable-only. Do not edit SDK-owned selectors, package controls, payment fields, totals, submit controls, receipt templates, route meta tags, or SDK JavaScript as part of theme application.
|
|
5076
5272
|
- If you copy starter-template files, copy the selected family atomically with dependent pages, _includes, _layouts, assets/css, and assets/js; do not copy only checkout.html and receipt.html.
|
|
5077
5273
|
- Resolve SDK routing meta tags to campaign-root paths such as ${campaignRouteRoot(packet) || `/${packet.campaign.public_route_slug}/`}upsell/, not source filenames or unrooted spec literals.
|
|
@@ -5143,7 +5339,7 @@ Read first:
|
|
|
5143
5339
|
|
|
5144
5340
|
Prepare the target page-kit structure and agent context, then record setup:
|
|
5145
5341
|
- ${cmd("record")} setup --packet ${packetPath}
|
|
5146
|
-
It sets
|
|
5342
|
+
It sets the Build Context's scaffold.required=false and the Assembly Report's stages.setup to completed, validated against their schemas; do not hand-edit either file.
|
|
5147
5343
|
|
|
5148
5344
|
When copying a starter template family, copy the family as an atomic page-kit slice: pages plus required _includes, _layouts, assets/css, and assets/js. Do not copy only checkout.html and receipt.html.
|
|
5149
5345
|
|
|
@@ -5466,10 +5662,13 @@ function installAgentContext(targetRepo, dryRun = false) {
|
|
|
5466
5662
|
["copilot-instructions.md", join(ROOT, "agents/copilot/copilot-instructions.md")],
|
|
5467
5663
|
];
|
|
5468
5664
|
const written = [];
|
|
5665
|
+
const fileContents = [];
|
|
5469
5666
|
if (!dryRun) mkdirSync(outDir, { recursive: true });
|
|
5470
5667
|
for (const [name, source] of files) {
|
|
5471
5668
|
const dest = join(outDir, name);
|
|
5472
|
-
|
|
5669
|
+
const content = readFileSync(source, "utf8");
|
|
5670
|
+
if (!dryRun) writeFileSync(dest, content);
|
|
5671
|
+
else fileContents.push({ destination: dest, content });
|
|
5473
5672
|
written.push(dest);
|
|
5474
5673
|
}
|
|
5475
5674
|
const gitignore = ensureRuntimeStateIgnored(targetRepo, { dryRun });
|
|
@@ -5479,6 +5678,7 @@ function installAgentContext(targetRepo, dryRun = false) {
|
|
|
5479
5678
|
target_repo: targetRepo,
|
|
5480
5679
|
directory: outDir,
|
|
5481
5680
|
files: written,
|
|
5681
|
+
...(dryRun ? { file_contents: fileContents } : {}),
|
|
5482
5682
|
gitignore,
|
|
5483
5683
|
note: "Context files are staged under .campaign-runtime/agent-context and do not overwrite root agent files.",
|
|
5484
5684
|
};
|
|
@@ -5841,11 +6041,22 @@ export function resolvePinSources(args, { cwd = process.cwd() } = {}) {
|
|
|
5841
6041
|
// An explicit packet names its project: its target repo, whatever the cwd.
|
|
5842
6042
|
const projectStart = packet ? targetRepoFor(packetPath, packet) : cwd;
|
|
5843
6043
|
const packageJsonPath = nearestPackageJson(projectStart);
|
|
6044
|
+
let packetUnread = false;
|
|
5844
6045
|
if (!packetPath) {
|
|
5845
6046
|
// The contracted home of the packet is the project root beside package.json
|
|
5846
6047
|
// (prepare-build's default --out), the same place readback discovers it.
|
|
5847
|
-
|
|
5848
|
-
|
|
6048
|
+
// A project holding several funnels has one packet each; reading any one
|
|
6049
|
+
// of them would be a guess, so none is read and the warning names them.
|
|
6050
|
+
const projectRoot = packageJsonPath ? dirname(packageJsonPath) : resolve(cwd);
|
|
6051
|
+
packetPath = join(projectRoot, DEFAULT_PACKET_FILENAME);
|
|
6052
|
+
try {
|
|
6053
|
+
packetPath = soleProjectPacket(projectRoot, { command: "tooling status" }) || packetPath;
|
|
6054
|
+
} catch (error) {
|
|
6055
|
+
if (error?.code !== "MULTIPLE_FUNNEL_PACKETS") throw error;
|
|
6056
|
+
packetUnread = true;
|
|
6057
|
+
warnings.push(`Packet pin not read: ${error.message}`);
|
|
6058
|
+
}
|
|
6059
|
+
if (!packetUnread && existsSync(packetPath)) packet = readPinJson(packetPath, packetProblem);
|
|
5849
6060
|
}
|
|
5850
6061
|
|
|
5851
6062
|
const { projectSpec, projectManifest, projectKey } = resolveProjectPin(packageJsonPath, warnings);
|
|
@@ -5862,7 +6073,7 @@ export function resolvePinSources(args, { cwd = process.cwd() } = {}) {
|
|
|
5862
6073
|
packetVersionIgnored = typeof recorded === "string" ? recorded : JSON.stringify(recorded);
|
|
5863
6074
|
warnings.push(`Packet campaigns_os_version ignored: ${JSON.stringify(recorded)} in ${packetPath} is not an exact version.`);
|
|
5864
6075
|
}
|
|
5865
|
-
} else if (existsSync(packetPath) && !packet) {
|
|
6076
|
+
} else if (!packetUnread && existsSync(packetPath) && !packet) {
|
|
5866
6077
|
warnings.push(`Packet pin unavailable: ${packetPath} ${packetProblem.reason}.`);
|
|
5867
6078
|
}
|
|
5868
6079
|
return {
|
|
@@ -6080,7 +6291,12 @@ function toolingCommand(args) {
|
|
|
6080
6291
|
? stalePlatforms.map((platform) => ["--platform", platform])
|
|
6081
6292
|
: [["--platform", args.platform || "all"]];
|
|
6082
6293
|
const commands = invocations.map((skillArgs) => `${cli.invocation_prefix} install-skills ${skillArgs.join(" ")}`);
|
|
6083
|
-
|
|
6294
|
+
// A per-platform refresh names the harnesses it is for, so a session in
|
|
6295
|
+
// another harness does not read it as its own required step (#683).
|
|
6296
|
+
const forHarnesses = !args.target && skillScope.scope === "installed_platforms" && stalePlatforms.length < SKILL_PLATFORMS.length
|
|
6297
|
+
? ` (for ${stalePlatforms.map((id) => SKILL_PLATFORMS.find((platform) => platform.id === id)?.label || id).join(" and ")}; not needed in a session that uses another harness)`
|
|
6298
|
+
: "";
|
|
6299
|
+
actions.push(`Refresh installed skills: ${commands.join(" and ")}${forHarnesses}. ${SKILLS_READ_NOW_FOLLOW_UP}`);
|
|
6084
6300
|
}
|
|
6085
6301
|
|
|
6086
6302
|
if (install.mode === "checkout" && cli.global_binary.status === "not_found") {
|
|
@@ -7320,7 +7536,7 @@ async function runRecordCommand(args, ambient = null, { silent = false, promptFo
|
|
|
7320
7536
|
const parsedSurfaces = parseRunRecordSurfaces(args.surfaces);
|
|
7321
7537
|
const packet = readJson(packetPath);
|
|
7322
7538
|
const explicitTargetRepo = resolveFromFile(packetPath, packet.assembly?.target_repo);
|
|
7323
|
-
const { baseDir, targetRepo, contextPath, reportPath } = resolveCampaignWorkspace(packetPath, {
|
|
7539
|
+
const { baseDir, runtimeRelPath, targetRepo, contextPath, reportPath } = resolveCampaignWorkspace(packetPath, {
|
|
7324
7540
|
packet,
|
|
7325
7541
|
contextPath: args.context ? resolve(args.context) : undefined,
|
|
7326
7542
|
reportPath: args.report ? resolve(args.report) : undefined,
|
|
@@ -7364,7 +7580,7 @@ async function runRecordCommand(args, ambient = null, { silent = false, promptFo
|
|
|
7364
7580
|
// session decides without it, and a target with a long history pays no I/O
|
|
7365
7581
|
// for a decision already made.
|
|
7366
7582
|
const needsDiskScan = listOnly || (!optionalString(args["run-id"]) && args["new-run"] !== true && !isNonEmptyString(ambient?.session?.run_id));
|
|
7367
|
-
const targetRecords = needsDiskScan ? readRunRecordsForTarget(baseDir) : [];
|
|
7583
|
+
const targetRecords = needsDiskScan ? readRunRecordsForTarget(baseDir, runtimeRelPath) : [];
|
|
7368
7584
|
const latestRecordEntry = needsDiskScan ? latestMatchingRunRecord(targetRecords, packet) : null;
|
|
7369
7585
|
let runId;
|
|
7370
7586
|
let runIdSource;
|
|
@@ -7505,7 +7721,7 @@ async function runRecordCommand(args, ambient = null, { silent = false, promptFo
|
|
|
7505
7721
|
// may not. A pure --no-write run reads nothing: it writes and sends nothing.
|
|
7506
7722
|
// A --dry-run does read it, because what a real run would do here is the
|
|
7507
7723
|
// very thing the dry run is being asked to report.
|
|
7508
|
-
const prior = write || dryRun ? readPriorRunRecord(runId, baseDir) : null;
|
|
7724
|
+
const prior = write || dryRun ? readPriorRunRecord(runId, baseDir, runtimeRelPath) : null;
|
|
7509
7725
|
const priorRemit = priorRemitOutcome(prior?.record);
|
|
7510
7726
|
const storedRemotely = priorRemit?.state === "ok";
|
|
7511
7727
|
// A pure local-inspection run (--no-write), an explicit --no-remit, or a
|
|
@@ -7704,7 +7920,7 @@ async function runRecordCommand(args, ambient = null, { silent = false, promptFo
|
|
|
7704
7920
|
// carries this run's remit_* outcome rather than the placeholders it was
|
|
7705
7921
|
// assembled with. Validated above, ahead of the send; nothing is written
|
|
7706
7922
|
// under --dry-run or --no-write.
|
|
7707
|
-
const recordPath = write ? writeRunRecord(record, { baseDir }) : null;
|
|
7923
|
+
const recordPath = write ? writeRunRecord(record, { baseDir, runtimeRelPath }) : null;
|
|
7708
7924
|
|
|
7709
7925
|
const summary = {
|
|
7710
7926
|
ok: true,
|
|
@@ -7736,7 +7952,7 @@ async function runRecordCommand(args, ambient = null, { silent = false, promptFo
|
|
|
7736
7952
|
// stopped the send anyway, and null with `would_remit_refused` set when the
|
|
7737
7953
|
// transport's destination gate refuses the base before any socket opens).
|
|
7738
7954
|
// Envelope only; never on the record.
|
|
7739
|
-
...(dryRun ? { dry_run: true, would_write: resolveRunRecordPath(runId, baseDir), would_remit: wouldRemitEndpoint, would_remit_refused: wouldRemitRefusal } : {}),
|
|
7955
|
+
...(dryRun ? { dry_run: true, would_write: resolveRunRecordPath(runId, baseDir, runtimeRelPath), would_remit: wouldRemitEndpoint, would_remit_refused: wouldRemitRefusal } : {}),
|
|
7740
7956
|
};
|
|
7741
7957
|
if (silent) return summary;
|
|
7742
7958
|
if (args.json) {
|
|
@@ -7789,10 +8005,10 @@ function assertRunRecordValid(record) {
|
|
|
7789
8005
|
// `writeRunRecord` could have written is trusted as a prior — the same
|
|
7790
8006
|
// validator gates both — so a file that merely says `remit_state: "ok"` is
|
|
7791
8007
|
// replaced like a corrupt one, never preserved or handed back as the record.
|
|
7792
|
-
function readPriorRunRecord(runId, baseDir) {
|
|
8008
|
+
function readPriorRunRecord(runId, baseDir, runtimeRelPath) {
|
|
7793
8009
|
let path;
|
|
7794
8010
|
try {
|
|
7795
|
-
path = resolveRunRecordPath(runId, baseDir);
|
|
8011
|
+
path = resolveRunRecordPath(runId, baseDir, runtimeRelPath);
|
|
7796
8012
|
} catch {
|
|
7797
8013
|
return null;
|
|
7798
8014
|
}
|
|
@@ -7874,6 +8090,26 @@ function artifactRefPath(kind, filePath, baseDir) {
|
|
|
7874
8090
|
return rel.startsWith(".") ? rel : `./${rel}`;
|
|
7875
8091
|
}
|
|
7876
8092
|
|
|
8093
|
+
// The packet's runtime directory for a printed prompt; a packet that records
|
|
8094
|
+
// an unusable one names the project-level directory (doctor refuses it).
|
|
8095
|
+
function runtimeRelPathOf(packet) {
|
|
8096
|
+
try {
|
|
8097
|
+
return packetRuntimeRelPath(packet);
|
|
8098
|
+
} catch {
|
|
8099
|
+
return RUNTIME_DIR_REL_PATH;
|
|
8100
|
+
}
|
|
8101
|
+
}
|
|
8102
|
+
|
|
8103
|
+
// The runtime directory of the packet at `packetPath`, or the project-level
|
|
8104
|
+
// one when it cannot be read (the printed command then carries --report).
|
|
8105
|
+
function runtimeRelPathAt(packetPath) {
|
|
8106
|
+
try {
|
|
8107
|
+
return packetPath ? packetRuntimeRelPath(readJson(packetPath)) : RUNTIME_DIR_REL_PATH;
|
|
8108
|
+
} catch {
|
|
8109
|
+
return RUNTIME_DIR_REL_PATH;
|
|
8110
|
+
}
|
|
8111
|
+
}
|
|
8112
|
+
|
|
7877
8113
|
// The verdict a Run Record should carry when none was named: every candidate
|
|
7878
8114
|
// the report records or the qa-output directories hold, minus any the QA
|
|
7879
8115
|
// verdict receiver stamped `trusted: false` (an anonymous submission is
|
|
@@ -7886,7 +8122,7 @@ function inferQaVerdictPath({ packet, report, reportPath = null, targetRepo = nu
|
|
|
7886
8122
|
const eligible = discoverQaVerdicts({
|
|
7887
8123
|
packet,
|
|
7888
8124
|
report,
|
|
7889
|
-
reportPath: reportPath || (targetRepo ? campaignSidecarPaths(targetRepo).reportPath : null),
|
|
8125
|
+
reportPath: reportPath || (targetRepo ? campaignSidecarPaths(targetRepo, packetRuntimeRelPath(packet)).reportPath : null),
|
|
7890
8126
|
roots: [targetRepo, baseDir],
|
|
7891
8127
|
}).filter((candidate) => candidate.verdict && candidate.trusted
|
|
7892
8128
|
&& ((packet?.spec?.local_spec_id == null && candidate.verdict.local_spec_id == null) || candidate.identityMatch));
|
|
@@ -7902,16 +8138,14 @@ const ARGV_SHAPE_PRIVATE_FLAGS = new Set(["auth-cookie", "no-remit", "no-write",
|
|
|
7902
8138
|
|
|
7903
8139
|
// argv SHAPE = selected flag NAMES present, never their values (minimization).
|
|
7904
8140
|
// Sorted + de-duplicated for deterministic records; opt-out and endpoint flags
|
|
7905
|
-
// stay private.
|
|
7906
|
-
//
|
|
7907
|
-
// leak into a persisted, potentially-remitted shape, breaking the guarantee.
|
|
8141
|
+
// stay private. parseArgs keeps a `--flag=value` token off the keys, so no
|
|
8142
|
+
// value (e.g. --auth-cookie=SECRET) can reach a persisted, remitted shape.
|
|
7908
8143
|
function argvShape(args) {
|
|
7909
8144
|
const names = new Set();
|
|
7910
8145
|
for (const key of Object.keys(args)) {
|
|
7911
8146
|
if (key === "_") continue;
|
|
7912
|
-
|
|
7913
|
-
|
|
7914
|
-
names.add(`--${name}`);
|
|
8147
|
+
if (ARGV_SHAPE_PRIVATE_FLAGS.has(key)) continue;
|
|
8148
|
+
names.add(`--${key}`);
|
|
7915
8149
|
}
|
|
7916
8150
|
return [...names].sort();
|
|
7917
8151
|
}
|
|
@@ -8427,7 +8661,7 @@ export function doctorRequiredActionLines(result) {
|
|
|
8427
8661
|
// cannot be ruled out.
|
|
8428
8662
|
const inspectedReportPath = typeof derived?.assembly_report_path === "string" ? derived.assembly_report_path : null;
|
|
8429
8663
|
const inferredReportPath = typeof derived?.target_repo === "string"
|
|
8430
|
-
? campaignSidecarPaths(derived.target_repo).reportPath
|
|
8664
|
+
? campaignSidecarPaths(derived.target_repo, runtimeRelPathAt(packetPath)).reportPath
|
|
8431
8665
|
: null;
|
|
8432
8666
|
const reportPath = inspectedReportPath && inspectedReportPath !== inferredReportPath ? inspectedReportPath : null;
|
|
8433
8667
|
const lines = [];
|
|
@@ -8464,6 +8698,19 @@ function printPrepareResult(result, args) {
|
|
|
8464
8698
|
}
|
|
8465
8699
|
}
|
|
8466
8700
|
console.log(`Packet: ${result.packetPath}`);
|
|
8701
|
+
if (result.funnelAddedBeside) {
|
|
8702
|
+
// A route differing from the default packet's is either another funnel
|
|
8703
|
+
// or a rename of that one; only the operator knows which, so say what was
|
|
8704
|
+
// done and how to undo it.
|
|
8705
|
+
const beside = result.funnelAddedBeside;
|
|
8706
|
+
const route = result.packet?.campaign?.public_route_slug;
|
|
8707
|
+
const runtimeDir = result.packet?.assembly?.runtime_dir;
|
|
8708
|
+
console.log(
|
|
8709
|
+
`Funnel: route ${route} was set up as another funnel beside ${beside.packetPath} (route ${beside.route || "unnamed"})`
|
|
8710
|
+
+ `${runtimeDir ? `, with its records under ${runtimeDir}/` : ""}. To rename that funnel's route instead, delete ${result.packetPath}`
|
|
8711
|
+
+ `${runtimeDir ? ` and ${runtimeDir}/` : ""}, then re-run prepare-build with --out ${beside.packetPath} --force.`,
|
|
8712
|
+
);
|
|
8713
|
+
}
|
|
8467
8714
|
if (result.briefPath) {
|
|
8468
8715
|
const brief = result.context?.build_brief;
|
|
8469
8716
|
console.log(`Brief: ${result.briefPath}${brief ? ` (${brief.mode}, ${brief.status}, questions=${brief.question_count})` : ""}`);
|
|
@@ -8611,16 +8858,30 @@ export async function specDeriveWithMapWriteback(args, { fetchImpl = undefined,
|
|
|
8611
8858
|
if (Object.hasOwn(args, "proxy-base") && !optionalString(args["proxy-base"])) {
|
|
8612
8859
|
throw refused("spec derive: --proxy-base needs a URL (https, or a loopback host); nothing was written.");
|
|
8613
8860
|
}
|
|
8614
|
-
if (
|
|
8615
|
-
throw refused(
|
|
8861
|
+
if (Object.hasOwn(args, "no-live-refs") && args["no-live-refs"] !== true) {
|
|
8862
|
+
throw refused(`--no-live-refs takes no value (got ${JSON.stringify(args["no-live-refs"])}); write \`--no-live-refs\` on its own, after the other flags.`);
|
|
8616
8863
|
}
|
|
8617
8864
|
const writeMap = args["write-map"] === true;
|
|
8618
8865
|
const localArgs = Object.fromEntries(Object.entries(args).filter(([key]) => !SPEC_DERIVE_MAP_FLAGS.includes(key)));
|
|
8866
|
+
// The live campaign read (#721), under the proxy --proxy-base names, is
|
|
8867
|
+
// made only once the local checks pass: a dry run of the repo half first,
|
|
8868
|
+
// so a missing packet, spec or entry (or an unknown flag) sends nothing.
|
|
8869
|
+
// --no-live-refs sends nothing at all.
|
|
8870
|
+
const unknownFlags = Object.keys(localArgs).filter((key) => key !== "_" && !SPEC_DERIVE_FLAGS.includes(key));
|
|
8871
|
+
// The store flags are judged first, as the local command judges them, so
|
|
8872
|
+
// the preflight below (which leaves them out) never changes which refusal
|
|
8873
|
+
// a malformed invocation gets.
|
|
8874
|
+
if (!unknownFlags.length) parseSpecDeriveStoreFlags(localArgs);
|
|
8875
|
+
const preflightArgs = Object.fromEntries(Object.entries(localArgs).filter(([key]) => key !== "from-store" && key !== "store-token-source"));
|
|
8876
|
+
const preflight = unknownFlags.length || liveRefsDisabled(args) ? null : specDeriveCommand({ ...preflightArgs, "dry-run": true });
|
|
8877
|
+
const liveCampaign = liveRefsDisabled(args)
|
|
8878
|
+
? disabledLiveRead()
|
|
8879
|
+
: preflight && !preflight.errors.length ? await readSpecDeriveLiveCampaign(args, { ...(fetchImpl ? { fetchImpl } : {}), env, ...(warn ? { warn } : {}) }) : undefined;
|
|
8619
8880
|
// A copy, so nothing here depends on the local command handing out a
|
|
8620
8881
|
// mutable object; the Map outcome is added beside its fields, not into them.
|
|
8621
8882
|
// The store source (--from-store) is the other network read; it runs
|
|
8622
8883
|
// before the Map write-back so the pin it reports is the one written.
|
|
8623
|
-
const local = Object.hasOwn(localArgs, "from-store") ? await specDeriveFromStoreCommand(localArgs) : specDeriveCommand(localArgs);
|
|
8884
|
+
const local = Object.hasOwn(localArgs, "from-store") ? await specDeriveFromStoreCommand(localArgs, { liveCampaign }) : specDeriveCommand(localArgs, { liveCampaign });
|
|
8624
8885
|
const result = { ...local, write_map: writeMap, map: null };
|
|
8625
8886
|
if (!writeMap) return result;
|
|
8626
8887
|
const skipped = (reason, detail) => {
|