@nextcommerce/campaigns-os 1.43.2 → 1.46.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 +5 -0
- package/CHANGELOG.md +648 -5103
- package/README.md +32 -11
- package/agents/claude/CLAUDE.md +1 -1
- package/agents/codex/AGENTS.md +1 -1
- package/agents/copilot/copilot-instructions.md +1 -1
- package/agents/cursor/campaigns-os.mdc +1 -1
- package/campaign-spec/dist/rules/analytics-contract-shape.d.ts +2 -2
- package/campaign-spec/dist/rules/analytics-contract-shape.js +2 -2
- package/campaign-spec/dist/rules/store-profile-shape.d.ts +5 -1
- package/campaign-spec/dist/rules/store-profile-shape.js +8 -10
- package/campaign-spec/dist/types.d.ts +2 -2
- package/contracts/archive/CHANGELOG.2026-09-30.md +5111 -0
- package/contracts/archive/release-ledger.2026-09-30.json +5068 -0
- package/contracts/effects.v1.json +1176 -113
- package/contracts/orientation-reason-codes.v1.json +7 -0
- package/contracts/release-ledger.json +2345 -6087
- package/contracts/supported-surface.json +7 -4
- package/contracts/template-slot-manifest.shared-content-core.v0.json +403 -0
- package/docs/brand-theme-bridge.md +81 -0
- package/docs/build-packet.md +158 -21
- package/docs/campaigns-os-build-flow.md +3 -3
- package/docs/design-source-package.md +73 -0
- package/docs/effects.md +50 -8
- package/docs/gateway-login.md +3 -0
- package/docs/local-setup.md +1 -1
- package/docs/orientation-contract-reference.md +42 -2
- package/docs/polish-evidence.md +74 -0
- package/docs/qa-and-test-orders.md +99 -13
- package/docs/release-ledger-authoring-guide.md +64 -4
- package/docs/runtime-readiness.md +1 -1
- package/docs/sdk-storage-compatibility.md +1 -1
- package/docs/skills-revision.md +10 -10
- package/docs/supported-surface.md +2 -2
- package/docs/versioning.md +4 -1
- package/package.json +1 -1
- package/schemas/campaigns-os-release-ledger.v1.schema.json +32 -2
- package/schemas/campaigns-os-tooling-orientation.v1.schema.json +1 -0
- package/skills/campaign-lifecycle-orientation/SKILL.md +16 -5
- package/skills/campaign-readback-classification/SKILL.md +3 -3
- package/skills/campaign-run-evidence/SKILL.md +7 -6
- package/skills/contribution-intake/SKILL.md +3 -3
- package/skills/next-campaigns-build/SKILL.md +7 -6
- package/skills/next-campaigns-os/SKILL.md +7 -7
- package/skills/next-campaigns-os/references/session-intake.md +9 -3
- package/skills/next-campaigns-os-setup/SKILL.md +5 -5
- package/skills/next-campaigns-polish/SKILL.md +28 -9
- package/skills/next-campaigns-qa/SKILL.md +7 -4
- package/skills.json +10 -10
- package/src/brand-theme.mjs +320 -20
- package/src/built-site-scope.mjs +16 -4
- package/src/cli.mjs +280 -46
- package/src/commercial-parity.mjs +48 -2
- package/src/deviation.mjs +13 -1
- package/src/diagnostic.mjs +5 -2
- package/src/doctor/checks.mjs +320 -81
- package/src/doctor/inspect.mjs +55 -13
- package/src/doctor/source-provenance.mjs +184 -0
- package/src/invocation.mjs +4 -0
- package/src/live-campaign-refs.mjs +466 -0
- package/src/login.mjs +2 -2
- package/src/page-kit-store-profile.mjs +69 -12
- package/src/page-kit-sync.mjs +31 -12
- package/src/progress-node.mjs +3 -1
- package/src/qa-browser.mjs +538 -28
- package/src/qa-commercial-parity.mjs +48 -5
- package/src/qa-node.mjs +122 -7
- package/src/qa-test-order-topology.mjs +148 -0
- package/src/sdk-markup.mjs +72 -8
- package/src/source-html-intake.mjs +116 -0
- package/src/stage-record.mjs +551 -0
- package/src/upsell-selector-scope.mjs +112 -2
package/src/cli.mjs
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
chmodSync,
|
|
8
8
|
cpSync,
|
|
9
9
|
existsSync,
|
|
10
|
+
lstatSync,
|
|
10
11
|
mkdirSync,
|
|
11
12
|
readdirSync,
|
|
12
13
|
readFileSync,
|
|
@@ -106,7 +107,10 @@ import {
|
|
|
106
107
|
import { ensureRuntimeStateIgnored } from "./runtime-state-ignore.mjs";
|
|
107
108
|
import {
|
|
108
109
|
createSourceHtmlIntake,
|
|
110
|
+
HOST_STRIPPED_CODE,
|
|
111
|
+
parseHostPrefixedRoute,
|
|
109
112
|
publicRouteForPage,
|
|
113
|
+
stripHostPrefixedRoutes,
|
|
110
114
|
} from "./source-html-intake.mjs";
|
|
111
115
|
import {
|
|
112
116
|
SOURCE_HTML_MANIFEST_REL_PATH,
|
|
@@ -122,7 +126,7 @@ import {
|
|
|
122
126
|
createStandardizationReport,
|
|
123
127
|
formatStandardizationReportMarkdown,
|
|
124
128
|
} from "./standardization-report.mjs";
|
|
125
|
-
import { singleLineDetail, singleLineField } from "./text-safety.mjs";
|
|
129
|
+
import { singleLineDetail, singleLineField, singleLineFragment } from "./text-safety.mjs";
|
|
126
130
|
import { derivePackagePin, LOCAL_INVOCATION_PREFIX, localInstallStatus, resolveInvocation } from "./install-mode.mjs";
|
|
127
131
|
import {
|
|
128
132
|
campaignRouteRoot,
|
|
@@ -195,6 +199,7 @@ import {
|
|
|
195
199
|
} from "./polish-node.mjs";
|
|
196
200
|
import { HIDDEN_EAGER_MEDIA_SCOPE, POLISH_CAPTURE_PROBLEM_CODES } from "./polish-page-load.mjs";
|
|
197
201
|
import { POLISH_BEACON_RESOURCE_TYPES, captureOrigin, redactCaptureUrl } from "./polish-capture.mjs";
|
|
202
|
+
import { SOURCE_PROVENANCE_EXPORTER_CLAIM_CODE, SOURCE_PROVENANCE_SCOPE } from "./doctor/source-provenance.mjs";
|
|
198
203
|
import {
|
|
199
204
|
appendCheckpointWaiver,
|
|
200
205
|
createCheckpointRegistry,
|
|
@@ -253,7 +258,7 @@ import {
|
|
|
253
258
|
addIssue,
|
|
254
259
|
} from "./cli-helpers.mjs";
|
|
255
260
|
import { resolveCampaignsApiKeySource, describeCampaignKeyRejection } from "./campaigns-api-key.mjs";
|
|
256
|
-
import { doctorCommand, doctorBuiltOutput, doctorPacket } from "./doctor/inspect.mjs";
|
|
261
|
+
import { doctorCommand, doctorBuiltOutput, doctorPacket, readDoctorLiveCampaign } from "./doctor/inspect.mjs";
|
|
257
262
|
import {
|
|
258
263
|
PACKET_SCHEMA,
|
|
259
264
|
CONTEXT_SCHEMA,
|
|
@@ -300,7 +305,7 @@ Usage:
|
|
|
300
305
|
[--brief <yaml|json>] [--proxy-base <url>] [--cached-spec] [--theme-policy <inspect_only|auto|off>]
|
|
301
306
|
[--wrapper-policy <strip_document_wrappers|preserve_document_wrappers|not_required|unknown>] [--design-manifest <path>]
|
|
302
307
|
[--allow-uncertified-template "<reason>"] [--order-path-depth <off|common|full>] [--no-run-session] [--force] # intake alias for prepare-build + doctor
|
|
303
|
-
campaigns-os doctor --packet <campaign-runtime.build.json> [--context <json>] [--report <json>] [--strip-paths] [--write] [--no-write] [--doctor-out <path>] [--json] # inspection by default; --doctor-out requires --write; --no-write wins
|
|
308
|
+
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
|
|
304
309
|
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
|
|
305
310
|
campaigns-os bundle check --packet <campaign-runtime.build.json> [--require-qa] [--json] # validate the canonical migration/readback JSON bundle; never substitutes markdown
|
|
306
311
|
campaigns-os sdk storage-check --target <git-root> --target-sdk <x.y.z> --manifest <SDK-manifest.json> --scope <dir,file> [--exclude <dir,file>] [--json]
|
|
@@ -308,11 +313,14 @@ Usage:
|
|
|
308
313
|
campaigns-os theme inspect --packet <campaign-runtime.build.json> [--context <json>] [--theme-policy <inspect_only|auto|off>] [--json]
|
|
309
314
|
campaigns-os theme generate --packet <campaign-runtime.build.json> [--context <json>] [--out-dir <dir>] [--force] [--json]
|
|
310
315
|
campaigns-os theme waive --packet <campaign-runtime.build.json> --reason "<why>" --waived-by "<named human>" [--expires-at <ISO>] [--report <json>] [--dry-run] [--json] # record an explicit theme-gate waiver on the assembly report; placeholders such as "operator" are refused. --dry-run validates the same way and prints the waiver it would write, without touching the report
|
|
311
|
-
campaigns-os checkpoint waive --packet <campaign-runtime.build.json> --gate <checkpoint-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. --dry-run runs every check (named human, bounds, registered and waivable gate) and prints the waiver it would write, without touching the report
|
|
316
|
+
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
|
|
312
317
|
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.
|
|
313
318
|
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. Repo-derived fields only and no network by default; --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.
|
|
314
319
|
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.
|
|
315
320
|
campaigns-os polish capture --packet <campaign-runtime.build.json> --base-url <url> [--report <json>] [--headed] [--auth-cookie <cookie>] [--json]
|
|
321
|
+
campaigns-os record setup --packet <campaign-runtime.build.json> [--context <json>] [--report <json>] [--dry-run] [--json] # record setup complete once the campaign output directory exists: Build Context scaffold.required=false and stages.setup completed, validated against their schemas before either is written
|
|
322
|
+
campaigns-os record build --packet <campaign-runtime.build.json> [--context <json>] [--report <json>] [--dry-run] [--json] # after page-kit build: stages.assembly completed with build_fingerprint = doctor's derived.build_output_fingerprint.value (and the Design Source Package material fingerprint when the report has one); stages.polish becomes required unless its evidence is bound to this exact output
|
|
323
|
+
campaigns-os record polish --packet <campaign-runtime.build.json> --evidence <polish-evidence.json> [--context <json>] [--report <json>] [--dry-run] [--json] # after polish capture: stages.polish from the file's status (completed, completed_with_warnings, blocked with blockers, or skipped with skip_reason), evidence and optional repair_loop_defect, bound to doctor's current fingerprint; a completed status is refused, writing nothing, unless the polish gate doctor evaluates would pass. --dry-run runs every check and writes nothing
|
|
316
324
|
campaigns-os readback <target-repo-root> [--json] [--packet <path>] [--doctor <path>] [--context <path>] [--report <path>] [--qa-verdict <path>] [--findings <path>] # read-only projection of one run's emitted artifacts (packet, doctor output, build context, assembly report, QA verdict, findings export): artifact states, per-artifact freshness against the checkout's HEAD reflog, doctor warning grouping, skip cascades and cross-artifact divergences. Writes nothing, starts no process, touches no network, and records no lifecycle entry; --json emits one campaigns-os-readback/v2 object (docs/readback.md). Exit 2 for a missing target root or a Build Packet set freshness cannot single out.
|
|
317
325
|
campaigns-os readback --example [--json] # project the bundled synthetic sample; freshness is not computable for it by design
|
|
318
326
|
campaigns-os validate-assembly-report --report <json> [--json]
|
|
@@ -330,7 +338,7 @@ Usage:
|
|
|
330
338
|
campaigns-os next deploy --packet <json> --report <json> [--json]
|
|
331
339
|
campaigns-os next qa --packet <json> --report <json> [--json]
|
|
332
340
|
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
|
|
333
|
-
campaigns-os qa run --packet <json> [--base-url <url>] [--browser] [--test-order <mode>] [--select-package <ref[:qty],...>] [--apply-coupon <code>] [--no-post-verdict] [--no-remit] [--output-dir <dir>] [--json]
|
|
341
|
+
campaigns-os qa run --packet <json> [--base-url <url>] [--browser] [--test-order <mode>] [--select-package <ref[:qty],...>] [--apply-coupon <code>] [--no-post-verdict] [--no-remit] [--no-live-refs] [--output-dir <dir>] [--json]
|
|
334
342
|
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
|
|
335
343
|
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
|
|
336
344
|
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
|
|
@@ -641,6 +649,7 @@ function persistDeviationIfDetected(args, command, lifecycle, ambient) {
|
|
|
641
649
|
const entry = detectDeviation({
|
|
642
650
|
lastRecommendation: ambient.session.last_recommendation,
|
|
643
651
|
command,
|
|
652
|
+
subcommand: optionalString(args._?.[1]) || null,
|
|
644
653
|
argvShape: lifecycle?.argv_shape || [],
|
|
645
654
|
runId: ambient.session.run_id || null,
|
|
646
655
|
deviationReason: optionalString(args["deviation-reason"]) || null,
|
|
@@ -648,8 +657,11 @@ function persistDeviationIfDetected(args, command, lifecycle, ambient) {
|
|
|
648
657
|
if (!entry) return;
|
|
649
658
|
const journalPath = join(ambient.dir, DEVIATION_JOURNAL_REL_PATH);
|
|
650
659
|
appendDeviation(journalPath, entry);
|
|
651
|
-
|
|
652
|
-
|
|
660
|
+
// A declared detour gets one confirming line; only an undeclared one is
|
|
661
|
+
// told how to declare intent.
|
|
662
|
+
process.stderr.write(entry.deviation_reason
|
|
663
|
+
? `[campaigns-os] deviation recorded with reason: \`${command}\` ran while next recommended stage "${entry.recommended_stage}"; reason: "${singleLineField(entry.deviation_reason)}".\n`
|
|
664
|
+
: `[campaigns-os] deviation recorded: \`${command}\` ran while next recommended stage "${entry.recommended_stage}" (expected: ${entry.recommended_commands.join(", ") || "none"}). Declare intent with --deviation-reason, or follow \`campaigns-os next\`.\n`,
|
|
653
665
|
);
|
|
654
666
|
} catch {
|
|
655
667
|
// telemetry never blocks a command
|
|
@@ -980,7 +992,10 @@ async function dispatch(command, args, { recorder = NOOP_RECORDER, ambient = nul
|
|
|
980
992
|
}
|
|
981
993
|
|
|
982
994
|
if (command === "doctor") {
|
|
983
|
-
|
|
995
|
+
// The live campaign ref check's one read (#533), made before the
|
|
996
|
+
// synchronous inspection; see readDoctorLiveCampaign.
|
|
997
|
+
const liveCampaign = await readDoctorLiveCampaign(args);
|
|
998
|
+
const result = doctorCommand(args, { liveCampaign });
|
|
984
999
|
writeResult(result, args, result.ok ? 0 : 2);
|
|
985
1000
|
printDoctorTinyPrompt(result, args);
|
|
986
1001
|
return;
|
|
@@ -1050,6 +1065,12 @@ async function dispatch(command, args, { recorder = NOOP_RECORDER, ambient = nul
|
|
|
1050
1065
|
return;
|
|
1051
1066
|
}
|
|
1052
1067
|
|
|
1068
|
+
if (command === "record") {
|
|
1069
|
+
const { recordStageCommand } = await import("./stage-record.mjs");
|
|
1070
|
+
writeResult(recordStageCommand(args), args, 0);
|
|
1071
|
+
return;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1053
1074
|
if (command === "readback") {
|
|
1054
1075
|
// Read-only projection over a target's already-emitted artifacts. It owns
|
|
1055
1076
|
// its own exit codes (0 for any projection it could form, 2 for a request
|
|
@@ -1267,10 +1288,14 @@ async function resolveSpecPath(args, opts = {}) {
|
|
|
1267
1288
|
}
|
|
1268
1289
|
return { specPath: cachePath, source: "cache", mapId, proxyBase };
|
|
1269
1290
|
}
|
|
1291
|
+
// Refuse a symlinked cache before fetching, and again at the write, which
|
|
1292
|
+
// may run later under the prepare-build lock.
|
|
1293
|
+
assertFetchedSpecCacheWritable(targetRepo, cachePath);
|
|
1270
1294
|
const spec = await fetchSpecByMapId(mapId, { proxyBase, fetchImpl: opts.fetchImpl });
|
|
1271
1295
|
const publishSpec = () => {
|
|
1296
|
+
assertFetchedSpecCacheWritable(targetRepo, cachePath);
|
|
1272
1297
|
mkdirSync(cacheDir, { recursive: true });
|
|
1273
|
-
|
|
1298
|
+
replaceFetchedSpec(cachePath, `${JSON.stringify(spec, null, 2)}\n`);
|
|
1274
1299
|
};
|
|
1275
1300
|
// deferCacheWrite hands the cache write back to the caller, which makes it
|
|
1276
1301
|
// under the prepare-build lock.
|
|
@@ -1288,6 +1313,45 @@ async function resolveSpecPath(args, opts = {}) {
|
|
|
1288
1313
|
);
|
|
1289
1314
|
}
|
|
1290
1315
|
|
|
1316
|
+
// The fetch cache is written only inside a real
|
|
1317
|
+
// <target>/.campaign-runtime/fetched-specs/ directory. Throws when
|
|
1318
|
+
// .campaign-runtime/, fetched-specs/ or the cache entry exists as a symlink or
|
|
1319
|
+
// as the wrong kind of file, so a cache write cannot follow a link out of the
|
|
1320
|
+
// target. Missing entries are fine; the caller creates them.
|
|
1321
|
+
function assertFetchedSpecCacheWritable(targetRepo, cachePath) {
|
|
1322
|
+
const checks = [
|
|
1323
|
+
[join(targetRepo, ".campaign-runtime"), "directory"],
|
|
1324
|
+
[dirname(cachePath), "directory"],
|
|
1325
|
+
[cachePath, "regular file"],
|
|
1326
|
+
];
|
|
1327
|
+
for (const [path, kind] of checks) {
|
|
1328
|
+
let stat;
|
|
1329
|
+
try {
|
|
1330
|
+
stat = lstatSync(path);
|
|
1331
|
+
} catch (error) {
|
|
1332
|
+
if (error.code === "ENOENT") continue;
|
|
1333
|
+
throw error;
|
|
1334
|
+
}
|
|
1335
|
+
if (kind === "directory" ? stat.isDirectory() : stat.isFile()) continue;
|
|
1336
|
+
throw new Error(
|
|
1337
|
+
`Refusing to write the fetched CampaignSpec: ${path} is ${stat.isSymbolicLink() ? "a symlink" : `not a ${kind}`}. `
|
|
1338
|
+
+ "The fetch cache is written only inside a real <target>/.campaign-runtime/fetched-specs/ directory; remove the link and run again.",
|
|
1339
|
+
);
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
// Writes `bytes` to a temp file beside the cache entry and renames it over the
|
|
1344
|
+
// entry, so another name for the old file (a hard link) is never written.
|
|
1345
|
+
function replaceFetchedSpec(cachePath, bytes) {
|
|
1346
|
+
const tmpPath = join(dirname(cachePath), `.${basename(cachePath)}.${randomUUID()}.tmp`);
|
|
1347
|
+
try {
|
|
1348
|
+
writeFileSync(tmpPath, bytes, { flag: "wx" });
|
|
1349
|
+
renameSync(tmpPath, cachePath);
|
|
1350
|
+
} finally {
|
|
1351
|
+
rmSync(tmpPath, { force: true });
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1291
1355
|
function relFromFile(filePath, targetPath) {
|
|
1292
1356
|
const fromDir = dirname(resolve(filePath));
|
|
1293
1357
|
const rel = relative(fromDir, resolve(targetPath));
|
|
@@ -1517,7 +1581,22 @@ function prepareBuildUnderLock({
|
|
|
1517
1581
|
publication,
|
|
1518
1582
|
}) {
|
|
1519
1583
|
const { packetPath, contextPath, reportPath, doctorOutPath, briefPath, designSourcePackagePath } = publication.paths;
|
|
1520
|
-
|
|
1584
|
+
// #531: a host-prefixed route ("shop.example.com/route/upsell/") in a Map
|
|
1585
|
+
// fetched by this run is reduced to its rooted path before anything reads
|
|
1586
|
+
// the spec. The fresh fetch is intake's own file under
|
|
1587
|
+
// .campaign-runtime/fetched-specs/, so the rooted spec replaces it once the
|
|
1588
|
+
// Assembly Report recording each change (with the value as fetched) is
|
|
1589
|
+
// published, and doctor, polish and QA read what intake read. A local --spec
|
|
1590
|
+
// file and a copy reused with --cached-spec are never rewritten: they are
|
|
1591
|
+
// read as they are, and doctor blocks on routing_meta.host_prefixed.
|
|
1592
|
+
const specOnDisk = readJson(specPath);
|
|
1593
|
+
const hostStripped = stripHostPrefixedRoutes(specOnDisk);
|
|
1594
|
+
const specSource = options.specInput?.source || "local";
|
|
1595
|
+
const strippedSpecBytes = specSource === "remote" && hostStripped.evidence.length
|
|
1596
|
+
? `${JSON.stringify(hostStripped.spec, null, 2)}\n`
|
|
1597
|
+
: null;
|
|
1598
|
+
const spec = strippedSpecBytes == null ? specOnDisk : hostStripped.spec;
|
|
1599
|
+
const specFileHash = strippedSpecBytes == null ? sha256File(specPath) : createHash("sha256").update(strippedSpecBytes).digest("hex");
|
|
1521
1600
|
const { mapId, publicRouteSlug, localSpecId } = campaignIdentity(spec, args);
|
|
1522
1601
|
if (!resolveCampaignIdentity({ map_id: mapId, local_spec_id: localSpecId })) {
|
|
1523
1602
|
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.");
|
|
@@ -1809,7 +1888,7 @@ function prepareBuildUnderLock({
|
|
|
1809
1888
|
},
|
|
1810
1889
|
qa: {
|
|
1811
1890
|
proof_policy: proofPolicy,
|
|
1812
|
-
test_order_policy_notes: "Test Orders use global test cards that bypass the gateway and create no transactions. Run them any time with `qa run --test-order common`
|
|
1891
|
+
test_order_policy_notes: "Test Orders use global test cards that bypass the gateway and create no transactions. Run them any time with `qa run --test-order common`: when every actual terminal path in the selected checkout topology fits under the flood cap, common runs them all (effective depth `full`, reason `under_cap`); above the cap it runs checkout, first-offer accept/decline and a deduplicated shortest real receipt path, then adds the shortest path that clicks the decline on each offer or downsell page no planned path declines yet, up to the cap, and names any page left out. The `browser-test-order:upsell-action-coverage` verdict row warns naming each offer page whose decline no executed order clicked. Use `--test-order full` for every actual terminal path in the selected checkout topology; cycles, missing routes, and reachable nonterminals block exhaustive proof before browser launch. Use `--test-order tiers` (or `tiers:common` / `tiers:full`) to drive one strict-selection order per selector tier the CampaignSpec declares on the checkout page, crossed with those path shapes; order-bump rows marked `is_upsell` are add-ons, not tiers, so a three-tier checkout with one bump plans 3 tiers, and `--select-package <ref[:qty],...>` narrows a tiers run to the listed tiers. The default accidental-flood cap is 6, and an overflow names the exact explicit `--max-test-orders` raise and lists the planned paths (up to 40 ids, the remainder counted). That cap bounds planned paths; `--max-order-creations` bounds actual order creations, defaults to the planned path count, and is reserved before each submit. Localhost on any port is a globally allowed Development domain; non-localhost preview/production origins still need SDK origin allowlist confirmation. There is no permission flag: depth is the only control.",
|
|
1813
1892
|
},
|
|
1814
1893
|
notes: "Generated by campaigns-os prepare-build. Replace demo refs from CampaignSpec/API before launch.",
|
|
1815
1894
|
};
|
|
@@ -1826,7 +1905,11 @@ function prepareBuildUnderLock({
|
|
|
1826
1905
|
: null,
|
|
1827
1906
|
map_id: specInput?.mapId || null,
|
|
1828
1907
|
proxy_base: specInput?.proxyBase || null,
|
|
1829
|
-
|
|
1908
|
+
// The Map revision stays the one fetched; the local material hash is the
|
|
1909
|
+
// spec as written after host stripping, so the two still read as aligned.
|
|
1910
|
+
saved_map_revision: specInput?.savedMapRevision
|
|
1911
|
+
? (strippedSpecBytes == null ? specInput.savedMapRevision : { ...specInput.savedMapRevision, local_spec_material_hash: specMaterialHash(spec) })
|
|
1912
|
+
: null,
|
|
1830
1913
|
source_root: portable(sourceRoot),
|
|
1831
1914
|
target_repo: portable(targetRepo),
|
|
1832
1915
|
template_family: explicitTemplateFamily || null,
|
|
@@ -1847,7 +1930,7 @@ function prepareBuildUnderLock({
|
|
|
1847
1930
|
design_source_package: designSourcePackage.referenceFor(contextPath),
|
|
1848
1931
|
spec: {
|
|
1849
1932
|
path: portable(specPath),
|
|
1850
|
-
hash:
|
|
1933
|
+
hash: specFileHash,
|
|
1851
1934
|
material_hash: specMaterialHash(spec),
|
|
1852
1935
|
active_pages: activePages.map((page) => ({
|
|
1853
1936
|
id: page.id,
|
|
@@ -1955,9 +2038,45 @@ function prepareBuildUnderLock({
|
|
|
1955
2038
|
declaredScopeSkips,
|
|
1956
2039
|
buildScopeReasonsInvalid,
|
|
1957
2040
|
templateSelection,
|
|
2041
|
+
evidence: strippedSpecBytes == null ? [] : hostStripped.evidence,
|
|
1958
2042
|
}));
|
|
1959
2043
|
|
|
2044
|
+
// Values a spec left as it is still holds that doctor blocks on. An
|
|
2045
|
+
// absolute http(s) page_url is not among them: projection takes its path.
|
|
2046
|
+
const unstrippedHostRoutes = strippedSpecBytes == null
|
|
2047
|
+
? hostStripped.evidence.filter((entry) => parseHostPrefixedRoute(entry.from.trim(), { keepAbsolute: true }))
|
|
2048
|
+
: [];
|
|
2049
|
+
// A cache that cannot be rewritten stops the run before the report that
|
|
2050
|
+
// records the stripped hosts is published.
|
|
2051
|
+
if (strippedSpecBytes != null) assertFetchedSpecCacheWritable(targetRepo, specPath);
|
|
1960
2052
|
publication.publish({ packet, brief: buildBrief.artifact, context, report });
|
|
2053
|
+
if (unstrippedHostRoutes.length) {
|
|
2054
|
+
const changes = unstrippedHostRoutes.map((entry) => `${JSON.stringify(entry.from)} -> ${JSON.stringify(entry.to)}`).join(", ");
|
|
2055
|
+
console.warn(specSource === "cache"
|
|
2056
|
+
? `[campaigns-os prepare-build] the cached spec copy ${specPath} holds ${unstrippedHostRoutes.length} host-prefixed route value(s): ${changes}; `
|
|
2057
|
+
+ "--cached-spec reuses the copy as it is, so it was not changed, and doctor blocks with routing_meta.host_prefixed; re-run without --cached-spec so the Map is fetched and normalised."
|
|
2058
|
+
: `[campaigns-os prepare-build] the spec file ${specPath} holds ${unstrippedHostRoutes.length} host-prefixed route value(s) and must be edited to the rooted form: ${changes}; `
|
|
2059
|
+
+ "it was not changed, and doctor blocks with routing_meta.host_prefixed until it is edited.");
|
|
2060
|
+
}
|
|
2061
|
+
// Only now that the report holding the evidence is out: a failed publish
|
|
2062
|
+
// leaves the copy exactly as fetched, so the rooted copy never exists
|
|
2063
|
+
// without the record of what it replaced.
|
|
2064
|
+
if (strippedSpecBytes != null) {
|
|
2065
|
+
try {
|
|
2066
|
+
replaceFetchedSpec(specPath, strippedSpecBytes);
|
|
2067
|
+
} catch (error) {
|
|
2068
|
+
console.warn(
|
|
2069
|
+
`[campaigns-os prepare-build] the assembly report ${reportPath} records the stripped host(s) as ${HOST_STRIPPED_CODE}, `
|
|
2070
|
+
+ `but the cached spec ${specPath} was not rewritten and still holds the values as fetched: ${error.message}`,
|
|
2071
|
+
);
|
|
2072
|
+
throw error;
|
|
2073
|
+
}
|
|
2074
|
+
const changes = hostStripped.evidence.map((entry) => `${JSON.stringify(entry.from)} -> ${JSON.stringify(entry.to)}`).join(", ");
|
|
2075
|
+
console.warn(
|
|
2076
|
+
`[campaigns-os prepare-build] removed the host from ${hostStripped.evidence.length} CampaignSpec route value(s) and rewrote the fetched copy ${specPath}: ${changes}; `
|
|
2077
|
+
+ `recorded as ${HOST_STRIPPED_CODE} on the assembly report evidence[].`,
|
|
2078
|
+
);
|
|
2079
|
+
}
|
|
1961
2080
|
|
|
1962
2081
|
let doctor = null;
|
|
1963
2082
|
// Housekeeping for the target's git history: the machine-local half of
|
|
@@ -2087,6 +2206,7 @@ function createAssemblyReport({
|
|
|
2087
2206
|
declaredScopeSkips = [],
|
|
2088
2207
|
buildScopeReasonsInvalid = false,
|
|
2089
2208
|
templateSelection = null,
|
|
2209
|
+
evidence = [],
|
|
2090
2210
|
}) {
|
|
2091
2211
|
const scaffoldRequired = context.scaffold.required;
|
|
2092
2212
|
const portable = (path) => relFromDir(targetRepo, path);
|
|
@@ -2104,7 +2224,7 @@ function createAssemblyReport({
|
|
|
2104
2224
|
public_route_slug: packet.campaign.public_route_slug,
|
|
2105
2225
|
campaign_directory: packet.campaign.campaign_directory,
|
|
2106
2226
|
live_url_path: packet.campaign.live_url_path,
|
|
2107
|
-
spec_hash:
|
|
2227
|
+
spec_hash: context.spec.hash,
|
|
2108
2228
|
spec_material_hash: context.spec.material_hash,
|
|
2109
2229
|
},
|
|
2110
2230
|
inputs: {
|
|
@@ -2145,7 +2265,7 @@ function createAssemblyReport({
|
|
|
2145
2265
|
adapter_decisions: cloneJson(context.adapter_decisions || createAdapterDecisions()),
|
|
2146
2266
|
proof_policy: cloneJson(packet.qa?.proof_policy || createProofPolicy()),
|
|
2147
2267
|
theme: assemblyThemeFromContext(context.theme),
|
|
2148
|
-
evidence:
|
|
2268
|
+
evidence: cloneJson(evidence),
|
|
2149
2269
|
blockers,
|
|
2150
2270
|
warnings: [
|
|
2151
2271
|
...(templateSelection?.overridden
|
|
@@ -2642,8 +2762,26 @@ const CHECKPOINT_EVALUATORS = createCheckpointRegistry([
|
|
|
2642
2762
|
id: HIDDEN_EAGER_MEDIA_SCOPE,
|
|
2643
2763
|
evaluate: ({ packet, report }) => evaluateRecordedHiddenEagerMediaCheckpoint({ packet, report }),
|
|
2644
2764
|
},
|
|
2765
|
+
{
|
|
2766
|
+
// Per page (#534): one gate per Figma-typed CampaignSpec page, selected
|
|
2767
|
+
// with --page. An id that names no such page is refused here rather than
|
|
2768
|
+
// reported as missing evidence, so the operator learns which ids exist.
|
|
2769
|
+
id: SOURCE_PROVENANCE_SCOPE,
|
|
2770
|
+
evaluate: ({ doctor, pageId }) => {
|
|
2771
|
+
const gates = Array.isArray(doctor?.derived?.checkpoint_gates)
|
|
2772
|
+
? doctor.derived.checkpoint_gates.filter((gate) => gate?.id === SOURCE_PROVENANCE_SCOPE)
|
|
2773
|
+
: [];
|
|
2774
|
+
const gate = gates.find((candidate) => candidate?.subject?.page_id === pageId);
|
|
2775
|
+
if (gate) return gate;
|
|
2776
|
+
const known = gates.map((candidate) => candidate.subject.page_id);
|
|
2777
|
+
throw new Error(`Page "${pageId}" has no ${SOURCE_PROVENANCE_SCOPE} checkpoint: it is not an active CampaignSpec page with a Figma design_source, or doctor found no valid source-html manifest to check. Pages with this checkpoint now: ${known.length ? known.join(", ") : "(none)"}.`);
|
|
2778
|
+
},
|
|
2779
|
+
},
|
|
2645
2780
|
]);
|
|
2646
2781
|
|
|
2782
|
+
// Registered gates that are waived one CampaignSpec page at a time.
|
|
2783
|
+
const PER_PAGE_CHECKPOINT_GATES = new Set([SOURCE_PROVENANCE_SCOPE]);
|
|
2784
|
+
|
|
2647
2785
|
// A waive refusal under --json is a JSON envelope on stdout, exit 1 — the same
|
|
2648
2786
|
// channel the success shape uses — so a caller parsing the output learns why
|
|
2649
2787
|
// (and, for a checkpoint, which gates exist) without reading free text on
|
|
@@ -2668,23 +2806,61 @@ function waiveOrRefuse(args, run, { gate = null, registeredGates = [] } = {}) {
|
|
|
2668
2806
|
function checkpointCommand(args) {
|
|
2669
2807
|
const subcommand = args._[1] || "help";
|
|
2670
2808
|
if (subcommand !== "waive") {
|
|
2671
|
-
throw refused(`Unknown checkpoint subcommand. Use: ${cmd("checkpoint")} waive --packet <campaign-runtime.build.json> --gate <checkpoint-id> --reason "<why>" --waived-by "<named human>" [--expires-at <ISO>] [--review-condition "<trigger>"]. Registered gates: ${Object.keys(CHECKPOINT_EVALUATORS).join(", ")}.`);
|
|
2809
|
+
throw refused(`Unknown checkpoint subcommand. Use: ${cmd("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>"]. Registered gates: ${Object.keys(CHECKPOINT_EVALUATORS).join(", ")}.`);
|
|
2672
2810
|
}
|
|
2673
2811
|
return checkpointWaive(args);
|
|
2674
2812
|
}
|
|
2675
2813
|
|
|
2814
|
+
// Every value-taking flag of `checkpoint waive`. Given bare (the parser reads
|
|
2815
|
+
// it as boolean true) or with an empty value, each is refused by name rather
|
|
2816
|
+
// than coerced to the string "true" or "" — a bare --review-condition must not
|
|
2817
|
+
// supply a bound, nor a bare --report a report path.
|
|
2818
|
+
const CHECKPOINT_WAIVE_VALUE_FLAGS = Object.freeze({
|
|
2819
|
+
packet: "<campaign-runtime.build.json>",
|
|
2820
|
+
gate: "<checkpoint-id>",
|
|
2821
|
+
page: "<page_id>",
|
|
2822
|
+
reason: "\"<why>\"",
|
|
2823
|
+
"waived-by": "\"<named human>\"",
|
|
2824
|
+
"expires-at": "<ISO>",
|
|
2825
|
+
"review-condition": "\"<trigger>\"",
|
|
2826
|
+
report: "<json>",
|
|
2827
|
+
});
|
|
2828
|
+
|
|
2829
|
+
function refuseBareCheckpointWaiveFlags(args) {
|
|
2830
|
+
for (const [key, placeholder] of Object.entries(CHECKPOINT_WAIVE_VALUE_FLAGS)) {
|
|
2831
|
+
if (!Object.hasOwn(args, key) || args[key] == null) continue;
|
|
2832
|
+
if (!isNonEmptyString(args[key])) {
|
|
2833
|
+
throw refused(`--${key} needs a value: pass --${key} ${placeholder}.`);
|
|
2834
|
+
}
|
|
2835
|
+
}
|
|
2836
|
+
}
|
|
2837
|
+
|
|
2676
2838
|
export function checkpointWaive(args) {
|
|
2839
|
+
refuseBareCheckpointWaiveFlags(args);
|
|
2677
2840
|
const packetPath = resolve(requireArg(args, "packet"));
|
|
2678
2841
|
const dryRun = isDryRun(args);
|
|
2679
2842
|
const gateId = requireArg(args, "gate").trim();
|
|
2843
|
+
const pageId = args.page == null ? null : args.page.trim();
|
|
2844
|
+
// One spelling for the page scope: --page. The <gate>:<page_id> form is
|
|
2845
|
+
// refused by name rather than falling through to "unknown gate".
|
|
2846
|
+
const colon = gateId.indexOf(":");
|
|
2847
|
+
if (colon > 0 && PER_PAGE_CHECKPOINT_GATES.has(gateId.slice(0, colon))) {
|
|
2848
|
+
throw new Error(`Checkpoint gate "${gateId}" uses the <gate>:<page_id> form, which is not accepted; pass --gate ${gateId.slice(0, colon)} --page ${gateId.slice(colon + 1) || "<page_id>"} instead.`);
|
|
2849
|
+
}
|
|
2850
|
+
if (PER_PAGE_CHECKPOINT_GATES.has(gateId) && !pageId) {
|
|
2851
|
+
throw new Error(`Checkpoint gate "${gateId}" is waived per page; pass --page <page_id> naming the CampaignSpec page.`);
|
|
2852
|
+
}
|
|
2853
|
+
if (pageId != null && Object.hasOwn(CHECKPOINT_EVALUATORS, gateId) && !PER_PAGE_CHECKPOINT_GATES.has(gateId)) {
|
|
2854
|
+
throw new Error(`--page applies only to per-page checkpoint gates (${[...PER_PAGE_CHECKPOINT_GATES].join(", ")}); "${gateId}" is waived for the whole campaign.`);
|
|
2855
|
+
}
|
|
2680
2856
|
const reason = requireArg(args, "reason");
|
|
2681
2857
|
const waivedBy = requireArg(args, "waived-by");
|
|
2682
|
-
const expiresAt = args["expires-at"]
|
|
2683
|
-
const reviewCondition = args["review-condition"]
|
|
2858
|
+
const expiresAt = args["expires-at"] ?? null;
|
|
2859
|
+
const reviewCondition = args["review-condition"] ?? null;
|
|
2684
2860
|
const packet = readJson(packetPath);
|
|
2685
2861
|
const workspace = resolveCampaignWorkspace(packetPath, {
|
|
2686
2862
|
packet,
|
|
2687
|
-
reportPath: args.report ? resolve(
|
|
2863
|
+
reportPath: args.report == null ? undefined : resolve(args.report),
|
|
2688
2864
|
followContextPointer: false,
|
|
2689
2865
|
});
|
|
2690
2866
|
const { reportPath } = workspace;
|
|
@@ -2693,12 +2869,15 @@ export function checkpointWaive(args) {
|
|
|
2693
2869
|
const doctor = doctorPacket(packetPath, { reportPath });
|
|
2694
2870
|
let waiver = null;
|
|
2695
2871
|
const recordWaiver = (report) => {
|
|
2696
|
-
const gate = evaluateCheckpointRegistry(CHECKPOINT_EVALUATORS, gateId, { doctor, packet, report });
|
|
2872
|
+
const gate = evaluateCheckpointRegistry(CHECKPOINT_EVALUATORS, gateId, { doctor, packet, report, pageId });
|
|
2697
2873
|
if (!gate) throw new Error(`Checkpoint gate "${gateId}" has no current evidence; repair the packet/spec/target and re-run doctor.`);
|
|
2698
2874
|
if (gate.status !== "blocked") {
|
|
2699
2875
|
throw new Error(`Checkpoint gate "${gateId}" is not blocked (status=${gate.status}); no waiver was recorded.`);
|
|
2700
2876
|
}
|
|
2701
2877
|
if (gate.waivable !== true) {
|
|
2878
|
+
if (gate.code === SOURCE_PROVENANCE_EXPORTER_CLAIM_CODE) {
|
|
2879
|
+
throw new Error(`Checkpoint gate "${gateId}" cannot be waived for page "${pageId}": the source-html manifest's generator claims figma-sections-export, so its missing provenance is the export's own defect. Re-run figma-sections-export, or, when the approved source is hand-written HTML, set the manifest's generator to name the real producer and waive again.`);
|
|
2880
|
+
}
|
|
2702
2881
|
const residueFields = gateId === PAGE_KIT_STORE_PROFILE_SCOPE ? storeProfileDemoResidueFields(gate) : [];
|
|
2703
2882
|
if (residueFields.length) {
|
|
2704
2883
|
throw new Error(`Checkpoint gate "${gateId}" cannot be waived: ${residueFields.join(", ")} still carr${residueFields.length === 1 ? "ies" : "y"} starter demo residue (a demo storefront URL or phone). Replace the demo value(s) in ${gate.subject?.target_path || "_data/campaigns.json"}[${gate.subject?.public_route_slug || "<public-route-slug>"}]; only spec mismatches and missing values are waivable.`);
|
|
@@ -2710,7 +2889,7 @@ export function checkpointWaive(args) {
|
|
|
2710
2889
|
const updated = appendCheckpointWaiver(report, waiver);
|
|
2711
2890
|
updated.evidence = [
|
|
2712
2891
|
...(Array.isArray(report.evidence) ? report.evidence : []),
|
|
2713
|
-
`Checkpoint waiver: ${gateId} waived by ${waiver.waived_by} at ${waiver.waived_at}: ${waiver.reason}`,
|
|
2892
|
+
`Checkpoint waiver: ${gateId}${pageId ? ` (page ${pageId})` : ""} waived by ${waiver.waived_by} at ${waiver.waived_at}: ${waiver.reason}`,
|
|
2714
2893
|
];
|
|
2715
2894
|
return updated;
|
|
2716
2895
|
};
|
|
@@ -2731,6 +2910,7 @@ export function checkpointWaive(args) {
|
|
|
2731
2910
|
dry_run: true,
|
|
2732
2911
|
action: "checkpoint-waive",
|
|
2733
2912
|
gate: gateId,
|
|
2913
|
+
...(pageId ? { page: pageId } : {}),
|
|
2734
2914
|
waiver,
|
|
2735
2915
|
report_path: reportPath,
|
|
2736
2916
|
would_write: reportPath,
|
|
@@ -2742,6 +2922,7 @@ export function checkpointWaive(args) {
|
|
|
2742
2922
|
...waiveReadiness(packetPath, reportPath),
|
|
2743
2923
|
action: "checkpoint-waive",
|
|
2744
2924
|
gate: gateId,
|
|
2925
|
+
...(pageId ? { page: pageId } : {}),
|
|
2745
2926
|
waiver,
|
|
2746
2927
|
report_path: reportPath,
|
|
2747
2928
|
note: "The exact checkpoint state is accepted under a bounded named-human exception and will report ready_with_waivers, never clean. Any state change makes this waiver stale and inert.",
|
|
@@ -2788,6 +2969,7 @@ export function pageKitSyncCommand(args) {
|
|
|
2788
2969
|
changes: [],
|
|
2789
2970
|
unchanged: [],
|
|
2790
2971
|
not_in_spec: [],
|
|
2972
|
+
spec_empty_not_applied: [],
|
|
2791
2973
|
not_synced: [],
|
|
2792
2974
|
errors: [],
|
|
2793
2975
|
warnings: [],
|
|
@@ -2930,6 +3112,7 @@ export function pageKitSyncCommand(args) {
|
|
|
2930
3112
|
result.changes = plan.changes;
|
|
2931
3113
|
result.unchanged = plan.unchanged;
|
|
2932
3114
|
result.not_in_spec = plan.not_in_spec;
|
|
3115
|
+
result.spec_empty_not_applied = plan.spec_empty_not_applied;
|
|
2933
3116
|
result.not_synced = plan.not_synced;
|
|
2934
3117
|
for (const row of plan.not_synced) {
|
|
2935
3118
|
addIssue(result.warnings, `page_kit.sync.${row.field}_not_synced`, `${row.field} was not written: ${row.detail}`, { reason: row.reason });
|
|
@@ -3738,7 +3921,12 @@ export function pageKitSyncTextLines(result) {
|
|
|
3738
3921
|
lines.push("Changes: none (every governed field the spec carries already matches)");
|
|
3739
3922
|
}
|
|
3740
3923
|
if (result.unchanged?.length) lines.push(`Unchanged: ${result.unchanged.map((row) => row.field).join(", ")}`);
|
|
3741
|
-
|
|
3924
|
+
const specEmptyNotApplied = result.spec_empty_not_applied || [];
|
|
3925
|
+
const notCarried = (result.not_in_spec || []).filter((field) => !specEmptyNotApplied.includes(field));
|
|
3926
|
+
if (notCarried.length) lines.push(`Not in spec (left as they are): ${notCarried.join(", ")}`);
|
|
3927
|
+
if (specEmptyNotApplied.length) {
|
|
3928
|
+
lines.push(`Spec "" not applied (the target holds a real, non-demo value; sync blanks only a starter demo value, so remove it by hand if the merchant has none): ${specEmptyNotApplied.join(", ")}`);
|
|
3929
|
+
}
|
|
3742
3930
|
if (result.warnings?.length) {
|
|
3743
3931
|
lines.push("Warnings:");
|
|
3744
3932
|
for (const issue of result.warnings) lines.push(`- ${formatIssueSummary(issue)}`);
|
|
@@ -3902,7 +4090,7 @@ export function nextStage(stage, args, ambient = null) {
|
|
|
3902
4090
|
// and the recommendation is recorded on the active run session for
|
|
3903
4091
|
// deviation telemetry.
|
|
3904
4092
|
const prepareBuildRecoveryPrompt = divergences.length
|
|
3905
|
-
? `The assembly report's ledger and the repository's artifacts disagree (
|
|
4093
|
+
? `The assembly report's ledger and the repository's artifacts disagree: ${quoteDivergences(divergences)} Inspect both sides and decide which is right before acting. Do not rerun \`${cmd("prepare-build")}\` or \`${cmd("start")}\` on the strength of the ledger alone.`
|
|
3906
4094
|
: prepareBuildGate?.binding_failure
|
|
3907
4095
|
? prepareBuildGate.reason
|
|
3908
4096
|
: prepareBuildGate?.stage
|
|
@@ -4119,6 +4307,9 @@ function withPacketSubstitutedIssue(issue, packetPath) {
|
|
|
4119
4307
|
}
|
|
4120
4308
|
|
|
4121
4309
|
function buildNextGates({ doctor, report, themeGate, polishGate, prepareBuildGate = prepareBuildGateIssue(report), packetPath = null }) {
|
|
4310
|
+
const checkpointGates = Array.isArray(doctor?.derived?.checkpoint_gates) ? doctor.derived.checkpoint_gates : [];
|
|
4311
|
+
const campaignCheckpointGates = checkpointGates.filter((gate) => !PER_PAGE_CHECKPOINT_GATES.has(gate?.id));
|
|
4312
|
+
const perPageGates = checkpointGates.filter((gate) => PER_PAGE_CHECKPOINT_GATES.has(gate?.id));
|
|
4122
4313
|
return [
|
|
4123
4314
|
{
|
|
4124
4315
|
id: "doctor",
|
|
@@ -4130,9 +4321,7 @@ function buildNextGates({ doctor, report, themeGate, polishGate, prepareBuildGat
|
|
|
4130
4321
|
status: prepareBuildGate ? "blocked" : "pass",
|
|
4131
4322
|
reason: prepareBuildGate ? prepareBuildGate.reason : "prepare_build stage is terminal.",
|
|
4132
4323
|
},
|
|
4133
|
-
...
|
|
4134
|
-
? doctor.derived.checkpoint_gates.map((gate) => withPacketSubstituted(gate, packetPath))
|
|
4135
|
-
: []),
|
|
4324
|
+
...campaignCheckpointGates.map((gate) => withPacketSubstituted(gate, packetPath)),
|
|
4136
4325
|
...(doctor?.derived?.polish_checkpoint_gate
|
|
4137
4326
|
? [withPacketSubstituted(doctor.derived.polish_checkpoint_gate, packetPath)]
|
|
4138
4327
|
: []),
|
|
@@ -4154,6 +4343,11 @@ function buildNextGates({ doctor, report, themeGate, polishGate, prepareBuildGat
|
|
|
4154
4343
|
waiver: polishGate?.waiver || null,
|
|
4155
4344
|
required_actions: polishGate?.required_actions || [],
|
|
4156
4345
|
}]),
|
|
4346
|
+
// Per-page gates last: one per page can outnumber the rest, and a
|
|
4347
|
+
// truncated projection of this list (progress keeps the first
|
|
4348
|
+
// PROGRESS_GATE_LIMIT, src/progress-node.mjs) must still carry every
|
|
4349
|
+
// campaign-wide gate.
|
|
4350
|
+
...perPageGates.map((gate) => withPacketSubstituted(gate, packetPath)),
|
|
4157
4351
|
];
|
|
4158
4352
|
}
|
|
4159
4353
|
|
|
@@ -4282,6 +4476,19 @@ function themeStarterPaletteAdvisory(themeGate, packetPath, residueState) {
|
|
|
4282
4476
|
//
|
|
4283
4477
|
// This action is emitted ALONE (see buildNextActions): a divergent packet is
|
|
4284
4478
|
// a stop-and-reconcile state, not a stage with a recommended command.
|
|
4479
|
+
// Each divergence inline, so the count is never stated without the entries it
|
|
4480
|
+
// counts: text output renders only the action description, not divergences[].
|
|
4481
|
+
// The evidence quotes values the toolkit did not write (a deploy URL from the
|
|
4482
|
+
// report or packet, a verdict file's campaign_slug and verdict), so each field
|
|
4483
|
+
// is folded to one line before it joins the sentence. singleLineFragment, not
|
|
4484
|
+
// singleLineDetail: a path or URL keeps its exact characters (no Markdown
|
|
4485
|
+
// escapes) and a list of verdict files is not cut at a length budget.
|
|
4486
|
+
function quoteDivergences(divergences) {
|
|
4487
|
+
return divergences
|
|
4488
|
+
.map((divergence, index) => `(${index + 1}) ${singleLineFragment(divergence.stage)}: ledger claims ${singleLineFragment(divergence.ledger_claim)}; artifact evidence: ${singleLineFragment(divergence.artifact_evidence).replace(/\.?$/, ".")}`)
|
|
4489
|
+
.join(" ");
|
|
4490
|
+
}
|
|
4491
|
+
|
|
4285
4492
|
function divergenceInspectAction(divergences, packetPath) {
|
|
4286
4493
|
const divergedStages = divergences.map((divergence) => divergence.stage);
|
|
4287
4494
|
const forwardHint = divergedStages.includes("qa")
|
|
@@ -4293,7 +4500,7 @@ function divergenceInspectAction(divergences, packetPath) {
|
|
|
4293
4500
|
id: "divergence_inspect",
|
|
4294
4501
|
kind: "manual",
|
|
4295
4502
|
command: null,
|
|
4296
|
-
description: `Ledger and artifacts disagree — ${divergences.length} divergence(s)
|
|
4503
|
+
description: `Ledger and artifacts disagree — ${divergences.length} divergence(s): ${quoteDivergences(divergences)} The same entries are the divergences[] field of \`${cmd("next")} --json\` output; they are not written to any file. This is the ONLY next action: stage actions are suppressed while the disagreement stands, because every one of them would be derived from the same contradictory evidence. Inspect both sides and decide which is right; update the assembly report only after inspection. Do not rerun start/prepare-build or redo completed-looking work on the strength of the ledger alone, and do not treat artifact presence as proof a stage is complete. ${forwardHint} Re-run \`${cmd("next")} --packet ${packetPath} --json\` once the report matches the artifacts to get the normal action list.`,
|
|
4297
4504
|
required: true,
|
|
4298
4505
|
};
|
|
4299
4506
|
}
|
|
@@ -4422,9 +4629,9 @@ export function buildNextActions({ result, packetPath, packet, themeGate, polish
|
|
|
4422
4629
|
}
|
|
4423
4630
|
}
|
|
4424
4631
|
if (result.stage === "setup") {
|
|
4425
|
-
push("setup_skill", "skill", "next-campaigns-os-setup",
|
|
4632
|
+
push("setup_skill", "skill", "next-campaigns-os-setup", `Prepare the target page-kit structure and agent context, then record setup with ${cmd("record")} setup --packet ${packetPath}.`);
|
|
4426
4633
|
} else if (result.stage === "build") {
|
|
4427
|
-
push("build_skill", "skill", "next-campaigns-build",
|
|
4634
|
+
push("build_skill", "skill", "next-campaigns-build", `Assemble the campaign per the build prompt, run the page-kit build, then record build with ${cmd("record")} build --packet ${packetPath}.`);
|
|
4428
4635
|
if (isLocalServePacket(packet)) {
|
|
4429
4636
|
push("build_local_proof", "command", LOCAL_PROOF_BUILD_COMMAND, `Local proof mode (deploy.target is local-serve): build page-kit in the ${LOCAL_PROOF_BUILD_ENVIRONMENT} environment into _site/ and record ${LOCAL_PROOF_BUILD_ENVIRONMENT_FIELD} as "${LOCAL_PROOF_BUILD_ENVIRONMENT}". Vendor loaders are environment-gated out of this render (their protocol-relative //host/... URLs fail over a plain-HTTP local serve); SDK dl_* events still fire. ${LOCAL_PROOF_NEVER_EDIT_RULE}`);
|
|
4430
4637
|
push("build_production_parity", "command", asInvocation(substitutePacket(LOCAL_PROOF_PARITY_COMMAND, packetPath)), "After the development build is proven, assert the production render differs from it only in environment-gated output (same pages, route slugs, Campaign Cart pin and next-api-key) before committing; the PR preview is the second check.");
|
|
@@ -4435,7 +4642,7 @@ export function buildNextActions({ result, packetPath, packet, themeGate, polish
|
|
|
4435
4642
|
}
|
|
4436
4643
|
}
|
|
4437
4644
|
} else if (result.stage === "polish") {
|
|
4438
|
-
push("polish_skill", "skill", "next-campaigns-polish",
|
|
4645
|
+
push("polish_skill", "skill", "next-campaigns-polish", `Run the visual polish pass and ${cmd("polish")} capture, then record polish with ${cmd("record")} polish --packet ${packetPath} --evidence <polish-evidence.json>.`);
|
|
4439
4646
|
if (polishCheckpointGate?.status === "blocked") pushPolishCheckpointActions();
|
|
4440
4647
|
} else if (result.stage === "deploy") {
|
|
4441
4648
|
if (packet.deploy?.target === LOCAL_SERVE_DEPLOY_TARGET) {
|
|
@@ -4604,7 +4811,7 @@ Rules:
|
|
|
4604
4811
|
- Replace demo refs; do not copy Olympus-style shipping_methods into shop-three-step.
|
|
4605
4812
|
- For two-step package-selection flows, treat the selector page as the pre-checkout step and pass the selected cart to checkout with forcePackageId; preserve normal tracking params and strip forcePackageId from visible checkout URLs after SDK initialization.
|
|
4606
4813
|
- After page-kit build, inspect rendered _site output before handoff: each active page should have a body, Campaign Cart runtime markers, SDK meta tags from CampaignSpec sdk_hints.meta_tags, and no stale copied funnel attribution.
|
|
4607
|
-
- Run page-kit build and SDK/template lint, then
|
|
4814
|
+
- Run page-kit build and SDK/template lint, then record build before polish: \`${cmd("record")} build --packet ${packetPath}\`. It stamps stages.assembly.build_fingerprint with the fingerprint doctor computes from the built output (derived.build_output_fingerprint.value, sha256 over the sorted path+sha256 manifest of _site/<slug>/; doctor reports built_output.fingerprint_stale whenever the output on disk stops matching the recorded value), records report.design_source_package.material_fingerprint on stages.assembly.source_package_material_fingerprint when present, and sets stages.polish to "required" (required_by="build", required_for=["qa"]). Re-run it after every rebuild; never hand-edit these fields. Build must not mark stages.polish as completed/completed_with_warnings/skipped. If you applied a brand theme, record report.theme.status, css_path, commerce_pages, load_order=after-next-core, evidence, and any repair-loop defect.
|
|
4608
4815
|
- Capture the machine-readable build summary as an artifact: \`${PAGE_KIT_BUILD_SUMMARY_CAPTURE_COMMAND}\` (requires next-campaign-page-kit >= 0.1.4). Doctor verifies it for per-page build errors and Page Kit shape warnings (NESTED_NO_PERMALINK, DUPLICATE_OUTPUT, MISSING_FRONTMATTER, LAYOUT_NOT_FOUND). If the installed page-kit predates --json, record that in the assembly report instead of skipping silently.${localProofPromptLines(packet)}`;
|
|
4609
4816
|
}
|
|
4610
4817
|
|
|
@@ -4629,9 +4836,9 @@ Read first:
|
|
|
4629
4836
|
- Target repo: ${packet.assembly.target_repo}
|
|
4630
4837
|
- Output dir: ${packet.assembly.output_dir}
|
|
4631
4838
|
|
|
4632
|
-
Prepare the target page-kit structure and agent context, then
|
|
4633
|
-
-
|
|
4634
|
-
- .campaign-runtime/assembly-report.json stages.setup
|
|
4839
|
+
Prepare the target page-kit structure and agent context, then record setup:
|
|
4840
|
+
- ${cmd("record")} setup --packet ${packetPath}
|
|
4841
|
+
It sets .campaign-runtime/build-context.json scaffold.required=false and .campaign-runtime/assembly-report.json stages.setup to completed, validated against their schemas; do not hand-edit either file.
|
|
4635
4842
|
|
|
4636
4843
|
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.
|
|
4637
4844
|
|
|
@@ -4656,13 +4863,10 @@ Before marking Polish complete, install the package-owned browser once and run t
|
|
|
4656
4863
|
|
|
4657
4864
|
The producer covers every mapped route at fixed desktop/mobile viewports and attaches stages.polish.evidence.visual_review.page_load to the current Assembly Report. Never hand-author or copy page_load. A missing, stale, malformed, incomplete, cache/service-worker-observed, or over-threshold result keeps Polish/deploy/QA blocked; repair and recapture, or use the exact named-human checkpoint waiver only for a complete hidden eager-media finding.
|
|
4658
4865
|
|
|
4659
|
-
Record Polish
|
|
4660
|
-
- status: completed or completed_with_warnings (or blocked with blockers)
|
|
4661
|
-
-
|
|
4662
|
-
-
|
|
4663
|
-
- source_package_material_fingerprint: the current report.design_source_package.material_fingerprint when present
|
|
4664
|
-
- completed_at: ISO timestamp
|
|
4665
|
-
- evidence.visual_review: representative screenshot paths/URLs plus package-generated page_load
|
|
4866
|
+
Record Polish before QA with ${cmd("record")} polish --packet ${packetPath} --evidence <polish-evidence.json>. It stamps performed_by (next-campaigns-polish), source_build_fingerprint (doctor's current build output fingerprint), source_package_material_fingerprint (when the report has one) and completed_at, keeps the captured page_load, and refuses a completed status, writing nothing, unless the polish gate would pass. The file is a JSON object:
|
|
4867
|
+
- status: completed or completed_with_warnings (default completed); or blocked with blockers (non-empty array of {code, message}), or skipped with skip_reason (string), evidence then optional; a blocked or skipped Polish keeps QA blocked
|
|
4868
|
+
- repair_loop_defect: optional; null or the first brand-layer repair-loop defect object, written to report.theme.repair_loop_defect
|
|
4869
|
+
- evidence.visual_review: object with representative screenshot paths/URLs (page_load comes from polish capture; never include it)
|
|
4666
4870
|
- evidence.brand_review: logo/favicon/brand color checks, including non-template favicon confirmation
|
|
4667
4871
|
- evidence.checkout_review: labels/placeholders, phone alignment, payment display, bump compare-price rule
|
|
4668
4872
|
- evidence.template_residue_review: NEXT Blue/template placeholder/starter favicon/lorem/product residue checks
|
|
@@ -4759,7 +4963,7 @@ ${cmd("qa")} install-browser
|
|
|
4759
4963
|
Node QA command:
|
|
4760
4964
|
${cmd("qa")} run --packet ${packetPath} --base-url ${url} --browser --test-order common
|
|
4761
4965
|
|
|
4762
|
-
Run the browser install once after install/update before --browser or --test-order. Test-order proof must exercise the campaign through the Campaign Cart SDK with the browser typed-card flow. Do not create hand-built backend API orders as launch proof. Compare visible placeholders, payment methods, variant media, promo/urgency copy, pricing presentation, and trust/guarantee claims against the Campaign Build Brief. Test Orders use global test cards that bypass the payment gateway and create no transactions, so they are safe to run any time and need no permission flags, packet policy, or merchant setup. Localhost on any port is a globally allowed Development domain for SDK initialization and suppresses Campaigns analytics events; non-localhost preview/production origins still need the SDK origin allowlist. Use --test-order common for checkout, first-offer accept/decline
|
|
4966
|
+
Run the browser install once after install/update before --browser or --test-order. Test-order proof must exercise the campaign through the Campaign Cart SDK with the browser typed-card flow. Do not create hand-built backend API orders as launch proof. Compare visible placeholders, payment methods, variant media, promo/urgency copy, pricing presentation, and trust/guarantee claims against the Campaign Build Brief. Test Orders use global test cards that bypass the payment gateway and create no transactions, so they are safe to run any time and need no permission flags, packet policy, or merchant setup. Localhost on any port is a globally allowed Development domain for SDK initialization and suppresses Campaigns analytics events; non-localhost preview/production origins still need the SDK origin allowlist. Use --test-order common for the default depth: every actual terminal path when they fit under the flood cap, otherwise checkout, first-offer accept/decline and a deduplicated shortest real receipt path plus the shortest path that clicks the decline on each offer or downsell page not yet declined, up to the cap (the verdict row browser-test-order:upsell-action-coverage warns naming each offer page whose decline no order clicked); use an explicit path such as accept-decline-accept for a targeted matrix; or use --test-order full for every actual terminal path in the selected checkout topology. Cycles, missing routes, and reachable nonterminals block exhaustive proof before browser launch. The default accidental-flood cap is 6, and an overflow names the exact explicit --max-test-orders raise. That cap bounds planned paths; --max-order-creations bounds actual order creations and is reserved before each submit click, defaulting to the planned path count. A path whose failure is classified as created (the order is already placed) is inspected read-only and never resubmitted. A not_created failure may be re-run once, if the creation budget has a slot no still-unrun planned path needs; an ambiguous failure stops that path with an explicit operator check instead of buying again. Read evidence.recovery to tell a recovered pass from a first-attempt pass. Click rendered SDK upsell accept/decline controls for upsell proof. For multi-tier package selectors, drive a specific card with --select-package <ref[:qty],...> (strict: the path fails if the requested card cannot be found or selected, unlike best-effort --cart), or use --test-order tiers / tiers:common / tiers:full to drive every selector tier the CampaignSpec declares on the checkout page in one run (order-bump rows marked is_upsell are add-ons, not tiers; --select-package narrows a tiers run to the listed tiers); prove coupon-bearing orders with --apply-coupon <code> (typed into the rendered promo input, verified against the persisted-order voucher read-back). Reuse one test customer email via --test-email or CAMPAIGNS_OS_QA_TEST_EMAIL (a real monitored inbox in internal runs) so repeated QA does not litter the customer list.
|
|
4763
4967
|
|
|
4764
4968
|
Launch readiness note: Campaigns OS can prove the campaign build, SDK wiring, browser behavior, and typed-card order paths. It does not prove the merchant is ready for real shoppers. Before launch, confirm the production storefront URL, live payment methods, shipping markets, legal/support URLs, analytics expectations, and any merchant-side configuration. Treat those as real-shopper readiness items, not Campaigns OS build blockers.
|
|
4765
4969
|
|
|
@@ -4991,6 +5195,7 @@ function installSkills(targetArg = null, dryRun = false, platformArg = null) {
|
|
|
4991
5195
|
source_directory: sourceDir,
|
|
4992
5196
|
targets: targetResults,
|
|
4993
5197
|
skills: targetResults.flatMap((target) => target.skills),
|
|
5198
|
+
read_now: targetResults.flatMap((target) => target.read_now),
|
|
4994
5199
|
available_platforms: SKILL_PLATFORMS.map((platform) => ({
|
|
4995
5200
|
platform: platform.id,
|
|
4996
5201
|
label: platform.label,
|
|
@@ -4998,10 +5203,24 @@ function installSkills(targetArg = null, dryRun = false, platformArg = null) {
|
|
|
4998
5203
|
})),
|
|
4999
5204
|
note: dryRun
|
|
5000
5205
|
? "Dry run only; no skill files were written."
|
|
5001
|
-
: "
|
|
5206
|
+
: skillsReadNowNote(targetResults.flatMap((target) => target.read_now), "local agent sessions"),
|
|
5002
5207
|
};
|
|
5003
5208
|
}
|
|
5004
5209
|
|
|
5210
|
+
// A running agent does not load skills written after it started, and it cannot
|
|
5211
|
+
// restart itself, so the session that ran install-skills is told to read the
|
|
5212
|
+
// written SKILL.md files directly. A restart is the secondary route: it only
|
|
5213
|
+
// matters to sessions started later.
|
|
5214
|
+
// The same instruction wherever an action hands the agent the install-skills
|
|
5215
|
+
// command (tooling status), so no action tells it to restart first. No
|
|
5216
|
+
// parentheses: the install action's command must stay runnable as printed.
|
|
5217
|
+
const SKILLS_READ_NOW_FOLLOW_UP = "Then read the SKILL.md files install-skills lists under Read now in this session, because a running session does not load skills installed after it started; restart the agent only if it cannot read them.";
|
|
5218
|
+
|
|
5219
|
+
function skillsReadNowNote(readNow, sessionLabel) {
|
|
5220
|
+
if (!readNow.length) return "No skill files changed; nothing new to read.";
|
|
5221
|
+
return `Read these now in this session: the SKILL.md files listed under "Read now" (a running session does not load skills installed after it started). New ${sessionLabel} load them on their own.`;
|
|
5222
|
+
}
|
|
5223
|
+
|
|
5005
5224
|
// A platform directory counts as installed when a skill already sits under one
|
|
5006
5225
|
// of the bundled names (current or not), or our own copy under a retired name. A
|
|
5007
5226
|
// slot install-skills would only create says nothing about that platform, and
|
|
@@ -5473,7 +5692,7 @@ function toolingCommand(args) {
|
|
|
5473
5692
|
// Code, the documented install). The other platforms follow in a separate
|
|
5474
5693
|
// sentence of prose: no `<a|b>` template or parenthesis a shell would read
|
|
5475
5694
|
// as a redirect or a subshell if the command were copied with it.
|
|
5476
|
-
actions.push(`Install bundled skills for the harness you use: ${cli.invocation_prefix} install-skills --platform claude. Use --platform codex for Codex, or --platform agents for shared agent skills such as Cursor's.
|
|
5695
|
+
actions.push(`Install bundled skills for the harness you use: ${cli.invocation_prefix} install-skills --platform claude. Use --platform codex for Codex, or --platform agents for shared agent skills such as Cursor's. ${SKILLS_READ_NOW_FOLLOW_UP}`);
|
|
5477
5696
|
} else if (staleSkills.length) {
|
|
5478
5697
|
const stalePlatforms = SKILL_PLATFORMS.map((platform) => platform.id)
|
|
5479
5698
|
.filter((id) => staleSkills.some((skill) => skill.platform === id));
|
|
@@ -5483,7 +5702,7 @@ function toolingCommand(args) {
|
|
|
5483
5702
|
? stalePlatforms.map((platform) => ["--platform", platform])
|
|
5484
5703
|
: [["--platform", args.platform || "all"]];
|
|
5485
5704
|
const commands = invocations.map((skillArgs) => `${cli.invocation_prefix} install-skills ${skillArgs.join(" ")}`);
|
|
5486
|
-
actions.push(`Refresh installed skills: ${commands.join(" and ")}.
|
|
5705
|
+
actions.push(`Refresh installed skills: ${commands.join(" and ")}. ${SKILLS_READ_NOW_FOLLOW_UP}`);
|
|
5487
5706
|
}
|
|
5488
5707
|
|
|
5489
5708
|
if (install.mode === "checkout" && cli.global_binary.status === "not_found") {
|
|
@@ -5773,6 +5992,11 @@ function installSkillsToTarget({ sourceDir, target, dryRun, retired = [] }) {
|
|
|
5773
5992
|
});
|
|
5774
5993
|
}
|
|
5775
5994
|
|
|
5995
|
+
// The SKILL.md files this run wrote; an unchanged one is what was already
|
|
5996
|
+
// there for the session to load.
|
|
5997
|
+
const readNow = dryRun
|
|
5998
|
+
? []
|
|
5999
|
+
: skills.filter((skill) => skill.action === "created" || skill.action === "updated").map((skill) => skill.destination);
|
|
5776
6000
|
return {
|
|
5777
6001
|
ok: true,
|
|
5778
6002
|
status: dryRun ? "dry_run" : "installed",
|
|
@@ -5781,9 +6005,10 @@ function installSkillsToTarget({ sourceDir, target, dryRun, retired = [] }) {
|
|
|
5781
6005
|
source_directory: sourceDir,
|
|
5782
6006
|
target_directory: targetDir,
|
|
5783
6007
|
skills,
|
|
6008
|
+
read_now: readNow,
|
|
5784
6009
|
note: dryRun
|
|
5785
6010
|
? "Dry run only; no skill files were written."
|
|
5786
|
-
:
|
|
6011
|
+
: skillsReadNowNote(readNow, `${target.platform_label} sessions`),
|
|
5787
6012
|
};
|
|
5788
6013
|
}
|
|
5789
6014
|
|
|
@@ -7918,6 +8143,11 @@ export function resultTextLines(result, { headerLines = [] } = {}) {
|
|
|
7918
8143
|
if (result.dry_run) lines.push(`Would write: ${result.would_write} (nothing was written)`);
|
|
7919
8144
|
if (result.next_stage) lines.push(`Next stage: ${result.next_stage}${result.next_stage_reason ? ` (${result.next_stage_reason})` : ""}`);
|
|
7920
8145
|
}
|
|
8146
|
+
if (result.action === "record") {
|
|
8147
|
+
lines.push(`${result.dry_run ? "Would record" : "Recorded"}: ${result.stage}`);
|
|
8148
|
+
for (const path of result.dry_run ? result.would_write : result.written) lines.push(`${result.dry_run ? "Would write" : "Wrote"}: ${path}${result.dry_run ? " (nothing was written)" : ""}`);
|
|
8149
|
+
if (result.next_stage) lines.push(`Next stage: ${result.next_stage}${result.next_stage_reason ? ` (${result.next_stage_reason})` : ""}`);
|
|
8150
|
+
}
|
|
7921
8151
|
if (result.targets?.length) {
|
|
7922
8152
|
lines.push("Targets:");
|
|
7923
8153
|
for (const target of result.targets) {
|
|
@@ -7930,6 +8160,10 @@ export function resultTextLines(result, { headerLines = [] } = {}) {
|
|
|
7930
8160
|
lines.push("Skills:");
|
|
7931
8161
|
for (const skill of result.skills) lines.push(`- ${formatSkillInstallSummary(skill)}`);
|
|
7932
8162
|
}
|
|
8163
|
+
if (result.read_now?.length) {
|
|
8164
|
+
lines.push("Read now (read these now in this session):");
|
|
8165
|
+
for (const path of result.read_now) lines.push(`- ${path}`);
|
|
8166
|
+
}
|
|
7933
8167
|
if (result.ready?.length) {
|
|
7934
8168
|
lines.push("Ready:");
|
|
7935
8169
|
for (const item of result.ready) lines.push(`- ${item}`);
|