create-agdf 0.4.9 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +9 -7
  2. package/bin/create-agdf.js +284 -152
  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-delivery-path-search/SKILL.md +14 -0
  7. package/generated/.github/skills/agdf-gate-check/SKILL.md +15 -10
  8. package/generated/.github/skills/agdf-release-or/SKILL.md +1 -1
  9. package/generated/.github/skills/agdf-runtime-contract.md +24 -8
  10. package/generated/.opencode/agdf-runtime-contract.md +24 -8
  11. package/generated/.opencode/agents/agdf-brownfield-analysis.md +2 -2
  12. package/generated/.opencode/agents/agdf-delivery-path-search.md +14 -0
  13. package/generated/.opencode/agents/agdf-gate-check.md +15 -10
  14. package/generated/.opencode/agents/agdf-release-or.md +1 -1
  15. package/generated/plugins/agdf/.codex-plugin/plugin.json +1 -1
  16. package/generated/plugins/agdf/control/README.md +15 -3
  17. package/generated/plugins/agdf/control/templates/RUN_STATE.md +48 -0
  18. package/generated/plugins/agdf/hooks/session-start.sh +2 -0
  19. package/generated/plugins/agdf/meta/agdf-plugin.definition.json +1 -1
  20. package/generated/plugins/agdf/meta/agdf-runtime-contract.md +24 -8
  21. package/generated/plugins/agdf/skills/brownfield-analysis/SKILL.md +3 -3
  22. package/generated/plugins/agdf/skills/delivery-path-search/SKILL.md +14 -0
  23. package/generated/plugins/agdf/skills/gate-check/SKILL.md +15 -10
  24. package/generated/plugins/agdf/skills/release-or/SKILL.md +1 -1
  25. package/lib/control-state/aggregate.js +18 -0
  26. package/lib/control-state/index.js +6 -0
  27. package/lib/control-state/legacy-migration.js +94 -0
  28. package/lib/control-state/run-state-parser.js +196 -0
  29. package/lib/control-state/run-state-repository.js +68 -0
  30. package/lib/control-state/run-state-resolver.js +45 -0
  31. package/lib/control-state/run-state-writer.js +96 -0
  32. package/lib/delivery-path-search/candidate-policy.js +67 -2
  33. package/lib/delivery-path-search/contracts.js +127 -0
  34. package/lib/delivery-path-search/evaluators/claude.js +2 -16
  35. package/lib/delivery-path-search/evaluators/codex.js +2 -16
  36. package/lib/delivery-path-search/generators/claude.js +40 -0
  37. package/lib/delivery-path-search/generators/codex.js +38 -0
  38. package/lib/delivery-path-search/generators/protocol.js +12 -0
  39. package/lib/delivery-path-search/persistence.js +5 -0
  40. package/lib/delivery-path-search/search-engine.js +64 -2
  41. package/lib/delivery-path-search/state-adapter.js +119 -16
  42. package/lib/delivery-path-search/transports/read-only-guard.js +43 -0
  43. package/package.json +4 -2
@@ -30,7 +30,8 @@ Do not add a separate `Quality outlook` line for pure Quick Tasks unless the tas
30
30
  ### Non-Normative Trivial Change Boundary
31
31
 
32
32
  A `quick_task` whose entire diff stays fully outside all of the following paths may close using only
33
- the compact output shape above, and must not create, rewrite or expand any of `AGDF_RUN.md`'s core
33
+ the compact output shape above, and must not create, rewrite or expand any selected canonical
34
+ `RUN_STATE.md` core
34
35
  sections (Run Meta, Objective, Current Control State, Source And Scope State, Run Status Card,
35
36
  Approvals, Artefacts, Mode/Slice Decision, Artefact Chain, Evidence, Missing Evidence, Risks, Context
36
37
  Graph Impact, Knowledge Persistence Decision, Closeout):
@@ -91,6 +92,12 @@ Delivery Path Search is an optional read-only planning step for high-impact deci
91
92
  - Search output is evidence only. Canonical `gate-check` independently decides what may proceed.
92
93
  - The bounded first-release algorithm must not be labelled MCTS.
93
94
  - Surface adapters may translate transport and presentation, but must not fork scoring, search or gate semantics.
95
+ - Optional AI-native candidate generation supplements the deterministic candidate baseline; it never replaces it.
96
+ - Generated proposals are untrusted. The core must validate schema, exact canonical gate action, scope, duplicates and material diversity before evaluation.
97
+ - Generation is opt-in and bounded to one call, five proposals, 30 seconds and five abstract cost units by default, within whole-run budgets.
98
+ - External generation receives only bounded normalized control summaries and references, never secrets, full artefacts, raw prompts, hidden reasoning or source snapshots.
99
+ - Generator status, provenance, accepted/rejected counts, separate budget use and typed failure must remain visible. Failure retains the deterministic baseline; automatic provider fallback is forbidden.
100
+ - Codex and Claude Code may provide tool-enforced generator transports. Copilot, OpenCode and generic surfaces remain instruction-only without conforming evidence.
94
101
 
95
102
  The field names above are the stable machine-readable contract used by JSON
96
103
  reports and automation. Human-facing Markdown must present the same projection
@@ -201,9 +208,10 @@ OR is not mandatory for a pure explanation, read-only inspection, small review,
201
208
  When in doubt, use a short OR-lite only if it clarifies gate state, evidence, risk, or the next permissible step.
202
209
 
203
210
  A `quick_task` fully inside the Non-Normative Trivial Change Boundary above stays exempt from the full
204
- `AGDF_RUN.md` ceremony even when it is otherwise a relevant run. If `AGDF_RUN.md` currently reflects
205
- another active run, append exactly one line to that run's `Prior Run Pointers` section noting what
206
- changed and that it is unrelated; do not edit any other section or any other existing line.
211
+ selected-run ceremony even when it is otherwise a relevant run. If a selected canonical
212
+ `RUN_STATE.md` currently reflects another scope, append exactly one line to that run's
213
+ `Prior Run Pointers` section noting what changed and that it is unrelated; do not edit any other
214
+ section or any other existing line.
207
215
 
208
216
  ## Gate Rules
209
217
 
@@ -219,7 +227,7 @@ changed and that it is unrelated; do not edit any other section or any other exi
219
227
  - Missing control files or missing current-state fields do not forbid the agent from preparing the current allowed artefact. They forbid later-gate work and implementation. For a fresh request, the default allowed work is to draft a minimal UR in the response and request `Approval: UR`. Initialize or write `.agdf/control/` only when the user explicitly asks for durable AGDF control state, the repository already uses `.agdf/control/` as its live working state, or a deterministic CLI/CI setup path is being executed.
220
228
  - `Approval: UR` permits Brownfield Review after G-00 first, then a Mode/Slice Decision. It never permits implementation by itself.
221
229
  - PRD, SD and TP depth is chosen after Brownfield Review through the Mode/Slice Decision, not before existing-system impact is understood.
222
- - The Mode/Slice Decision must be visible before any PRD shortcut, Quick Task execution or implementation: record the decision, required next gate, scope reason and evidence in `AGDF_RUN.md` or an equivalent linked control artefact.
230
+ - The Mode/Slice Decision must be visible before any PRD shortcut, Quick Task execution or implementation: record the decision, required next gate, scope reason and evidence in the selected canonical `RUN_STATE.md` or an equivalent linked control artefact.
223
231
  - `Approval: PRD` permits Solution Design drafting, not implementation.
224
232
  - `Approval: SD` permits Task/Test Plan drafting, not implementation.
225
233
  - `Approval: TP` permits implementation-preparation Brownfield Analysis and then CD+Tests when Brownfield evidence supports it.
@@ -321,10 +329,18 @@ Relevant-run closeout must reconcile Context Graph impact before presenting a cl
321
329
 
322
330
  ## Control Scaffold
323
331
 
332
+ ### Run-Scoped Control State
333
+
334
+ Canonical mutable run state lives at `.agdf/control/runs/<run_id>/RUN_STATE.md`, one file per run.
335
+ Repository-level discovery is derived; do not maintain a writable active-run dashboard. Select with
336
+ `--run`, then `AGDF_RUN_ID`, or automatically only when exactly one run is active. Ambiguity fails
337
+ closed. Legacy `AGDF_RUN.md` is migration input or an explicitly rendered non-authoritative projection,
338
+ never a second writable owner. Read-only commands must not migrate state.
339
+
324
340
  When a repository needs durable AGDF state, use the plugin-local `control/` scaffold as the starting point.
325
341
 
326
342
  - `config.json` stores project language preferences. Use `artifact_language` for generated AGDF artefacts and `chat_language` for user-facing responses unless the user explicitly asks otherwise. Runtime rules remain English.
327
- - `AGDF_RUN.md` is the current run dashboard.
343
+ - `.agdf/control/runs/<run_id>/RUN_STATE.md` is the canonical current-run dashboard; legacy `AGDF_RUN.md` is migration input or an explicit non-authoritative projection.
328
344
  - `MASTER_BACKLOG.md` is the living pointer for active delivery work.
329
345
  - `BROWNFIELD_REVIEW.md` records the post-UR existing-system view and Mode/Slice Decision before PRD depth or Quick Task execution is chosen.
330
346
  - `SOT_REGISTRY.md` prevents parallel sources of truth.
@@ -372,8 +388,8 @@ They make the repository state checkable, but they do not replace the native ski
372
388
 
373
389
  ## Delivery Map
374
390
 
375
- `delivery-map --json` is the machine-readable delivery picture for the active control state.
376
- It derives, but does not replace, the live `.agdf/control/AGDF_RUN.md` and `MASTER_BACKLOG.md` state.
391
+ `delivery-map --json` is the machine-readable delivery picture for the selected canonical control state.
392
+ It derives, but does not replace, the selected `RUN_STATE.md` and `MASTER_BACKLOG.md` state.
377
393
 
378
394
  It must expose:
379
395
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: brownfield-analysis
3
- description: Use this skill after gate-check or live AGDF control state confirms that Brownfield Review or implementation preparation is allowed. Use it after Approval: UR as the lightweight Brownfield Review after G-00 before PRD when existing systems may be affected, and again before non-trivial implementation after TP. Do not use it as the first AGDF skill for a fresh "I want to build/change X" prompt when approval, scope, or next allowed action is unclear.
3
+ description: "Use this skill after gate-check or live AGDF control state confirms that Brownfield Review or implementation preparation is allowed. Use it after Approval: UR as the lightweight Brownfield Review after G-00 before PRD when existing systems may be affected, and again before non-trivial implementation after TP. Do not use it as the first AGDF skill for a fresh \"I want to build/change X\" prompt when approval, scope, or next allowed action is unclear."
4
4
  ---
5
5
 
6
6
  # brownfield-analysis
@@ -47,8 +47,8 @@ Do not mix both modes silently. Name the active mode in the output.
47
47
  13. Specification archive migrations must follow the archive index if present.
48
48
 
49
49
  ## When To Use
50
- - after `gate-check` permits `Brownfield Review` or live `.agdf/control/AGDF_RUN.md` names Brownfield Review as the next allowed action
51
- - after `gate-check` permits implementation preparation or live `.agdf/control/AGDF_RUN.md` already names Brownfield Analysis as the next allowed action
50
+ - after `gate-check` permits `Brownfield Review` or the selected canonical run record names Brownfield Review as the next allowed action
51
+ - after `gate-check` permits implementation preparation or the selected canonical run record already names Brownfield Analysis as the next allowed action
52
52
  - after `Approval: UR` when existing owners, SoT, contracts, policies, persistence, runtime paths, tests, UI, UX or architecture may affect PRD/SD scope
53
53
  - before `CD+Tests`
54
54
  - before changes to an existing repository
@@ -37,6 +37,17 @@ Skip for routine questions, obvious local fixes and approved implementation with
37
37
  npx --yes @agdf/cli@latest delivery-path-search --surface <surface> --json
38
38
  ```
39
39
 
40
+ Optional AI-native candidate generation is explicit:
41
+
42
+ ```bash
43
+ npx --yes @agdf/cli@latest delivery-path-search \
44
+ --surface codex \
45
+ --generate-candidates \
46
+ --json
47
+ ```
48
+
49
+ It supplements the deterministic baseline with at most one bounded generator call. Treat proposals as untrusted until the canonical core validates schema, gate action, scope, duplicates and material diversity. Report generation provenance, budgets, rejections and failure. Never hide deterministic fallback or switch providers automatically.
50
+
40
51
  5. Treat facts, repository evidence, assumptions and model judgements separately.
41
52
  6. Return exactly one recommendation or `no_safe_recommendation`.
42
53
  7. Run canonical AGDF gate-check after search. Never translate a recommendation into permission.
@@ -73,3 +84,6 @@ Persist only the redacted decision summary when durable evidence is required. Do
73
84
  - Do not invent default scores for invalid evaluator output.
74
85
  - Do not implement, approve or release based on search output.
75
86
  - Do not create agent-specific scoring, gate logic or skill copies.
87
+ - Do not replace deterministic candidates with generated proposals.
88
+ - Do not send secrets, full artefacts, source snapshots, raw prompts or hidden reasoning to a generator.
89
+ - Do not claim executable generation for instruction-only surfaces.
@@ -28,7 +28,7 @@ This skill is the primary operating path for gate judgement.
28
28
 
29
29
  First inspect the durable control state directly:
30
30
 
31
- - `.agdf/control/AGDF_RUN.md`
31
+ - selected `.agdf/control/runs/<run_id>/RUN_STATE.md` (or legacy `AGDF_RUN.md` before explicit migration)
32
32
  - `.agdf/control/MASTER_BACKLOG.md`
33
33
  - `.agdf/control/SOT_REGISTRY.md`
34
34
  - `.agdf/control/CONTEXT_GRAPH.md`
@@ -47,7 +47,7 @@ npx --yes @agdf/cli@latest gate-check --json
47
47
  npx --yes @agdf/cli@latest delivery-map --json
48
48
  ```
49
49
 
50
- `init` creates the control scaffold. `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.
50
+ `init` creates the control scaffold. `doctor` checks whether `.agdf/control/` is actionable. `gate-check` consumes that result and the selected canonical run record to report the operative process decision: `open | blocked`, current gate, blocking reason, missing approval, allowed outputs, forbidden outputs, next allowed action and evidence references.
51
51
  `delivery-map` reports the durable delivery picture: active artefacts, approvals, Artefact Chain relationships, evidence refs, missing evidence, risks, Context Graph gate effect and machine-readable findings.
52
52
 
53
53
  The CLI reports are validators and JSON evidence, not the primary user experience, not a required ritual for normal work and not a second rule system. If a report says `blocked`, do not continue with later-gate artefacts until the reported blocker is resolved.
@@ -124,14 +124,19 @@ If a status is not explicit, do not assume it is satisfied.
124
124
  12. If branch, workspace and durable artefacts disagree, do not choose a scope silently; report the ambiguity and ask for the smallest clarifying gate action.
125
125
 
126
126
  ## Output
127
- Keep the result short and operational:
128
-
129
- - **Current gate:** `<GateName or internal step>`
130
- - **Status:** `open | blocked`
131
- - **Allowed:** `<allowed outputs>`
132
- - **Forbidden:** `<forbidden outputs>`
133
- - **Missing approval:** `Approval: <GateName> | none`
134
- - **Next step:** `<single permissible next step>`
127
+ Keep the result short and operational. Render the Runtime Contract's compact human-facing Run Status Card rather than a surface-specific summary:
128
+
129
+ | Run status | Value |
130
+ |---|---|
131
+ | Status | `open | blocked` |
132
+ | Current gate | `<GateName or internal step>` |
133
+ | Allowed now | `<currently allowed outputs>` |
134
+ | Blocked by | `<blocking condition or none>` |
135
+ | Missing approval | `Approval: <GateName> | none` |
136
+ | Next step | `<single permissible next step>` |
137
+ | Quality outlook | `<next meaningful quality focus or none>` |
138
+
139
+ When an approval is missing, also include `Next gate after approval` and `Allowed after approval` exactly as constrained by the Runtime Contract. Keep forbidden outputs, evidence and next-skill detail in the concise surrounding text when relevant; they are not extra status-card rows.
135
140
 
136
141
  If this skill creates or updates control artefacts, do not paste full file bodies into the chat.
137
142
  List paths, summarize the decision, name the blocker or approval needed, and keep the durable content in the files.
@@ -28,7 +28,7 @@ It reports:
28
28
  Use `../../meta/agdf-runtime-contract.md` for Quality Contract output, Context Graph fields, gate terms, and non-duplication rules.
29
29
 
30
30
  OR-specific output must make gate status, delivered and intentionally not delivered content, missing approvals, missing evidence, risks, retained fallbacks, and the next permissible step visible.
31
- When `.agdf/control/` is present, persist or link the OR under `.agdf/control/artefacts/<key>/OR.md` and reference it from `AGDF_RUN.md` or `MASTER_BACKLOG.md` when it is steering-relevant.
31
+ When `.agdf/control/` is present, persist or link the OR under `.agdf/control/artefacts/<key>/OR.md` and reference it from the selected canonical `RUN_STATE.md` or `MASTER_BACKLOG.md` when it is steering-relevant.
32
32
  Use the Runtime Contract definition of `Relevant Run` to decide whether OR is mandatory or whether a Quick Task mini-closeout is enough.
33
33
 
34
34
  ## Rules
@@ -0,0 +1,18 @@
1
+ const STATUS_RANK = { pass: 0, warn: 1, revise: 2, block: 3 };
2
+
3
+ export function aggregate(results, { allowNoActiveRuns = false } = {}) {
4
+ const runs = [...results].sort((left, right) =>
5
+ left.run_id.localeCompare(right.run_id),
6
+ );
7
+ const status = runs.length
8
+ ? runs.reduce(
9
+ (current, run) =>
10
+ STATUS_RANK[run.status] > STATUS_RANK[current] ? run.status : current,
11
+ "pass",
12
+ )
13
+ : allowNoActiveRuns
14
+ ? "pass"
15
+ : "revise";
16
+
17
+ return { status, runs };
18
+ }
@@ -0,0 +1,6 @@
1
+ export * from "./run-state-parser.js";
2
+ export * from "./run-state-repository.js";
3
+ export * from "./run-state-resolver.js";
4
+ export * from "./run-state-writer.js";
5
+ export * from "./legacy-migration.js";
6
+ export * from "./aggregate.js";
@@ -0,0 +1,94 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync, mkdirSync, readFileSync, rmdirSync, unlinkSync } from "node:fs";
3
+ import { dirname, join, relative, resolve } from "node:path";
4
+ import {
5
+ discoverRuns,
6
+ renderRunState,
7
+ runPath,
8
+ } from "./run-state-repository.js";
9
+ import {
10
+ parseRunState,
11
+ RUN_ID_PATTERN,
12
+ scalarFields,
13
+ semanticBody,
14
+ semanticFingerprint,
15
+ } from "./run-state-parser.js";
16
+ import { atomicWrite } from "./run-state-writer.js";
17
+ export function migrateLegacy(root, requested, io = {}) {
18
+ const writeCanonical = io.atomicWrite ?? atomicWrite;
19
+ const readCanonical = io.readFile ?? readFileSync;
20
+ const lp = join(root, ".agdf", "control", "AGDF_RUN.md");
21
+ if (!existsSync(lp)) throw Error("AGDF_LEGACY_RUN_MISSING");
22
+ const legacy = readFileSync(lp, "utf8");
23
+ if (legacy.startsWith("<!-- AGDF LEGACY PROJECTION"))
24
+ throw Error("AGDF_LEGACY_ALREADY_PROJECTED");
25
+ if (!legacy.startsWith("# AGDF Run State") || !semanticBody(legacy))
26
+ throw Error("AGDF_LEGACY_RUN_INVALID");
27
+ const id = requested || scalarFields(legacy).values.get("run_id");
28
+ if (!RUN_ID_PATTERN.test(id ?? "")) throw Error("AGDF_RUN_ID_INVALID");
29
+ const legacyMeta = Object.fromEntries(scalarFields(legacy).values);
30
+ const dest = runPath(root, id),
31
+ next = renderRunState(id, semanticBody(legacy), legacyMeta);
32
+ if (existsSync(dest)) {
33
+ if (
34
+ semanticFingerprint(readFileSync(dest, "utf8")) ===
35
+ semanticFingerprint(next)
36
+ )
37
+ return { status: "already_migrated", path: dest };
38
+ throw Error("AGDF_RUN_COLLISION");
39
+ }
40
+ mkdirSync(dirname(dest), { recursive: true });
41
+ try {
42
+ writeCanonical(dest, next);
43
+ const persisted = readCanonical(dest, "utf8"),
44
+ parsed = parseRunState(persisted, id);
45
+ if (
46
+ !parsed.valid ||
47
+ semanticFingerprint(persisted) !== semanticFingerprint(next)
48
+ ) throw Error("AGDF_MIGRATION_VERIFICATION_FAILED");
49
+ } catch (error) {
50
+ if (existsSync(dest)) unlinkSync(dest);
51
+ try {
52
+ rmdirSync(dirname(dest));
53
+ } catch (cleanupError) {
54
+ if (!["ENOENT", "ENOTEMPTY"].includes(cleanupError.code)) throw cleanupError;
55
+ }
56
+ throw error;
57
+ }
58
+ return { status: "migrated", path: dest };
59
+ }
60
+ export function renderLegacyProjection(path) {
61
+ const content = readFileSync(path, "utf8"),
62
+ parsed = parseRunState(content),
63
+ digest = createHash("sha256").update(content).digest("hex");
64
+ if (!parsed.valid) throw Error("AGDF_RUN_NOT_SELECTABLE");
65
+ return `<!-- AGDF LEGACY PROJECTION: NON-AUTHORITATIVE -->\n<!-- canonical_source: ${path} -->\n<!-- run_id: ${parsed.meta.run_id} -->\n<!-- revision_id: ${parsed.meta.revision_id} -->\n<!-- sha256: ${digest} -->\n${content}`;
66
+ }
67
+ export function writeLegacyProjection(path, canonical) {
68
+ atomicWrite(path, renderLegacyProjection(canonical));
69
+ }
70
+ export function verifyLegacyProjection(root) {
71
+ const path = join(root, ".agdf", "control", "AGDF_RUN.md");
72
+ if (!existsSync(path)) return { status: "absent" };
73
+ const p = readFileSync(path, "utf8"),
74
+ source = p.match(/canonical_source:\s*(.*?)\s*-->/)?.[1],
75
+ runId = p.match(/<!-- run_id:\s*(.*?)\s*-->/)?.[1],
76
+ digest = p.match(/sha256:\s*([0-9a-f]{64})/)?.[1],
77
+ projectedStart = p.indexOf("# AGDF Run State"),
78
+ projected = projectedStart >= 0 ? p.slice(projectedStart) : undefined;
79
+ if (!source || !runId || !digest || projected === undefined)
80
+ return { status: discoverRuns(root).length ? "mixed_authority" : "legacy" };
81
+ const target = resolve(source),
82
+ rel = relative(resolve(root), target),
83
+ expected = relative(resolve(root), resolve(runPath(root, runId)));
84
+ if (rel.startsWith("..") || !rel || rel !== expected || !existsSync(target))
85
+ return { status: "projection_source_invalid" };
86
+ const canonical = readFileSync(target, "utf8"),
87
+ canonicalDigest = createHash("sha256").update(canonical).digest("hex");
88
+ return {
89
+ status:
90
+ canonicalDigest === digest && projected === canonical
91
+ ? "valid"
92
+ : "legacy_projection_drift",
93
+ };
94
+ }
@@ -0,0 +1,196 @@
1
+ export const RUN_ID_PATTERN = /^[a-z0-9][a-z0-9._-]{0,127}$/;
2
+ export const LIFECYCLES = new Set([
3
+ "active",
4
+ "completed",
5
+ "superseded",
6
+ "abandoned",
7
+ ]);
8
+ export function scalarFields(content) {
9
+ const values = new Map(),
10
+ duplicates = [];
11
+ const runMeta = content.match(/(?:^|\n)## Run Meta\s*\n([\s\S]*?)(?=\n## |$)/)?.[1] ?? "";
12
+ for (const [, k, v] of runMeta.matchAll(/^- ([a-z_]+):\s*(.*?)\s*$/gm)) {
13
+ if (values.has(k)) duplicates.push(k);
14
+ else values.set(k, v.replace(/^`|`$/g, ""));
15
+ }
16
+ return { values, duplicates };
17
+ }
18
+ export function parseRunState(content, expected) {
19
+ const { values, duplicates } = scalarFields(content),
20
+ findings = duplicates.map((field) => ({
21
+ code: "AGDF_RUN_FIELD_DUPLICATE",
22
+ field,
23
+ })),
24
+ id = values.get("run_id") ?? "";
25
+ if (content.split(/\r?\n/).some((line) => line.trimStart().startsWith("|") && !line.trimEnd().endsWith("|"))) {
26
+ findings.push({ code: "AGDF_RUN_TABLE_INVALID" });
27
+ }
28
+ if (!RUN_ID_PATTERN.test(id)) findings.push({ code: "AGDF_RUN_ID_INVALID" });
29
+ if (expected && id !== expected)
30
+ findings.push({ code: "AGDF_RUN_PATH_MISMATCH" });
31
+ if (values.get("control_state_version") !== "2")
32
+ findings.push({ code: "AGDF_RUN_VERSION_UNSUPPORTED" });
33
+ if (!LIFECYCLES.has(values.get("lifecycle")))
34
+ findings.push({ code: "AGDF_RUN_LIFECYCLE_INVALID" });
35
+ if (!/^[1-9]\d*$/.test(values.get("revision") ?? ""))
36
+ findings.push({ code: "AGDF_RUN_REVISION_INVALID" });
37
+ if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(values.get("revision_id") ?? ""))
38
+ findings.push({ code: "AGDF_RUN_REVISION_ID_INVALID" });
39
+ return {
40
+ content,
41
+ meta: Object.fromEntries(values),
42
+ findings,
43
+ valid: !findings.length,
44
+ };
45
+ }
46
+ export function semanticBody(content) {
47
+ return content
48
+ .replace(/^# AGDF Run State\s*/m, "")
49
+ .replace(/## Run Meta[\s\S]*?(?=\n## )/, "")
50
+ .trim();
51
+ }
52
+ export function semanticFingerprint(content) {
53
+ return semanticBody(content).replace(/\s+/g, " ");
54
+ }
55
+
56
+ function section(c, h) {
57
+ const e = h.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
58
+ return (
59
+ c.match(
60
+ new RegExp(`(?:^|\\n)## ${e}\\s*\\n([\\s\\S]*?)(?:\\n## |\\n# |$)`),
61
+ )?.[1] ?? ""
62
+ );
63
+ }
64
+ function rows(s) {
65
+ return s
66
+ .split(/\r?\n/)
67
+ .filter((l) => l.trim().startsWith("|"))
68
+ .map((l) =>
69
+ l
70
+ .split("|")
71
+ .slice(1, -1)
72
+ .map((x) => x.trim()),
73
+ )
74
+ .filter((r) => !r.every((x) => /^[-:]+$/.test(x)));
75
+ }
76
+ function clean(v = "") {
77
+ return v.replace(/^`|`$/g, "").trim();
78
+ }
79
+ function meaningful(v = "") {
80
+ return Boolean(v && !(v.startsWith("`") && v.includes("|")));
81
+ }
82
+ function field(c, k) {
83
+ return (
84
+ c.match(new RegExp(`^- ${k}:[^\\S\\r\\n]*(.*)$`, "m"))?.[1]?.trim() ?? ""
85
+ );
86
+ }
87
+ function sectionField(c, h, k) {
88
+ return field(section(c, h), k);
89
+ }
90
+ function dataRows(c, h, header) {
91
+ return rows(section(c, h)).filter(
92
+ (r) =>
93
+ r[0] !== header &&
94
+ (h === "Evidence" ? r.slice(0, 3).some(meaningful) : r.some(meaningful)),
95
+ );
96
+ }
97
+ export function parseControlState(
98
+ content,
99
+ { userGates = [], internalSteps = [] } = {},
100
+ ) {
101
+ const approvals = new Map();
102
+ for (const h of ["Approvals", "Gate Checklist"])
103
+ for (const [g, s, e] of rows(section(content, h))) {
104
+ if (userGates.includes(g))
105
+ approvals.set(g, {
106
+ status: clean(s) === "passed" && g === "QA" ? "approved" : clean(s),
107
+ evidence: e ?? "",
108
+ });
109
+ }
110
+ const artefacts = new Map();
111
+ for (const [t, p, s, n] of rows(section(content, "Artefacts"))) {
112
+ if (userGates.includes(t) || internalSteps.includes(t))
113
+ artefacts.set(t, { path: p ?? "", status: clean(s), notes: n ?? "" });
114
+ }
115
+ const mapRows = (h, header, fn) => dataRows(content, h, header).map(fn);
116
+ const mode = "Mode / Slice Decision",
117
+ source = "Source And Scope State",
118
+ memory = "Knowledge Persistence Decision";
119
+ return {
120
+ current_gate: field(content, "current_gate"),
121
+ next_allowed_action: field(content, "next_allowed_action"),
122
+ approvals,
123
+ artefacts,
124
+ evidence_refs: mapRows("Evidence", "Evidence", (r) => ({
125
+ evidence: r[0] ?? "",
126
+ source: r[1] ?? "",
127
+ covers: r[2] ?? "",
128
+ strength: clean(r[3]),
129
+ })),
130
+ artefact_chain: mapRows("Artefact Chain", "From", (r) => ({
131
+ from: r[0] ?? "",
132
+ relationship: clean(r[1]),
133
+ to: clean(r[2]),
134
+ evidence: r[3] ?? "",
135
+ })),
136
+ missing_evidence: mapRows("Missing Evidence", "Missing evidence", (r) => ({
137
+ missing_evidence: r[0] ?? "",
138
+ impact: clean(r[1]),
139
+ required_next_step: r[2] ?? "",
140
+ })),
141
+ risks: mapRows("Risks", "Risk", (r) => ({
142
+ risk: r[0] ?? "",
143
+ impact: clean(r[1]),
144
+ mitigation_or_owner: r[2] ?? "",
145
+ })),
146
+ context_graph: {
147
+ impact: clean(field(content, "context_graph_impact")),
148
+ refs: field(content, "context_graph_refs"),
149
+ required_action: clean(field(content, "context_graph_required_action")),
150
+ gate_effect: clean(field(content, "context_graph_gate_effect")),
151
+ evidence: field(content, "context_graph_evidence"),
152
+ },
153
+ quality_outlook: field(content, "quality_outlook"),
154
+ mode_slice_decision: {
155
+ decision: clean(sectionField(content, mode, "decision")),
156
+ required_next_gate: clean(
157
+ sectionField(content, mode, "required_next_gate"),
158
+ ),
159
+ scope_reason: sectionField(content, mode, "scope_reason"),
160
+ evidence: sectionField(content, mode, "evidence"),
161
+ },
162
+ source_scope: {
163
+ normative_instruction_source: sectionField(
164
+ content,
165
+ source,
166
+ "normative_instruction_source",
167
+ ),
168
+ multi_scope_state: clean(
169
+ sectionField(content, source, "multi_scope_state"),
170
+ ),
171
+ active_scope_evidence: sectionField(
172
+ content,
173
+ source,
174
+ "active_scope_evidence",
175
+ ),
176
+ competing_scope_lines: sectionField(
177
+ content,
178
+ source,
179
+ "competing_scope_lines",
180
+ ),
181
+ branch_workspace_evidence: sectionField(
182
+ content,
183
+ source,
184
+ "branch_workspace_evidence",
185
+ ),
186
+ branch_workspace_scope_effect: clean(
187
+ sectionField(content, source, "branch_workspace_scope_effect"),
188
+ ),
189
+ },
190
+ memory: {
191
+ target: clean(sectionField(content, memory, "memory_target")),
192
+ reason: sectionField(content, memory, "memory_reason"),
193
+ refs: sectionField(content, memory, "memory_refs"),
194
+ },
195
+ };
196
+ }
@@ -0,0 +1,68 @@
1
+ import {
2
+ existsSync,
3
+ lstatSync,
4
+ mkdirSync,
5
+ readFileSync,
6
+ readdirSync,
7
+ writeFileSync,
8
+ } from "node:fs";
9
+ import { dirname, join } from "node:path";
10
+ import { randomUUID } from "node:crypto";
11
+ import { parseRunState, RUN_ID_PATTERN } from "./run-state-parser.js";
12
+ export function runPath(root, id) {
13
+ if (!RUN_ID_PATTERN.test(id)) throw Error(`invalid run_id: ${id}`);
14
+ return join(root, ".agdf", "control", "runs", id, "RUN_STATE.md");
15
+ }
16
+ export function discoverRuns(root) {
17
+ const dir = join(root, ".agdf", "control", "runs");
18
+ if (!existsSync(dir)) return [];
19
+ const rootStats = lstatSync(dir);
20
+ if (rootStats.isSymbolicLink() || !rootStats.isDirectory()) {
21
+ return [invalidRun("<repository>", dir)];
22
+ }
23
+ return readdirSync(dir)
24
+ .filter((x) => !x.startsWith("."))
25
+ .sort()
26
+ .map((id) => {
27
+ const d = join(dir, id),
28
+ path = join(d, "RUN_STATE.md");
29
+ const directoryStats = lstatSync(d);
30
+ if (
31
+ !RUN_ID_PATTERN.test(id) ||
32
+ directoryStats.isSymbolicLink() ||
33
+ !directoryStats.isDirectory() ||
34
+ !existsSync(path)
35
+ )
36
+ return invalidRun(id, path);
37
+
38
+ const stateStats = lstatSync(path);
39
+ if (stateStats.isSymbolicLink() || !stateStats.isFile()) {
40
+ return invalidRun(id, path);
41
+ }
42
+ return {
43
+ run_id: id,
44
+ path,
45
+ ...parseRunState(readFileSync(path, "utf8"), id),
46
+ };
47
+ });
48
+ }
49
+
50
+ function invalidRun(runId, path) {
51
+ return {
52
+ run_id: runId,
53
+ path,
54
+ valid: false,
55
+ findings: [{ code: "AGDF_RUN_PATH_INVALID", path }],
56
+ };
57
+ }
58
+ const DEFAULT_BODY = `## Objective\n\nDescribe the trustworthy outcome.\n\n## Current Control State\n\n| Question | Answer |\n|---|---|\n| What is known? | New run created. |\n| What is approved? | Nothing yet. |\n| What is missing? | Durable UR and exact approval. |\n| What is the next allowed action? | Draft the UR. |\n| What is explicitly forbidden right now? | Later artefacts and implementation. |\n\n## Evidence\n\n| Evidence | Source | Covers | Strength |\n|---|---|---|---|\n| Run creation | run-create | Control initialization | direct |\n\n## Closeout\n\n- next_allowed_action: Draft the UR.\n- quality_outlook: Establish evidence before later gates.\n`;
59
+ export function renderRunState(id, body = DEFAULT_BODY, meta = {}) {
60
+ return `# AGDF Run State\n\n## Run Meta\n\n- control_state_version: 2\n- run_id: ${id}\n- lifecycle: ${meta.lifecycle ?? "active"}\n- revision: 1\n- revision_id: ${randomUUID()}\n- mode: ${meta.mode ?? "structured_delivery"}\n- current_gate: ${meta.current_gate ?? "UR"}\n- decision: ${meta.decision ?? "in_progress"}\n- owner: ${meta.owner ?? "agent"}\n\n${body}`;
61
+ }
62
+ export function createRun(root, id, body = "") {
63
+ const path = runPath(root, id);
64
+ if (existsSync(path)) throw Error("AGDF_RUN_COLLISION");
65
+ mkdirSync(dirname(path), { recursive: true });
66
+ writeFileSync(path, renderRunState(id, body), { flag: "wx" });
67
+ return path;
68
+ }
@@ -0,0 +1,45 @@
1
+ import { discoverRuns } from "./run-state-repository.js";
2
+ export function resolveRuns(
3
+ root,
4
+ { runIdArg, runIdEnv, allActive = false } = {},
5
+ ) {
6
+ if (
7
+ (allActive && (runIdArg || runIdEnv)) ||
8
+ (runIdArg && runIdEnv && runIdArg !== runIdEnv)
9
+ )
10
+ throw Error("AGDF_SELECTOR_CONFLICT");
11
+ const runs = discoverRuns(root),
12
+ active = runs.filter((r) => r.valid && r.meta.lifecycle === "active");
13
+ const findings = runs
14
+ .filter((run) => !run.valid)
15
+ .flatMap((run) =>
16
+ run.findings.map((finding) => ({ ...finding, run_id: run.run_id })),
17
+ );
18
+ if (allActive)
19
+ return { selection_source: "all_active", runs: active, findings };
20
+ const id = runIdArg || runIdEnv;
21
+ if (id) {
22
+ const run = runs.find((r) => r.run_id === id);
23
+ if (!run?.valid) {
24
+ throw Error(
25
+ `AGDF_RUN_NOT_SELECTABLE:${id}; discovered=${runs.map((item) => item.run_id).join(",")}; next=select a valid run`,
26
+ );
27
+ }
28
+ return {
29
+ selection_source: runIdArg ? "cli" : "environment",
30
+ run,
31
+ active_run_ids: active.map((r) => r.run_id),
32
+ };
33
+ }
34
+ if (active.length !== 1)
35
+ throw Error(
36
+ active.length
37
+ ? `AGDF_ACTIVE_RUN_AMBIGUOUS:${active.map((r) => r.run_id)}`
38
+ : "AGDF_ACTIVE_RUN_MISSING",
39
+ );
40
+ return {
41
+ selection_source: "single_active",
42
+ run: active[0],
43
+ active_run_ids: [active[0].run_id],
44
+ };
45
+ }