create-agdf 0.9.3 → 0.9.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.
@@ -15,12 +15,28 @@ const testNpmCliPath = process.env.NODE_ENV === "test" ? process.env.AGDF_TEST_N
15
15
  const npmCommand = testNpmCliPath ? process.execPath : process.platform === "win32" ? process.execPath : "npm";
16
16
  const npmPrefixArgs = testNpmCliPath ? [testNpmCliPath] : process.platform === "win32" ? [join(dirname(process.execPath), "node_modules", "npm", "bin", "npm-cli.js")] : [];
17
17
 
18
+ export function openCodeNpmInvocation(args) {
19
+ return { executable: npmCommand, args: [...npmPrefixArgs, ...args] };
20
+ }
21
+
22
+ function openCodeLifecycleError(phase, message, evidence = {}) {
23
+ const error = new Error(message);
24
+ error.name = "LifecycleAdapterError";
25
+ error.phase = phase;
26
+ error.evidence = evidence;
27
+ return error;
28
+ }
29
+
18
30
  export function defaultOpenCodeConfigDir() {
19
31
  return process.env.OPENCODE_CONFIG_DIR || join(homedir(), ".config", "opencode");
20
32
  }
21
33
 
22
34
  export function installOpenCodeGlobalPlugin(configDir) {
23
- assertGlobalOpenCodeSurfaceWritable(configDir);
35
+ try {
36
+ assertGlobalOpenCodeSurfaceWritable(configDir);
37
+ } catch (error) {
38
+ throw openCodeLifecycleError("ownership_preflight", error.message, { configDir });
39
+ }
24
40
  const previousPackage = resolveOpenCodePackage(configDir);
25
41
  const configPath = join(configDir, "opencode.json");
26
42
  let config = {};
@@ -29,12 +45,12 @@ export function installOpenCodeGlobalPlugin(configDir) {
29
45
  try {
30
46
  config = JSON.parse(readFileSync(configPath, "utf8"));
31
47
  } catch {
32
- throw new Error(`Refusing to update unreadable OpenCode config: ${configPath}`);
48
+ throw openCodeLifecycleError("configuration", `Refusing to update unreadable OpenCode config: ${configPath}`, { configPath });
33
49
  }
34
50
  }
35
51
 
36
52
  if (config.plugin !== undefined && !Array.isArray(config.plugin)) {
37
- throw new Error(`Refusing to update OpenCode config with non-array plugin field: ${configPath}`);
53
+ throw openCodeLifecycleError("configuration", `Refusing to update OpenCode config with non-array plugin field: ${configPath}`, { configPath });
38
54
  }
39
55
 
40
56
  const plugins = [...(config.plugin ?? [])];
@@ -50,7 +66,7 @@ export function installOpenCodeGlobalPlugin(configDir) {
50
66
  };
51
67
 
52
68
  if (nextConfig.instructions !== undefined && !Array.isArray(nextConfig.instructions)) {
53
- throw new Error(`Refusing to update OpenCode config with non-array instructions field: ${configPath}`);
69
+ throw openCodeLifecycleError("configuration", `Refusing to update OpenCode config with non-array instructions field: ${configPath}`, { configPath });
54
70
  }
55
71
  nextConfig.instructions = [...(nextConfig.instructions ?? [])];
56
72
  if (!nextConfig.instructions.includes("AGDF.md")) nextConfig.instructions.push("AGDF.md");
@@ -71,14 +87,22 @@ export function installOpenCodeGlobalPlugin(configDir) {
71
87
  mkdirSync(configDir, { recursive: true });
72
88
  try {
73
89
  const packageSpecifier = `${pluginDefinition.opencode.npmPackage}@${pluginDefinition.version}`;
74
- execFileSync(npmCommand, [...npmPrefixArgs, "install", "--silent", "--save-prod", "--save-exact", packageSpecifier], {
90
+ const invocation = openCodeNpmInvocation(["install", "--silent", "--save-prod", "--save-exact", packageSpecifier]);
91
+ execFileSync(invocation.executable, invocation.args, {
75
92
  cwd: configDir,
76
93
  stdio: "pipe",
77
94
  });
78
95
  } catch (error) {
79
- throw new Error(`Failed to install ${pluginDefinition.opencode.npmPackage} into the OpenCode config directory: ${(error.stderr || error.message).toString().trim()}`);
96
+ throw openCodeLifecycleError("plugin_operation", `Failed to install ${pluginDefinition.opencode.npmPackage} into the OpenCode config directory: ${(error.stderr || error.message).toString().trim()}`, {
97
+ executable: npmCommand,
98
+ args: [...npmPrefixArgs, "install"],
99
+ });
100
+ }
101
+ try {
102
+ writeFileSync(configPath, `${JSON.stringify(nextConfig, null, 2)}\n`, "utf8");
103
+ } catch (error) {
104
+ throw openCodeLifecycleError("configuration", `Failed to write OpenCode config ${configPath}: ${error.message}`, { configPath });
80
105
  }
81
- writeFileSync(configPath, `${JSON.stringify(nextConfig, null, 2)}\n`, "utf8");
82
106
  const installedPackage = resolveOpenCodePackage(configDir);
83
107
 
84
108
  return {
@@ -3,48 +3,77 @@ import { pluginDefinition } from "../cli/runtime-context.js";
3
3
 
4
4
  export function installCodexGlobalPlugin({ exec = execFileSync, io = console } = {}) {
5
5
  const expectedVersion = pluginDefinition.version;
6
-
7
- try {
8
- exec("codex", ["plugin", "marketplace", "add", "arndtgold/ai-native-governance-delivery-framework"], { stdio: "inherit" });
9
- exec("codex", ["plugin", "marketplace", "upgrade", "agdf"], { stdio: "inherit" });
10
- exec("codex", ["plugin", "add", "agdf", "--marketplace", "agdf"], { stdio: "inherit" });
11
- const listOutput = exec("codex", ["plugin", "list"], { encoding: "utf8", stdio: "pipe" });
12
- const installedVersion = pluginVersionFromList(listOutput, "agdf@agdf");
13
- if (installedVersion !== expectedVersion) {
14
- throw new Error(versionMismatchMessage("Codex", "agdf@agdf", expectedVersion, installedVersion, "codex plugin marketplace upgrade agdf && codex plugin add agdf --marketplace agdf"));
15
- }
16
- io.log(`AGDF Codex plugin version verified: ${installedVersion}.`);
17
- } catch (error) {
18
- if (error.message?.startsWith("AGDF Codex plugin version mismatch")) throw error;
19
- throw new Error(`Failed to install the AGDF Codex plugin. Make sure the Codex CLI is installed and available on PATH, then rerun this command. ${commandErrorText(error)}`.trim());
6
+ runPluginPhase(exec, "codex", ["plugin", "marketplace", "add", "arndtgold/ai-native-governance-delivery-framework"], "marketplace", { stdio: "inherit" });
7
+ runPluginPhase(exec, "codex", ["plugin", "marketplace", "upgrade", "agdf"], "marketplace", { stdio: "inherit" });
8
+ runPluginPhase(exec, "codex", ["plugin", "add", "agdf", "--marketplace", "agdf"], "plugin_operation", { stdio: "inherit" });
9
+ const listOutput = runPluginPhase(exec, "codex", ["plugin", "list"], "verification", { encoding: "utf8", stdio: "pipe" });
10
+ const installedVersion = pluginVersionFromList(listOutput, "agdf@agdf");
11
+ if (installedVersion !== expectedVersion) {
12
+ throw lifecycleAdapterError("version", versionMismatchMessage("Codex", "agdf@agdf", expectedVersion, installedVersion, "codex plugin marketplace upgrade agdf && codex plugin add agdf --marketplace agdf"));
20
13
  }
14
+ return { surface: "codex", expectedVersion, installedVersion, verificationStatus: "healthy", evidence: ["codex plugin list"] };
21
15
  }
22
16
 
23
17
  export function installClaudeGlobalPlugin({ exec = execFileSync, io = console } = {}) {
24
18
  const expectedVersion = pluginDefinition.version;
19
+ runPluginPhase(exec, "claude", ["plugin", "marketplace", "add", "arndtgold/ai-native-governance-delivery-framework"], "marketplace", { stdio: "inherit" });
20
+ runPluginPhase(exec, "claude", ["plugin", "marketplace", "update", "agdf"], "marketplace", { stdio: "inherit" });
21
+ const beforeList = runPluginPhase(exec, "claude", ["plugin", "list"], "verification", { encoding: "utf8", stdio: "pipe" });
22
+ const alreadyInstalled = pluginListHasPlugin(beforeList, "agdf@agdf");
23
+ runPluginPhase(exec, "claude", ["plugin", alreadyInstalled ? "update" : "install", "agdf@agdf"], "plugin_operation", { stdio: "inherit" });
24
+ const afterList = runPluginPhase(exec, "claude", ["plugin", "list"], "verification", { encoding: "utf8", stdio: "pipe" });
25
+ const installedVersion = pluginVersionFromList(afterList, "agdf@agdf");
26
+ if (installedVersion && installedVersion !== expectedVersion) {
27
+ throw lifecycleAdapterError("version", versionMismatchMessage("Claude Code", "agdf@agdf", expectedVersion, installedVersion, "claude plugin marketplace update agdf && claude plugin update agdf@agdf"));
28
+ }
29
+ return {
30
+ surface: "claude",
31
+ expectedVersion,
32
+ installedVersion,
33
+ verificationStatus: installedVersion ? "healthy" : "degraded",
34
+ evidence: ["claude plugin list", ...(installedVersion ? [] : ["host_did_not_expose_version"])],
35
+ };
36
+ }
37
+
38
+ export function inspectPluginSurface(surface, exec = execFileSync) {
39
+ const executable = surface === "claude" ? "claude" : "codex";
40
+ const pluginId = "agdf@agdf";
41
+ try {
42
+ const output = exec(executable, ["plugin", "list"], { encoding: "utf8", stdio: "pipe" });
43
+ const installed = pluginListHasPlugin(output, pluginId);
44
+ const version = installed ? pluginVersionFromList(output, pluginId) : "";
45
+ return {
46
+ status: !installed ? "not_installed" : version === pluginDefinition.version ? "healthy" : "degraded",
47
+ surface,
48
+ version: version || null,
49
+ expected_version: pluginDefinition.version,
50
+ evidence: [`${executable} plugin list`, ...(installed && !version ? ["host_did_not_expose_version"] : [])],
51
+ };
52
+ } catch (error) {
53
+ return { status: "unknown", surface, version: null, expected_version: pluginDefinition.version, evidence: [commandErrorText(error)] };
54
+ }
55
+ }
25
56
 
57
+ function runPluginPhase(exec, executable, args, phase, options) {
26
58
  try {
27
- exec("claude", ["plugin", "marketplace", "add", "arndtgold/ai-native-governance-delivery-framework"], { stdio: "inherit" });
28
- exec("claude", ["plugin", "marketplace", "update", "agdf"], { stdio: "inherit" });
29
- const beforeList = exec("claude", ["plugin", "list"], { encoding: "utf8", stdio: "pipe" });
30
- const alreadyInstalled = pluginListHasPlugin(beforeList, "agdf@agdf");
31
- exec("claude", ["plugin", alreadyInstalled ? "update" : "install", "agdf@agdf"], { stdio: "inherit" });
32
- const afterList = exec("claude", ["plugin", "list"], { encoding: "utf8", stdio: "pipe" });
33
- const installedVersion = pluginVersionFromList(afterList, "agdf@agdf");
34
- if (installedVersion) {
35
- if (installedVersion !== expectedVersion) {
36
- throw new Error(versionMismatchMessage("Claude Code", "agdf@agdf", expectedVersion, installedVersion, "claude plugin marketplace update agdf && claude plugin update agdf@agdf"));
37
- }
38
- io.log(`AGDF Claude Code plugin version verified: ${installedVersion}.`);
39
- } else {
40
- io.log("AGDF Claude Code plugin installed or updated. Claude Code did not expose a plugin version in `claude plugin list`; verify with `claude plugin list` after restart if needed.");
41
- }
59
+ return exec(executable, args, options);
42
60
  } catch (error) {
43
- if (error.message?.startsWith("AGDF Claude Code plugin version mismatch")) throw error;
44
- throw new Error(`Failed to install the AGDF Claude Code plugin. Make sure the Claude Code CLI is installed and available on PATH, then rerun this command. ${commandErrorText(error)}`.trim());
61
+ const effectivePhase = error?.code === "ENOENT" ? "executable" : phase;
62
+ throw lifecycleAdapterError(effectivePhase, commandErrorText(error) || `${executable} ${args.join(" ")} failed`, {
63
+ executable,
64
+ args,
65
+ });
45
66
  }
46
67
  }
47
68
 
69
+ function lifecycleAdapterError(phase, message, evidence = {}) {
70
+ const error = new Error(message);
71
+ error.name = "LifecycleAdapterError";
72
+ error.phase = phase;
73
+ error.evidence = evidence;
74
+ return error;
75
+ }
76
+
48
77
  function commandErrorText(error) {
49
78
  return (error.stderr || error.stdout || error.message || "").toString().trim();
50
79
  }
@@ -0,0 +1,177 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ import { pluginDefinition } from "../cli/runtime-context.js";
5
+ import { evaluateOpenCodeStatus, openCodeNpmInvocation } from "../installers/opencode.js";
6
+ import { inspectPluginSurface } from "../installers/plugin-installers.js";
7
+
8
+ const CODEX_DISABLE_MARKER = "# AGDF-OWNED-REPOSITORY-PLUGIN-STATE";
9
+
10
+ function pluginSection(content, selector) {
11
+ const escaped = JSON.stringify(selector).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
12
+ return content.match(new RegExp(`\\[plugins\\.${escaped}\\][\\s\\S]*?(?=\\n\\[|$)`))?.[0] ?? "";
13
+ }
14
+
15
+ function repositorySelector(targetDir) {
16
+ return existsSync(join(targetDir, ".agents", "plugins", "marketplace.json")) ? "agdf@agdf-repo" : "agdf@agdf";
17
+ }
18
+
19
+ export function planRepositoryDisable(targetDir, surface) {
20
+ if (surface !== "codex") {
21
+ throw new Error(`Repository disable is not supported safely for ${surface}; no files were changed.`);
22
+ }
23
+ const path = join(targetDir, ".codex", "config.toml");
24
+ const existing = existsSync(path) ? readFileSync(path, "utf8") : "";
25
+ const selector = repositorySelector(targetDir);
26
+ const escaped = JSON.stringify(selector).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
27
+ const sectionPattern = new RegExp(`\\[plugins\\.${escaped}\\][\\s\\S]*?(?=\\n\\[|$)`);
28
+ const currentSection = pluginSection(existing, selector);
29
+ const enabledMatches = [...currentSection.matchAll(/^enabled\s*=\s*(true|false)\s*$/gm)];
30
+ if (currentSection && enabledMatches.length !== 1) {
31
+ throw new Error(`Refusing to modify ambiguous AGDF plugin state in ${path}.`);
32
+ }
33
+ const section = currentSection
34
+ ? currentSection.replace(/^enabled\s*=\s*(true|false)\s*$/m, "enabled = false")
35
+ : `${CODEX_DISABLE_MARKER}\n[plugins.${JSON.stringify(selector)}]\nenabled = false`;
36
+ const content = currentSection
37
+ ? existing.replace(sectionPattern, section).replace(/\s*$/, "\n")
38
+ : `${existing.replace(/\s*$/, "")}${existing.trim() ? "\n\n" : ""}${section}\n`;
39
+ return Object.freeze({
40
+ operation: "disable",
41
+ surface,
42
+ scope: "repository",
43
+ mutations: Object.freeze([{ kind: "write", path, content, ownership: currentSection ? "exact_plugin_section" : "agdf_marker" }]),
44
+ retained: Object.freeze([join(targetDir, ".agdf", "control"), "global AGDF plugin availability"]),
45
+ expected: Object.freeze({ repository_status: "disabled" }),
46
+ });
47
+ }
48
+
49
+ export function verifyRepositoryDisabled(targetDir) {
50
+ const path = join(targetDir, ".codex", "config.toml");
51
+ if (!existsSync(path)) return { status: "failed", evidence: [`missing:${path}`] };
52
+ const selector = repositorySelector(targetDir);
53
+ const section = pluginSection(readFileSync(path, "utf8"), selector);
54
+ const matches = [...section.matchAll(/^enabled\s*=\s*(true|false)\s*$/gm)];
55
+ return matches.length === 1 && matches[0][1] === "false"
56
+ ? { status: "healthy", evidence: [`${path}:${selector}:enabled=false`] }
57
+ : { status: "failed", evidence: [`postcondition_failed:${path}:${selector}`] };
58
+ }
59
+
60
+ export function verifyGlobalUninstall(plan, targetDir, { configDir, exec, inspect = inspectPluginSurface, evaluateOpenCode = evaluateOpenCodeStatus } = {}) {
61
+ if (plan.surface === "opencode") {
62
+ const report = evaluateOpenCode(targetDir, configDir);
63
+ const remainingOwnedFiles = plan.mutations
64
+ .filter((mutation) => mutation.kind === "remove")
65
+ .map((mutation) => mutation.path)
66
+ .filter((path) => existsSync(path));
67
+ return report.status === "not_configured" && remainingOwnedFiles.length === 0
68
+ ? { status: "healthy", evidence: [report.global_config.path, "marker_owned_global_files_removed"] }
69
+ : { status: "failed", evidence: [report.global_config.path, ...remainingOwnedFiles.map((path) => `still_present:${path}`)] };
70
+ }
71
+ const report = inspect(plan.surface, exec);
72
+ return report.status === "not_installed"
73
+ ? { status: "healthy", evidence: report.evidence }
74
+ : { status: "failed", evidence: [...report.evidence, `observed:${report.status}`] };
75
+ }
76
+
77
+ export function planGlobalUninstall(surface, { configDir } = {}) {
78
+ if (surface === "codex") {
79
+ return nativeUninstallPlan(surface, "codex", ["plugin", "remove", "agdf@agdf"]);
80
+ }
81
+ if (surface === "claude") {
82
+ return nativeUninstallPlan(surface, "claude", ["plugin", "uninstall", "agdf@agdf", "--scope", "user"]);
83
+ }
84
+ if (surface === "opencode") {
85
+ if (!configDir) throw new Error("OpenCode uninstall requires its explicit config directory.");
86
+ const configPath = join(configDir, "opencode.json");
87
+ if (!existsSync(configPath)) throw new Error(`OpenCode config not found: ${configPath}`);
88
+ let config;
89
+ try {
90
+ config = JSON.parse(readFileSync(configPath, "utf8"));
91
+ } catch {
92
+ throw new Error(`Refusing to modify unreadable OpenCode config: ${configPath}`);
93
+ }
94
+ if (config.plugin !== undefined && !Array.isArray(config.plugin)) {
95
+ throw new Error(`Refusing to modify OpenCode config with non-array plugin field: ${configPath}`);
96
+ }
97
+ const next = { ...config };
98
+ next.plugin = (config.plugin ?? []).filter((entry) => entry !== pluginDefinition.opencode.npmPackage);
99
+ if (Array.isArray(config.instructions)) next.instructions = config.instructions.filter((entry) => entry !== "AGDF.md");
100
+ const npm = openCodeNpmInvocation(["uninstall", "--silent", pluginDefinition.opencode.npmPackage]);
101
+ const mutations = [
102
+ { kind: "write", path: configPath, content: `${JSON.stringify(next, null, 2)}\n`, ownership: "exact_known_entries" },
103
+ { kind: "command", executable: npm.executable, args: npm.args, cwd: configDir },
104
+ ];
105
+ const retained = ["repository AGDF files", ".agdf/control"];
106
+ for (const candidate of openCodeOwnedGlobalFiles(configDir)) {
107
+ if (!existsSync(candidate.path)) continue;
108
+ const content = readFileSync(candidate.path, "utf8");
109
+ if (candidate.owned(content)) mutations.push({ kind: "remove", path: candidate.path, ownership: "agdf_marker" });
110
+ else retained.push(candidate.path);
111
+ }
112
+ return Object.freeze({
113
+ operation: "uninstall",
114
+ surface,
115
+ scope: "global",
116
+ mutations: Object.freeze(mutations),
117
+ retained: Object.freeze(retained),
118
+ expected: Object.freeze({ installation_status: "not_installed" }),
119
+ });
120
+ }
121
+ throw new Error(`Global uninstall is not supported for ${surface}.`);
122
+ }
123
+
124
+ function openCodeOwnedGlobalFiles(configDir) {
125
+ const firstLine = (marker) => (content) => content.split(/\r?\n/)[0] === marker;
126
+ const afterFrontmatter = (marker) => (content) => {
127
+ const lines = content.split(/\r?\n/);
128
+ const end = lines.findIndex((line, index) => index > 0 && line === "---");
129
+ return end >= 0 && lines[end + 1] === marker;
130
+ };
131
+ const files = [
132
+ { path: join(configDir, pluginDefinition.opencode.instructionsFileName), owned: firstLine("<!-- AGDF-GLOBAL-INSTRUCTIONS -->") },
133
+ { path: join(configDir, pluginDefinition.opencode.runtimeContractFileName), owned: firstLine("<!-- AGDF-GLOBAL-RUNTIME-CONTRACT -->") },
134
+ ];
135
+ for (const moduleName of ["gate-transition.md", "interaction.md", "modes.md", "quality.md", "context-graph.md", "control-scaffold.md", "closeout.md"]) {
136
+ files.push({ path: join(configDir, "contracts", moduleName), owned: firstLine("<!-- AGDF-GLOBAL-RUNTIME-CONTRACT -->") });
137
+ }
138
+ for (const skill of pluginDefinition.skillSet) {
139
+ const name = `${pluginDefinition.opencode.globalSkillPrefix}${skill.slug}`;
140
+ const marker = `<!-- AGDF-GLOBAL-SKILL: ${name} -->`;
141
+ files.push({ path: join(configDir, "skills", name, "SKILL.md"), owned: afterFrontmatter(marker) });
142
+ }
143
+ return files;
144
+ }
145
+
146
+ function nativeUninstallPlan(surface, executable, args) {
147
+ return Object.freeze({
148
+ operation: "uninstall",
149
+ surface,
150
+ scope: "global",
151
+ mutations: Object.freeze([{ kind: "command", executable, args: Object.freeze(args) }]),
152
+ retained: Object.freeze(["repository AGDF files", ".agdf/control", "ambiguous configuration"]),
153
+ expected: Object.freeze({ installation_status: "not_installed" }),
154
+ });
155
+ }
156
+
157
+ export function applyLifecyclePlan(plan, { exec = execFileSync } = {}) {
158
+ const completed = [];
159
+ for (const mutation of plan.mutations) {
160
+ try {
161
+ if (mutation.kind === "write") {
162
+ mkdirSync(dirname(mutation.path), { recursive: true });
163
+ writeFileSync(mutation.path, mutation.content, "utf8");
164
+ completed.push({ kind: "write", path: mutation.path });
165
+ } else if (mutation.kind === "remove") {
166
+ rmSync(mutation.path);
167
+ completed.push({ kind: "remove", path: mutation.path });
168
+ } else if (mutation.kind === "command") {
169
+ exec(mutation.executable, mutation.args, { cwd: mutation.cwd, stdio: "inherit" });
170
+ completed.push({ kind: "command", executable: mutation.executable, args: mutation.args });
171
+ }
172
+ } catch (error) {
173
+ return { status: completed.length ? "partial" : "failed", completed, error, retained: [...plan.retained] };
174
+ }
175
+ }
176
+ return { status: "success", completed, error: null, retained: [...plan.retained] };
177
+ }
@@ -0,0 +1,52 @@
1
+ import { interactionLocales } from "../cli/runtime-context.js";
2
+
3
+ function copy(locale) {
4
+ const language = String(locale ?? "").toLowerCase().split("-")[0];
5
+ return (interactionLocales.locales[language] ?? interactionLocales.locales[interactionLocales.fallbackLocale]).primary.lifecycleResult;
6
+ }
7
+
8
+ export function lifecycleCardLines(report, locale = "en") {
9
+ const c = copy(locale);
10
+ const version = report.version.transition === "updated" && report.version.previous && report.version.installed
11
+ ? `${report.version.previous} -> ${report.version.installed}`
12
+ : report.version.installed || (report.version.expected ? `${c.unknown}; expected ${report.version.expected}` : c.unknown);
13
+ const versionStatus = report.version.transition && report.version.transition !== "updated"
14
+ ? `${report.version.status}; transition ${report.version.transition}`
15
+ : report.version.status;
16
+ return [
17
+ c.title,
18
+ `${c.result}: ${report.result}`,
19
+ `${c.surface}: ${report.surface}`,
20
+ `${c.scope}: ${report.scope}`,
21
+ `${c.version}: ${version} (${versionStatus})`,
22
+ `${c.verification}: ${report.verification.status}`,
23
+ `${c.restart}: ${report.restart.required ? c.yes : c.no}${report.restart.reason !== "none" ? ` (${report.restart.reason})` : ""}`,
24
+ `${c.next}: ${report.next_action.text}`,
25
+ ];
26
+ }
27
+
28
+ export function printLifecycleResult(report, { json = false, locale = "en", io = console } = {}) {
29
+ if (json) {
30
+ io.log(JSON.stringify(report, null, 2));
31
+ return;
32
+ }
33
+ for (const line of lifecycleCardLines(report, locale)) io.log(line);
34
+ if (report.failure) io.log(`Failure phase: ${report.failure.phase}: ${report.failure.message}`);
35
+ if (report.retained.length) {
36
+ io.log("Retained:");
37
+ for (const item of report.retained) io.log(`- ${typeof item === "string" ? item : JSON.stringify(item)}`);
38
+ }
39
+ }
40
+
41
+ export function printGeneralStatus(report, { json = false, locale = "en", io = console } = {}) {
42
+ if (json) {
43
+ io.log(JSON.stringify(report, null, 2));
44
+ return;
45
+ }
46
+ const c = copy(locale);
47
+ io.log(c.statusTitle);
48
+ io.log(`${c.installation}: ${report.installation.status}${report.installation.version ? ` (${report.installation.version})` : ""}`);
49
+ io.log(`${c.repository}: ${report.repository.status}`);
50
+ io.log(`${c.delivery}: ${report.delivery.status}${report.delivery.current_gate ? ` (${report.delivery.current_gate})` : ""}`);
51
+ io.log(`${c.next}: ${report.next_action.text}`);
52
+ }
@@ -0,0 +1,69 @@
1
+ const OPERATIONS = new Set(["install", "update", "status", "disable", "uninstall", "repository_setup"]);
2
+ const RESULTS = new Set(["success", "partial", "failed", "preview"]);
3
+ const SCOPES = new Set(["global", "repository"]);
4
+
5
+ function text(value) {
6
+ return typeof value === "string" ? value.trim() : "";
7
+ }
8
+
9
+ export function createLifecycleResult(input = {}) {
10
+ const result = {
11
+ schema_version: 1,
12
+ operation: text(input.operation),
13
+ result: text(input.result),
14
+ surface: text(input.surface),
15
+ scope: text(input.scope),
16
+ version: {
17
+ expected: text(input.version?.expected) || null,
18
+ installed: text(input.version?.installed) || null,
19
+ previous: text(input.version?.previous) || null,
20
+ status: text(input.version?.status) || "unknown",
21
+ transition: text(input.version?.transition) || null,
22
+ },
23
+ verification: {
24
+ status: text(input.verification?.status) || "unknown",
25
+ evidence: [...(input.verification?.evidence ?? [])].map(String),
26
+ },
27
+ restart: {
28
+ required: Boolean(input.restart?.required),
29
+ reason: text(input.restart?.reason) || "none",
30
+ },
31
+ next_action: {
32
+ kind: text(input.next_action?.kind) || "none",
33
+ text: text(input.next_action?.text) || "No further action required.",
34
+ },
35
+ changes: [...(input.changes ?? [])],
36
+ retained: [...(input.retained ?? [])],
37
+ failure: input.failure ?? null,
38
+ };
39
+ assertLifecycleResult(result);
40
+ return Object.freeze(result);
41
+ }
42
+
43
+ export function assertLifecycleResult(value) {
44
+ if (value?.schema_version !== 1) throw new Error("Lifecycle result requires schema_version 1.");
45
+ if (!OPERATIONS.has(value.operation)) throw new Error(`Unsupported lifecycle operation: ${value.operation || "missing"}.`);
46
+ if (!RESULTS.has(value.result)) throw new Error(`Unsupported lifecycle result: ${value.result || "missing"}.`);
47
+ if (!value.surface) throw new Error("Lifecycle result requires a surface.");
48
+ if (!SCOPES.has(value.scope)) throw new Error(`Unsupported lifecycle scope: ${value.scope || "missing"}.`);
49
+ if (!value.next_action || Array.isArray(value.next_action) || !text(value.next_action.text)) {
50
+ throw new Error("Lifecycle result requires exactly one next action.");
51
+ }
52
+ if (value.version.status === "verified" && !value.version.installed) {
53
+ throw new Error("A verified lifecycle version requires an observed installed version.");
54
+ }
55
+ if (value.result === "failed" && !value.failure) throw new Error("Failed lifecycle results require failure evidence.");
56
+ return value;
57
+ }
58
+
59
+ export function lifecycleFailure({ operation, surface, scope, phase, message, evidence = [], nextAction }) {
60
+ return createLifecycleResult({
61
+ operation,
62
+ result: "failed",
63
+ surface,
64
+ scope,
65
+ verification: { status: "degraded", evidence },
66
+ next_action: { kind: "recovery", text: nextAction },
67
+ failure: { phase, message },
68
+ });
69
+ }
@@ -0,0 +1,108 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { pluginDefinition } from "../cli/runtime-context.js";
4
+ import { evaluateDoctor } from "../control-evaluation/doctor.js";
5
+ import { evaluateGateCheck } from "../control-evaluation/gate-check.js";
6
+ import { evaluateOpenCodeStatus } from "../installers/opencode.js";
7
+ import { inspectPluginSurface } from "../installers/plugin-installers.js";
8
+
9
+ function deliveryStatus(targetDir, selection, dependencies) {
10
+ const controlDir = join(targetDir, ".agdf", "control");
11
+ const hasLiveControl = existsSync(join(controlDir, "runs"))
12
+ || (existsSync(join(controlDir, "AGDF_RUN.md")) && existsSync(join(controlDir, "MASTER_BACKLOG.md")));
13
+ if (!hasLiveControl) {
14
+ return { status: "not_configured", run_id: null, current_gate: null, evidence: [] };
15
+ }
16
+ try {
17
+ const doctor = dependencies.evaluateDoctor(targetDir, selection);
18
+ const gate = dependencies.evaluateGateCheck(targetDir, selection);
19
+ return {
20
+ status: gate.status === "blocked" || doctor.status === "block" ? "blocked" : gate.status === "complete" ? "complete" : "open",
21
+ run_id: selection.runId || null,
22
+ current_gate: gate.current_gate || null,
23
+ evidence: [`doctor:${doctor.status}`, `gate-check:${gate.status}`],
24
+ };
25
+ } catch (error) {
26
+ return { status: "unknown", run_id: selection.runId || null, current_gate: null, evidence: [error.message] };
27
+ }
28
+ }
29
+
30
+ function repositoryStatus(targetDir, surface) {
31
+ if (surface === "codex") {
32
+ const marketplace = join(targetDir, ".agents", "plugins", "marketplace.json");
33
+ const disabled = join(targetDir, ".codex", "config.toml");
34
+ const config = existsSync(disabled) ? readFileSync(disabled, "utf8") : "";
35
+ const isDisabled = ["agdf@agdf-repo", "agdf@agdf"].some((selector) => {
36
+ const escaped = JSON.stringify(selector).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
37
+ const section = config.match(new RegExp(`\\[plugins\\.${escaped}\\][\\s\\S]*?(?=\\n\\[|$)`))?.[0] ?? "";
38
+ return /^enabled\s*=\s*false\s*$/m.test(section);
39
+ });
40
+ return { status: isDisabled ? "disabled" : existsSync(marketplace) ? "active" : "not_configured", scope: "repository", evidence: [marketplace] };
41
+ }
42
+ if (surface === "opencode") {
43
+ const instructions = join(targetDir, ".opencode", pluginDefinition.opencode.instructionsFileName);
44
+ return { status: existsSync(instructions) ? "active" : "not_configured", scope: "repository", evidence: [instructions] };
45
+ }
46
+ return { status: "unknown", scope: "repository", evidence: [] };
47
+ }
48
+
49
+ function inspectInstallation(surface, targetDir, options, deps, exec) {
50
+ if (surface === "opencode") {
51
+ const report = deps.evaluateOpenCodeStatus(targetDir, options.configDir);
52
+ return {
53
+ status: report.status === "configured" && report.package.version_status === "current" ? "healthy" : report.status === "configured" ? "degraded" : "not_installed",
54
+ surface,
55
+ version: report.package.installed_version,
56
+ evidence: [report.global_config.path],
57
+ };
58
+ }
59
+ if (["codex", "claude"].includes(surface)) return deps.inspectPluginSurface(surface, exec);
60
+ return { status: "unknown", surface, version: null, evidence: [`unsupported_status_probe:${surface}`] };
61
+ }
62
+
63
+ export function evaluateGeneralStatus(targetDir, options = {}, dependencies = {}) {
64
+ const deps = {
65
+ evaluateDoctor: dependencies.evaluateDoctor ?? evaluateDoctor,
66
+ evaluateGateCheck: dependencies.evaluateGateCheck ?? evaluateGateCheck,
67
+ evaluateOpenCodeStatus: dependencies.evaluateOpenCodeStatus ?? evaluateOpenCodeStatus,
68
+ inspectPluginSurface: dependencies.inspectPluginSurface ?? inspectPluginSurface,
69
+ };
70
+ let surface = options.surface && options.surface !== "generic" ? options.surface : null;
71
+ let installation;
72
+ if (surface) {
73
+ installation = inspectInstallation(surface, targetDir, options, deps, dependencies.exec);
74
+ } else {
75
+ const probes = ["codex", "claude", "opencode"].map((candidate) => inspectInstallation(candidate, targetDir, options, deps, dependencies.exec));
76
+ const observed = probes.filter((probe) => ["healthy", "degraded"].includes(probe.status));
77
+ if (observed.length === 1) {
78
+ installation = observed[0];
79
+ surface = installation.surface;
80
+ } else {
81
+ surface = observed.length > 1 ? "multiple" : "generic";
82
+ installation = {
83
+ status: "unknown",
84
+ surface,
85
+ version: null,
86
+ evidence: probes.flatMap((probe) => probe.evidence.map((item) => `${probe.surface}:${item}`)),
87
+ };
88
+ }
89
+ }
90
+ const repository = repositoryStatus(targetDir, surface);
91
+ const delivery = deliveryStatus(targetDir, { runId: options.runId }, deps);
92
+ const nextText = surface === "multiple"
93
+ ? "Multiple AGDF host surfaces were detected; rerun status with an explicit --surface."
94
+ : surface === "generic"
95
+ ? "No installed AGDF host surface could be selected safely; rerun status with an explicit --surface."
96
+ : installation.status !== "healthy"
97
+ ? `Run npx --yes @agdf/cli@latest ${surface} to install or repair AGDF.`
98
+ : repository.status === "disabled"
99
+ ? "Restart the host to apply the repository-local disable; the healthy global AGDF installation remains available."
100
+ : repository.status === "not_configured"
101
+ ? `Run npx --yes @agdf/cli@latest ${surface === "codex" ? "codex-repo" : `${surface}-repo`} in this repository.`
102
+ : delivery.status === "blocked"
103
+ ? "Resolve the reported delivery blocker without changing the healthy installation."
104
+ : delivery.status === "not_configured"
105
+ ? "Start a new task; status created no run and requires no approval. Initialize durable control only when the repository needs it."
106
+ : "Continue with the reported delivery next step.";
107
+ return { schema_version: 1, installation, repository, delivery, next_action: { kind: "action", text: nextText } };
108
+ }
@@ -1,5 +1,8 @@
1
+ import { existsSync, readFileSync } from "node:fs";
1
2
  import { join } from "node:path";
2
3
  import { pluginDefinition } from "../cli/runtime-context.js";
4
+ import { printLifecycleResult } from "../lifecycle/presentation.js";
5
+ import { createLifecycleResult } from "../lifecycle/result.js";
3
6
  import { agdfFragmentPath, openCodeConfigFragmentPath } from "./plan.js";
4
7
 
5
8
  const pluginInstallCommand = "npx --yes @agdf/cli@latest claude";
@@ -27,6 +30,27 @@ export function printNextSteps(target, destination, files, wroteAgentsFragment,
27
30
  for (const preserved of preservedFiles) io.log(`- ${preserved}`);
28
31
  }
29
32
 
33
+ if (target === "codex-repo") {
34
+ const verified = files.every((file) => {
35
+ const path = join(destination, file.path);
36
+ return existsSync(path) && readFileSync(path, "utf8") === file.content;
37
+ });
38
+ printLifecycleResult(createLifecycleResult({
39
+ operation: "repository_setup",
40
+ result: verified ? "success" : "partial",
41
+ surface: "codex",
42
+ scope: "repository",
43
+ version: { expected: pluginDefinition.version, status: "expected" },
44
+ verification: { status: verified ? "healthy" : "degraded", evidence: files.map((file) => file.path) },
45
+ restart: { required: true, reason: "host_reload_and_repository_plugin_activation" },
46
+ next_action: {
47
+ kind: "host_action",
48
+ text: "Restart Codex, open /plugins, select This repository and install agdf; then start a new task with: Run an AGDF gate check for this request.",
49
+ },
50
+ }), { locale: JSON.parse(files.find((file) => file.path === join(".agdf", "control", "config.json"))?.content ?? "{}").chat_language, io });
51
+ return;
52
+ }
53
+
30
54
  io.log("");
31
55
  io.log("Next steps:");
32
56
  const languageConfig = files.find((file) => file.path === join(".agdf", "control", "config.json"));
@@ -48,7 +72,7 @@ export function printNextSteps(target, destination, files, wroteAgentsFragment,
48
72
  if (wroteAgentsFragment) {
49
73
  io.log(`- Existing AGENTS.md detected. Merge ${agdfFragmentPath} into your current AGENTS.md before using Copilot with AGDF.`);
50
74
  }
51
- if (target === "codex-repo" || target === "both") {
75
+ if (target === "both") {
52
76
  io.log("- Restart Codex in this repository, open /plugins, select This repository and install agdf.");
53
77
  io.log("- Start a new Codex thread in this repository and ask: Run an AGDF gate check for this request.");
54
78
  }