create-agdf 0.5.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/README.md +5 -4
  2. package/bin/create-agdf.js +210 -148
  3. package/generated/.agdf/control/README.md +15 -3
  4. package/generated/.agdf/control/templates/RUN_STATE.md +48 -0
  5. package/generated/.github/skills/agdf-brownfield-analysis/SKILL.md +3 -3
  6. package/generated/.github/skills/agdf-gate-check/SKILL.md +15 -10
  7. package/generated/.github/skills/agdf-release-or/SKILL.md +1 -1
  8. package/generated/.github/skills/agdf-runtime-contract.md +18 -8
  9. package/generated/.opencode/agdf-runtime-contract.md +18 -8
  10. package/generated/.opencode/agents/agdf-brownfield-analysis.md +2 -2
  11. package/generated/.opencode/agents/agdf-gate-check.md +15 -10
  12. package/generated/.opencode/agents/agdf-release-or.md +1 -1
  13. package/generated/plugins/agdf/.codex-plugin/plugin.json +1 -1
  14. package/generated/plugins/agdf/control/README.md +15 -3
  15. package/generated/plugins/agdf/control/templates/RUN_STATE.md +48 -0
  16. package/generated/plugins/agdf/hooks/session-start.sh +2 -0
  17. package/generated/plugins/agdf/meta/agdf-plugin.definition.json +1 -1
  18. package/generated/plugins/agdf/meta/agdf-runtime-contract.md +18 -8
  19. package/generated/plugins/agdf/skills/brownfield-analysis/SKILL.md +3 -3
  20. package/generated/plugins/agdf/skills/gate-check/SKILL.md +15 -10
  21. package/generated/plugins/agdf/skills/release-or/SKILL.md +1 -1
  22. package/lib/control-state/aggregate.js +18 -0
  23. package/lib/control-state/index.js +6 -0
  24. package/lib/control-state/legacy-migration.js +94 -0
  25. package/lib/control-state/run-state-parser.js +196 -0
  26. package/lib/control-state/run-state-repository.js +68 -0
  27. package/lib/control-state/run-state-resolver.js +45 -0
  28. package/lib/control-state/run-state-writer.js +96 -0
  29. package/lib/delivery-path-search/state-adapter.js +94 -23
  30. package/package.json +3 -2
package/README.md CHANGED
@@ -92,7 +92,7 @@ Using OpenCode's plugin installer or global config for `create-agdf` can add the
92
92
 
93
93
  The generated `.agdf/control/templates/` files are reusable starting points for durable AGDF state:
94
94
 
95
- - `AGDF_RUN.md` for the current run dashboard
95
+ - `runs/<run_id>/RUN_STATE.md` for each canonical current run dashboard
96
96
  - `MASTER_BACKLOG.md` for active delivery pointers
97
97
  - `templates/artefacts/` for durable UR, PRD, SD, TP and QA report artefact templates
98
98
  - `SOT_REGISTRY.md` for one source of truth per domain
@@ -107,7 +107,7 @@ npm create agdf@latest -- init
107
107
 
108
108
  This writes:
109
109
 
110
- - `.agdf/control/AGDF_RUN.md`
110
+ - `.agdf/control/runs/<run_id>/RUN_STATE.md`
111
111
  - `.agdf/control/MASTER_BACKLOG.md`
112
112
  - `.agdf/control/config.json`
113
113
  - `.agdf/control/templates/artefacts/UR.md`
@@ -138,7 +138,8 @@ npx --yes create-agdf@latest doctor --json
138
138
 
139
139
  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.
140
140
 
141
- Use `gate-check` to derive the next process decision from the doctor result and `AGDF_RUN.md`:
141
+ Use `gate-check` to derive the next process decision from the selected canonical
142
+ `.agdf/control/runs/<run_id>/RUN_STATE.md`. Use `--run <run_id>` or `AGDF_RUN_ID` when several runs are active:
142
143
 
143
144
  ```bash
144
145
  npm create agdf@latest -- gate-check
@@ -159,7 +160,7 @@ The runtime uses bounded best-first Delivery Path Search, not MCTS. It is read-o
159
160
 
160
161
  Requirements and boundaries:
161
162
 
162
- - run it only in a repository with live `.agdf/control/AGDF_RUN.md` state
163
+ - run it only with selected canonical `.agdf/control/runs/<run_id>/RUN_STATE.md` state
163
164
  - the current control state must expose legal next actions
164
165
  - Codex CLI must be installed and authenticated for `--surface codex`
165
166
  - Claude Code CLI must be installed and authenticated for `--surface claude`
@@ -16,6 +16,15 @@ import { claudeGenerator } from "../lib/delivery-path-search/generators/claude.j
16
16
  import { fixtureGenerator } from "../lib/delivery-path-search/generators/protocol.js";
17
17
  import { enforcementForSurface } from "../lib/delivery-path-search/surfaces/capabilities.js";
18
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";
19
28
 
20
29
  const __dirname = dirname(fileURLToPath(import.meta.url));
21
30
  const packageRoot = resolve(__dirname, "..");
@@ -25,7 +34,7 @@ const pluginDefinition = JSON.parse(readFileSync(pluginDefinitionPath, "utf8"));
25
34
  const pluginInstallCommand = "npx --yes @agdf/cli@latest claude";
26
35
  const npmCommand = process.platform === "win32" ? process.execPath : "npm";
27
36
  const npmPrefixArgs = process.platform === "win32" ? [join(dirname(process.execPath), "node_modules", "npm", "bin", "npm-cli.js")] : [];
28
- 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"]);
37
+ 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"]);
29
38
  const agdfFragmentPath = "AGENTS.agdf.md";
30
39
  const openCodeConfigFragmentPath = "opencode.agdf.json";
31
40
  const userGateOrder = ["UR", "PRD", "SD", "TP", "QA", "UAT"];
@@ -76,6 +85,7 @@ const codexPluginFiles = [
76
85
  join("plugins", "agdf", ".codex-plugin", "plugin.json"),
77
86
  join("plugins", "agdf", "control", "README.md"),
78
87
  join("plugins", "agdf", "control", "templates", "AGDF_RUN.md"),
88
+ join("plugins", "agdf", "control", "templates", "RUN_STATE.md"),
79
89
  join("plugins", "agdf", "control", "templates", "MASTER_BACKLOG.md"),
80
90
  join("plugins", "agdf", "control", "templates", "SOT_REGISTRY.md"),
81
91
  join("plugins", "agdf", "control", "templates", "CONTEXT_GRAPH.md"),
@@ -99,6 +109,7 @@ const codexPluginFiles = [
99
109
  const controlFiles = [
100
110
  join(".agdf", "control", "README.md"),
101
111
  join(".agdf", "control", "templates", "AGDF_RUN.md"),
112
+ join(".agdf", "control", "templates", "RUN_STATE.md"),
102
113
  join(".agdf", "control", "templates", "MASTER_BACKLOG.md"),
103
114
  join(".agdf", "control", "templates", "SOT_REGISTRY.md"),
104
115
  join(".agdf", "control", "templates", "CONTEXT_GRAPH.md"),
@@ -180,6 +191,9 @@ Preferred AGDF CLI:
180
191
  npx --yes @agdf/cli@latest gate-check --json
181
192
  npx --yes @agdf/cli@latest delivery-path-search --surface codex --json
182
193
  npx --yes @agdf/cli@latest delivery-path-search --surface claude --json
194
+ npx --yes @agdf/cli@latest run-create --run <run_id>
195
+ npx --yes @agdf/cli@latest run-migrate [--run <run_id>]
196
+ npx --yes @agdf/cli@latest run-render-legacy --run <run_id>
183
197
 
184
198
  Scaffold-compatible npm create usage:
185
199
  npm create agdf@latest -- codex
@@ -210,6 +224,8 @@ Options:
210
224
  --lang <de|en> Alias for --language
211
225
  --json Print machine-readable command output as JSON
212
226
  --status-card Print compact gate-check status-card output for interactive use
227
+ --run <run_id> Select one canonical run
228
+ --all-active Evaluate every active run (doctor and delivery-map only)
213
229
  --surface <codex|claude|copilot|opencode|generic>
214
230
  Declare the active Delivery Path Search surface
215
231
  --fixture <path>
@@ -296,6 +312,8 @@ function parseArgs(argv) {
296
312
  let maxGeneratedCandidates = 5;
297
313
  let generationTimeoutMs = 30000;
298
314
  let generationCostUnits = 5;
315
+ let runId;
316
+ let allActive = false;
299
317
 
300
318
  for (let i = 0; i < args.length; i += 1) {
301
319
  const arg = args[i];
@@ -331,6 +349,22 @@ function parseArgs(argv) {
331
349
  continue;
332
350
  }
333
351
 
352
+ if (arg === "--all-active") {
353
+ allActive = true;
354
+ continue;
355
+ }
356
+
357
+ if (arg === "--run") {
358
+ const next = args[i + 1];
359
+ if (!next) {
360
+ console.error("Missing value for --run");
361
+ process.exit(1);
362
+ }
363
+ runId = next;
364
+ i += 1;
365
+ continue;
366
+ }
367
+
334
368
  if (["--surface", "--fixture", "--model", "--generator-model", "--max-generated-candidates", "--generation-timeout-ms", "--generation-cost-units"].includes(arg)) {
335
369
  const next = args[i + 1];
336
370
  if (!next) {
@@ -428,6 +462,8 @@ function parseArgs(argv) {
428
462
  persist,
429
463
  model,
430
464
  generateCandidates,
465
+ runId,
466
+ allActive,
431
467
  generatorModel,
432
468
  maxGeneratedCandidates,
433
469
  generationTimeoutMs,
@@ -862,7 +898,7 @@ function printNextSteps(target, destination, files, wroteAgentsFragment, wroteOp
862
898
  console.log(`- AGDF language preference: artefacts=${language.artifact_language}, chat=${language.chat_language}, runtime=${language.runtime_language}.`);
863
899
  }
864
900
  if (target === "init") {
865
- console.log("- Fill .agdf/control/AGDF_RUN.md with the current gate, evidence and next allowed action.");
901
+ console.log("- Create or migrate a canonical run, then fill its RUN_STATE.md with the current gate, evidence and next allowed action.");
866
902
  console.log("- Run npx --yes @agdf/cli@latest doctor to check the control state before the next agent run.");
867
903
  console.log("- Commit the live control files once they represent the repository's current delivery state.");
868
904
  return;
@@ -1173,6 +1209,16 @@ function hasFilledEvidenceRow(content) {
1173
1209
  });
1174
1210
  }
1175
1211
 
1212
+ function allowNoActiveRuns(targetDir) {
1213
+ const configPath = join(targetDir, ".agdf", "control", "config.json");
1214
+ if (!existsSync(configPath)) return false;
1215
+ try {
1216
+ return JSON.parse(readFileSync(configPath, "utf8")).allow_no_active_runs === true;
1217
+ } catch {
1218
+ return false;
1219
+ }
1220
+ }
1221
+
1176
1222
  function markdownSection(content, heading) {
1177
1223
  const escaped = heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1178
1224
  return content.match(new RegExp(`(?:^|\\n)## ${escaped}\\s*\\n([\\s\\S]*?)(?:\\n## |\\n# |$)`))?.[1] ?? "";
@@ -1182,12 +1228,6 @@ function filled(value) {
1182
1228
  return Boolean(value && !isPlaceholderValue(value) && value.trim() !== "");
1183
1229
  }
1184
1230
 
1185
- function nonTemplateRows(section, headerCell) {
1186
- return tableRows(section)
1187
- .filter((cells) => cells[0] !== headerCell)
1188
- .filter((cells) => cells.some((cell) => filled(cell)));
1189
- }
1190
-
1191
1231
  function parseQualityContracts(content) {
1192
1232
  const parsed = JSON.parse(content);
1193
1233
  if (!Array.isArray(parsed.contracts) || parsed.contracts.length === 0) {
@@ -1201,9 +1241,61 @@ function parseQualityContracts(content) {
1201
1241
  return parsed;
1202
1242
  }
1203
1243
 
1204
- function evaluateDoctor(targetDir) {
1244
+ function evaluateDoctor(targetDir, selection = {}) {
1245
+ if (selection.allActive) {
1246
+ const selected = resolveRuns(targetDir, { allActive: true });
1247
+ const runs = selected.runs.map((run) => ({
1248
+ run_id: run.run_id,
1249
+ report: evaluateDoctor(targetDir, { runId: run.run_id }),
1250
+ }));
1251
+ const status = selected.findings.length
1252
+ ? "block"
1253
+ : aggregate(
1254
+ runs.map((item) => ({ run_id: item.run_id, status: item.report.status })),
1255
+ { allowNoActiveRuns: allowNoActiveRuns(targetDir) },
1256
+ ).status;
1257
+ const findings = [
1258
+ ...selected.findings.map((finding) => ({
1259
+ ...finding,
1260
+ severity: "block",
1261
+ message: "Invalid canonical run entry.",
1262
+ next_step: "Repair or remove the invalid run entry.",
1263
+ })),
1264
+ ...runs.flatMap((item) => item.report.findings.map((finding) => ({ ...finding, run_id: item.run_id }))),
1265
+ ];
1266
+ return {
1267
+ schema_version: "1",
1268
+ status,
1269
+ checked_at: new Date().toISOString(),
1270
+ target_dir: targetDir,
1271
+ summary: {
1272
+ findings: findings.length,
1273
+ block: findings.filter((finding) => finding.severity === "block").length,
1274
+ revise: findings.filter((finding) => finding.severity === "revise").length,
1275
+ warn: findings.filter((finding) => finding.severity === "warn").length,
1276
+ },
1277
+ findings,
1278
+ runs,
1279
+ };
1280
+ }
1205
1281
  const findings = [];
1206
- const missing = doctorRequiredFiles.filter((relativePath) => !existsSync(join(targetDir, relativePath)));
1282
+ const hasCanonicalRuns = existsSync(join(targetDir, ".agdf", "control", "runs"));
1283
+ if (hasCanonicalRuns) {
1284
+ const projection = verifyLegacyProjection(targetDir);
1285
+ if (!["absent", "valid"].includes(projection.status)) {
1286
+ addFinding(
1287
+ findings,
1288
+ "block",
1289
+ "AGDF_LEGACY_PROJECTION_DRIFT",
1290
+ `Legacy compatibility state is not a valid projection: ${projection.status}.`,
1291
+ join(".agdf", "control", "AGDF_RUN.md"),
1292
+ "Regenerate the explicit legacy projection or remove it after legacy consumers are retired.",
1293
+ );
1294
+ }
1295
+ }
1296
+ const missing = doctorRequiredFiles.filter(
1297
+ (relativePath) => !(relativePath.endsWith("AGDF_RUN.md") && hasCanonicalRuns) && !existsSync(join(targetDir, relativePath)),
1298
+ );
1207
1299
 
1208
1300
  for (const relativePath of missing) {
1209
1301
  const templatePath = join(dirname(relativePath), "templates", relativePath.split("/").at(-1));
@@ -1221,8 +1313,9 @@ function evaluateDoctor(targetDir) {
1221
1313
  }
1222
1314
 
1223
1315
  if (missing.length === 0) {
1224
- const runPath = join(".agdf", "control", "AGDF_RUN.md");
1225
- const run = readTargetFile(targetDir, runPath);
1316
+ const selectedRunState = readRunState(targetDir, selection);
1317
+ const runPath = selectedRunState.path;
1318
+ const run = selectedRunState.content;
1226
1319
  const currentGateLine = run.match(/^- current_gate:[^\S\r\n]*(.*)$/m)?.[1]?.trim() ?? "";
1227
1320
  const nextActionLine = run.match(/^- next_allowed_action:[^\S\r\n]*(.*)$/m)?.[1]?.trim() ?? "";
1228
1321
  const hasEvidence = hasFilledEvidenceRow(run);
@@ -1232,7 +1325,7 @@ function evaluateDoctor(targetDir) {
1232
1325
  findings,
1233
1326
  "revise",
1234
1327
  "AGDF_CURRENT_GATE_MISSING",
1235
- "AGDF_RUN.md does not name the current gate.",
1328
+ "The selected run state does not name the current gate.",
1236
1329
  runPath,
1237
1330
  "Set current_gate to the current delivery gate or none.",
1238
1331
  );
@@ -1243,7 +1336,7 @@ function evaluateDoctor(targetDir) {
1243
1336
  findings,
1244
1337
  "revise",
1245
1338
  "AGDF_NEXT_ALLOWED_ACTION_MISSING",
1246
- "AGDF_RUN.md does not state the next allowed action.",
1339
+ "The selected run state does not state the next allowed action.",
1247
1340
  runPath,
1248
1341
  "Fill the next allowed action before asking an agent to continue delivery work.",
1249
1342
  );
@@ -1254,7 +1347,7 @@ function evaluateDoctor(targetDir) {
1254
1347
  findings,
1255
1348
  "warn",
1256
1349
  "AGDF_EVIDENCE_EMPTY",
1257
- "AGDF_RUN.md has no visible evidence row yet.",
1350
+ "The selected run state has no visible evidence row yet.",
1258
1351
  runPath,
1259
1352
  "Add at least one evidence row or explicitly document that no evidence exists yet.",
1260
1353
  );
@@ -1337,7 +1430,7 @@ function evaluateDoctor(targetDir) {
1337
1430
  );
1338
1431
  }
1339
1432
 
1340
- const runState = readRunState(targetDir);
1433
+ const runState = selectedRunState;
1341
1434
  for (const finding of analyzeDurableGateArtefactConsistency(runState)) {
1342
1435
  addFinding(findings, finding.severity, finding.code, finding.message, finding.path, finding.next_step);
1343
1436
  }
@@ -1389,34 +1482,22 @@ function extractField(content, field) {
1389
1482
  return content.match(pattern)?.[1]?.trim() ?? "";
1390
1483
  }
1391
1484
 
1392
- function extractSectionField(section, field) {
1393
- const pattern = new RegExp(`^- ${field}:[^\\S\\r\\n]*(.*)$`, "m");
1394
- return section.match(pattern)?.[1]?.trim() ?? "";
1395
- }
1396
-
1397
1485
  function cleanStatusCell(value) {
1398
1486
  return value.replace(/^`|`$/g, "").trim();
1399
1487
  }
1400
1488
 
1401
- function normalizeGateStatus(gate, status) {
1402
- const normalized = cleanStatusCell(status ?? "");
1403
- if (gate === "QA" && normalized === "passed") return "approved";
1404
- return normalized;
1405
- }
1406
-
1407
- function addApprovalRows(approvals, section) {
1408
- for (const cells of tableRows(section)) {
1409
- const [gate, status, evidence] = cells;
1410
- if (!userGateOrder.includes(gate)) continue;
1411
- approvals.set(gate, {
1412
- status: normalizeGateStatus(gate, status),
1413
- evidence: evidence ?? "",
1489
+ function readRunState(targetDir, selection = {}) {
1490
+ let runPath = join(".agdf", "control", "AGDF_RUN.md");
1491
+ const canonicalRoot = join(targetDir, ".agdf", "control", "runs");
1492
+ if (existsSync(canonicalRoot)) {
1493
+ const selected = resolveRuns(targetDir, {
1494
+ runIdArg: selection.runId,
1495
+ runIdEnv: process.env.AGDF_RUN_ID,
1414
1496
  });
1497
+ runPath = selected.run.path.startsWith(targetDir)
1498
+ ? selected.run.path.slice(targetDir.length + 1)
1499
+ : selected.run.path;
1415
1500
  }
1416
- }
1417
-
1418
- function readRunState(targetDir) {
1419
- const runPath = join(".agdf", "control", "AGDF_RUN.md");
1420
1501
  if (!existsSync(join(targetDir, runPath))) {
1421
1502
  return {
1422
1503
  path: runPath,
@@ -1438,107 +1519,15 @@ function readRunState(targetDir) {
1438
1519
  }
1439
1520
 
1440
1521
  const content = readTargetFile(targetDir, runPath);
1441
- const approvals = new Map();
1442
- addApprovalRows(approvals, content.match(/## Approvals([\s\S]*?)(?:\n## |\n# |$)/)?.[1] ?? "");
1443
- addApprovalRows(approvals, content.match(/## Gate Checklist([\s\S]*?)(?:\n## |\n# |$)/)?.[1] ?? "");
1444
-
1445
- const artefacts = new Map();
1446
- const artefactsSection = markdownSection(content, "Artefacts");
1447
- for (const cells of tableRows(artefactsSection)) {
1448
- const [type, path, status, notes] = cells;
1449
- if (!userGateOrder.includes(type) && !internalStepArtefacts.has(type)) continue;
1450
- artefacts.set(type, {
1451
- path: path ?? "",
1452
- status: cleanStatusCell(status ?? ""),
1453
- notes: notes ?? "",
1454
- });
1455
- }
1456
-
1457
- const artefactChain = nonTemplateRows(markdownSection(content, "Artefact Chain"), "From")
1458
- .map((cells) => ({
1459
- from: cells[0] ?? "",
1460
- relationship: cleanStatusCell(cells[1] ?? ""),
1461
- to: cleanStatusCell(cells[2] ?? ""),
1462
- evidence: cells[3] ?? "",
1463
- }));
1464
-
1465
- const evidence_refs = tableRows(markdownSection(content, "Evidence"))
1466
- .filter((cells) => cells[0] !== "Evidence")
1467
- .filter((cells) => filled(cells[0]) || filled(cells[1]) || filled(cells[2]))
1468
- .map((cells) => ({
1469
- evidence: cells[0] ?? "",
1470
- source: cells[1] ?? "",
1471
- covers: cells[2] ?? "",
1472
- strength: cleanStatusCell(cells[3] ?? ""),
1473
- }));
1474
-
1475
- const missingEvidence = tableRows(markdownSection(content, "Missing Evidence"))
1476
- .filter((cells) => cells[0] !== "Missing evidence")
1477
- .filter((cells) => filled(cells[0]))
1478
- .map((cells) => ({
1479
- missing_evidence: cells[0] ?? "",
1480
- impact: cleanStatusCell(cells[1] ?? ""),
1481
- required_next_step: cells[2] ?? "",
1482
- }));
1483
-
1484
- const risks = tableRows(markdownSection(content, "Risks"))
1485
- .filter((cells) => cells[0] !== "Risk")
1486
- .filter((cells) => filled(cells[0]))
1487
- .map((cells) => ({
1488
- risk: cells[0] ?? "",
1489
- impact: cleanStatusCell(cells[1] ?? ""),
1490
- mitigation_or_owner: cells[2] ?? "",
1491
- }));
1492
-
1493
- const contextGraph = {
1494
- impact: cleanStatusCell(extractField(content, "context_graph_impact")),
1495
- refs: extractField(content, "context_graph_refs"),
1496
- required_action: cleanStatusCell(extractField(content, "context_graph_required_action")),
1497
- gate_effect: cleanStatusCell(extractField(content, "context_graph_gate_effect")),
1498
- evidence: extractField(content, "context_graph_evidence"),
1499
- };
1500
-
1501
- const modeSliceSection = markdownSection(content, "Mode / Slice Decision");
1502
- const modeSliceDecision = {
1503
- decision: cleanStatusCell(extractSectionField(modeSliceSection, "decision")),
1504
- required_next_gate: cleanStatusCell(extractSectionField(modeSliceSection, "required_next_gate")),
1505
- scope_reason: extractSectionField(modeSliceSection, "scope_reason"),
1506
- evidence: extractSectionField(modeSliceSection, "evidence"),
1507
- };
1508
-
1509
- const sourceScopeSection = markdownSection(content, "Source And Scope State");
1510
- const sourceScope = {
1511
- normative_instruction_source: extractSectionField(sourceScopeSection, "normative_instruction_source"),
1512
- multi_scope_state: cleanStatusCell(extractSectionField(sourceScopeSection, "multi_scope_state")),
1513
- active_scope_evidence: extractSectionField(sourceScopeSection, "active_scope_evidence"),
1514
- competing_scope_lines: extractSectionField(sourceScopeSection, "competing_scope_lines"),
1515
- branch_workspace_evidence: extractSectionField(sourceScopeSection, "branch_workspace_evidence"),
1516
- branch_workspace_scope_effect: cleanStatusCell(extractSectionField(sourceScopeSection, "branch_workspace_scope_effect")),
1517
- };
1518
-
1519
- const memorySection = markdownSection(content, "Knowledge Persistence Decision");
1520
- const memory = {
1521
- target: cleanStatusCell(extractSectionField(memorySection, "memory_target")),
1522
- reason: extractSectionField(memorySection, "memory_reason"),
1523
- refs: extractSectionField(memorySection, "memory_refs"),
1524
- };
1522
+ const parsed = parseControlState(content, {
1523
+ userGates: userGateOrder,
1524
+ internalSteps: [...internalStepArtefacts],
1525
+ });
1525
1526
 
1526
1527
  return {
1527
1528
  path: runPath,
1528
1529
  content,
1529
- current_gate: extractField(content, "current_gate"),
1530
- next_allowed_action: extractField(content, "next_allowed_action"),
1531
- approvals,
1532
- artefacts,
1533
- evidence_refs,
1534
- artefact_chain: artefactChain,
1535
- mode_slice_decision: modeSliceDecision,
1536
- missing_evidence: missingEvidence,
1537
- risks,
1538
- context_graph: contextGraph,
1539
- quality_outlook: extractField(content, "quality_outlook"),
1540
- source_scope: sourceScope,
1541
- memory,
1530
+ ...parsed,
1542
1531
  };
1543
1532
  }
1544
1533
 
@@ -1599,7 +1588,7 @@ function analyzeDurableGateArtefactConsistency(runState) {
1599
1588
  code: "AGDF_GATE_ARTEFACT_STATUS_INCONSISTENT",
1600
1589
  message: `${gate} approval is recorded, but the durable artefact row uses status \`${artefact.status}\`; expected ${describeDurableArtefactStatuses(gate)}.`,
1601
1590
  path: runState.path,
1602
- next_step: `Update the ${gate} artefact row in AGDF_RUN.md to use the gate-specific durable status vocabulary.`,
1591
+ next_step: `Update the ${gate} artefact row in the selected RUN_STATE.md to use the gate-specific durable status vocabulary.`,
1603
1592
  });
1604
1593
  }
1605
1594
 
@@ -1847,7 +1836,7 @@ function durableArtefactBlock(gate, nextGate) {
1847
1836
  missing_approval: "none",
1848
1837
  allowed: [
1849
1838
  `persist the approved ${label} in a stable artefact path such as ${stablePath}`,
1850
- "link the artefact from AGDF_RUN.md and MASTER_BACKLOG.md",
1839
+ "link the artefact from the selected RUN_STATE.md and MASTER_BACKLOG.md",
1851
1840
  "run gate-check again",
1852
1841
  ],
1853
1842
  forbidden: nextGate
@@ -1892,7 +1881,7 @@ function transitionDecisionForRunState(runState) {
1892
1881
  allowed: [
1893
1882
  "run Brownfield Review after G-00",
1894
1883
  "identify existing workstream, owners, SoT, reuse risks and open PRD/SD questions",
1895
- "mark Brownfield Review as done or not_applicable in AGDF_RUN.md",
1884
+ "mark Brownfield Review as done or not_applicable in the selected RUN_STATE.md",
1896
1885
  ],
1897
1886
  forbidden: ["create PRD before Brownfield Review is resolved", "create SD", "create TP", "implement code", "claim QA or release readiness"],
1898
1887
  next_allowed_action: "Run Brownfield Review after G-00 before drafting PRD, or mark Brownfield Review not_applicable with evidence.",
@@ -1908,7 +1897,7 @@ function transitionDecisionForRunState(runState) {
1908
1897
  missing_approval: "none",
1909
1898
  allowed: [
1910
1899
  "decide whether the approved UR is quick_task, structured_slice, structured_delivery or block",
1911
- "record scope reason, evidence and required next gate depth in AGDF_RUN.md",
1900
+ "record scope reason, evidence and required next gate depth in the selected RUN_STATE.md",
1912
1901
  "choose the next required gate depth before drafting PRD or implementing",
1913
1902
  ],
1914
1903
  forbidden: ["create PRD before process size is decided", "create SD", "create TP", "implement code", "claim QA or release readiness"],
@@ -2018,9 +2007,9 @@ function transitionDecisionForRunState(runState) {
2018
2007
  };
2019
2008
  }
2020
2009
 
2021
- function evaluateGateCheck(targetDir) {
2022
- const doctorReport = evaluateDoctor(targetDir);
2023
- const runState = readRunState(targetDir);
2010
+ function evaluateGateCheck(targetDir, selection = {}) {
2011
+ const doctorReport = evaluateDoctor(targetDir, selection);
2012
+ const runState = readRunState(targetDir, selection);
2024
2013
  const transitionDecision = transitionDecisionForRunState(runState);
2025
2014
  const deliveryMap = analyzeDeliveryMap(runState);
2026
2015
  const doctorBlocker = doctorReport.findings.find((finding) => finding.severity === "block");
@@ -2155,9 +2144,51 @@ function readBacklogPointers(targetDir) {
2155
2144
  return parseBacklogSection(activeSection);
2156
2145
  }
2157
2146
 
2158
- function evaluateDeliveryMap(targetDir) {
2159
- const doctorReport = evaluateDoctor(targetDir);
2160
- const runState = readRunState(targetDir);
2147
+ function evaluateDeliveryMap(targetDir, selection = {}) {
2148
+ if (selection.allActive) {
2149
+ const selected = resolveRuns(targetDir, { allActive: true });
2150
+ const runs = selected.runs.map((run) => ({
2151
+ run_id: run.run_id,
2152
+ report: evaluateDeliveryMap(targetDir, { runId: run.run_id }),
2153
+ }));
2154
+ const status = selected.findings.length
2155
+ ? "block"
2156
+ : aggregate(
2157
+ runs.map((item) => ({
2158
+ run_id: item.run_id,
2159
+ status: item.report.status,
2160
+ })),
2161
+ { allowNoActiveRuns: allowNoActiveRuns(targetDir) },
2162
+ ).status;
2163
+ const findings = [
2164
+ ...selected.findings.map((finding) => ({
2165
+ ...finding,
2166
+ severity: "block",
2167
+ message: "Invalid canonical run entry.",
2168
+ next_step: "Repair or remove the invalid run entry.",
2169
+ })),
2170
+ ...runs.flatMap((item) =>
2171
+ item.report.findings.map((finding) => ({
2172
+ ...finding,
2173
+ run_id: item.run_id,
2174
+ })),
2175
+ ),
2176
+ ];
2177
+ return {
2178
+ schema_version: "1",
2179
+ status,
2180
+ current_gate: "all-active",
2181
+ next_allowed_action: runs.length
2182
+ ? "Resolve per-run findings."
2183
+ : "Create or activate a governed run.",
2184
+ quality_outlook: "Keep every active run independently actionable.",
2185
+ relationships: runs.flatMap((item) => item.report.relationships),
2186
+ findings,
2187
+ runs,
2188
+ };
2189
+ }
2190
+ const doctorReport = evaluateDoctor(targetDir, selection);
2191
+ const runState = readRunState(targetDir, selection);
2161
2192
  const map = analyzeDeliveryMap(runState);
2162
2193
  const gateDecision = transitionDecisionForRunState(runState);
2163
2194
  const currentGate = effectiveCurrentGate(runState, gateDecision);
@@ -2238,6 +2269,7 @@ async function executeDeliveryPathSearch(options) {
2238
2269
  const fixtureGeneration = fixture?.generator_response;
2239
2270
  const generationEnabled = Boolean(fixtureGeneration || options.generateCandidates);
2240
2271
  const baseInput = fixture?.input ?? searchInputFromControl(options.dir, {
2272
+ scopeKey: options.runId,
2241
2273
  enforcement,
2242
2274
  generation: generationEnabled ? {
2243
2275
  enabled: true,
@@ -2298,20 +2330,50 @@ async function executeDeliveryPathSearch(options) {
2298
2330
  async function main() {
2299
2331
  const options = parseArgs(process.argv.slice(2));
2300
2332
 
2333
+ if (
2334
+ options.allActive &&
2335
+ !["doctor", "delivery-map"].includes(options.target)
2336
+ ) {
2337
+ throw new Error(
2338
+ "--all-active is supported only by doctor and delivery-map",
2339
+ );
2340
+ }
2341
+
2342
+ if (options.target === "run-create") {
2343
+ if (!options.runId || options.allActive)
2344
+ throw new Error("run-create requires --run and rejects --all-active");
2345
+ console.log(createRun(options.dir, options.runId));
2346
+ return;
2347
+ }
2348
+ if (options.target === "run-migrate") {
2349
+ console.log(
2350
+ JSON.stringify(migrateLegacy(options.dir, options.runId), null, 2),
2351
+ );
2352
+ return;
2353
+ }
2354
+ if (options.target === "run-render-legacy") {
2355
+ if (!options.runId) throw new Error("run-render-legacy requires --run");
2356
+ const selected = resolveRuns(options.dir, { runIdArg: options.runId });
2357
+ const output = join(options.dir, ".agdf", "control", "AGDF_RUN.md");
2358
+ writeLegacyProjection(output, selected.run.path);
2359
+ console.log(output);
2360
+ return;
2361
+ }
2362
+
2301
2363
  if (options.target === "doctor") {
2302
- const report = evaluateDoctor(options.dir);
2364
+ const report = evaluateDoctor(options.dir, options);
2303
2365
  printDoctorReport(report, options.json);
2304
2366
  process.exit(report.status === "block" ? 2 : 0);
2305
2367
  }
2306
2368
 
2307
2369
  if (options.target === "gate-check") {
2308
- const report = evaluateGateCheck(options.dir);
2370
+ const report = evaluateGateCheck(options.dir, options);
2309
2371
  printGateCheckReport(report, options.json, options.statusCard);
2310
2372
  process.exit(report.status === "blocked" ? 2 : 0);
2311
2373
  }
2312
2374
 
2313
2375
  if (options.target === "delivery-map") {
2314
- const report = evaluateDeliveryMap(options.dir);
2376
+ const report = evaluateDeliveryMap(options.dir, options);
2315
2377
  printDeliveryMapReport(report, options.json);
2316
2378
  process.exit(report.status === "block" ? 2 : 0);
2317
2379
  }
@@ -9,7 +9,8 @@ Use this scaffold when a target repository should keep AGDF state outside chat h
9
9
  | File | Purpose |
10
10
  |---|---|
11
11
  | `config.json` | Project language preference for generated AGDF artefacts and user-facing chat |
12
- | `templates/AGDF_RUN.md` | Template for current run state: mode, gate, approvals, evidence, risks, next allowed action |
12
+ | `templates/RUN_STATE.md` | Canonical version-2 template for isolated per-run state |
13
+ | `templates/AGDF_RUN.md` | Legacy migration-input and compatibility-projection template |
13
14
  | `templates/MASTER_BACKLOG.md` | Template for the living backlog pointer: active initiatives and current UR/Brownfield/PRD/SD/TP/QA/OR artefacts |
14
15
  | `templates/artefacts/UR.md` | Template for the durable user requirement artefact of a work item |
15
16
  | `templates/artefacts/BROWNFIELD_REVIEW.md` | Template for the durable Brownfield Review and Mode/Slice Decision |
@@ -27,6 +28,9 @@ Use this scaffold when a target repository should keep AGDF state outside chat h
27
28
  ```text
28
29
  .agdf/
29
30
  control/
31
+ runs/
32
+ <run_id>/
33
+ RUN_STATE.md
30
34
  config.json
31
35
  AGDF_RUN.md
32
36
  MASTER_BACKLOG.md
@@ -71,7 +75,13 @@ npx --yes @agdf/cli@latest gate-check --json
71
75
 
72
76
  ## Operating Rules
73
77
 
74
- - `AGDF_RUN.md` is the current run dashboard.
78
+ - Canonical mutable state is isolated per run at `runs/<run_id>/RUN_STATE.md`. Run discovery is
79
+ derived; do not maintain a writable active-run index. Select explicitly with `--run` or
80
+ `AGDF_RUN_ID` when more than one run is active.
81
+ - Migrate legacy state explicitly with `run-migrate`; read-only commands never migrate. A retained
82
+ `AGDF_RUN.md` is migration input or a non-authoritative projection, not a second writable owner.
83
+
84
+ - The selected `runs/<run_id>/RUN_STATE.md` is the current run dashboard.
75
85
  - `config.json` stores the project preference for artefact and chat language; runtime rules stay English.
76
86
  - `MASTER_BACKLOG.md` points to active delivery work; detailed artefacts live beside the work item.
77
87
  - Brownfield Review records the post-UR Mode/Slice Decision before PRD depth or Quick Task execution is chosen.
@@ -80,6 +90,8 @@ npx --yes @agdf/cli@latest gate-check --json
80
90
  - `SOT_REGISTRY.md` decides which document owns which domain.
81
91
  - `CONTEXT_GRAPH.md` records durable project knowledge only when it has evidence and an exit criterion.
82
92
  - `AGENT_QUALITY_CONTRACTS.json` names reusable block, revise and warning conditions.
83
- - `AGDF_RUN.md` can be edited concurrently from different machines or sessions. Give it a `.gitattributes` entry (`.agdf/control/AGDF_RUN.md merge=union`, git's built-in union driver — no local `git config` registration needed, unlike a custom named driver) so concurrent edits merge automatically instead of blocking. Always re-run `doctor`/`gate-check` and visually skim the merged file after a merge or pull that could have touched it: a union merge on conflicting lines (e.g. two different `current_gate` values) keeps both lines rather than erroring, and `doctor` reads only the first match per field, so it will not by itself flag that duplication — it only catches missing or structurally malformed fields.
93
+ - Never assign `merge=union` to canonical `runs/<run_id>/RUN_STATE.md` records. Concurrent edits to the
94
+ same run must remain conflict-visible; independent runs naturally change different files. After a
95
+ merge or pull, rerun `doctor` and `gate-check` for the selected run.
84
96
 
85
97
  Do not duplicate full product documentation in this scaffold. Link to the authoritative artefact instead.