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.
- package/README.md +9 -7
- package/bin/create-agdf.js +284 -152
- package/generated/.agdf/control/README.md +15 -3
- package/generated/.agdf/control/templates/RUN_STATE.md +48 -0
- package/generated/.github/skills/agdf-brownfield-analysis/SKILL.md +3 -3
- package/generated/.github/skills/agdf-delivery-path-search/SKILL.md +14 -0
- package/generated/.github/skills/agdf-gate-check/SKILL.md +15 -10
- package/generated/.github/skills/agdf-release-or/SKILL.md +1 -1
- package/generated/.github/skills/agdf-runtime-contract.md +24 -8
- package/generated/.opencode/agdf-runtime-contract.md +24 -8
- package/generated/.opencode/agents/agdf-brownfield-analysis.md +2 -2
- package/generated/.opencode/agents/agdf-delivery-path-search.md +14 -0
- package/generated/.opencode/agents/agdf-gate-check.md +15 -10
- package/generated/.opencode/agents/agdf-release-or.md +1 -1
- package/generated/plugins/agdf/.codex-plugin/plugin.json +1 -1
- package/generated/plugins/agdf/control/README.md +15 -3
- package/generated/plugins/agdf/control/templates/RUN_STATE.md +48 -0
- package/generated/plugins/agdf/hooks/session-start.sh +2 -0
- package/generated/plugins/agdf/meta/agdf-plugin.definition.json +1 -1
- package/generated/plugins/agdf/meta/agdf-runtime-contract.md +24 -8
- package/generated/plugins/agdf/skills/brownfield-analysis/SKILL.md +3 -3
- package/generated/plugins/agdf/skills/delivery-path-search/SKILL.md +14 -0
- package/generated/plugins/agdf/skills/gate-check/SKILL.md +15 -10
- package/generated/plugins/agdf/skills/release-or/SKILL.md +1 -1
- package/lib/control-state/aggregate.js +18 -0
- package/lib/control-state/index.js +6 -0
- package/lib/control-state/legacy-migration.js +94 -0
- package/lib/control-state/run-state-parser.js +196 -0
- package/lib/control-state/run-state-repository.js +68 -0
- package/lib/control-state/run-state-resolver.js +45 -0
- package/lib/control-state/run-state-writer.js +96 -0
- package/lib/delivery-path-search/candidate-policy.js +67 -2
- package/lib/delivery-path-search/contracts.js +127 -0
- package/lib/delivery-path-search/evaluators/claude.js +2 -16
- package/lib/delivery-path-search/evaluators/codex.js +2 -16
- package/lib/delivery-path-search/generators/claude.js +40 -0
- package/lib/delivery-path-search/generators/codex.js +38 -0
- package/lib/delivery-path-search/generators/protocol.js +12 -0
- package/lib/delivery-path-search/persistence.js +5 -0
- package/lib/delivery-path-search/search-engine.js +64 -2
- package/lib/delivery-path-search/state-adapter.js +119 -16
- package/lib/delivery-path-search/transports/read-only-guard.js +43 -0
- 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
|
|
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
|
-
|
|
205
|
-
another
|
|
206
|
-
changed and that it is unrelated; do not edit any other
|
|
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 `
|
|
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
|
-
-
|
|
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
|
|
376
|
-
It derives, but does not replace, the
|
|
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
|
|
51
|
-
- after `gate-check` permits implementation preparation or
|
|
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
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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 `
|
|
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,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
|
+
}
|