@nextcommerce/campaigns-os 1.41.2 → 1.43.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/CHANGELOG.md +178 -0
  2. package/README.md +6 -4
  3. package/agents/claude/CLAUDE.md +5 -1
  4. package/campaign-spec/dist/types.d.ts +2 -0
  5. package/contracts/effects.v1.json +111 -18
  6. package/contracts/migration-sidecar-bundle.v0.json +9 -0
  7. package/contracts/release-ledger.json +752 -0
  8. package/contracts/supported-surface.json +12 -11
  9. package/docs/build-packet.md +56 -6
  10. package/docs/campaigns-os-build-flow.md +2 -2
  11. package/docs/effects.md +70 -1
  12. package/docs/local-setup.md +51 -0
  13. package/docs/migration-sidecar-bundle.md +6 -1
  14. package/docs/orientation-contract-reference.md +1 -1
  15. package/docs/progress-snapshots.md +6 -0
  16. package/docs/qa-and-test-orders.md +26 -10
  17. package/docs/runtime-readiness.md +1 -1
  18. package/docs/skills-revision.md +10 -10
  19. package/package.json +3 -2
  20. package/schemas/campaign-runtime-assembly-report.v0.schema.json +6 -1
  21. package/schemas/campaign-runtime-build-packet.v0.schema.json +6 -1
  22. package/schemas/campaign-spec.v4.schema.json +4 -0
  23. package/schemas/campaigns-os-progress-snapshot.v0.schema.json +1 -0
  24. package/schemas/campaigns-os-qa-verdict-sidecar.v0.schema.json +1 -0
  25. package/schemas/campaigns-os-qa-verdict.v0.schema.json +1 -0
  26. package/schemas/campaigns-os-run-record.v0.schema.json +1 -0
  27. package/skills/campaign-lifecycle-orientation/SKILL.md +13 -8
  28. package/skills/campaign-readback-classification/SKILL.md +3 -3
  29. package/skills/campaign-run-evidence/SKILL.md +8 -6
  30. package/skills/contribution-intake/SKILL.md +3 -3
  31. package/skills/next-campaigns-build/SKILL.md +3 -3
  32. package/skills/next-campaigns-os/SKILL.md +17 -4
  33. package/skills/next-campaigns-os/references/session-intake.md +4 -4
  34. package/skills/next-campaigns-os-setup/SKILL.md +3 -3
  35. package/skills/next-campaigns-polish/SKILL.md +3 -3
  36. package/skills/next-campaigns-qa/SKILL.md +10 -9
  37. package/skills.json +11 -11
  38. package/src/cli.mjs +187 -70
  39. package/src/finding-cause.mjs +14 -10
  40. package/src/lifecycle.mjs +5 -4
  41. package/src/polish-node.mjs +5 -2
  42. package/src/progress-node.mjs +3 -2
  43. package/src/progress.mjs +5 -3
  44. package/src/qa-node.mjs +58 -26
  45. package/src/qa-publish.mjs +4 -0
  46. package/src/qa-sidecar.mjs +2 -0
  47. package/src/qa-verdict-discovery.mjs +11 -0
  48. package/src/qa-verdict-publish.mjs +1 -0
  49. package/src/qa-verdict.mjs +8 -1
  50. package/src/readback.mjs +2 -1
  51. package/src/run-record-closeout.mjs +3 -4
  52. package/src/run-record.mjs +4 -0
  53. package/src/sidecar-bundle.mjs +21 -0
  54. package/src/spec-source-identity.mjs +44 -0
  55. package/src/stage-ledger.mjs +4 -1
  56. package/src/tooling-setup.mjs +160 -0
@@ -0,0 +1,160 @@
1
+ import { existsSync, lstatSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
2
+ import { isAbsolute, join, relative, resolve, sep } from "node:path";
3
+
4
+ const PACKAGE = "@nextcommerce/campaigns-os";
5
+ const CONTEXT = ".campaign-runtime/agent-context/CLAUDE.md";
6
+ const IMPORT = `@${CONTEXT}`;
7
+
8
+ // Setup composes the existing installers after npm has installed the project
9
+ // dependencies. It never chooses a campaign, scaffolds pages, or opens a run.
10
+ export function setupArguments(args, argv) {
11
+ const values = new Set(["target", "platform"]);
12
+ const flags = new Set(["dry-run", "json"]);
13
+ const seen = new Set();
14
+ const tokens = argv[0] === "campaigns-os" ? argv.slice(1) : argv;
15
+ for (let i = 2; i < tokens.length; i++) {
16
+ const token = tokens[i];
17
+ const key = token.startsWith("--") ? token.slice(2) : "";
18
+ if ((!values.has(key) && !flags.has(key)) || seen.has(key)) {
19
+ throw new Error(`tooling setup: unsupported or repeated argument ${JSON.stringify(token)}.`);
20
+ }
21
+ seen.add(key);
22
+ if (values.has(key)) {
23
+ if (!tokens[i + 1] || tokens[i + 1].startsWith("--")) throw new Error(`tooling setup: --${key} requires a value.`);
24
+ i++;
25
+ }
26
+ }
27
+ if (typeof args.target !== "string" || !args.target.trim()) throw new Error("tooling setup: select the campaign folder with --target <directory>.");
28
+ if (args.platform && args.platform !== "claude") throw new Error("tooling setup: this entry supports --platform claude. Other agents can use install-skills and install-agent-context.");
29
+ }
30
+
31
+ function json(path) {
32
+ return JSON.parse(readFileSync(path, "utf8"));
33
+ }
34
+
35
+ function hasContextImport(text) {
36
+ let fence = null;
37
+ let found = false;
38
+ for (const line of text.split(/\r?\n/)) {
39
+ const marker = line.match(/^ {0,3}(`{3,}|~{3,})(.*)$/);
40
+ if (fence) {
41
+ if (marker && marker[1][0] === fence[0] && marker[1].length >= fence.length && !marker[2].trim()) fence = null;
42
+ } else if (marker) {
43
+ fence = marker[1];
44
+ } else if (/^ {0,3}@/.test(line) && line.trim() === IMPORT) {
45
+ found = true;
46
+ }
47
+ }
48
+ if (!found && fence) throw new Error("tooling setup: close the unterminated code fence in CLAUDE.md before setup can append an active context import; no files were changed.");
49
+ return found;
50
+ }
51
+
52
+ function regularDestination(root, path) {
53
+ const rel = relative(root, path);
54
+ if (!rel || rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) {
55
+ throw new Error(`tooling setup: destination must stay inside the selected project: ${path}.`);
56
+ }
57
+ const parts = rel.split(sep);
58
+ let current = root;
59
+ for (let i = 0; i < parts.length; i++) {
60
+ current = join(current, parts[i]);
61
+ let stat;
62
+ try { stat = lstatSync(current); } catch (error) {
63
+ // Every existing ancestor has already been checked. Once a component is
64
+ // absent, its descendants cannot exist; dangling symlinks still have lstat.
65
+ if (error.code === "ENOENT") return;
66
+ throw error;
67
+ }
68
+ if (stat.isSymbolicLink() || (i < parts.length - 1 ? !stat.isDirectory() : !stat.isFile())) {
69
+ throw new Error(`tooling setup: preserve ${current}; expected a regular ${i < parts.length - 1 ? "directory" : "file"}, not a symlink or another file type.`);
70
+ }
71
+ }
72
+ }
73
+
74
+ export function setupTooling(args, { packageRoot, installSkills, installAgentContext, installBrowser }) {
75
+ const target = realpathSync(resolve(args.target));
76
+ const pkg = json(join(packageRoot, "package.json"));
77
+ const manifestPath = join(target, "package.json");
78
+ const lockPath = join(target, "package-lock.json");
79
+ if (!existsSync(manifestPath)) {
80
+ throw new Error(`tooling setup: package.json is missing. For a new project, follow the pinned install in ${join(packageRoot, "docs/local-setup.md")}. For an existing project, restore its manifest and lockfile and run npm ci.`);
81
+ }
82
+ if (!existsSync(lockPath)) {
83
+ throw new Error("tooling setup: package-lock.json is missing. Restore the project's reviewed lockfile, or generate it from its existing dependency pins with npm install, then rerun setup. Do not replace the project's page-kit pin with a new-project example.");
84
+ }
85
+ const manifest = json(manifestPath);
86
+ const pins = [manifest.devDependencies?.[PACKAGE], manifest.dependencies?.[PACKAGE]].filter(Boolean);
87
+ if (!pins.length || pins.some((pin) => pin !== pkg.version)) {
88
+ throw new Error(`tooling setup: the project must pin ${PACKAGE} exactly to the running version ${pkg.version}; preserve its current pin or explicitly install the reviewed version first.`);
89
+ }
90
+ const installed = join(target, "node_modules", "@nextcommerce", "campaigns-os");
91
+ if (!existsSync(installed) || realpathSync(installed) !== realpathSync(packageRoot)) {
92
+ throw new Error("tooling setup: run the selected project's installed copy: cd into that folder and use npx --no-install campaigns-os tooling setup --target . --platform claude.");
93
+ }
94
+ const lock = json(lockPath);
95
+ if (lock.packages?.["node_modules/@nextcommerce/campaigns-os"]?.version !== pkg.version) {
96
+ throw new Error("tooling setup: the lockfile does not match the project toolkit pin; reconcile the reviewed dependency with npm before setup.");
97
+ }
98
+ if (!manifest.dependencies?.["next-campaign-page-kit"] && !manifest.devDependencies?.["next-campaign-page-kit"]) {
99
+ throw new Error("tooling setup: install next-campaign-page-kit in this project first. No campaign pages have been scaffolded or changed.");
100
+ }
101
+ if (!existsSync(join(target, "node_modules", "next-campaign-page-kit", "package.json"))) {
102
+ throw new Error("tooling setup: page-kit is declared but not installed; run npm ci in the selected project first.");
103
+ }
104
+
105
+ // Preflight every destination before any installer runs. Custom repository
106
+ // instructions are preserved; only one Claude import line is appended.
107
+ const instructions = join(target, "CLAUDE.md");
108
+ regularDestination(target, instructions);
109
+ regularDestination(target, join(target, ".gitignore"));
110
+ for (const name of ["CLAUDE.md", "AGENTS.md", "campaigns-os.mdc", "copilot-instructions.md"]) {
111
+ const dest = join(target, ".campaign-runtime", "agent-context", name);
112
+ regularDestination(target, dest);
113
+ const source = { "CLAUDE.md": "agents/claude/CLAUDE.md", "AGENTS.md": "agents/codex/AGENTS.md", "campaigns-os.mdc": "agents/cursor/campaigns-os.mdc", "copilot-instructions.md": "agents/copilot/copilot-instructions.md" }[name];
114
+ if (existsSync(dest) && readFileSync(dest, "utf8") !== readFileSync(join(packageRoot, source), "utf8")) {
115
+ throw new Error(`tooling setup: ${dest} differs from this toolkit's context. Preserve and reconcile it before rerunning setup; no files were changed.`);
116
+ }
117
+ }
118
+ const prior = existsSync(instructions) ? readFileSync(instructions, "utf8") : "";
119
+ const hasImport = hasContextImport(prior);
120
+ // Do the fallible download before changing the shared skills or project.
121
+ const browser = args["dry-run"] ? { ok: true, status: "not_run" } : installBrowser({ json: Boolean(args.json) });
122
+ const skills = browser.ok ? installSkills(null, Boolean(args["dry-run"]), "claude") : null;
123
+ const context = browser.ok ? installAgentContext(target, Boolean(args["dry-run"])) : null;
124
+ const contextFailed = context?.gitignore?.action === "skipped";
125
+ const ready = browser.ok && !contextFailed;
126
+ if (ready && !args["dry-run"] && !hasImport) {
127
+ writeFileSync(instructions, `${prior}${prior && !prior.endsWith("\n") ? "\n" : ""}\n${IMPORT}\n`);
128
+ }
129
+ const revision = json(join(packageRoot, "skills.json")).bundle_revision;
130
+ return {
131
+ ok: ready,
132
+ status: !browser.ok ? "browser_install_failed" : contextFailed ? "context_install_failed" : args["dry-run"] ? "dry_run" : "restart_required",
133
+ target_repo: target,
134
+ skills_revision: revision,
135
+ skills,
136
+ context,
137
+ instructions: { path: instructions, action: !ready ? "not_run" : hasImport ? "unchanged" : "append_import" },
138
+ browser,
139
+ next_action: contextFailed
140
+ ? `Setup could not add the runtime ignore block (${context.gitignore.reason}). Fix .gitignore and rerun setup; skills and context files may already be installed.`
141
+ : args["dry-run"]
142
+ ? "Run tooling setup with the same target and without --dry-run to install the browser, skills and agent context."
143
+ : browser.ok
144
+ ? `Restart Claude Code in this folder, then use the next-campaigns-os skill with your CampaignSpec and source material. Confirm the loaded bundle with npx --no-install campaigns-os tooling status --platform claude --skills-revision ${revision}.`
145
+ : "Fix the browser installation error and rerun tooling setup; existing page source and repository instructions are preserved.",
146
+ note: "Setup prepares tools; it does not scaffold pages, create a spec, select a campaign, log in, or prove that an agent loaded the installed skills.",
147
+ };
148
+ }
149
+
150
+ export function setupTextLines(result) {
151
+ return [
152
+ `Status: ${result.status.toUpperCase()}`,
153
+ `Campaign folder: ${result.target_repo}`,
154
+ `Skills revision: ${result.skills_revision}`,
155
+ `Browser: ${result.browser.status}`,
156
+ ...(result.browser.note ? [result.browser.note] : []),
157
+ result.next_action,
158
+ result.note,
159
+ ];
160
+ }