@echopath-labs/forgerail 0.1.0-alpha.2 → 0.1.0-alpha.4

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 (63) hide show
  1. package/.codex-plugin/plugin.json +2 -3
  2. package/CHANGELOG.md +22 -1
  3. package/CODE_OF_CONDUCT.md +34 -0
  4. package/CONTRIBUTING.md +68 -4
  5. package/README.md +126 -49
  6. package/README.zh-CN.md +131 -28
  7. package/SECURITY.md +48 -4
  8. package/SUPPORT.md +37 -0
  9. package/adapters/claude-code.json +6 -1
  10. package/adapters/codex.json +6 -0
  11. package/adapters/cursor.json +5 -0
  12. package/contracts/adoption-plan.schema.json +39 -18
  13. package/contracts/effective-profile.schema.json +4 -4
  14. package/contracts/host-adapter.schema.json +66 -4
  15. package/contracts/host-binding-receipt.schema.json +1 -1
  16. package/contracts/launch-contract.schema.json +38 -2
  17. package/contracts/profile-change-candidate.schema.json +1 -1
  18. package/contracts/return-receipt.schema.json +1 -1
  19. package/contracts/task-envelope.schema.json +1 -1
  20. package/directory/README.md +1 -1
  21. package/directory/release-notes-alpha3.md +7 -0
  22. package/directory/release-notes-alpha4.md +9 -0
  23. package/directory/submission-candidate.json +5 -6
  24. package/docs/adoption.md +63 -26
  25. package/docs/adoption.zh-CN.md +62 -25
  26. package/docs/architecture-acceptance.md +1 -1
  27. package/docs/composable-autonomy.zh-CN.md +16 -22
  28. package/docs/installation.md +72 -40
  29. package/docs/installation.zh-CN.md +90 -31
  30. package/docs/release-alpha3.md +25 -0
  31. package/docs/release-alpha3.zh-CN.md +25 -0
  32. package/docs/release-alpha4.md +33 -0
  33. package/docs/release-alpha4.zh-CN.md +33 -0
  34. package/package.json +7 -3
  35. package/scripts/adoption-closeout-regressions.mjs +100 -0
  36. package/scripts/build-universal-directory-candidate.mjs +2 -2
  37. package/scripts/disposable-consumer.mjs +11 -18
  38. package/scripts/fixtures/contracts/adoption-plan.multi-host.valid.json +16 -7
  39. package/scripts/fixtures/contracts/adoption-plan.mutating.invalid.json +6 -3
  40. package/scripts/fixtures/contracts/adoption-plan.single-host.valid.json +9 -4
  41. package/scripts/fixtures/contracts/effective-profile.duplicate-rule.invalid.json +1 -1
  42. package/scripts/fixtures/contracts/effective-profile.valid.json +3 -4
  43. package/scripts/fixtures/contracts/host-adapter.claude-code.profile-only.valid.json +6 -1
  44. package/scripts/fixtures/contracts/host-adapter.codex.valid.json +6 -0
  45. package/scripts/fixtures/contracts/host-adapter.cursor.profile-only.valid.json +5 -0
  46. package/scripts/fixtures/contracts/host-adapter.false-supported.invalid.json +6 -1
  47. package/scripts/fixtures/contracts/launch-contract.execution-owner.invalid.json +5 -1
  48. package/scripts/fixtures/contracts/launch-contract.valid.json +5 -1
  49. package/scripts/fixtures/open-source-docs/cases.json +65 -0
  50. package/scripts/forgerail.mjs +61 -16
  51. package/scripts/integrity-regressions.mjs +1261 -0
  52. package/scripts/lib/adoption.mjs +666 -51
  53. package/scripts/lib/bounded-read.mjs +80 -0
  54. package/scripts/lib/composition.mjs +77 -7
  55. package/scripts/lib/contracts.mjs +126 -40
  56. package/scripts/lib/diagnosis.mjs +146 -39
  57. package/scripts/shadow-comparison.mjs +52 -34
  58. package/scripts/validate-open-source-docs.mjs +132 -0
  59. package/scripts/validate-release.mjs +77 -13
  60. package/scripts/validate-universal-directory.mjs +17 -5
  61. package/skills/forgerail/references/adoption.md +2 -2
  62. package/skills/forgerail/references/contracts.md +2 -2
  63. package/scripts/lib/bundle.mjs +0 -77
@@ -1,14 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { existsSync, readFileSync } from "node:fs";
3
+ import { spawnSync } from "node:child_process";
4
+ import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
5
+ import { tmpdir } from "node:os";
4
6
  import { dirname, resolve } from "node:path";
5
7
  import { fileURLToPath } from "node:url";
6
8
 
7
9
  const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
8
10
  const expectedPackageName = "@echopath-labs/forgerail";
9
- const expectedVersion = "0.1.0-alpha.2";
11
+ const expectedVersion = "0.1.0-alpha.4";
10
12
  const expectedTag = `v${expectedVersion}`;
11
- const expectedDate = "2026-08-30";
13
+ const expectedDate = "2026-09-01";
12
14
  const expectedPlugins = [
13
15
  "forgerail",
14
16
  "forgerail-cross-workspace-orchestration",
@@ -38,6 +40,30 @@ function findExisting(candidates) {
38
40
 
39
41
  const packageJson = json("package.json");
40
42
  const packageLock = json("package-lock.json");
43
+ const launchContractSchema = json("contracts/launch-contract.schema.json");
44
+ const effectiveProfileSchema = json("contracts/effective-profile.schema.json");
45
+ const publicCli = read("scripts/forgerail.mjs");
46
+ const packCache = mkdtempSync(resolve(tmpdir(), "forgerail-pack-cache-"));
47
+ const packEnvironment = Object.fromEntries(
48
+ Object.entries(process.env).filter(([key]) => key.toLowerCase() !== "npm_config_cache"),
49
+ );
50
+ packEnvironment.NPM_CONFIG_CACHE = packCache;
51
+ let packResult;
52
+ try {
53
+ packResult = spawnSync("npm", ["pack", "--dry-run", "--json", "--ignore-scripts"], {
54
+ cwd: root,
55
+ encoding: "utf8",
56
+ env: packEnvironment,
57
+ });
58
+ } finally {
59
+ rmSync(packCache, { recursive: true, force: true });
60
+ }
61
+ let packedFiles = [];
62
+ try {
63
+ packedFiles = packResult.status === 0
64
+ ? JSON.parse(packResult.stdout)[0]?.files?.map(({ path }) => path) ?? []
65
+ : [];
66
+ } catch {}
41
67
  record("package-name", packageJson.name === expectedPackageName, packageJson.name);
42
68
  record("package-lock-name", packageLock.name === expectedPackageName && packageLock.packages?.[""]?.name === expectedPackageName, packageLock.name);
43
69
  record("package-version", packageJson.version === expectedVersion, packageJson.version);
@@ -45,9 +71,26 @@ record("package-lock-version", packageLock.version === expectedVersion && packag
45
71
  record("package-license", packageJson.license === "Apache-2.0", packageJson.license);
46
72
  record("package-lock-license", packageLock.packages?.[""]?.license === "Apache-2.0", packageLock.packages?.[""]?.license ?? null);
47
73
  record("npm-next-tag", packageJson.publishConfig?.tag === "next", packageJson.publishConfig?.tag ?? null);
74
+ record("no-public-bundle-builder-command", !publicCli.includes('command === "build-bundle"'), "source-repository maintainer tool only");
75
+ record("npm-pack-dry-run", packResult.status === 0 && packedFiles.length > 0, packResult.status === 0 ? `${packedFiles.length} files` : packResult.stderr.trim());
76
+ record("bundle-builder-source-only", existsSync(resolve(root, "tools/lib/bundle.mjs")) && !packedFiles.includes("tools/lib/bundle.mjs"), "tools/lib/bundle.mjs");
77
+ record(
78
+ "launch-requested-pack-schema-native-binding",
79
+ launchContractSchema.properties?.envelope?.properties?.packs?.type === "object"
80
+ && launchContractSchema.properties?.envelope?.properties?.packs?.additionalProperties?.pattern === "^[0-9a-f]{64}$",
81
+ launchContractSchema.properties?.envelope?.properties?.packs ?? null,
82
+ );
83
+ record(
84
+ "profile-pack-schema-native-identity",
85
+ effectiveProfileSchema.properties?.packs?.type === "object"
86
+ && effectiveProfileSchema.properties?.packs?.propertyNames?.pattern === "^[a-z][a-z0-9-]+$"
87
+ && effectiveProfileSchema.properties?.packs?.additionalProperties?.required?.includes("state")
88
+ && effectiveProfileSchema.properties?.packs?.additionalProperties?.required?.includes("reason"),
89
+ effectiveProfileSchema.properties?.packs ?? null,
90
+ );
48
91
  record(
49
92
  "prepublish-gate",
50
- ["npm test", "npm run test:shadow", "npm run test:release", "npm run test:consumer", "npm run test:directory"].every((command) => packageJson.scripts?.prepublishOnly?.includes(command)),
93
+ ["npm test", "npm run test:integrity", "npm run test:shadow", "npm run test:release", "npm run test:consumer", "npm run test:directory"].every((command) => packageJson.scripts?.prepublishOnly?.includes(command)),
51
94
  packageJson.scripts?.prepublishOnly ?? null,
52
95
  );
53
96
 
@@ -89,7 +132,11 @@ for (const phrase of ["Workspace Diagnosis", "Return Receipts", "GitHub Rulesets
89
132
  record(`changelog-${phrase.toLowerCase().replaceAll(/[^a-z0-9]+/g, "-")}`, changelog.includes(phrase), phrase);
90
133
  }
91
134
 
92
- const marketplacePath = findExisting(["marketplace/.agents/plugins/marketplace.json", ".agents/plugins/marketplace.json"]);
135
+ const marketplacePath = findExisting([
136
+ "marketplace/.agents/plugins/marketplace.json",
137
+ ".agents/plugins/marketplace.json",
138
+ "../../.agents/plugins/marketplace.json",
139
+ ]);
93
140
  const marketplace = json(marketplacePath);
94
141
  const marketplacePlugins = new Map(marketplace.plugins.map((plugin) => [plugin.name, plugin]));
95
142
  record("marketplace-name", marketplace.name === "echopath-labs", marketplace.name);
@@ -99,15 +146,17 @@ for (const name of expectedPlugins) {
99
146
  if (name !== "forgerail") record(`marketplace-${name}-on-use`, plugin?.policy?.authentication === "ON_USE", plugin?.policy?.authentication ?? null);
100
147
  }
101
148
 
102
- const installation = `${read("docs/installation.md")}\n${read("docs/installation.zh-CN.md")}`;
149
+ const installation = `${read("docs/installation.md")}\n${read("docs/installation.zh-CN.md")}\n${read("docs/adoption.md")}\n${read("docs/adoption.zh-CN.md")}`;
103
150
  for (const phrase of [
104
- "codex plugin marketplace add echopath-labs/forgerail --ref v0.1.0-alpha.1",
151
+ `codex plugin marketplace add echopath-labs/forgerail --ref ${expectedTag}`,
105
152
  "codex plugin add forgerail@echopath-labs",
106
153
  "codex plugin add forgerail-cross-workspace-orchestration@echopath-labs",
107
154
  "codex plugin add forgerail-github-rulesets@echopath-labs",
108
155
  `${expectedPackageName}@${expectedVersion}`,
109
156
  "new Codex task",
110
157
  "adoption-plan --workspace . --host codex",
158
+ "adoption-plan --workspace . --selection all-detected",
159
+ "adoption-plan --workspace . --selection all-available",
111
160
  "Host Binding Receipt",
112
161
  ]) {
113
162
  record(`installation-${phrase.toLowerCase().replaceAll(/[^a-z0-9]+/g, "-")}`, installation.includes(phrase), phrase);
@@ -122,6 +171,9 @@ for (const path of [
122
171
  "adapters/cursor.json",
123
172
  "templates/FORGERAIL.md",
124
173
  "templates/bindings/codex-compact.md",
174
+ "templates/bindings/codex-thin.md",
175
+ "templates/bindings/claude-code-thin.md",
176
+ "templates/bindings/cursor-thin.mdc",
125
177
  "docs/adoption.md",
126
178
  "docs/adoption.zh-CN.md",
127
179
  ]) record(`adoption-path-${path.replaceAll(/[^a-z0-9]+/gi, "-").toLowerCase()}`, existsSync(resolve(root, path)), path);
@@ -129,15 +181,24 @@ for (const path of [
129
181
  const codexAdapter = json("adapters/codex.json");
130
182
  const claudeAdapter = json("adapters/claude-code.json");
131
183
  const cursorAdapter = json("adapters/cursor.json");
132
- record("codex-adapter-supported", codexAdapter.status === "supported" && codexAdapter.bindingTarget === "AGENTS.md", codexAdapter.status);
133
- record("claude-adapter-profile-only", claudeAdapter.status === "profile-only", claudeAdapter.status);
134
- record("cursor-adapter-profile-only", cursorAdapter.status === "profile-only", cursorAdapter.status);
184
+ record("codex-adapter-supported", codexAdapter.status === "supported" && codexAdapter.bindingTarget === "AGENTS.md" && codexAdapter.detectionTargets?.includes("AGENTS.md"), codexAdapter.status);
185
+ record("claude-adapter-profile-only", claudeAdapter.status === "profile-only" && claudeAdapter.detectionTargets?.includes("CLAUDE.md"), claudeAdapter.status);
186
+ record("claude-adapter-thin-only", JSON.stringify(claudeAdapter.bindingModes) === JSON.stringify(["thin-reference"]), claudeAdapter.bindingModes);
187
+ record("cursor-adapter-profile-only", cursorAdapter.status === "profile-only" && cursorAdapter.detectionTargets?.includes(".cursor"), cursorAdapter.status);
188
+ for (const adapter of [codexAdapter, claudeAdapter, cursorAdapter]) {
189
+ const modes = Object.keys(adapter.bindingTemplates ?? {}).sort();
190
+ record(`adapter-${adapter.id}-template-modes`, JSON.stringify(modes) === JSON.stringify([...adapter.bindingModes].sort()), { modes, bindingModes: adapter.bindingModes });
191
+ record(`adapter-${adapter.id}-all-host-thin-reference`, adapter.bindingModes.includes("thin-reference"), adapter.bindingModes);
192
+ for (const [mode, template] of Object.entries(adapter.bindingTemplates ?? {})) {
193
+ record(`adapter-${adapter.id}-${mode}-template`, existsSync(resolve(root, "templates", template)), template);
194
+ }
195
+ }
135
196
  record("package-adapters", packageJson.files?.includes("adapters/"), packageJson.files ?? null);
136
197
  record("package-templates", packageJson.files?.includes("templates/"), packageJson.files ?? null);
137
198
  record("no-apply-adoption-script", !read("scripts/forgerail.mjs").includes('command === "apply-adoption"'), "no apply-adoption command");
138
199
 
139
- const releaseEnglish = read("docs/release-alpha2.md");
140
- const releaseChinese = read("docs/release-alpha2.zh-CN.md");
200
+ const releaseEnglish = read("docs/release-alpha4.md");
201
+ const releaseChinese = read("docs/release-alpha4.zh-CN.md");
141
202
  const releaseDocs = `${releaseEnglish}\n${releaseChinese}`;
142
203
  for (const phrase of [
143
204
  "remote_integration_approval",
@@ -146,7 +207,7 @@ for (const phrase of [
146
207
  expectedVersion,
147
208
  expectedTag,
148
209
  "Node.js 22 and 24",
149
- "codex/forgerail-alpha2-scoped",
210
+ "codex/forgerail-alpha4-critical-integrity",
150
211
  "Do not unpublish",
151
212
  "AGW",
152
213
  "Host Binding Receipt",
@@ -179,8 +240,11 @@ record("runbook-no-fixed-external-pack-count", !releaseChinese.includes("三个
179
240
  const workflow = read(".github/workflows/plugin-contracts.yml");
180
241
  record("ci-node-22", workflow.includes("- 22"), "Node.js 22");
181
242
  record("ci-node-24", workflow.includes("- 24"), "Node.js 24");
243
+ record("ci-full-core", workflow.includes("run: npm test"), "npm test");
244
+ record("ci-integrity-regressions", workflow.includes("run: npm run test:integrity"), "npm run test:integrity");
182
245
  record("ci-release-source", workflow.includes("node scripts/validate-release.mjs"), "release source validator");
183
246
  record("ci-progressive-adoption", workflow.includes("node scripts/forgerail.mjs validate-adoption"), "progressive adoption validator");
247
+ record("ci-directory", workflow.includes("node scripts/validate-universal-directory.mjs"), "Universal Directory validator");
184
248
 
185
249
  const failures = checks.filter((check) => !check.passed);
186
250
  const report = {
@@ -64,11 +64,11 @@ function validateMarkdownLinks(relativePath) {
64
64
  }
65
65
 
66
66
  assert(candidate.submissionType === "skills_only", "candidate is Skills-only");
67
- assert(candidate.status === "local_alpha2_source_candidate", "candidate status is local alpha.2 source preparation");
67
+ assert(candidate.status === "local_alpha4_integrity_candidate", "candidate status is local alpha.4 integrity preparation");
68
68
  assert(candidate.approval.status === "not_granted", "submission approval is not granted");
69
69
  assert(candidate.plugin.id === manifest.name, "candidate and manifest Plugin identity match");
70
70
  assert(candidate.plugin.version === manifest.version, "candidate and manifest version match");
71
- assert(candidate.plugin.version === "0.1.0-alpha.2", "candidate version is alpha.2");
71
+ assert(candidate.plugin.version === "0.1.0-alpha.4", "candidate version is alpha.4");
72
72
  assert(packageJson.version === candidate.plugin.version, "optional scoped package version matches candidate");
73
73
  assert(candidate.plugin.mcpServers.length === 0, "candidate has no MCP server requirement");
74
74
  assert(candidate.plugin.authentication === "none", "candidate has no authentication requirement");
@@ -76,6 +76,18 @@ assert(candidate.plugin.coreRequirements.projectPackageJson === false, "Core req
76
76
  assert(candidate.plugin.coreRequirements.projectNodeModules === false, "Core requires no project node_modules");
77
77
  assert(candidate.plugin.coreRequirements.npmCli === "optional", "npm CLI is optional");
78
78
 
79
+ const expectedDefaultPrompts = [
80
+ "Use ForgeRail to govern this engineering task.",
81
+ "Diagnose this workspace before recommending governance changes.",
82
+ "Review workspace health or audit duplicated architecture ownership using the matching independent ForgeRail Skill.",
83
+ ];
84
+ assert(Array.isArray(manifest.interface.defaultPrompt), "defaultPrompt is an array");
85
+ assert(manifest.interface.defaultPrompt.length <= 3, "defaultPrompt stays within the Codex maximum of three");
86
+ assert(manifest.interface.defaultPrompt.length === expectedDefaultPrompts.length, "defaultPrompt exposes the three reviewed intents");
87
+ assert(new Set(manifest.interface.defaultPrompt).size === manifest.interface.defaultPrompt.length, "defaultPrompt entries are unique");
88
+ assert(manifest.interface.defaultPrompt.every((prompt) => typeof prompt === "string" && prompt.trim().length > 0), "defaultPrompt entries are non-empty strings");
89
+ assert(JSON.stringify(manifest.interface.defaultPrompt) === JSON.stringify(expectedDefaultPrompts), "defaultPrompt entries match the reviewed governance, diagnosis, and either-or review router");
90
+
79
91
  for (const skill of candidate.plugin.skills) {
80
92
  assert(existsSync(resolve(pluginRoot, "skills", skill, "SKILL.md")), `declared Skill exists: ${skill}`);
81
93
  }
@@ -96,8 +108,8 @@ for (const field of ["websiteUrl", "supportUrl", "privacyPolicyUrl", "termsOfSer
96
108
  }
97
109
  assert(candidate.availability.intent.state === "confirmed_by_user" && candidate.availability.intent.value === "all_platform_supported_regions", "all-platform-supported-regions intent is user-confirmed");
98
110
  assert(candidate.availability.portalEnumeration.state === "pending_confirmation" && candidate.availability.portalEnumeration.values.length === 0, "portal region enumeration remains pending without an invented country list");
99
- assert(candidate.releaseNotes.state === "candidate" && candidate.releaseNotes.path === "./directory/release-notes-alpha2.md", "alpha.2 release notes path is explicit");
100
- assert(existsSync(resolve(pluginRoot, candidate.releaseNotes.path)), "alpha.2 release notes file exists");
111
+ assert(candidate.releaseNotes.state === "candidate" && candidate.releaseNotes.path === "./directory/release-notes-alpha4.md", "alpha.4 release notes path is explicit");
112
+ assert(existsSync(resolve(pluginRoot, candidate.releaseNotes.path)), "alpha.4 release notes file exists");
101
113
 
102
114
  const privacy = readFileSync(resolve(pluginRoot, "PRIVACY.md"), "utf8");
103
115
  const terms = readFileSync(resolve(pluginRoot, "TERMS.md"), "utf8");
@@ -107,7 +119,7 @@ for (const phrase of ["Skills-only Agent Plugin", "does not operate its own serv
107
119
  for (const phrase of ["Apache License 2.0", "without warranties", "You are responsible", "external actions", "does not provide legal advice", "service-level agreement"]) {
108
120
  assert(terms.includes(phrase), `Terms contain required boundary: ${phrase}`);
109
121
  }
110
- for (const path of ["PRIVACY.md", "TERMS.md", "README.md", "README.zh-CN.md", "docs/installation.md", "docs/installation.zh-CN.md", "docs/release-alpha2.md", "docs/release-alpha2.zh-CN.md"]) {
122
+ for (const path of ["PRIVACY.md", "TERMS.md", "README.md", "README.zh-CN.md", "docs/installation.md", "docs/installation.zh-CN.md", "docs/release-alpha4.md", "docs/release-alpha4.zh-CN.md"]) {
111
123
  validateMarkdownLinks(path);
112
124
  }
113
125
 
@@ -6,8 +6,8 @@ Use the minimum level:
6
6
  2. `lightweight-adoption`: user-confirmed host instruction binding.
7
7
  3. `persisted-governance`: evidence-gated and deferred in alpha.1.
8
8
 
9
- For one host, propose one versioned managed block in its native target. For multiple hosts, propose `FORGERAIL.md` plus thin references. Host files are adapters, not Core sources.
9
+ For one host, propose one versioned managed block when its adapter supports that mode. For multiple hosts, or one thin-reference-only host, propose `FORGERAIL.md` plus thin references. Host files are adapters, not Core sources.
10
10
 
11
- Run `forgerail adoption-plan --workspace <path> --host <adapter>` when the deterministic CLI is available. Never infer permission to apply the returned writes. Display exact content and paths, obtain confirmation, verify base digests immediately before writing, preserve unrelated content, and produce a Host Binding Receipt.
11
+ Translate the user's natural-language host intent into one deterministic selection: repeated `--host <adapter>` for an explicit subset, `--selection all-detected` for registry adapters evidenced in the workspace, or `--selection all-available` for every adapter in the current registry. Omitting both options defaults to `all-detected`. Do not invent a host ID or instruction path; an unknown host needs a reviewed Host Adapter. Run `forgerail adoption-plan --workspace <path> ...` when the deterministic CLI is available. Never infer permission to apply the returned writes. Display the retained selection, exact content, paths and each write's `approvalSha256`, obtain confirmation, and preserve the approved digest separately from the mutable proposal. Node-based integrations must pass that retained digest as the third argument to `applyApprovedAdoptionWrite()` from `scripts/lib/adoption.mjs` to revalidate the canonical workspace identity and complete executable write metadata from one immutable snapshot, plus confinement, no-follow open, file identity and base digest at write time. Produce a Host Binding Receipt after verification.
12
12
 
13
13
  Codex is `supported` in alpha.1. Claude Code and Cursor are `profile-only`; do not close their binding as verified without a host-specific activation check.
@@ -16,7 +16,7 @@ Task authorization expires with the task. Never promote it into workspace policy
16
16
 
17
17
  ## Launch Contract
18
18
 
19
- Give the host Agent the Envelope plus the effective rule sources. Specify outcomes and boundaries, not unnecessary implementation steps.
19
+ Give the host Agent the resolved Envelope plus the effective rule sources. In the Launch Contract, `envelope.packs` is an identity-to-canonical-manifest-digest map rather than the Task Envelope's pre-resolution id list. This makes every requested Pack self-bound for schema-only consumers; `effectivePackManifests` retains the complete active Pack set. Specify outcomes and boundaries, not unnecessary implementation steps.
20
20
 
21
21
  ## Return Receipt
22
22
 
@@ -37,7 +37,7 @@ Compare the receipt with observable state. A mismatch keeps closeout incomplete.
37
37
  Record:
38
38
 
39
39
  - current and proposed adoption level;
40
- - explicitly selected Host Adapters and their support status;
40
+ - deterministically resolved Host Adapters, their selection mode, and their support status;
41
41
  - exact target paths, operations, base SHA-256 digests, candidate content, and content digests;
42
42
  - required user confirmation and activation verification;
43
43
  - confirmed non-mutations.
@@ -1,77 +0,0 @@
1
- import { createHash } from "node:crypto";
2
- import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync } from "node:fs";
3
- import { dirname, relative, resolve, sep } from "node:path";
4
-
5
- const roots = [".codex-plugin", ".github", "adapters", "contracts", "docs", "packs", "scripts", "skills", "templates"];
6
- const files = ["CHANGELOG.md", "CONTRIBUTING.md", "LICENSE", "NOTICE", "PLUGIN.md", "README.md", "README.zh-CN.md", "SECURITY.md", "package.json"];
7
- const catalog = "marketplace/.agents/plugins/marketplace.json";
8
- const externalPluginNames = [
9
- "forgerail-cross-workspace-orchestration",
10
- "forgerail-github-rulesets",
11
- "forgerail-release-safety",
12
- "forgerail-thread-closure",
13
- ];
14
-
15
- function below(base, prefix, result = []) {
16
- for (const entry of readdirSync(resolve(base, prefix), { withFileTypes: true })) {
17
- const path = `${prefix}/${entry.name}`;
18
- if (entry.isDirectory()) below(base, path, result);
19
- else if (entry.isFile()) result.push(path);
20
- else throw new Error(`unsupported entry: ${path}`);
21
- }
22
- return result;
23
- }
24
-
25
- export function buildBundle(root, output) {
26
- const target = resolve(output);
27
- const relativeToTmp = relative("/private/tmp", target);
28
- const relativeToSystemTmp = relative("/tmp", target);
29
- const safe = (value) => value !== "" && value !== ".." && !value.startsWith(`..${sep}`) && !value.startsWith("/");
30
- if (!safe(relativeToTmp) && !safe(relativeToSystemTmp)) throw new Error("output must be a new directory below /private/tmp or /tmp");
31
- if (existsSync(target)) throw new Error("output already exists");
32
- for (const required of [...roots, ...files, catalog]) if (!existsSync(resolve(root, required))) throw new Error(`public bundle source is missing: ${required}`);
33
- const payload = [...files, ...roots.flatMap((prefix) => below(root, prefix))].sort();
34
- const externalPlugins = externalPluginNames.map((name) => {
35
- const pluginRoot = resolve(root, `../${name}`);
36
- if (!existsSync(pluginRoot)) throw new Error(`external Plugin source is missing: ${name}`);
37
- return {
38
- name,
39
- root: pluginRoot,
40
- files: below(pluginRoot, ".").map((path) => path.startsWith("./") ? path.slice(2) : path).sort(),
41
- };
42
- });
43
- const inventory = [];
44
- const projections = [
45
- { source: catalog, target: ".agents/plugins/marketplace.json" },
46
- ...payload.flatMap((path) => [
47
- { source: path, target: path },
48
- { source: path, target: `plugins/forgerail/${path}` },
49
- ]),
50
- ...externalPlugins.flatMap((plugin) => plugin.files.map((path) => ({
51
- source: resolve(plugin.root, path),
52
- target: `plugins/${plugin.name}/${path}`,
53
- externalSource: `../${plugin.name}/${path}`,
54
- absolute: true,
55
- }))),
56
- ].sort((left, right) => left.target.localeCompare(right.target));
57
- for (const { source: path, target: publicPath, externalSource, absolute = false } of projections) {
58
- const source = absolute ? path : resolve(root, path);
59
- if (!statSync(source).isFile()) throw new Error(`bundle source is not a file: ${path}`);
60
- const destination = resolve(target, publicPath);
61
- mkdirSync(dirname(destination), { recursive: true });
62
- copyFileSync(source, destination);
63
- const bytes = readFileSync(source);
64
- inventory.push({ path: publicPath, source: absolute ? externalSource : path, bytes: bytes.length, sha256: createHash("sha256").update(bytes).digest("hex") });
65
- }
66
- const digest = createHash("sha256").update(`${JSON.stringify(inventory)}\n`).digest("hex");
67
- return {
68
- schemaVersion: "1.0",
69
- productId: "forgerail",
70
- projection: "marketplace-root-plus-nested-plugin",
71
- fileCount: inventory.length,
72
- totalBytes: inventory.reduce((sum, item) => sum + item.bytes, 0),
73
- digest,
74
- receiptDigest: createHash("sha256").update(`forgerail\n${digest}\n${inventory.length}\n`).digest("hex"),
75
- files: inventory,
76
- };
77
- }