create-agdf 0.3.4 → 0.4.0

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 (33) hide show
  1. package/README.md +29 -11
  2. package/bin/create-agdf.js +546 -32
  3. package/generated/.agdf/control/templates/AGDF_RUN.md +29 -0
  4. package/generated/.agdf/control/templates/AGENT_QUALITY_CONTRACTS.json +48 -0
  5. package/generated/.agdf/control/templates/MASTER_BACKLOG.md +7 -10
  6. package/generated/.agdf/control/templates/artefacts/OR.md +20 -4
  7. package/generated/.github/skills/agdf-gate-check/SKILL.md +7 -0
  8. package/generated/.github/skills/agdf-release-or/SKILL.md +2 -0
  9. package/generated/.github/skills/agdf-runtime-contract.md +132 -0
  10. package/generated/.opencode/AGDF.md +8 -0
  11. package/generated/.opencode/README.md +4 -0
  12. package/generated/.opencode/agdf-runtime-contract.md +132 -0
  13. package/generated/.opencode/agents/agdf-brownfield-analysis.md +0 -5
  14. package/generated/.opencode/agents/agdf-clean-implementation-review.md +0 -5
  15. package/generated/.opencode/agents/agdf-code-review.md +0 -5
  16. package/generated/.opencode/agents/agdf-delivery-closeout.md +0 -5
  17. package/generated/.opencode/agents/agdf-gate-check.md +7 -5
  18. package/generated/.opencode/agents/agdf-qa-gate.md +0 -5
  19. package/generated/.opencode/agents/agdf-release-or.md +2 -5
  20. package/generated/.opencode/agents/agdf-task-plan-review.md +0 -5
  21. package/generated/AGENTS.md +4 -0
  22. package/generated/plugins/agdf/.codex-plugin/plugin.json +1 -1
  23. package/generated/plugins/agdf/control/templates/AGDF_RUN.md +29 -0
  24. package/generated/plugins/agdf/control/templates/AGENT_QUALITY_CONTRACTS.json +48 -0
  25. package/generated/plugins/agdf/control/templates/MASTER_BACKLOG.md +7 -10
  26. package/generated/plugins/agdf/control/templates/artefacts/OR.md +20 -4
  27. package/generated/plugins/agdf/meta/agdf-agent-router.md +4 -0
  28. package/generated/plugins/agdf/meta/agdf-plugin.definition.json +1 -1
  29. package/generated/plugins/agdf/meta/agdf-runtime-contract.md +132 -0
  30. package/generated/plugins/agdf/skills/gate-check/SKILL.md +7 -0
  31. package/generated/plugins/agdf/skills/release-or/SKILL.md +2 -0
  32. package/opencode-plugin.js +16 -2
  33. package/package.json +1 -1
@@ -1,7 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
- import { dirname, join, resolve } from "node:path";
4
+ import { execFileSync } from "node:child_process";
5
+ import { homedir } from "node:os";
6
+ import { dirname, join, posix, resolve } from "node:path";
5
7
  import { fileURLToPath } from "node:url";
6
8
  import process from "node:process";
7
9
 
@@ -10,8 +12,8 @@ const packageRoot = resolve(__dirname, "..");
10
12
  const generatedRoot = join(packageRoot, "generated");
11
13
  const pluginDefinitionPath = join(generatedRoot, "plugins", "agdf", "meta", "agdf-plugin.definition.json");
12
14
  const pluginDefinition = JSON.parse(readFileSync(pluginDefinitionPath, "utf8"));
13
- const pluginInstallCommand = "claude plugin add arndtgold/ai-native-governance-delivery-framework";
14
- const allowedTargets = new Set(["codex", "copilot", "opencode", "both", "init", "config", "doctor", "gate-check", "delivery-map"]);
15
+ const pluginInstallCommand = "npx --yes @agdf/cli@latest claude";
16
+ const allowedTargets = new Set(["codex", "codex-repo", "claude", "copilot", "opencode", "opencode-repo", "both", "init", "config", "doctor", "gate-check", "delivery-map"]);
15
17
  const agdfFragmentPath = "AGENTS.agdf.md";
16
18
  const openCodeConfigFragmentPath = "opencode.agdf.json";
17
19
  const userGateOrder = ["UR", "PRD", "SD", "TP", "QA", "UAT"];
@@ -24,6 +26,36 @@ const deliveryRelationships = [
24
26
  { from: "TP", relationship: "derived_from", to: "SD", requiredBy: "TP" },
25
27
  { from: "QA_REPORT", relationship: "tests", to: "TP", requiredBy: "QA" },
26
28
  ];
29
+ const nextSkillByGate = {
30
+ UR: "gate-check",
31
+ "Brownfield Review": "brownfield-analysis",
32
+ "Mode/Slice Decision": "gate-check",
33
+ "Quick Task Execution": "none",
34
+ PRD: "gate-check",
35
+ SD: "gate-check",
36
+ TP: "gate-check",
37
+ "Brownfield Analysis": "brownfield-analysis",
38
+ "CD+Tests": "none",
39
+ CR: "code-review",
40
+ QA: "qa-gate",
41
+ UAT: "delivery-closeout",
42
+ OR: "release-or",
43
+ };
44
+ const gateProgressOrder = [
45
+ "UR",
46
+ "Brownfield Review",
47
+ "Mode/Slice Decision",
48
+ "Quick Task Execution",
49
+ "PRD",
50
+ "SD",
51
+ "TP",
52
+ "Brownfield Analysis",
53
+ "CD+Tests",
54
+ "CR",
55
+ "QA",
56
+ "UAT",
57
+ "OR",
58
+ ];
27
59
  const codexSkillNames = pluginDefinition.skillSet.map((skill) => `${pluginDefinition.codex.skillPrefix}${skill.slug}`);
28
60
  const copilotSkillNames = pluginDefinition.skillSet.map((skill) => `${pluginDefinition.copilot.skillPrefix}${skill.slug}`);
29
61
  const openCodeSkillNames = pluginDefinition.skillSet.map((skill) => `${pluginDefinition.opencode.skillPrefix}${skill.slug}`);
@@ -126,15 +158,21 @@ function printUsage() {
126
158
 
127
159
  Preferred AGDF CLI:
128
160
  npx --yes @agdf/cli@latest codex
161
+ npx --yes @agdf/cli@latest codex-repo
162
+ npx --yes @agdf/cli@latest claude
129
163
  npx --yes @agdf/cli@latest opencode
164
+ npx --yes @agdf/cli@latest opencode-repo
130
165
  npx --yes @agdf/cli@latest init
131
166
  npx --yes @agdf/cli@latest doctor
132
167
  npx --yes @agdf/cli@latest gate-check --json
133
168
 
134
169
  Scaffold-compatible npm create usage:
135
170
  npm create agdf@latest -- codex
171
+ npm create agdf@latest -- codex-repo
172
+ npm create agdf@latest -- claude
136
173
  npm create agdf@latest -- copilot
137
174
  npm create agdf@latest -- opencode
175
+ npm create agdf@latest -- opencode-repo
138
176
  npm create agdf@latest -- both
139
177
  npm create agdf@latest -- init
140
178
  npm create agdf@latest -- config --language de
@@ -147,7 +185,7 @@ Backward-compatible create-agdf usage:
147
185
  npx --yes create-agdf@latest gate-check --json
148
186
 
149
187
  Options:
150
- --dir <path> Write files into a specific directory
188
+ --dir <path> Write files into a specific directory. With opencode, use this as the OpenCode config directory.
151
189
  --force Overwrite existing generated files
152
190
  --language <de|en>
153
191
  Set AGDF chat and artefact language. Defaults to detected system locale.
@@ -215,6 +253,7 @@ function parseArgs(argv) {
215
253
  let force = false;
216
254
  let json = false;
217
255
  let language;
256
+ let dirExplicit = false;
218
257
 
219
258
  for (let i = 0; i < args.length; i += 1) {
220
259
  const arg = args[i];
@@ -258,6 +297,7 @@ function parseArgs(argv) {
258
297
  process.exit(1);
259
298
  }
260
299
  dir = next;
300
+ dirExplicit = true;
261
301
  i += 1;
262
302
  continue;
263
303
  }
@@ -283,7 +323,7 @@ function parseArgs(argv) {
283
323
  }
284
324
 
285
325
  if (!target || !allowedTargets.has(target)) {
286
- console.error("Please choose one target: codex, copilot, opencode, both, init, config, doctor, gate-check or delivery-map.");
326
+ console.error("Please choose one target: codex, codex-repo, claude, copilot, opencode, opencode-repo, both, init, config, doctor, gate-check or delivery-map.");
287
327
  printUsage();
288
328
  process.exit(1);
289
329
  }
@@ -293,10 +333,69 @@ function parseArgs(argv) {
293
333
  dir: resolve(process.cwd(), dir),
294
334
  force,
295
335
  json,
336
+ dirExplicit,
296
337
  language: resolveLanguagePreference(language),
297
338
  };
298
339
  }
299
340
 
341
+ function defaultOpenCodeConfigDir() {
342
+ return process.env.OPENCODE_CONFIG_DIR || join(homedir(), ".config", "opencode");
343
+ }
344
+
345
+ function installOpenCodeGlobalPlugin(configDir) {
346
+ const configPath = join(configDir, "opencode.json");
347
+ let config = {};
348
+
349
+ if (existsSync(configPath)) {
350
+ try {
351
+ config = JSON.parse(readFileSync(configPath, "utf8"));
352
+ } catch {
353
+ throw new Error(`Refusing to update unreadable OpenCode config: ${configPath}`);
354
+ }
355
+ }
356
+
357
+ if (config.plugin !== undefined && !Array.isArray(config.plugin)) {
358
+ throw new Error(`Refusing to update OpenCode config with non-array plugin field: ${configPath}`);
359
+ }
360
+
361
+ const plugins = [...(config.plugin ?? [])];
362
+ const alreadyInstalled = plugins.includes(pluginDefinition.opencode.npmPackage);
363
+ if (!alreadyInstalled) {
364
+ plugins.push(pluginDefinition.opencode.npmPackage);
365
+ }
366
+
367
+ const nextConfig = {
368
+ "$schema": config.$schema ?? "https://opencode.ai/config.json",
369
+ ...config,
370
+ plugin: plugins,
371
+ };
372
+
373
+ mkdirSync(configDir, { recursive: true });
374
+ writeFileSync(configPath, `${JSON.stringify(nextConfig, null, 2)}\n`, "utf8");
375
+
376
+ return {
377
+ configPath,
378
+ added: !alreadyInstalled,
379
+ };
380
+ }
381
+
382
+ function installCodexGlobalPlugin() {
383
+ try {
384
+ execFileSync("codex", ["plugin", "marketplace", "add", "arndtgold/ai-native-governance-delivery-framework"], { stdio: "inherit" });
385
+ execFileSync("codex", ["plugin", "add", "agdf", "--marketplace", "agdf"], { stdio: "inherit" });
386
+ } catch (error) {
387
+ 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.");
388
+ }
389
+ }
390
+
391
+ function installClaudeGlobalPlugin() {
392
+ try {
393
+ execFileSync("claude", ["plugin", "add", "arndtgold/ai-native-governance-delivery-framework"], { stdio: "inherit" });
394
+ } catch {
395
+ 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.");
396
+ }
397
+ }
398
+
300
399
  function loadAsset(relativePath) {
301
400
  return readFileSync(join(generatedRoot, relativePath), "utf8");
302
401
  }
@@ -355,7 +454,7 @@ function generatedFilesForTarget(target, targetDir, force, languagePreference) {
355
454
  return files;
356
455
  }
357
456
 
358
- if (target === "codex" || target === "both") {
457
+ if (target === "codex-repo" || target === "both") {
359
458
  addLanguageConfig(files, languagePreference);
360
459
  for (const codexPath of codexPluginFiles) {
361
460
  files.push({
@@ -395,7 +494,7 @@ function generatedFilesForTarget(target, targetDir, force, languagePreference) {
395
494
  }
396
495
  }
397
496
 
398
- if (target === "opencode") {
497
+ if (target === "opencode-repo") {
399
498
  addLanguageConfig(files, languagePreference);
400
499
  const openCodeConfigTargetPath = existsSync(join(targetDir, "opencode.json")) && !force ? openCodeConfigFragmentPath : "opencode.json";
401
500
  files.push({
@@ -451,20 +550,23 @@ function printNextSteps(target, destination, files, wroteAgentsFragment, wroteOp
451
550
  if (wroteAgentsFragment) {
452
551
  console.log(`- Existing AGENTS.md detected. Merge ${agdfFragmentPath} into your current AGENTS.md before using Copilot with AGDF.`);
453
552
  }
454
- if (target === "codex" || target === "both") {
553
+ if (target === "codex-repo" || target === "both") {
455
554
  console.log("- Restart Codex in this repository, open /plugins, select This repository and install agdf.");
456
555
  console.log("- Start a new Codex thread in this repository and ask: Run an AGDF gate check for this request.");
457
556
  }
458
557
  if (target === "both") {
459
558
  console.log(`- Optional global Claude Code install: ${pluginInstallCommand}`);
460
559
  }
461
- if (target === "opencode") {
560
+ if (target === "opencode-repo") {
462
561
  if (wroteOpenCodeConfigFragment) {
463
562
  console.log(`- Existing opencode.json detected. Merge ${openCodeConfigFragmentPath} into your current opencode.json so OpenCode loads .opencode/AGDF.md.`);
464
563
  }
465
564
  console.log(`- OpenCode will install the ${pluginDefinition.opencode.npmPackage} npm plugin from opencode.json at startup.`);
565
+ console.log("- Optional: also add create-agdf to ~/.config/opencode/opencode.json plugin[] for a user-wide OpenCode hook.");
566
+ console.log("- The global hook does not replace repository instructions or subagents; this repository's .opencode files remain the AGDF source of truth.");
466
567
  console.log("- Start OpenCode in this repository; it will load opencode.json, .opencode/AGDF.md and the AGDF subagents.");
467
- console.log("- Use @agdf-gate-check for new build/change intent or unclear approval before later artefacts or implementation.");
568
+ console.log("- AGDF OpenCode agents are mode: subagent workflow controls; they are not .opencode/skills entries or primary menu agents.");
569
+ console.log("- Use @agdf-gate-check as the visible entry point for new build/change intent or unclear approval before later artefacts or implementation.");
468
570
  console.log("- Run npx --yes @agdf/cli@latest init when the repository needs live AGDF control files.");
469
571
  }
470
572
  if (target === "copilot" || target === "both") {
@@ -510,6 +612,222 @@ function tableRows(content) {
510
612
  });
511
613
  }
512
614
 
615
+ const compactBacklogHeaders = ["priority", "key", "work item", "status", "artefacts", "current spec", "next step"];
616
+ const legacyBacklogHeaders = ["prio", "key", "title", "status", "ur", "brownfield review", "prd", "sd", "tp", "qa", "or", "current spec", "notes"];
617
+ const backlogStatusLabels = new Map([
618
+ ["needs ur", "needs_ur"],
619
+ ["awaiting brownfield review", "awaiting_brownfield_review"],
620
+ ["awaiting prd", "awaiting_prd"],
621
+ ["awaiting prd approval", "awaiting_prd_approval"],
622
+ ["awaiting sd", "awaiting_sd"],
623
+ ["awaiting sd approval", "awaiting_sd_approval"],
624
+ ["awaiting tp", "awaiting_tp"],
625
+ ["awaiting tp approval", "awaiting_tp_approval"],
626
+ ["in progress", "in_progress"],
627
+ ["blocked", "blocked"],
628
+ ["awaiting qa", "awaiting_qa"],
629
+ ["awaiting uat", "awaiting_uat"],
630
+ ["completed", "completed"],
631
+ ["superseded", "superseded"],
632
+ ["abandoned", "abandoned"],
633
+ ]);
634
+ const backlogArtefactLabels = new Map([
635
+ ["ur", "ur"],
636
+ ["brownfield", "brownfield_review"],
637
+ ["prd", "prd"],
638
+ ["sd", "sd"],
639
+ ["tp", "tp"],
640
+ ["qa", "qa"],
641
+ ["or", "or"],
642
+ ]);
643
+
644
+ function normalizeBacklogHeader(value) {
645
+ return value.replace(/`/g, "").trim().toLowerCase();
646
+ }
647
+
648
+ function markdownLink(value) {
649
+ const match = value.trim().match(/^\[([^\]]+)\]\(([^)]+)\)$/);
650
+ return match ? { label: match[1].trim(), target: match[2].trim() } : null;
651
+ }
652
+
653
+ function resolvedBacklogLinkTarget(target) {
654
+ if (!target
655
+ || target.startsWith("/")
656
+ || target.includes("\\")
657
+ || /^[a-z][a-z0-9+.-]*:/i.test(target)) {
658
+ return null;
659
+ }
660
+ const resolved = posix.normalize(posix.join(".agdf/control", target));
661
+ if (resolved === ".." || resolved.startsWith("../")) return null;
662
+ return resolved;
663
+ }
664
+
665
+ function normalizedBacklogLinkTarget(value, findings, backlogPath, label) {
666
+ const link = markdownLink(value);
667
+ if (!link) return value.trim();
668
+ const resolved = resolvedBacklogLinkTarget(link.target);
669
+ if (!resolved) {
670
+ if (findings) {
671
+ addFinding(
672
+ findings,
673
+ "revise",
674
+ "AGDF_BACKLOG_LINK_TARGET_INVALID",
675
+ `Backlog ${label} link must be relative to MASTER_BACKLOG.md: ${link.target || "<empty>"}.`,
676
+ backlogPath,
677
+ "Use a document-relative artefact target such as artefacts/<key>/UR.md.",
678
+ );
679
+ }
680
+ return link.target;
681
+ }
682
+ return resolved;
683
+ }
684
+
685
+ function normalizeBacklogStatus(value, findings, backlogPath) {
686
+ const cleaned = cleanStatusCell(value ?? "");
687
+ const lookup = cleaned.replaceAll("_", " ").toLowerCase();
688
+ const normalized = backlogStatusLabels.get(lookup);
689
+ if (normalized) return normalized;
690
+ if (/^[a-z][a-z0-9_]*$/.test(cleaned)) return cleaned;
691
+ if (findings && filled(cleaned)) {
692
+ addFinding(
693
+ findings,
694
+ "revise",
695
+ "AGDF_BACKLOG_STATUS_UNKNOWN",
696
+ `MASTER_BACKLOG.md uses an unknown human status label: ${cleaned}.`,
697
+ backlogPath,
698
+ "Use a documented human status label or a supported legacy snake_case value.",
699
+ );
700
+ }
701
+ return cleaned;
702
+ }
703
+
704
+ function emptyBacklogPointer() {
705
+ return {
706
+ prio: "",
707
+ key: "",
708
+ title: "",
709
+ status: "",
710
+ ur: "",
711
+ brownfield_review: "",
712
+ prd: "",
713
+ sd: "",
714
+ tp: "",
715
+ qa: "",
716
+ or: "",
717
+ current_spec: "",
718
+ notes: "",
719
+ };
720
+ }
721
+
722
+ function parseCompactArtefacts(value, pointer, findings, backlogPath) {
723
+ if (!filled(value)) return;
724
+ const seen = new Set();
725
+ for (const entry of value.split(/\s+·\s+/)) {
726
+ const link = markdownLink(entry);
727
+ if (!link) {
728
+ if (findings) {
729
+ addFinding(
730
+ findings,
731
+ "revise",
732
+ "AGDF_BACKLOG_ARTEFACT_LINK_INVALID",
733
+ `Compact Artefacts entry is not a Markdown link: ${entry}.`,
734
+ backlogPath,
735
+ "Use [UR](artefacts/<key>/UR.md) style links separated by a middle dot.",
736
+ );
737
+ }
738
+ continue;
739
+ }
740
+ const field = backlogArtefactLabels.get(link.label.toLowerCase());
741
+ if (!field) {
742
+ if (findings) {
743
+ addFinding(
744
+ findings,
745
+ "revise",
746
+ "AGDF_BACKLOG_ARTEFACT_LABEL_UNKNOWN",
747
+ `Compact Artefacts uses an unknown label: ${link.label}.`,
748
+ backlogPath,
749
+ "Use UR, Brownfield, PRD, SD, TP, QA or OR.",
750
+ );
751
+ }
752
+ continue;
753
+ }
754
+ if (seen.has(field)) {
755
+ if (findings) {
756
+ addFinding(
757
+ findings,
758
+ "revise",
759
+ "AGDF_BACKLOG_ARTEFACT_LABEL_DUPLICATE",
760
+ `Compact Artefacts repeats the ${link.label} link.`,
761
+ backlogPath,
762
+ "Keep exactly one link per artefact type.",
763
+ );
764
+ }
765
+ continue;
766
+ }
767
+ seen.add(field);
768
+ pointer[field] = normalizedBacklogLinkTarget(entry, findings, backlogPath, link.label);
769
+ }
770
+ }
771
+
772
+ function parseBacklogSection(section, findings = null, backlogPath = ".agdf/control/MASTER_BACKLOG.md") {
773
+ const rows = tableRows(section);
774
+ if (rows.length === 0) return [];
775
+ const headers = rows[0].map(normalizeBacklogHeader);
776
+ const layout = headers.join("|") === compactBacklogHeaders.join("|")
777
+ ? "compact"
778
+ : headers.join("|") === legacyBacklogHeaders.join("|")
779
+ ? "legacy"
780
+ : "unknown";
781
+
782
+ if (layout === "unknown") {
783
+ if (findings) {
784
+ addFinding(
785
+ findings,
786
+ "revise",
787
+ "AGDF_BACKLOG_LAYOUT_UNKNOWN",
788
+ `MASTER_BACKLOG.md uses an unsupported table layout: ${headers.join(", ")}.`,
789
+ backlogPath,
790
+ "Use the canonical compact layout or the supported legacy 13-column layout.",
791
+ );
792
+ }
793
+ return [];
794
+ }
795
+
796
+ return rows.slice(1)
797
+ .filter((cells) => cells.some((cell) => filled(cell)))
798
+ .map((cells) => {
799
+ const pointer = emptyBacklogPointer();
800
+ if (layout === "legacy") {
801
+ [
802
+ pointer.prio,
803
+ pointer.key,
804
+ pointer.title,
805
+ pointer.status,
806
+ pointer.ur,
807
+ pointer.brownfield_review,
808
+ pointer.prd,
809
+ pointer.sd,
810
+ pointer.tp,
811
+ pointer.qa,
812
+ pointer.or,
813
+ pointer.current_spec,
814
+ pointer.notes,
815
+ ] = cells.map((cell) => cell ?? "");
816
+ pointer.status = normalizeBacklogStatus(pointer.status, findings, backlogPath);
817
+ return pointer;
818
+ }
819
+
820
+ pointer.prio = cells[0] ?? "";
821
+ pointer.key = cleanStatusCell(cells[1] ?? "");
822
+ pointer.title = cells[2] ?? "";
823
+ pointer.status = normalizeBacklogStatus(cells[3], findings, backlogPath);
824
+ parseCompactArtefacts(cells[4] ?? "", pointer, findings, backlogPath);
825
+ pointer.current_spec = normalizedBacklogLinkTarget(cells[5] ?? "", findings, backlogPath, "Current spec");
826
+ pointer.notes = cells[6] ?? "";
827
+ return pointer;
828
+ });
829
+ }
830
+
513
831
  function hasFilledTableRow(content, firstCellPattern) {
514
832
  return tableRows(content)
515
833
  .filter((cells) => firstCellPattern.test(cells[0] ?? ""))
@@ -619,7 +937,12 @@ function evaluateDoctor(targetDir) {
619
937
 
620
938
  const backlogPath = join(".agdf", "control", "MASTER_BACKLOG.md");
621
939
  const backlog = readTargetFile(targetDir, backlogPath);
622
- if (!hasFilledTableRow(backlog, /^P[0-9]/)) {
940
+ parseBacklogSection(markdownSection(backlog, "Active Backlog"), findings, backlogPath);
941
+ parseBacklogSection(markdownSection(backlog, "Planned / Parking Lot"), findings, backlogPath);
942
+ const completedRows = tableRows(markdownSection(backlog, "Completed / Superseded Pointers"))
943
+ .slice(1)
944
+ .filter((cells) => cells.some((cell) => filled(cell)));
945
+ if (!hasFilledTableRow(backlog, /^P[0-9]/) && completedRows.length === 0) {
623
946
  addFinding(
624
947
  findings,
625
948
  "warn",
@@ -747,12 +1070,18 @@ function cleanStatusCell(value) {
747
1070
  return value.replace(/^`|`$/g, "").trim();
748
1071
  }
749
1072
 
1073
+ function normalizeGateStatus(gate, status) {
1074
+ const normalized = cleanStatusCell(status ?? "");
1075
+ if (gate === "QA" && normalized === "passed") return "approved";
1076
+ return normalized;
1077
+ }
1078
+
750
1079
  function addApprovalRows(approvals, section) {
751
1080
  for (const cells of tableRows(section)) {
752
1081
  const [gate, status, evidence] = cells;
753
1082
  if (!userGateOrder.includes(gate)) continue;
754
1083
  approvals.set(gate, {
755
- status: cleanStatusCell(status ?? ""),
1084
+ status: normalizeGateStatus(gate, status),
756
1085
  evidence: evidence ?? "",
757
1086
  });
758
1087
  }
@@ -774,6 +1103,9 @@ function readRunState(targetDir) {
774
1103
  missing_evidence: [],
775
1104
  risks: [],
776
1105
  context_graph: {},
1106
+ quality_outlook: "",
1107
+ source_scope: {},
1108
+ memory: {},
777
1109
  };
778
1110
  }
779
1111
 
@@ -846,6 +1178,23 @@ function readRunState(targetDir) {
846
1178
  evidence: extractSectionField(modeSliceSection, "evidence"),
847
1179
  };
848
1180
 
1181
+ const sourceScopeSection = markdownSection(content, "Source And Scope State");
1182
+ const sourceScope = {
1183
+ normative_instruction_source: extractSectionField(sourceScopeSection, "normative_instruction_source"),
1184
+ multi_scope_state: cleanStatusCell(extractSectionField(sourceScopeSection, "multi_scope_state")),
1185
+ active_scope_evidence: extractSectionField(sourceScopeSection, "active_scope_evidence"),
1186
+ competing_scope_lines: extractSectionField(sourceScopeSection, "competing_scope_lines"),
1187
+ branch_workspace_evidence: extractSectionField(sourceScopeSection, "branch_workspace_evidence"),
1188
+ branch_workspace_scope_effect: cleanStatusCell(extractSectionField(sourceScopeSection, "branch_workspace_scope_effect")),
1189
+ };
1190
+
1191
+ const memorySection = markdownSection(content, "Knowledge Persistence Decision");
1192
+ const memory = {
1193
+ target: cleanStatusCell(extractSectionField(memorySection, "memory_target")),
1194
+ reason: extractSectionField(memorySection, "memory_reason"),
1195
+ refs: extractSectionField(memorySection, "memory_refs"),
1196
+ };
1197
+
849
1198
  return {
850
1199
  path: runPath,
851
1200
  content,
@@ -859,6 +1208,9 @@ function readRunState(targetDir) {
859
1208
  missing_evidence: missingEvidence,
860
1209
  risks,
861
1210
  context_graph: contextGraph,
1211
+ quality_outlook: extractField(content, "quality_outlook"),
1212
+ source_scope: sourceScope,
1213
+ memory,
862
1214
  };
863
1215
  }
864
1216
 
@@ -890,7 +1242,7 @@ function isDurableGateArtefactSatisfied(runState, gate) {
890
1242
  if (!durableGateArtefacts.has(gate)) return true;
891
1243
  const artefact = gateArtefactStatus(runState, gate);
892
1244
  if (!artefact.path || isPlaceholderValue(artefact.path)) return false;
893
- if (gate === "QA") return artefact.status === "pass";
1245
+ if (gate === "QA") return artefact.status === "pass" || artefact.status === "passed";
894
1246
  return artefact.status === "approved";
895
1247
  }
896
1248
 
@@ -997,15 +1349,97 @@ function analyzeDeliveryMap(runState) {
997
1349
  });
998
1350
  }
999
1351
 
1352
+ const multiScopeState = runState.source_scope?.multi_scope_state;
1353
+ if (multiScopeState === "ambiguous" || multiScopeState === "blocked") {
1354
+ findings.push({
1355
+ severity: multiScopeState === "blocked" ? "block" : "revise",
1356
+ code: "AGDF_SCOPE_AMBIGUOUS",
1357
+ message: "Multiple active scope lines are plausible; the agent must not choose one silently.",
1358
+ path: runState.path,
1359
+ next_step: "List competing scope lines with gate and artefact evidence, then clarify the active scope.",
1360
+ });
1361
+ }
1362
+
1363
+ const branchEffect = runState.source_scope?.branch_workspace_scope_effect;
1364
+ if (branchEffect === "conflicts" || branchEffect === "insufficient") {
1365
+ findings.push({
1366
+ severity: branchEffect === "conflicts" ? "revise" : "warn",
1367
+ code: "AGDF_BRANCH_NOT_SCOPE_PROOF",
1368
+ message: "Branch or workspace evidence is not sufficient scope proof.",
1369
+ path: runState.path,
1370
+ next_step: "Confirm the active scope from durable artefacts or record why branch/workspace evidence is only supporting evidence.",
1371
+ });
1372
+ }
1373
+
1374
+ if (runState.memory?.target && runState.memory.target !== "none" && !filled(runState.memory.reason)) {
1375
+ findings.push({
1376
+ severity: "warn",
1377
+ code: "AGDF_MEMORY_TARGET_REASON_MISSING",
1378
+ message: "Knowledge persistence target is set without a reason.",
1379
+ path: runState.path,
1380
+ next_step: "Fill memory_reason or set memory_target to none.",
1381
+ });
1382
+ }
1383
+
1000
1384
  return {
1001
1385
  relationships,
1002
1386
  missing_evidence: runState.missing_evidence,
1003
1387
  risks: runState.risks,
1004
1388
  context_graph: runState.context_graph,
1389
+ source_scope: runState.source_scope,
1390
+ memory: runState.memory,
1005
1391
  findings,
1006
1392
  };
1007
1393
  }
1008
1394
 
1395
+ function deriveQualityOutlook(runState, findings = []) {
1396
+ if (filled(runState.quality_outlook)) return runState.quality_outlook;
1397
+ if (findings.some((finding) => finding.severity === "block" || finding.severity === "revise")) {
1398
+ return "Resolve blocking or revise-level delivery-map findings before making stronger quality claims.";
1399
+ }
1400
+ if (findings.some((finding) => finding.severity === "warn")) {
1401
+ return "Review warning-level findings when investing further in delivery confidence.";
1402
+ }
1403
+ return "No additional quality follow-up identified from the current control state.";
1404
+ }
1405
+
1406
+ function buildStatusCard({
1407
+ status,
1408
+ currentGate,
1409
+ allowed = [],
1410
+ forbidden = [],
1411
+ blockingReason = "none",
1412
+ missingApproval = "none",
1413
+ nextAllowedAction,
1414
+ runState,
1415
+ findings = [],
1416
+ }) {
1417
+ const qualityOutlook = deriveQualityOutlook(runState, findings);
1418
+ return {
1419
+ mode: extractField(runState.content ?? "", "mode") || "unknown",
1420
+ status,
1421
+ current_gate: currentGate,
1422
+ mode_slice_decision: runState.mode_slice_decision?.decision || "undecided",
1423
+ allowed_now: allowed,
1424
+ forbidden_now: forbidden,
1425
+ blocking_condition: blockingReason || "none",
1426
+ missing_approval: missingApproval || "none",
1427
+ evidence: runState.evidence_refs,
1428
+ next_skill: nextSkillByGate[currentGate] ?? "gate-check",
1429
+ next_step: nextAllowedAction,
1430
+ quality_outlook: qualityOutlook,
1431
+ };
1432
+ }
1433
+
1434
+ function effectiveCurrentGate(runState, transitionDecision) {
1435
+ if (transitionDecision.status !== "open") return transitionDecision.current_gate;
1436
+ const explicitGate = normalizeCurrentGate(runState.current_gate, "");
1437
+ const explicitIndex = gateProgressOrder.indexOf(explicitGate);
1438
+ const fallbackIndex = gateProgressOrder.indexOf(transitionDecision.current_gate);
1439
+ if (explicitIndex === -1 || fallbackIndex === -1) return transitionDecision.current_gate;
1440
+ return explicitIndex > fallbackIndex ? explicitGate : transitionDecision.current_gate;
1441
+ }
1442
+
1009
1443
  function durableArtefactBlock(gate, nextGate) {
1010
1444
  const label = gate === "QA" ? "QA report" : `${gate} artefact`;
1011
1445
  const stablePath = gate === "QA" ? ".agdf/control/artefacts/<key>/QA_REPORT.md" : `.agdf/control/artefacts/<key>/${gate}.md`;
@@ -1030,6 +1464,7 @@ function isGateSatisfied(runState, gate) {
1030
1464
  const status = gateApprovalStatus(runState, gate);
1031
1465
  if (status === "not_applicable") return true;
1032
1466
  if (status !== "approved") return false;
1467
+ if (gate === "UAT") return true;
1033
1468
  return isDurableGateArtefactSatisfied(runState, gate);
1034
1469
  }
1035
1470
 
@@ -1151,6 +1586,30 @@ function transitionDecisionForRunState(runState) {
1151
1586
 
1152
1587
  if (gateApprovalStatus(runState, "QA") === "approved" && !isGateSatisfied(runState, "QA")) return durableArtefactBlock("QA", "UAT");
1153
1588
 
1589
+ if (isGateSatisfied(runState, "QA") && !isGateSatisfied(runState, "UAT")) {
1590
+ return {
1591
+ status: "open",
1592
+ current_gate: "UAT",
1593
+ blocking_reason: "none",
1594
+ missing_approval: "Approval: UAT",
1595
+ allowed: ["request exact UAT approval", "prepare non-operative delivery summary"],
1596
+ forbidden: ["release", "push", "open PR", "commit without explicit user instruction and required approval"],
1597
+ next_allowed_action: "Request exact approval: Approval: UAT before delivery handoff.",
1598
+ };
1599
+ }
1600
+
1601
+ if (isGateSatisfied(runState, "UAT")) {
1602
+ return {
1603
+ status: "open",
1604
+ current_gate: "OR",
1605
+ blocking_reason: "none",
1606
+ missing_approval: "none",
1607
+ allowed: ["produce OR or delivery closeout", "prepare commit, push or PR handoff when requested"],
1608
+ forbidden: ["commit, push, open PR or release automatically"],
1609
+ next_allowed_action: "Produce delivery closeout or requested handoff; do not perform VCS actions automatically.",
1610
+ };
1611
+ }
1612
+
1154
1613
  return {
1155
1614
  status: "open",
1156
1615
  current_gate: "Brownfield Analysis",
@@ -1171,7 +1630,7 @@ function evaluateGateCheck(targetDir) {
1171
1630
  const doctorRevise = doctorReport.findings.find((finding) => finding.severity === "revise");
1172
1631
 
1173
1632
  let status = transitionDecision.status;
1174
- let currentGate = transitionDecision.current_gate;
1633
+ let currentGate = effectiveCurrentGate(runState, transitionDecision);
1175
1634
  let blockingReason = transitionDecision.blocking_reason;
1176
1635
  let missingApproval = transitionDecision.missing_approval;
1177
1636
  let allowed = transitionDecision.allowed;
@@ -1220,10 +1679,24 @@ function evaluateGateCheck(targetDir) {
1220
1679
  doctor_status: doctorReport.status,
1221
1680
  doctor_summary: doctorReport.summary,
1222
1681
  evidence_refs: runState.evidence_refs,
1682
+ quality_outlook: deriveQualityOutlook(runState, deliveryMap.findings),
1683
+ status_card: buildStatusCard({
1684
+ status,
1685
+ currentGate,
1686
+ allowed,
1687
+ forbidden,
1688
+ blockingReason,
1689
+ missingApproval,
1690
+ nextAllowedAction,
1691
+ runState,
1692
+ findings: deliveryMap.findings,
1693
+ }),
1223
1694
  delivery_map: {
1224
1695
  relationships: deliveryMap.relationships,
1225
1696
  mode_slice_decision: runState.mode_slice_decision,
1226
1697
  context_graph: deliveryMap.context_graph,
1698
+ source_scope: deliveryMap.source_scope,
1699
+ memory: deliveryMap.memory,
1227
1700
  findings: deliveryMap.findings,
1228
1701
  },
1229
1702
  doctor_report: doctorReport,
@@ -1240,6 +1713,7 @@ function printGateCheckReport(report, json) {
1240
1713
  console.log(`Current gate: ${report.current_gate}`);
1241
1714
  console.log(`Blocking reason: ${report.blocking_reason}`);
1242
1715
  console.log(`Missing approval: ${report.missing_approval}`);
1716
+ console.log(`Quality outlook: ${report.quality_outlook}`);
1243
1717
  console.log(`Doctor: ${report.doctor_status} (${report.doctor_summary.findings} findings)`);
1244
1718
  console.log("");
1245
1719
  console.log("Allowed:");
@@ -1257,22 +1731,7 @@ function readBacklogPointers(targetDir) {
1257
1731
 
1258
1732
  const backlog = readTargetFile(targetDir, backlogPath);
1259
1733
  const activeSection = markdownSection(backlog, "Active Backlog");
1260
- return nonTemplateRows(activeSection, "Prio")
1261
- .map((cells) => ({
1262
- prio: cells[0] ?? "",
1263
- key: cells[1] ?? "",
1264
- title: cells[2] ?? "",
1265
- status: cleanStatusCell(cells[3] ?? ""),
1266
- ur: cells[4] ?? "",
1267
- brownfield_review: cells[5] ?? "",
1268
- prd: cells[6] ?? "",
1269
- sd: cells[7] ?? "",
1270
- tp: cells[8] ?? "",
1271
- qa: cells[9] ?? "",
1272
- or: cells[10] ?? "",
1273
- current_spec: cells[11] ?? "",
1274
- notes: cells[12] ?? "",
1275
- }));
1734
+ return parseBacklogSection(activeSection);
1276
1735
  }
1277
1736
 
1278
1737
  function evaluateDeliveryMap(targetDir) {
@@ -1280,6 +1739,7 @@ function evaluateDeliveryMap(targetDir) {
1280
1739
  const runState = readRunState(targetDir);
1281
1740
  const map = analyzeDeliveryMap(runState);
1282
1741
  const gateDecision = transitionDecisionForRunState(runState);
1742
+ const currentGate = effectiveCurrentGate(runState, gateDecision);
1283
1743
 
1284
1744
  const severityRank = { block: 3, revise: 2, warn: 1 };
1285
1745
  const deliverySeverity = map.findings.reduce((max, finding) => Math.max(max, severityRank[finding.severity] ?? 0), 0);
@@ -1287,13 +1747,28 @@ function evaluateDeliveryMap(targetDir) {
1287
1747
  const maxSeverity = Math.max(deliverySeverity, doctorSeverity);
1288
1748
  const status = maxSeverity >= 3 ? "block" : maxSeverity === 2 ? "revise" : maxSeverity === 1 ? "warn" : "pass";
1289
1749
 
1750
+ const qualityOutlook = deriveQualityOutlook(runState, map.findings);
1751
+ const nextAllowedAction = isPlaceholderValue(runState.next_allowed_action) ? gateDecision.next_allowed_action : runState.next_allowed_action;
1752
+
1290
1753
  return {
1291
1754
  schema_version: "1",
1292
1755
  status,
1293
1756
  checked_at: new Date().toISOString(),
1294
1757
  target_dir: targetDir,
1295
- current_gate: gateDecision.current_gate,
1296
- next_allowed_action: isPlaceholderValue(runState.next_allowed_action) ? gateDecision.next_allowed_action : runState.next_allowed_action,
1758
+ current_gate: currentGate,
1759
+ next_allowed_action: nextAllowedAction,
1760
+ quality_outlook: qualityOutlook,
1761
+ status_card: buildStatusCard({
1762
+ status,
1763
+ currentGate,
1764
+ allowed: gateDecision.allowed,
1765
+ forbidden: gateDecision.forbidden,
1766
+ blockingReason: gateDecision.blocking_reason,
1767
+ missingApproval: gateDecision.missing_approval,
1768
+ nextAllowedAction,
1769
+ runState,
1770
+ findings: map.findings,
1771
+ }),
1297
1772
  backlog_pointers: readBacklogPointers(targetDir),
1298
1773
  artefacts: Object.fromEntries([...runState.artefacts.entries()]),
1299
1774
  approvals: Object.fromEntries([...runState.approvals.entries()]),
@@ -1303,6 +1778,8 @@ function evaluateDeliveryMap(targetDir) {
1303
1778
  missing_evidence: map.missing_evidence,
1304
1779
  risks: map.risks,
1305
1780
  context_graph: map.context_graph,
1781
+ source_scope: map.source_scope,
1782
+ memory: map.memory,
1306
1783
  findings: map.findings,
1307
1784
  doctor_status: doctorReport.status,
1308
1785
  doctor_summary: doctorReport.summary,
@@ -1319,6 +1796,7 @@ function printDeliveryMapReport(report, json) {
1319
1796
  console.log(`AGDF delivery-map: ${report.status}`);
1320
1797
  console.log(`Current gate: ${report.current_gate}`);
1321
1798
  console.log(`Next allowed action: ${report.next_allowed_action}`);
1799
+ console.log(`Quality outlook: ${report.quality_outlook}`);
1322
1800
  console.log(`Relationships: ${report.relationships.filter((item) => item.status === "pass").length}/${report.relationships.length} evidenced`);
1323
1801
  console.log(`Findings: ${report.findings.length}`);
1324
1802
 
@@ -1351,6 +1829,42 @@ function main() {
1351
1829
  process.exit(report.status === "block" ? 2 : 0);
1352
1830
  }
1353
1831
 
1832
+ if (options.target === "codex") {
1833
+ try {
1834
+ installCodexGlobalPlugin();
1835
+ console.log("AGDF Codex plugin installed globally.");
1836
+ console.log("Run npx --yes @agdf/cli@latest codex-repo in a repository when you want to test AGDF from repository-local plugin files.");
1837
+ } catch (error) {
1838
+ console.error(error.message);
1839
+ process.exit(1);
1840
+ }
1841
+ return;
1842
+ }
1843
+
1844
+ if (options.target === "claude") {
1845
+ try {
1846
+ installClaudeGlobalPlugin();
1847
+ console.log("AGDF Claude Code plugin installed globally.");
1848
+ } catch (error) {
1849
+ console.error(error.message);
1850
+ process.exit(1);
1851
+ }
1852
+ return;
1853
+ }
1854
+
1855
+ if (options.target === "opencode") {
1856
+ const configDir = options.dirExplicit ? options.dir : defaultOpenCodeConfigDir();
1857
+ try {
1858
+ const result = installOpenCodeGlobalPlugin(configDir);
1859
+ console.log(`AGDF OpenCode global plugin ${result.added ? "installed" : "already present"}: ${result.configPath}`);
1860
+ console.log("Run npx --yes @agdf/cli@latest opencode-repo in each repository where AGDF governance should be active and reviewable.");
1861
+ } catch (error) {
1862
+ console.error(error.message);
1863
+ process.exit(1);
1864
+ }
1865
+ return;
1866
+ }
1867
+
1354
1868
  const files = generatedFilesForTarget(options.target, options.dir, options.force, options.language);
1355
1869
  const wroteAgentsFragment = files.some(file => file.path === agdfFragmentPath);
1356
1870
  const wroteOpenCodeConfigFragment = files.some(file => file.path === openCodeConfigFragmentPath);