create-agdf 0.14.3 → 0.14.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 (80) hide show
  1. package/README.md +34 -2
  2. package/generated/.opencode/contracts/interaction.md +2 -2
  3. package/generated/.opencode/skills/agdf-gate-check/SKILL.md +6 -4
  4. package/generated/plugins/agdf/.claude-plugin/plugin.json +1 -1
  5. package/generated/plugins/agdf/.codex-plugin/plugin.json +1 -1
  6. package/generated/plugins/agdf/meta/agdf-plugin.definition.json +1 -1
  7. package/generated/plugins/agdf/meta/contracts/interaction.md +2 -2
  8. package/generated/plugins/agdf/meta/copilot-payload-baseline.json +3 -3
  9. package/generated/plugins/agdf/meta/distribution-profile-history.json +125 -0
  10. package/generated/plugins/agdf/runtime/create-agdf/generated/plugins/agdf/meta/agdf-plugin.definition.json +1 -1
  11. package/generated/plugins/agdf/runtime/create-agdf/lib/cli/command-registry.js +8 -1
  12. package/generated/plugins/agdf/runtime/create-agdf/lib/cli/parse-args.js +3 -0
  13. package/generated/plugins/agdf/runtime/create-agdf/lib/control-evaluation/doctor.js +13 -0
  14. package/generated/plugins/agdf/runtime/create-agdf/lib/control-evaluation/gate-check.js +64 -13
  15. package/generated/plugins/agdf/runtime/create-agdf/lib/control-evaluation/run-state.js +6 -0
  16. package/generated/plugins/agdf/runtime/create-agdf/lib/control-state/run-identity.js +9 -0
  17. package/generated/plugins/agdf/runtime/create-agdf/lib/control-state/run-state-parser.js +4 -2
  18. package/generated/plugins/agdf/runtime/create-agdf/lib/interaction-presentation.js +57 -7
  19. package/generated/plugins/agdf/runtime/create-agdf/lib/runtime/plugin-provenance.js +46 -3
  20. package/generated/plugins/agdf/runtime/create-agdf/package.json +1 -1
  21. package/generated/plugins/agdf/runtime/runtime-manifest.json +2 -2
  22. package/generated/plugins/agdf/scripts/check-runtime-integrity.mjs +2 -0
  23. package/generated/plugins/agdf/skills/gate-check/SKILL.md +6 -4
  24. package/generated/plugins/agdf/submission/openai/availability.md +1 -1
  25. package/generated/plugins/agdf/submission/openai/capability-matrix.json +1 -1
  26. package/generated/plugins/agdf/submission/openai/release-notes.md +1 -1
  27. package/generated/plugins/agdf/submission/openai/reviewer-cases.json +1 -1
  28. package/generated/plugins/copilot/agdf/.agdf-payload-inventory.json +55 -35
  29. package/generated/plugins/copilot/agdf/copilot-skills/agdf-gate-check/SKILL.md +6 -4
  30. package/generated/plugins/copilot/agdf/copilot-skills/contracts/interaction.md +2 -2
  31. package/generated/plugins/copilot/agdf/meta/agdf-plugin.definition.json +1 -1
  32. package/generated/plugins/copilot/agdf/meta/distribution-profile-history.json +125 -0
  33. package/generated/plugins/copilot/agdf/plugin.json +1 -1
  34. package/generated/plugins/copilot/agdf/runtime/create-agdf/generated/plugins/agdf/meta/agdf-plugin.definition.json +1 -1
  35. package/generated/plugins/copilot/agdf/runtime/create-agdf/lib/cli/command-registry.js +8 -1
  36. package/generated/plugins/copilot/agdf/runtime/create-agdf/lib/cli/parse-args.js +3 -0
  37. package/generated/plugins/copilot/agdf/runtime/create-agdf/lib/control-evaluation/doctor.js +13 -0
  38. package/generated/plugins/copilot/agdf/runtime/create-agdf/lib/control-evaluation/gate-check.js +64 -13
  39. package/generated/plugins/copilot/agdf/runtime/create-agdf/lib/control-evaluation/run-state.js +6 -0
  40. package/generated/plugins/copilot/agdf/runtime/create-agdf/lib/control-state/run-identity.js +9 -0
  41. package/generated/plugins/copilot/agdf/runtime/create-agdf/lib/control-state/run-state-parser.js +4 -2
  42. package/generated/plugins/copilot/agdf/runtime/create-agdf/lib/interaction-presentation.js +57 -7
  43. package/generated/plugins/copilot/agdf/runtime/create-agdf/lib/runtime/plugin-provenance.js +46 -3
  44. package/generated/plugins/copilot/agdf/runtime/create-agdf/package.json +1 -1
  45. package/generated/plugins/copilot/agdf/runtime/runtime-manifest.json +2 -2
  46. package/generated/submissions/openai/agdf/.codex-plugin/plugin.json +1 -1
  47. package/generated/submissions/openai/agdf/meta/agdf-plugin.definition.json +1 -1
  48. package/generated/submissions/openai/agdf/meta/contracts/interaction.md +2 -2
  49. package/generated/submissions/openai/agdf/meta/copilot-payload-baseline.json +3 -3
  50. package/generated/submissions/openai/agdf/meta/distribution-profile-history.json +125 -0
  51. package/generated/submissions/openai/agdf/skills/gate-check/SKILL.md +6 -4
  52. package/generated/submissions/openai/agdf/submission/openai/availability.md +1 -1
  53. package/generated/submissions/openai/agdf/submission/openai/capability-matrix.json +1 -1
  54. package/generated/submissions/openai/agdf/submission/openai/inventory.json +16 -12
  55. package/generated/submissions/openai/agdf/submission/openai/listing.json +1 -1
  56. package/generated/submissions/openai/agdf/submission/openai/readiness.json +2 -2
  57. package/generated/submissions/openai/agdf/submission/openai/readiness.md +2 -2
  58. package/generated/submissions/openai/agdf/submission/openai/release-notes.md +1 -1
  59. package/generated/submissions/openai/agdf/submission/openai/reviewer-cases.json +1 -1
  60. package/lib/cli/application.js +24 -6
  61. package/lib/cli/command-registry.js +8 -1
  62. package/lib/cli/parse-args.js +3 -0
  63. package/lib/control-evaluation/doctor.js +13 -0
  64. package/lib/control-evaluation/gate-check.js +64 -13
  65. package/lib/control-evaluation/run-state.js +6 -0
  66. package/lib/control-state/run-identity.js +9 -0
  67. package/lib/control-state/run-state-parser.js +4 -2
  68. package/lib/installers/claude-cache-recovery.js +76 -0
  69. package/lib/installers/copilot-settings.js +146 -16
  70. package/lib/installers/local-marketplace.js +169 -14
  71. package/lib/installers/plugin-installers.js +96 -9
  72. package/lib/interaction-presentation.js +57 -7
  73. package/lib/lifecycle/operations.js +28 -4
  74. package/lib/lifecycle/result.js +4 -4
  75. package/lib/release/profile-history.js +222 -0
  76. package/lib/release/version-bump.js +527 -0
  77. package/lib/release/version-coherence.js +92 -38
  78. package/lib/runtime/distribution-profile-history.js +173 -0
  79. package/lib/runtime/plugin-provenance.js +46 -3
  80. package/package.json +7 -3
@@ -0,0 +1,76 @@
1
+ import { existsSync, lstatSync, realpathSync, rmSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { win32 } from "node:path";
4
+ import process from "node:process";
5
+
6
+ function result(status, reason, removedPath = null, evidence = []) {
7
+ return Object.freeze({ status, reason, removed_path: removedPath, evidence });
8
+ }
9
+
10
+ function normalized(path) {
11
+ return win32.resolve(path).toLowerCase();
12
+ }
13
+
14
+ function parseRenameOperands(text) {
15
+ const matches = [...text.matchAll(/\brename\s+['"]([^'"]+)['"]\s*->\s*['"]([^'"]+)['"]/gi)];
16
+ return matches.length === 1 ? matches[0].slice(1, 3) : null;
17
+ }
18
+
19
+ export function recoverClaudeCacheTemp({
20
+ error,
21
+ expectedVersion,
22
+ platform = process.platform,
23
+ env = process.env,
24
+ home = homedir(),
25
+ fs = { existsSync, lstatSync, realpathSync, rmSync },
26
+ } = {}) {
27
+ const text = String(error?.message ?? error ?? "");
28
+ if (platform !== "win32" || !/\bEPERM\b/i.test(text) || !/\brename\b/i.test(text)) {
29
+ return result("not_applicable", "claude_cache_temp_recovery_not_applicable");
30
+ }
31
+ const operands = parseRenameOperands(text);
32
+ if (!operands || typeof expectedVersion !== "string" || !/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(expectedVersion)) {
33
+ return result("unsafe", "claude_cache_temp_recovery_unsafe");
34
+ }
35
+ const configRoot = win32.resolve(env.CLAUDE_CONFIG_DIR || win32.join(home, ".claude"));
36
+ const namespace = win32.join(configRoot, "plugins", "cache", "agdf", "agdf");
37
+ const absoluteOperands = operands.filter((path) => win32.isAbsolute(path));
38
+ if (absoluteOperands.length !== 2
39
+ || absoluteOperands.some((path) => normalized(win32.dirname(path)) !== normalized(namespace))) {
40
+ return result("unsafe", "claude_cache_temp_recovery_unsafe");
41
+ }
42
+ const [candidate, destination] = absoluteOperands;
43
+ if (!/^temp_local_[0-9A-Za-z._-]+$/.test(win32.basename(candidate))
44
+ || !fs.existsSync(candidate)) {
45
+ return result("unsafe", "claude_cache_temp_recovery_unsafe");
46
+ }
47
+ if (win32.basename(destination) !== expectedVersion || fs.existsSync(destination)) {
48
+ return result("unsafe", "claude_cache_temp_recovery_unsafe");
49
+ }
50
+ let candidateStats;
51
+ let realNamespace;
52
+ let realCandidate;
53
+ try {
54
+ candidateStats = fs.lstatSync(candidate);
55
+ realNamespace = fs.realpathSync(namespace);
56
+ realCandidate = fs.realpathSync(candidate);
57
+ } catch {
58
+ return result("unsafe", "claude_cache_temp_recovery_unsafe");
59
+ }
60
+ if (!candidateStats.isDirectory() || candidateStats.isSymbolicLink()
61
+ || normalized(win32.dirname(realCandidate)) !== normalized(realNamespace)
62
+ || normalized(realCandidate) !== normalized(candidate)) {
63
+ return result("unsafe", "claude_cache_temp_recovery_unsafe");
64
+ }
65
+ try {
66
+ fs.rmSync(candidate, { recursive: true, force: false });
67
+ } catch {
68
+ return result("unsafe", "claude_cache_temp_recovery_unsafe");
69
+ }
70
+ return result(
71
+ "recovered",
72
+ "claude_cache_temp_recovery_bounded_retry",
73
+ candidate,
74
+ [`cache_entry:${win32.basename(candidate)}`, `target_version:${expectedVersion}`],
75
+ );
76
+ }
@@ -1,49 +1,179 @@
1
+ import { execFileSync } from "node:child_process";
1
2
  import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
3
  import { homedir } from "node:os";
3
- import { dirname, isAbsolute, join, resolve } from "node:path";
4
+ import { dirname, isAbsolute, join, relative, resolve } from "node:path";
4
5
  import process from "node:process";
5
6
  import { renameSyncWithRetry } from "../fs-swap.js";
6
7
 
8
+ const COPILOT_PLUGIN_SELECTOR = "agdf@agdf";
9
+ const COPILOT_LOCAL_SETTINGS = join(".github", "copilot", "settings.local.json");
10
+ const COPILOT_SHARED_SETTINGS = join(".github", "copilot", "settings.json");
11
+
7
12
  export function defaultCopilotSettingsPath({ env = process.env, home = homedir() } = {}) {
8
13
  return join(resolve(env.COPILOT_HOME || join(home, ".copilot")), "settings.json");
9
14
  }
10
15
 
11
16
  export function readCopilotSettings(path = defaultCopilotSettingsPath()) {
12
- if (!existsSync(path)) return { exists: false, settings: {} };
17
+ if (!existsSync(path)) return { exists: false, settings: {}, content: null };
13
18
  const stats = lstatSync(path);
14
19
  if (!stats.isFile() || stats.isSymbolicLink()) throw new Error("AGDF_COPILOT_SETTINGS_UNOWNED_PATH");
15
20
  try {
16
- const settings = JSON.parse(readFileSync(path, "utf8"));
21
+ const content = readFileSync(path, "utf8");
22
+ const settings = JSON.parse(content);
17
23
  if (!settings || typeof settings !== "object" || Array.isArray(settings)) throw new Error("not an object");
18
24
  if (settings.enabledPlugins !== undefined
19
25
  && (!settings.enabledPlugins || typeof settings.enabledPlugins !== "object" || Array.isArray(settings.enabledPlugins))) {
20
26
  throw new Error("enabledPlugins must be an object");
21
27
  }
22
- return { exists: true, settings };
28
+ return { exists: true, settings, content };
23
29
  } catch (error) {
24
30
  throw new Error(`AGDF_COPILOT_SETTINGS_INVALID: ${error.message}`);
25
31
  }
26
32
  }
27
33
 
28
- function atomicSettingsWrite(path, settings) {
29
- mkdirSync(dirname(path), { recursive: true });
34
+ function atomicContentWrite(path, content, dependencies = {}) {
35
+ const makeDirectory = dependencies.mkdir ?? mkdirSync;
36
+ const write = dependencies.write ?? writeFileSync;
37
+ const rename = dependencies.rename ?? renameSyncWithRetry;
38
+ const remove = dependencies.remove ?? rmSync;
39
+ const changeMode = dependencies.chmod ?? chmodSync;
40
+ const exists = dependencies.exists ?? existsSync;
41
+ const stat = dependencies.lstat ?? lstatSync;
42
+ makeDirectory(dirname(path), { recursive: true });
30
43
  const temp = `${path}.agdf-tmp-${process.pid}`;
31
44
  const backup = `${path}.agdf-backup-${process.pid}`;
32
- const hadPrevious = existsSync(path);
33
- const mode = hadPrevious ? lstatSync(path).mode & 0o777 : 0o600;
34
- writeFileSync(temp, `${JSON.stringify(settings, null, 2)}\n`, { encoding: "utf8", flag: "wx", mode });
35
- if (process.platform !== "win32") chmodSync(temp, mode);
45
+ const hadPrevious = exists(path);
46
+ const mode = hadPrevious ? stat(path).mode & 0o777 : 0o600;
47
+ write(temp, content, { encoding: "utf8", flag: "wx", mode });
48
+ if (process.platform !== "win32") changeMode(temp, mode);
36
49
  try {
37
- if (hadPrevious && process.platform === "win32") renameSyncWithRetry(path, backup);
38
- renameSyncWithRetry(temp, path);
39
- if (hadPrevious && process.platform === "win32") rmSync(backup, { force: true });
50
+ if (hadPrevious && process.platform === "win32") rename(path, backup);
51
+ rename(temp, path);
52
+ if (hadPrevious && process.platform === "win32") remove(backup, { force: true });
40
53
  } catch (error) {
41
- try { rmSync(temp, { force: true }); } catch {}
42
- if (hadPrevious && process.platform === "win32" && existsSync(backup) && !existsSync(path)) renameSyncWithRetry(backup, path);
54
+ try { remove(temp, { force: true }); } catch {}
55
+ if (hadPrevious && process.platform === "win32" && exists(backup) && !exists(path)) rename(backup, path);
43
56
  throw error;
44
57
  }
45
58
  }
46
59
 
60
+ export function atomicSettingsWrite(path, settings, dependencies = {}) {
61
+ atomicContentWrite(path, `${JSON.stringify(settings, null, 2)}\n`, dependencies);
62
+ }
63
+
64
+ export function repositoryCopilotSettingsPath(targetDir, { shared = false } = {}) {
65
+ const root = resolve(targetDir);
66
+ if (!existsSync(root) || lstatSync(root).isSymbolicLink() || !lstatSync(root).isDirectory()) {
67
+ throw new Error(`AGDF_COPILOT_REPOSITORY_UNOWNED_PATH: ${root}`);
68
+ }
69
+ const path = resolve(root, shared ? COPILOT_SHARED_SETTINGS : COPILOT_LOCAL_SETTINGS);
70
+ const fromRoot = relative(root, path);
71
+ if (!fromRoot || fromRoot.startsWith("..") || isAbsolute(fromRoot)) {
72
+ throw new Error("AGDF_COPILOT_REPOSITORY_SETTINGS_OUTSIDE_ROOT");
73
+ }
74
+ for (const candidate of [root, join(root, ".github"), join(root, ".github", "copilot")]) {
75
+ if (existsSync(candidate) && lstatSync(candidate).isSymbolicLink()) {
76
+ throw new Error(`AGDF_COPILOT_SETTINGS_UNOWNED_PATH: ${candidate}`);
77
+ }
78
+ if (existsSync(candidate) && !lstatSync(candidate).isDirectory()) {
79
+ throw new Error(`AGDF_COPILOT_SETTINGS_UNOWNED_PATH: ${candidate}`);
80
+ }
81
+ }
82
+ if (existsSync(path) && (lstatSync(path).isSymbolicLink() || !lstatSync(path).isFile())) {
83
+ throw new Error(`AGDF_COPILOT_SETTINGS_UNOWNED_PATH: ${path}`);
84
+ }
85
+ return path;
86
+ }
87
+
88
+ export function assertCopilotLocalSettingsIgnored(targetDir, { exec = execFileSync } = {}) {
89
+ const root = resolve(targetDir);
90
+ try {
91
+ exec("git", ["check-ignore", "--quiet", "--", COPILOT_LOCAL_SETTINGS], {
92
+ cwd: root,
93
+ encoding: "utf8",
94
+ stdio: ["ignore", "pipe", "pipe"],
95
+ });
96
+ } catch (error) {
97
+ if (error?.code === "ENOENT") {
98
+ throw new Error("AGDF_COPILOT_GIT_UNAVAILABLE: install Git and retry personal disable, or use --shared deliberately.");
99
+ }
100
+ if (error?.status === 1) {
101
+ throw new Error(`AGDF_COPILOT_LOCAL_SETTINGS_NOT_IGNORED: add ${COPILOT_LOCAL_SETTINGS} to an effective Git ignore source and retry, or use --shared deliberately.`);
102
+ }
103
+ throw new Error(`AGDF_COPILOT_GIT_IGNORE_UNVERIFIED: ${error?.message || "Git could not verify the repository ignore state."}`);
104
+ }
105
+ return { status: "ignored", path: COPILOT_LOCAL_SETTINGS };
106
+ }
107
+
108
+ export function planCopilotRepositoryDisable({ targetDir, shared = false, exec = execFileSync }) {
109
+ const root = resolve(targetDir);
110
+ const path = repositoryCopilotSettingsPath(root, { shared });
111
+ if (!shared) assertCopilotLocalSettingsIgnored(root, { exec });
112
+ const before = readCopilotSettings(path);
113
+ const current = before.settings.enabledPlugins?.[COPILOT_PLUGIN_SELECTOR];
114
+ if (current !== undefined && typeof current !== "boolean") {
115
+ throw new Error(`AGDF_COPILOT_SETTINGS_AMBIGUOUS_PLUGIN_STATE: ${path}`);
116
+ }
117
+ const next = structuredClone(before.settings);
118
+ next.enabledPlugins = { ...(next.enabledPlugins ?? {}), [COPILOT_PLUGIN_SELECTOR]: false };
119
+ return Object.freeze({
120
+ path,
121
+ root,
122
+ shared,
123
+ audience: shared ? "shared" : "personal",
124
+ selector: COPILOT_PLUGIN_SELECTOR,
125
+ before: Object.freeze({ exists: before.exists, settings: structuredClone(before.settings), content: before.content }),
126
+ settings: Object.freeze(next),
127
+ changed: current !== false,
128
+ });
129
+ }
130
+
131
+ export function applyCopilotRepositoryDisable(mutation, dependencies = {}) {
132
+ if (!mutation?.path || mutation.selector !== COPILOT_PLUGIN_SELECTOR || mutation.settings?.enabledPlugins?.[COPILOT_PLUGIN_SELECTOR] !== false) {
133
+ throw new Error("AGDF_COPILOT_SETTINGS_MUTATION_INVALID");
134
+ }
135
+ const read = dependencies.read ?? readCopilotSettings;
136
+ const write = dependencies.writeSettings ?? ((path, settings) => atomicSettingsWrite(path, settings, dependencies));
137
+ const writeContent = dependencies.writeContent ?? ((path, content) => atomicContentWrite(path, content, dependencies));
138
+ const remove = dependencies.remove ?? rmSync;
139
+ const resolvedPath = repositoryCopilotSettingsPath(mutation.root, { shared: mutation.shared });
140
+ if (resolvedPath !== mutation.path) throw new Error("AGDF_COPILOT_SETTINGS_MUTATION_PATH_MISMATCH");
141
+ const current = read(mutation.path);
142
+ if (current.exists !== mutation.before.exists || (current.exists && current.content !== mutation.before.content)) {
143
+ throw new Error(`AGDF_COPILOT_SETTINGS_CHANGED_DURING_APPLY: ${mutation.path}`);
144
+ }
145
+ if (!mutation.changed) return { status: "unchanged", path: mutation.path };
146
+ try {
147
+ write(mutation.path, mutation.settings);
148
+ const verified = read(mutation.path);
149
+ if (verified.settings.enabledPlugins?.[COPILOT_PLUGIN_SELECTOR] !== false) {
150
+ throw new Error("AGDF_COPILOT_SETTINGS_VERIFICATION_FAILED");
151
+ }
152
+ return { status: "written", path: mutation.path };
153
+ } catch (error) {
154
+ try {
155
+ if (mutation.before.exists) writeContent(mutation.path, mutation.before.content);
156
+ else remove(mutation.path, { force: true });
157
+ } catch (rollbackError) {
158
+ error.rollbackError = rollbackError;
159
+ }
160
+ throw error;
161
+ }
162
+ }
163
+
164
+ export function verifyCopilotRepositoryDisabled(targetDir, { shared = false } = {}) {
165
+ try {
166
+ const path = repositoryCopilotSettingsPath(targetDir, { shared });
167
+ if (!existsSync(path)) return { status: "failed", evidence: [`missing:${path}`] };
168
+ const state = readCopilotSettings(path);
169
+ return state.settings.enabledPlugins?.[COPILOT_PLUGIN_SELECTOR] === false
170
+ ? { status: "healthy", evidence: [`${path}:${COPILOT_PLUGIN_SELECTOR}:enabled=false`, `audience:${shared ? "shared" : "personal"}`] }
171
+ : { status: "failed", evidence: [`postcondition_failed:${path}:${COPILOT_PLUGIN_SELECTOR}`] };
172
+ } catch (error) {
173
+ return { status: "failed", evidence: [`verification_failed:${resolve(targetDir)}:${error.message}`] };
174
+ }
175
+ }
176
+
47
177
  export function configureCopilotDeclarativePlugin({ path = defaultCopilotSettingsPath(), pluginRoot }) {
48
178
  if (typeof pluginRoot !== "string" || !pluginRoot.trim() || !isAbsolute(pluginRoot)) {
49
179
  throw new Error("AGDF_COPILOT_PLUGIN_SPEC_INVALID");
@@ -64,7 +194,7 @@ export function configureCopilotDeclarativePlugin({ path = defaultCopilotSetting
64
194
  path,
65
195
  spec,
66
196
  rollback() {
67
- if (before.exists) atomicSettingsWrite(path, before.settings);
197
+ if (before.exists) atomicContentWrite(path, before.content);
68
198
  else rmSync(path, { force: true });
69
199
  },
70
200
  };
@@ -2,15 +2,17 @@ import {
2
2
  cpSync,
3
3
  existsSync,
4
4
  mkdirSync,
5
+ mkdtempSync,
5
6
  readFileSync,
6
7
  rmSync,
7
8
  writeFileSync,
8
9
  } from "node:fs";
9
- import { homedir } from "node:os";
10
+ import { homedir, tmpdir } from "node:os";
10
11
  import { dirname, isAbsolute, join, posix, relative, resolve, sep, win32 } from "node:path";
11
12
  import process from "node:process";
12
13
  import { generatedRoot, packageRoot, pluginDefinition } from "../cli/runtime-context.js";
13
14
  import { renameSyncWithRetry } from "../fs-swap.js";
15
+ import { classifyHistoricalDistributionProfile } from "../runtime/distribution-profile-history.js";
14
16
  import {
15
17
  INSTALLATION_PROVENANCE_FILE,
16
18
  LEGACY_LOCAL_INSTALL_FILE,
@@ -31,6 +33,14 @@ function pathInside(root, candidate) {
31
33
  return rel === "" || (rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel));
32
34
  }
33
35
 
36
+ function readOptionalJson(path) {
37
+ try {
38
+ return JSON.parse(readFileSync(path, "utf8"));
39
+ } catch {
40
+ return null;
41
+ }
42
+ }
43
+
34
44
  export function digestPluginSource(root, canonicalVersion = pluginDefinition.version) {
35
45
  return digestNormalizedPluginSource(root, canonicalVersion);
36
46
  }
@@ -58,6 +68,59 @@ export function isCodexLocalInstallVersion(version, candidate, sourceDigest = ""
58
68
  return new RegExp(`^${escaped}\\+codex\\.local-[a-f0-9]{12}$`).test(candidate);
59
69
  }
60
70
 
71
+ function localInstallSourceUnstable(detail) {
72
+ const error = new Error(`AGDF local plugin source changed during snapshot capture: ${detail}`);
73
+ error.code = "local_install_source_unstable";
74
+ return error;
75
+ }
76
+
77
+ export function captureLocalPluginSnapshot({
78
+ builtPluginRoot,
79
+ expectedVersion = pluginDefinition.version,
80
+ profileId = "runtime-plugin",
81
+ adapters = {},
82
+ } = {}) {
83
+ if (!builtPluginRoot) throw new Error("AGDF local plugin source root is required.");
84
+ const sourceRoot = resolve(builtPluginRoot);
85
+ const digest = adapters.digest ?? digestPluginSource;
86
+ const createRoot = adapters.createRoot ?? (() => mkdtempSync(join(tmpdir(), "agdf-local-plugin-")));
87
+ const copy = adapters.copy ?? ((source, target) => cpSync(source, target, { recursive: true }));
88
+ const remove = adapters.remove ?? ((root) => rmSync(root, { recursive: true, force: true }));
89
+ const beforeDigest = digest(sourceRoot, expectedVersion);
90
+ let snapshotRoot = null;
91
+ try {
92
+ snapshotRoot = resolve(createRoot());
93
+ const pluginRoot = join(snapshotRoot, "agdf");
94
+ copy(sourceRoot, pluginRoot);
95
+ const snapshotDigest = digest(pluginRoot, expectedVersion);
96
+ const afterDigest = digest(sourceRoot, expectedVersion);
97
+ if (beforeDigest !== snapshotDigest || snapshotDigest !== afterDigest) {
98
+ throw localInstallSourceUnstable(
99
+ `before=${beforeDigest}, snapshot=${snapshotDigest}, after=${afterDigest}`,
100
+ );
101
+ }
102
+ const copilotProfile = profileId === "copilot-runtime-plugin";
103
+ return Object.freeze({
104
+ canonicalVersion: expectedVersion,
105
+ profileId,
106
+ pluginRoot,
107
+ sourceDigest: snapshotDigest,
108
+ codexInstallVersion: copilotProfile
109
+ ? expectedVersion
110
+ : codexLocalInstallVersion(expectedVersion, snapshotDigest),
111
+ cleanup() {
112
+ if (!snapshotRoot) return;
113
+ const ownedRoot = snapshotRoot;
114
+ remove(ownedRoot);
115
+ snapshotRoot = null;
116
+ },
117
+ });
118
+ } catch (error) {
119
+ if (snapshotRoot) remove(snapshotRoot);
120
+ throw error;
121
+ }
122
+ }
123
+
61
124
  function readJson(path, label) {
62
125
  try {
63
126
  return JSON.parse(readFileSync(path, "utf8"));
@@ -160,6 +223,9 @@ function validateBuiltPlugin(pluginRoot, expectedVersion, expectedCodexInstallVe
160
223
  requireInstallationProvenance = false,
161
224
  allowLegacyProvenanceForMigration = false,
162
225
  allowPreProvenanceShape = false,
226
+ allowHistoricalProfilesForMigration = false,
227
+ distributionProfileHistory = null,
228
+ classifyHistoricalProfile = classifyHistoricalDistributionProfile,
163
229
  profileId = "runtime-plugin",
164
230
  } = {}) {
165
231
  const definition = readJson(join(pluginRoot, "meta", "agdf-plugin.definition.json"), "built plugin definition");
@@ -193,10 +259,18 @@ function validateBuiltPlugin(pluginRoot, expectedVersion, expectedCodexInstallVe
193
259
  runtimeManifest: runtime,
194
260
  pluginVersion: expectedCodexInstallVersion,
195
261
  allowLegacy: true,
262
+ allowHistoricalProfilesForMigration,
263
+ distributionProfileHistory,
264
+ classifyHistoricalProfile,
196
265
  });
197
266
  }
198
- if (profile.status !== "matched" && provenance?.status !== "legacy" && !isPreProvenanceShape) {
199
- throw new Error("Built plugin distribution profile contract is invalid.");
267
+ const supportedHistoricalProfile = provenance?.status === "matched"
268
+ && provenance.profileClassification === "supported_historical";
269
+ if (profile.status !== "matched"
270
+ && provenance?.status !== "legacy"
271
+ && !supportedHistoricalProfile
272
+ && !isPreProvenanceShape) {
273
+ throw new Error(`Built plugin distribution profile contract is invalid (${provenance?.reason ?? profile.reason}).`);
200
274
  }
201
275
  if (!copilotProfile && codex.version !== expectedCodexInstallVersion) {
202
276
  throw new Error(`Built Codex plugin version mismatch: expected ${expectedCodexInstallVersion}, observed ${codex.version}`);
@@ -226,6 +300,9 @@ function validateBuiltPlugin(pluginRoot, expectedVersion, expectedCodexInstallVe
226
300
  runtimeManifest: runtime,
227
301
  pluginVersion: expectedCodexInstallVersion,
228
302
  allowLegacy: allowLegacyProvenanceForMigration,
303
+ allowHistoricalProfilesForMigration,
304
+ distributionProfileHistory,
305
+ classifyHistoricalProfile,
229
306
  profileId,
230
307
  inventoryDigest: copilotInventory?.inventoryDigest ?? null,
231
308
  });
@@ -234,7 +311,11 @@ function validateBuiltPlugin(pluginRoot, expectedVersion, expectedCodexInstallVe
234
311
  throw new Error(`Invalid AGDF installation provenance (${provenance.reason}): ${pluginRoot}`);
235
312
  }
236
313
  }
237
- return definition;
314
+ return {
315
+ definition,
316
+ provenance,
317
+ profileClassification: provenance?.profileClassification ?? "current",
318
+ };
238
319
  }
239
320
 
240
321
  function validateMarketplaceRoot(root, definition = pluginDefinition, { profileId = "runtime-plugin" } = {}) {
@@ -262,7 +343,10 @@ function validateMarketplaceRoot(root, definition = pluginDefinition, { profileI
262
343
  return Object.freeze({ codexShape });
263
344
  }
264
345
 
265
- function classifyExistingMarketplace(stableRoot, marker, { profileId = "runtime-plugin" } = {}) {
346
+ function classifyExistingMarketplace(stableRoot, marker, {
347
+ profileId = "runtime-plugin",
348
+ distributionProfileHistory = null,
349
+ } = {}) {
266
350
  const existingPluginRoot = join(stableRoot, "plugins", MARKETPLACE_ID);
267
351
  const copilotProfile = profileId === "copilot-runtime-plugin";
268
352
  const existingCodexInstallVersion = copilotProfile ? marker.version : marker.codex_install_version ?? marker.version;
@@ -279,15 +363,32 @@ function classifyExistingMarketplace(stableRoot, marker, { profileId = "runtime-
279
363
  const preProvenanceCandidate = !hasCurrentProvenance
280
364
  && !hasLegacyProvenance
281
365
  && !Object.hasOwn(definition, "distributionProfiles");
282
- const existingDefinition = validateBuiltPlugin(
366
+ const existingValidation = validateBuiltPlugin(
283
367
  existingPluginRoot,
284
368
  marker.version,
285
369
  existingCodexInstallVersion,
286
370
  marker.source_digest ?? "",
287
371
  preProvenanceCandidate
288
372
  ? { allowPreProvenanceShape: true }
289
- : { requireInstallationProvenance: true, allowLegacyProvenanceForMigration: true, profileId },
373
+ : {
374
+ requireInstallationProvenance: true,
375
+ allowLegacyProvenanceForMigration: true,
376
+ allowHistoricalProfilesForMigration: !copilotProfile,
377
+ distributionProfileHistory,
378
+ profileId,
379
+ },
290
380
  );
381
+ if (!preProvenanceCandidate) {
382
+ if (!/^[a-f0-9]{64}$/.test(marker.source_digest ?? "")) {
383
+ throw new Error(`Refusing invalid AGDF marketplace ownership source digest: ${stableRoot}`);
384
+ }
385
+ const observedSourceDigest = existingValidation.provenance?.observedSourceDigest
386
+ ?? digestPluginSource(existingPluginRoot, marker.version);
387
+ if (marker.source_digest !== observedSourceDigest) {
388
+ throw new Error(`Refusing AGDF marketplace ownership source digest mismatch: ${stableRoot}`);
389
+ }
390
+ }
391
+ const existingDefinition = existingValidation.definition;
291
392
  const marketplace = validateMarketplaceRoot(stableRoot, existingDefinition, { profileId });
292
393
  if (digestDirectory(existingPluginRoot) !== marker.plugin_digest) {
293
394
  throw new Error(`Refusing tampered AGDF marketplace root: ${stableRoot}`);
@@ -295,9 +396,19 @@ function classifyExistingMarketplace(stableRoot, marker, { profileId = "runtime-
295
396
  return Object.freeze({
296
397
  classification: preProvenanceCandidate
297
398
  ? "owned_pre_provenance_rebuild"
298
- : "current_or_marker_migration",
399
+ : existingValidation.profileClassification === "supported_historical"
400
+ ? "owned_supported_historical_rebuild"
401
+ : "current_or_marker_migration",
299
402
  definition: existingDefinition,
300
403
  marketplace,
404
+ historicalEvidence: existingValidation.profileClassification === "supported_historical"
405
+ ? {
406
+ releaseVersion: existingValidation.provenance.historicalReleaseVersion,
407
+ contractId: existingValidation.provenance.historicalContractId,
408
+ contractDigest: existingValidation.provenance.historicalContractDigest,
409
+ entryDigest: existingValidation.provenance.historicalEntryDigest,
410
+ }
411
+ : null,
301
412
  });
302
413
  }
303
414
 
@@ -322,13 +433,15 @@ function recoverInterruptedTransaction(stableRoot, stageRoot, backupRoot, failed
322
433
  if (existsSync(failedRoot)) removeOwnedRoot(failedRoot, parent);
323
434
  }
324
435
 
325
- export function prepareLocalMarketplace({
436
+ function prepareLocalMarketplaceFromSource({
326
437
  dataRoot = defaultAgdfDataRoot(),
327
438
  builtPluginRoot,
328
439
  expectedVersion = pluginDefinition.version,
329
440
  codexInstallVersion = expectedVersion,
330
441
  codexRegistrationRevision = null,
331
442
  profileId = "runtime-plugin",
443
+ knownSourceDigest = "",
444
+ sourceStaged = () => {},
332
445
  } = {}) {
333
446
  dataRoot = resolve(dataRoot);
334
447
  const copilotProfile = profileId === "copilot-runtime-plugin";
@@ -342,12 +455,15 @@ export function prepareLocalMarketplace({
342
455
  const backupRoot = `${stableRoot}.backup`;
343
456
  const failedRoot = `${stableRoot}.failed`;
344
457
  if (!pathInside(dataRoot, stableRoot) || dirname(stableRoot) !== parent) throw new Error(`Unsafe AGDF marketplace root: ${stableRoot}`);
345
- const sourceDigest = digestPluginSource(builtPluginRoot, expectedVersion);
458
+ const sourceDigest = knownSourceDigest || digestPluginSource(builtPluginRoot, expectedVersion);
346
459
  if (!copilotProfile && codexInstallVersion !== expectedVersion
347
460
  && !isCodexLocalInstallVersion(expectedVersion, codexInstallVersion, sourceDigest)) {
348
461
  throw new Error(`Invalid AGDF Codex local install version: ${codexInstallVersion}`);
349
462
  }
350
- const targetDefinition = validateBuiltPlugin(builtPluginRoot, expectedVersion, expectedVersion, "", { profileId });
463
+ const targetDefinition = validateBuiltPlugin(builtPluginRoot, expectedVersion, expectedVersion, "", { profileId }).definition;
464
+ const distributionProfileHistory = readOptionalJson(
465
+ join(builtPluginRoot, "meta", "distribution-profile-history.json"),
466
+ );
351
467
  mkdirSync(parent, { recursive: true });
352
468
  recoverInterruptedTransaction(stableRoot, stageRoot, backupRoot, failedRoot);
353
469
 
@@ -365,15 +481,17 @@ export function prepareLocalMarketplace({
365
481
  ?? 0;
366
482
  let existingMarketplace = null;
367
483
  let existingClassification = "none";
484
+ let historicalEvidence = null;
368
485
  if (existing) {
369
486
  let classified;
370
487
  try {
371
- classified = classifyExistingMarketplace(stableRoot, existing, { profileId });
488
+ classified = classifyExistingMarketplace(stableRoot, existing, { profileId, distributionProfileHistory });
372
489
  } catch (error) {
373
490
  throw invalidExistingMarketplace(error);
374
491
  }
375
492
  existingClassification = classified.classification;
376
493
  existingMarketplace = classified.marketplace;
494
+ historicalEvidence = classified.historicalEvidence;
377
495
  }
378
496
  mkdirSync(stageRoot, { recursive: false });
379
497
  try {
@@ -392,6 +510,7 @@ export function prepareLocalMarketplace({
392
510
  });
393
511
  const stagedPluginRoot = join(stageRoot, "plugins", MARKETPLACE_ID);
394
512
  cpSync(builtPluginRoot, stagedPluginRoot, { recursive: true });
513
+ sourceStaged();
395
514
  validateBuiltPlugin(stagedPluginRoot, expectedVersion, expectedVersion, "", { profileId });
396
515
  if (!copilotProfile && codexInstallVersion !== expectedVersion) {
397
516
  const codexManifestPath = join(stagedPluginRoot, ".codex-plugin", "plugin.json");
@@ -450,6 +569,7 @@ export function prepareLocalMarketplace({
450
569
  digest: pluginDigest,
451
570
  runtimeDigest: runtimeManifest.digest,
452
571
  existingClassification,
572
+ historicalEvidence,
453
573
  changed: false,
454
574
  commit() {},
455
575
  rollback() {},
@@ -473,6 +593,7 @@ export function prepareLocalMarketplace({
473
593
  digest: pluginDigest,
474
594
  runtimeDigest: runtimeManifest.digest,
475
595
  existingClassification,
596
+ historicalEvidence,
476
597
  changed: true,
477
598
  commit() {
478
599
  if (closed) return;
@@ -498,8 +619,42 @@ export function prepareLocalMarketplace({
498
619
  }
499
620
  }
500
621
 
622
+ export function prepareLocalMarketplace(options = {}) {
623
+ if (!options.snapshotSource) return prepareLocalMarketplaceFromSource(options);
624
+ if (Object.hasOwn(options, "codexInstallVersion")) {
625
+ throw new Error("AGDF snapshot preparation owns the Codex local install version.");
626
+ }
627
+ const snapshot = captureLocalPluginSnapshot({
628
+ builtPluginRoot: options.builtPluginRoot,
629
+ expectedVersion: options.expectedVersion,
630
+ profileId: options.profileId,
631
+ adapters: options.snapshotAdapters,
632
+ });
633
+ try {
634
+ const { snapshotSource: _snapshotSource, snapshotAdapters: _snapshotAdapters, ...stableOptions } = options;
635
+ return prepareLocalMarketplaceFromSource({
636
+ ...stableOptions,
637
+ builtPluginRoot: snapshot.pluginRoot,
638
+ expectedVersion: snapshot.canonicalVersion,
639
+ profileId: snapshot.profileId,
640
+ codexInstallVersion: snapshot.codexInstallVersion,
641
+ knownSourceDigest: snapshot.sourceDigest,
642
+ sourceStaged: snapshot.cleanup,
643
+ });
644
+ } finally {
645
+ snapshot.cleanup();
646
+ }
647
+ }
648
+
501
649
  export function prepareCopilotMarketplace(options = {}) {
502
- return prepareLocalMarketplace({ ...options, profileId: "copilot-runtime-plugin", codexInstallVersion: options.expectedVersion ?? pluginDefinition.version });
650
+ const { codexInstallVersion, ...copilotOptions } = options;
651
+ return prepareLocalMarketplace({
652
+ ...copilotOptions,
653
+ profileId: "copilot-runtime-plugin",
654
+ ...(!options.snapshotSource
655
+ ? { codexInstallVersion: codexInstallVersion ?? options.expectedVersion ?? pluginDefinition.version }
656
+ : {}),
657
+ });
503
658
  }
504
659
 
505
660
  export function inspectLocalMarketplaceProjection({
@@ -517,7 +672,7 @@ export function inspectLocalMarketplaceProjection({
517
672
  codexInstallVersion,
518
673
  marker.source_digest ?? "",
519
674
  { requireInstallationProvenance: true },
520
- );
675
+ ).definition;
521
676
  validateMarketplaceRoot(root, definition);
522
677
  if (marker.version !== expectedVersion || digestDirectory(pluginRoot) !== marker.plugin_digest) {
523
678
  throw new Error(`Refusing stale or tampered AGDF marketplace projection: ${root}`);