create-agdf 0.9.0 → 0.9.3
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.
- package/README.md +26 -3
- package/bin/create-agdf.js +2 -3333
- package/generated/.agdf/control/templates/MASTER_BACKLOG.md +3 -3
- package/generated/plugins/agdf/.codex-plugin/plugin.json +1 -1
- package/generated/plugins/agdf/control/templates/MASTER_BACKLOG.md +3 -3
- package/generated/plugins/agdf/meta/agdf-plugin.definition.json +1 -1
- package/lib/cli/application.js +191 -0
- package/lib/cli/command-registry.js +101 -0
- package/lib/cli/delivery-path-search-command.js +75 -0
- package/lib/cli/parse-args.js +142 -0
- package/lib/cli/runtime-context.js +83 -0
- package/lib/control-evaluation/delivery-map.js +291 -0
- package/lib/control-evaluation/doctor.js +265 -0
- package/lib/control-evaluation/gate-check.js +404 -0
- package/lib/control-evaluation/gate-policy.js +283 -0
- package/lib/control-evaluation/run-state.js +217 -0
- package/lib/control-evaluation/shared.js +344 -0
- package/lib/control-evaluation/verified-change.js +156 -0
- package/lib/installers/opencode.js +419 -0
- package/lib/installers/plugin-installers.js +70 -0
- package/lib/scaffold/plan.js +278 -0
- package/lib/scaffold/presentation.js +74 -0
- package/lib/scaffold/write.js +45 -0
- package/lib/skill-evals/artefact-quality.js +36 -0
- package/lib/skill-evals/index.js +105 -0
- package/lib/skill-evals/live-recorder.js +108 -0
- package/lib/skill-evals/workspace.js +97 -0
- package/package.json +7 -2
package/bin/create-agdf.js
CHANGED
|
@@ -1,3341 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
4
|
-
import { execFileSync } from "node:child_process";
|
|
5
|
-
import { homedir } from "node:os";
|
|
6
|
-
import { dirname, isAbsolute, join, posix, relative, resolve } from "node:path";
|
|
7
|
-
import { fileURLToPath } from "node:url";
|
|
8
3
|
import process from "node:process";
|
|
9
|
-
import {
|
|
10
|
-
import { runDeliveryPathSearch } from "../lib/delivery-path-search/search-engine.js";
|
|
11
|
-
import { codexEvaluator } from "../lib/delivery-path-search/evaluators/codex.js";
|
|
12
|
-
import { claudeEvaluator } from "../lib/delivery-path-search/evaluators/claude.js";
|
|
13
|
-
import { fixtureEvaluator } from "../lib/delivery-path-search/evaluators/protocol.js";
|
|
14
|
-
import { codexGenerator } from "../lib/delivery-path-search/generators/codex.js";
|
|
15
|
-
import { claudeGenerator } from "../lib/delivery-path-search/generators/claude.js";
|
|
16
|
-
import { fixtureGenerator } from "../lib/delivery-path-search/generators/protocol.js";
|
|
17
|
-
import { enforcementForSurface } from "../lib/delivery-path-search/surfaces/capabilities.js";
|
|
18
|
-
import { persistSearchResult } from "../lib/delivery-path-search/persistence.js";
|
|
19
|
-
import {
|
|
20
|
-
aggregate,
|
|
21
|
-
createRun,
|
|
22
|
-
resolveRuns,
|
|
23
|
-
writeLegacyProjection,
|
|
24
|
-
migrateLegacy,
|
|
25
|
-
parseControlState,
|
|
26
|
-
verifyLegacyProjection,
|
|
27
|
-
} from "../lib/control-state/index.js";
|
|
28
|
-
import {
|
|
29
|
-
attachApprovalOrientationSnapshot,
|
|
30
|
-
buildArtefactRefs,
|
|
31
|
-
buildRunCandidates,
|
|
32
|
-
buildQualityReadiness,
|
|
33
|
-
canonicalizeLanguageTag,
|
|
34
|
-
formatArtefactRefs,
|
|
35
|
-
gateTitle,
|
|
36
|
-
localePack,
|
|
37
|
-
resolveHumanRunTitle,
|
|
38
|
-
resolvePresentationLocale,
|
|
39
|
-
} from "../lib/interaction-presentation.js";
|
|
40
|
-
|
|
41
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
42
|
-
const packageRoot = resolve(__dirname, "..");
|
|
43
|
-
const generatedRoot = join(packageRoot, "generated");
|
|
44
|
-
const pluginDefinitionPath = join(generatedRoot, "plugins", "agdf", "meta", "agdf-plugin.definition.json");
|
|
45
|
-
const pluginDefinition = JSON.parse(readFileSync(pluginDefinitionPath, "utf8"));
|
|
46
|
-
const interactionLocalesPath = join(generatedRoot, "plugins", "agdf", "meta", "agdf-interaction-locales.json");
|
|
47
|
-
const interactionLocales = JSON.parse(readFileSync(interactionLocalesPath, "utf8"));
|
|
48
|
-
const pluginInstallCommand = "npx --yes @agdf/cli@latest claude";
|
|
49
|
-
const testNpmCliPath = process.env.NODE_ENV === "test" ? process.env.AGDF_TEST_NPM_CLI_PATH || "" : "";
|
|
50
|
-
const npmCommand = testNpmCliPath
|
|
51
|
-
? process.execPath
|
|
52
|
-
: process.platform === "win32"
|
|
53
|
-
? process.execPath
|
|
54
|
-
: "npm";
|
|
55
|
-
const npmPrefixArgs = testNpmCliPath
|
|
56
|
-
? [testNpmCliPath]
|
|
57
|
-
: process.platform === "win32"
|
|
58
|
-
? [join(dirname(process.execPath), "node_modules", "npm", "bin", "npm-cli.js")]
|
|
59
|
-
: [];
|
|
60
|
-
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"]);
|
|
61
|
-
const agdfFragmentPath = "AGENTS.agdf.md";
|
|
62
|
-
const openCodeConfigFragmentPath = "opencode.agdf.json";
|
|
63
|
-
const userGateOrder = ["UR", "PRD", "SD", "TP", "QA", "UAT"];
|
|
64
|
-
const durableGateArtefacts = new Set(["UR", "PRD", "SD", "TP", "QA"]);
|
|
65
|
-
const internalStepArtefacts = new Set(["Brownfield Review", "Verified Change", "Brownfield Analysis", "CD+Tests", "TP Review", "Clean Implementation Review", "Clean Review", "CR", "Code Review"]);
|
|
66
|
-
const closeoutArtefacts = new Set(["OR"]);
|
|
67
|
-
const deliveryRelationships = [
|
|
68
|
-
{ from: "UR", relationship: "approved_by", to: "Approval: UR", requiredBy: "UR" },
|
|
69
|
-
{ from: "PRD", relationship: "derived_from", to: "UR", requiredBy: "PRD" },
|
|
70
|
-
{ from: "SD", relationship: "derived_from", to: "PRD", requiredBy: "SD" },
|
|
71
|
-
{ from: "TP", relationship: "derived_from", to: "SD", requiredBy: "TP" },
|
|
72
|
-
{ from: "QA_REPORT", relationship: "tests", to: "TP", requiredBy: "QA" },
|
|
73
|
-
];
|
|
74
|
-
const nextSkillByGate = {
|
|
75
|
-
UR: "gate-check",
|
|
76
|
-
"Brownfield Review": "brownfield-analysis",
|
|
77
|
-
"Mode/Slice Decision": "gate-check",
|
|
78
|
-
"Quick Task Execution": "none",
|
|
79
|
-
"Verified Change Execution": "gate-check",
|
|
80
|
-
PRD: "gate-check",
|
|
81
|
-
SD: "gate-check",
|
|
82
|
-
TP: "gate-check",
|
|
83
|
-
"Brownfield Analysis": "brownfield-analysis",
|
|
84
|
-
"CD+Tests": "none",
|
|
85
|
-
CR: "code-review",
|
|
86
|
-
QA: "qa-gate",
|
|
87
|
-
UAT: "delivery-closeout",
|
|
88
|
-
OR: "release-or",
|
|
89
|
-
};
|
|
90
|
-
const codexSkillNames = pluginDefinition.skillSet.map((skill) => `${pluginDefinition.codex.skillPrefix}${skill.slug}`);
|
|
91
|
-
const copilotSkillNames = pluginDefinition.skillSet.map((skill) => `${pluginDefinition.copilot.skillPrefix}${skill.slug}`);
|
|
92
|
-
const openCodeSkillNames = pluginDefinition.skillSet.map((skill) => `${pluginDefinition.opencode.skillPrefix}${skill.slug}`);
|
|
93
|
-
const globalOpenCodeSkillNames = pluginDefinition.skillSet.map((skill) => `${pluginDefinition.opencode.globalSkillPrefix}${skill.slug}`);
|
|
94
|
-
const contractModules = [
|
|
95
|
-
"gate-transition.md",
|
|
96
|
-
"interaction.md",
|
|
97
|
-
"modes.md",
|
|
98
|
-
"quality.md",
|
|
99
|
-
"context-graph.md",
|
|
100
|
-
"control-scaffold.md",
|
|
101
|
-
"closeout.md",
|
|
102
|
-
];
|
|
103
|
-
const globalOpenCodeSkillOwnershipMarker = "<!-- AGDF-GLOBAL-SKILL: ";
|
|
104
|
-
const globalOpenCodeInstructionsOwnershipMarker = "<!-- AGDF-GLOBAL-INSTRUCTIONS -->";
|
|
105
|
-
const globalOpenCodeRuntimeContractOwnershipMarker = "<!-- AGDF-GLOBAL-RUNTIME-CONTRACT -->";
|
|
106
|
-
const codexPluginFiles = [
|
|
107
|
-
join(".agents", "plugins", "marketplace.json"),
|
|
108
|
-
join("plugins", "agdf", ".codex-plugin", "plugin.json"),
|
|
109
|
-
join("plugins", "agdf", "control", "README.md"),
|
|
110
|
-
join("plugins", "agdf", "control", "templates", "AGDF_RUN.md"),
|
|
111
|
-
join("plugins", "agdf", "control", "templates", "RUN_STATE.md"),
|
|
112
|
-
join("plugins", "agdf", "control", "templates", "MASTER_BACKLOG.md"),
|
|
113
|
-
join("plugins", "agdf", "control", "templates", "SOT_REGISTRY.md"),
|
|
114
|
-
join("plugins", "agdf", "control", "templates", "CONTEXT_GRAPH.md"),
|
|
115
|
-
join("plugins", "agdf", "control", "templates", "AGENT_QUALITY_CONTRACTS.json"),
|
|
116
|
-
join("plugins", "agdf", "control", "templates", "artefacts", "UR.md"),
|
|
117
|
-
join("plugins", "agdf", "control", "templates", "artefacts", "BROWNFIELD_REVIEW.md"),
|
|
118
|
-
join("plugins", "agdf", "control", "templates", "artefacts", "VERIFIED_CHANGE.md"),
|
|
119
|
-
join("plugins", "agdf", "control", "templates", "artefacts", "PRD.md"),
|
|
120
|
-
join("plugins", "agdf", "control", "templates", "artefacts", "SD.md"),
|
|
121
|
-
join("plugins", "agdf", "control", "templates", "artefacts", "TP.md"),
|
|
122
|
-
join("plugins", "agdf", "control", "templates", "artefacts", "QA_REPORT.md"),
|
|
123
|
-
join("plugins", "agdf", "control", "templates", "artefacts", "OR.md"),
|
|
124
|
-
join("plugins", "agdf", "hooks", "hooks.json"),
|
|
125
|
-
join("plugins", "agdf", "hooks", "session-start.sh"),
|
|
126
|
-
join("plugins", "agdf", "meta", "agdf-agent-router.md"),
|
|
127
|
-
join("plugins", "agdf", "meta", "agdf-constitution.md"),
|
|
128
|
-
join("plugins", "agdf", "meta", "agdf-plugin.definition.json"),
|
|
129
|
-
join("plugins", "agdf", "meta", "agdf-runtime-contract.md"),
|
|
130
|
-
join("plugins", "agdf", "meta", "agdf-tenets.md"),
|
|
131
|
-
...contractModules.map((moduleName) => join("plugins", "agdf", "meta", "contracts", moduleName)),
|
|
132
|
-
...codexSkillNames.map((skillName) => join("plugins", "agdf", "skills", skillName, "SKILL.md")),
|
|
133
|
-
];
|
|
134
|
-
const controlFiles = [
|
|
135
|
-
join(".agdf", "control", "README.md"),
|
|
136
|
-
join(".agdf", "control", "templates", "AGDF_RUN.md"),
|
|
137
|
-
join(".agdf", "control", "templates", "RUN_STATE.md"),
|
|
138
|
-
join(".agdf", "control", "templates", "MASTER_BACKLOG.md"),
|
|
139
|
-
join(".agdf", "control", "templates", "SOT_REGISTRY.md"),
|
|
140
|
-
join(".agdf", "control", "templates", "CONTEXT_GRAPH.md"),
|
|
141
|
-
join(".agdf", "control", "templates", "AGENT_QUALITY_CONTRACTS.json"),
|
|
142
|
-
join(".agdf", "control", "templates", "artefacts", "UR.md"),
|
|
143
|
-
join(".agdf", "control", "templates", "artefacts", "BROWNFIELD_REVIEW.md"),
|
|
144
|
-
join(".agdf", "control", "templates", "artefacts", "VERIFIED_CHANGE.md"),
|
|
145
|
-
join(".agdf", "control", "templates", "artefacts", "PRD.md"),
|
|
146
|
-
join(".agdf", "control", "templates", "artefacts", "SD.md"),
|
|
147
|
-
join(".agdf", "control", "templates", "artefacts", "TP.md"),
|
|
148
|
-
join(".agdf", "control", "templates", "artefacts", "QA_REPORT.md"),
|
|
149
|
-
join(".agdf", "control", "templates", "artefacts", "OR.md"),
|
|
150
|
-
];
|
|
151
|
-
const liveControlFiles = [
|
|
152
|
-
{
|
|
153
|
-
path: join(".agdf", "control", "AGDF_RUN.md"),
|
|
154
|
-
source: join(".agdf", "control", "templates", "AGDF_RUN.md"),
|
|
155
|
-
},
|
|
156
|
-
{
|
|
157
|
-
path: join(".agdf", "control", "MASTER_BACKLOG.md"),
|
|
158
|
-
source: join(".agdf", "control", "templates", "MASTER_BACKLOG.md"),
|
|
159
|
-
},
|
|
160
|
-
{
|
|
161
|
-
path: join(".agdf", "control", "SOT_REGISTRY.md"),
|
|
162
|
-
source: join(".agdf", "control", "templates", "SOT_REGISTRY.md"),
|
|
163
|
-
},
|
|
164
|
-
{
|
|
165
|
-
path: join(".agdf", "control", "CONTEXT_GRAPH.md"),
|
|
166
|
-
source: join(".agdf", "control", "templates", "CONTEXT_GRAPH.md"),
|
|
167
|
-
},
|
|
168
|
-
{
|
|
169
|
-
path: join(".agdf", "control", "AGENT_QUALITY_CONTRACTS.json"),
|
|
170
|
-
source: join(".agdf", "control", "templates", "AGENT_QUALITY_CONTRACTS.json"),
|
|
171
|
-
},
|
|
172
|
-
];
|
|
173
|
-
const artefactTemplateFiles = [
|
|
174
|
-
join(".agdf", "control", "templates", "artefacts", "UR.md"),
|
|
175
|
-
join(".agdf", "control", "templates", "artefacts", "BROWNFIELD_REVIEW.md"),
|
|
176
|
-
join(".agdf", "control", "templates", "artefacts", "VERIFIED_CHANGE.md"),
|
|
177
|
-
join(".agdf", "control", "templates", "artefacts", "PRD.md"),
|
|
178
|
-
join(".agdf", "control", "templates", "artefacts", "SD.md"),
|
|
179
|
-
join(".agdf", "control", "templates", "artefacts", "TP.md"),
|
|
180
|
-
join(".agdf", "control", "templates", "artefacts", "QA_REPORT.md"),
|
|
181
|
-
join(".agdf", "control", "templates", "artefacts", "OR.md"),
|
|
182
|
-
];
|
|
183
|
-
const doctorRequiredFiles = [
|
|
184
|
-
join(".agdf", "control", "AGDF_RUN.md"),
|
|
185
|
-
join(".agdf", "control", "MASTER_BACKLOG.md"),
|
|
186
|
-
join(".agdf", "control", "SOT_REGISTRY.md"),
|
|
187
|
-
join(".agdf", "control", "CONTEXT_GRAPH.md"),
|
|
188
|
-
join(".agdf", "control", "AGENT_QUALITY_CONTRACTS.json"),
|
|
189
|
-
];
|
|
190
|
-
const copilotInstructionFiles = [
|
|
191
|
-
join(".github", "copilot-instructions.md"),
|
|
192
|
-
join(".github", "instructions", "agdf-governance.instructions.md"),
|
|
193
|
-
];
|
|
194
|
-
const copilotSkillFiles = [
|
|
195
|
-
join(".github", "skills", "README.md"),
|
|
196
|
-
join(".github", "skills", pluginDefinition.copilot.runtimeContractFileName),
|
|
197
|
-
...contractModules.map((moduleName) => join(".github", "skills", "contracts", moduleName)),
|
|
198
|
-
...copilotSkillNames.map((skillName) => join(".github", "skills", skillName, "SKILL.md")),
|
|
199
|
-
];
|
|
200
|
-
const openCodeFiles = [
|
|
201
|
-
join(".opencode", pluginDefinition.opencode.instructionsFileName),
|
|
202
|
-
join(".opencode", "README.md"),
|
|
203
|
-
join(".opencode", pluginDefinition.opencode.runtimeContractFileName),
|
|
204
|
-
...contractModules.map((moduleName) => join(".opencode", "contracts", moduleName)),
|
|
205
|
-
...openCodeSkillNames.map((skillName) => join(".opencode", "skills", skillName, "SKILL.md")),
|
|
206
|
-
];
|
|
207
|
-
|
|
208
|
-
function printUsage() {
|
|
209
|
-
console.log(`create-agdf
|
|
210
|
-
|
|
211
|
-
Preferred AGDF CLI:
|
|
212
|
-
npx --yes @agdf/cli@latest codex
|
|
213
|
-
npx --yes @agdf/cli@latest codex-repo
|
|
214
|
-
npx --yes @agdf/cli@latest claude
|
|
215
|
-
npx --yes @agdf/cli@latest opencode
|
|
216
|
-
npx --yes @agdf/cli@latest opencode-status
|
|
217
|
-
npx --yes @agdf/cli@latest opencode-repo
|
|
218
|
-
npx --yes @agdf/cli@latest init
|
|
219
|
-
npx --yes @agdf/cli@latest doctor
|
|
220
|
-
npx --yes @agdf/cli@latest gate-check --json
|
|
221
|
-
npx --yes @agdf/cli@latest delivery-path-search --surface codex --json
|
|
222
|
-
npx --yes @agdf/cli@latest delivery-path-search --surface claude --json
|
|
223
|
-
npx --yes @agdf/cli@latest run-create --run <run_id>
|
|
224
|
-
npx --yes @agdf/cli@latest run-migrate [--run <run_id>]
|
|
225
|
-
npx --yes @agdf/cli@latest run-render-legacy --run <run_id>
|
|
226
|
-
|
|
227
|
-
Scaffold-compatible npm create usage:
|
|
228
|
-
npm create agdf@latest -- codex
|
|
229
|
-
npm create agdf@latest -- codex-repo
|
|
230
|
-
npm create agdf@latest -- claude
|
|
231
|
-
npm create agdf@latest -- copilot
|
|
232
|
-
npm create agdf@latest -- opencode
|
|
233
|
-
npm create agdf@latest -- opencode-status
|
|
234
|
-
npm create agdf@latest -- opencode-repo
|
|
235
|
-
npm create agdf@latest -- both
|
|
236
|
-
npm create agdf@latest -- init
|
|
237
|
-
npm create agdf@latest -- config --language de
|
|
238
|
-
npm create agdf@latest -- doctor
|
|
239
|
-
npm create agdf@latest -- gate-check
|
|
240
|
-
npm create agdf@latest -- delivery-map
|
|
241
|
-
npm create agdf@latest -- delivery-path-search --surface codex
|
|
242
|
-
npm create agdf@latest -- delivery-path-search --surface claude
|
|
243
|
-
|
|
244
|
-
Backward-compatible create-agdf usage:
|
|
245
|
-
npx --yes create-agdf@latest doctor --json
|
|
246
|
-
npx --yes create-agdf@latest gate-check --json
|
|
247
|
-
|
|
248
|
-
Options:
|
|
249
|
-
--dir <path> Write files into a specific directory. With opencode, use this as the OpenCode config directory.
|
|
250
|
-
--force Overwrite existing generated files
|
|
251
|
-
--language <tag>
|
|
252
|
-
Set AGDF chat and artefact language. Defaults to detected system locale.
|
|
253
|
-
--lang <tag> Alias for --language
|
|
254
|
-
--json Print machine-readable command output as JSON
|
|
255
|
-
--status-card Print compact gate-check status-card output for interactive use
|
|
256
|
-
--run <run_id> Select one canonical run
|
|
257
|
-
--all-active Evaluate every active run (doctor and delivery-map only)
|
|
258
|
-
--surface <codex|claude|copilot|opencode|generic>
|
|
259
|
-
Declare the active Delivery Path Search surface
|
|
260
|
-
--fixture <path>
|
|
261
|
-
Use deterministic evaluator/candidate fixtures instead of a live evaluator
|
|
262
|
-
--persist Persist the redacted Delivery Path Search result under the current scope
|
|
263
|
-
--model <id> Optional Codex evaluator model
|
|
264
|
-
--generate-candidates
|
|
265
|
-
Add one bounded AI-native candidate-generation call before evaluation
|
|
266
|
-
--generator-model <id>
|
|
267
|
-
Optional candidate-generator model override
|
|
268
|
-
--max-generated-candidates <1-5>
|
|
269
|
-
--generation-timeout-ms <1-30000>
|
|
270
|
-
--generation-cost-units <1-5>
|
|
271
|
-
--help Show this help
|
|
272
|
-
`);
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
function normalizeLanguage(value) {
|
|
276
|
-
return canonicalizeLanguageTag(value);
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
function configuredLanguage(value) {
|
|
280
|
-
const normalized = normalizeLanguage(value);
|
|
281
|
-
if (!normalized) return "";
|
|
282
|
-
const language = normalized.split("-")[0];
|
|
283
|
-
if (interactionLocales.locales[normalized] || interactionLocales.locales[language]) {
|
|
284
|
-
return resolvePresentationLocale(interactionLocales, normalized);
|
|
285
|
-
}
|
|
286
|
-
return normalized;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
function detectSystemLocale() {
|
|
290
|
-
const envLocale = process.env.LC_ALL || process.env.LC_MESSAGES || process.env.LANG || process.env.LANGUAGE || "";
|
|
291
|
-
if (envLocale) return envLocale;
|
|
292
|
-
try {
|
|
293
|
-
return Intl.DateTimeFormat().resolvedOptions().locale || "";
|
|
294
|
-
} catch {
|
|
295
|
-
return "";
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
function resolveLanguagePreference(explicitLanguage) {
|
|
300
|
-
const explicit = configuredLanguage(explicitLanguage);
|
|
301
|
-
if (explicit) {
|
|
302
|
-
return {
|
|
303
|
-
artifact_language: explicit,
|
|
304
|
-
chat_language: explicit,
|
|
305
|
-
runtime_language: "en",
|
|
306
|
-
source: "parameter",
|
|
307
|
-
detected_locale: detectSystemLocale() || "unknown",
|
|
308
|
-
};
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
const detectedLocale = detectSystemLocale();
|
|
312
|
-
const detected = configuredLanguage(detectedLocale) || interactionLocales.fallbackLocale;
|
|
313
|
-
return {
|
|
314
|
-
artifact_language: detected,
|
|
315
|
-
chat_language: detected,
|
|
316
|
-
runtime_language: "en",
|
|
317
|
-
source: detectedLocale ? "system_locale" : "default",
|
|
318
|
-
detected_locale: detectedLocale || "unknown",
|
|
319
|
-
};
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
function resolveConfiguredChatLanguage(targetDir) {
|
|
323
|
-
const configPath = join(targetDir, ".agdf", "control", "config.json");
|
|
324
|
-
if (!existsSync(configPath)) return interactionLocales.fallbackLocale;
|
|
325
|
-
try {
|
|
326
|
-
const config = JSON.parse(readFileSync(configPath, "utf8"));
|
|
327
|
-
return resolvePresentationLocale(interactionLocales, config.chat_language);
|
|
328
|
-
} catch {
|
|
329
|
-
return interactionLocales.fallbackLocale;
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
function languageConfigContent(languagePreference) {
|
|
334
|
-
return `${JSON.stringify({
|
|
335
|
-
artifact_language: languagePreference.artifact_language,
|
|
336
|
-
chat_language: languagePreference.chat_language,
|
|
337
|
-
runtime_language: languagePreference.runtime_language,
|
|
338
|
-
source: languagePreference.source,
|
|
339
|
-
detected_locale: languagePreference.detected_locale,
|
|
340
|
-
}, null, 2)}\n`;
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
function parseArgs(argv) {
|
|
344
|
-
const args = [...argv];
|
|
345
|
-
let target;
|
|
346
|
-
let dir = ".";
|
|
347
|
-
let force = false;
|
|
348
|
-
let json = false;
|
|
349
|
-
let statusCard = false;
|
|
350
|
-
let language;
|
|
351
|
-
let dirExplicit = false;
|
|
352
|
-
let surface = "generic";
|
|
353
|
-
let fixture;
|
|
354
|
-
let persist = false;
|
|
355
|
-
let model;
|
|
356
|
-
let generateCandidates = false;
|
|
357
|
-
let generatorModel;
|
|
358
|
-
let maxGeneratedCandidates = 5;
|
|
359
|
-
let generationTimeoutMs = 30000;
|
|
360
|
-
let generationCostUnits = 5;
|
|
361
|
-
let runId;
|
|
362
|
-
let allActive = false;
|
|
363
|
-
|
|
364
|
-
for (let i = 0; i < args.length; i += 1) {
|
|
365
|
-
const arg = args[i];
|
|
366
|
-
if (!arg) continue;
|
|
367
|
-
|
|
368
|
-
if (arg === "--help" || arg === "-h") {
|
|
369
|
-
printUsage();
|
|
370
|
-
process.exit(0);
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
if (arg === "--force") {
|
|
374
|
-
force = true;
|
|
375
|
-
continue;
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
if (arg === "--json") {
|
|
379
|
-
json = true;
|
|
380
|
-
continue;
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
if (arg === "--status-card") {
|
|
384
|
-
statusCard = true;
|
|
385
|
-
continue;
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
if (arg === "--persist") {
|
|
389
|
-
persist = true;
|
|
390
|
-
continue;
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
if (arg === "--generate-candidates") {
|
|
394
|
-
generateCandidates = true;
|
|
395
|
-
continue;
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
if (arg === "--all-active") {
|
|
399
|
-
allActive = true;
|
|
400
|
-
continue;
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
if (arg === "--run") {
|
|
404
|
-
const next = args[i + 1];
|
|
405
|
-
if (!next) {
|
|
406
|
-
console.error("Missing value for --run");
|
|
407
|
-
process.exit(1);
|
|
408
|
-
}
|
|
409
|
-
runId = next;
|
|
410
|
-
i += 1;
|
|
411
|
-
continue;
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
if (["--surface", "--fixture", "--model", "--generator-model", "--max-generated-candidates", "--generation-timeout-ms", "--generation-cost-units"].includes(arg)) {
|
|
415
|
-
const next = args[i + 1];
|
|
416
|
-
if (!next) {
|
|
417
|
-
console.error(`Missing value for ${arg}`);
|
|
418
|
-
process.exit(1);
|
|
419
|
-
}
|
|
420
|
-
if (arg === "--surface") {
|
|
421
|
-
if (!["codex", "claude", "copilot", "opencode", "generic"].includes(next)) {
|
|
422
|
-
console.error("Unsupported surface. Use codex, claude, copilot, opencode or generic.");
|
|
423
|
-
process.exit(1);
|
|
424
|
-
}
|
|
425
|
-
surface = next;
|
|
426
|
-
} else if (arg === "--fixture") fixture = next;
|
|
427
|
-
else if (arg === "--model") model = next;
|
|
428
|
-
else if (arg === "--generator-model") generatorModel = next;
|
|
429
|
-
else {
|
|
430
|
-
const value = Number(next);
|
|
431
|
-
const maximum = arg === "--generation-timeout-ms" ? 30000 : 5;
|
|
432
|
-
if (!Number.isInteger(value) || value < 1 || value > maximum) {
|
|
433
|
-
console.error(`${arg} must be an integer from 1 to ${maximum}.`);
|
|
434
|
-
process.exit(1);
|
|
435
|
-
}
|
|
436
|
-
if (arg === "--max-generated-candidates") maxGeneratedCandidates = value;
|
|
437
|
-
else if (arg === "--generation-timeout-ms") generationTimeoutMs = value;
|
|
438
|
-
else generationCostUnits = value;
|
|
439
|
-
}
|
|
440
|
-
i += 1;
|
|
441
|
-
continue;
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
if (arg === "--language" || arg === "--lang") {
|
|
445
|
-
const next = args[i + 1];
|
|
446
|
-
if (!next) {
|
|
447
|
-
console.error(`Missing value for ${arg}`);
|
|
448
|
-
process.exit(1);
|
|
449
|
-
}
|
|
450
|
-
const normalized = configuredLanguage(next);
|
|
451
|
-
if (!normalized) {
|
|
452
|
-
console.error("Invalid language tag. Use a BCP 47 tag such as de, en or fr-CA.");
|
|
453
|
-
process.exit(1);
|
|
454
|
-
}
|
|
455
|
-
language = normalized;
|
|
456
|
-
i += 1;
|
|
457
|
-
continue;
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
if (arg === "--dir") {
|
|
461
|
-
const next = args[i + 1];
|
|
462
|
-
if (!next) {
|
|
463
|
-
console.error("Missing value for --dir");
|
|
464
|
-
process.exit(1);
|
|
465
|
-
}
|
|
466
|
-
dir = next;
|
|
467
|
-
dirExplicit = true;
|
|
468
|
-
i += 1;
|
|
469
|
-
continue;
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
if (arg === "--target" || arg === "-t") {
|
|
473
|
-
const next = args[i + 1];
|
|
474
|
-
if (!next) {
|
|
475
|
-
console.error("Missing value for --target");
|
|
476
|
-
process.exit(1);
|
|
477
|
-
}
|
|
478
|
-
target = next;
|
|
479
|
-
i += 1;
|
|
480
|
-
continue;
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
if (!arg.startsWith("-") && !target) {
|
|
484
|
-
target = arg;
|
|
485
|
-
continue;
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
console.error(`Unknown argument: ${arg}`);
|
|
489
|
-
process.exit(1);
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
if (!target || !allowedTargets.has(target)) {
|
|
493
|
-
console.error("Please choose one target: codex, codex-repo, claude, copilot, opencode, opencode-status, opencode-repo, both, init, config, doctor, gate-check, delivery-map or delivery-path-search.");
|
|
494
|
-
printUsage();
|
|
495
|
-
process.exit(1);
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
return {
|
|
499
|
-
target,
|
|
500
|
-
dir: resolve(process.cwd(), dir),
|
|
501
|
-
force,
|
|
502
|
-
json,
|
|
503
|
-
statusCard,
|
|
504
|
-
dirExplicit,
|
|
505
|
-
language: resolveLanguagePreference(language),
|
|
506
|
-
surface,
|
|
507
|
-
fixture: fixture ? resolve(process.cwd(), fixture) : null,
|
|
508
|
-
persist,
|
|
509
|
-
model,
|
|
510
|
-
generateCandidates,
|
|
511
|
-
runId,
|
|
512
|
-
allActive,
|
|
513
|
-
generatorModel,
|
|
514
|
-
maxGeneratedCandidates,
|
|
515
|
-
generationTimeoutMs,
|
|
516
|
-
generationCostUnits,
|
|
517
|
-
};
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
function defaultOpenCodeConfigDir() {
|
|
521
|
-
return process.env.OPENCODE_CONFIG_DIR || join(homedir(), ".config", "opencode");
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
function installOpenCodeGlobalPlugin(configDir) {
|
|
525
|
-
assertGlobalOpenCodeSurfaceWritable(configDir);
|
|
526
|
-
const previousPackage = resolveOpenCodePackage(configDir);
|
|
527
|
-
const configPath = join(configDir, "opencode.json");
|
|
528
|
-
let config = {};
|
|
529
|
-
|
|
530
|
-
if (existsSync(configPath)) {
|
|
531
|
-
try {
|
|
532
|
-
config = JSON.parse(readFileSync(configPath, "utf8"));
|
|
533
|
-
} catch {
|
|
534
|
-
throw new Error(`Refusing to update unreadable OpenCode config: ${configPath}`);
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
if (config.plugin !== undefined && !Array.isArray(config.plugin)) {
|
|
539
|
-
throw new Error(`Refusing to update OpenCode config with non-array plugin field: ${configPath}`);
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
const plugins = [...(config.plugin ?? [])];
|
|
543
|
-
const alreadyInstalled = plugins.includes(pluginDefinition.opencode.npmPackage);
|
|
544
|
-
if (!alreadyInstalled) {
|
|
545
|
-
plugins.push(pluginDefinition.opencode.npmPackage);
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
const nextConfig = {
|
|
549
|
-
"$schema": config.$schema ?? "https://opencode.ai/config.json",
|
|
550
|
-
...config,
|
|
551
|
-
plugin: plugins,
|
|
552
|
-
};
|
|
553
|
-
|
|
554
|
-
if (nextConfig.instructions !== undefined && !Array.isArray(nextConfig.instructions)) {
|
|
555
|
-
throw new Error(`Refusing to update OpenCode config with non-array instructions field: ${configPath}`);
|
|
556
|
-
}
|
|
557
|
-
nextConfig.instructions = [...(nextConfig.instructions ?? [])];
|
|
558
|
-
if (!nextConfig.instructions.includes("AGDF.md")) nextConfig.instructions.push("AGDF.md");
|
|
559
|
-
|
|
560
|
-
if (nextConfig.permission === undefined) {
|
|
561
|
-
nextConfig.permission = { question: "allow", skill: { "agdf-*": "allow" } };
|
|
562
|
-
} else if (nextConfig.permission && typeof nextConfig.permission === "object" && !Array.isArray(nextConfig.permission)) {
|
|
563
|
-
nextConfig.permission = { ...nextConfig.permission };
|
|
564
|
-
if (nextConfig.permission.question === undefined) nextConfig.permission.question = "allow";
|
|
565
|
-
if (nextConfig.permission.skill === undefined) {
|
|
566
|
-
nextConfig.permission.skill = { "agdf-*": "allow" };
|
|
567
|
-
} else if (nextConfig.permission.skill && typeof nextConfig.permission.skill === "object" && !Array.isArray(nextConfig.permission.skill)) {
|
|
568
|
-
nextConfig.permission.skill = { ...nextConfig.permission.skill };
|
|
569
|
-
if (nextConfig.permission.skill["agdf-*"] === undefined) nextConfig.permission.skill["agdf-*"] = "allow";
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
mkdirSync(configDir, { recursive: true });
|
|
574
|
-
try {
|
|
575
|
-
const packageSpecifier = `${pluginDefinition.opencode.npmPackage}@${pluginDefinition.version}`;
|
|
576
|
-
execFileSync(npmCommand, [...npmPrefixArgs, "install", "--silent", "--save-prod", "--save-exact", packageSpecifier], {
|
|
577
|
-
cwd: configDir,
|
|
578
|
-
stdio: "pipe",
|
|
579
|
-
});
|
|
580
|
-
} catch (error) {
|
|
581
|
-
throw new Error(`Failed to install ${pluginDefinition.opencode.npmPackage} into the OpenCode config directory: ${(error.stderr || error.message).toString().trim()}`);
|
|
582
|
-
}
|
|
583
|
-
writeFileSync(configPath, `${JSON.stringify(nextConfig, null, 2)}\n`, "utf8");
|
|
584
|
-
const installedPackage = resolveOpenCodePackage(configDir);
|
|
585
|
-
|
|
586
|
-
return {
|
|
587
|
-
configPath,
|
|
588
|
-
added: !alreadyInstalled,
|
|
589
|
-
transition: openCodePackageTransition(previousPackage, installedPackage),
|
|
590
|
-
};
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
function globalOpenCodeConfigPaths(configDir) {
|
|
594
|
-
return {
|
|
595
|
-
instructions: join(configDir, pluginDefinition.opencode.instructionsFileName),
|
|
596
|
-
runtimeContract: join(configDir, pluginDefinition.opencode.runtimeContractFileName),
|
|
597
|
-
contracts: join(configDir, "contracts"),
|
|
598
|
-
skills: join(configDir, "skills"),
|
|
599
|
-
};
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
function globalOpenCodeOwnershipMarkerIsValid(content, marker, placement) {
|
|
603
|
-
const lines = content.split(/\r?\n/);
|
|
604
|
-
if (placement === "first-line") return lines[0] === marker;
|
|
605
|
-
const frontmatterEnd = lines.findIndex((line, index) => index > 0 && line === "---");
|
|
606
|
-
return frontmatterEnd >= 0 && lines[frontmatterEnd + 1] === marker;
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
function assertGlobalOpenCodeFileWritable(path, marker, placement) {
|
|
610
|
-
if (!existsSync(path)) return;
|
|
611
|
-
const existing = readFileSync(path, "utf8");
|
|
612
|
-
if (!globalOpenCodeOwnershipMarkerIsValid(existing, marker, placement)) {
|
|
613
|
-
throw new Error(`Refusing to overwrite unowned global OpenCode file: ${path}`);
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
function assertGlobalOpenCodeSurfaceWritable(configDir) {
|
|
618
|
-
const paths = globalOpenCodeConfigPaths(configDir);
|
|
619
|
-
assertGlobalOpenCodeFileWritable(paths.instructions, globalOpenCodeInstructionsOwnershipMarker, "first-line");
|
|
620
|
-
assertGlobalOpenCodeFileWritable(paths.runtimeContract, globalOpenCodeRuntimeContractOwnershipMarker, "first-line");
|
|
621
|
-
for (const moduleName of contractModules) {
|
|
622
|
-
assertGlobalOpenCodeFileWritable(
|
|
623
|
-
join(paths.contracts, moduleName),
|
|
624
|
-
globalOpenCodeRuntimeContractOwnershipMarker,
|
|
625
|
-
"first-line",
|
|
626
|
-
);
|
|
627
|
-
}
|
|
628
|
-
for (const skillName of globalOpenCodeSkillNames) {
|
|
629
|
-
assertGlobalOpenCodeFileWritable(
|
|
630
|
-
join(paths.skills, skillName, "SKILL.md"),
|
|
631
|
-
`${globalOpenCodeSkillOwnershipMarker}${skillName} -->`,
|
|
632
|
-
"after-frontmatter",
|
|
633
|
-
);
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
function writeOwnedGlobalOpenCodeFile(path, content, marker, placement) {
|
|
638
|
-
if (existsSync(path)) {
|
|
639
|
-
assertGlobalOpenCodeFileWritable(path, marker, placement);
|
|
640
|
-
}
|
|
641
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
642
|
-
writeFileSync(path, content, "utf8");
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
function globalOpenCodeBoundary() {
|
|
646
|
-
return [
|
|
647
|
-
"## Global OpenCode Surface Boundary",
|
|
648
|
-
"",
|
|
649
|
-
"This skill is globally discoverable, but global plugin or skill presence is not repository governance activation.",
|
|
650
|
-
"Before applying AGDF gates, later artefacts or implementation guidance, inspect the current repository for `.opencode/AGDF.md`, `.opencode/skills/agdf-gate-check/SKILL.md` and `.agdf/control/`.",
|
|
651
|
-
"If the repository surface is absent, stop and direct the user to `npx --yes @agdf/cli@latest opencode-repo` in this repository.",
|
|
652
|
-
"When the repository surface exists, its local instructions, native skills and `.agdf/control/` state are authoritative.",
|
|
653
|
-
"",
|
|
654
|
-
].join("\n");
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
function toGlobalOpenCodeContent(content) {
|
|
658
|
-
let next = content;
|
|
659
|
-
for (const skill of pluginDefinition.skillSet) {
|
|
660
|
-
const localName = `${pluginDefinition.opencode.skillPrefix}${skill.slug}`;
|
|
661
|
-
const globalName = `${pluginDefinition.opencode.globalSkillPrefix}${skill.slug}`;
|
|
662
|
-
next = next.replaceAll(localName, globalName);
|
|
663
|
-
}
|
|
664
|
-
return next;
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
function installOpenCodeGlobalSurface(configDir) {
|
|
668
|
-
const paths = globalOpenCodeConfigPaths(configDir);
|
|
669
|
-
const generatedOpenCodeRoot = join(generatedRoot, ".opencode");
|
|
670
|
-
const generatedInstructions = readFileSync(join(generatedOpenCodeRoot, pluginDefinition.opencode.instructionsFileName), "utf8");
|
|
671
|
-
const generatedRuntimeContract = readFileSync(join(generatedOpenCodeRoot, pluginDefinition.opencode.runtimeContractFileName), "utf8");
|
|
672
|
-
const globalInstructions = [
|
|
673
|
-
globalOpenCodeInstructionsOwnershipMarker,
|
|
674
|
-
"# AGDF Global OpenCode instructions",
|
|
675
|
-
"",
|
|
676
|
-
"AGDF native skills are globally discoverable through OpenCode.",
|
|
677
|
-
"",
|
|
678
|
-
globalOpenCodeBoundary(),
|
|
679
|
-
"The repository-local `.opencode/AGDF.md`, `.opencode/skills/` and `.agdf/control/` files remain the source of truth for active governance.",
|
|
680
|
-
"",
|
|
681
|
-
toGlobalOpenCodeContent(generatedInstructions),
|
|
682
|
-
].join("\n");
|
|
683
|
-
const globalRuntimeContract = `${globalOpenCodeRuntimeContractOwnershipMarker}\n${generatedRuntimeContract}`;
|
|
684
|
-
|
|
685
|
-
writeOwnedGlobalOpenCodeFile(paths.instructions, globalInstructions, globalOpenCodeInstructionsOwnershipMarker, "first-line");
|
|
686
|
-
writeOwnedGlobalOpenCodeFile(paths.runtimeContract, globalRuntimeContract, globalOpenCodeRuntimeContractOwnershipMarker, "first-line");
|
|
687
|
-
for (const moduleName of contractModules) {
|
|
688
|
-
const generatedModule = readFileSync(join(generatedOpenCodeRoot, "contracts", moduleName), "utf8");
|
|
689
|
-
writeOwnedGlobalOpenCodeFile(
|
|
690
|
-
join(paths.contracts, moduleName),
|
|
691
|
-
`${globalOpenCodeRuntimeContractOwnershipMarker}\n${generatedModule}`,
|
|
692
|
-
globalOpenCodeRuntimeContractOwnershipMarker,
|
|
693
|
-
"first-line",
|
|
694
|
-
);
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
for (let index = 0; index < openCodeSkillNames.length; index += 1) {
|
|
698
|
-
const sourceName = openCodeSkillNames[index];
|
|
699
|
-
const skillName = globalOpenCodeSkillNames[index];
|
|
700
|
-
const sourcePath = join(generatedOpenCodeRoot, "skills", sourceName, "SKILL.md");
|
|
701
|
-
const sourceContent = readFileSync(sourcePath, "utf8");
|
|
702
|
-
const marker = `${globalOpenCodeSkillOwnershipMarker}${skillName} -->`;
|
|
703
|
-
const content = toGlobalOpenCodeContent(sourceContent).replace(/^(---[\s\S]*?\n---\n)/, `$1${marker}\n\n${globalOpenCodeBoundary()}`);
|
|
704
|
-
writeOwnedGlobalOpenCodeFile(join(paths.skills, skillName, "SKILL.md"), content, marker, "after-frontmatter");
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
return {
|
|
708
|
-
instructions: paths.instructions,
|
|
709
|
-
runtimeContract: paths.runtimeContract,
|
|
710
|
-
contracts: paths.contracts,
|
|
711
|
-
skills: paths.skills,
|
|
712
|
-
};
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
function readOpenCodeConfig(configPath) {
|
|
716
|
-
if (!existsSync(configPath)) return { exists: false, parseError: "", config: {} };
|
|
717
|
-
try {
|
|
718
|
-
return { exists: true, parseError: "", config: JSON.parse(readFileSync(configPath, "utf8")) };
|
|
719
|
-
} catch (error) {
|
|
720
|
-
return { exists: true, parseError: error.message, config: {} };
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
function isOpenCodeRepositorySurfacePresent(targetDir) {
|
|
725
|
-
return existsSync(join(targetDir, ".opencode", pluginDefinition.opencode.instructionsFileName))
|
|
726
|
-
&& existsSync(join(targetDir, ".opencode", "skills", `${pluginDefinition.opencode.skillPrefix}gate-check`, "SKILL.md"));
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
function resolveOpenCodePackage(configDir) {
|
|
730
|
-
const packageName = pluginDefinition.opencode.npmPackage;
|
|
731
|
-
try {
|
|
732
|
-
const resolvedPath = execFileSync(process.execPath, ["-e", `process.stdout.write(require.resolve(${JSON.stringify(packageName)}))`], {
|
|
733
|
-
cwd: configDir,
|
|
734
|
-
encoding: "utf8",
|
|
735
|
-
stdio: "pipe",
|
|
736
|
-
});
|
|
737
|
-
let installedVersion = "";
|
|
738
|
-
try {
|
|
739
|
-
const packageManifest = JSON.parse(readFileSync(join(dirname(resolvedPath), "package.json"), "utf8"));
|
|
740
|
-
installedVersion = typeof packageManifest.version === "string" && packageManifest.version.trim() ? packageManifest.version.trim() : "";
|
|
741
|
-
} catch {
|
|
742
|
-
installedVersion = "";
|
|
743
|
-
}
|
|
744
|
-
return {
|
|
745
|
-
loadable: true,
|
|
746
|
-
path: resolvedPath,
|
|
747
|
-
installed_version: installedVersion,
|
|
748
|
-
error: "",
|
|
749
|
-
};
|
|
750
|
-
} catch (error) {
|
|
751
|
-
return {
|
|
752
|
-
loadable: false,
|
|
753
|
-
path: "",
|
|
754
|
-
installed_version: "",
|
|
755
|
-
error: (error.stderr || error.message || "package not resolvable").toString().trim(),
|
|
756
|
-
};
|
|
757
|
-
}
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
function openCodePackageVersionStatus(packageState) {
|
|
761
|
-
if (!packageState.loadable) return "unloadable";
|
|
762
|
-
if (!packageState.installed_version) return "unknown";
|
|
763
|
-
return packageState.installed_version === pluginDefinition.version ? "current" : "outdated";
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
function openCodePackageTransition(previousPackage, installedPackage) {
|
|
767
|
-
const previousVersion = previousPackage.loadable ? previousPackage.installed_version : "";
|
|
768
|
-
const installedVersion = installedPackage.loadable ? installedPackage.installed_version : "";
|
|
769
|
-
if (!installedVersion || (previousPackage.loadable && !previousVersion)) {
|
|
770
|
-
return {
|
|
771
|
-
previous_version: previousVersion,
|
|
772
|
-
installed_version: installedVersion,
|
|
773
|
-
status: "unknown",
|
|
774
|
-
};
|
|
775
|
-
}
|
|
776
|
-
if (!previousPackage.loadable) {
|
|
777
|
-
return {
|
|
778
|
-
previous_version: "",
|
|
779
|
-
installed_version: installedVersion,
|
|
780
|
-
status: "installed",
|
|
781
|
-
};
|
|
782
|
-
}
|
|
783
|
-
return {
|
|
784
|
-
previous_version: previousVersion,
|
|
785
|
-
installed_version: installedVersion,
|
|
786
|
-
status: previousVersion === installedVersion ? "unchanged" : "updated",
|
|
787
|
-
};
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
function evaluateGlobalOpenCodeSurface(configDir) {
|
|
791
|
-
const paths = globalOpenCodeConfigPaths(configDir);
|
|
792
|
-
const skillCount = globalOpenCodeSkillNames.filter((skillName) => existsSync(join(paths.skills, skillName, "SKILL.md"))).length;
|
|
793
|
-
const contractCount = contractModules.filter((moduleName) => existsSync(join(paths.contracts, moduleName))).length;
|
|
794
|
-
return {
|
|
795
|
-
path: paths.skills,
|
|
796
|
-
instructions: paths.instructions,
|
|
797
|
-
runtime_contract: paths.runtimeContract,
|
|
798
|
-
contracts: paths.contracts,
|
|
799
|
-
expected_skill_count: globalOpenCodeSkillNames.length,
|
|
800
|
-
skill_count: skillCount,
|
|
801
|
-
expected_contract_count: contractModules.length,
|
|
802
|
-
contract_count: contractCount,
|
|
803
|
-
present: existsSync(paths.skills),
|
|
804
|
-
complete: existsSync(paths.instructions)
|
|
805
|
-
&& existsSync(paths.runtimeContract)
|
|
806
|
-
&& contractCount === contractModules.length
|
|
807
|
-
&& skillCount === openCodeSkillNames.length,
|
|
808
|
-
};
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
function evaluateOpenCodeStatus(targetDir, configDir = defaultOpenCodeConfigDir(), transition = null) {
|
|
812
|
-
const configPath = join(configDir, "opencode.json");
|
|
813
|
-
const configState = readOpenCodeConfig(configPath);
|
|
814
|
-
const plugins = Array.isArray(configState.config.plugin) ? configState.config.plugin : [];
|
|
815
|
-
const globalConfigured = plugins.includes(pluginDefinition.opencode.npmPackage);
|
|
816
|
-
const packageState = resolveOpenCodePackage(configDir);
|
|
817
|
-
const packageVersionStatus = openCodePackageVersionStatus(packageState);
|
|
818
|
-
const globalNativeSurface = evaluateGlobalOpenCodeSurface(configDir);
|
|
819
|
-
const sessionSignals = {
|
|
820
|
-
active: process.env.AGDF_PLUGIN_ACTIVE === "1",
|
|
821
|
-
version: process.env.AGDF_PLUGIN_VERSION || "",
|
|
822
|
-
control_dir: process.env.AGDF_CONTROL_DIR || "",
|
|
823
|
-
repository_surface: process.env.AGDF_OPENCODE_REPOSITORY_SURFACE === "1",
|
|
824
|
-
};
|
|
825
|
-
const repositorySurface = isOpenCodeRepositorySurfacePresent(targetDir);
|
|
826
|
-
const gateCheckSkillPath = join(targetDir, ".opencode", "skills", `${pluginDefinition.opencode.skillPrefix}gate-check`, "SKILL.md");
|
|
827
|
-
|
|
828
|
-
const findings = [];
|
|
829
|
-
if (!configState.exists) findings.push("OpenCode global config not found.");
|
|
830
|
-
if (configState.parseError) findings.push(`OpenCode global config is not valid JSON: ${configState.parseError}`);
|
|
831
|
-
if (!globalConfigured) findings.push(`OpenCode global config does not include ${pluginDefinition.opencode.npmPackage}.`);
|
|
832
|
-
if (!packageState.loadable) findings.push(`${pluginDefinition.opencode.npmPackage} is not loadable from the OpenCode config directory.`);
|
|
833
|
-
if (packageVersionStatus === "outdated") findings.push(`${pluginDefinition.opencode.npmPackage} version ${packageState.installed_version} is outdated; expected ${pluginDefinition.version}.`);
|
|
834
|
-
if (packageVersionStatus === "unknown" && packageState.loadable) findings.push(`${pluginDefinition.opencode.npmPackage} is loadable but its installed version is unknown.`);
|
|
835
|
-
if (!globalNativeSurface.complete) {
|
|
836
|
-
findings.push(
|
|
837
|
-
`Global OpenCode native surface is incomplete (${globalNativeSurface.skill_count}/${globalNativeSurface.expected_skill_count} skills; ${globalNativeSurface.contract_count}/${globalNativeSurface.expected_contract_count} contract modules).`,
|
|
838
|
-
);
|
|
839
|
-
}
|
|
840
|
-
if (!sessionSignals.active) findings.push("No active AGDF OpenCode session signal is visible in this process.");
|
|
841
|
-
if (!repositorySurface) findings.push("Current repository does not contain the AGDF OpenCode surface.");
|
|
842
|
-
|
|
843
|
-
return {
|
|
844
|
-
schema_version: "1",
|
|
845
|
-
status: globalConfigured && packageState.loadable ? "configured" : "not_configured",
|
|
846
|
-
global_config: {
|
|
847
|
-
path: configPath,
|
|
848
|
-
exists: configState.exists,
|
|
849
|
-
parse_error: configState.parseError,
|
|
850
|
-
plugin_configured: globalConfigured,
|
|
851
|
-
},
|
|
852
|
-
package: {
|
|
853
|
-
name: pluginDefinition.opencode.npmPackage,
|
|
854
|
-
loadable: packageState.loadable,
|
|
855
|
-
resolved_path: packageState.path,
|
|
856
|
-
installed_version: packageState.installed_version || null,
|
|
857
|
-
expected_version: pluginDefinition.version,
|
|
858
|
-
version_status: packageVersionStatus,
|
|
859
|
-
...(transition ? { transition } : {}),
|
|
860
|
-
error: packageState.error,
|
|
861
|
-
},
|
|
862
|
-
global_native_surface: globalNativeSurface,
|
|
863
|
-
session: sessionSignals,
|
|
864
|
-
repository_surface: {
|
|
865
|
-
path: targetDir,
|
|
866
|
-
present: repositorySurface,
|
|
867
|
-
instructions: join(targetDir, ".opencode", pluginDefinition.opencode.instructionsFileName),
|
|
868
|
-
gate_check_agent: gateCheckSkillPath,
|
|
869
|
-
gate_check_skill: gateCheckSkillPath,
|
|
870
|
-
},
|
|
871
|
-
visible_entrypoint: repositorySurface ? `${pluginDefinition.opencode.skillPrefix}gate-check (native skill)` : "none until opencode-repo is installed for this repository",
|
|
872
|
-
findings,
|
|
873
|
-
next_step: packageVersionStatus !== "current"
|
|
874
|
-
? "Run npx --yes @agdf/cli@latest opencode to install or repair the OpenCode package version."
|
|
875
|
-
: !globalNativeSurface.complete
|
|
876
|
-
? "Run npx --yes @agdf/cli@latest opencode to install or repair the global native OpenCode skill surface."
|
|
877
|
-
: repositorySurface
|
|
878
|
-
? `Restart OpenCode if needed, then load ${pluginDefinition.opencode.skillPrefix}gate-check through the native skill tool for new build/change intent.`
|
|
879
|
-
: "Run npx --yes @agdf/cli@latest opencode-repo in repositories where AGDF governance should be active and reviewable.",
|
|
880
|
-
};
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
function printOpenCodeStatus(report, json) {
|
|
884
|
-
if (json) {
|
|
885
|
-
console.log(JSON.stringify(report, null, 2));
|
|
886
|
-
return;
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
console.log("AGDF OpenCode status");
|
|
890
|
-
console.log(`Status: ${report.status}`);
|
|
891
|
-
console.log(`Global config: ${report.global_config.plugin_configured ? "configured" : "missing"} (${report.global_config.path})`);
|
|
892
|
-
if (report.global_config.parse_error) console.log(`Config parse error: ${report.global_config.parse_error}`);
|
|
893
|
-
console.log(`Package loadable: ${report.package.loadable ? "yes" : "no"}`);
|
|
894
|
-
if (report.package.resolved_path) console.log(`Package path: ${report.package.resolved_path}`);
|
|
895
|
-
console.log(`Package version: ${report.package.installed_version || "unknown"}`);
|
|
896
|
-
console.log(`Expected version: ${report.package.expected_version}`);
|
|
897
|
-
console.log(`Version status: ${report.package.version_status}`);
|
|
898
|
-
if (report.package.transition?.status === "updated") {
|
|
899
|
-
console.log(`Version transition: ${report.package.transition.previous_version} -> ${report.package.transition.installed_version}`);
|
|
900
|
-
} else if (report.package.transition?.status === "installed") {
|
|
901
|
-
console.log(`Version transition: new install (${report.package.transition.installed_version || "unknown"})`);
|
|
902
|
-
} else if (report.package.transition?.status === "unchanged") {
|
|
903
|
-
console.log(`Version transition: unchanged (${report.package.transition.installed_version})`);
|
|
904
|
-
} else if (report.package.transition?.status === "unknown") {
|
|
905
|
-
console.log("Version transition: unknown");
|
|
906
|
-
}
|
|
907
|
-
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})`);
|
|
908
|
-
console.log(`Global skill path: ${report.global_native_surface.path}`);
|
|
909
|
-
console.log(`Session active signal: ${report.session.active ? "yes" : "no"}`);
|
|
910
|
-
if (report.session.version) console.log(`Session plugin version: ${report.session.version}`);
|
|
911
|
-
if (report.session.control_dir) console.log(`Session control dir: ${report.session.control_dir}`);
|
|
912
|
-
console.log(`Repository surface: ${report.repository_surface.present ? "present" : "missing"}`);
|
|
913
|
-
console.log(`Visible entrypoint: ${report.visible_entrypoint}`);
|
|
914
|
-
console.log(`Next step: ${report.next_step}`);
|
|
915
|
-
|
|
916
|
-
if (report.findings.length > 0) {
|
|
917
|
-
console.log("");
|
|
918
|
-
console.log("Findings:");
|
|
919
|
-
for (const finding of report.findings) console.log(`- ${finding}`);
|
|
920
|
-
}
|
|
921
|
-
}
|
|
922
|
-
|
|
923
|
-
function installCodexGlobalPlugin() {
|
|
924
|
-
const expectedVersion = pluginDefinition.version;
|
|
925
|
-
|
|
926
|
-
try {
|
|
927
|
-
execFileSync("codex", ["plugin", "marketplace", "add", "arndtgold/ai-native-governance-delivery-framework"], { stdio: "inherit" });
|
|
928
|
-
execFileSync("codex", ["plugin", "marketplace", "upgrade", "agdf"], { stdio: "inherit" });
|
|
929
|
-
execFileSync("codex", ["plugin", "add", "agdf", "--marketplace", "agdf"], { stdio: "inherit" });
|
|
930
|
-
const listOutput = execFileSync("codex", ["plugin", "list"], { encoding: "utf8", stdio: "pipe" });
|
|
931
|
-
const installedVersion = pluginVersionFromList(listOutput, "agdf@agdf");
|
|
932
|
-
if (installedVersion !== expectedVersion) {
|
|
933
|
-
throw new Error(versionMismatchMessage("Codex", "agdf@agdf", expectedVersion, installedVersion, "codex plugin marketplace upgrade agdf && codex plugin add agdf --marketplace agdf"));
|
|
934
|
-
}
|
|
935
|
-
console.log(`AGDF Codex plugin version verified: ${installedVersion}.`);
|
|
936
|
-
} catch (error) {
|
|
937
|
-
if (error.message?.startsWith("AGDF Codex plugin version mismatch")) throw error;
|
|
938
|
-
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());
|
|
939
|
-
}
|
|
940
|
-
}
|
|
941
|
-
|
|
942
|
-
function installClaudeGlobalPlugin() {
|
|
943
|
-
const expectedVersion = pluginDefinition.version;
|
|
944
|
-
|
|
945
|
-
try {
|
|
946
|
-
execFileSync("claude", ["plugin", "marketplace", "add", "arndtgold/ai-native-governance-delivery-framework"], { stdio: "inherit" });
|
|
947
|
-
execFileSync("claude", ["plugin", "marketplace", "update", "agdf"], { stdio: "inherit" });
|
|
948
|
-
const beforeList = execFileSync("claude", ["plugin", "list"], { encoding: "utf8", stdio: "pipe" });
|
|
949
|
-
const alreadyInstalled = pluginListHasPlugin(beforeList, "agdf@agdf");
|
|
950
|
-
execFileSync("claude", ["plugin", alreadyInstalled ? "update" : "install", "agdf@agdf"], { stdio: "inherit" });
|
|
951
|
-
const afterList = execFileSync("claude", ["plugin", "list"], { encoding: "utf8", stdio: "pipe" });
|
|
952
|
-
const installedVersion = pluginVersionFromList(afterList, "agdf@agdf");
|
|
953
|
-
if (installedVersion) {
|
|
954
|
-
if (installedVersion !== expectedVersion) {
|
|
955
|
-
throw new Error(versionMismatchMessage("Claude Code", "agdf@agdf", expectedVersion, installedVersion, "claude plugin marketplace update agdf && claude plugin update agdf@agdf"));
|
|
956
|
-
}
|
|
957
|
-
console.log(`AGDF Claude Code plugin version verified: ${installedVersion}.`);
|
|
958
|
-
} else {
|
|
959
|
-
console.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.");
|
|
960
|
-
}
|
|
961
|
-
} catch (error) {
|
|
962
|
-
if (error.message?.startsWith("AGDF Claude Code plugin version mismatch")) throw error;
|
|
963
|
-
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());
|
|
964
|
-
}
|
|
965
|
-
}
|
|
966
|
-
|
|
967
|
-
function commandErrorText(error) {
|
|
968
|
-
return (error.stderr || error.stdout || error.message || "").toString().trim();
|
|
969
|
-
}
|
|
970
|
-
|
|
971
|
-
function pluginListHasPlugin(output, pluginId) {
|
|
972
|
-
return output
|
|
973
|
-
.split(/\r?\n/)
|
|
974
|
-
.some((line) => line.includes(pluginId));
|
|
975
|
-
}
|
|
976
|
-
|
|
977
|
-
function pluginVersionFromList(output, pluginId) {
|
|
978
|
-
const escapedPluginId = pluginId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
979
|
-
const line = output
|
|
980
|
-
.split(/\r?\n/)
|
|
981
|
-
.find((entry) => new RegExp(`(^|\\s)${escapedPluginId}(\\s|$)`).test(entry));
|
|
982
|
-
if (!line) return "";
|
|
983
|
-
const versionMatch = line.match(/\b(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)\b/);
|
|
984
|
-
return versionMatch?.[1] ?? "";
|
|
985
|
-
}
|
|
986
|
-
|
|
987
|
-
function versionMismatchMessage(surface, pluginId, expectedVersion, installedVersion, correctiveCommand) {
|
|
988
|
-
return `AGDF ${surface} plugin version mismatch for ${pluginId}: expected ${expectedVersion}, observed ${installedVersion || "unknown"}. Refresh with: ${correctiveCommand}`;
|
|
989
|
-
}
|
|
990
|
-
|
|
991
|
-
function loadAsset(relativePath) {
|
|
992
|
-
return readFileSync(join(generatedRoot, relativePath), "utf8");
|
|
993
|
-
}
|
|
994
|
-
|
|
995
|
-
function writeGeneratedFile(targetDir, relativePath, content, force, allowOverwrite = false) {
|
|
996
|
-
const outputPath = join(targetDir, relativePath);
|
|
997
|
-
mkdirSync(dirname(outputPath), { recursive: true });
|
|
998
|
-
|
|
999
|
-
if (existsSync(outputPath) && !force && !allowOverwrite) {
|
|
1000
|
-
throw new Error(`Refusing to overwrite existing file: ${relativePath}. Re-run with --force if you want to replace it.`);
|
|
1001
|
-
}
|
|
1002
|
-
|
|
1003
|
-
writeFileSync(outputPath, content, "utf8");
|
|
1004
|
-
}
|
|
1005
|
-
|
|
1006
|
-
function removeOwnedLegacyOpenCodeAgents(targetDir) {
|
|
1007
|
-
const agentsDir = join(targetDir, ".opencode", "agents");
|
|
1008
|
-
if (!existsSync(agentsDir)) return [];
|
|
1009
|
-
|
|
1010
|
-
const removed = [];
|
|
1011
|
-
for (const skill of pluginDefinition.skillSet) {
|
|
1012
|
-
const skillName = `${pluginDefinition.opencode.skillPrefix}${skill.slug}`;
|
|
1013
|
-
const relativePath = join(".opencode", "agents", `${skillName}.md`);
|
|
1014
|
-
const agentPath = join(targetDir, relativePath);
|
|
1015
|
-
if (!existsSync(agentPath)) continue;
|
|
1016
|
-
|
|
1017
|
-
const content = readFileSync(agentPath, "utf8");
|
|
1018
|
-
const ownedHeader = `---\ndescription: ${skill.useFor}\nmode: subagent\n---`;
|
|
1019
|
-
if (!content.startsWith(ownedHeader) || !content.includes(`# ${skill.slug}`)) continue;
|
|
1020
|
-
|
|
1021
|
-
rmSync(agentPath);
|
|
1022
|
-
removed.push(relativePath);
|
|
1023
|
-
}
|
|
1024
|
-
|
|
1025
|
-
if (readdirSync(agentsDir).length === 0) rmSync(agentsDir, { recursive: true });
|
|
1026
|
-
return removed;
|
|
1027
|
-
}
|
|
1028
|
-
|
|
1029
|
-
function assertGeneratedWritePlan(targetDir, files, force) {
|
|
1030
|
-
const blocked = files.find((file) => existsSync(join(targetDir, file.path)) && !force && !file.allowOverwrite);
|
|
1031
|
-
if (blocked) {
|
|
1032
|
-
throw new Error(`Refusing to overwrite existing file: ${blocked.path}. Re-run with --force if you want to replace it.`);
|
|
1033
|
-
}
|
|
1034
|
-
}
|
|
1035
|
-
|
|
1036
|
-
function isAgdfOwnedAgentsFile(content) {
|
|
1037
|
-
return content.includes("## Surface Convention")
|
|
1038
|
-
&& content.includes("GitHub Copilot repository skills do not have a plugin namespace")
|
|
1039
|
-
&& content.includes("AGDF is agent-native first and CLI-verifiable by design");
|
|
1040
|
-
}
|
|
1041
|
-
|
|
1042
|
-
function addCopilotAgentsFile(files, targetDir, force) {
|
|
1043
|
-
const agentsPath = join(targetDir, "AGENTS.md");
|
|
1044
|
-
if (!existsSync(agentsPath) || force) {
|
|
1045
|
-
files.push({
|
|
1046
|
-
path: "AGENTS.md",
|
|
1047
|
-
content: loadAsset("AGENTS.md"),
|
|
1048
|
-
});
|
|
1049
|
-
return;
|
|
1050
|
-
}
|
|
1051
|
-
|
|
1052
|
-
const existingAgents = readFileSync(agentsPath, "utf8");
|
|
1053
|
-
if (isAgdfOwnedAgentsFile(existingAgents)) {
|
|
1054
|
-
files.push({
|
|
1055
|
-
path: "AGENTS.md",
|
|
1056
|
-
content: loadAsset("AGENTS.md"),
|
|
1057
|
-
allowOverwrite: true,
|
|
1058
|
-
action: "refreshed",
|
|
1059
|
-
});
|
|
1060
|
-
return;
|
|
1061
|
-
}
|
|
1062
|
-
|
|
1063
|
-
files.push({
|
|
1064
|
-
path: agdfFragmentPath,
|
|
1065
|
-
content: loadAsset("AGENTS.md"),
|
|
1066
|
-
allowOverwrite: true,
|
|
1067
|
-
action: "refreshed",
|
|
1068
|
-
preserved: "AGENTS.md",
|
|
1069
|
-
});
|
|
1070
|
-
}
|
|
1071
|
-
|
|
1072
|
-
function shouldWriteLanguageConfig(target, targetDir, force) {
|
|
1073
|
-
if (force) return true;
|
|
1074
|
-
if ((target === "copilot" || target === "both") && existsSync(join(targetDir, ".agdf", "control", "config.json"))) return false;
|
|
1075
|
-
return true;
|
|
1076
|
-
}
|
|
1077
|
-
|
|
1078
|
-
function addLanguageConfig(files, languagePreference) {
|
|
1079
|
-
files.push({
|
|
1080
|
-
path: join(".agdf", "control", "config.json"),
|
|
1081
|
-
content: languageConfigContent(languagePreference),
|
|
1082
|
-
});
|
|
1083
|
-
}
|
|
1084
|
-
|
|
1085
|
-
function generatedFilesForTarget(target, targetDir, force, languagePreference) {
|
|
1086
|
-
const files = [];
|
|
1087
|
-
|
|
1088
|
-
if (target === "config") {
|
|
1089
|
-
files.push({
|
|
1090
|
-
path: join(".agdf", "control", "config.json"),
|
|
1091
|
-
content: languageConfigContent(languagePreference),
|
|
1092
|
-
allowOverwrite: true,
|
|
1093
|
-
});
|
|
1094
|
-
return files;
|
|
1095
|
-
}
|
|
1096
|
-
|
|
1097
|
-
if (target === "init") {
|
|
1098
|
-
addLanguageConfig(files, languagePreference);
|
|
1099
|
-
files.push({
|
|
1100
|
-
path: join(".agdf", "control", "README.md"),
|
|
1101
|
-
content: loadAsset(join(".agdf", "control", "README.md")),
|
|
1102
|
-
});
|
|
1103
|
-
|
|
1104
|
-
for (const controlPath of liveControlFiles) {
|
|
1105
|
-
files.push({
|
|
1106
|
-
path: controlPath.path,
|
|
1107
|
-
content: loadAsset(controlPath.source),
|
|
1108
|
-
});
|
|
1109
|
-
}
|
|
1110
|
-
|
|
1111
|
-
for (const templatePath of artefactTemplateFiles) {
|
|
1112
|
-
files.push({
|
|
1113
|
-
path: templatePath,
|
|
1114
|
-
content: loadAsset(templatePath),
|
|
1115
|
-
});
|
|
1116
|
-
}
|
|
1117
|
-
|
|
1118
|
-
return files;
|
|
1119
|
-
}
|
|
1120
|
-
|
|
1121
|
-
if (target === "codex-repo" || target === "both") {
|
|
1122
|
-
if (shouldWriteLanguageConfig(target, targetDir, force)) addLanguageConfig(files, languagePreference);
|
|
1123
|
-
for (const codexPath of codexPluginFiles) {
|
|
1124
|
-
files.push({
|
|
1125
|
-
path: codexPath,
|
|
1126
|
-
content: loadAsset(codexPath),
|
|
1127
|
-
});
|
|
1128
|
-
}
|
|
1129
|
-
}
|
|
1130
|
-
|
|
1131
|
-
if (target === "copilot" || target === "both") {
|
|
1132
|
-
if (target !== "both" && shouldWriteLanguageConfig(target, targetDir, force)) addLanguageConfig(files, languagePreference);
|
|
1133
|
-
addCopilotAgentsFile(files, targetDir, force);
|
|
1134
|
-
|
|
1135
|
-
for (const controlPath of controlFiles) {
|
|
1136
|
-
files.push({
|
|
1137
|
-
path: controlPath,
|
|
1138
|
-
content: loadAsset(controlPath),
|
|
1139
|
-
allowOverwrite: true,
|
|
1140
|
-
action: "refreshed",
|
|
1141
|
-
});
|
|
1142
|
-
}
|
|
1143
|
-
|
|
1144
|
-
for (const instructionPath of copilotInstructionFiles) {
|
|
1145
|
-
files.push({
|
|
1146
|
-
path: instructionPath,
|
|
1147
|
-
content: loadAsset(instructionPath),
|
|
1148
|
-
allowOverwrite: true,
|
|
1149
|
-
action: "refreshed",
|
|
1150
|
-
});
|
|
1151
|
-
}
|
|
1152
|
-
|
|
1153
|
-
for (const skillPath of copilotSkillFiles) {
|
|
1154
|
-
files.push({
|
|
1155
|
-
path: skillPath,
|
|
1156
|
-
content: loadAsset(skillPath),
|
|
1157
|
-
allowOverwrite: true,
|
|
1158
|
-
action: "refreshed",
|
|
1159
|
-
});
|
|
1160
|
-
}
|
|
1161
|
-
}
|
|
1162
|
-
|
|
1163
|
-
if (target === "opencode-repo") {
|
|
1164
|
-
addLanguageConfig(files, languagePreference);
|
|
1165
|
-
const openCodeConfigTargetPath = existsSync(join(targetDir, "opencode.json")) && !force ? openCodeConfigFragmentPath : "opencode.json";
|
|
1166
|
-
files.push({
|
|
1167
|
-
path: openCodeConfigTargetPath,
|
|
1168
|
-
content: loadAsset("opencode.json"),
|
|
1169
|
-
});
|
|
1170
|
-
|
|
1171
|
-
for (const openCodePath of openCodeFiles) {
|
|
1172
|
-
files.push({
|
|
1173
|
-
path: openCodePath,
|
|
1174
|
-
content: loadAsset(openCodePath),
|
|
1175
|
-
});
|
|
1176
|
-
}
|
|
1177
|
-
|
|
1178
|
-
for (const controlPath of controlFiles) {
|
|
1179
|
-
files.push({
|
|
1180
|
-
path: controlPath,
|
|
1181
|
-
content: loadAsset(controlPath),
|
|
1182
|
-
});
|
|
1183
|
-
}
|
|
1184
|
-
}
|
|
1185
|
-
|
|
1186
|
-
return files;
|
|
1187
|
-
}
|
|
1188
|
-
|
|
1189
|
-
function printNextSteps(target, destination, files, wroteAgentsFragment, wroteOpenCodeConfigFragment, removedOpenCodeAgents = []) {
|
|
1190
|
-
console.log("");
|
|
1191
|
-
console.log(`AGDF bootstrap complete in ${destination}`);
|
|
1192
|
-
console.log("");
|
|
1193
|
-
console.log("Generated:");
|
|
1194
|
-
for (const file of files) {
|
|
1195
|
-
const action = file.action ? `${file.action}: ` : "";
|
|
1196
|
-
console.log(`- ${action}${file.path}`);
|
|
1197
|
-
}
|
|
1198
|
-
|
|
1199
|
-
if (removedOpenCodeAgents.length > 0) {
|
|
1200
|
-
console.log("");
|
|
1201
|
-
console.log("Removed generated legacy OpenCode agents:");
|
|
1202
|
-
for (const relativePath of removedOpenCodeAgents) console.log(`- ${relativePath}`);
|
|
1203
|
-
}
|
|
1204
|
-
|
|
1205
|
-
const preservedFiles = [...new Set(files.map((file) => file.preserved).filter(Boolean))];
|
|
1206
|
-
if (preservedFiles.length > 0) {
|
|
1207
|
-
console.log("");
|
|
1208
|
-
console.log("Preserved:");
|
|
1209
|
-
for (const preserved of preservedFiles) console.log(`- ${preserved}`);
|
|
1210
|
-
}
|
|
1211
|
-
|
|
1212
|
-
console.log("");
|
|
1213
|
-
console.log("Next steps:");
|
|
1214
|
-
const languageConfig = files.find((file) => file.path === join(".agdf", "control", "config.json"));
|
|
1215
|
-
if (languageConfig) {
|
|
1216
|
-
const language = JSON.parse(languageConfig.content);
|
|
1217
|
-
console.log(`- AGDF language preference: artefacts=${language.artifact_language}, chat=${language.chat_language}, runtime=${language.runtime_language}.`);
|
|
1218
|
-
}
|
|
1219
|
-
if (target === "init") {
|
|
1220
|
-
console.log("- Create or migrate a canonical run, then fill its RUN_STATE.md with the current gate, evidence and next allowed action.");
|
|
1221
|
-
console.log("- Run npx --yes @agdf/cli@latest doctor to check the control state before the next agent run.");
|
|
1222
|
-
console.log("- Commit the live control files once they represent the repository's current delivery state.");
|
|
1223
|
-
return;
|
|
1224
|
-
}
|
|
1225
|
-
if (target === "config") {
|
|
1226
|
-
console.log("- Restart or start a new agent session so the AGDF SessionStart hook reads the updated project language config.");
|
|
1227
|
-
console.log("- Run npx --yes @agdf/cli@latest doctor when this repository also uses durable AGDF control state.");
|
|
1228
|
-
return;
|
|
1229
|
-
}
|
|
1230
|
-
if (wroteAgentsFragment) {
|
|
1231
|
-
console.log(`- Existing AGENTS.md detected. Merge ${agdfFragmentPath} into your current AGENTS.md before using Copilot with AGDF.`);
|
|
1232
|
-
}
|
|
1233
|
-
if (target === "codex-repo" || target === "both") {
|
|
1234
|
-
console.log("- Restart Codex in this repository, open /plugins, select This repository and install agdf.");
|
|
1235
|
-
console.log("- Start a new Codex thread in this repository and ask: Run an AGDF gate check for this request.");
|
|
1236
|
-
}
|
|
1237
|
-
if (target === "both") {
|
|
1238
|
-
console.log(`- Optional global Claude Code install: ${pluginInstallCommand}`);
|
|
1239
|
-
}
|
|
1240
|
-
if (target === "opencode-repo") {
|
|
1241
|
-
if (wroteOpenCodeConfigFragment) {
|
|
1242
|
-
console.log(`- Existing opencode.json detected. Review ${openCodeConfigFragmentPath} and merge its owned entries so OpenCode loads .opencode/AGDF.md; preserve an explicit permission.question decision.`);
|
|
1243
|
-
}
|
|
1244
|
-
console.log(`- OpenCode will install the ${pluginDefinition.opencode.npmPackage} npm plugin from opencode.json at startup.`);
|
|
1245
|
-
console.log("- Optional: also add create-agdf to ~/.config/opencode/opencode.json plugin[] for a user-wide OpenCode hook.");
|
|
1246
|
-
console.log("- The global hook does not replace repository instructions or native skills; this repository's .opencode files remain the AGDF source of truth.");
|
|
1247
|
-
console.log("- Start OpenCode in this repository; it will load opencode.json, .opencode/AGDF.md and the native AGDF skills.");
|
|
1248
|
-
console.log("- Load agdf-gate-check through OpenCode's native skill tool for new build/change intent or unclear approval before later artefacts or implementation.");
|
|
1249
|
-
console.log("- Run npx --yes @agdf/cli@latest init when the repository needs live AGDF control files.");
|
|
1250
|
-
}
|
|
1251
|
-
if (target === "copilot" || target === "both") {
|
|
1252
|
-
console.log("- In GitHub Copilot CLI, run /instructions after the AGENTS.md step is complete to confirm that AGDF instructions and the repository skills are visible.");
|
|
1253
|
-
console.log("- Run npx --yes @agdf/cli@latest init when the repository needs live AGDF control files.");
|
|
1254
|
-
}
|
|
1255
|
-
console.log("- Commit the generated files so the repository becomes the source of truth.");
|
|
1256
|
-
}
|
|
1257
|
-
|
|
1258
|
-
function readTargetFile(targetDir, relativePath) {
|
|
1259
|
-
return readFileSync(join(targetDir, relativePath), "utf8");
|
|
1260
|
-
}
|
|
1261
|
-
|
|
1262
|
-
function addFinding(findings, severity, code, message, path, nextStep) {
|
|
1263
|
-
findings.push({
|
|
1264
|
-
severity,
|
|
1265
|
-
code,
|
|
1266
|
-
message,
|
|
1267
|
-
path,
|
|
1268
|
-
next_step: nextStep,
|
|
1269
|
-
});
|
|
1270
|
-
}
|
|
1271
|
-
|
|
1272
|
-
function nonEmptyTableRows(content) {
|
|
1273
|
-
return content
|
|
1274
|
-
.split("\n")
|
|
1275
|
-
.filter((line) => line.trim().startsWith("|"))
|
|
1276
|
-
.filter((line) => !line.includes("---"))
|
|
1277
|
-
.map((line) => line.split("|").map((cell) => cell.trim()).filter(Boolean))
|
|
1278
|
-
.filter((cells) => cells.some((cell) => cell && !cell.startsWith("`")));
|
|
1279
|
-
}
|
|
1280
|
-
|
|
1281
|
-
function tableRows(content) {
|
|
1282
|
-
return content
|
|
1283
|
-
.split("\n")
|
|
1284
|
-
.filter((line) => line.trim().startsWith("|"))
|
|
1285
|
-
.filter((line) => !line.includes("---"))
|
|
1286
|
-
.map((line) => {
|
|
1287
|
-
const cells = line.split("|").map((cell) => cell.trim());
|
|
1288
|
-
if (cells[0] === "") cells.shift();
|
|
1289
|
-
if (cells.at(-1) === "") cells.pop();
|
|
1290
|
-
return cells;
|
|
1291
|
-
});
|
|
1292
|
-
}
|
|
1293
|
-
|
|
1294
|
-
const compactBacklogHeaders = ["priority", "key", "work item", "status", "artefacts", "current spec", "next step"];
|
|
1295
|
-
const legacyBacklogHeaders = ["prio", "key", "title", "status", "ur", "brownfield review", "prd", "sd", "tp", "qa", "or", "current spec", "notes"];
|
|
1296
|
-
const backlogStatusLabels = new Map([
|
|
1297
|
-
["needs ur", "needs_ur"],
|
|
1298
|
-
["awaiting brownfield review", "awaiting_brownfield_review"],
|
|
1299
|
-
["awaiting prd", "awaiting_prd"],
|
|
1300
|
-
["awaiting prd approval", "awaiting_prd_approval"],
|
|
1301
|
-
["awaiting sd", "awaiting_sd"],
|
|
1302
|
-
["awaiting sd approval", "awaiting_sd_approval"],
|
|
1303
|
-
["awaiting tp", "awaiting_tp"],
|
|
1304
|
-
["awaiting tp approval", "awaiting_tp_approval"],
|
|
1305
|
-
["in progress", "in_progress"],
|
|
1306
|
-
["blocked", "blocked"],
|
|
1307
|
-
["awaiting qa", "awaiting_qa"],
|
|
1308
|
-
["awaiting uat", "awaiting_uat"],
|
|
1309
|
-
["completed", "completed"],
|
|
1310
|
-
["superseded", "superseded"],
|
|
1311
|
-
["abandoned", "abandoned"],
|
|
1312
|
-
]);
|
|
1313
|
-
const backlogArtefactLabels = new Map([
|
|
1314
|
-
["ur", "ur"],
|
|
1315
|
-
["brownfield", "brownfield_review"],
|
|
1316
|
-
["prd", "prd"],
|
|
1317
|
-
["sd", "sd"],
|
|
1318
|
-
["tp", "tp"],
|
|
1319
|
-
["qa", "qa"],
|
|
1320
|
-
["or", "or"],
|
|
1321
|
-
]);
|
|
1322
|
-
const backlogScopeLabels = new Map([
|
|
1323
|
-
["framework-maintenance", "framework_maintenance"],
|
|
1324
|
-
["external-delivery", "external_delivery"],
|
|
1325
|
-
]);
|
|
1326
|
-
|
|
1327
|
-
function normalizeBacklogHeader(value) {
|
|
1328
|
-
return value.replace(/`/g, "").trim().toLowerCase();
|
|
1329
|
-
}
|
|
1330
|
-
|
|
1331
|
-
function markdownLink(value) {
|
|
1332
|
-
const match = value.trim().match(/^\[([^\]]+)\]\(([^)]+)\)$/);
|
|
1333
|
-
return match ? { label: match[1].trim(), target: match[2].trim() } : null;
|
|
1334
|
-
}
|
|
1335
|
-
|
|
1336
|
-
function resolvedBacklogLinkTarget(target) {
|
|
1337
|
-
if (!target
|
|
1338
|
-
|| target.startsWith("/")
|
|
1339
|
-
|| target.includes("\\")
|
|
1340
|
-
|| /^[a-z][a-z0-9+.-]*:/i.test(target)) {
|
|
1341
|
-
return null;
|
|
1342
|
-
}
|
|
1343
|
-
const resolved = posix.normalize(posix.join(".agdf/control", target));
|
|
1344
|
-
if (resolved === ".." || resolved.startsWith("../")) return null;
|
|
1345
|
-
return resolved;
|
|
1346
|
-
}
|
|
1347
|
-
|
|
1348
|
-
function normalizedBacklogLinkTarget(value, findings, backlogPath, label) {
|
|
1349
|
-
const link = markdownLink(value);
|
|
1350
|
-
if (!link) return value.trim();
|
|
1351
|
-
const resolved = resolvedBacklogLinkTarget(link.target);
|
|
1352
|
-
if (!resolved) {
|
|
1353
|
-
if (findings) {
|
|
1354
|
-
addFinding(
|
|
1355
|
-
findings,
|
|
1356
|
-
"revise",
|
|
1357
|
-
"AGDF_BACKLOG_LINK_TARGET_INVALID",
|
|
1358
|
-
`Backlog ${label} link must be relative to MASTER_BACKLOG.md: ${link.target || "<empty>"}.`,
|
|
1359
|
-
backlogPath,
|
|
1360
|
-
"Use a document-relative artefact target such as artefacts/<key>/UR.md.",
|
|
1361
|
-
);
|
|
1362
|
-
}
|
|
1363
|
-
return link.target;
|
|
1364
|
-
}
|
|
1365
|
-
return resolved;
|
|
1366
|
-
}
|
|
1367
|
-
|
|
1368
|
-
function normalizeBacklogStatus(value, findings, backlogPath) {
|
|
1369
|
-
const cleaned = cleanStatusCell(value ?? "");
|
|
1370
|
-
const lookup = cleaned.replaceAll("_", " ").toLowerCase();
|
|
1371
|
-
const normalized = backlogStatusLabels.get(lookup);
|
|
1372
|
-
if (normalized) return normalized;
|
|
1373
|
-
if (/^[a-z][a-z0-9_]*$/.test(cleaned)) return cleaned;
|
|
1374
|
-
if (findings && filled(cleaned)) {
|
|
1375
|
-
addFinding(
|
|
1376
|
-
findings,
|
|
1377
|
-
"revise",
|
|
1378
|
-
"AGDF_BACKLOG_STATUS_UNKNOWN",
|
|
1379
|
-
`MASTER_BACKLOG.md uses an unknown human status label: ${cleaned}.`,
|
|
1380
|
-
backlogPath,
|
|
1381
|
-
"Use a documented human status label or a supported legacy snake_case value.",
|
|
1382
|
-
);
|
|
1383
|
-
}
|
|
1384
|
-
return cleaned;
|
|
1385
|
-
}
|
|
1386
|
-
|
|
1387
|
-
function normalizeBacklogScope(workItem, findings, backlogPath) {
|
|
1388
|
-
const match = (workItem ?? "").match(/^\[([^\]]+)\]/);
|
|
1389
|
-
if (!match) return undefined;
|
|
1390
|
-
const cleaned = match[1].trim();
|
|
1391
|
-
const lookup = cleaned.replaceAll("_", " ").toLowerCase().replaceAll(" ", "-");
|
|
1392
|
-
const normalized = backlogScopeLabels.get(lookup);
|
|
1393
|
-
if (normalized) return normalized;
|
|
1394
|
-
if (findings) {
|
|
1395
|
-
addFinding(
|
|
1396
|
-
findings,
|
|
1397
|
-
"revise",
|
|
1398
|
-
"AGDF_BACKLOG_SCOPE_LABEL_UNKNOWN",
|
|
1399
|
-
`MASTER_BACKLOG.md uses an unknown Work item scope tag: [${cleaned}].`,
|
|
1400
|
-
backlogPath,
|
|
1401
|
-
"Use [framework-maintenance] or [external-delivery], or remove the bracketed tag.",
|
|
1402
|
-
);
|
|
1403
|
-
}
|
|
1404
|
-
return cleaned;
|
|
1405
|
-
}
|
|
1406
|
-
|
|
1407
|
-
function emptyBacklogPointer() {
|
|
1408
|
-
return {
|
|
1409
|
-
prio: "",
|
|
1410
|
-
key: "",
|
|
1411
|
-
title: "",
|
|
1412
|
-
scope: "",
|
|
1413
|
-
status: "",
|
|
1414
|
-
ur: "",
|
|
1415
|
-
brownfield_review: "",
|
|
1416
|
-
prd: "",
|
|
1417
|
-
sd: "",
|
|
1418
|
-
tp: "",
|
|
1419
|
-
qa: "",
|
|
1420
|
-
or: "",
|
|
1421
|
-
current_spec: "",
|
|
1422
|
-
notes: "",
|
|
1423
|
-
};
|
|
1424
|
-
}
|
|
1425
|
-
|
|
1426
|
-
function parseCompactArtefacts(value, pointer, findings, backlogPath) {
|
|
1427
|
-
if (!filled(value)) return;
|
|
1428
|
-
const seen = new Set();
|
|
1429
|
-
for (const entry of value.split(/\s+·\s+/)) {
|
|
1430
|
-
const link = markdownLink(entry);
|
|
1431
|
-
if (!link) {
|
|
1432
|
-
if (findings) {
|
|
1433
|
-
addFinding(
|
|
1434
|
-
findings,
|
|
1435
|
-
"revise",
|
|
1436
|
-
"AGDF_BACKLOG_ARTEFACT_LINK_INVALID",
|
|
1437
|
-
`Compact Artefacts entry is not a Markdown link: ${entry}.`,
|
|
1438
|
-
backlogPath,
|
|
1439
|
-
"Use [UR](artefacts/<key>/UR.md) style links separated by a middle dot.",
|
|
1440
|
-
);
|
|
1441
|
-
}
|
|
1442
|
-
continue;
|
|
1443
|
-
}
|
|
1444
|
-
const field = backlogArtefactLabels.get(link.label.toLowerCase());
|
|
1445
|
-
if (!field) {
|
|
1446
|
-
if (findings) {
|
|
1447
|
-
addFinding(
|
|
1448
|
-
findings,
|
|
1449
|
-
"revise",
|
|
1450
|
-
"AGDF_BACKLOG_ARTEFACT_LABEL_UNKNOWN",
|
|
1451
|
-
`Compact Artefacts uses an unknown label: ${link.label}.`,
|
|
1452
|
-
backlogPath,
|
|
1453
|
-
"Use UR, Brownfield, PRD, SD, TP, QA or OR.",
|
|
1454
|
-
);
|
|
1455
|
-
}
|
|
1456
|
-
continue;
|
|
1457
|
-
}
|
|
1458
|
-
if (seen.has(field)) {
|
|
1459
|
-
if (findings) {
|
|
1460
|
-
addFinding(
|
|
1461
|
-
findings,
|
|
1462
|
-
"revise",
|
|
1463
|
-
"AGDF_BACKLOG_ARTEFACT_LABEL_DUPLICATE",
|
|
1464
|
-
`Compact Artefacts repeats the ${link.label} link.`,
|
|
1465
|
-
backlogPath,
|
|
1466
|
-
"Keep exactly one link per artefact type.",
|
|
1467
|
-
);
|
|
1468
|
-
}
|
|
1469
|
-
continue;
|
|
1470
|
-
}
|
|
1471
|
-
seen.add(field);
|
|
1472
|
-
pointer[field] = normalizedBacklogLinkTarget(entry, findings, backlogPath, link.label);
|
|
1473
|
-
}
|
|
1474
|
-
}
|
|
1475
|
-
|
|
1476
|
-
function parseBacklogSection(section, findings = null, backlogPath = ".agdf/control/MASTER_BACKLOG.md") {
|
|
1477
|
-
const rows = tableRows(section);
|
|
1478
|
-
if (rows.length === 0) return [];
|
|
1479
|
-
const headers = rows[0].map(normalizeBacklogHeader);
|
|
1480
|
-
const layout = headers.join("|") === compactBacklogHeaders.join("|")
|
|
1481
|
-
? "compact"
|
|
1482
|
-
: headers.join("|") === legacyBacklogHeaders.join("|")
|
|
1483
|
-
? "legacy"
|
|
1484
|
-
: "unknown";
|
|
1485
|
-
|
|
1486
|
-
if (layout === "unknown") {
|
|
1487
|
-
if (findings) {
|
|
1488
|
-
addFinding(
|
|
1489
|
-
findings,
|
|
1490
|
-
"revise",
|
|
1491
|
-
"AGDF_BACKLOG_LAYOUT_UNKNOWN",
|
|
1492
|
-
`MASTER_BACKLOG.md uses an unsupported table layout: ${headers.join(", ")}.`,
|
|
1493
|
-
backlogPath,
|
|
1494
|
-
"Use the canonical compact layout or the supported legacy 13-column layout.",
|
|
1495
|
-
);
|
|
1496
|
-
}
|
|
1497
|
-
return [];
|
|
1498
|
-
}
|
|
1499
|
-
|
|
1500
|
-
return rows.slice(1)
|
|
1501
|
-
.filter((cells) => cells.some((cell) => filled(cell)))
|
|
1502
|
-
.map((cells) => {
|
|
1503
|
-
const pointer = emptyBacklogPointer();
|
|
1504
|
-
if (layout === "legacy") {
|
|
1505
|
-
[
|
|
1506
|
-
pointer.prio,
|
|
1507
|
-
pointer.key,
|
|
1508
|
-
pointer.title,
|
|
1509
|
-
pointer.status,
|
|
1510
|
-
pointer.ur,
|
|
1511
|
-
pointer.brownfield_review,
|
|
1512
|
-
pointer.prd,
|
|
1513
|
-
pointer.sd,
|
|
1514
|
-
pointer.tp,
|
|
1515
|
-
pointer.qa,
|
|
1516
|
-
pointer.or,
|
|
1517
|
-
pointer.current_spec,
|
|
1518
|
-
pointer.notes,
|
|
1519
|
-
] = cells.map((cell) => cell ?? "");
|
|
1520
|
-
pointer.status = normalizeBacklogStatus(pointer.status, findings, backlogPath);
|
|
1521
|
-
return pointer;
|
|
1522
|
-
}
|
|
1523
|
-
|
|
1524
|
-
pointer.prio = cells[0] ?? "";
|
|
1525
|
-
pointer.key = cleanStatusCell(cells[1] ?? "");
|
|
1526
|
-
pointer.title = cells[2] ?? "";
|
|
1527
|
-
pointer.scope = normalizeBacklogScope(cells[2], findings, backlogPath) ?? "";
|
|
1528
|
-
pointer.status = normalizeBacklogStatus(cells[3], findings, backlogPath);
|
|
1529
|
-
parseCompactArtefacts(cells[4] ?? "", pointer, findings, backlogPath);
|
|
1530
|
-
pointer.current_spec = normalizedBacklogLinkTarget(cells[5] ?? "", findings, backlogPath, "Current spec");
|
|
1531
|
-
pointer.notes = cells[6] ?? "";
|
|
1532
|
-
return pointer;
|
|
1533
|
-
});
|
|
1534
|
-
}
|
|
1535
|
-
|
|
1536
|
-
function hasFilledTableRow(content, firstCellPattern) {
|
|
1537
|
-
return tableRows(content)
|
|
1538
|
-
.filter((cells) => firstCellPattern.test(cells[0] ?? ""))
|
|
1539
|
-
.some((cells) => cells.slice(1).some((cell) => cell && !cell.startsWith("`")));
|
|
1540
|
-
}
|
|
1541
|
-
|
|
1542
|
-
function isPlaceholderValue(value) {
|
|
1543
|
-
return !value || (value.startsWith("`") && value.includes("|"));
|
|
1544
|
-
}
|
|
1545
|
-
|
|
1546
|
-
function hasFilledEvidenceRow(content) {
|
|
1547
|
-
const evidenceSection = content.match(/## Evidence([\s\S]*?)(?:\n## |\n# |$)/)?.[1] ?? "";
|
|
1548
|
-
return tableRows(evidenceSection)
|
|
1549
|
-
.filter((cells) => cells[0] !== "Evidence")
|
|
1550
|
-
.some((cells) => {
|
|
1551
|
-
const [evidence, source, covers] = cells;
|
|
1552
|
-
return Boolean(evidence || source || covers);
|
|
1553
|
-
});
|
|
1554
|
-
}
|
|
1555
|
-
|
|
1556
|
-
function allowNoActiveRuns(targetDir) {
|
|
1557
|
-
const configPath = join(targetDir, ".agdf", "control", "config.json");
|
|
1558
|
-
if (!existsSync(configPath)) return false;
|
|
1559
|
-
try {
|
|
1560
|
-
return JSON.parse(readFileSync(configPath, "utf8")).allow_no_active_runs === true;
|
|
1561
|
-
} catch {
|
|
1562
|
-
return false;
|
|
1563
|
-
}
|
|
1564
|
-
}
|
|
1565
|
-
|
|
1566
|
-
function markdownSection(content, heading) {
|
|
1567
|
-
const escaped = heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1568
|
-
return content.match(new RegExp(`(?:^|\\n)## ${escaped}\\s*\\n([\\s\\S]*?)(?:\\n## |\\n# |$)`))?.[1] ?? "";
|
|
1569
|
-
}
|
|
1570
|
-
|
|
1571
|
-
function filled(value) {
|
|
1572
|
-
return Boolean(value && !isPlaceholderValue(value) && value.trim() !== "");
|
|
1573
|
-
}
|
|
1574
|
-
|
|
1575
|
-
function runSelectionRecovery(target) {
|
|
1576
|
-
const directSelection = "Pass --run <run_id> or set AGDF_RUN_ID to select the intended run";
|
|
1577
|
-
return ["doctor", "delivery-map"].includes(target)
|
|
1578
|
-
? `${directSelection}, or use --all-active to evaluate every active run independently.`
|
|
1579
|
-
: `${directSelection}.`;
|
|
1580
|
-
}
|
|
1581
|
-
|
|
1582
|
-
function parseQualityContracts(content) {
|
|
1583
|
-
const parsed = JSON.parse(content);
|
|
1584
|
-
if (!Array.isArray(parsed.contracts) || parsed.contracts.length === 0) {
|
|
1585
|
-
throw new Error("contracts must be a non-empty array");
|
|
1586
|
-
}
|
|
1587
|
-
for (const contract of parsed.contracts) {
|
|
1588
|
-
if (!contract.code || !contract.impact || !contract.required_evidence) {
|
|
1589
|
-
throw new Error(`contract ${contract.code ?? "<unknown>"} is missing code, impact or required_evidence`);
|
|
1590
|
-
}
|
|
1591
|
-
}
|
|
1592
|
-
return parsed;
|
|
1593
|
-
}
|
|
1594
|
-
|
|
1595
|
-
function evaluateDoctor(targetDir, selection = {}) {
|
|
1596
|
-
if (selection.allActive) {
|
|
1597
|
-
const selected = resolveRuns(targetDir, { allActive: true });
|
|
1598
|
-
const runs = selected.runs.map((run) => ({
|
|
1599
|
-
run_id: run.run_id,
|
|
1600
|
-
report: evaluateDoctor(targetDir, { runId: run.run_id }),
|
|
1601
|
-
}));
|
|
1602
|
-
const status = selected.findings.length
|
|
1603
|
-
? "block"
|
|
1604
|
-
: aggregate(
|
|
1605
|
-
runs.map((item) => ({ run_id: item.run_id, status: item.report.status })),
|
|
1606
|
-
{ allowNoActiveRuns: allowNoActiveRuns(targetDir) },
|
|
1607
|
-
).status;
|
|
1608
|
-
const findings = [
|
|
1609
|
-
...selected.findings.map((finding) => ({
|
|
1610
|
-
...finding,
|
|
1611
|
-
severity: "block",
|
|
1612
|
-
message: "Invalid canonical run entry.",
|
|
1613
|
-
next_step: "Repair or remove the invalid run entry.",
|
|
1614
|
-
})),
|
|
1615
|
-
...runs.flatMap((item) => item.report.findings.map((finding) => ({ ...finding, run_id: item.run_id }))),
|
|
1616
|
-
];
|
|
1617
|
-
return {
|
|
1618
|
-
schema_version: "1",
|
|
1619
|
-
status,
|
|
1620
|
-
checked_at: new Date().toISOString(),
|
|
1621
|
-
target_dir: targetDir,
|
|
1622
|
-
summary: {
|
|
1623
|
-
findings: findings.length,
|
|
1624
|
-
block: findings.filter((finding) => finding.severity === "block").length,
|
|
1625
|
-
revise: findings.filter((finding) => finding.severity === "revise").length,
|
|
1626
|
-
warn: findings.filter((finding) => finding.severity === "warn").length,
|
|
1627
|
-
},
|
|
1628
|
-
findings,
|
|
1629
|
-
runs,
|
|
1630
|
-
};
|
|
1631
|
-
}
|
|
1632
|
-
const findings = [];
|
|
1633
|
-
const hasCanonicalRuns = existsSync(join(targetDir, ".agdf", "control", "runs"));
|
|
1634
|
-
if (hasCanonicalRuns) {
|
|
1635
|
-
const projection = verifyLegacyProjection(targetDir);
|
|
1636
|
-
if (!["absent", "valid"].includes(projection.status)) {
|
|
1637
|
-
addFinding(
|
|
1638
|
-
findings,
|
|
1639
|
-
"block",
|
|
1640
|
-
"AGDF_LEGACY_PROJECTION_DRIFT",
|
|
1641
|
-
`Legacy compatibility state is not a valid projection: ${projection.status}.`,
|
|
1642
|
-
join(".agdf", "control", "AGDF_RUN.md"),
|
|
1643
|
-
"Regenerate the explicit legacy projection or remove it after legacy consumers are retired.",
|
|
1644
|
-
);
|
|
1645
|
-
}
|
|
1646
|
-
}
|
|
1647
|
-
const missing = doctorRequiredFiles.filter(
|
|
1648
|
-
(relativePath) => !(relativePath.endsWith("AGDF_RUN.md") && hasCanonicalRuns) && !existsSync(join(targetDir, relativePath)),
|
|
1649
|
-
);
|
|
1650
|
-
|
|
1651
|
-
for (const relativePath of missing) {
|
|
1652
|
-
const templatePath = join(dirname(relativePath), "templates", relativePath.split("/").at(-1));
|
|
1653
|
-
const hasTemplate = existsSync(join(targetDir, templatePath));
|
|
1654
|
-
addFinding(
|
|
1655
|
-
findings,
|
|
1656
|
-
"block",
|
|
1657
|
-
"AGDF_CONTROL_FILE_MISSING",
|
|
1658
|
-
hasTemplate
|
|
1659
|
-
? "Live control file is missing; only the template exists."
|
|
1660
|
-
: "Required live control file is missing.",
|
|
1661
|
-
relativePath,
|
|
1662
|
-
"Run npx --yes @agdf/cli@latest init only when the repository should own durable AGDF control state or deterministic setup is explicitly needed.",
|
|
1663
|
-
);
|
|
1664
|
-
}
|
|
1665
|
-
|
|
1666
|
-
if (missing.length === 0) {
|
|
1667
|
-
const selectedRunState = readRunState(targetDir, selection);
|
|
1668
|
-
const runPath = selectedRunState.path;
|
|
1669
|
-
const run = selectedRunState.content;
|
|
1670
|
-
|
|
1671
|
-
if (selectedRunState.resolution_error) {
|
|
1672
|
-
addFinding(
|
|
1673
|
-
findings,
|
|
1674
|
-
"block",
|
|
1675
|
-
selectedRunState.resolution_error.split(":")[0] || "AGDF_ACTIVE_RUN_UNRESOLVED",
|
|
1676
|
-
`Run selection failed: ${selectedRunState.resolution_error}`,
|
|
1677
|
-
runPath,
|
|
1678
|
-
runSelectionRecovery(selection.target),
|
|
1679
|
-
);
|
|
1680
|
-
} else {
|
|
1681
|
-
const currentGateLine = run.match(/^- current_gate:[^\S\r\n]*(.*)$/m)?.[1]?.trim() ?? "";
|
|
1682
|
-
const nextActionLine = run.match(/^- next_allowed_action:[^\S\r\n]*(.*)$/m)?.[1]?.trim() ?? "";
|
|
1683
|
-
const hasEvidence = hasFilledEvidenceRow(run);
|
|
1684
|
-
|
|
1685
|
-
if (isPlaceholderValue(currentGateLine)) {
|
|
1686
|
-
addFinding(
|
|
1687
|
-
findings,
|
|
1688
|
-
"revise",
|
|
1689
|
-
"AGDF_CURRENT_GATE_MISSING",
|
|
1690
|
-
"The selected run state does not name the current gate.",
|
|
1691
|
-
runPath,
|
|
1692
|
-
"Set current_gate to the current delivery gate or none.",
|
|
1693
|
-
);
|
|
1694
|
-
}
|
|
1695
|
-
|
|
1696
|
-
if (isPlaceholderValue(nextActionLine)) {
|
|
1697
|
-
addFinding(
|
|
1698
|
-
findings,
|
|
1699
|
-
"revise",
|
|
1700
|
-
"AGDF_NEXT_ALLOWED_ACTION_MISSING",
|
|
1701
|
-
"The selected run state does not state the next allowed action.",
|
|
1702
|
-
runPath,
|
|
1703
|
-
"Fill the next allowed action before asking an agent to continue delivery work.",
|
|
1704
|
-
);
|
|
1705
|
-
}
|
|
1706
|
-
|
|
1707
|
-
if (!hasEvidence) {
|
|
1708
|
-
addFinding(
|
|
1709
|
-
findings,
|
|
1710
|
-
"warn",
|
|
1711
|
-
"AGDF_EVIDENCE_EMPTY",
|
|
1712
|
-
"The selected run state has no visible evidence row yet.",
|
|
1713
|
-
runPath,
|
|
1714
|
-
"Add at least one evidence row or explicitly document that no evidence exists yet.",
|
|
1715
|
-
);
|
|
1716
|
-
}
|
|
1717
|
-
}
|
|
1718
|
-
|
|
1719
|
-
const backlogPath = join(".agdf", "control", "MASTER_BACKLOG.md");
|
|
1720
|
-
const backlog = readTargetFile(targetDir, backlogPath);
|
|
1721
|
-
parseBacklogSection(markdownSection(backlog, "Active Backlog"), findings, backlogPath);
|
|
1722
|
-
parseBacklogSection(markdownSection(backlog, "Planned / Parking Lot"), findings, backlogPath);
|
|
1723
|
-
const completedRows = tableRows(markdownSection(backlog, "Completed / Superseded Pointers"))
|
|
1724
|
-
.slice(1)
|
|
1725
|
-
.filter((cells) => cells.some((cell) => filled(cell)));
|
|
1726
|
-
if (!hasFilledTableRow(backlog, /^P[0-9]/) && completedRows.length === 0) {
|
|
1727
|
-
addFinding(
|
|
1728
|
-
findings,
|
|
1729
|
-
"warn",
|
|
1730
|
-
"AGDF_BACKLOG_POINTER_EMPTY",
|
|
1731
|
-
"MASTER_BACKLOG.md does not contain an active or planned work pointer.",
|
|
1732
|
-
backlogPath,
|
|
1733
|
-
"Add the active item or document that no governed delivery item is active.",
|
|
1734
|
-
);
|
|
1735
|
-
}
|
|
1736
|
-
|
|
1737
|
-
const sotPath = join(".agdf", "control", "SOT_REGISTRY.md");
|
|
1738
|
-
const sot = readTargetFile(targetDir, sotPath);
|
|
1739
|
-
if (!hasFilledTableRow(sot, /Product intent|Architecture|Runtime contracts|UX \/ user flows|Operations \/ release/)) {
|
|
1740
|
-
addFinding(
|
|
1741
|
-
findings,
|
|
1742
|
-
"warn",
|
|
1743
|
-
"AGDF_SOT_REGISTRY_EMPTY",
|
|
1744
|
-
"SOT_REGISTRY.md has no filled primary source-of-truth row.",
|
|
1745
|
-
sotPath,
|
|
1746
|
-
"Assign one primary source of truth for at least the domains relevant to the next run.",
|
|
1747
|
-
);
|
|
1748
|
-
}
|
|
1749
|
-
|
|
1750
|
-
const activeDomains = new Map();
|
|
1751
|
-
for (const cells of nonEmptyTableRows(sot)) {
|
|
1752
|
-
const [domain, document, status] = cells;
|
|
1753
|
-
if (!domain || !document || status !== "active") continue;
|
|
1754
|
-
activeDomains.set(domain, (activeDomains.get(domain) ?? 0) + 1);
|
|
1755
|
-
}
|
|
1756
|
-
for (const [domain, count] of activeDomains.entries()) {
|
|
1757
|
-
if (count > 1) {
|
|
1758
|
-
addFinding(
|
|
1759
|
-
findings,
|
|
1760
|
-
"block",
|
|
1761
|
-
"AGDF_PARALLEL_SOT",
|
|
1762
|
-
`Domain has ${count} active source-of-truth rows: ${domain}.`,
|
|
1763
|
-
sotPath,
|
|
1764
|
-
"Keep exactly one active primary source of truth for this domain.",
|
|
1765
|
-
);
|
|
1766
|
-
}
|
|
1767
|
-
}
|
|
1768
|
-
|
|
1769
|
-
const contextPath = join(".agdf", "control", "CONTEXT_GRAPH.md");
|
|
1770
|
-
const contextGraph = readTargetFile(targetDir, contextPath);
|
|
1771
|
-
if (contextGraph.includes("### CG-001 Example")) {
|
|
1772
|
-
addFinding(
|
|
1773
|
-
findings,
|
|
1774
|
-
"warn",
|
|
1775
|
-
"AGDF_CONTEXT_GRAPH_TEMPLATE_NODE",
|
|
1776
|
-
"CONTEXT_GRAPH.md still contains the example node.",
|
|
1777
|
-
contextPath,
|
|
1778
|
-
"Remove the example or replace it with an evidenced node that has an exit criterion.",
|
|
1779
|
-
);
|
|
1780
|
-
}
|
|
1781
|
-
|
|
1782
|
-
const contractsPath = join(".agdf", "control", "AGENT_QUALITY_CONTRACTS.json");
|
|
1783
|
-
try {
|
|
1784
|
-
parseQualityContracts(readTargetFile(targetDir, contractsPath));
|
|
1785
|
-
} catch (error) {
|
|
1786
|
-
addFinding(
|
|
1787
|
-
findings,
|
|
1788
|
-
"block",
|
|
1789
|
-
"AGDF_QUALITY_CONTRACTS_INVALID",
|
|
1790
|
-
`AGENT_QUALITY_CONTRACTS.json is invalid: ${error.message}`,
|
|
1791
|
-
contractsPath,
|
|
1792
|
-
"Restore the generated contracts or fix the JSON contract schema.",
|
|
1793
|
-
);
|
|
1794
|
-
}
|
|
1795
|
-
|
|
1796
|
-
const runState = selectedRunState;
|
|
1797
|
-
if (modeSliceDecision(runState) === "verified_change") {
|
|
1798
|
-
const verifiedChange = evaluateVerifiedChange(targetDir, runState);
|
|
1799
|
-
for (const finding of verifiedChange.findings) {
|
|
1800
|
-
addFinding(findings, finding.severity, finding.code, finding.message, finding.path, finding.next_step);
|
|
1801
|
-
}
|
|
1802
|
-
}
|
|
1803
|
-
for (const finding of analyzeDurableGateArtefactConsistency(runState)) {
|
|
1804
|
-
addFinding(findings, finding.severity, finding.code, finding.message, finding.path, finding.next_step);
|
|
1805
|
-
}
|
|
1806
|
-
for (const finding of analyzeArtefactRoleConsistency(targetDir, runState)) {
|
|
1807
|
-
addFinding(findings, finding.severity, finding.code, finding.message, finding.path, finding.next_step);
|
|
1808
|
-
}
|
|
1809
|
-
for (const finding of analyzeDeliveryMap(runState).findings) {
|
|
1810
|
-
addFinding(findings, finding.severity, finding.code, finding.message, finding.path, finding.next_step);
|
|
1811
|
-
}
|
|
1812
|
-
}
|
|
1813
|
-
|
|
1814
|
-
const severityRank = { block: 3, revise: 2, warn: 1 };
|
|
1815
|
-
const maxSeverity = findings.reduce((max, finding) => Math.max(max, severityRank[finding.severity] ?? 0), 0);
|
|
1816
|
-
const status = maxSeverity >= 3 ? "block" : maxSeverity === 2 ? "revise" : maxSeverity === 1 ? "warn" : "pass";
|
|
1817
|
-
|
|
1818
|
-
return {
|
|
1819
|
-
schema_version: "1",
|
|
1820
|
-
status,
|
|
1821
|
-
checked_at: new Date().toISOString(),
|
|
1822
|
-
target_dir: targetDir,
|
|
1823
|
-
summary: {
|
|
1824
|
-
findings: findings.length,
|
|
1825
|
-
block: findings.filter((finding) => finding.severity === "block").length,
|
|
1826
|
-
revise: findings.filter((finding) => finding.severity === "revise").length,
|
|
1827
|
-
warn: findings.filter((finding) => finding.severity === "warn").length,
|
|
1828
|
-
},
|
|
1829
|
-
findings,
|
|
1830
|
-
};
|
|
1831
|
-
}
|
|
1832
|
-
|
|
1833
|
-
function printDoctorReport(report, json) {
|
|
1834
|
-
if (json) {
|
|
1835
|
-
console.log(JSON.stringify(report, null, 2));
|
|
1836
|
-
return;
|
|
1837
|
-
}
|
|
1838
|
-
|
|
1839
|
-
console.log(`AGDF doctor: ${report.status}`);
|
|
1840
|
-
console.log(`Checked: ${report.target_dir}`);
|
|
1841
|
-
console.log(`Findings: ${report.summary.findings} (${report.summary.block} block, ${report.summary.revise} revise, ${report.summary.warn} warn)`);
|
|
1842
|
-
|
|
1843
|
-
for (const finding of report.findings) {
|
|
1844
|
-
console.log("");
|
|
1845
|
-
console.log(`[${finding.severity}] ${finding.code}`);
|
|
1846
|
-
console.log(`Path: ${finding.path}`);
|
|
1847
|
-
console.log(finding.message);
|
|
1848
|
-
console.log(`Next step: ${finding.next_step}`);
|
|
1849
|
-
}
|
|
1850
|
-
}
|
|
1851
|
-
|
|
1852
|
-
function extractField(content, field) {
|
|
1853
|
-
const pattern = new RegExp(`^- ${field}:[^\\S\\r\\n]*(.*)$`, "m");
|
|
1854
|
-
return content.match(pattern)?.[1]?.trim() ?? "";
|
|
1855
|
-
}
|
|
1856
|
-
|
|
1857
|
-
const verifiedChangeStatuses = new Set(["draft", "eligible", "executed", "escalated"]);
|
|
1858
|
-
const verifiedChangeEscalationTargets = new Set(["structured_slice", "structured_delivery"]);
|
|
1859
|
-
|
|
1860
|
-
function parseVerifiedChangePathList(value) {
|
|
1861
|
-
const cleaned = cleanStatusCell(value ?? "");
|
|
1862
|
-
if (!cleaned || cleaned === "none") return [];
|
|
1863
|
-
return cleaned.split(",").map((item) => item.trim()).filter(Boolean);
|
|
1864
|
-
}
|
|
1865
|
-
|
|
1866
|
-
function isSafeRepoRelativePath(value) {
|
|
1867
|
-
if (!value || value === "none" || value.startsWith("/") || value.includes("\\")) return false;
|
|
1868
|
-
const normalized = posix.normalize(value);
|
|
1869
|
-
return normalized !== "." && normalized !== ".." && !normalized.startsWith("../") && normalized === value;
|
|
1870
|
-
}
|
|
1871
|
-
|
|
1872
|
-
function gitPathList(targetDir, args) {
|
|
1873
|
-
try {
|
|
1874
|
-
return execFileSync("git", args, { cwd: targetDir, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] })
|
|
1875
|
-
.split("\n")
|
|
1876
|
-
.map((item) => item.trim())
|
|
1877
|
-
.filter(Boolean);
|
|
1878
|
-
} catch {
|
|
1879
|
-
return null;
|
|
1880
|
-
}
|
|
1881
|
-
}
|
|
1882
|
-
|
|
1883
|
-
function gitValue(targetDir, args) {
|
|
1884
|
-
try {
|
|
1885
|
-
return execFileSync("git", args, { cwd: targetDir, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
1886
|
-
} catch {
|
|
1887
|
-
return null;
|
|
1888
|
-
}
|
|
1889
|
-
}
|
|
1890
|
-
|
|
1891
|
-
function readVerifiedChangeRecord(targetDir, runState) {
|
|
1892
|
-
const artefact = runState.artefacts.get("Verified Change");
|
|
1893
|
-
if (!artefact?.path || isPlaceholderValue(artefact.path)) return { status: "missing", path: "", content: "" };
|
|
1894
|
-
if (!isSafeRepoRelativePath(artefact.path)) return { status: "invalid", path: artefact.path, content: "", error: "record_path_invalid" };
|
|
1895
|
-
const absolutePath = resolve(targetDir, artefact.path);
|
|
1896
|
-
const relativePath = relative(resolve(targetDir), absolutePath);
|
|
1897
|
-
if (isAbsolute(relativePath) || relativePath === ".." || relativePath.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) || !existsSync(absolutePath)) {
|
|
1898
|
-
return { status: "missing", path: artefact.path, content: "", error: "record_missing" };
|
|
1899
|
-
}
|
|
1900
|
-
return { status: "present", path: artefact.path, content: readFileSync(absolutePath, "utf8") };
|
|
1901
|
-
}
|
|
1902
|
-
|
|
1903
|
-
function evaluateVerifiedChange(targetDir, runState) {
|
|
1904
|
-
const record = readVerifiedChangeRecord(targetDir, runState);
|
|
1905
|
-
const findings = [];
|
|
1906
|
-
const add = (code, message, nextStep, severity = "revise") => findings.push({
|
|
1907
|
-
severity,
|
|
1908
|
-
code,
|
|
1909
|
-
message,
|
|
1910
|
-
path: record.path || runState.path,
|
|
1911
|
-
next_step: nextStep,
|
|
1912
|
-
});
|
|
1913
|
-
|
|
1914
|
-
if (record.status === "missing") {
|
|
1915
|
-
return { status: "missing", record, findings };
|
|
1916
|
-
}
|
|
1917
|
-
if (record.status === "invalid") {
|
|
1918
|
-
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");
|
|
1919
|
-
return { status: "invalid", record, findings };
|
|
1920
|
-
}
|
|
1921
|
-
|
|
1922
|
-
const status = cleanStatusCell(extractField(record.content, "status"));
|
|
1923
|
-
const relatedUr = cleanStatusCell(extractField(record.content, "related_ur"));
|
|
1924
|
-
const escalationTarget = cleanStatusCell(extractField(record.content, "escalation_target"));
|
|
1925
|
-
const canonicalOwner = cleanStatusCell(extractField(record.content, "canonical_owner"));
|
|
1926
|
-
const sourcePaths = parseVerifiedChangePathList(extractField(record.content, "allowed_source_paths"));
|
|
1927
|
-
const derivedPaths = parseVerifiedChangePathList(extractField(record.content, "allowed_derived_paths"));
|
|
1928
|
-
const prohibitedImpacts = cleanStatusCell(extractField(record.content, "prohibited_impacts"));
|
|
1929
|
-
const propagationCommand = extractField(record.content, "propagation_command");
|
|
1930
|
-
const validationCommands = extractField(record.content, "validation_commands");
|
|
1931
|
-
const baselineTracked = parseVerifiedChangePathList(extractField(record.content, "baseline_tracked_paths"));
|
|
1932
|
-
const baselineUntracked = parseVerifiedChangePathList(extractField(record.content, "baseline_untracked_paths"));
|
|
1933
|
-
const baselineCommit = cleanStatusCell(extractField(record.content, "baseline_commit"));
|
|
1934
|
-
const executionChangedPaths = parseVerifiedChangePathList(extractField(record.content, "execution_changed_paths"));
|
|
1935
|
-
const executionScopeStatus = cleanStatusCell(extractField(record.content, "execution_scope_status"));
|
|
1936
|
-
const validationStatus = cleanStatusCell(extractField(record.content, "validation_status"));
|
|
1937
|
-
const propagationStatus = cleanStatusCell(extractField(record.content, "propagation_status"));
|
|
1938
|
-
const urArtefact = runState.artefacts.get("UR");
|
|
1939
|
-
const lifecycle = extractField(runState.content, "lifecycle") || "active";
|
|
1940
|
-
const runId = extractField(runState.content, "run_id") || "";
|
|
1941
|
-
|
|
1942
|
-
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.");
|
|
1943
|
-
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.");
|
|
1944
|
-
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.");
|
|
1945
|
-
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.");
|
|
1946
|
-
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.");
|
|
1947
|
-
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.");
|
|
1948
|
-
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.");
|
|
1949
|
-
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.");
|
|
1950
|
-
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.");
|
|
1951
|
-
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.");
|
|
1952
|
-
if (["eligible", "executed"].includes(status) && !/^[0-9a-f]{40,64}$/i.test(baselineCommit)) add("AGDF_VERIFIED_CHANGE_BASELINE_COMMIT_INVALID", "Eligible or executed Verified Change requires a full Git baseline_commit.", "Capture the current full Git commit id before eligibility or escalate.");
|
|
1953
|
-
|
|
1954
|
-
if (["eligible", "executed"].includes(status) && lifecycle !== "completed" && /^[0-9a-f]{40,64}$/i.test(baselineCommit)) {
|
|
1955
|
-
const currentHead = gitValue(targetDir, ["rev-parse", "HEAD"]);
|
|
1956
|
-
if (!currentHead) {
|
|
1957
|
-
add("AGDF_VERIFIED_CHANGE_BASELINE_COMMIT_UNAVAILABLE", "Active Verified Change requires a readable current Git HEAD for baseline identity validation.", "Restore Git repository access or use the structured path.", "block");
|
|
1958
|
-
} else if (baselineCommit !== currentHead) {
|
|
1959
|
-
add("AGDF_VERIFIED_CHANGE_BASELINE_COMMIT_MISMATCH", "Active Verified Change baseline_commit does not match the current Git HEAD.", "Recapture the baseline before eligibility or escalate; do not reuse a stale or fabricated baseline identity.", "block");
|
|
1960
|
-
}
|
|
1961
|
-
}
|
|
1962
|
-
|
|
1963
|
-
const allowedPaths = new Set([...sourcePaths, ...derivedPaths]);
|
|
1964
|
-
const baselinePaths = new Set([...baselineTracked, ...baselineUntracked]);
|
|
1965
|
-
const dirtyCandidate = [...allowedPaths].find((path) => baselinePaths.has(path));
|
|
1966
|
-
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");
|
|
1967
|
-
|
|
1968
|
-
const permittedControlPaths = new Set([record.path, runState.path, ".agdf/control/MASTER_BACKLOG.md"]);
|
|
1969
|
-
const runArtefactPrefix = `.agdf/control/artefacts/${runId}/`;
|
|
1970
|
-
for (const artefact of runState.artefacts.values()) {
|
|
1971
|
-
if (artefact.path_format === "invalid") continue;
|
|
1972
|
-
if (isSafeRepoRelativePath(artefact.path) && artefact.path.startsWith(runArtefactPrefix)) permittedControlPaths.add(artefact.path);
|
|
1973
|
-
}
|
|
1974
|
-
|
|
1975
|
-
if (!(lifecycle === "completed" && status === "executed")) {
|
|
1976
|
-
const currentTracked = gitPathList(targetDir, ["diff", "HEAD", "--name-only"]);
|
|
1977
|
-
const currentUntracked = gitPathList(targetDir, ["ls-files", "--others", "--exclude-standard"]);
|
|
1978
|
-
if (currentTracked === null || currentUntracked === null) {
|
|
1979
|
-
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");
|
|
1980
|
-
} else {
|
|
1981
|
-
const introduced = [...new Set([...currentTracked, ...currentUntracked])].filter((path) => !baselinePaths.has(path)).sort();
|
|
1982
|
-
const unexpected = introduced.filter((path) => !allowedPaths.has(path) && !permittedControlPaths.has(path));
|
|
1983
|
-
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");
|
|
1984
|
-
if (status === "executed" && JSON.stringify(introduced) !== JSON.stringify([...executionChangedPaths].sort())) {
|
|
1985
|
-
add("AGDF_VERIFIED_CHANGE_EXECUTION_SCOPE_MISMATCH", "Executed Verified Change execution_changed_paths must equal the post-baseline changed-path set.", "Record the exact changed paths or escalate.", "block");
|
|
1986
|
-
}
|
|
1987
|
-
}
|
|
1988
|
-
} else {
|
|
1989
|
-
const unsafeExecutionPath = executionChangedPaths.find((path) => !isSafeRepoRelativePath(path) || (!allowedPaths.has(path) && !permittedControlPaths.has(path)));
|
|
1990
|
-
if (unsafeExecutionPath) add("AGDF_VERIFIED_CHANGE_EXECUTION_SCOPE_INVALID", `Recorded execution path is outside the eligible scope: ${unsafeExecutionPath}.`, "Correct the historical execution snapshot or escalate.", "block");
|
|
1991
|
-
}
|
|
1992
|
-
|
|
1993
|
-
if (status === "executed") {
|
|
1994
|
-
if (executionChangedPaths.length === 0 || !executionChangedPaths.some((path) => allowedPaths.has(path)) || executionScopeStatus !== "pass") add("AGDF_VERIFIED_CHANGE_EXECUTION_EVIDENCE_MISSING", "Executed Verified Change requires a non-empty execution_changed_paths set containing a declared source/derived path and execution_scope_status: pass.", "Record complete execution-scope evidence or escalate.");
|
|
1995
|
-
if (validationStatus !== "pass") add("AGDF_VERIFIED_CHANGE_VALIDATION_EVIDENCE_MISSING", "Executed Verified Change requires validation_status: pass.", "Record passing deterministic validation evidence or escalate.");
|
|
1996
|
-
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.");
|
|
1997
|
-
}
|
|
1998
|
-
|
|
1999
|
-
if (status === "escalated") return { status: "escalated", record, escalation_target: escalationTarget, findings };
|
|
2000
|
-
if (findings.length > 0) return { status: "invalid", record, escalation_target: escalationTarget, findings };
|
|
2001
|
-
return { status, record, escalation_target: escalationTarget, findings };
|
|
2002
|
-
}
|
|
2003
|
-
|
|
2004
|
-
function cleanStatusCell(value) {
|
|
2005
|
-
return value.replace(/^`|`$/g, "").trim();
|
|
2006
|
-
}
|
|
2007
|
-
|
|
2008
|
-
function readRunState(targetDir, selection = {}) {
|
|
2009
|
-
let runPath = join(".agdf", "control", "AGDF_RUN.md");
|
|
2010
|
-
const canonicalRoot = join(targetDir, ".agdf", "control", "runs");
|
|
2011
|
-
let resolutionError;
|
|
2012
|
-
let candidateRuns = [];
|
|
2013
|
-
if (existsSync(canonicalRoot)) {
|
|
2014
|
-
try {
|
|
2015
|
-
const selected = resolveRuns(targetDir, {
|
|
2016
|
-
runIdArg: selection.runId,
|
|
2017
|
-
runIdEnv: process.env.AGDF_RUN_ID,
|
|
2018
|
-
});
|
|
2019
|
-
runPath = selected.run.path.startsWith(targetDir)
|
|
2020
|
-
? selected.run.path.slice(targetDir.length + 1)
|
|
2021
|
-
: selected.run.path;
|
|
2022
|
-
} catch (error) {
|
|
2023
|
-
resolutionError = error.message;
|
|
2024
|
-
if (resolutionError.startsWith("AGDF_ACTIVE_RUN_AMBIGUOUS")) {
|
|
2025
|
-
const active = resolveRuns(targetDir, { allActive: true }).runs.map((run) => {
|
|
2026
|
-
const controlState = parseControlState(run.content, {
|
|
2027
|
-
userGates: userGateOrder,
|
|
2028
|
-
internalSteps: [...internalStepArtefacts],
|
|
2029
|
-
closeoutArtefacts: [...closeoutArtefacts],
|
|
2030
|
-
});
|
|
2031
|
-
return {
|
|
2032
|
-
...run,
|
|
2033
|
-
control_state: controlState,
|
|
2034
|
-
current_artefact_heading: readArtefactHeading(targetDir, controlState.artefacts.get(controlState.current_gate)),
|
|
2035
|
-
ur_heading: readArtefactHeading(targetDir, controlState.artefacts.get("UR")),
|
|
2036
|
-
};
|
|
2037
|
-
});
|
|
2038
|
-
candidateRuns = buildRunCandidates(active);
|
|
2039
|
-
}
|
|
2040
|
-
}
|
|
2041
|
-
}
|
|
2042
|
-
if (resolutionError || !existsSync(join(targetDir, runPath))) {
|
|
2043
|
-
return {
|
|
2044
|
-
path: runPath,
|
|
2045
|
-
content: "",
|
|
2046
|
-
current_gate: "",
|
|
2047
|
-
next_allowed_action: "",
|
|
2048
|
-
approvals: new Map(),
|
|
2049
|
-
artefacts: new Map(),
|
|
2050
|
-
evidence_refs: [],
|
|
2051
|
-
artefact_chain: [],
|
|
2052
|
-
mode_slice_decision: {},
|
|
2053
|
-
missing_evidence: [],
|
|
2054
|
-
risks: [],
|
|
2055
|
-
context_graph: {},
|
|
2056
|
-
quality_outlook: "",
|
|
2057
|
-
source_scope: {},
|
|
2058
|
-
memory: {},
|
|
2059
|
-
candidate_runs: candidateRuns,
|
|
2060
|
-
resolution_error: resolutionError,
|
|
2061
|
-
};
|
|
2062
|
-
}
|
|
2063
|
-
|
|
2064
|
-
const content = readTargetFile(targetDir, runPath);
|
|
2065
|
-
const parsed = parseControlState(content, {
|
|
2066
|
-
userGates: userGateOrder,
|
|
2067
|
-
internalSteps: [...internalStepArtefacts],
|
|
2068
|
-
closeoutArtefacts: [...closeoutArtefacts],
|
|
2069
|
-
});
|
|
2070
|
-
|
|
2071
|
-
return {
|
|
2072
|
-
path: runPath,
|
|
2073
|
-
content,
|
|
2074
|
-
...parsed,
|
|
2075
|
-
};
|
|
2076
|
-
}
|
|
2077
|
-
|
|
2078
|
-
function gateApprovalStatus(runState, gate) {
|
|
2079
|
-
if (!userGateOrder.includes(gate)) return "not_applicable";
|
|
2080
|
-
return runState.approvals.get(gate)?.status ?? "";
|
|
2081
|
-
}
|
|
2082
|
-
|
|
2083
|
-
function gateArtefactStatus(runState, gate) {
|
|
2084
|
-
if (!userGateOrder.includes(gate)) return { status: "not_applicable", path: "" };
|
|
2085
|
-
return runState.artefacts.get(gate) ?? { status: "", path: "" };
|
|
2086
|
-
}
|
|
2087
|
-
|
|
2088
|
-
function isDurableGateArtefactSatisfied(runState, gate) {
|
|
2089
|
-
if (!durableGateArtefacts.has(gate)) return true;
|
|
2090
|
-
const artefact = gateArtefactStatus(runState, gate);
|
|
2091
|
-
if (!artefact.path || isPlaceholderValue(artefact.path)) return false;
|
|
2092
|
-
if (gate === "QA") return artefact.status === "pass" || artefact.status === "passed";
|
|
2093
|
-
return artefact.status === "approved";
|
|
2094
|
-
}
|
|
2095
|
-
|
|
2096
|
-
function expectedDurableArtefactStatuses(gate) {
|
|
2097
|
-
return gate === "QA" ? ["pass", "passed"] : ["approved"];
|
|
2098
|
-
}
|
|
2099
|
-
|
|
2100
|
-
function describeDurableArtefactStatuses(gate) {
|
|
2101
|
-
return expectedDurableArtefactStatuses(gate).map((status) => `\`${status}\``).join(" or ");
|
|
2102
|
-
}
|
|
2103
|
-
|
|
2104
|
-
function analyzeDurableGateArtefactConsistency(runState) {
|
|
2105
|
-
const findings = [];
|
|
2106
|
-
|
|
2107
|
-
for (const gate of durableGateArtefacts) {
|
|
2108
|
-
if (gateApprovalStatus(runState, gate) !== "approved") continue;
|
|
2109
|
-
|
|
2110
|
-
const artefact = gateArtefactStatus(runState, gate);
|
|
2111
|
-
if (!artefact.path || isPlaceholderValue(artefact.path) || !artefact.status) continue;
|
|
2112
|
-
|
|
2113
|
-
const expectedStatuses = expectedDurableArtefactStatuses(gate);
|
|
2114
|
-
if (expectedStatuses.includes(artefact.status)) continue;
|
|
2115
|
-
|
|
2116
|
-
findings.push({
|
|
2117
|
-
severity: "revise",
|
|
2118
|
-
code: "AGDF_GATE_ARTEFACT_STATUS_INCONSISTENT",
|
|
2119
|
-
message: `${gate} approval is recorded, but the durable artefact row uses status \`${artefact.status}\`; expected ${describeDurableArtefactStatuses(gate)}.`,
|
|
2120
|
-
path: runState.path,
|
|
2121
|
-
next_step: `Update the ${gate} artefact row in the selected RUN_STATE.md to use the gate-specific durable status vocabulary.`,
|
|
2122
|
-
});
|
|
2123
|
-
}
|
|
2124
|
-
|
|
2125
|
-
return findings;
|
|
2126
|
-
}
|
|
2127
|
-
|
|
2128
|
-
function analyzeArtefactRoleConsistency(targetDir, runState) {
|
|
2129
|
-
const findings = [];
|
|
2130
|
-
for (const [type, artefact] of runState.artefacts) {
|
|
2131
|
-
if (artefact.path_format === "invalid") {
|
|
2132
|
-
findings.push({
|
|
2133
|
-
severity: "block",
|
|
2134
|
-
code: "AGDF_ARTEFACT_PATH_FORMAT_INVALID",
|
|
2135
|
-
message: `${type} uses an invalid artefact path cell (${artefact.path_reason}).`,
|
|
2136
|
-
path: runState.path,
|
|
2137
|
-
next_step: "Use a plain repository-relative path or one complete Markdown code span.",
|
|
2138
|
-
});
|
|
2139
|
-
} else if (artefact.path && !isSafeRepoRelativePath(artefact.path)) {
|
|
2140
|
-
findings.push({
|
|
2141
|
-
severity: "block",
|
|
2142
|
-
code: "AGDF_ARTEFACT_PATH_INVALID",
|
|
2143
|
-
message: `${type} must use a normalized repository-relative artefact path.`,
|
|
2144
|
-
path: runState.path,
|
|
2145
|
-
next_step: "Remove absolute, traversal, unsupported-backslash or otherwise non-normalized path text.",
|
|
2146
|
-
});
|
|
2147
|
-
}
|
|
2148
|
-
}
|
|
2149
|
-
const rolesByPath = new Map();
|
|
2150
|
-
for (const [type, artefact] of runState.artefacts) {
|
|
2151
|
-
if (!artefact.path) continue;
|
|
2152
|
-
rolesByPath.set(artefact.path, [...(rolesByPath.get(artefact.path) ?? []), type]);
|
|
2153
|
-
}
|
|
2154
|
-
for (const [path, roles] of rolesByPath) {
|
|
2155
|
-
if (roles.length < 2) continue;
|
|
2156
|
-
const allowedRoles = new Set(["Brownfield Review", "Verified Change", "OR"]);
|
|
2157
|
-
if (modeSliceDecision(runState) !== "verified_change" || roles.some((role) => !allowedRoles.has(role))) {
|
|
2158
|
-
findings.push({ severity: "block", code: "AGDF_ARTEFACT_ROLE_ALIAS_INVALID", message: `Artefact path is reused across incompatible roles: ${roles.join(", ")}.`, path, next_step: "Use distinct artefacts or a lifecycle-consistent Verified Change compact record." });
|
|
2159
|
-
continue;
|
|
2160
|
-
}
|
|
2161
|
-
const record = readVerifiedChangeRecord(targetDir, runState);
|
|
2162
|
-
if (record.status !== "present") continue;
|
|
2163
|
-
const recordStatus = cleanStatusCell(extractField(record.content, "status"));
|
|
2164
|
-
const brownfieldComplete = extractField(record.content, "decision") === "verified_change" && filled(extractField(record.content, "scope_reason")) && filled(extractField(record.content, "evidence"));
|
|
2165
|
-
const miniCloseoutComplete = ["delivered", "intentionally_not_delivered", "residual_risk", "next_step"].every((field) => filled(extractField(record.content, field)));
|
|
2166
|
-
if (roles.includes("Brownfield Review") && (runState.artefacts.get("Brownfield Review")?.status !== "done" || !brownfieldComplete)) findings.push({ severity: "block", code: "AGDF_VERIFIED_CHANGE_BROWNFIELD_ROLE_INVALID", message: "Consolidated Brownfield Review role is incomplete.", path, next_step: "Complete Brownfield Selection before eligibility." });
|
|
2167
|
-
if (roles.includes("Verified Change") && runState.artefacts.get("Verified Change")?.status !== recordStatus) findings.push({ severity: "block", code: "AGDF_VERIFIED_CHANGE_ROLE_STATUS_MISMATCH", message: "Verified Change artefact row status does not match the compact record.", path, next_step: "Align the run-state row and record status." });
|
|
2168
|
-
if (roles.includes("OR") && runState.artefacts.get("OR")?.status === "done" && (recordStatus !== "executed" || !miniCloseoutComplete)) findings.push({ severity: "block", code: "AGDF_VERIFIED_CHANGE_OR_ROLE_INVALID", message: "Consolidated OR is complete before execution and Mini-Closeout evidence are complete.", path, next_step: "Complete execution and Mini-Closeout evidence before marking OR done." });
|
|
2169
|
-
}
|
|
2170
|
-
return findings;
|
|
2171
|
-
}
|
|
2172
|
-
|
|
2173
|
-
function isInternalStepSatisfied(runState, step) {
|
|
2174
|
-
const artefact = runState.artefacts.get(step);
|
|
2175
|
-
if (!artefact) return false;
|
|
2176
|
-
if (artefact.status === "done") return true;
|
|
2177
|
-
return artefact.status === "not_applicable"
|
|
2178
|
-
&& (step === "Brownfield Review" || step === "Brownfield Analysis");
|
|
2179
|
-
}
|
|
2180
|
-
|
|
2181
|
-
function modeSliceDecision(runState) {
|
|
2182
|
-
const decision = runState.mode_slice_decision?.decision ?? "";
|
|
2183
|
-
const scopeReason = runState.mode_slice_decision?.scope_reason ?? "";
|
|
2184
|
-
const evidence = runState.mode_slice_decision?.evidence ?? "";
|
|
2185
|
-
if (!decision || isPlaceholderValue(decision) || !filled(scopeReason) || !filled(evidence)) return "undecided";
|
|
2186
|
-
return decision;
|
|
2187
|
-
}
|
|
2188
|
-
|
|
2189
|
-
function relationshipRequired(runState, requiredBy) {
|
|
2190
|
-
if (gateApprovalStatus(runState, requiredBy) === "not_applicable") return false;
|
|
2191
|
-
if (requiredBy === "QA") {
|
|
2192
|
-
return isGateSatisfied(runState, "QA");
|
|
2193
|
-
}
|
|
2194
|
-
|
|
2195
|
-
return isGateSatisfied(runState, requiredBy);
|
|
2196
|
-
}
|
|
2197
|
-
|
|
2198
|
-
function findRelationship(runState, expected) {
|
|
2199
|
-
return runState.artefact_chain.find((row) =>
|
|
2200
|
-
row.from === expected.from
|
|
2201
|
-
&& row.relationship === expected.relationship
|
|
2202
|
-
&& row.to === expected.to
|
|
2203
|
-
);
|
|
2204
|
-
}
|
|
2205
|
-
|
|
2206
|
-
function severityFromImpact(value) {
|
|
2207
|
-
if (value === "block") return "block";
|
|
2208
|
-
if (value === "revise") return "revise";
|
|
2209
|
-
if (value === "warning" || value === "warn") return "warn";
|
|
2210
|
-
return null;
|
|
2211
|
-
}
|
|
2212
|
-
|
|
2213
|
-
function analyzeDeliveryMap(runState) {
|
|
2214
|
-
const findings = [];
|
|
2215
|
-
const relationships = deliveryRelationships.map((expected) => {
|
|
2216
|
-
const row = findRelationship(runState, expected);
|
|
2217
|
-
const required = relationshipRequired(runState, expected.requiredBy);
|
|
2218
|
-
const evidence = row?.evidence ?? "";
|
|
2219
|
-
const status = !row ? "missing" : filled(evidence) ? "pass" : required ? "missing_evidence" : "template";
|
|
2220
|
-
|
|
2221
|
-
if (required && status !== "pass") {
|
|
2222
|
-
findings.push({
|
|
2223
|
-
severity: "revise",
|
|
2224
|
-
code: status === "missing" ? "AGDF_DELIVERY_RELATIONSHIP_MISSING" : "AGDF_DELIVERY_RELATIONSHIP_EVIDENCE_MISSING",
|
|
2225
|
-
message: `${expected.from} must be traceable via ${expected.relationship} ${expected.to}.`,
|
|
2226
|
-
path: runState.path,
|
|
2227
|
-
next_step: "Fill the Artefact Chain row with concrete evidence before treating the delivery map as complete.",
|
|
2228
|
-
});
|
|
2229
|
-
}
|
|
2230
|
-
|
|
2231
|
-
return {
|
|
2232
|
-
from: expected.from,
|
|
2233
|
-
relationship: expected.relationship,
|
|
2234
|
-
to: expected.to,
|
|
2235
|
-
required,
|
|
2236
|
-
status,
|
|
2237
|
-
evidence,
|
|
2238
|
-
};
|
|
2239
|
-
});
|
|
2240
|
-
|
|
2241
|
-
for (const item of runState.missing_evidence) {
|
|
2242
|
-
const severity = severityFromImpact(item.impact);
|
|
2243
|
-
if (!severity) continue;
|
|
2244
|
-
findings.push({
|
|
2245
|
-
severity,
|
|
2246
|
-
code: "AGDF_MISSING_EVIDENCE_DECLARED",
|
|
2247
|
-
message: item.missing_evidence,
|
|
2248
|
-
path: runState.path,
|
|
2249
|
-
next_step: item.required_next_step || "Resolve or explicitly accept the missing evidence before advancing the gate.",
|
|
2250
|
-
});
|
|
2251
|
-
}
|
|
2252
|
-
|
|
2253
|
-
for (const item of runState.risks) {
|
|
2254
|
-
const severity = severityFromImpact(item.impact);
|
|
2255
|
-
if (!severity) continue;
|
|
2256
|
-
findings.push({
|
|
2257
|
-
severity,
|
|
2258
|
-
code: "AGDF_RISK_DECLARED",
|
|
2259
|
-
message: item.risk,
|
|
2260
|
-
path: runState.path,
|
|
2261
|
-
next_step: item.mitigation_or_owner || "Assign mitigation or ownership before advancing the gate.",
|
|
2262
|
-
});
|
|
2263
|
-
}
|
|
2264
|
-
|
|
2265
|
-
const contextSeverity = severityFromImpact(runState.context_graph?.gate_effect);
|
|
2266
|
-
if (contextSeverity) {
|
|
2267
|
-
findings.push({
|
|
2268
|
-
severity: contextSeverity,
|
|
2269
|
-
code: "AGDF_CONTEXT_GRAPH_GATE_EFFECT",
|
|
2270
|
-
message: `Context Graph impact is ${runState.context_graph.impact || "unspecified"} with gate effect ${runState.context_graph.gate_effect}.`,
|
|
2271
|
-
path: runState.path,
|
|
2272
|
-
next_step: runState.context_graph.required_action && runState.context_graph.required_action !== "none"
|
|
2273
|
-
? `Resolve Context Graph action: ${runState.context_graph.required_action}.`
|
|
2274
|
-
: "Clarify the Context Graph impact before advancing the gate.",
|
|
2275
|
-
});
|
|
2276
|
-
}
|
|
2277
|
-
|
|
2278
|
-
const multiScopeState = runState.source_scope?.multi_scope_state;
|
|
2279
|
-
if (multiScopeState === "ambiguous" || multiScopeState === "blocked") {
|
|
2280
|
-
findings.push({
|
|
2281
|
-
severity: multiScopeState === "blocked" ? "block" : "revise",
|
|
2282
|
-
code: "AGDF_SCOPE_AMBIGUOUS",
|
|
2283
|
-
message: "Multiple active scope lines are plausible; the agent must not choose one silently.",
|
|
2284
|
-
path: runState.path,
|
|
2285
|
-
next_step: "List competing scope lines with gate and artefact evidence, then clarify the active scope.",
|
|
2286
|
-
});
|
|
2287
|
-
}
|
|
2288
|
-
|
|
2289
|
-
const branchEffect = runState.source_scope?.branch_workspace_scope_effect;
|
|
2290
|
-
if (branchEffect === "conflicts" || branchEffect === "insufficient") {
|
|
2291
|
-
findings.push({
|
|
2292
|
-
severity: branchEffect === "conflicts" ? "revise" : "warn",
|
|
2293
|
-
code: "AGDF_BRANCH_NOT_SCOPE_PROOF",
|
|
2294
|
-
message: "Branch or workspace evidence is not sufficient scope proof.",
|
|
2295
|
-
path: runState.path,
|
|
2296
|
-
next_step: "Confirm the active scope from durable artefacts or record why branch/workspace evidence is only supporting evidence.",
|
|
2297
|
-
});
|
|
2298
|
-
}
|
|
2299
|
-
|
|
2300
|
-
if (runState.memory?.target && runState.memory.target !== "none" && !filled(runState.memory.reason)) {
|
|
2301
|
-
findings.push({
|
|
2302
|
-
severity: "warn",
|
|
2303
|
-
code: "AGDF_MEMORY_TARGET_REASON_MISSING",
|
|
2304
|
-
message: "Knowledge persistence target is set without a reason.",
|
|
2305
|
-
path: runState.path,
|
|
2306
|
-
next_step: "Fill memory_reason or set memory_target to none.",
|
|
2307
|
-
});
|
|
2308
|
-
}
|
|
2309
|
-
|
|
2310
|
-
return {
|
|
2311
|
-
relationships,
|
|
2312
|
-
missing_evidence: runState.missing_evidence,
|
|
2313
|
-
risks: runState.risks,
|
|
2314
|
-
context_graph: runState.context_graph,
|
|
2315
|
-
source_scope: runState.source_scope,
|
|
2316
|
-
memory: runState.memory,
|
|
2317
|
-
findings,
|
|
2318
|
-
};
|
|
2319
|
-
}
|
|
2320
|
-
|
|
2321
|
-
function deriveQualityOutlook(runState, findings = []) {
|
|
2322
|
-
if (filled(runState.quality_outlook)) return runState.quality_outlook;
|
|
2323
|
-
if (findings.some((finding) => finding.severity === "block" || finding.severity === "revise")) {
|
|
2324
|
-
return "Resolve blocking or revise-level delivery-map findings before making stronger quality claims.";
|
|
2325
|
-
}
|
|
2326
|
-
if (findings.some((finding) => finding.severity === "warn")) {
|
|
2327
|
-
return "Review warning-level findings when investing further in delivery confidence.";
|
|
2328
|
-
}
|
|
2329
|
-
return "No additional quality follow-up identified from the current control state.";
|
|
2330
|
-
}
|
|
2331
|
-
|
|
2332
|
-
function postApprovalTransition(missingApproval) {
|
|
2333
|
-
const transitions = new Map([
|
|
2334
|
-
["Approval: UR", {
|
|
2335
|
-
next_gate_after_approval: "Brownfield Review",
|
|
2336
|
-
allowed_after_approval: "Run Brownfield Review and record Mode/Slice Decision before PRD or implementation.",
|
|
2337
|
-
internal_next_step: "Brownfield Review and Mode/Slice Decision",
|
|
2338
|
-
next_user_gate: "PRD",
|
|
2339
|
-
user_action_required: "yes",
|
|
2340
|
-
}],
|
|
2341
|
-
["Approval: PRD", {
|
|
2342
|
-
next_gate_after_approval: "SD",
|
|
2343
|
-
allowed_after_approval: "Draft Solution Design; implementation remains forbidden.",
|
|
2344
|
-
internal_next_step: "draft Solution Design",
|
|
2345
|
-
next_user_gate: "SD",
|
|
2346
|
-
user_action_required: "yes",
|
|
2347
|
-
}],
|
|
2348
|
-
["Approval: SD", {
|
|
2349
|
-
next_gate_after_approval: "TP",
|
|
2350
|
-
allowed_after_approval: "Draft Task/Test Plan; implementation remains forbidden.",
|
|
2351
|
-
internal_next_step: "draft Task/Test Plan",
|
|
2352
|
-
next_user_gate: "TP",
|
|
2353
|
-
user_action_required: "yes",
|
|
2354
|
-
}],
|
|
2355
|
-
["Approval: TP", {
|
|
2356
|
-
next_gate_after_approval: "none",
|
|
2357
|
-
allowed_after_approval: "Run implementation-prep Brownfield Analysis before CD+Tests; no further user approval is required at this internal step.",
|
|
2358
|
-
internal_next_step: "pre-implementation Brownfield Analysis",
|
|
2359
|
-
next_user_gate: "none",
|
|
2360
|
-
user_action_required: "no",
|
|
2361
|
-
}],
|
|
2362
|
-
["Approval: QA", {
|
|
2363
|
-
next_gate_after_approval: "UAT",
|
|
2364
|
-
allowed_after_approval: "Request UAT when QA has passed; release remains gated.",
|
|
2365
|
-
internal_next_step: "prepare UAT evidence",
|
|
2366
|
-
next_user_gate: "UAT",
|
|
2367
|
-
user_action_required: "yes",
|
|
2368
|
-
}],
|
|
2369
|
-
["Approval: UAT", {
|
|
2370
|
-
next_gate_after_approval: "OR",
|
|
2371
|
-
allowed_after_approval: "Produce OR or delivery closeout; VCS and release actions still require explicit instruction.",
|
|
2372
|
-
internal_next_step: "OR or delivery closeout",
|
|
2373
|
-
next_user_gate: "none",
|
|
2374
|
-
user_action_required: "no",
|
|
2375
|
-
}],
|
|
2376
|
-
]);
|
|
2377
|
-
|
|
2378
|
-
return transitions.get(missingApproval) ?? {
|
|
2379
|
-
next_gate_after_approval: "none",
|
|
2380
|
-
allowed_after_approval: "none",
|
|
2381
|
-
};
|
|
2382
|
-
}
|
|
2383
|
-
|
|
2384
|
-
const BREADCRUMB_PATH_TEMPLATES = {
|
|
2385
|
-
structured_delivery: ["UR", "PRD", "SD", "TP", "QA", "UAT"],
|
|
2386
|
-
structured_slice: ["UR", "PRD", "SD", "TP", "QA", "UAT"],
|
|
2387
|
-
verified_change: ["UR", "Verified Change", "OR"],
|
|
2388
|
-
quick_task: ["UR", "Quick Task"],
|
|
2389
|
-
block: ["UR", "Block"],
|
|
2390
|
-
undecided: ["UR"],
|
|
2391
|
-
};
|
|
2392
|
-
|
|
2393
|
-
const BREADCRUMB_STANDARD_GATES = ["UR", "PRD", "SD", "TP", "QA", "UAT"];
|
|
2394
|
-
|
|
2395
|
-
function isReadyUserGateApproval({ status, currentGate, missingApproval }) {
|
|
2396
|
-
return status === "open"
|
|
2397
|
-
&& BREADCRUMB_STANDARD_GATES.includes(currentGate)
|
|
2398
|
-
&& missingApproval === `Approval: ${currentGate}`;
|
|
2399
|
-
}
|
|
2400
|
-
|
|
2401
|
-
function buildBreadcrumbPath(modeSliceDecision, currentGate, runState) {
|
|
2402
|
-
const gates = BREADCRUMB_PATH_TEMPLATES[modeSliceDecision] ?? ["UR"];
|
|
2403
|
-
return gates.map((gate) => {
|
|
2404
|
-
if (BREADCRUMB_STANDARD_GATES.includes(gate)) {
|
|
2405
|
-
if (gateApprovalStatus(runState, gate) === "approved") return { gate, status: "fulfilled" };
|
|
2406
|
-
if (gate === currentGate) return { gate, status: "current" };
|
|
2407
|
-
return { gate, status: "open" };
|
|
2408
|
-
}
|
|
2409
|
-
if (gate === currentGate) return { gate, status: "current" };
|
|
2410
|
-
if ((gate === "Verified Change" || gate === "Quick Task") && currentGate === "OR")
|
|
2411
|
-
return { gate, status: "fulfilled" };
|
|
2412
|
-
if (gate === "Verified Change" && modeSliceDecision === "verified_change")
|
|
2413
|
-
return { gate, status: "current" };
|
|
2414
|
-
if (gate === "Quick Task" && modeSliceDecision === "quick_task")
|
|
2415
|
-
return { gate, status: "current" };
|
|
2416
|
-
if (gate === "Block" && modeSliceDecision === "block")
|
|
2417
|
-
return { gate, status: "current" };
|
|
2418
|
-
if (gate === "OR" && currentGate === "OR")
|
|
2419
|
-
return { gate, status: "current" };
|
|
2420
|
-
return { gate, status: "open" };
|
|
2421
|
-
});
|
|
2422
|
-
}
|
|
2423
|
-
|
|
2424
|
-
function buildStatusCard({
|
|
2425
|
-
status,
|
|
2426
|
-
currentGate,
|
|
2427
|
-
allowed = [],
|
|
2428
|
-
forbidden = [],
|
|
2429
|
-
blockingReason = "none",
|
|
2430
|
-
missingApproval = "none",
|
|
2431
|
-
nextAllowedAction,
|
|
2432
|
-
runState,
|
|
2433
|
-
chatLanguage = "en",
|
|
2434
|
-
findings = [],
|
|
2435
|
-
}) {
|
|
2436
|
-
const qualityOutlook = deriveQualityOutlook(runState, findings);
|
|
2437
|
-
const postApproval = postApprovalTransition(missingApproval);
|
|
2438
|
-
const isUserGateApproval = isReadyUserGateApproval({ status, currentGate, missingApproval });
|
|
2439
|
-
const lifecycle = extractField(runState.content ?? "", "lifecycle") || "unknown";
|
|
2440
|
-
const nativeAttemptRequired = false;
|
|
2441
|
-
const interactionKind = status === "open" && isUserGateApproval ? "gate_approval" : status === "blocked" ? "blocked" : "status";
|
|
2442
|
-
return {
|
|
2443
|
-
run_id: extractField(runState.content ?? "", "run_id") || "unknown",
|
|
2444
|
-
presentation_language: chatLanguage,
|
|
2445
|
-
mode: extractField(runState.content ?? "", "mode") || "unknown",
|
|
2446
|
-
lifecycle,
|
|
2447
|
-
delivery_state: lifecycle === "completed" && currentGate === "OR" ? "completed_closeout_pending" : lifecycle,
|
|
2448
|
-
status,
|
|
2449
|
-
current_gate: currentGate,
|
|
2450
|
-
mode_slice_decision: runState.mode_slice_decision?.decision || "undecided",
|
|
2451
|
-
breadcrumb: buildBreadcrumbPath(
|
|
2452
|
-
runState.mode_slice_decision?.decision || "undecided",
|
|
2453
|
-
currentGate,
|
|
2454
|
-
runState,
|
|
2455
|
-
),
|
|
2456
|
-
allowed_now: allowed,
|
|
2457
|
-
forbidden_now: forbidden,
|
|
2458
|
-
blocking_condition: blockingReason || "none",
|
|
2459
|
-
missing_approval: missingApproval || "none",
|
|
2460
|
-
next_gate_after_approval: postApproval.next_gate_after_approval,
|
|
2461
|
-
allowed_after_approval: postApproval.allowed_after_approval,
|
|
2462
|
-
user_visible_outcome_after_approval: postApproval.allowed_after_approval,
|
|
2463
|
-
internal_next_step: postApproval.internal_next_step || (isUserGateApproval ? "none" : nextAllowedAction),
|
|
2464
|
-
next_user_gate: postApproval.next_user_gate || "none",
|
|
2465
|
-
user_action_required: postApproval.user_action_required || (isUserGateApproval ? "yes" : "no"),
|
|
2466
|
-
evidence: runState.evidence_refs,
|
|
2467
|
-
next_skill: nextSkillByGate[currentGate] ?? "gate-check",
|
|
2468
|
-
next_step: nextAllowedAction,
|
|
2469
|
-
quality_outlook: qualityOutlook,
|
|
2470
|
-
interaction_kind: interactionKind,
|
|
2471
|
-
native_attempt_required: nativeAttemptRequired,
|
|
2472
|
-
native_preflight_outcome: isUserGateApproval ? "unavailable_before_invocation" : "not_applicable",
|
|
2473
|
-
native_preflight_reason: isUserGateApproval ? "capability_missing" : "none",
|
|
2474
|
-
};
|
|
2475
|
-
}
|
|
2476
|
-
|
|
2477
|
-
function statusFromReviewArtefact(artefact) {
|
|
2478
|
-
if (!artefact) return "unknown";
|
|
2479
|
-
const text = `${artefact.status ?? ""} ${artefact.notes ?? ""}`.toLowerCase();
|
|
2480
|
-
for (const status of ["block", "revise", "warn", "pass"]) {
|
|
2481
|
-
if (new RegExp(`\\b${status}(?:ed)?\\b`).test(text)) return status;
|
|
2482
|
-
}
|
|
2483
|
-
return "unknown";
|
|
2484
|
-
}
|
|
2485
|
-
|
|
2486
|
-
function qualityReadinessForRunState(runState, nextAction) {
|
|
2487
|
-
const artefacts = runState.artefacts;
|
|
2488
|
-
const plan = artefacts.get("TP Review");
|
|
2489
|
-
const clean = artefacts.get("Clean Implementation Review") ?? artefacts.get("Clean Review");
|
|
2490
|
-
const code = artefacts.get("CR") ?? artefacts.get("Code Review");
|
|
2491
|
-
const qa = artefacts.get("QA");
|
|
2492
|
-
if (![plan, clean, code, qa].some(Boolean)) return null;
|
|
2493
|
-
const reviewRows = [plan, clean, code, qa].filter(Boolean);
|
|
2494
|
-
const decisive = reviewRows.find((artefact) => /\b(?:block|revise)\b/i.test(`${artefact.status ?? ""} ${artefact.notes ?? ""}`));
|
|
2495
|
-
const readiness = buildQualityReadiness({
|
|
2496
|
-
planCoverage: statusFromReviewArtefact(plan),
|
|
2497
|
-
solutionIntegrity: statusFromReviewArtefact(clean),
|
|
2498
|
-
codeQuality: statusFromReviewArtefact(code),
|
|
2499
|
-
qaDecision: statusFromReviewArtefact(qa),
|
|
2500
|
-
decisiveReason: decisive?.notes ?? "",
|
|
2501
|
-
nextAction,
|
|
2502
|
-
});
|
|
2503
|
-
if (!readiness) return null;
|
|
2504
|
-
return Object.freeze({ ...readiness, decisive_reference: decisive?.path ?? "" });
|
|
2505
|
-
}
|
|
2506
|
-
|
|
2507
|
-
function resolvedArtefactFile(targetDir, rawPath) {
|
|
2508
|
-
const normalizedPath = String(rawPath ?? "").replace(/^`|`$/g, "").trim();
|
|
2509
|
-
if (!normalizedPath || isAbsolute(normalizedPath) || normalizedPath.includes("<") || normalizedPath.includes(">")) return "";
|
|
2510
|
-
const absolutePath = resolve(targetDir, normalizedPath);
|
|
2511
|
-
if (relative(targetDir, absolutePath).startsWith("..") || !existsSync(absolutePath)) return "";
|
|
2512
|
-
try {
|
|
2513
|
-
const realPath = realpathSync(absolutePath);
|
|
2514
|
-
return relative(realpathSync(targetDir), realPath).startsWith("..") || !statSync(realPath).isFile() ? "" : realPath;
|
|
2515
|
-
} catch {
|
|
2516
|
-
return "";
|
|
2517
|
-
}
|
|
2518
|
-
}
|
|
2519
|
-
|
|
2520
|
-
function readArtefactHeading(targetDir, artefact) {
|
|
2521
|
-
const artefactPath = resolvedArtefactFile(targetDir, artefact?.path);
|
|
2522
|
-
if (!artefactPath) return "";
|
|
2523
|
-
try {
|
|
2524
|
-
return readFileSync(artefactPath, "utf8").match(/^#\s+(.+)$/m)?.[1]?.trim() ?? "";
|
|
2525
|
-
} catch {
|
|
2526
|
-
return "";
|
|
2527
|
-
}
|
|
2528
|
-
}
|
|
2529
|
-
|
|
2530
|
-
function buildHumanPresentation(targetDir, runState, currentGate, presentationLocale) {
|
|
2531
|
-
const currentArtefactHeading = readArtefactHeading(targetDir, runState.artefacts.get(currentGate));
|
|
2532
|
-
const urHeading = readArtefactHeading(targetDir, runState.artefacts.get("UR"));
|
|
2533
|
-
return {
|
|
2534
|
-
runTitle: resolveHumanRunTitle({
|
|
2535
|
-
currentArtefactHeading,
|
|
2536
|
-
urHeading,
|
|
2537
|
-
runContent: runState.content,
|
|
2538
|
-
runId: extractField(runState.content ?? "", "run_id") || "unknown",
|
|
2539
|
-
}),
|
|
2540
|
-
gateTitle: gateTitle(interactionLocales, presentationLocale, currentGate),
|
|
2541
|
-
artefactRefs: buildArtefactRefs(runState.artefacts, interactionLocales, presentationLocale, {
|
|
2542
|
-
pathExists(path) {
|
|
2543
|
-
return Boolean(resolvedArtefactFile(targetDir, path));
|
|
2544
|
-
},
|
|
2545
|
-
}),
|
|
2546
|
-
};
|
|
2547
|
-
}
|
|
2548
|
-
|
|
2549
|
-
function durableArtefactBlock(gate, nextGate) {
|
|
2550
|
-
const label = gate === "QA" ? "QA report" : `${gate} artefact`;
|
|
2551
|
-
const stablePath = gate === "QA" ? ".agdf/control/artefacts/<key>/QA_REPORT.md" : `.agdf/control/artefacts/<key>/${gate}.md`;
|
|
2552
|
-
return {
|
|
2553
|
-
status: "blocked",
|
|
2554
|
-
current_gate: gate,
|
|
2555
|
-
blocking_reason: `missing_durable_${gate.toLowerCase()}_artefact`,
|
|
2556
|
-
missing_approval: "none",
|
|
2557
|
-
allowed: [
|
|
2558
|
-
`persist the approved ${label} in a stable artefact path such as ${stablePath}`,
|
|
2559
|
-
"link the artefact from the selected RUN_STATE.md and MASTER_BACKLOG.md",
|
|
2560
|
-
"run gate-check again",
|
|
2561
|
-
],
|
|
2562
|
-
forbidden: nextGate
|
|
2563
|
-
? [`create ${nextGate}`, "create later-gate artefacts", "implement gated work", "claim QA or release readiness"]
|
|
2564
|
-
: ["create later-gate artefacts", "implement gated work", "claim release readiness"],
|
|
2565
|
-
next_allowed_action: `Persist the approved ${label} and link it from the AGDF control state before continuing.`,
|
|
2566
|
-
};
|
|
2567
|
-
}
|
|
2568
|
-
|
|
2569
|
-
function isGateSatisfied(runState, gate) {
|
|
2570
|
-
const status = gateApprovalStatus(runState, gate);
|
|
2571
|
-
if (status === "not_applicable") return true;
|
|
2572
|
-
if (status !== "approved") return false;
|
|
2573
|
-
if (gate === "UAT") return true;
|
|
2574
|
-
return isDurableGateArtefactSatisfied(runState, gate);
|
|
2575
|
-
}
|
|
2576
|
-
|
|
2577
|
-
function qaRevisionRequired(runState) {
|
|
2578
|
-
return gateArtefactStatus(runState, "QA").status === "revise";
|
|
2579
|
-
}
|
|
2580
|
-
|
|
2581
|
-
function transitionDecisionForRunState(runState, verifiedChange = null) {
|
|
2582
|
-
if (gateApprovalStatus(runState, "UR") === "approved" && !isGateSatisfied(runState, "UR")) return durableArtefactBlock("UR", "PRD");
|
|
2583
|
-
|
|
2584
|
-
if (!isGateSatisfied(runState, "UR")) {
|
|
2585
|
-
return {
|
|
2586
|
-
status: "open",
|
|
2587
|
-
current_gate: "UR",
|
|
2588
|
-
blocking_reason: "none",
|
|
2589
|
-
missing_approval: "Approval: UR",
|
|
2590
|
-
allowed: ["clarify user need", "formulate and persist UR", "record evidence", "request exact UR approval"],
|
|
2591
|
-
forbidden: ["create PRD", "create SD", "create TP", "run Brownfield Analysis", "implement code", "claim QA or release readiness"],
|
|
2592
|
-
next_allowed_action: "Clarify the user requirement, persist the UR, and request exact approval: Approval: UR",
|
|
2593
|
-
};
|
|
2594
|
-
}
|
|
2595
|
-
|
|
2596
|
-
if (gateApprovalStatus(runState, "PRD") === "approved" && !isGateSatisfied(runState, "PRD")) return durableArtefactBlock("PRD", "SD");
|
|
2597
|
-
|
|
2598
|
-
if (!isGateSatisfied(runState, "PRD")) {
|
|
2599
|
-
if (!isInternalStepSatisfied(runState, "Brownfield Review")) {
|
|
2600
|
-
return {
|
|
2601
|
-
status: "open",
|
|
2602
|
-
current_gate: "Brownfield Review",
|
|
2603
|
-
blocking_reason: "none",
|
|
2604
|
-
missing_approval: "none",
|
|
2605
|
-
allowed: [
|
|
2606
|
-
"run Brownfield Review after G-00",
|
|
2607
|
-
"identify existing workstream, owners, SoT, reuse risks and open PRD/SD questions",
|
|
2608
|
-
"mark Brownfield Review as done or not_applicable in the selected RUN_STATE.md",
|
|
2609
|
-
],
|
|
2610
|
-
forbidden: ["create PRD before Brownfield Review is resolved", "create SD", "create TP", "implement code", "claim QA or release readiness"],
|
|
2611
|
-
next_allowed_action: "Run Brownfield Review after G-00 before drafting PRD, or mark Brownfield Review not_applicable with evidence.",
|
|
2612
|
-
};
|
|
2613
|
-
}
|
|
2614
|
-
|
|
2615
|
-
const modeDecision = modeSliceDecision(runState);
|
|
2616
|
-
if (modeDecision === "undecided") {
|
|
2617
|
-
return {
|
|
2618
|
-
status: "open",
|
|
2619
|
-
current_gate: "Mode/Slice Decision",
|
|
2620
|
-
blocking_reason: "none",
|
|
2621
|
-
missing_approval: "none",
|
|
2622
|
-
allowed: [
|
|
2623
|
-
"decide whether the approved UR is quick_task, verified_change, structured_slice, structured_delivery or block",
|
|
2624
|
-
"record scope reason, evidence and required next gate depth in the selected RUN_STATE.md",
|
|
2625
|
-
"choose the next required gate depth before drafting PRD or implementing",
|
|
2626
|
-
],
|
|
2627
|
-
forbidden: ["create PRD before process size is decided", "create SD", "create TP", "implement code", "claim QA or release readiness"],
|
|
2628
|
-
next_allowed_action: "Record the Mode/Slice Decision from Brownfield Review with scope reason and evidence before choosing PRD depth or Quick Task execution.",
|
|
2629
|
-
};
|
|
2630
|
-
}
|
|
2631
|
-
|
|
2632
|
-
if (modeDecision === "block") {
|
|
2633
|
-
return {
|
|
2634
|
-
status: "blocked",
|
|
2635
|
-
current_gate: "Mode/Slice Decision",
|
|
2636
|
-
blocking_reason: "mode_slice_decision_blocked",
|
|
2637
|
-
missing_approval: "none",
|
|
2638
|
-
allowed: ["resolve ownership, SoT, evidence, impact or product-direction uncertainty", "run gate-check again"],
|
|
2639
|
-
forbidden: ["create PRD", "create SD", "create TP", "implement code", "claim QA or release readiness"],
|
|
2640
|
-
next_allowed_action: "Resolve the Brownfield Review blocker before choosing a delivery path.",
|
|
2641
|
-
};
|
|
2642
|
-
}
|
|
2643
|
-
|
|
2644
|
-
if (modeDecision === "quick_task") {
|
|
2645
|
-
return {
|
|
2646
|
-
status: "open",
|
|
2647
|
-
current_gate: "Quick Task Execution",
|
|
2648
|
-
blocking_reason: "none",
|
|
2649
|
-
missing_approval: "none",
|
|
2650
|
-
allowed: ["implement the narrow approved UR scope", "run relevant checks", "record evidence and close with OR-lite"],
|
|
2651
|
-
forbidden: ["expand scope beyond the Brownfield Review decision", "create broad PRD/SD/TP artefacts by ritual", "claim QA or release readiness without evidence"],
|
|
2652
|
-
next_allowed_action: "Proceed as a Quick Task within the Brownfield Review scope and record verification evidence.",
|
|
2653
|
-
};
|
|
2654
|
-
}
|
|
2655
|
-
|
|
2656
|
-
if (modeDecision === "verified_change") {
|
|
2657
|
-
const state = verifiedChange?.status ?? "missing";
|
|
2658
|
-
const escalationTarget = verifiedChange?.escalation_target ?? "structured_slice";
|
|
2659
|
-
if (state === "executed") {
|
|
2660
|
-
return {
|
|
2661
|
-
status: "open",
|
|
2662
|
-
current_gate: "OR",
|
|
2663
|
-
blocking_reason: "none",
|
|
2664
|
-
missing_approval: "none",
|
|
2665
|
-
allowed: ["use the Verified Change mini-closeout", "offer delivery closeout when requested"],
|
|
2666
|
-
forbidden: ["create PRD, SD, TP, QA or UAT by ritual", "commit, push, open PR or release automatically"],
|
|
2667
|
-
next_allowed_action: "Close the executed Verified Change with its compact record and offer delivery closeout; do not perform VCS actions automatically.",
|
|
2668
|
-
};
|
|
2669
|
-
}
|
|
2670
|
-
if (state === "escalated" || (state === "invalid" && verifiedChangeEscalationTargets.has(escalationTarget))) {
|
|
2671
|
-
return {
|
|
2672
|
-
status: "open",
|
|
2673
|
-
current_gate: "PRD",
|
|
2674
|
-
blocking_reason: state === "escalated" ? "verified_change_escalated" : "verified_change_invalid_escalated",
|
|
2675
|
-
missing_approval: "Approval: PRD",
|
|
2676
|
-
allowed: ["draft the structured PRD required by the declared escalation target", "retain the Verified Change escalation evidence"],
|
|
2677
|
-
forbidden: ["implement through Verified Change", "create SD or TP before PRD approval", "claim QA or release readiness"],
|
|
2678
|
-
next_allowed_action: `Proceed as ${escalationTarget}: draft the required PRD and request exact approval: Approval: PRD.`,
|
|
2679
|
-
};
|
|
2680
|
-
}
|
|
2681
|
-
if (state === "eligible") {
|
|
2682
|
-
return {
|
|
2683
|
-
status: "open",
|
|
2684
|
-
current_gate: "Verified Change Execution",
|
|
2685
|
-
blocking_reason: "none",
|
|
2686
|
-
missing_approval: "none",
|
|
2687
|
-
allowed: ["implement only declared source and derived paths", "run declared propagation and validation commands", "record execution evidence and mini-closeout"],
|
|
2688
|
-
forbidden: ["touch unlisted paths", "add prohibited impacts", "claim QA, UAT or release readiness"],
|
|
2689
|
-
next_allowed_action: "Implement only the eligible Verified Change record scope, then record passing propagation and validation evidence.",
|
|
2690
|
-
};
|
|
2691
|
-
}
|
|
2692
|
-
return {
|
|
2693
|
-
status: state === "missing" || state === "draft" ? "open" : "blocked",
|
|
2694
|
-
current_gate: "Verified Change Execution",
|
|
2695
|
-
blocking_reason: state === "invalid" ? "verified_change_invalid" : "verified_change_record_required",
|
|
2696
|
-
missing_approval: "none",
|
|
2697
|
-
allowed: ["create or refine the compact Verified Change record", "capture baseline paths", "prove fail-closed eligibility with doctor and gate-check"],
|
|
2698
|
-
forbidden: ["implement candidate changes", "claim validation, QA, UAT or release readiness"],
|
|
2699
|
-
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.",
|
|
2700
|
-
};
|
|
2701
|
-
}
|
|
2702
|
-
|
|
2703
|
-
return {
|
|
2704
|
-
status: "open",
|
|
2705
|
-
current_gate: "PRD",
|
|
2706
|
-
blocking_reason: "none",
|
|
2707
|
-
missing_approval: "Approval: PRD",
|
|
2708
|
-
allowed: ["draft or refine PRD", "define scope", "define acceptance criteria", "define non-goals", "request exact PRD approval"],
|
|
2709
|
-
forbidden: ["create SD", "create TP", "run Brownfield Analysis as implementation preparation", "implement code", "claim QA or release readiness"],
|
|
2710
|
-
next_allowed_action: modeDecision === "structured_slice"
|
|
2711
|
-
? "Draft or refine the smallest PRD slice justified by Brownfield Review; do not implement before required artefacts are approved."
|
|
2712
|
-
: "Draft or refine the PRD; do not implement before PRD, SD and TP are approved.",
|
|
2713
|
-
};
|
|
2714
|
-
}
|
|
2715
|
-
|
|
2716
|
-
if (gateApprovalStatus(runState, "SD") === "approved" && !isGateSatisfied(runState, "SD")) return durableArtefactBlock("SD", "TP");
|
|
2717
|
-
|
|
2718
|
-
if (!isGateSatisfied(runState, "SD")) {
|
|
2719
|
-
return {
|
|
2720
|
-
status: "open",
|
|
2721
|
-
current_gate: "SD",
|
|
2722
|
-
blocking_reason: "none",
|
|
2723
|
-
missing_approval: "Approval: SD",
|
|
2724
|
-
allowed: ["draft or refine Solution Design", "define architecture", "define ownership", "request exact SD approval"],
|
|
2725
|
-
forbidden: ["create TP", "implement code", "claim QA or release readiness"],
|
|
2726
|
-
next_allowed_action: "Draft or refine the Solution Design; do not implement before SD and TP are approved.",
|
|
2727
|
-
};
|
|
2728
|
-
}
|
|
2729
|
-
|
|
2730
|
-
if (gateApprovalStatus(runState, "TP") === "approved" && !isGateSatisfied(runState, "TP")) return durableArtefactBlock("TP", "Brownfield Analysis");
|
|
2731
|
-
|
|
2732
|
-
if (!isGateSatisfied(runState, "TP")) {
|
|
2733
|
-
return {
|
|
2734
|
-
status: "open",
|
|
2735
|
-
current_gate: "TP",
|
|
2736
|
-
blocking_reason: "none",
|
|
2737
|
-
missing_approval: "Approval: TP",
|
|
2738
|
-
allowed: ["draft or refine Task/Test Plan", "define task IDs", "define test evidence", "request exact TP approval"],
|
|
2739
|
-
forbidden: ["implement code", "claim QA or release readiness"],
|
|
2740
|
-
next_allowed_action: "Draft or refine the Task/Test Plan; do not implement before TP is approved.",
|
|
2741
|
-
};
|
|
2742
|
-
}
|
|
2743
|
-
|
|
2744
|
-
const implementationStepsRequired = gateApprovalStatus(runState, "TP") === "approved";
|
|
2745
|
-
if (implementationStepsRequired && !isInternalStepSatisfied(runState, "Brownfield Analysis")) {
|
|
2746
|
-
return {
|
|
2747
|
-
status: "open",
|
|
2748
|
-
current_gate: "Brownfield Analysis",
|
|
2749
|
-
blocking_reason: "none",
|
|
2750
|
-
missing_approval: "none",
|
|
2751
|
-
allowed: ["run Brownfield Analysis for the approved TP scope", "verify existing owners, reuse paths and regression risks"],
|
|
2752
|
-
forbidden: ["implement before Brownfield evidence supports the approved TP path", "claim QA or release readiness"],
|
|
2753
|
-
next_allowed_action: "Run Brownfield Analysis for the approved TP scope before CD+Tests.",
|
|
2754
|
-
};
|
|
2755
|
-
}
|
|
2756
|
-
|
|
2757
|
-
if (implementationStepsRequired && !isInternalStepSatisfied(runState, "CD+Tests")) {
|
|
2758
|
-
return {
|
|
2759
|
-
status: "open",
|
|
2760
|
-
current_gate: "CD+Tests",
|
|
2761
|
-
blocking_reason: "none",
|
|
2762
|
-
missing_approval: "none",
|
|
2763
|
-
allowed: ["implement the approved TP tasks", "run the approved test plan", "record implementation and test evidence"],
|
|
2764
|
-
forbidden: ["claim QA pass", "request UAT approval", "release"],
|
|
2765
|
-
next_allowed_action: "Implement the approved TP scope, run its tests, and record CD+Tests evidence before CR.",
|
|
2766
|
-
};
|
|
2767
|
-
}
|
|
2768
|
-
|
|
2769
|
-
if (implementationStepsRequired && !isInternalStepSatisfied(runState, "CR")) {
|
|
2770
|
-
return {
|
|
2771
|
-
status: "open",
|
|
2772
|
-
current_gate: "CR",
|
|
2773
|
-
blocking_reason: "none",
|
|
2774
|
-
missing_approval: "none",
|
|
2775
|
-
allowed: ["run mandatory code review", "record correctness, regression, security and maintainability findings", "fix blocking review findings"],
|
|
2776
|
-
forbidden: ["claim QA pass", "request UAT approval", "release"],
|
|
2777
|
-
next_allowed_action: "Run Code Review for the implemented TP scope and resolve blocking findings before QA.",
|
|
2778
|
-
};
|
|
2779
|
-
}
|
|
2780
|
-
|
|
2781
|
-
if (qaRevisionRequired(runState)) {
|
|
2782
|
-
return {
|
|
2783
|
-
status: "open",
|
|
2784
|
-
current_gate: "QA",
|
|
2785
|
-
blocking_reason: "qa_revise_required",
|
|
2786
|
-
missing_approval: "none",
|
|
2787
|
-
allowed: ["revise the implementation against the QA findings", "refresh CD+Tests and mandatory reviews", "rerun QA with refreshed evidence"],
|
|
2788
|
-
forbidden: ["request QA approval", "request UAT approval", "release", "claim delivery readiness"],
|
|
2789
|
-
next_allowed_action: "Resolve the QA revise findings, refresh CD+Tests and reviews, then rerun QA. Do not request Approval: QA from a revise report.",
|
|
2790
|
-
};
|
|
2791
|
-
}
|
|
2792
|
-
|
|
2793
|
-
if (gateApprovalStatus(runState, "QA") === "approved" && !isGateSatisfied(runState, "QA")) return durableArtefactBlock("QA", "UAT");
|
|
2794
|
-
|
|
2795
|
-
if (isGateSatisfied(runState, "QA") && !isGateSatisfied(runState, "UAT")) {
|
|
2796
|
-
return {
|
|
2797
|
-
status: "open",
|
|
2798
|
-
current_gate: "UAT",
|
|
2799
|
-
blocking_reason: "none",
|
|
2800
|
-
missing_approval: "Approval: UAT",
|
|
2801
|
-
allowed: ["request exact UAT approval", "prepare non-operative delivery summary"],
|
|
2802
|
-
forbidden: ["release", "push", "open PR", "commit without explicit user instruction and required approval"],
|
|
2803
|
-
next_allowed_action: "Request exact approval: Approval: UAT before delivery handoff.",
|
|
2804
|
-
};
|
|
2805
|
-
}
|
|
2806
|
-
|
|
2807
|
-
if (isGateSatisfied(runState, "UAT")) {
|
|
2808
|
-
return {
|
|
2809
|
-
status: "open",
|
|
2810
|
-
current_gate: "OR",
|
|
2811
|
-
blocking_reason: "none",
|
|
2812
|
-
missing_approval: "none",
|
|
2813
|
-
allowed: ["produce OR or delivery closeout", "prepare commit, push or PR handoff when requested"],
|
|
2814
|
-
forbidden: ["commit, push, open PR or release automatically"],
|
|
2815
|
-
next_allowed_action: "Produce delivery closeout or requested handoff; do not perform VCS actions automatically.",
|
|
2816
|
-
};
|
|
2817
|
-
}
|
|
2818
|
-
|
|
2819
|
-
return {
|
|
2820
|
-
status: "open",
|
|
2821
|
-
current_gate: "QA",
|
|
2822
|
-
blocking_reason: "none",
|
|
2823
|
-
missing_approval: "Approval: QA",
|
|
2824
|
-
allowed: ["run QA gate", "persist or refine the QA report", "request exact QA approval"],
|
|
2825
|
-
forbidden: ["request UAT approval", "release", "claim delivery readiness before QA approval and report evidence"],
|
|
2826
|
-
next_allowed_action: "Run the QA gate, persist the QA report, and request exact approval: Approval: QA",
|
|
2827
|
-
};
|
|
2828
|
-
}
|
|
2829
|
-
|
|
2830
|
-
function evaluateGateCheck(targetDir, selection = {}) {
|
|
2831
|
-
const doctorReport = evaluateDoctor(targetDir, selection);
|
|
2832
|
-
const runState = readRunState(targetDir, selection);
|
|
2833
|
-
const verifiedChange = modeSliceDecision(runState) === "verified_change"
|
|
2834
|
-
? evaluateVerifiedChange(targetDir, runState)
|
|
2835
|
-
: null;
|
|
2836
|
-
const transitionDecision = transitionDecisionForRunState(runState, verifiedChange);
|
|
2837
|
-
const deliveryMap = analyzeDeliveryMap(runState);
|
|
2838
|
-
const doctorBlocker = doctorReport.findings.find((finding) => finding.severity === "block");
|
|
2839
|
-
const doctorRevise = doctorReport.findings.find((finding) => finding.severity === "revise");
|
|
2840
|
-
const routesInvalidVerifiedChange = modeSliceDecision(runState) === "verified_change"
|
|
2841
|
-
&& verifiedChange?.status === "invalid"
|
|
2842
|
-
&& verifiedChangeEscalationTargets.has(verifiedChange.escalation_target);
|
|
2843
|
-
|
|
2844
|
-
let status = transitionDecision.status;
|
|
2845
|
-
let currentGate = transitionDecision.current_gate;
|
|
2846
|
-
let blockingReason = transitionDecision.blocking_reason;
|
|
2847
|
-
let missingApproval = transitionDecision.missing_approval;
|
|
2848
|
-
let allowed = transitionDecision.allowed;
|
|
2849
|
-
let forbidden = transitionDecision.forbidden;
|
|
2850
|
-
let nextAllowedAction = modeSliceDecision(runState) === "verified_change"
|
|
2851
|
-
? transitionDecision.next_allowed_action
|
|
2852
|
-
: isPlaceholderValue(runState.next_allowed_action)
|
|
2853
|
-
? transitionDecision.next_allowed_action
|
|
2854
|
-
: runState.next_allowed_action;
|
|
2855
|
-
|
|
2856
|
-
if (doctorBlocker?.code === "AGDF_CONTROL_FILE_MISSING") {
|
|
2857
|
-
status = "blocked";
|
|
2858
|
-
blockingReason = doctorBlocker.code;
|
|
2859
|
-
currentGate = "UR";
|
|
2860
|
-
missingApproval = "Approval: UR";
|
|
2861
|
-
allowed = [
|
|
2862
|
-
"draft the minimal UR for the requested change in the response",
|
|
2863
|
-
"request exact approval: Approval: UR",
|
|
2864
|
-
"run npx --yes @agdf/cli@latest init only when durable control state or deterministic setup is explicitly needed",
|
|
2865
|
-
];
|
|
2866
|
-
forbidden = ["create PRD", "create SD", "create TP", "run Brownfield Analysis", "implement code", "claim QA or release readiness"];
|
|
2867
|
-
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.";
|
|
2868
|
-
} else if (doctorBlocker && !routesInvalidVerifiedChange) {
|
|
2869
|
-
status = "blocked";
|
|
2870
|
-
blockingReason = doctorBlocker.code;
|
|
2871
|
-
allowed = ["repair the AGDF control scaffold", ...transitionDecision.allowed, "run doctor again"];
|
|
2872
|
-
forbidden = ["create later-gate artefacts beyond the current allowed gate", "implement gated work", "claim QA or release readiness"];
|
|
2873
|
-
nextAllowedAction = doctorBlocker.next_step;
|
|
2874
|
-
} else if (doctorRevise && !routesInvalidVerifiedChange) {
|
|
2875
|
-
status = "blocked";
|
|
2876
|
-
blockingReason = doctorRevise.code;
|
|
2877
|
-
allowed = [...new Set(["complete the current control-state fields", ...transitionDecision.allowed, "run doctor again"])];
|
|
2878
|
-
forbidden = ["create later-gate artefacts beyond the current allowed gate", "implement gated work before the gate allows it", "claim QA or release readiness"];
|
|
2879
|
-
nextAllowedAction = transitionDecision.current_gate === "UR"
|
|
2880
|
-
? "Fill the current UR control state, persist the UR draft, and request exact approval: Approval: UR."
|
|
2881
|
-
: doctorRevise.next_step;
|
|
2882
|
-
}
|
|
2883
|
-
|
|
2884
|
-
const postApproval = postApprovalTransition(missingApproval);
|
|
2885
|
-
const presentationLocale = resolveConfiguredChatLanguage(targetDir);
|
|
2886
|
-
const statusCard = buildStatusCard({
|
|
2887
|
-
status,
|
|
2888
|
-
currentGate,
|
|
2889
|
-
allowed,
|
|
2890
|
-
forbidden,
|
|
2891
|
-
blockingReason,
|
|
2892
|
-
missingApproval,
|
|
2893
|
-
nextAllowedAction,
|
|
2894
|
-
runState,
|
|
2895
|
-
chatLanguage: presentationLocale,
|
|
2896
|
-
findings: deliveryMap.findings,
|
|
2897
|
-
});
|
|
2898
|
-
const humanPresentation = buildHumanPresentation(targetDir, runState, currentGate, presentationLocale);
|
|
2899
|
-
Object.defineProperty(statusCard, "humanPresentation", {
|
|
2900
|
-
value: humanPresentation,
|
|
2901
|
-
enumerable: false,
|
|
2902
|
-
});
|
|
2903
|
-
attachApprovalOrientationSnapshot(statusCard, {
|
|
2904
|
-
ready: isReadyUserGateApproval({ status, currentGate, missingApproval }),
|
|
2905
|
-
humanPresentation,
|
|
2906
|
-
revisionId: extractField(runState.content ?? "", "revision_id"),
|
|
2907
|
-
registry: interactionLocales,
|
|
2908
|
-
requestedLocale: presentationLocale,
|
|
2909
|
-
});
|
|
2910
|
-
Object.defineProperty(statusCard, "runState", {
|
|
2911
|
-
value: runState,
|
|
2912
|
-
enumerable: false,
|
|
2913
|
-
});
|
|
2914
|
-
|
|
2915
|
-
return {
|
|
2916
|
-
schema_version: "1",
|
|
2917
|
-
status,
|
|
2918
|
-
current_gate: currentGate,
|
|
2919
|
-
blocking_reason: blockingReason,
|
|
2920
|
-
missing_approval: missingApproval,
|
|
2921
|
-
next_gate_after_approval: postApproval.next_gate_after_approval,
|
|
2922
|
-
allowed_after_approval: postApproval.allowed_after_approval,
|
|
2923
|
-
allowed,
|
|
2924
|
-
forbidden,
|
|
2925
|
-
next_allowed_action: nextAllowedAction,
|
|
2926
|
-
doctor_status: doctorReport.status,
|
|
2927
|
-
doctor_summary: doctorReport.summary,
|
|
2928
|
-
evidence_refs: runState.evidence_refs,
|
|
2929
|
-
quality_outlook: deriveQualityOutlook(runState, deliveryMap.findings),
|
|
2930
|
-
interaction_kind: statusCard.interaction_kind,
|
|
2931
|
-
native_attempt_required: statusCard.native_attempt_required,
|
|
2932
|
-
candidate_runs: runState.candidate_runs ?? [],
|
|
2933
|
-
status_card: statusCard,
|
|
2934
|
-
delivery_map: {
|
|
2935
|
-
relationships: deliveryMap.relationships,
|
|
2936
|
-
mode_slice_decision: runState.mode_slice_decision,
|
|
2937
|
-
context_graph: deliveryMap.context_graph,
|
|
2938
|
-
source_scope: deliveryMap.source_scope,
|
|
2939
|
-
memory: deliveryMap.memory,
|
|
2940
|
-
findings: deliveryMap.findings,
|
|
2941
|
-
},
|
|
2942
|
-
verified_change: verifiedChange,
|
|
2943
|
-
doctor_report: doctorReport,
|
|
2944
|
-
};
|
|
2945
|
-
}
|
|
2946
|
-
|
|
2947
|
-
function printGateCheckStatusCard(report) {
|
|
2948
|
-
const card = report.status_card;
|
|
2949
|
-
const labels = localePack(interactionLocales, card.presentation_language).statusCard;
|
|
2950
|
-
const primary = localePack(interactionLocales, card.presentation_language).primary;
|
|
2951
|
-
const presentation = card.humanPresentation ?? {
|
|
2952
|
-
runTitle: card.run_id,
|
|
2953
|
-
gateTitle: gateTitle(interactionLocales, card.presentation_language, card.current_gate),
|
|
2954
|
-
artefactRefs: buildArtefactRefs({}, interactionLocales, card.presentation_language),
|
|
2955
|
-
};
|
|
2956
|
-
console.log(`${labels.title}: ${primary.status[card.status] ?? card.status}`);
|
|
2957
|
-
console.log(`${labels.run}: ${card.run_id}`);
|
|
2958
|
-
console.log(`${labels.humanTitle}: ${presentation.runTitle}`);
|
|
2959
|
-
console.log(`${labels.gate}: ${card.current_gate} — ${presentation.gateTitle}`);
|
|
2960
|
-
console.log(`${labels.lifecycle}: ${primary.lifecycle[card.delivery_state] ?? primary.lifecycle.unknown}`);
|
|
2961
|
-
console.log(`${labels.artefacts}: ${formatArtefactRefs(presentation.artefactRefs)}`);
|
|
2962
|
-
console.log(`${labels.blocked}: ${card.blocking_condition === "none" ? primary.none : primary.blocked}`);
|
|
2963
|
-
console.log(`${labels.missing}: ${card.missing_approval === "none" ? primary.none : card.missing_approval}`);
|
|
2964
|
-
if (card.next_gate_after_approval !== "none") {
|
|
2965
|
-
console.log(`${labels.nextGate}: ${card.next_gate_after_approval} — ${gateTitle(interactionLocales, card.presentation_language, card.next_gate_after_approval)}`);
|
|
2966
|
-
console.log(`${labels.allowedAfter}: ${primary.afterApproval[card.current_gate] ?? primary.actions[card.current_gate]}`);
|
|
2967
|
-
}
|
|
2968
|
-
console.log(`${labels.allowed}: ${primary.actions[card.current_gate] ?? primary.none}`);
|
|
2969
|
-
console.log(`${labels.step}: ${primary.actions[card.current_gate] ?? primary.none}`);
|
|
2970
|
-
console.log(`${labels.quality}: ${primary.quality}`);
|
|
2971
|
-
const readiness = qualityReadinessForRunState(card.runState, report.next_allowed_action);
|
|
2972
|
-
if (readiness) {
|
|
2973
|
-
const quality = localePack(interactionLocales, card.presentation_language).qualityReadiness;
|
|
2974
|
-
console.log("");
|
|
2975
|
-
console.log(`${quality.title}: ${primary.status[readiness.status] ?? readiness.status}`);
|
|
2976
|
-
for (const row of readiness.rows) {
|
|
2977
|
-
console.log(`${quality.rows[row.id]}: ${primary.status[row.status] ?? quality.unknown}`);
|
|
2978
|
-
}
|
|
2979
|
-
const reason = readiness.decisive_reason
|
|
2980
|
-
|| (readiness.status === "pass" ? primary.none : quality.fallbackReasons[readiness.decisive_dimension] || primary.none);
|
|
2981
|
-
console.log(`${quality.reason}: ${reason}`);
|
|
2982
|
-
if (readiness.status !== "pass" && readiness.decisive_reference) console.log(`${quality.reference}: ${readiness.decisive_reference}`);
|
|
2983
|
-
console.log(`${quality.nextAction}: ${readiness.next_action || primary.none}`);
|
|
2984
|
-
console.log(`${quality.decisionOwner}: ${quality.decisionOwnerValue}`);
|
|
2985
|
-
}
|
|
2986
|
-
}
|
|
2987
|
-
|
|
2988
|
-
function printGateCheckReport(report, json, statusCard = false) {
|
|
2989
|
-
if (json) {
|
|
2990
|
-
console.log(JSON.stringify(report, null, 2));
|
|
2991
|
-
return;
|
|
2992
|
-
}
|
|
2993
|
-
|
|
2994
|
-
if (statusCard) {
|
|
2995
|
-
printGateCheckStatusCard(report);
|
|
2996
|
-
return;
|
|
2997
|
-
}
|
|
2998
|
-
|
|
2999
|
-
console.log(`AGDF gate-check: ${report.status}`);
|
|
3000
|
-
console.log(`Current gate: ${report.current_gate}`);
|
|
3001
|
-
console.log(`Blocking reason: ${report.blocking_reason}`);
|
|
3002
|
-
console.log(`Missing approval: ${report.missing_approval}`);
|
|
3003
|
-
console.log(`Quality outlook: ${report.quality_outlook}`);
|
|
3004
|
-
console.log(`Doctor: ${report.doctor_status} (${report.doctor_summary.findings} findings)`);
|
|
3005
|
-
console.log("");
|
|
3006
|
-
console.log("Allowed:");
|
|
3007
|
-
for (const item of report.allowed) console.log(`- ${item}`);
|
|
3008
|
-
console.log("");
|
|
3009
|
-
console.log("Forbidden:");
|
|
3010
|
-
for (const item of report.forbidden) console.log(`- ${item}`);
|
|
3011
|
-
console.log("");
|
|
3012
|
-
console.log(`Next allowed action: ${report.next_allowed_action}`);
|
|
3013
|
-
}
|
|
3014
|
-
|
|
3015
|
-
function readBacklogPointers(targetDir) {
|
|
3016
|
-
const backlogPath = join(".agdf", "control", "MASTER_BACKLOG.md");
|
|
3017
|
-
if (!existsSync(join(targetDir, backlogPath))) return [];
|
|
3018
|
-
|
|
3019
|
-
const backlog = readTargetFile(targetDir, backlogPath);
|
|
3020
|
-
const activeSection = markdownSection(backlog, "Active Backlog");
|
|
3021
|
-
return parseBacklogSection(activeSection);
|
|
3022
|
-
}
|
|
3023
|
-
|
|
3024
|
-
function evaluateDeliveryMap(targetDir, selection = {}) {
|
|
3025
|
-
if (selection.allActive) {
|
|
3026
|
-
const selected = resolveRuns(targetDir, { allActive: true });
|
|
3027
|
-
const runs = selected.runs.map((run) => ({
|
|
3028
|
-
run_id: run.run_id,
|
|
3029
|
-
report: evaluateDeliveryMap(targetDir, { runId: run.run_id }),
|
|
3030
|
-
}));
|
|
3031
|
-
const status = selected.findings.length
|
|
3032
|
-
? "block"
|
|
3033
|
-
: aggregate(
|
|
3034
|
-
runs.map((item) => ({
|
|
3035
|
-
run_id: item.run_id,
|
|
3036
|
-
status: item.report.status,
|
|
3037
|
-
})),
|
|
3038
|
-
{ allowNoActiveRuns: allowNoActiveRuns(targetDir) },
|
|
3039
|
-
).status;
|
|
3040
|
-
const findings = [
|
|
3041
|
-
...selected.findings.map((finding) => ({
|
|
3042
|
-
...finding,
|
|
3043
|
-
severity: "block",
|
|
3044
|
-
message: "Invalid canonical run entry.",
|
|
3045
|
-
next_step: "Repair or remove the invalid run entry.",
|
|
3046
|
-
})),
|
|
3047
|
-
...runs.flatMap((item) =>
|
|
3048
|
-
(item.report.doctor_report?.findings ?? item.report.findings).map((finding) => ({
|
|
3049
|
-
...finding,
|
|
3050
|
-
run_id: item.run_id,
|
|
3051
|
-
})),
|
|
3052
|
-
),
|
|
3053
|
-
];
|
|
3054
|
-
return {
|
|
3055
|
-
schema_version: "1",
|
|
3056
|
-
status,
|
|
3057
|
-
current_gate: "all-active",
|
|
3058
|
-
next_allowed_action: runs.length
|
|
3059
|
-
? "Resolve per-run findings."
|
|
3060
|
-
: "Create or activate a governed run.",
|
|
3061
|
-
quality_outlook: "Keep every active run independently actionable.",
|
|
3062
|
-
relationships: runs.flatMap((item) => item.report.relationships),
|
|
3063
|
-
findings,
|
|
3064
|
-
runs,
|
|
3065
|
-
};
|
|
3066
|
-
}
|
|
3067
|
-
const doctorReport = evaluateDoctor(targetDir, selection);
|
|
3068
|
-
const runState = readRunState(targetDir, selection);
|
|
3069
|
-
const map = analyzeDeliveryMap(runState);
|
|
3070
|
-
const gateDecision = transitionDecisionForRunState(runState);
|
|
3071
|
-
const currentGate = gateDecision.current_gate;
|
|
3072
|
-
|
|
3073
|
-
const severityRank = { block: 3, revise: 2, warn: 1 };
|
|
3074
|
-
const deliverySeverity = map.findings.reduce((max, finding) => Math.max(max, severityRank[finding.severity] ?? 0), 0);
|
|
3075
|
-
const doctorSeverity = severityRank[doctorReport.status] ?? 0;
|
|
3076
|
-
const maxSeverity = Math.max(deliverySeverity, doctorSeverity);
|
|
3077
|
-
const status = maxSeverity >= 3 ? "block" : maxSeverity === 2 ? "revise" : maxSeverity === 1 ? "warn" : "pass";
|
|
3078
|
-
|
|
3079
|
-
const qualityOutlook = deriveQualityOutlook(runState, map.findings);
|
|
3080
|
-
const nextAllowedAction = isPlaceholderValue(runState.next_allowed_action) ? gateDecision.next_allowed_action : runState.next_allowed_action;
|
|
3081
|
-
const postApproval = postApprovalTransition(gateDecision.missing_approval);
|
|
3082
|
-
|
|
3083
|
-
return {
|
|
3084
|
-
schema_version: "1",
|
|
3085
|
-
status,
|
|
3086
|
-
checked_at: new Date().toISOString(),
|
|
3087
|
-
target_dir: targetDir,
|
|
3088
|
-
current_gate: currentGate,
|
|
3089
|
-
next_allowed_action: nextAllowedAction,
|
|
3090
|
-
next_gate_after_approval: postApproval.next_gate_after_approval,
|
|
3091
|
-
allowed_after_approval: postApproval.allowed_after_approval,
|
|
3092
|
-
quality_outlook: qualityOutlook,
|
|
3093
|
-
status_card: buildStatusCard({
|
|
3094
|
-
status,
|
|
3095
|
-
currentGate,
|
|
3096
|
-
allowed: gateDecision.allowed,
|
|
3097
|
-
forbidden: gateDecision.forbidden,
|
|
3098
|
-
blockingReason: gateDecision.blocking_reason,
|
|
3099
|
-
missingApproval: gateDecision.missing_approval,
|
|
3100
|
-
nextAllowedAction,
|
|
3101
|
-
runState,
|
|
3102
|
-
chatLanguage: resolveConfiguredChatLanguage(targetDir),
|
|
3103
|
-
findings: map.findings,
|
|
3104
|
-
}),
|
|
3105
|
-
backlog_pointers: readBacklogPointers(targetDir),
|
|
3106
|
-
artefacts: Object.fromEntries([...runState.artefacts.entries()]),
|
|
3107
|
-
approvals: Object.fromEntries([...runState.approvals.entries()]),
|
|
3108
|
-
mode_slice_decision: runState.mode_slice_decision,
|
|
3109
|
-
relationships: map.relationships,
|
|
3110
|
-
evidence_refs: runState.evidence_refs,
|
|
3111
|
-
missing_evidence: map.missing_evidence,
|
|
3112
|
-
risks: map.risks,
|
|
3113
|
-
context_graph: map.context_graph,
|
|
3114
|
-
source_scope: map.source_scope,
|
|
3115
|
-
memory: map.memory,
|
|
3116
|
-
findings: map.findings,
|
|
3117
|
-
doctor_status: doctorReport.status,
|
|
3118
|
-
doctor_summary: doctorReport.summary,
|
|
3119
|
-
doctor_report: doctorReport,
|
|
3120
|
-
};
|
|
3121
|
-
}
|
|
3122
|
-
|
|
3123
|
-
function printDeliveryMapReport(report, json) {
|
|
3124
|
-
if (json) {
|
|
3125
|
-
console.log(JSON.stringify(report, null, 2));
|
|
3126
|
-
return;
|
|
3127
|
-
}
|
|
3128
|
-
|
|
3129
|
-
console.log(`AGDF delivery-map: ${report.status}`);
|
|
3130
|
-
console.log(`Current gate: ${report.current_gate}`);
|
|
3131
|
-
console.log(`Next allowed action: ${report.next_allowed_action}`);
|
|
3132
|
-
console.log(`Quality outlook: ${report.quality_outlook}`);
|
|
3133
|
-
console.log(`Relationships: ${report.relationships.filter((item) => item.status === "pass").length}/${report.relationships.length} evidenced`);
|
|
3134
|
-
console.log(`Findings: ${report.findings.length}`);
|
|
3135
|
-
|
|
3136
|
-
for (const finding of report.findings) {
|
|
3137
|
-
console.log("");
|
|
3138
|
-
console.log(`[${finding.severity}] ${finding.code}`);
|
|
3139
|
-
console.log(finding.message);
|
|
3140
|
-
console.log(`Next step: ${finding.next_step}`);
|
|
3141
|
-
}
|
|
3142
|
-
}
|
|
3143
|
-
|
|
3144
|
-
async function executeDeliveryPathSearch(options) {
|
|
3145
|
-
const fixture = options.fixture ? JSON.parse(readFileSync(options.fixture, "utf8")) : null;
|
|
3146
|
-
const enforcement = fixture?.input?.enforcement ?? enforcementForSurface(options.surface);
|
|
3147
|
-
const fixtureGeneration = fixture?.generator_response;
|
|
3148
|
-
const generationEnabled = Boolean(fixtureGeneration || options.generateCandidates);
|
|
3149
|
-
const baseInput = fixture?.input ?? searchInputFromControl(options.dir, {
|
|
3150
|
-
scopeKey: options.runId,
|
|
3151
|
-
enforcement,
|
|
3152
|
-
generation: generationEnabled ? {
|
|
3153
|
-
enabled: true,
|
|
3154
|
-
maxProposals: options.maxGeneratedCandidates,
|
|
3155
|
-
maxDurationMs: options.generationTimeoutMs,
|
|
3156
|
-
maxCostUnits: options.generationCostUnits,
|
|
3157
|
-
} : undefined,
|
|
3158
|
-
});
|
|
3159
|
-
const input = generationEnabled && !baseInput.generation ? {
|
|
3160
|
-
...baseInput,
|
|
3161
|
-
generation: {
|
|
3162
|
-
enabled: true,
|
|
3163
|
-
max_calls: 1,
|
|
3164
|
-
max_proposals: options.maxGeneratedCandidates,
|
|
3165
|
-
max_duration_ms: options.generationTimeoutMs,
|
|
3166
|
-
max_cost_units: options.generationCostUnits,
|
|
3167
|
-
},
|
|
3168
|
-
} : baseInput;
|
|
3169
|
-
const evaluator = fixture
|
|
3170
|
-
? fixtureEvaluator(fixture.evaluations ?? {})
|
|
3171
|
-
: options.surface === "codex"
|
|
3172
|
-
? codexEvaluator({ cwd: options.dir, model: options.model })
|
|
3173
|
-
: options.surface === "claude"
|
|
3174
|
-
? claudeEvaluator({ cwd: options.dir, model: options.model })
|
|
3175
|
-
: null;
|
|
3176
|
-
if (!evaluator) {
|
|
3177
|
-
throw new Error(`${options.surface} has no executable evaluator in this release. Codex and Claude are the reference adapters; --fixture is available for deterministic contract testing.`);
|
|
3178
|
-
}
|
|
3179
|
-
const generator = fixtureGeneration
|
|
3180
|
-
? fixtureGenerator(fixtureGeneration)
|
|
3181
|
-
: options.generateCandidates && options.surface === "codex"
|
|
3182
|
-
? codexGenerator({ cwd: options.dir, model: options.generatorModel, timeoutMs: options.generationTimeoutMs })
|
|
3183
|
-
: options.generateCandidates && options.surface === "claude"
|
|
3184
|
-
? claudeGenerator({ cwd: options.dir, model: options.generatorModel, timeoutMs: options.generationTimeoutMs })
|
|
3185
|
-
: null;
|
|
3186
|
-
if (options.generateCandidates && !generator) {
|
|
3187
|
-
throw new Error(`${options.surface} has no executable candidate generator; Codex and Claude are tool-enforced reference transports.`);
|
|
3188
|
-
}
|
|
3189
|
-
const result = await runDeliveryPathSearch(input, evaluator, { candidates: fixture?.candidates, generator });
|
|
3190
|
-
if (options.persist) result.persistence = persistSearchResult(options.dir, result);
|
|
3191
|
-
if (options.json) {
|
|
3192
|
-
console.log(JSON.stringify(result, null, 2));
|
|
3193
|
-
return result;
|
|
3194
|
-
}
|
|
3195
|
-
console.log(`AGDF Delivery Path Search: ${result.status}`);
|
|
3196
|
-
console.log(`Current gate: ${result.current_gate}`);
|
|
3197
|
-
console.log(`Enforcement: ${result.enforcement.level}`);
|
|
3198
|
-
console.log(`Recommendation: ${result.recommendation?.action ?? "none"}`);
|
|
3199
|
-
console.log(`Evaluations: ${result.budgets.evaluations}`);
|
|
3200
|
-
console.log(`Generation: ${result.generation.status} (${result.generation.accepted}/${result.generation.returned} accepted)`);
|
|
3201
|
-
console.log(`Generation budget: ${result.generation.cost_units} cost units, ${result.generation.duration_ms} ms`);
|
|
3202
|
-
if (result.generation.failure_code) console.log(`Generation failure: ${result.generation.failure_code}`);
|
|
3203
|
-
console.log(`Stopping reason: ${result.stopping_reason}`);
|
|
3204
|
-
console.log(result.next_gate_action);
|
|
3205
|
-
return result;
|
|
3206
|
-
}
|
|
3207
|
-
|
|
3208
|
-
async function main() {
|
|
3209
|
-
const options = parseArgs(process.argv.slice(2));
|
|
3210
|
-
|
|
3211
|
-
if (
|
|
3212
|
-
options.allActive &&
|
|
3213
|
-
!["doctor", "delivery-map"].includes(options.target)
|
|
3214
|
-
) {
|
|
3215
|
-
throw new Error(
|
|
3216
|
-
"--all-active is supported only by doctor and delivery-map",
|
|
3217
|
-
);
|
|
3218
|
-
}
|
|
3219
|
-
|
|
3220
|
-
if (options.target === "run-create") {
|
|
3221
|
-
if (!options.runId || options.allActive)
|
|
3222
|
-
throw new Error("run-create requires --run and rejects --all-active");
|
|
3223
|
-
console.log(createRun(options.dir, options.runId));
|
|
3224
|
-
return;
|
|
3225
|
-
}
|
|
3226
|
-
if (options.target === "run-migrate") {
|
|
3227
|
-
console.log(
|
|
3228
|
-
JSON.stringify(migrateLegacy(options.dir, options.runId), null, 2),
|
|
3229
|
-
);
|
|
3230
|
-
return;
|
|
3231
|
-
}
|
|
3232
|
-
if (options.target === "run-render-legacy") {
|
|
3233
|
-
if (!options.runId) throw new Error("run-render-legacy requires --run");
|
|
3234
|
-
const selected = resolveRuns(options.dir, { runIdArg: options.runId });
|
|
3235
|
-
const output = join(options.dir, ".agdf", "control", "AGDF_RUN.md");
|
|
3236
|
-
writeLegacyProjection(output, selected.run.path);
|
|
3237
|
-
console.log(output);
|
|
3238
|
-
return;
|
|
3239
|
-
}
|
|
3240
|
-
|
|
3241
|
-
if (options.target === "doctor") {
|
|
3242
|
-
const report = evaluateDoctor(options.dir, options);
|
|
3243
|
-
printDoctorReport(report, options.json);
|
|
3244
|
-
process.exit(report.status === "block" ? 2 : 0);
|
|
3245
|
-
}
|
|
3246
|
-
|
|
3247
|
-
if (options.target === "gate-check") {
|
|
3248
|
-
const report = evaluateGateCheck(options.dir, options);
|
|
3249
|
-
printGateCheckReport(report, options.json, options.statusCard);
|
|
3250
|
-
process.exit(report.status === "blocked" ? 2 : 0);
|
|
3251
|
-
}
|
|
3252
|
-
|
|
3253
|
-
if (options.target === "delivery-map") {
|
|
3254
|
-
const report = evaluateDeliveryMap(options.dir, options);
|
|
3255
|
-
printDeliveryMapReport(report, options.json);
|
|
3256
|
-
process.exit(report.status === "block" ? 2 : 0);
|
|
3257
|
-
}
|
|
3258
|
-
|
|
3259
|
-
if (options.target === "delivery-path-search") {
|
|
3260
|
-
try {
|
|
3261
|
-
const result = await executeDeliveryPathSearch(options);
|
|
3262
|
-
process.exitCode = result.status === "recommendation" ? 0 : 2;
|
|
3263
|
-
} catch (error) {
|
|
3264
|
-
console.error(`Delivery Path Search failed: ${error.message}`);
|
|
3265
|
-
process.exitCode = 2;
|
|
3266
|
-
}
|
|
3267
|
-
return;
|
|
3268
|
-
}
|
|
3269
|
-
|
|
3270
|
-
if (options.target === "opencode-status") {
|
|
3271
|
-
const configDir = process.env.OPENCODE_CONFIG_DIR || defaultOpenCodeConfigDir();
|
|
3272
|
-
const report = evaluateOpenCodeStatus(options.dir, configDir);
|
|
3273
|
-
printOpenCodeStatus(report, options.json);
|
|
3274
|
-
process.exit(report.status === "configured" ? 0 : 1);
|
|
3275
|
-
}
|
|
3276
|
-
|
|
3277
|
-
if (options.target === "codex") {
|
|
3278
|
-
try {
|
|
3279
|
-
installCodexGlobalPlugin();
|
|
3280
|
-
console.log("AGDF Codex plugin installed globally.");
|
|
3281
|
-
console.log("Run npx --yes @agdf/cli@latest codex-repo in a repository when you want to test AGDF from repository-local plugin files.");
|
|
3282
|
-
} catch (error) {
|
|
3283
|
-
console.error(error.message);
|
|
3284
|
-
process.exit(1);
|
|
3285
|
-
}
|
|
3286
|
-
return;
|
|
3287
|
-
}
|
|
3288
|
-
|
|
3289
|
-
if (options.target === "claude") {
|
|
3290
|
-
try {
|
|
3291
|
-
installClaudeGlobalPlugin();
|
|
3292
|
-
console.log("AGDF Claude Code plugin installed globally.");
|
|
3293
|
-
} catch (error) {
|
|
3294
|
-
console.error(error.message);
|
|
3295
|
-
process.exit(1);
|
|
3296
|
-
}
|
|
3297
|
-
return;
|
|
3298
|
-
}
|
|
3299
|
-
|
|
3300
|
-
if (options.target === "opencode") {
|
|
3301
|
-
const configDir = options.dirExplicit ? options.dir : defaultOpenCodeConfigDir();
|
|
3302
|
-
try {
|
|
3303
|
-
const result = installOpenCodeGlobalPlugin(configDir);
|
|
3304
|
-
installOpenCodeGlobalSurface(configDir);
|
|
3305
|
-
console.log(`AGDF OpenCode global plugin ${result.added ? "installed" : "already present"}: ${result.configPath}`);
|
|
3306
|
-
const report = evaluateOpenCodeStatus(options.dir, configDir, result.transition);
|
|
3307
|
-
printOpenCodeStatus(report, false);
|
|
3308
|
-
console.log("Restart OpenCode so it loads the updated global plugin config.");
|
|
3309
|
-
} catch (error) {
|
|
3310
|
-
console.error(error.message);
|
|
3311
|
-
process.exit(1);
|
|
3312
|
-
}
|
|
3313
|
-
return;
|
|
3314
|
-
}
|
|
3315
|
-
|
|
3316
|
-
const files = generatedFilesForTarget(options.target, options.dir, options.force, options.language);
|
|
3317
|
-
const wroteAgentsFragment = files.some(file => file.path === agdfFragmentPath);
|
|
3318
|
-
const wroteOpenCodeConfigFragment = files.some(file => file.path === openCodeConfigFragmentPath);
|
|
3319
|
-
let removedOpenCodeAgents = [];
|
|
3320
|
-
|
|
3321
|
-
try {
|
|
3322
|
-
assertGeneratedWritePlan(options.dir, files, options.force);
|
|
3323
|
-
for (const file of files) {
|
|
3324
|
-
writeGeneratedFile(options.dir, file.path, file.content, options.force, file.allowOverwrite);
|
|
3325
|
-
}
|
|
3326
|
-
if (options.target === "opencode-repo") {
|
|
3327
|
-
removedOpenCodeAgents = removeOwnedLegacyOpenCodeAgents(options.dir);
|
|
3328
|
-
}
|
|
3329
|
-
} catch (error) {
|
|
3330
|
-
console.error(error.message);
|
|
3331
|
-
process.exit(1);
|
|
3332
|
-
}
|
|
3333
|
-
|
|
3334
|
-
printNextSteps(options.target, options.dir, files, wroteAgentsFragment, wroteOpenCodeConfigFragment, removedOpenCodeAgents);
|
|
3335
|
-
}
|
|
4
|
+
import { main } from "../lib/cli/application.js";
|
|
3336
5
|
|
|
3337
6
|
try {
|
|
3338
|
-
await main();
|
|
7
|
+
process.exitCode = await main(process.argv.slice(2));
|
|
3339
8
|
} catch (error) {
|
|
3340
9
|
console.error(error instanceof Error ? error.message : String(error));
|
|
3341
10
|
process.exitCode = 1;
|