create-agdf 0.13.5 → 0.13.6

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 (38) hide show
  1. package/README.md +7 -1
  2. package/generated/plugins/agdf/.claude-plugin/plugin.json +1 -1
  3. package/generated/plugins/agdf/.codex-plugin/plugin.json +2 -3
  4. package/generated/plugins/agdf/hooks/session-start.sh +31 -0
  5. package/generated/plugins/agdf/meta/agdf-plugin.definition.json +30 -9
  6. package/generated/plugins/agdf/runtime/agdf-local.js +8 -1
  7. package/generated/plugins/agdf/runtime/create-agdf/generated/plugins/agdf/meta/agdf-plugin.definition.json +30 -9
  8. package/generated/plugins/agdf/runtime/create-agdf/lib/runtime/local-validator.js +116 -31
  9. package/generated/plugins/agdf/runtime/create-agdf/lib/runtime/plugin-provenance.js +206 -0
  10. package/generated/plugins/agdf/runtime/create-agdf/package.json +1 -1
  11. package/generated/plugins/agdf/runtime/runtime-manifest.json +2 -2
  12. package/generated/plugins/agdf/scripts/check-runtime-integrity.mjs +64 -19
  13. package/generated/plugins/agdf/submission/openai/availability.md +1 -1
  14. package/generated/plugins/agdf/submission/openai/capability-matrix.json +1 -1
  15. package/generated/plugins/agdf/submission/openai/release-notes.md +1 -1
  16. package/generated/plugins/agdf/submission/openai/reviewer-cases.json +1 -1
  17. package/generated/submissions/openai/agdf/.codex-plugin/plugin.json +1 -1
  18. package/generated/submissions/openai/agdf/meta/agdf-plugin.definition.json +30 -9
  19. package/generated/submissions/openai/agdf/submission/openai/availability.md +1 -1
  20. package/generated/submissions/openai/agdf/submission/openai/capability-matrix.json +1 -1
  21. package/generated/submissions/openai/agdf/submission/openai/inventory.json +9 -9
  22. package/generated/submissions/openai/agdf/submission/openai/listing.json +1 -1
  23. package/generated/submissions/openai/agdf/submission/openai/readiness.json +2 -2
  24. package/generated/submissions/openai/agdf/submission/openai/readiness.md +2 -2
  25. package/generated/submissions/openai/agdf/submission/openai/release-notes.md +1 -1
  26. package/generated/submissions/openai/agdf/submission/openai/reviewer-cases.json +1 -1
  27. package/lib/cli/application.js +4 -3
  28. package/lib/fs-swap.js +26 -0
  29. package/lib/installers/local-development.js +202 -0
  30. package/lib/installers/local-marketplace.js +181 -36
  31. package/lib/installers/opencode.js +17 -2
  32. package/lib/installers/plugin-installers.js +86 -22
  33. package/lib/lifecycle/operations.js +9 -5
  34. package/lib/lifecycle/status.js +10 -2
  35. package/lib/public-plugin/builder.js +5 -4
  36. package/lib/runtime/local-validator.js +116 -31
  37. package/lib/runtime/plugin-provenance.js +206 -0
  38. package/package.json +6 -2
@@ -6,6 +6,7 @@ import process from "node:process";
6
6
  import { generatedRoot, pluginDefinition } from "../cli/runtime-context.js";
7
7
  import { evaluateOpenCodeRepositoryActivation } from "./opencode-activation.js";
8
8
  import { resolveLocalValidator } from "../runtime/local-validator.js";
9
+ import { validateLocalOpenCodePackageSource } from "./local-development.js";
9
10
 
10
11
  const contractModules = ["gate-transition.md", "interaction.md", "modes.md", "quality.md", "context-graph.md", "control-scaffold.md", "closeout.md"];
11
12
  const openCodeSkillNames = pluginDefinition.skillSet.map((skill) => pluginDefinition.opencode.skillPrefix + skill.slug);
@@ -36,7 +37,15 @@ export function defaultOpenCodeConfigDir() {
36
37
  return process.env.OPENCODE_CONFIG_DIR || join(homedir(), ".config", "opencode");
37
38
  }
38
39
 
39
- export function installOpenCodeGlobalPlugin(configDir) {
40
+ export function resolveOpenCodeInstallPackageSource(packageSource) {
41
+ const local = packageSource ? validateLocalOpenCodePackageSource(packageSource) : null;
42
+ return Object.freeze({
43
+ local,
44
+ specifier: local?.specifier ?? `${pluginDefinition.opencode.npmPackage}@${pluginDefinition.version}`,
45
+ });
46
+ }
47
+
48
+ export function installOpenCodeGlobalPlugin(configDir, dependencies = {}) {
40
49
  try {
41
50
  assertGlobalOpenCodeSurfaceWritable(configDir);
42
51
  } catch (error) {
@@ -97,8 +106,11 @@ export function installOpenCodeGlobalPlugin(configDir) {
97
106
  }
98
107
 
99
108
  mkdirSync(configDir, { recursive: true });
109
+ let localPackageSource = null;
100
110
  try {
101
- const packageSpecifier = `${pluginDefinition.opencode.npmPackage}@${pluginDefinition.version}`;
111
+ const packageSource = resolveOpenCodeInstallPackageSource(dependencies.packageSource);
112
+ localPackageSource = packageSource.local;
113
+ const packageSpecifier = packageSource.specifier;
102
114
  const invocation = openCodeNpmInvocation(["install", "--silent", "--save-prod", "--save-exact", packageSpecifier]);
103
115
  execFileSync(invocation.executable, invocation.args, {
104
116
  cwd: configDir,
@@ -123,6 +135,9 @@ export function installOpenCodeGlobalPlugin(configDir) {
123
135
  added: !alreadyInstalled,
124
136
  transition: openCodePackageTransition(previousPackage, installedPackage),
125
137
  sdk_alignment: sdkAlignment,
138
+ package_source: localPackageSource
139
+ ? { kind: "local_checkout", path: localPackageSource.tarball, digest: localPackageSource.digest, version: localPackageSource.version }
140
+ : { kind: "registry", path: "", digest: "", version: pluginDefinition.version },
126
141
  };
127
142
  }
128
143
 
@@ -1,25 +1,34 @@
1
1
  import { execFileSync } from "node:child_process";
2
2
  import { pluginDefinition } from "../cli/runtime-context.js";
3
- import { classifyMarketplaceList, prepareLocalMarketplace } from "./local-marketplace.js";
3
+ import { CODEX_REGISTRATION_REVISION, classifyMarketplaceList, inspectLocalMarketplaceProjection, isCodexLocalInstallVersion, prepareLocalMarketplace } from "./local-marketplace.js";
4
4
 
5
5
  export function installCodexGlobalPlugin({ exec = execFileSync, prepare = prepareLocalMarketplace, dataRoot } = {}) {
6
6
  const expectedVersion = pluginDefinition.version;
7
7
  const nativeOutput = [];
8
- const transaction = prepare({ expectedVersion, ...(dataRoot ? { dataRoot } : {}) });
9
- const migration = { state: "unknown", source: "", addedLocal: false, removedLegacy: false };
8
+ const transaction = prepare({ expectedVersion, codexRegistrationRevision: CODEX_REGISTRATION_REVISION, ...(dataRoot ? { dataRoot } : {}) });
9
+ const expectedInstallVersion = transaction.codexInstallVersion ?? expectedVersion;
10
+ const migration = { state: "unknown", source: "", addedLocal: false, removedLegacy: false, refreshOwnedLocal: false, removedOwnedLocal: false };
10
11
  try {
11
12
  const marketplaceOutput = runPluginPhase(exec, "codex", ["plugin", "marketplace", "list", "--json"], "marketplace", captureOptions());
12
- migrateMarketplace({ surface: "codex", exec, output: marketplaceOutput, root: transaction.root, nativeOutput, migration });
13
- nativeOutput.push(runPluginPhase(exec, "codex", ["plugin", "add", "agdf", "--marketplace", "agdf"], "plugin_operation", captureOptions()));
13
+ migrateMarketplace({ surface: "codex", exec, output: marketplaceOutput, root: transaction.root, nativeOutput, migration, refreshOwnedLocal: transaction.changed === true });
14
+ nativeOutput.push(runPluginPhase(exec, "codex", ["plugin", "add", "agdf@agdf", "--json"], "plugin_operation", captureOptions()));
14
15
  const listOutput = runPluginPhase(exec, "codex", ["plugin", "list"], "verification", captureOptions());
15
16
  const installedVersion = pluginVersionFromList(listOutput, "agdf@agdf");
16
- if (installedVersion !== expectedVersion) {
17
- throw lifecycleAdapterError("version", versionMismatchMessage("Codex", "agdf@agdf", expectedVersion, installedVersion, "npx --yes @agdf/cli@latest codex"));
17
+ if (installedVersion !== expectedInstallVersion) {
18
+ throw lifecycleAdapterError("version", versionMismatchMessage("Codex", "agdf@agdf", expectedInstallVersion, installedVersion, "npx --yes @agdf/cli@latest codex"));
18
19
  }
19
20
  transaction.commit();
20
21
  return {
21
- surface: "codex", operation: migration.state === "owned_local_current" ? "update" : "install", expectedVersion, installedVersion, verificationStatus: "healthy",
22
- evidence: ["durable_local_marketplace", `marketplace:${migration.state}`, "codex plugin list"],
22
+ surface: "codex", operation: migration.state === "owned_local_current" ? "update" : "install", expectedVersion: expectedInstallVersion, canonicalVersion: expectedVersion, installedVersion, verificationStatus: "healthy",
23
+ evidence: [
24
+ "durable_local_marketplace",
25
+ `marketplace:${migration.state}`,
26
+ "codex plugin list",
27
+ ...(transaction.pluginRoot ? [`staged_plugin_root:${transaction.pluginRoot}`, "staged_installation_provenance:matched"] : []),
28
+ ...(transaction.sourceDigest ? [`source_digest:${transaction.sourceDigest}`] : []),
29
+ ...(transaction.digest ? [`plugin_digest:${transaction.digest}`] : []),
30
+ ...(expectedInstallVersion === expectedVersion ? [] : [`canonical_version:${expectedVersion}`, `local_install_version:${expectedInstallVersion}`]),
31
+ ],
23
32
  nativeOutput: nativeOutput.filter(Boolean).map(String),
24
33
  };
25
34
  } catch (error) {
@@ -39,7 +48,12 @@ export function installClaudeGlobalPlugin({ exec = execFileSync, prepare = prepa
39
48
  nativeOutput.push(runPluginPhase(exec, "claude", ["plugin", "marketplace", "update", "agdf"], "marketplace", captureOptions()));
40
49
  const beforeList = runPluginPhase(exec, "claude", ["plugin", "list"], "verification", captureOptions());
41
50
  const alreadyInstalled = pluginListHasPlugin(beforeList, "agdf@agdf");
42
- nativeOutput.push(runPluginPhase(exec, "claude", ["plugin", alreadyInstalled ? "update" : "install", "agdf@agdf"], "plugin_operation", captureOptions()));
51
+ // `claude plugin update` keeps the cached copy when the version is unchanged, so a
52
+ // same-version local source change never reaches the host; reinstall replaces the content.
53
+ if (alreadyInstalled) {
54
+ nativeOutput.push(runPluginPhase(exec, "claude", ["plugin", "uninstall", "agdf@agdf"], "plugin_operation", captureOptions()));
55
+ }
56
+ nativeOutput.push(runPluginPhase(exec, "claude", ["plugin", "install", "agdf@agdf"], "plugin_operation", captureOptions()));
43
57
  const afterList = runPluginPhase(exec, "claude", ["plugin", "list"], "verification", captureOptions());
44
58
  const installedVersion = pluginVersionFromList(afterList, "agdf@agdf");
45
59
  if (installedVersion && installedVersion !== expectedVersion) {
@@ -52,7 +66,15 @@ export function installClaudeGlobalPlugin({ exec = execFileSync, prepare = prepa
52
66
  expectedVersion,
53
67
  installedVersion,
54
68
  verificationStatus: installedVersion ? "healthy" : "degraded",
55
- evidence: ["durable_local_marketplace", `marketplace:${migration.state}`, "claude plugin list", ...(installedVersion ? [] : ["host_did_not_expose_version"])],
69
+ evidence: [
70
+ "durable_local_marketplace",
71
+ `marketplace:${migration.state}`,
72
+ "claude plugin list",
73
+ ...(transaction.pluginRoot ? [`staged_plugin_root:${transaction.pluginRoot}`, "staged_installation_provenance:matched"] : []),
74
+ ...(transaction.sourceDigest ? [`source_digest:${transaction.sourceDigest}`] : []),
75
+ ...(transaction.digest ? [`plugin_digest:${transaction.digest}`] : []),
76
+ ...(installedVersion ? [] : ["host_did_not_expose_version"]),
77
+ ],
56
78
  nativeOutput: nativeOutput.filter(Boolean).map(String),
57
79
  };
58
80
  } catch (error) {
@@ -61,13 +83,21 @@ export function installClaudeGlobalPlugin({ exec = execFileSync, prepare = prepa
61
83
  }
62
84
  }
63
85
 
64
- function migrateMarketplace({ surface, exec, output, root, nativeOutput, migration }) {
86
+ function migrateMarketplace({ surface, exec, output, root, nativeOutput, migration, refreshOwnedLocal = false }) {
65
87
  const classification = classifyMarketplaceList(surface, output, root);
66
88
  Object.assign(migration, classification);
67
89
  if (["conflict", "unknown"].includes(classification.state)) {
68
90
  throw lifecycleAdapterError("marketplace", `Refusing to replace non-AGDF marketplace registration agdf (${classification.reason || classification.state}).`, classification);
69
91
  }
70
92
  const executable = surface === "claude" ? "claude" : "codex";
93
+ if (surface === "codex" && classification.state === "owned_local_current" && refreshOwnedLocal) {
94
+ migration.refreshOwnedLocal = true;
95
+ nativeOutput.push(runPluginPhase(exec, executable, ["plugin", "marketplace", "remove", "agdf", "--json"], "marketplace", captureOptions()));
96
+ migration.removedOwnedLocal = true;
97
+ nativeOutput.push(runPluginPhase(exec, executable, ["plugin", "marketplace", "add", root, "--json"], "marketplace", captureOptions()));
98
+ migration.addedLocal = true;
99
+ return;
100
+ }
71
101
  if (classification.state === "legacy_github") {
72
102
  const args = surface === "claude"
73
103
  ? ["plugin", "marketplace", "remove", "agdf", "--scope", "user"]
@@ -91,10 +121,23 @@ function recoverMarketplace({ surface, exec, migration, transaction, error }) {
91
121
  try {
92
122
  exec(executable, args, captureOptions());
93
123
  recovery.push({ args, status: "restored" });
124
+ return true;
94
125
  } catch (rollbackError) {
95
126
  recovery.push({ args, status: "failed", message: commandErrorText(rollbackError) });
127
+ return false;
96
128
  }
97
129
  };
130
+ if (migration?.refreshOwnedLocal) {
131
+ const refreshedRegistrationRemoved = migration.addedLocal
132
+ ? attempt(["plugin", "marketplace", "remove", "agdf", "--json"])
133
+ : true;
134
+ rollbackMarketplaceFilesystem(transaction, recovery);
135
+ if (migration.removedOwnedLocal && refreshedRegistrationRemoved) {
136
+ attempt(["plugin", "marketplace", "add", migration.source || transaction.root, "--json"]);
137
+ }
138
+ error.evidence = { ...(error.evidence ?? {}), rollback: recovery };
139
+ return;
140
+ }
98
141
  if (migration?.addedLocal) {
99
142
  attempt(surface === "claude"
100
143
  ? ["plugin", "marketplace", "remove", "agdf", "--scope", "user"]
@@ -105,32 +148,44 @@ function recoverMarketplace({ surface, exec, migration, transaction, error }) {
105
148
  ? ["plugin", "marketplace", "add", migration.source, "--scope", "user"]
106
149
  : ["plugin", "marketplace", "add", migration.source, "--json"]);
107
150
  }
151
+ rollbackMarketplaceFilesystem(transaction, recovery);
152
+ error.evidence = { ...(error.evidence ?? {}), rollback: recovery };
153
+ }
154
+
155
+ function rollbackMarketplaceFilesystem(transaction, recovery) {
108
156
  try {
109
157
  transaction.rollback();
110
158
  recovery.push({ filesystem: transaction.root, status: "restored" });
111
159
  } catch (rollbackError) {
112
160
  recovery.push({ filesystem: transaction.root, status: "failed", message: rollbackError.message });
113
161
  }
114
- error.evidence = { ...(error.evidence ?? {}), rollback: recovery };
115
162
  }
116
163
 
117
164
  function captureOptions() {
118
165
  return { encoding: "utf8", stdio: "pipe" };
119
166
  }
120
167
 
121
- export function inspectPluginSurface(surface, exec = execFileSync) {
168
+ export function inspectPluginSurface(surface, exec = execFileSync, options = {}) {
122
169
  const executable = surface === "claude" ? "claude" : "codex";
123
170
  const pluginId = "agdf@agdf";
124
171
  try {
125
172
  const output = exec(executable, ["plugin", "list"], { encoding: "utf8", stdio: "pipe" });
126
173
  const installed = pluginListHasPlugin(output, pluginId);
127
174
  const version = installed ? pluginVersionFromList(output, pluginId) : "";
175
+ let localDevelopmentVersion = false;
176
+ if (surface === "codex" && isCodexLocalInstallVersion(pluginDefinition.version, version)) {
177
+ try {
178
+ localDevelopmentVersion = inspectLocalMarketplaceProjection(options)?.codexInstallVersion === version;
179
+ } catch {
180
+ localDevelopmentVersion = false;
181
+ }
182
+ }
128
183
  return {
129
- status: !installed ? "not_installed" : version === pluginDefinition.version ? "healthy" : "degraded",
184
+ status: !installed ? "not_installed" : version === pluginDefinition.version || localDevelopmentVersion ? "healthy" : "degraded",
130
185
  surface,
131
186
  version: version || null,
132
187
  expected_version: pluginDefinition.version,
133
- evidence: [`${executable} plugin list`, ...(installed && !version ? ["host_did_not_expose_version"] : [])],
188
+ evidence: [`${executable} plugin list`, ...(localDevelopmentVersion ? [`canonical_version:${pluginDefinition.version}`, `local_install_version:${version}`] : []), ...(installed && !version ? ["host_did_not_expose_version"] : [])],
134
189
  };
135
190
  } catch (error) {
136
191
  return { status: "unknown", surface, version: null, expected_version: pluginDefinition.version, evidence: [commandErrorText(error)] };
@@ -167,14 +222,23 @@ export function pluginListHasPlugin(output, pluginId) {
167
222
  .some((line) => line.includes(pluginId));
168
223
  }
169
224
 
225
+ const VERSION_PATTERN = "(\\d+\\.\\d+\\.\\d+(?:[-+][0-9A-Za-z.-]+)?)";
226
+
170
227
  export function pluginVersionFromList(output, pluginId) {
171
228
  const escapedPluginId = pluginId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
172
- const line = output
173
- .split(/\r?\n/)
174
- .find((entry) => new RegExp(`(^|\\s)${escapedPluginId}(\\s|$)`).test(entry));
175
- if (!line) return "";
176
- const versionMatch = line.match(/\b(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)\b/);
177
- return versionMatch?.[1] ?? "";
229
+ const lines = output.split(/\r?\n/);
230
+ const entryIndex = lines.findIndex((entry) => new RegExp(`(^|\\s)${escapedPluginId}(\\s|$)`).test(entry));
231
+ if (entryIndex < 0) return "";
232
+ const sameLine = lines[entryIndex].match(new RegExp(`\\b${VERSION_PATTERN}\\b`));
233
+ if (sameLine) return sameLine[1];
234
+ // Current `claude plugin list` prints the plugin id and its `Version:` on separate
235
+ // lines; scan the entry's block until the next plugin id line.
236
+ for (let index = entryIndex + 1; index < lines.length; index += 1) {
237
+ if (/\S+@\S+/.test(lines[index])) break;
238
+ const blockMatch = lines[index].match(new RegExp(`\\bVersion:?\\s*${VERSION_PATTERN}\\b`, "i"));
239
+ if (blockMatch) return blockMatch[1];
240
+ }
241
+ return "";
178
242
  }
179
243
 
180
244
  function versionMismatchMessage(surface, pluginId, expectedVersion, installedVersion, correctiveCommand) {
@@ -4,6 +4,7 @@ import { dirname, join } from "node:path";
4
4
  import { pluginDefinition } from "../cli/runtime-context.js";
5
5
  import { evaluateOpenCodeStatus, openCodeNpmInvocation } from "../installers/opencode.js";
6
6
  import { inspectPluginSurface } from "../installers/plugin-installers.js";
7
+ import { inspectGeneratedRepositoryMarketplace } from "../runtime/plugin-provenance.js";
7
8
 
8
9
  const CODEX_DISABLE_MARKER = "# AGDF-OWNED-REPOSITORY-PLUGIN-STATE";
9
10
 
@@ -12,8 +13,10 @@ function pluginSection(content, selector) {
12
13
  return content.match(new RegExp(`\\[plugins\\.${escaped}\\][\\s\\S]*?(?=\\n\\[|$)`))?.[0] ?? "";
13
14
  }
14
15
 
15
- function repositorySelector(targetDir) {
16
- return existsSync(join(targetDir, ".agents", "plugins", "marketplace.json")) ? "agdf@agdf-repo" : "agdf@agdf";
16
+ function repositorySelector(targetDir, config = "") {
17
+ const repository = inspectGeneratedRepositoryMarketplace(targetDir);
18
+ if (repository.status === "matched") return repository.selector;
19
+ return pluginSection(config, "agdf@agdf-repo") ? "agdf@agdf-repo" : "agdf@agdf";
17
20
  }
18
21
 
19
22
  export function planRepositoryDisable(targetDir, surface) {
@@ -22,7 +25,7 @@ export function planRepositoryDisable(targetDir, surface) {
22
25
  }
23
26
  const path = join(targetDir, ".codex", "config.toml");
24
27
  const existing = existsSync(path) ? readFileSync(path, "utf8") : "";
25
- const selector = repositorySelector(targetDir);
28
+ const selector = repositorySelector(targetDir, existing);
26
29
  const escaped = JSON.stringify(selector).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
27
30
  const sectionPattern = new RegExp(`\\[plugins\\.${escaped}\\][\\s\\S]*?(?=\\n\\[|$)`);
28
31
  const currentSection = pluginSection(existing, selector);
@@ -49,8 +52,9 @@ export function planRepositoryDisable(targetDir, surface) {
49
52
  export function verifyRepositoryDisabled(targetDir) {
50
53
  const path = join(targetDir, ".codex", "config.toml");
51
54
  if (!existsSync(path)) return { status: "failed", evidence: [`missing:${path}`] };
52
- const selector = repositorySelector(targetDir);
53
- const section = pluginSection(readFileSync(path, "utf8"), selector);
55
+ const config = readFileSync(path, "utf8");
56
+ const selector = repositorySelector(targetDir, config);
57
+ const section = pluginSection(config, selector);
54
58
  const matches = [...section.matchAll(/^enabled\s*=\s*(true|false)\s*$/gm)];
55
59
  return matches.length === 1 && matches[0][1] === "false"
56
60
  ? { status: "healthy", evidence: [`${path}:${selector}:enabled=false`] }
@@ -5,6 +5,7 @@ import { evaluateDoctor } from "../control-evaluation/doctor.js";
5
5
  import { evaluateGateCheck } from "../control-evaluation/gate-check.js";
6
6
  import { evaluateOpenCodeStatus } from "../installers/opencode.js";
7
7
  import { inspectPluginSurface } from "../installers/plugin-installers.js";
8
+ import { inspectGeneratedRepositoryMarketplace } from "../runtime/plugin-provenance.js";
8
9
 
9
10
  function deliveryStatus(targetDir, selection, dependencies) {
10
11
  const controlDir = join(targetDir, ".agdf", "control");
@@ -29,7 +30,8 @@ function deliveryStatus(targetDir, selection, dependencies) {
29
30
 
30
31
  function repositoryStatus(targetDir, surface) {
31
32
  if (surface === "codex") {
32
- const marketplace = join(targetDir, ".agents", "plugins", "marketplace.json");
33
+ const repository = inspectGeneratedRepositoryMarketplace(targetDir);
34
+ const marketplace = repository.marketplacePath;
33
35
  const disabled = join(targetDir, ".codex", "config.toml");
34
36
  const config = existsSync(disabled) ? readFileSync(disabled, "utf8") : "";
35
37
  const isDisabled = ["agdf@agdf-repo", "agdf@agdf"].some((selector) => {
@@ -37,7 +39,11 @@ function repositoryStatus(targetDir, surface) {
37
39
  const section = config.match(new RegExp(`\\[plugins\\.${escaped}\\][\\s\\S]*?(?=\\n\\[|$)`))?.[0] ?? "";
38
40
  return /^enabled\s*=\s*false\s*$/m.test(section);
39
41
  });
40
- return { status: isDisabled ? "disabled" : existsSync(marketplace) ? "active" : "not_configured", scope: "repository", evidence: [marketplace] };
42
+ return {
43
+ status: isDisabled ? "disabled" : repository.status === "matched" ? "active" : repository.status === "invalid" ? "degraded" : "not_configured",
44
+ scope: "repository",
45
+ evidence: [marketplace, ...(repository.reason ? [repository.reason] : [])],
46
+ };
41
47
  }
42
48
  if (surface === "opencode") {
43
49
  const instructions = join(targetDir, ".opencode", pluginDefinition.opencode.instructionsFileName);
@@ -97,6 +103,8 @@ export function evaluateGeneralStatus(targetDir, options = {}, dependencies = {}
97
103
  ? `Run npx --yes @agdf/cli@latest ${surface} to install or repair AGDF.`
98
104
  : repository.status === "disabled"
99
105
  ? "Restart the host to apply the repository-local disable; the healthy global AGDF installation remains available."
106
+ : repository.status === "degraded"
107
+ ? "The repository marketplace is incomplete or invalid; use the supported repository scaffold or the healthy durable global AGDF installation."
100
108
  : repository.status === "not_configured"
101
109
  ? `Run npx --yes @agdf/cli@latest ${surface === "codex" ? "codex-repo" : `${surface}-repo`} in this repository.`
102
110
  : delivery.status === "blocked"
@@ -1,6 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
- import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
2
+ import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { dirname, join, resolve } from "node:path";
4
+ import { renameSyncWithRetry } from "../fs-swap.js";
4
5
  import { assertPublicPluginContract, loadJson } from "./contract.js";
5
6
  import { renderCodexPluginManifest } from "./manifest.js";
6
7
  import { createReadinessReport, renderReadinessReport } from "./report.js";
@@ -66,11 +67,11 @@ export function buildPublicPluginCandidate({ repoRoot, outputRoot }) {
66
67
  const backupRoot = `${outputRoot}.previous`;
67
68
  if (existsSync(backupRoot)) throw new Error(`Refusing candidate swap while recovery backup exists: ${backupRoot}`);
68
69
  const hadPrevious = existsSync(outputRoot);
69
- if (hadPrevious) renameSync(outputRoot, backupRoot);
70
+ if (hadPrevious) renameSyncWithRetry(outputRoot, backupRoot);
70
71
  try {
71
- renameSync(temporaryRoot, outputRoot);
72
+ renameSyncWithRetry(temporaryRoot, outputRoot);
72
73
  } catch (error) {
73
- if (hadPrevious && existsSync(backupRoot) && !existsSync(outputRoot)) renameSync(backupRoot, outputRoot);
74
+ if (hadPrevious && existsSync(backupRoot) && !existsSync(outputRoot)) renameSyncWithRetry(backupRoot, outputRoot);
74
75
  throw error;
75
76
  }
76
77
  if (hadPrevious) rmSync(backupRoot, { recursive: true, force: true });
@@ -1,8 +1,14 @@
1
- import { createHash } from "node:crypto";
2
1
  import { spawnSync } from "node:child_process";
3
- import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
2
+ import { existsSync, readFileSync, realpathSync } from "node:fs";
4
3
  import { isAbsolute, join, relative, resolve, sep } from "node:path";
5
4
  import process from "node:process";
5
+ import {
6
+ INSTALLATION_PROVENANCE_FILE,
7
+ digestDirectory,
8
+ inspectGeneratedRepositoryMarketplace,
9
+ inspectInstallationProvenance,
10
+ validateDistributionProfiles,
11
+ } from "./plugin-provenance.js";
6
12
 
7
13
  function readJson(path) {
8
14
  try {
@@ -12,27 +18,12 @@ function readJson(path) {
12
18
  }
13
19
  }
14
20
 
15
- export function digestDirectory(root) {
16
- const files = [];
17
- function visit(directory) {
18
- for (const name of readdirSync(directory).sort()) {
19
- const path = join(directory, name);
20
- const stats = statSync(path);
21
- if (stats.isDirectory()) visit(path);
22
- else if (stats.isFile()) files.push(path);
23
- }
24
- }
25
- visit(root);
26
- const hash = createHash("sha256");
27
- for (const path of files) {
28
- hash.update(relative(root, path).replaceAll("\\", "/"));
29
- hash.update("\0");
30
- hash.update(readFileSync(path));
31
- hash.update("\0");
32
- }
33
- return hash.digest("hex");
21
+ function canonicalPath(path) {
22
+ try { return realpathSync(path); } catch { return resolve(path); }
34
23
  }
35
24
 
25
+ export { digestDirectory };
26
+
36
27
  function envelope(machineValidation, options, evidence = {}) {
37
28
  return {
38
29
  schema_version: "1",
@@ -42,6 +33,14 @@ function envelope(machineValidation, options, evidence = {}) {
42
33
  observed_version: evidence.observedVersion ?? null,
43
34
  source: evidence.source ?? null,
44
35
  registry_access: false,
36
+ distribution_profile: evidence.distributionProfile ?? null,
37
+ evidence_plane: evidence.evidencePlane ?? null,
38
+ canonical_version: evidence.canonicalVersion ?? options.expectedVersion ?? null,
39
+ plugin_version: evidence.pluginVersion ?? null,
40
+ runtime_digest: evidence.runtimeDigest ?? null,
41
+ source_digest: evidence.sourceDigest ?? null,
42
+ plugin_root: evidence.pluginRoot ?? null,
43
+ provenance_status: evidence.provenanceStatus ?? null,
45
44
  ...(evidence.reason ? { reason: evidence.reason } : {}),
46
45
  };
47
46
  }
@@ -51,17 +50,17 @@ export function resolveLocalValidator(options) {
51
50
  const packageManifest = readJson(join(options.ownedPackageRoot, "package.json"));
52
51
  const observedVersion = packageManifest?.version ?? null;
53
52
  if (!packageManifest) {
54
- return { envelope: envelope("unavailable", options, { source: "config_local_package", reason: "package_missing" }) };
53
+ return { envelope: envelope("unavailable", options, { source: "config_local_package", evidencePlane: "opencode_config_local", distributionProfile: "opencode-config-local", reason: "package_missing" }) };
55
54
  }
56
55
  if (observedVersion !== options.expectedVersion) {
57
- return { envelope: envelope("version_mismatch", options, { observedVersion, source: "config_local_package" }) };
56
+ return { envelope: envelope("version_mismatch", options, { observedVersion, source: "config_local_package", evidencePlane: "opencode_config_local", distributionProfile: "opencode-config-local" }) };
58
57
  }
59
58
  const entrypoint = join(options.ownedPackageRoot, "bin", "create-agdf.js");
60
59
  if (!existsSync(entrypoint)) {
61
- return { envelope: envelope("unavailable", options, { observedVersion, source: "config_local_package", reason: "entrypoint_missing" }) };
60
+ return { envelope: envelope("unavailable", options, { observedVersion, source: "config_local_package", evidencePlane: "opencode_config_local", distributionProfile: "opencode-config-local", reason: "entrypoint_missing" }) };
62
61
  }
63
62
  return {
64
- envelope: envelope("owned_version_matched", options, { observedVersion, source: "config_local_package" }),
63
+ envelope: envelope("owned_version_matched", options, { observedVersion, source: "config_local_package", evidencePlane: "opencode_config_local", distributionProfile: "opencode-config-local", provenanceStatus: "matched" }),
65
64
  executable: process.execPath,
66
65
  prefixArgs: [entrypoint],
67
66
  };
@@ -73,30 +72,116 @@ export function resolveLocalValidator(options) {
73
72
  return { envelope: envelope("version_mismatch", options, { source: "plugin_bundle", reason: "manifest_invalid" }) };
74
73
  }
75
74
  if (manifest) {
75
+ const pluginRoot = canonicalPath(options.pluginRoot ?? join(options.runtimeRoot, ".."));
76
+ if (options.expectedPluginRoot && canonicalPath(options.expectedPluginRoot) !== pluginRoot) {
77
+ return { envelope: envelope("version_mismatch", options, { source: "plugin_bundle", pluginRoot, evidencePlane: "loaded_session", distributionProfile: "runtime-plugin", provenanceStatus: "mismatch", reason: "plugin_root_mismatch" }) };
78
+ }
79
+ const definition = readJson(join(pluginRoot, "meta", "agdf-plugin.definition.json"));
80
+ const profile = validateDistributionProfiles(definition);
81
+ if (profile.status !== "matched") {
82
+ return { envelope: envelope("version_mismatch", options, { source: "plugin_bundle", pluginRoot, distributionProfile: "runtime-plugin", provenanceStatus: "invalid", reason: "profile_invalid" }) };
83
+ }
84
+ const codexManifest = readJson(join(pluginRoot, ".codex-plugin", "plugin.json"));
85
+ const claudeManifest = readJson(join(pluginRoot, ".claude-plugin", "plugin.json"));
86
+ const pluginVersion = codexManifest?.version ?? null;
87
+ if (!codexManifest || !claudeManifest || claudeManifest.version !== definition.version) {
88
+ return { envelope: envelope("version_mismatch", options, { source: "plugin_bundle", pluginRoot, pluginVersion, canonicalVersion: definition.version, distributionProfile: "runtime-plugin", provenanceStatus: "invalid", reason: "manifest_invalid" }) };
89
+ }
76
90
  const packageRoot = join(options.runtimeRoot, "create-agdf");
77
91
  if (!existsSync(packageRoot)) {
78
- return { envelope: envelope("unavailable", options, { source: "plugin_bundle", reason: "package_missing" }) };
92
+ return { envelope: envelope("unavailable", options, { source: "plugin_bundle", pluginRoot, pluginVersion, distributionProfile: "runtime-plugin", reason: "runtime_missing" }) };
79
93
  }
80
94
  const packageManifest = readJson(join(packageRoot, "package.json"));
81
95
  const observedVersion = packageManifest?.version ?? manifest.version ?? null;
82
96
  if (observedVersion !== options.expectedVersion || manifest.version !== options.expectedVersion) {
83
- return { envelope: envelope("version_mismatch", options, { observedVersion, source: "plugin_bundle" }) };
97
+ return { envelope: envelope("version_mismatch", options, { observedVersion, source: "plugin_bundle", pluginRoot, pluginVersion, canonicalVersion: definition.version, distributionProfile: "runtime-plugin" }) };
84
98
  }
85
99
  let observedDigest = null;
86
100
  try { observedDigest = digestDirectory(packageRoot); } catch {}
87
101
  if (!observedDigest || manifest.digest !== observedDigest) {
88
- return { envelope: envelope("version_mismatch", options, { observedVersion, source: "plugin_bundle", reason: "digest_mismatch" }) };
102
+ return { envelope: envelope("version_mismatch", options, { observedVersion, source: "plugin_bundle", pluginRoot, pluginVersion, canonicalVersion: definition.version, runtimeDigest: observedDigest, distributionProfile: "runtime-plugin", reason: "runtime_digest_mismatch" }) };
89
103
  }
90
104
  const entrypoint = resolve(options.runtimeRoot, manifest.entrypoint ?? "");
91
105
  const entrypointRelative = relative(options.runtimeRoot, entrypoint);
92
106
  if (!manifest.entrypoint || entrypointRelative === ".." || entrypointRelative.startsWith(`..${sep}`) || isAbsolute(entrypointRelative)) {
93
- return { envelope: envelope("version_mismatch", options, { observedVersion, source: "plugin_bundle", reason: "invalid_entrypoint" }) };
107
+ return { envelope: envelope("version_mismatch", options, { observedVersion, source: "plugin_bundle", pluginRoot, pluginVersion, canonicalVersion: definition.version, runtimeDigest: observedDigest, distributionProfile: "runtime-plugin", reason: "invalid_entrypoint" }) };
94
108
  }
95
109
  if (!existsSync(entrypoint)) {
96
- return { envelope: envelope("unavailable", options, { observedVersion, source: "plugin_bundle", reason: "entrypoint_missing" }) };
110
+ return { envelope: envelope("unavailable", options, { observedVersion, source: "plugin_bundle", pluginRoot, pluginVersion, canonicalVersion: definition.version, runtimeDigest: observedDigest, distributionProfile: "runtime-plugin", reason: "entrypoint_missing" }) };
111
+ }
112
+ const installed = existsSync(join(pluginRoot, INSTALLATION_PROVENANCE_FILE));
113
+ let provenanceStatus = "not_applicable";
114
+ let sourceDigest = null;
115
+ let evidencePlane = "loaded_session";
116
+ if (installed) {
117
+ const provenance = inspectInstallationProvenance(pluginRoot, {
118
+ definition,
119
+ runtimeManifest: manifest,
120
+ pluginVersion,
121
+ });
122
+ provenanceStatus = provenance.status;
123
+ sourceDigest = provenance.marker?.source_digest ?? null;
124
+ if (provenance.status !== "matched") {
125
+ return { envelope: envelope("version_mismatch", options, {
126
+ observedVersion,
127
+ source: "plugin_bundle",
128
+ pluginRoot,
129
+ pluginVersion,
130
+ canonicalVersion: definition.version,
131
+ runtimeDigest: observedDigest,
132
+ sourceDigest,
133
+ distributionProfile: "runtime-plugin",
134
+ evidencePlane: "loaded_session",
135
+ provenanceStatus,
136
+ reason: provenance.reason,
137
+ }) };
138
+ }
139
+ } else if (existsSync(join(pluginRoot, ".agdf-local-install.json"))) {
140
+ return { envelope: envelope("unavailable", options, {
141
+ observedVersion,
142
+ source: "plugin_bundle",
143
+ pluginRoot,
144
+ pluginVersion,
145
+ canonicalVersion: definition.version,
146
+ runtimeDigest: observedDigest,
147
+ distributionProfile: "runtime-plugin",
148
+ evidencePlane: "loaded_session",
149
+ provenanceStatus: "missing",
150
+ reason: "installation_provenance_missing",
151
+ }) };
152
+ } else {
153
+ const generatedRepository = inspectGeneratedRepositoryMarketplace(resolve(pluginRoot, "..", ".."));
154
+ if (pluginVersion !== definition.version
155
+ || generatedRepository.status !== "matched"
156
+ || canonicalPath(generatedRepository.pluginRoot) !== pluginRoot) {
157
+ return { envelope: envelope("unavailable", options, {
158
+ observedVersion,
159
+ source: "plugin_bundle",
160
+ pluginRoot,
161
+ pluginVersion,
162
+ canonicalVersion: definition.version,
163
+ runtimeDigest: observedDigest,
164
+ distributionProfile: "runtime-plugin",
165
+ evidencePlane: "installed_plugin_root",
166
+ provenanceStatus: "missing",
167
+ reason: "installation_provenance_missing",
168
+ }) };
169
+ }
170
+ evidencePlane = "generated_bundle";
97
171
  }
98
172
  return {
99
- envelope: envelope("owned_version_matched", options, { observedVersion, source: "plugin_bundle" }),
173
+ envelope: envelope("owned_version_matched", options, {
174
+ observedVersion,
175
+ source: "plugin_bundle",
176
+ pluginRoot,
177
+ pluginVersion,
178
+ canonicalVersion: definition.version,
179
+ runtimeDigest: observedDigest,
180
+ sourceDigest,
181
+ distributionProfile: "runtime-plugin",
182
+ evidencePlane,
183
+ provenanceStatus,
184
+ }),
100
185
  executable: process.execPath,
101
186
  prefixArgs: [entrypoint],
102
187
  };