create-agdf 0.6.9 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,7 +3,7 @@
3
3
  import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
4
4
  import { execFileSync } from "node:child_process";
5
5
  import { homedir } from "node:os";
6
- import { dirname, join, posix, resolve } from "node:path";
6
+ import { dirname, isAbsolute, join, posix, relative, resolve } from "node:path";
7
7
  import { fileURLToPath } from "node:url";
8
8
  import process from "node:process";
9
9
  import { searchInputFromControl } from "../lib/delivery-path-search/state-adapter.js";
@@ -32,14 +32,23 @@ const generatedRoot = join(packageRoot, "generated");
32
32
  const pluginDefinitionPath = join(generatedRoot, "plugins", "agdf", "meta", "agdf-plugin.definition.json");
33
33
  const pluginDefinition = JSON.parse(readFileSync(pluginDefinitionPath, "utf8"));
34
34
  const pluginInstallCommand = "npx --yes @agdf/cli@latest claude";
35
- const npmCommand = process.platform === "win32" ? process.execPath : "npm";
36
- const npmPrefixArgs = process.platform === "win32" ? [join(dirname(process.execPath), "node_modules", "npm", "bin", "npm-cli.js")] : [];
35
+ const testNpmCliPath = process.env.NODE_ENV === "test" ? process.env.AGDF_TEST_NPM_CLI_PATH || "" : "";
36
+ const npmCommand = testNpmCliPath
37
+ ? process.execPath
38
+ : process.platform === "win32"
39
+ ? process.execPath
40
+ : "npm";
41
+ const npmPrefixArgs = testNpmCliPath
42
+ ? [testNpmCliPath]
43
+ : process.platform === "win32"
44
+ ? [join(dirname(process.execPath), "node_modules", "npm", "bin", "npm-cli.js")]
45
+ : [];
37
46
  const allowedTargets = new Set(["codex", "codex-repo", "claude", "copilot", "opencode", "opencode-status", "opencode-repo", "both", "init", "config", "doctor", "gate-check", "delivery-map", "delivery-path-search", "run-create", "run-migrate", "run-render-legacy"]);
38
47
  const agdfFragmentPath = "AGENTS.agdf.md";
39
48
  const openCodeConfigFragmentPath = "opencode.agdf.json";
40
49
  const userGateOrder = ["UR", "PRD", "SD", "TP", "QA", "UAT"];
41
50
  const durableGateArtefacts = new Set(["UR", "PRD", "SD", "TP", "QA"]);
42
- const internalStepArtefacts = new Set(["Brownfield Review"]);
51
+ const internalStepArtefacts = new Set(["Brownfield Review", "Verified Change", "Brownfield Analysis", "CD+Tests", "CR"]);
43
52
  const deliveryRelationships = [
44
53
  { from: "UR", relationship: "approved_by", to: "Approval: UR", requiredBy: "UR" },
45
54
  { from: "PRD", relationship: "derived_from", to: "UR", requiredBy: "PRD" },
@@ -52,6 +61,7 @@ const nextSkillByGate = {
52
61
  "Brownfield Review": "brownfield-analysis",
53
62
  "Mode/Slice Decision": "gate-check",
54
63
  "Quick Task Execution": "none",
64
+ "Verified Change Execution": "gate-check",
55
65
  PRD: "gate-check",
56
66
  SD: "gate-check",
57
67
  TP: "gate-check",
@@ -62,21 +72,6 @@ const nextSkillByGate = {
62
72
  UAT: "delivery-closeout",
63
73
  OR: "release-or",
64
74
  };
65
- const gateProgressOrder = [
66
- "UR",
67
- "Brownfield Review",
68
- "Mode/Slice Decision",
69
- "Quick Task Execution",
70
- "PRD",
71
- "SD",
72
- "TP",
73
- "Brownfield Analysis",
74
- "CD+Tests",
75
- "CR",
76
- "QA",
77
- "UAT",
78
- "OR",
79
- ];
80
75
  const codexSkillNames = pluginDefinition.skillSet.map((skill) => `${pluginDefinition.codex.skillPrefix}${skill.slug}`);
81
76
  const copilotSkillNames = pluginDefinition.skillSet.map((skill) => `${pluginDefinition.copilot.skillPrefix}${skill.slug}`);
82
77
  const openCodeSkillNames = pluginDefinition.skillSet.map((skill) => `${pluginDefinition.opencode.skillPrefix}${skill.slug}`);
@@ -96,6 +91,7 @@ const codexPluginFiles = [
96
91
  join("plugins", "agdf", "control", "templates", "AGENT_QUALITY_CONTRACTS.json"),
97
92
  join("plugins", "agdf", "control", "templates", "artefacts", "UR.md"),
98
93
  join("plugins", "agdf", "control", "templates", "artefacts", "BROWNFIELD_REVIEW.md"),
94
+ join("plugins", "agdf", "control", "templates", "artefacts", "VERIFIED_CHANGE.md"),
99
95
  join("plugins", "agdf", "control", "templates", "artefacts", "PRD.md"),
100
96
  join("plugins", "agdf", "control", "templates", "artefacts", "SD.md"),
101
97
  join("plugins", "agdf", "control", "templates", "artefacts", "TP.md"),
@@ -120,6 +116,7 @@ const controlFiles = [
120
116
  join(".agdf", "control", "templates", "AGENT_QUALITY_CONTRACTS.json"),
121
117
  join(".agdf", "control", "templates", "artefacts", "UR.md"),
122
118
  join(".agdf", "control", "templates", "artefacts", "BROWNFIELD_REVIEW.md"),
119
+ join(".agdf", "control", "templates", "artefacts", "VERIFIED_CHANGE.md"),
123
120
  join(".agdf", "control", "templates", "artefacts", "PRD.md"),
124
121
  join(".agdf", "control", "templates", "artefacts", "SD.md"),
125
122
  join(".agdf", "control", "templates", "artefacts", "TP.md"),
@@ -151,6 +148,7 @@ const liveControlFiles = [
151
148
  const artefactTemplateFiles = [
152
149
  join(".agdf", "control", "templates", "artefacts", "UR.md"),
153
150
  join(".agdf", "control", "templates", "artefacts", "BROWNFIELD_REVIEW.md"),
151
+ join(".agdf", "control", "templates", "artefacts", "VERIFIED_CHANGE.md"),
154
152
  join(".agdf", "control", "templates", "artefacts", "PRD.md"),
155
153
  join(".agdf", "control", "templates", "artefacts", "SD.md"),
156
154
  join(".agdf", "control", "templates", "artefacts", "TP.md"),
@@ -481,6 +479,7 @@ function defaultOpenCodeConfigDir() {
481
479
 
482
480
  function installOpenCodeGlobalPlugin(configDir) {
483
481
  assertGlobalOpenCodeSurfaceWritable(configDir);
482
+ const previousPackage = resolveOpenCodePackage(configDir);
484
483
  const configPath = join(configDir, "opencode.json");
485
484
  let config = {};
486
485
 
@@ -528,15 +527,21 @@ function installOpenCodeGlobalPlugin(configDir) {
528
527
 
529
528
  mkdirSync(configDir, { recursive: true });
530
529
  try {
531
- execFileSync(npmCommand, [...npmPrefixArgs, "install", "--silent", "--save-prod", "--prefix", configDir, packageRoot], { stdio: "pipe" });
530
+ const packageSpecifier = `${pluginDefinition.opencode.npmPackage}@${pluginDefinition.version}`;
531
+ execFileSync(npmCommand, [...npmPrefixArgs, "install", "--silent", "--save-prod", "--save-exact", packageSpecifier], {
532
+ cwd: configDir,
533
+ stdio: "pipe",
534
+ });
532
535
  } catch (error) {
533
536
  throw new Error(`Failed to install ${pluginDefinition.opencode.npmPackage} into the OpenCode config directory: ${(error.stderr || error.message).toString().trim()}`);
534
537
  }
535
538
  writeFileSync(configPath, `${JSON.stringify(nextConfig, null, 2)}\n`, "utf8");
539
+ const installedPackage = resolveOpenCodePackage(configDir);
536
540
 
537
541
  return {
538
542
  configPath,
539
543
  added: !alreadyInstalled,
544
+ transition: openCodePackageTransition(previousPackage, installedPackage),
540
545
  };
541
546
  }
542
547
 
@@ -661,24 +666,64 @@ function isOpenCodeRepositorySurfacePresent(targetDir) {
661
666
  function resolveOpenCodePackage(configDir) {
662
667
  const packageName = pluginDefinition.opencode.npmPackage;
663
668
  try {
669
+ const resolvedPath = execFileSync(process.execPath, ["-e", `process.stdout.write(require.resolve(${JSON.stringify(packageName)}))`], {
670
+ cwd: configDir,
671
+ encoding: "utf8",
672
+ stdio: "pipe",
673
+ });
674
+ let installedVersion = "";
675
+ try {
676
+ const packageManifest = JSON.parse(readFileSync(join(dirname(resolvedPath), "package.json"), "utf8"));
677
+ installedVersion = typeof packageManifest.version === "string" && packageManifest.version.trim() ? packageManifest.version.trim() : "";
678
+ } catch {
679
+ installedVersion = "";
680
+ }
664
681
  return {
665
682
  loadable: true,
666
- path: execFileSync(process.execPath, ["-e", `process.stdout.write(require.resolve(${JSON.stringify(packageName)}))`], {
667
- cwd: configDir,
668
- encoding: "utf8",
669
- stdio: "pipe",
670
- }),
683
+ path: resolvedPath,
684
+ installed_version: installedVersion,
671
685
  error: "",
672
686
  };
673
687
  } catch (error) {
674
688
  return {
675
689
  loadable: false,
676
690
  path: "",
691
+ installed_version: "",
677
692
  error: (error.stderr || error.message || "package not resolvable").toString().trim(),
678
693
  };
679
694
  }
680
695
  }
681
696
 
697
+ function openCodePackageVersionStatus(packageState) {
698
+ if (!packageState.loadable) return "unloadable";
699
+ if (!packageState.installed_version) return "unknown";
700
+ return packageState.installed_version === pluginDefinition.version ? "current" : "outdated";
701
+ }
702
+
703
+ function openCodePackageTransition(previousPackage, installedPackage) {
704
+ const previousVersion = previousPackage.loadable ? previousPackage.installed_version : "";
705
+ const installedVersion = installedPackage.loadable ? installedPackage.installed_version : "";
706
+ if (!installedVersion || (previousPackage.loadable && !previousVersion)) {
707
+ return {
708
+ previous_version: previousVersion,
709
+ installed_version: installedVersion,
710
+ status: "unknown",
711
+ };
712
+ }
713
+ if (!previousPackage.loadable) {
714
+ return {
715
+ previous_version: "",
716
+ installed_version: installedVersion,
717
+ status: "installed",
718
+ };
719
+ }
720
+ return {
721
+ previous_version: previousVersion,
722
+ installed_version: installedVersion,
723
+ status: previousVersion === installedVersion ? "unchanged" : "updated",
724
+ };
725
+ }
726
+
682
727
  function evaluateGlobalOpenCodeSurface(configDir) {
683
728
  const paths = globalOpenCodeConfigPaths(configDir);
684
729
  const skillCount = globalOpenCodeSkillNames.filter((skillName) => existsSync(join(paths.skills, skillName, "SKILL.md"))).length;
@@ -693,12 +738,13 @@ function evaluateGlobalOpenCodeSurface(configDir) {
693
738
  };
694
739
  }
695
740
 
696
- function evaluateOpenCodeStatus(targetDir, configDir = defaultOpenCodeConfigDir()) {
741
+ function evaluateOpenCodeStatus(targetDir, configDir = defaultOpenCodeConfigDir(), transition = null) {
697
742
  const configPath = join(configDir, "opencode.json");
698
743
  const configState = readOpenCodeConfig(configPath);
699
744
  const plugins = Array.isArray(configState.config.plugin) ? configState.config.plugin : [];
700
745
  const globalConfigured = plugins.includes(pluginDefinition.opencode.npmPackage);
701
746
  const packageState = resolveOpenCodePackage(configDir);
747
+ const packageVersionStatus = openCodePackageVersionStatus(packageState);
702
748
  const globalNativeSurface = evaluateGlobalOpenCodeSurface(configDir);
703
749
  const sessionSignals = {
704
750
  active: process.env.AGDF_PLUGIN_ACTIVE === "1",
@@ -714,6 +760,8 @@ function evaluateOpenCodeStatus(targetDir, configDir = defaultOpenCodeConfigDir(
714
760
  if (configState.parseError) findings.push(`OpenCode global config is not valid JSON: ${configState.parseError}`);
715
761
  if (!globalConfigured) findings.push(`OpenCode global config does not include ${pluginDefinition.opencode.npmPackage}.`);
716
762
  if (!packageState.loadable) findings.push(`${pluginDefinition.opencode.npmPackage} is not loadable from the OpenCode config directory.`);
763
+ if (packageVersionStatus === "outdated") findings.push(`${pluginDefinition.opencode.npmPackage} version ${packageState.installed_version} is outdated; expected ${pluginDefinition.version}.`);
764
+ if (packageVersionStatus === "unknown" && packageState.loadable) findings.push(`${pluginDefinition.opencode.npmPackage} is loadable but its installed version is unknown.`);
717
765
  if (!globalNativeSurface.complete) findings.push(`Global OpenCode native skill surface is incomplete (${globalNativeSurface.skill_count}/${globalNativeSurface.expected_skill_count} skills).`);
718
766
  if (!sessionSignals.active) findings.push("No active AGDF OpenCode session signal is visible in this process.");
719
767
  if (!repositorySurface) findings.push("Current repository does not contain the AGDF OpenCode surface.");
@@ -731,6 +779,10 @@ function evaluateOpenCodeStatus(targetDir, configDir = defaultOpenCodeConfigDir(
731
779
  name: pluginDefinition.opencode.npmPackage,
732
780
  loadable: packageState.loadable,
733
781
  resolved_path: packageState.path,
782
+ installed_version: packageState.installed_version || null,
783
+ expected_version: pluginDefinition.version,
784
+ version_status: packageVersionStatus,
785
+ ...(transition ? { transition } : {}),
734
786
  error: packageState.error,
735
787
  },
736
788
  global_native_surface: globalNativeSurface,
@@ -744,7 +796,9 @@ function evaluateOpenCodeStatus(targetDir, configDir = defaultOpenCodeConfigDir(
744
796
  },
745
797
  visible_entrypoint: repositorySurface ? `${pluginDefinition.opencode.skillPrefix}gate-check (native skill)` : "none until opencode-repo is installed for this repository",
746
798
  findings,
747
- next_step: !globalNativeSurface.complete
799
+ next_step: packageVersionStatus !== "current"
800
+ ? "Run npx --yes @agdf/cli@latest opencode to install or repair the OpenCode package version."
801
+ : !globalNativeSurface.complete
748
802
  ? "Run npx --yes @agdf/cli@latest opencode to install or repair the global native OpenCode skill surface."
749
803
  : repositorySurface
750
804
  ? `Restart OpenCode if needed, then load ${pluginDefinition.opencode.skillPrefix}gate-check through the native skill tool for new build/change intent.`
@@ -764,6 +818,18 @@ function printOpenCodeStatus(report, json) {
764
818
  if (report.global_config.parse_error) console.log(`Config parse error: ${report.global_config.parse_error}`);
765
819
  console.log(`Package loadable: ${report.package.loadable ? "yes" : "no"}`);
766
820
  if (report.package.resolved_path) console.log(`Package path: ${report.package.resolved_path}`);
821
+ console.log(`Package version: ${report.package.installed_version || "unknown"}`);
822
+ console.log(`Expected version: ${report.package.expected_version}`);
823
+ console.log(`Version status: ${report.package.version_status}`);
824
+ if (report.package.transition?.status === "updated") {
825
+ console.log(`Version transition: ${report.package.transition.previous_version} -> ${report.package.transition.installed_version}`);
826
+ } else if (report.package.transition?.status === "installed") {
827
+ console.log(`Version transition: new install (${report.package.transition.installed_version || "unknown"})`);
828
+ } else if (report.package.transition?.status === "unchanged") {
829
+ console.log(`Version transition: unchanged (${report.package.transition.installed_version})`);
830
+ } else if (report.package.transition?.status === "unknown") {
831
+ console.log("Version transition: unknown");
832
+ }
767
833
  console.log(`Global native skills: ${report.global_native_surface.complete ? "complete" : "incomplete"} (${report.global_native_surface.skill_count}/${report.global_native_surface.expected_skill_count})`);
768
834
  console.log(`Global skill path: ${report.global_native_surface.path}`);
769
835
  console.log(`Session active signal: ${report.session.active ? "yes" : "no"}`);
@@ -1647,6 +1713,12 @@ function evaluateDoctor(targetDir, selection = {}) {
1647
1713
  }
1648
1714
 
1649
1715
  const runState = selectedRunState;
1716
+ if (modeSliceDecision(runState) === "verified_change") {
1717
+ const verifiedChange = evaluateVerifiedChange(targetDir, runState);
1718
+ for (const finding of verifiedChange.findings) {
1719
+ addFinding(findings, finding.severity, finding.code, finding.message, finding.path, finding.next_step);
1720
+ }
1721
+ }
1650
1722
  for (const finding of analyzeDurableGateArtefactConsistency(runState)) {
1651
1723
  addFinding(findings, finding.severity, finding.code, finding.message, finding.path, finding.next_step);
1652
1724
  }
@@ -1698,6 +1770,115 @@ function extractField(content, field) {
1698
1770
  return content.match(pattern)?.[1]?.trim() ?? "";
1699
1771
  }
1700
1772
 
1773
+ const verifiedChangeStatuses = new Set(["draft", "eligible", "executed", "escalated"]);
1774
+ const verifiedChangeEscalationTargets = new Set(["structured_slice", "structured_delivery"]);
1775
+
1776
+ function parseVerifiedChangePathList(value) {
1777
+ const cleaned = cleanStatusCell(value ?? "");
1778
+ if (!cleaned || cleaned === "none") return [];
1779
+ return cleaned.split(",").map((item) => item.trim()).filter(Boolean);
1780
+ }
1781
+
1782
+ function isSafeRepoRelativePath(value) {
1783
+ if (!value || value === "none" || value.startsWith("/") || value.includes("\\")) return false;
1784
+ const normalized = posix.normalize(value);
1785
+ return normalized !== "." && normalized !== ".." && !normalized.startsWith("../") && normalized === value;
1786
+ }
1787
+
1788
+ function gitPathList(targetDir, args) {
1789
+ try {
1790
+ return execFileSync("git", args, { cwd: targetDir, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] })
1791
+ .split("\n")
1792
+ .map((item) => item.trim())
1793
+ .filter(Boolean);
1794
+ } catch {
1795
+ return null;
1796
+ }
1797
+ }
1798
+
1799
+ function readVerifiedChangeRecord(targetDir, runState) {
1800
+ const artefact = runState.artefacts.get("Verified Change");
1801
+ if (!artefact?.path || isPlaceholderValue(artefact.path)) return { status: "missing", path: "", content: "" };
1802
+ if (!isSafeRepoRelativePath(artefact.path)) return { status: "invalid", path: artefact.path, content: "", error: "record_path_invalid" };
1803
+ const absolutePath = resolve(targetDir, artefact.path);
1804
+ const relativePath = relative(resolve(targetDir), absolutePath);
1805
+ if (isAbsolute(relativePath) || relativePath === ".." || relativePath.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) || !existsSync(absolutePath)) {
1806
+ return { status: "missing", path: artefact.path, content: "", error: "record_missing" };
1807
+ }
1808
+ return { status: "present", path: artefact.path, content: readFileSync(absolutePath, "utf8") };
1809
+ }
1810
+
1811
+ function evaluateVerifiedChange(targetDir, runState) {
1812
+ const record = readVerifiedChangeRecord(targetDir, runState);
1813
+ const findings = [];
1814
+ const add = (code, message, nextStep, severity = "revise") => findings.push({
1815
+ severity,
1816
+ code,
1817
+ message,
1818
+ path: record.path || runState.path,
1819
+ next_step: nextStep,
1820
+ });
1821
+
1822
+ if (record.status === "missing") {
1823
+ return { status: "missing", record, findings };
1824
+ }
1825
+ if (record.status === "invalid") {
1826
+ add("AGDF_VERIFIED_CHANGE_RECORD_PATH_INVALID", "Verified Change record path must be a repository-relative path inside the target repository.", "Link a stable repository-relative VERIFIED_CHANGE.md artefact from the selected run state.", "block");
1827
+ return { status: "invalid", record, findings };
1828
+ }
1829
+
1830
+ const status = cleanStatusCell(extractField(record.content, "status"));
1831
+ const relatedUr = cleanStatusCell(extractField(record.content, "related_ur"));
1832
+ const escalationTarget = cleanStatusCell(extractField(record.content, "escalation_target"));
1833
+ const canonicalOwner = cleanStatusCell(extractField(record.content, "canonical_owner"));
1834
+ const sourcePaths = parseVerifiedChangePathList(extractField(record.content, "allowed_source_paths"));
1835
+ const derivedPaths = parseVerifiedChangePathList(extractField(record.content, "allowed_derived_paths"));
1836
+ const prohibitedImpacts = cleanStatusCell(extractField(record.content, "prohibited_impacts"));
1837
+ const propagationCommand = extractField(record.content, "propagation_command");
1838
+ const validationCommands = extractField(record.content, "validation_commands");
1839
+ const baselineTracked = parseVerifiedChangePathList(extractField(record.content, "baseline_tracked_paths"));
1840
+ const baselineUntracked = parseVerifiedChangePathList(extractField(record.content, "baseline_untracked_paths"));
1841
+ const validationStatus = cleanStatusCell(extractField(record.content, "validation_status"));
1842
+ const propagationStatus = cleanStatusCell(extractField(record.content, "propagation_status"));
1843
+ const urArtefact = runState.artefacts.get("UR");
1844
+
1845
+ if (!verifiedChangeStatuses.has(status)) add("AGDF_VERIFIED_CHANGE_STATUS_INVALID", "Verified Change record must use status draft, eligible, executed or escalated.", "Set a supported record status or escalate to the declared structured target.");
1846
+ if (!isSafeRepoRelativePath(relatedUr) || relatedUr !== urArtefact?.path) add("AGDF_VERIFIED_CHANGE_RELATED_UR_INVALID", "Verified Change record must link exactly to the selected run's repository-relative UR artefact.", "Set related_ur to the selected run's durable UR artefact path.");
1847
+ if (!verifiedChangeEscalationTargets.has(escalationTarget)) add("AGDF_VERIFIED_CHANGE_ESCALATION_INVALID", "Verified Change record must declare structured_slice or structured_delivery as its escalation target.", "Record the Brownfield-selected structured escalation target.");
1848
+ if (!canonicalOwner || canonicalOwner.includes(",") || !isSafeRepoRelativePath(canonicalOwner)) add("AGDF_VERIFIED_CHANGE_OWNER_INVALID", "Verified Change requires exactly one repository-relative canonical_owner.", "Name one canonical owner path; otherwise escalate.");
1849
+ if (sourcePaths.length === 0 || sourcePaths.some((path) => !isSafeRepoRelativePath(path))) add("AGDF_VERIFIED_CHANGE_SOURCE_PATHS_INVALID", "Verified Change requires non-empty normalized allowed_source_paths.", "Declare a bounded comma-separated source path list; otherwise escalate.");
1850
+ if (derivedPaths.some((path) => !isSafeRepoRelativePath(path))) add("AGDF_VERIFIED_CHANGE_DERIVED_PATHS_INVALID", "Verified Change derived paths must be normalized repository-relative paths or none.", "Correct the derived path list or escalate.");
1851
+ if (prohibitedImpacts !== "none") add("AGDF_VERIFIED_CHANGE_IMPACTS_INVALID", "Verified Change must declare prohibited_impacts: none after checking gates, permissions, security, persistence, architecture, external API, CLI and release behavior.", "Use the structured path when any prohibited impact applies.");
1852
+ if (!validationCommands || validationCommands === "none") add("AGDF_VERIFIED_CHANGE_VALIDATION_MISSING", "Verified Change requires at least one deterministic validation command.", "Record a deterministic acceptance or consistency check, or escalate.");
1853
+ if (derivedPaths.length > 0 && (!propagationCommand || propagationCommand === "none")) add("AGDF_VERIFIED_CHANGE_PROPAGATION_MISSING", "Derived paths require a deterministic propagation command.", "Record the propagation command or escalate.");
1854
+ if (!extractField(record.content, "baseline_tracked_paths") || !extractField(record.content, "baseline_untracked_paths")) add("AGDF_VERIFIED_CHANGE_BASELINE_MISSING", "Verified Change requires both tracked and untracked baseline path fields, using none when empty.", "Capture the worktree baseline before marking the record eligible.");
1855
+
1856
+ const allowedPaths = new Set([...sourcePaths, ...derivedPaths]);
1857
+ const baselinePaths = new Set([...baselineTracked, ...baselineUntracked]);
1858
+ const dirtyCandidate = [...allowedPaths].find((path) => baselinePaths.has(path));
1859
+ if (dirtyCandidate) add("AGDF_VERIFIED_CHANGE_BASELINE_CANDIDATE_DIRTY", `Declared candidate path is already dirty at baseline: ${dirtyCandidate}.`, "Escalate or start from a clean candidate path; do not adopt pre-existing edits.", "block");
1860
+
1861
+ const currentTracked = gitPathList(targetDir, ["diff", "HEAD", "--name-only"]);
1862
+ const currentUntracked = gitPathList(targetDir, ["ls-files", "--others", "--exclude-standard"]);
1863
+ if (currentTracked === null || currentUntracked === null) {
1864
+ add("AGDF_VERIFIED_CHANGE_GIT_BASELINE_UNAVAILABLE", "Verified Change requires a readable Git worktree for scoped baseline validation.", "Use the structured path or restore Git worktree access.", "block");
1865
+ } else {
1866
+ const currentPaths = new Set([...currentTracked, ...currentUntracked]);
1867
+ const permittedControlPaths = new Set([record.path, runState.path, ".agdf/control/MASTER_BACKLOG.md"]);
1868
+ const unexpected = [...currentPaths].filter((path) => !baselinePaths.has(path) && !allowedPaths.has(path) && !permittedControlPaths.has(path));
1869
+ if (unexpected.length > 0) add("AGDF_VERIFIED_CHANGE_SCOPE_ESCAPE", `Verified Change introduced unlisted path(s): ${unexpected.join(", ")}.`, "Mark the record escalated and continue at the declared structured target.", "block");
1870
+ }
1871
+
1872
+ if (status === "executed") {
1873
+ if (validationStatus !== "pass") add("AGDF_VERIFIED_CHANGE_VALIDATION_EVIDENCE_MISSING", "Executed Verified Change requires validation_status: pass.", "Record passing deterministic validation evidence or escalate.");
1874
+ if (derivedPaths.length > 0 && propagationStatus !== "pass") add("AGDF_VERIFIED_CHANGE_PROPAGATION_EVIDENCE_MISSING", "Executed Verified Change with derived paths requires propagation_status: pass.", "Record successful propagation evidence or escalate.");
1875
+ }
1876
+
1877
+ if (status === "escalated") return { status: "escalated", record, escalation_target: escalationTarget, findings };
1878
+ if (findings.length > 0) return { status: "invalid", record, escalation_target: escalationTarget, findings };
1879
+ return { status, record, escalation_target: escalationTarget, findings };
1880
+ }
1881
+
1701
1882
  function cleanStatusCell(value) {
1702
1883
  return value.replace(/^`|`$/g, "").trim();
1703
1884
  }
@@ -1753,20 +1934,6 @@ function readRunState(targetDir, selection = {}) {
1753
1934
  };
1754
1935
  }
1755
1936
 
1756
- function firstUnapprovedGate(approvals) {
1757
- for (const gate of userGateOrder) {
1758
- const status = approvals.get(gate)?.status ?? "";
1759
- if (status !== "approved" && status !== "not_applicable") return gate;
1760
- }
1761
- return "OR";
1762
- }
1763
-
1764
- function normalizeCurrentGate(value, fallbackGate) {
1765
- if (isPlaceholderValue(value)) return fallbackGate;
1766
- const normalized = value.replace(/`/g, "").trim();
1767
- return normalized || fallbackGate;
1768
- }
1769
-
1770
1937
  function gateApprovalStatus(runState, gate) {
1771
1938
  if (!userGateOrder.includes(gate)) return "not_applicable";
1772
1939
  return runState.approvals.get(gate)?.status ?? "";
@@ -1820,7 +1987,9 @@ function analyzeDurableGateArtefactConsistency(runState) {
1820
1987
  function isInternalStepSatisfied(runState, step) {
1821
1988
  const artefact = runState.artefacts.get(step);
1822
1989
  if (!artefact) return false;
1823
- return artefact.status === "done" || artefact.status === "not_applicable";
1990
+ if (artefact.status === "done") return true;
1991
+ return artefact.status === "not_applicable"
1992
+ && (step === "Brownfield Review" || step === "Brownfield Analysis");
1824
1993
  }
1825
1994
 
1826
1995
  function modeSliceDecision(runState) {
@@ -2039,15 +2208,6 @@ function buildStatusCard({
2039
2208
  };
2040
2209
  }
2041
2210
 
2042
- function effectiveCurrentGate(runState, transitionDecision) {
2043
- if (transitionDecision.status !== "open") return transitionDecision.current_gate;
2044
- const explicitGate = normalizeCurrentGate(runState.current_gate, "");
2045
- const explicitIndex = gateProgressOrder.indexOf(explicitGate);
2046
- const fallbackIndex = gateProgressOrder.indexOf(transitionDecision.current_gate);
2047
- if (explicitIndex === -1 || fallbackIndex === -1) return transitionDecision.current_gate;
2048
- return explicitIndex > fallbackIndex ? explicitGate : transitionDecision.current_gate;
2049
- }
2050
-
2051
2211
  function durableArtefactBlock(gate, nextGate) {
2052
2212
  const label = gate === "QA" ? "QA report" : `${gate} artefact`;
2053
2213
  const stablePath = gate === "QA" ? ".agdf/control/artefacts/<key>/QA_REPORT.md" : `.agdf/control/artefacts/<key>/${gate}.md`;
@@ -2076,7 +2236,7 @@ function isGateSatisfied(runState, gate) {
2076
2236
  return isDurableGateArtefactSatisfied(runState, gate);
2077
2237
  }
2078
2238
 
2079
- function transitionDecisionForRunState(runState) {
2239
+ function transitionDecisionForRunState(runState, verifiedChange = null) {
2080
2240
  if (gateApprovalStatus(runState, "UR") === "approved" && !isGateSatisfied(runState, "UR")) return durableArtefactBlock("UR", "PRD");
2081
2241
 
2082
2242
  if (!isGateSatisfied(runState, "UR")) {
@@ -2118,7 +2278,7 @@ function transitionDecisionForRunState(runState) {
2118
2278
  blocking_reason: "none",
2119
2279
  missing_approval: "none",
2120
2280
  allowed: [
2121
- "decide whether the approved UR is quick_task, structured_slice, structured_delivery or block",
2281
+ "decide whether the approved UR is quick_task, verified_change, structured_slice, structured_delivery or block",
2122
2282
  "record scope reason, evidence and required next gate depth in the selected RUN_STATE.md",
2123
2283
  "choose the next required gate depth before drafting PRD or implementing",
2124
2284
  ],
@@ -2151,6 +2311,53 @@ function transitionDecisionForRunState(runState) {
2151
2311
  };
2152
2312
  }
2153
2313
 
2314
+ if (modeDecision === "verified_change") {
2315
+ const state = verifiedChange?.status ?? "missing";
2316
+ const escalationTarget = verifiedChange?.escalation_target ?? "structured_slice";
2317
+ if (state === "executed") {
2318
+ return {
2319
+ status: "open",
2320
+ current_gate: "OR",
2321
+ blocking_reason: "none",
2322
+ missing_approval: "none",
2323
+ allowed: ["use the Verified Change mini-closeout", "offer delivery closeout when requested"],
2324
+ forbidden: ["create PRD, SD, TP, QA or UAT by ritual", "commit, push, open PR or release automatically"],
2325
+ next_allowed_action: "Close the executed Verified Change with its compact record and offer delivery closeout; do not perform VCS actions automatically.",
2326
+ };
2327
+ }
2328
+ if (state === "escalated" || (state === "invalid" && verifiedChangeEscalationTargets.has(escalationTarget))) {
2329
+ return {
2330
+ status: "open",
2331
+ current_gate: "PRD",
2332
+ blocking_reason: state === "escalated" ? "verified_change_escalated" : "verified_change_invalid_escalated",
2333
+ missing_approval: "Approval: PRD",
2334
+ allowed: ["draft the structured PRD required by the declared escalation target", "retain the Verified Change escalation evidence"],
2335
+ forbidden: ["implement through Verified Change", "create SD or TP before PRD approval", "claim QA or release readiness"],
2336
+ next_allowed_action: `Proceed as ${escalationTarget}: draft the required PRD and request exact approval: Approval: PRD.`,
2337
+ };
2338
+ }
2339
+ if (state === "eligible") {
2340
+ return {
2341
+ status: "open",
2342
+ current_gate: "Verified Change Execution",
2343
+ blocking_reason: "none",
2344
+ missing_approval: "none",
2345
+ allowed: ["implement only declared source and derived paths", "run declared propagation and validation commands", "record execution evidence and mini-closeout"],
2346
+ forbidden: ["touch unlisted paths", "add prohibited impacts", "claim QA, UAT or release readiness"],
2347
+ next_allowed_action: "Implement only the eligible Verified Change record scope, then record passing propagation and validation evidence.",
2348
+ };
2349
+ }
2350
+ return {
2351
+ status: state === "missing" || state === "draft" ? "open" : "blocked",
2352
+ current_gate: "Verified Change Execution",
2353
+ blocking_reason: state === "invalid" ? "verified_change_invalid" : "verified_change_record_required",
2354
+ missing_approval: "none",
2355
+ allowed: ["create or refine the compact Verified Change record", "capture baseline paths", "prove fail-closed eligibility with doctor and gate-check"],
2356
+ forbidden: ["implement candidate changes", "claim validation, QA, UAT or release readiness"],
2357
+ next_allowed_action: "Create or repair VERIFIED_CHANGE.md, capture the baseline and satisfy every eligibility check before implementation; escalate when any condition cannot be proven.",
2358
+ };
2359
+ }
2360
+
2154
2361
  return {
2155
2362
  status: "open",
2156
2363
  current_gate: "PRD",
@@ -2192,6 +2399,43 @@ function transitionDecisionForRunState(runState) {
2192
2399
  };
2193
2400
  }
2194
2401
 
2402
+ const implementationStepsRequired = gateApprovalStatus(runState, "TP") === "approved";
2403
+ if (implementationStepsRequired && !isInternalStepSatisfied(runState, "Brownfield Analysis")) {
2404
+ return {
2405
+ status: "open",
2406
+ current_gate: "Brownfield Analysis",
2407
+ blocking_reason: "none",
2408
+ missing_approval: "none",
2409
+ allowed: ["run Brownfield Analysis for the approved TP scope", "verify existing owners, reuse paths and regression risks"],
2410
+ forbidden: ["implement before Brownfield evidence supports the approved TP path", "claim QA or release readiness"],
2411
+ next_allowed_action: "Run Brownfield Analysis for the approved TP scope before CD+Tests.",
2412
+ };
2413
+ }
2414
+
2415
+ if (implementationStepsRequired && !isInternalStepSatisfied(runState, "CD+Tests")) {
2416
+ return {
2417
+ status: "open",
2418
+ current_gate: "CD+Tests",
2419
+ blocking_reason: "none",
2420
+ missing_approval: "none",
2421
+ allowed: ["implement the approved TP tasks", "run the approved test plan", "record implementation and test evidence"],
2422
+ forbidden: ["claim QA pass", "request UAT approval", "release"],
2423
+ next_allowed_action: "Implement the approved TP scope, run its tests, and record CD+Tests evidence before CR.",
2424
+ };
2425
+ }
2426
+
2427
+ if (implementationStepsRequired && !isInternalStepSatisfied(runState, "CR")) {
2428
+ return {
2429
+ status: "open",
2430
+ current_gate: "CR",
2431
+ blocking_reason: "none",
2432
+ missing_approval: "none",
2433
+ allowed: ["run mandatory code review", "record correctness, regression, security and maintainability findings", "fix blocking review findings"],
2434
+ forbidden: ["claim QA pass", "request UAT approval", "release"],
2435
+ next_allowed_action: "Run Code Review for the implemented TP scope and resolve blocking findings before QA.",
2436
+ };
2437
+ }
2438
+
2195
2439
  if (gateApprovalStatus(runState, "QA") === "approved" && !isGateSatisfied(runState, "QA")) return durableArtefactBlock("QA", "UAT");
2196
2440
 
2197
2441
  if (isGateSatisfied(runState, "QA") && !isGateSatisfied(runState, "UAT")) {
@@ -2220,30 +2464,38 @@ function transitionDecisionForRunState(runState) {
2220
2464
 
2221
2465
  return {
2222
2466
  status: "open",
2223
- current_gate: "Brownfield Analysis",
2467
+ current_gate: "QA",
2224
2468
  blocking_reason: "none",
2225
- missing_approval: "none",
2226
- allowed: ["run Brownfield Analysis for the approved TP scope", "verify existing owners, reuse paths and regression risks"],
2227
- forbidden: ["implement before Brownfield evidence supports the approved TP path", "claim QA or release readiness"],
2228
- next_allowed_action: "Run Brownfield Analysis for the approved TP scope before CD+Tests.",
2469
+ missing_approval: "Approval: QA",
2470
+ allowed: ["run QA gate", "persist or refine the QA report", "request exact QA approval"],
2471
+ forbidden: ["request UAT approval", "release", "claim delivery readiness before QA approval and report evidence"],
2472
+ next_allowed_action: "Run the QA gate, persist the QA report, and request exact approval: Approval: QA",
2229
2473
  };
2230
2474
  }
2231
2475
 
2232
2476
  function evaluateGateCheck(targetDir, selection = {}) {
2233
2477
  const doctorReport = evaluateDoctor(targetDir, selection);
2234
2478
  const runState = readRunState(targetDir, selection);
2235
- const transitionDecision = transitionDecisionForRunState(runState);
2479
+ const verifiedChange = modeSliceDecision(runState) === "verified_change"
2480
+ ? evaluateVerifiedChange(targetDir, runState)
2481
+ : null;
2482
+ const transitionDecision = transitionDecisionForRunState(runState, verifiedChange);
2236
2483
  const deliveryMap = analyzeDeliveryMap(runState);
2237
2484
  const doctorBlocker = doctorReport.findings.find((finding) => finding.severity === "block");
2238
2485
  const doctorRevise = doctorReport.findings.find((finding) => finding.severity === "revise");
2486
+ const routesInvalidVerifiedChange = modeSliceDecision(runState) === "verified_change"
2487
+ && verifiedChange?.status === "invalid"
2488
+ && verifiedChangeEscalationTargets.has(verifiedChange.escalation_target);
2239
2489
 
2240
2490
  let status = transitionDecision.status;
2241
- let currentGate = effectiveCurrentGate(runState, transitionDecision);
2491
+ let currentGate = transitionDecision.current_gate;
2242
2492
  let blockingReason = transitionDecision.blocking_reason;
2243
2493
  let missingApproval = transitionDecision.missing_approval;
2244
2494
  let allowed = transitionDecision.allowed;
2245
2495
  let forbidden = transitionDecision.forbidden;
2246
- let nextAllowedAction = isPlaceholderValue(runState.next_allowed_action)
2496
+ let nextAllowedAction = modeSliceDecision(runState) === "verified_change"
2497
+ ? transitionDecision.next_allowed_action
2498
+ : isPlaceholderValue(runState.next_allowed_action)
2247
2499
  ? transitionDecision.next_allowed_action
2248
2500
  : runState.next_allowed_action;
2249
2501
 
@@ -2259,13 +2511,13 @@ function evaluateGateCheck(targetDir, selection = {}) {
2259
2511
  ];
2260
2512
  forbidden = ["create PRD", "create SD", "create TP", "run Brownfield Analysis", "implement code", "claim QA or release readiness"];
2261
2513
  nextAllowedAction = "Draft the minimal UR for the request in the response, then ask for exact approval: Approval: UR. Do not write a full .agdf/control scaffold unless durable control state or deterministic setup is explicitly needed.";
2262
- } else if (doctorBlocker) {
2514
+ } else if (doctorBlocker && !routesInvalidVerifiedChange) {
2263
2515
  status = "blocked";
2264
2516
  blockingReason = doctorBlocker.code;
2265
2517
  allowed = ["repair the AGDF control scaffold", ...transitionDecision.allowed, "run doctor again"];
2266
2518
  forbidden = ["create later-gate artefacts beyond the current allowed gate", "implement gated work", "claim QA or release readiness"];
2267
2519
  nextAllowedAction = doctorBlocker.next_step;
2268
- } else if (doctorRevise) {
2520
+ } else if (doctorRevise && !routesInvalidVerifiedChange) {
2269
2521
  status = "blocked";
2270
2522
  blockingReason = doctorRevise.code;
2271
2523
  allowed = [...new Set(["complete the current control-state fields", ...transitionDecision.allowed, "run doctor again"])];
@@ -2311,6 +2563,7 @@ function evaluateGateCheck(targetDir, selection = {}) {
2311
2563
  memory: deliveryMap.memory,
2312
2564
  findings: deliveryMap.findings,
2313
2565
  },
2566
+ verified_change: verifiedChange,
2314
2567
  doctor_report: doctorReport,
2315
2568
  };
2316
2569
  }
@@ -2413,7 +2666,7 @@ function evaluateDeliveryMap(targetDir, selection = {}) {
2413
2666
  const runState = readRunState(targetDir, selection);
2414
2667
  const map = analyzeDeliveryMap(runState);
2415
2668
  const gateDecision = transitionDecisionForRunState(runState);
2416
- const currentGate = effectiveCurrentGate(runState, gateDecision);
2669
+ const currentGate = gateDecision.current_gate;
2417
2670
 
2418
2671
  const severityRank = { block: 3, revise: 2, warn: 1 };
2419
2672
  const deliverySeverity = map.findings.reduce((max, finding) => Math.max(max, severityRank[finding.severity] ?? 0), 0);
@@ -2647,7 +2900,7 @@ async function main() {
2647
2900
  const result = installOpenCodeGlobalPlugin(configDir);
2648
2901
  installOpenCodeGlobalSurface(configDir);
2649
2902
  console.log(`AGDF OpenCode global plugin ${result.added ? "installed" : "already present"}: ${result.configPath}`);
2650
- const report = evaluateOpenCodeStatus(options.dir, configDir);
2903
+ const report = evaluateOpenCodeStatus(options.dir, configDir, result.transition);
2651
2904
  printOpenCodeStatus(report, false);
2652
2905
  console.log("Restart OpenCode so it loads the updated global plugin config.");
2653
2906
  } catch (error) {
@@ -14,6 +14,7 @@ Use this scaffold when a target repository should keep AGDF state outside chat h
14
14
  | `templates/MASTER_BACKLOG.md` | Template for the living backlog pointer: active initiatives and current UR/Brownfield/PRD/SD/TP/QA/OR artefacts |
15
15
  | `templates/artefacts/UR.md` | Template for the durable user requirement artefact of a work item |
16
16
  | `templates/artefacts/BROWNFIELD_REVIEW.md` | Template for the durable Brownfield Review and Mode/Slice Decision |
17
+ | `templates/artefacts/VERIFIED_CHANGE.md` | Compact fail-closed record for an eligible Verified Change |
17
18
  | `templates/artefacts/PRD.md` | Template for the durable product requirements artefact |
18
19
  | `templates/artefacts/SD.md` | Template for the durable solution design artefact |
19
20
  | `templates/artefacts/TP.md` | Template for the durable task and test plan artefact |
@@ -84,7 +85,7 @@ npx --yes @agdf/cli@latest gate-check --json
84
85
  - The selected `runs/<run_id>/RUN_STATE.md` is the current run dashboard.
85
86
  - `config.json` stores the project preference for artefact and chat language; runtime rules stay English.
86
87
  - `MASTER_BACKLOG.md` points to active delivery work; detailed artefacts live beside the work item.
87
- - Brownfield Review records the post-UR Mode/Slice Decision before PRD depth or Quick Task execution is chosen.
88
+ - Brownfield Review records the post-UR Mode/Slice Decision before PRD depth, Verified Change execution or Quick Task execution is chosen.
88
89
  - UR, PRD, SD, TP and QA decisions require durable artefacts or links to the authoritative repository source of truth before the next gate can open.
89
90
  - OR records the run closeout and next permissible step; it does not approve later gates.
90
91
  - `SOT_REGISTRY.md` decides which document owns which domain.