create-agdf 0.1.7 → 0.1.9
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 +35 -2
- package/bin/create-agdf.js +475 -3
- package/generated/.agdf/control/README.md +22 -5
- package/generated/.github/skills/agdf-gate-check/SKILL.md +13 -0
- package/generated/plugins/agdf/.codex-plugin/plugin.json +1 -1
- package/generated/plugins/agdf/control/README.md +22 -5
- package/generated/plugins/agdf/meta/agdf-plugin.definition.json +1 -1
- package/generated/plugins/agdf/skills/gate-check/SKILL.md +13 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,6 +8,9 @@ Bootstrap AGDF repository instructions for one repository.
|
|
|
8
8
|
npm create agdf@latest codex
|
|
9
9
|
npm create agdf@latest copilot
|
|
10
10
|
npm create agdf@latest both
|
|
11
|
+
npm create agdf@latest init
|
|
12
|
+
npm create agdf@latest doctor
|
|
13
|
+
npm create agdf@latest gate-check
|
|
11
14
|
```
|
|
12
15
|
|
|
13
16
|
Optional flags:
|
|
@@ -29,7 +32,7 @@ After `npm create agdf@latest codex`, restart Codex in that repository, open `/p
|
|
|
29
32
|
|
|
30
33
|
## Control scaffold
|
|
31
34
|
|
|
32
|
-
The generated `.agdf/control/templates/` files are starting points for durable AGDF state:
|
|
35
|
+
The generated `.agdf/control/templates/` files are reusable starting points for durable AGDF state:
|
|
33
36
|
|
|
34
37
|
- `AGDF_RUN.md` for the current run dashboard
|
|
35
38
|
- `MASTER_BACKLOG.md` for active delivery pointers
|
|
@@ -37,7 +40,37 @@ The generated `.agdf/control/templates/` files are starting points for durable A
|
|
|
37
40
|
- `CONTEXT_GRAPH.md` for durable Brownfield findings, decisions, risks, evidence and exit criteria
|
|
38
41
|
- `AGENT_QUALITY_CONTRACTS.json` for reusable block, revise and warning conditions
|
|
39
42
|
|
|
40
|
-
|
|
43
|
+
Use `init` to promote those templates into live control files:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npm create agdf@latest init
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
This writes:
|
|
50
|
+
|
|
51
|
+
- `.agdf/control/AGDF_RUN.md`
|
|
52
|
+
- `.agdf/control/MASTER_BACKLOG.md`
|
|
53
|
+
- `.agdf/control/SOT_REGISTRY.md`
|
|
54
|
+
- `.agdf/control/CONTEXT_GRAPH.md`
|
|
55
|
+
- `.agdf/control/AGENT_QUALITY_CONTRACTS.json`
|
|
56
|
+
|
|
57
|
+
Use `doctor` to check whether the live control state is actionable:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
npm create agdf@latest doctor
|
|
61
|
+
npm create agdf@latest doctor --json
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The doctor reports missing live control files, missing current gate, missing next allowed action, empty evidence, empty backlog pointer, empty source-of-truth registry, duplicate active SoT rows and invalid quality contracts. It exits non-zero only for blocking control failures.
|
|
65
|
+
|
|
66
|
+
Use `gate-check` to derive the next process decision from the doctor result and `AGDF_RUN.md`:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
npm create agdf@latest gate-check
|
|
70
|
+
npm create agdf@latest gate-check --json
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The gate check reports `open | blocked`, the current gate, blocking reason, missing exact approval, allowed outputs, forbidden outputs, next allowed action, evidence references and the embedded doctor report.
|
|
41
74
|
|
|
42
75
|
## Single source of truth
|
|
43
76
|
|
package/bin/create-agdf.js
CHANGED
|
@@ -11,8 +11,9 @@ const generatedRoot = join(packageRoot, "generated");
|
|
|
11
11
|
const pluginDefinitionPath = join(generatedRoot, "plugins", "agdf", "meta", "agdf-plugin.definition.json");
|
|
12
12
|
const pluginDefinition = JSON.parse(readFileSync(pluginDefinitionPath, "utf8"));
|
|
13
13
|
const pluginInstallCommand = "claude plugin add arndtgold/ai-native-governance-delivery-framework";
|
|
14
|
-
const allowedTargets = new Set(["codex", "copilot", "both"]);
|
|
14
|
+
const allowedTargets = new Set(["codex", "copilot", "both", "init", "doctor", "gate-check"]);
|
|
15
15
|
const agdfFragmentPath = "AGENTS.agdf.md";
|
|
16
|
+
const userGateOrder = ["UR", "PRD", "SD", "TP", "QA", "UAT"];
|
|
16
17
|
const codexSkillNames = pluginDefinition.skillSet.map((skill) => `${pluginDefinition.codex.skillPrefix}${skill.slug}`);
|
|
17
18
|
const copilotSkillNames = pluginDefinition.skillSet.map((skill) => `${pluginDefinition.copilot.skillPrefix}${skill.slug}`);
|
|
18
19
|
const codexPluginFiles = [
|
|
@@ -41,6 +42,35 @@ const controlFiles = [
|
|
|
41
42
|
join(".agdf", "control", "templates", "CONTEXT_GRAPH.md"),
|
|
42
43
|
join(".agdf", "control", "templates", "AGENT_QUALITY_CONTRACTS.json"),
|
|
43
44
|
];
|
|
45
|
+
const liveControlFiles = [
|
|
46
|
+
{
|
|
47
|
+
path: join(".agdf", "control", "AGDF_RUN.md"),
|
|
48
|
+
source: join(".agdf", "control", "templates", "AGDF_RUN.md"),
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
path: join(".agdf", "control", "MASTER_BACKLOG.md"),
|
|
52
|
+
source: join(".agdf", "control", "templates", "MASTER_BACKLOG.md"),
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
path: join(".agdf", "control", "SOT_REGISTRY.md"),
|
|
56
|
+
source: join(".agdf", "control", "templates", "SOT_REGISTRY.md"),
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
path: join(".agdf", "control", "CONTEXT_GRAPH.md"),
|
|
60
|
+
source: join(".agdf", "control", "templates", "CONTEXT_GRAPH.md"),
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
path: join(".agdf", "control", "AGENT_QUALITY_CONTRACTS.json"),
|
|
64
|
+
source: join(".agdf", "control", "templates", "AGENT_QUALITY_CONTRACTS.json"),
|
|
65
|
+
},
|
|
66
|
+
];
|
|
67
|
+
const doctorRequiredFiles = [
|
|
68
|
+
join(".agdf", "control", "AGDF_RUN.md"),
|
|
69
|
+
join(".agdf", "control", "MASTER_BACKLOG.md"),
|
|
70
|
+
join(".agdf", "control", "SOT_REGISTRY.md"),
|
|
71
|
+
join(".agdf", "control", "CONTEXT_GRAPH.md"),
|
|
72
|
+
join(".agdf", "control", "AGENT_QUALITY_CONTRACTS.json"),
|
|
73
|
+
];
|
|
44
74
|
const copilotInstructionFiles = [
|
|
45
75
|
join(".github", "copilot-instructions.md"),
|
|
46
76
|
join(".github", "instructions", "agdf-governance.instructions.md"),
|
|
@@ -58,10 +88,14 @@ Usage:
|
|
|
58
88
|
npm create agdf@latest codex
|
|
59
89
|
npm create agdf@latest copilot
|
|
60
90
|
npm create agdf@latest both
|
|
91
|
+
npm create agdf@latest init
|
|
92
|
+
npm create agdf@latest doctor
|
|
93
|
+
npm create agdf@latest gate-check
|
|
61
94
|
|
|
62
95
|
Options:
|
|
63
96
|
--dir <path> Write files into a specific directory
|
|
64
97
|
--force Overwrite existing generated files
|
|
98
|
+
--json Print doctor or gate-check output as JSON
|
|
65
99
|
--help Show this help
|
|
66
100
|
`);
|
|
67
101
|
}
|
|
@@ -71,6 +105,7 @@ function parseArgs(argv) {
|
|
|
71
105
|
let target;
|
|
72
106
|
let dir = ".";
|
|
73
107
|
let force = false;
|
|
108
|
+
let json = false;
|
|
74
109
|
|
|
75
110
|
for (let i = 0; i < args.length; i += 1) {
|
|
76
111
|
const arg = args[i];
|
|
@@ -86,6 +121,11 @@ function parseArgs(argv) {
|
|
|
86
121
|
continue;
|
|
87
122
|
}
|
|
88
123
|
|
|
124
|
+
if (arg === "--json") {
|
|
125
|
+
json = true;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
|
|
89
129
|
if (arg === "--dir") {
|
|
90
130
|
const next = args[i + 1];
|
|
91
131
|
if (!next) {
|
|
@@ -118,7 +158,7 @@ function parseArgs(argv) {
|
|
|
118
158
|
}
|
|
119
159
|
|
|
120
160
|
if (!target || !allowedTargets.has(target)) {
|
|
121
|
-
console.error("Please choose one target: codex, copilot or
|
|
161
|
+
console.error("Please choose one target: codex, copilot, both, init, doctor or gate-check.");
|
|
122
162
|
printUsage();
|
|
123
163
|
process.exit(1);
|
|
124
164
|
}
|
|
@@ -127,6 +167,7 @@ function parseArgs(argv) {
|
|
|
127
167
|
target,
|
|
128
168
|
dir: resolve(process.cwd(), dir),
|
|
129
169
|
force,
|
|
170
|
+
json,
|
|
130
171
|
};
|
|
131
172
|
}
|
|
132
173
|
|
|
@@ -148,6 +189,22 @@ function writeGeneratedFile(targetDir, relativePath, content, force) {
|
|
|
148
189
|
function generatedFilesForTarget(target, targetDir, force) {
|
|
149
190
|
const files = [];
|
|
150
191
|
|
|
192
|
+
if (target === "init") {
|
|
193
|
+
files.push({
|
|
194
|
+
path: join(".agdf", "control", "README.md"),
|
|
195
|
+
content: loadAsset(join(".agdf", "control", "README.md")),
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
for (const controlPath of liveControlFiles) {
|
|
199
|
+
files.push({
|
|
200
|
+
path: controlPath.path,
|
|
201
|
+
content: loadAsset(controlPath.source),
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return files;
|
|
206
|
+
}
|
|
207
|
+
|
|
151
208
|
if (target === "codex" || target === "both") {
|
|
152
209
|
for (const codexPath of codexPluginFiles) {
|
|
153
210
|
files.push({
|
|
@@ -200,6 +257,12 @@ function printNextSteps(target, destination, files, wroteAgentsFragment) {
|
|
|
200
257
|
|
|
201
258
|
console.log("");
|
|
202
259
|
console.log("Next steps:");
|
|
260
|
+
if (target === "init") {
|
|
261
|
+
console.log("- Fill .agdf/control/AGDF_RUN.md with the current gate, evidence and next allowed action.");
|
|
262
|
+
console.log("- Run npm create agdf@latest doctor to check the control state before the next agent run.");
|
|
263
|
+
console.log("- Commit the live control files once they represent the repository's current delivery state.");
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
203
266
|
if (wroteAgentsFragment) {
|
|
204
267
|
console.log(`- Existing AGENTS.md detected. Merge ${agdfFragmentPath} into your current AGENTS.md before using Copilot with AGDF.`);
|
|
205
268
|
}
|
|
@@ -212,13 +275,422 @@ function printNextSteps(target, destination, files, wroteAgentsFragment) {
|
|
|
212
275
|
}
|
|
213
276
|
if (target === "copilot" || target === "both") {
|
|
214
277
|
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.");
|
|
215
|
-
console.log("-
|
|
278
|
+
console.log("- Run npm create agdf@latest init when the repository needs live AGDF control files.");
|
|
216
279
|
}
|
|
217
280
|
console.log("- Commit the generated files so the repository becomes the source of truth.");
|
|
218
281
|
}
|
|
219
282
|
|
|
283
|
+
function readTargetFile(targetDir, relativePath) {
|
|
284
|
+
return readFileSync(join(targetDir, relativePath), "utf8");
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function addFinding(findings, severity, code, message, path, nextStep) {
|
|
288
|
+
findings.push({
|
|
289
|
+
severity,
|
|
290
|
+
code,
|
|
291
|
+
message,
|
|
292
|
+
path,
|
|
293
|
+
next_step: nextStep,
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function nonEmptyTableRows(content) {
|
|
298
|
+
return content
|
|
299
|
+
.split("\n")
|
|
300
|
+
.filter((line) => line.trim().startsWith("|"))
|
|
301
|
+
.filter((line) => !line.includes("---"))
|
|
302
|
+
.map((line) => line.split("|").map((cell) => cell.trim()).filter(Boolean))
|
|
303
|
+
.filter((cells) => cells.some((cell) => cell && !cell.startsWith("`")));
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function tableRows(content) {
|
|
307
|
+
return content
|
|
308
|
+
.split("\n")
|
|
309
|
+
.filter((line) => line.trim().startsWith("|"))
|
|
310
|
+
.filter((line) => !line.includes("---"))
|
|
311
|
+
.map((line) => {
|
|
312
|
+
const cells = line.split("|").map((cell) => cell.trim());
|
|
313
|
+
if (cells[0] === "") cells.shift();
|
|
314
|
+
if (cells.at(-1) === "") cells.pop();
|
|
315
|
+
return cells;
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function hasFilledTableRow(content, firstCellPattern) {
|
|
320
|
+
return tableRows(content)
|
|
321
|
+
.filter((cells) => firstCellPattern.test(cells[0] ?? ""))
|
|
322
|
+
.some((cells) => cells.slice(1).some((cell) => cell && !cell.startsWith("`")));
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function isPlaceholderValue(value) {
|
|
326
|
+
return !value || (value.startsWith("`") && value.includes("|"));
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function hasFilledEvidenceRow(content) {
|
|
330
|
+
const evidenceSection = content.match(/## Evidence([\s\S]*?)(?:\n## |\n# |$)/)?.[1] ?? "";
|
|
331
|
+
return tableRows(evidenceSection)
|
|
332
|
+
.filter((cells) => cells[0] !== "Evidence")
|
|
333
|
+
.some((cells) => {
|
|
334
|
+
const [evidence, source, covers] = cells;
|
|
335
|
+
return Boolean(evidence || source || covers);
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function parseQualityContracts(content) {
|
|
340
|
+
const parsed = JSON.parse(content);
|
|
341
|
+
if (!Array.isArray(parsed.contracts) || parsed.contracts.length === 0) {
|
|
342
|
+
throw new Error("contracts must be a non-empty array");
|
|
343
|
+
}
|
|
344
|
+
for (const contract of parsed.contracts) {
|
|
345
|
+
if (!contract.code || !contract.impact || !contract.required_evidence) {
|
|
346
|
+
throw new Error(`contract ${contract.code ?? "<unknown>"} is missing code, impact or required_evidence`);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return parsed;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function evaluateDoctor(targetDir) {
|
|
353
|
+
const findings = [];
|
|
354
|
+
const missing = doctorRequiredFiles.filter((relativePath) => !existsSync(join(targetDir, relativePath)));
|
|
355
|
+
|
|
356
|
+
for (const relativePath of missing) {
|
|
357
|
+
const templatePath = join(dirname(relativePath), "templates", relativePath.split("/").at(-1));
|
|
358
|
+
const hasTemplate = existsSync(join(targetDir, templatePath));
|
|
359
|
+
addFinding(
|
|
360
|
+
findings,
|
|
361
|
+
"block",
|
|
362
|
+
"AGDF_CONTROL_FILE_MISSING",
|
|
363
|
+
hasTemplate
|
|
364
|
+
? "Live control file is missing; only the template exists."
|
|
365
|
+
: "Required live control file is missing.",
|
|
366
|
+
relativePath,
|
|
367
|
+
"Run npm create agdf@latest init, then fill the live control files with the current repository state.",
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (missing.length === 0) {
|
|
372
|
+
const runPath = join(".agdf", "control", "AGDF_RUN.md");
|
|
373
|
+
const run = readTargetFile(targetDir, runPath);
|
|
374
|
+
const currentGateLine = run.match(/^- current_gate:[^\S\r\n]*(.*)$/m)?.[1]?.trim() ?? "";
|
|
375
|
+
const nextActionLine = run.match(/^- next_allowed_action:[^\S\r\n]*(.*)$/m)?.[1]?.trim() ?? "";
|
|
376
|
+
const hasEvidence = hasFilledEvidenceRow(run);
|
|
377
|
+
|
|
378
|
+
if (isPlaceholderValue(currentGateLine)) {
|
|
379
|
+
addFinding(
|
|
380
|
+
findings,
|
|
381
|
+
"revise",
|
|
382
|
+
"AGDF_CURRENT_GATE_MISSING",
|
|
383
|
+
"AGDF_RUN.md does not name the current gate.",
|
|
384
|
+
runPath,
|
|
385
|
+
"Set current_gate to the current delivery gate or none.",
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (isPlaceholderValue(nextActionLine)) {
|
|
390
|
+
addFinding(
|
|
391
|
+
findings,
|
|
392
|
+
"revise",
|
|
393
|
+
"AGDF_NEXT_ALLOWED_ACTION_MISSING",
|
|
394
|
+
"AGDF_RUN.md does not state the next allowed action.",
|
|
395
|
+
runPath,
|
|
396
|
+
"Fill the next allowed action before asking an agent to continue delivery work.",
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (!hasEvidence) {
|
|
401
|
+
addFinding(
|
|
402
|
+
findings,
|
|
403
|
+
"warn",
|
|
404
|
+
"AGDF_EVIDENCE_EMPTY",
|
|
405
|
+
"AGDF_RUN.md has no visible evidence row yet.",
|
|
406
|
+
runPath,
|
|
407
|
+
"Add at least one evidence row or explicitly document that no evidence exists yet.",
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const backlogPath = join(".agdf", "control", "MASTER_BACKLOG.md");
|
|
412
|
+
const backlog = readTargetFile(targetDir, backlogPath);
|
|
413
|
+
if (!hasFilledTableRow(backlog, /^P[0-9]/)) {
|
|
414
|
+
addFinding(
|
|
415
|
+
findings,
|
|
416
|
+
"warn",
|
|
417
|
+
"AGDF_BACKLOG_POINTER_EMPTY",
|
|
418
|
+
"MASTER_BACKLOG.md does not contain an active or planned work pointer.",
|
|
419
|
+
backlogPath,
|
|
420
|
+
"Add the active item or document that no governed delivery item is active.",
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const sotPath = join(".agdf", "control", "SOT_REGISTRY.md");
|
|
425
|
+
const sot = readTargetFile(targetDir, sotPath);
|
|
426
|
+
if (!hasFilledTableRow(sot, /Product intent|Architecture|Runtime contracts|UX \/ user flows|Operations \/ release/)) {
|
|
427
|
+
addFinding(
|
|
428
|
+
findings,
|
|
429
|
+
"warn",
|
|
430
|
+
"AGDF_SOT_REGISTRY_EMPTY",
|
|
431
|
+
"SOT_REGISTRY.md has no filled primary source-of-truth row.",
|
|
432
|
+
sotPath,
|
|
433
|
+
"Assign one primary source of truth for at least the domains relevant to the next run.",
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const activeDomains = new Map();
|
|
438
|
+
for (const cells of nonEmptyTableRows(sot)) {
|
|
439
|
+
const [domain, document, status] = cells;
|
|
440
|
+
if (!domain || !document || status !== "active") continue;
|
|
441
|
+
activeDomains.set(domain, (activeDomains.get(domain) ?? 0) + 1);
|
|
442
|
+
}
|
|
443
|
+
for (const [domain, count] of activeDomains.entries()) {
|
|
444
|
+
if (count > 1) {
|
|
445
|
+
addFinding(
|
|
446
|
+
findings,
|
|
447
|
+
"block",
|
|
448
|
+
"AGDF_PARALLEL_SOT",
|
|
449
|
+
`Domain has ${count} active source-of-truth rows: ${domain}.`,
|
|
450
|
+
sotPath,
|
|
451
|
+
"Keep exactly one active primary source of truth for this domain.",
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const contextPath = join(".agdf", "control", "CONTEXT_GRAPH.md");
|
|
457
|
+
const contextGraph = readTargetFile(targetDir, contextPath);
|
|
458
|
+
if (contextGraph.includes("### CG-001 Example")) {
|
|
459
|
+
addFinding(
|
|
460
|
+
findings,
|
|
461
|
+
"warn",
|
|
462
|
+
"AGDF_CONTEXT_GRAPH_TEMPLATE_NODE",
|
|
463
|
+
"CONTEXT_GRAPH.md still contains the example node.",
|
|
464
|
+
contextPath,
|
|
465
|
+
"Remove the example or replace it with an evidenced node that has an exit criterion.",
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const contractsPath = join(".agdf", "control", "AGENT_QUALITY_CONTRACTS.json");
|
|
470
|
+
try {
|
|
471
|
+
parseQualityContracts(readTargetFile(targetDir, contractsPath));
|
|
472
|
+
} catch (error) {
|
|
473
|
+
addFinding(
|
|
474
|
+
findings,
|
|
475
|
+
"block",
|
|
476
|
+
"AGDF_QUALITY_CONTRACTS_INVALID",
|
|
477
|
+
`AGENT_QUALITY_CONTRACTS.json is invalid: ${error.message}`,
|
|
478
|
+
contractsPath,
|
|
479
|
+
"Restore the generated contracts or fix the JSON contract schema.",
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const severityRank = { block: 3, revise: 2, warn: 1 };
|
|
485
|
+
const maxSeverity = findings.reduce((max, finding) => Math.max(max, severityRank[finding.severity] ?? 0), 0);
|
|
486
|
+
const status = maxSeverity >= 3 ? "block" : maxSeverity === 2 ? "revise" : maxSeverity === 1 ? "warn" : "pass";
|
|
487
|
+
|
|
488
|
+
return {
|
|
489
|
+
schema_version: "1",
|
|
490
|
+
status,
|
|
491
|
+
checked_at: new Date().toISOString(),
|
|
492
|
+
target_dir: targetDir,
|
|
493
|
+
summary: {
|
|
494
|
+
findings: findings.length,
|
|
495
|
+
block: findings.filter((finding) => finding.severity === "block").length,
|
|
496
|
+
revise: findings.filter((finding) => finding.severity === "revise").length,
|
|
497
|
+
warn: findings.filter((finding) => finding.severity === "warn").length,
|
|
498
|
+
},
|
|
499
|
+
findings,
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function printDoctorReport(report, json) {
|
|
504
|
+
if (json) {
|
|
505
|
+
console.log(JSON.stringify(report, null, 2));
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
console.log(`AGDF doctor: ${report.status}`);
|
|
510
|
+
console.log(`Checked: ${report.target_dir}`);
|
|
511
|
+
console.log(`Findings: ${report.summary.findings} (${report.summary.block} block, ${report.summary.revise} revise, ${report.summary.warn} warn)`);
|
|
512
|
+
|
|
513
|
+
for (const finding of report.findings) {
|
|
514
|
+
console.log("");
|
|
515
|
+
console.log(`[${finding.severity}] ${finding.code}`);
|
|
516
|
+
console.log(`Path: ${finding.path}`);
|
|
517
|
+
console.log(finding.message);
|
|
518
|
+
console.log(`Next step: ${finding.next_step}`);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function extractField(content, field) {
|
|
523
|
+
const pattern = new RegExp(`^- ${field}:[^\\S\\r\\n]*(.*)$`, "m");
|
|
524
|
+
return content.match(pattern)?.[1]?.trim() ?? "";
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function cleanStatusCell(value) {
|
|
528
|
+
return value.replace(/^`|`$/g, "").trim();
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function readRunState(targetDir) {
|
|
532
|
+
const runPath = join(".agdf", "control", "AGDF_RUN.md");
|
|
533
|
+
if (!existsSync(join(targetDir, runPath))) {
|
|
534
|
+
return {
|
|
535
|
+
path: runPath,
|
|
536
|
+
content: "",
|
|
537
|
+
current_gate: "",
|
|
538
|
+
next_allowed_action: "",
|
|
539
|
+
approvals: new Map(),
|
|
540
|
+
evidence_refs: [],
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
const content = readTargetFile(targetDir, runPath);
|
|
545
|
+
const approvalsSection = content.match(/## Approvals([\s\S]*?)(?:\n## |\n# |$)/)?.[1] ?? "";
|
|
546
|
+
const approvals = new Map();
|
|
547
|
+
for (const cells of tableRows(approvalsSection)) {
|
|
548
|
+
const [gate, status, evidence] = cells;
|
|
549
|
+
if (!userGateOrder.includes(gate)) continue;
|
|
550
|
+
approvals.set(gate, {
|
|
551
|
+
status: cleanStatusCell(status ?? ""),
|
|
552
|
+
evidence: evidence ?? "",
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
const evidenceSection = content.match(/## Evidence([\s\S]*?)(?:\n## |\n# |$)/)?.[1] ?? "";
|
|
557
|
+
const evidence_refs = tableRows(evidenceSection)
|
|
558
|
+
.filter((cells) => cells[0] !== "Evidence")
|
|
559
|
+
.filter((cells) => Boolean(cells[0] || cells[1] || cells[2]))
|
|
560
|
+
.map((cells) => ({
|
|
561
|
+
evidence: cells[0] ?? "",
|
|
562
|
+
source: cells[1] ?? "",
|
|
563
|
+
covers: cells[2] ?? "",
|
|
564
|
+
strength: cleanStatusCell(cells[3] ?? ""),
|
|
565
|
+
}));
|
|
566
|
+
|
|
567
|
+
return {
|
|
568
|
+
path: runPath,
|
|
569
|
+
content,
|
|
570
|
+
current_gate: extractField(content, "current_gate"),
|
|
571
|
+
next_allowed_action: extractField(content, "next_allowed_action"),
|
|
572
|
+
approvals,
|
|
573
|
+
evidence_refs,
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function firstUnapprovedGate(approvals) {
|
|
578
|
+
for (const gate of userGateOrder) {
|
|
579
|
+
const status = approvals.get(gate)?.status ?? "";
|
|
580
|
+
if (status !== "approved" && status !== "not_applicable") return gate;
|
|
581
|
+
}
|
|
582
|
+
return "OR";
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function normalizeCurrentGate(value, fallbackGate) {
|
|
586
|
+
if (isPlaceholderValue(value)) return fallbackGate;
|
|
587
|
+
const normalized = value.replace(/`/g, "").trim();
|
|
588
|
+
return normalized || fallbackGate;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function gateApprovalStatus(runState, gate) {
|
|
592
|
+
if (!userGateOrder.includes(gate)) return "not_applicable";
|
|
593
|
+
return runState.approvals.get(gate)?.status ?? "";
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function evaluateGateCheck(targetDir) {
|
|
597
|
+
const doctorReport = evaluateDoctor(targetDir);
|
|
598
|
+
const runState = readRunState(targetDir);
|
|
599
|
+
const fallbackGate = firstUnapprovedGate(runState.approvals);
|
|
600
|
+
const currentGate = normalizeCurrentGate(runState.current_gate, fallbackGate);
|
|
601
|
+
const currentApprovalStatus = gateApprovalStatus(runState, currentGate);
|
|
602
|
+
const doctorBlocker = doctorReport.findings.find((finding) => finding.severity === "block");
|
|
603
|
+
const doctorRevise = doctorReport.findings.find((finding) => finding.severity === "revise");
|
|
604
|
+
|
|
605
|
+
let status = "open";
|
|
606
|
+
let blockingReason = "none";
|
|
607
|
+
let missingApproval = "none";
|
|
608
|
+
let allowed = [
|
|
609
|
+
"continue with the documented next allowed action",
|
|
610
|
+
"update evidence when new facts are observed",
|
|
611
|
+
"run doctor or gate-check again after control changes",
|
|
612
|
+
];
|
|
613
|
+
let forbidden = [
|
|
614
|
+
"bypass the documented gate state",
|
|
615
|
+
"treat warnings as resolved without evidence",
|
|
616
|
+
];
|
|
617
|
+
let nextAllowedAction = isPlaceholderValue(runState.next_allowed_action)
|
|
618
|
+
? "Continue with the current documented gate only after setting next_allowed_action."
|
|
619
|
+
: runState.next_allowed_action;
|
|
620
|
+
|
|
621
|
+
if (doctorBlocker) {
|
|
622
|
+
status = "blocked";
|
|
623
|
+
blockingReason = doctorBlocker.code;
|
|
624
|
+
allowed = ["repair the AGDF control scaffold", "run doctor again"];
|
|
625
|
+
forbidden = ["continue governed delivery", "create later-gate artefacts", "implement gated work"];
|
|
626
|
+
nextAllowedAction = doctorBlocker.next_step;
|
|
627
|
+
} else if (doctorRevise) {
|
|
628
|
+
status = "blocked";
|
|
629
|
+
blockingReason = doctorRevise.code;
|
|
630
|
+
allowed = ["repair the incomplete control state", "run doctor again"];
|
|
631
|
+
forbidden = ["continue governed delivery", "create later-gate artefacts", "implement gated work"];
|
|
632
|
+
nextAllowedAction = doctorRevise.next_step;
|
|
633
|
+
} else if (userGateOrder.includes(currentGate) && currentApprovalStatus !== "approved" && currentApprovalStatus !== "not_applicable") {
|
|
634
|
+
status = "blocked";
|
|
635
|
+
blockingReason = "missing_exact_approval";
|
|
636
|
+
missingApproval = `Approval: ${currentGate}`;
|
|
637
|
+
allowed = ["clarify the current gate", "record evidence", "write OR-lite status", "request exact approval"];
|
|
638
|
+
forbidden = ["create later-gate artefacts", "implement gated work", "mark QA or UAT as passed"];
|
|
639
|
+
nextAllowedAction = `Request exact approval: Approval: ${currentGate}`;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
return {
|
|
643
|
+
schema_version: "1",
|
|
644
|
+
status,
|
|
645
|
+
current_gate: currentGate,
|
|
646
|
+
blocking_reason: blockingReason,
|
|
647
|
+
missing_approval: missingApproval,
|
|
648
|
+
allowed,
|
|
649
|
+
forbidden,
|
|
650
|
+
next_allowed_action: nextAllowedAction,
|
|
651
|
+
doctor_status: doctorReport.status,
|
|
652
|
+
doctor_summary: doctorReport.summary,
|
|
653
|
+
evidence_refs: runState.evidence_refs,
|
|
654
|
+
doctor_report: doctorReport,
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function printGateCheckReport(report, json) {
|
|
659
|
+
if (json) {
|
|
660
|
+
console.log(JSON.stringify(report, null, 2));
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
console.log(`AGDF gate-check: ${report.status}`);
|
|
665
|
+
console.log(`Current gate: ${report.current_gate}`);
|
|
666
|
+
console.log(`Blocking reason: ${report.blocking_reason}`);
|
|
667
|
+
console.log(`Missing approval: ${report.missing_approval}`);
|
|
668
|
+
console.log(`Doctor: ${report.doctor_status} (${report.doctor_summary.findings} findings)`);
|
|
669
|
+
console.log("");
|
|
670
|
+
console.log("Allowed:");
|
|
671
|
+
for (const item of report.allowed) console.log(`- ${item}`);
|
|
672
|
+
console.log("");
|
|
673
|
+
console.log("Forbidden:");
|
|
674
|
+
for (const item of report.forbidden) console.log(`- ${item}`);
|
|
675
|
+
console.log("");
|
|
676
|
+
console.log(`Next allowed action: ${report.next_allowed_action}`);
|
|
677
|
+
}
|
|
678
|
+
|
|
220
679
|
function main() {
|
|
221
680
|
const options = parseArgs(process.argv.slice(2));
|
|
681
|
+
|
|
682
|
+
if (options.target === "doctor") {
|
|
683
|
+
const report = evaluateDoctor(options.dir);
|
|
684
|
+
printDoctorReport(report, options.json);
|
|
685
|
+
process.exit(report.status === "block" ? 2 : 0);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
if (options.target === "gate-check") {
|
|
689
|
+
const report = evaluateGateCheck(options.dir);
|
|
690
|
+
printGateCheckReport(report, options.json);
|
|
691
|
+
process.exit(report.status === "blocked" ? 2 : 0);
|
|
692
|
+
}
|
|
693
|
+
|
|
222
694
|
const files = generatedFilesForTarget(options.target, options.dir, options.force);
|
|
223
695
|
const wroteAgentsFragment = files.some(file => file.path === agdfFragmentPath);
|
|
224
696
|
|
|
@@ -8,11 +8,11 @@ Use this scaffold when a target repository should keep AGDF state outside chat h
|
|
|
8
8
|
|
|
9
9
|
| File | Purpose |
|
|
10
10
|
|---|---|
|
|
11
|
-
| `templates/AGDF_RUN.md` |
|
|
12
|
-
| `templates/MASTER_BACKLOG.md` |
|
|
13
|
-
| `templates/SOT_REGISTRY.md` |
|
|
14
|
-
| `templates/CONTEXT_GRAPH.md` |
|
|
15
|
-
| `templates/AGENT_QUALITY_CONTRACTS.json` |
|
|
11
|
+
| `templates/AGDF_RUN.md` | Template for current run state: mode, gate, approvals, evidence, risks, next allowed action |
|
|
12
|
+
| `templates/MASTER_BACKLOG.md` | Template for the living backlog pointer: active initiatives and current UR/PRD/SD/TP artefacts |
|
|
13
|
+
| `templates/SOT_REGISTRY.md` | Template for the source-of-truth registry: one primary owner per domain |
|
|
14
|
+
| `templates/CONTEXT_GRAPH.md` | Template for project memory: durable decisions, Brownfield findings, risks, evidence and exit criteria |
|
|
15
|
+
| `templates/AGENT_QUALITY_CONTRACTS.json` | Template for machine-readable warning, revise and block contracts for review and QA |
|
|
16
16
|
|
|
17
17
|
## Recommended Target Layout
|
|
18
18
|
|
|
@@ -28,6 +28,23 @@ Use this scaffold when a target repository should keep AGDF state outside chat h
|
|
|
28
28
|
|
|
29
29
|
Keep these files small and reviewable. They are control artefacts, not a second documentation site.
|
|
30
30
|
|
|
31
|
+
Create the live files with:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npm create agdf@latest init
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Then check whether the live state is actionable:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npm create agdf@latest doctor
|
|
41
|
+
npm create agdf@latest doctor --json
|
|
42
|
+
npm create agdf@latest gate-check --json
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`doctor` does not replace agent judgment. It catches basic control failures: missing live files, missing current gate, missing next allowed action, empty evidence, empty backlog pointer, empty source-of-truth registry, duplicate active SoT rows and invalid quality contracts.
|
|
46
|
+
`gate-check` consumes the doctor result and live run state to report whether the next process step is `open` or `blocked`.
|
|
47
|
+
|
|
31
48
|
## Operating Rules
|
|
32
49
|
|
|
33
50
|
- `AGDF_RUN.md` is the current run dashboard.
|
|
@@ -20,6 +20,19 @@ This skill must not create later artefacts such as PRD, SD, TP, CD, CR, QA, or U
|
|
|
20
20
|
## Runtime Contract
|
|
21
21
|
Use `../agdf-runtime-contract.md` for gate terms, closeout discipline, and non-duplication rules.
|
|
22
22
|
|
|
23
|
+
## Machine-Readable Control Path
|
|
24
|
+
|
|
25
|
+
When the target repository contains live AGDF control files, prefer the executable control path before deriving a gate decision manually:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm create agdf@latest doctor --json
|
|
29
|
+
npm create agdf@latest gate-check --json
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
`doctor` checks whether `.agdf/control/` is actionable. `agdf-gate-check` consumes that result and `AGDF_RUN.md` to report the operative process decision: `open | blocked`, current gate, blocking reason, missing approval, allowed outputs, forbidden outputs, next allowed action and evidence references.
|
|
33
|
+
|
|
34
|
+
The CLI report is evidence for this skill, not a second rule system. If the report says `blocked`, do not continue with later-gate artefacts until the reported blocker is resolved.
|
|
35
|
+
|
|
23
36
|
## Rules
|
|
24
37
|
1. Fail closed when a required approval or artefact status is missing.
|
|
25
38
|
2. The earliest blocking gate wins.
|
|
@@ -8,11 +8,11 @@ Use this scaffold when a target repository should keep AGDF state outside chat h
|
|
|
8
8
|
|
|
9
9
|
| File | Purpose |
|
|
10
10
|
|---|---|
|
|
11
|
-
| `templates/AGDF_RUN.md` |
|
|
12
|
-
| `templates/MASTER_BACKLOG.md` |
|
|
13
|
-
| `templates/SOT_REGISTRY.md` |
|
|
14
|
-
| `templates/CONTEXT_GRAPH.md` |
|
|
15
|
-
| `templates/AGENT_QUALITY_CONTRACTS.json` |
|
|
11
|
+
| `templates/AGDF_RUN.md` | Template for current run state: mode, gate, approvals, evidence, risks, next allowed action |
|
|
12
|
+
| `templates/MASTER_BACKLOG.md` | Template for the living backlog pointer: active initiatives and current UR/PRD/SD/TP artefacts |
|
|
13
|
+
| `templates/SOT_REGISTRY.md` | Template for the source-of-truth registry: one primary owner per domain |
|
|
14
|
+
| `templates/CONTEXT_GRAPH.md` | Template for project memory: durable decisions, Brownfield findings, risks, evidence and exit criteria |
|
|
15
|
+
| `templates/AGENT_QUALITY_CONTRACTS.json` | Template for machine-readable warning, revise and block contracts for review and QA |
|
|
16
16
|
|
|
17
17
|
## Recommended Target Layout
|
|
18
18
|
|
|
@@ -28,6 +28,23 @@ Use this scaffold when a target repository should keep AGDF state outside chat h
|
|
|
28
28
|
|
|
29
29
|
Keep these files small and reviewable. They are control artefacts, not a second documentation site.
|
|
30
30
|
|
|
31
|
+
Create the live files with:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npm create agdf@latest init
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Then check whether the live state is actionable:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npm create agdf@latest doctor
|
|
41
|
+
npm create agdf@latest doctor --json
|
|
42
|
+
npm create agdf@latest gate-check --json
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`doctor` does not replace agent judgment. It catches basic control failures: missing live files, missing current gate, missing next allowed action, empty evidence, empty backlog pointer, empty source-of-truth registry, duplicate active SoT rows and invalid quality contracts.
|
|
46
|
+
`gate-check` consumes the doctor result and live run state to report whether the next process step is `open` or `blocked`.
|
|
47
|
+
|
|
31
48
|
## Operating Rules
|
|
32
49
|
|
|
33
50
|
- `AGDF_RUN.md` is the current run dashboard.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"id": "agdf",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.9",
|
|
5
5
|
"displayName": "AI Governance & Delivery Framework",
|
|
6
6
|
"developerName": "AI-native Governance & Delivery Framework",
|
|
7
7
|
"description": "Codex-first workflow skills for gated, auditable AI-assisted software delivery.",
|
|
@@ -20,6 +20,19 @@ This skill must not create later artefacts such as PRD, SD, TP, CD, CR, QA, or U
|
|
|
20
20
|
## Runtime Contract
|
|
21
21
|
Use `../../meta/agdf-runtime-contract.md` for gate terms, closeout discipline, and non-duplication rules.
|
|
22
22
|
|
|
23
|
+
## Machine-Readable Control Path
|
|
24
|
+
|
|
25
|
+
When the target repository contains live AGDF control files, prefer the executable control path before deriving a gate decision manually:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm create agdf@latest doctor --json
|
|
29
|
+
npm create agdf@latest gate-check --json
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
`doctor` checks whether `.agdf/control/` is actionable. `gate-check` consumes that result and `AGDF_RUN.md` to report the operative process decision: `open | blocked`, current gate, blocking reason, missing approval, allowed outputs, forbidden outputs, next allowed action and evidence references.
|
|
33
|
+
|
|
34
|
+
The CLI report is evidence for this skill, not a second rule system. If the report says `blocked`, do not continue with later-gate artefacts until the reported blocker is resolved.
|
|
35
|
+
|
|
23
36
|
## Rules
|
|
24
37
|
1. Fail closed when a required approval or artefact status is missing.
|
|
25
38
|
2. The earliest blocking gate wins.
|