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.
- package/README.md +5 -4
- package/bin/create-agdf.js +210 -148
- 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-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 +18 -8
- package/generated/.opencode/agdf-runtime-contract.md +18 -8
- package/generated/.opencode/agents/agdf-brownfield-analysis.md +2 -2
- 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 +18 -8
- package/generated/plugins/agdf/skills/brownfield-analysis/SKILL.md +3 -3
- 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/state-adapter.js +94 -23
- package/package.json +3 -2
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
closeSync,
|
|
4
|
+
fsyncSync,
|
|
5
|
+
existsSync,
|
|
6
|
+
lstatSync,
|
|
7
|
+
openSync,
|
|
8
|
+
readFileSync,
|
|
9
|
+
renameSync,
|
|
10
|
+
unlinkSync,
|
|
11
|
+
writeFileSync,
|
|
12
|
+
} from "node:fs";
|
|
13
|
+
import { dirname } from "node:path";
|
|
14
|
+
|
|
15
|
+
import { parseRunState } from "./run-state-parser.js";
|
|
16
|
+
|
|
17
|
+
function fsyncDirectory(path) {
|
|
18
|
+
const descriptor = openSync(path, "r");
|
|
19
|
+
try {
|
|
20
|
+
fsyncSync(descriptor);
|
|
21
|
+
} finally {
|
|
22
|
+
closeSync(descriptor);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function atomicWrite(path, content) {
|
|
27
|
+
if (existsSync(path)) {
|
|
28
|
+
const destination = lstatSync(path);
|
|
29
|
+
if (destination.isSymbolicLink() || !destination.isFile()) {
|
|
30
|
+
throw new Error("AGDF_RUN_PATH_INVALID");
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const parent = lstatSync(dirname(path));
|
|
35
|
+
if (parent.isSymbolicLink() || !parent.isDirectory()) {
|
|
36
|
+
throw new Error("AGDF_RUN_PATH_INVALID");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const temp = `${path}.tmp-${process.pid}-${randomUUID()}`;
|
|
40
|
+
let descriptor;
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
descriptor = openSync(temp, "wx");
|
|
44
|
+
writeFileSync(descriptor, content, "utf8");
|
|
45
|
+
fsyncSync(descriptor);
|
|
46
|
+
closeSync(descriptor);
|
|
47
|
+
descriptor = undefined;
|
|
48
|
+
|
|
49
|
+
renameSync(temp, path);
|
|
50
|
+
fsyncDirectory(dirname(path));
|
|
51
|
+
} catch (error) {
|
|
52
|
+
if (descriptor !== undefined) closeSync(descriptor);
|
|
53
|
+
try {
|
|
54
|
+
unlinkSync(temp);
|
|
55
|
+
} catch (cleanupError) {
|
|
56
|
+
if (cleanupError.code !== "ENOENT") throw cleanupError;
|
|
57
|
+
}
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function writeRun(path, content, expectedRevisionId) {
|
|
63
|
+
const lockPath = `${path}.lock`;
|
|
64
|
+
let lockDescriptor;
|
|
65
|
+
try {
|
|
66
|
+
lockDescriptor = openSync(lockPath, "wx");
|
|
67
|
+
} catch (error) {
|
|
68
|
+
if (error.code === "EEXIST") throw new Error("AGDF_RUN_WRITE_LOCKED");
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const current = parseRunState(readFileSync(path, "utf8"));
|
|
74
|
+
if (!current.valid) throw new Error("AGDF_RUN_STATE_INVALID");
|
|
75
|
+
if (current.meta.revision_id !== expectedRevisionId) {
|
|
76
|
+
throw new Error("AGDF_STALE_RUN_REVISION");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const next = content
|
|
80
|
+
.replace(
|
|
81
|
+
/^- revision:\s*.*$/m,
|
|
82
|
+
`- revision: ${Number(current.meta.revision) + 1}`,
|
|
83
|
+
)
|
|
84
|
+
.replace(/^- revision_id:\s*.*$/m, `- revision_id: ${randomUUID()}`);
|
|
85
|
+
const candidate = parseRunState(next);
|
|
86
|
+
if (!candidate.valid || candidate.meta.run_id !== current.meta.run_id) {
|
|
87
|
+
throw new Error("AGDF_RUN_STATE_INVALID");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
atomicWrite(path, next);
|
|
91
|
+
return candidate;
|
|
92
|
+
} finally {
|
|
93
|
+
closeSync(lockDescriptor);
|
|
94
|
+
unlinkSync(lockPath);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -1,16 +1,34 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
CONTRACT_VERSION,
|
|
5
|
+
GENERATOR_CONTRACT_VERSION,
|
|
6
|
+
validateGeneratorRequest,
|
|
7
|
+
} from "./contracts.js";
|
|
8
|
+
import { resolveRuns, verifyLegacyProjection } from "../control-state/index.js";
|
|
4
9
|
|
|
5
10
|
function section(content, heading) {
|
|
6
|
-
return
|
|
11
|
+
return (
|
|
12
|
+
content.match(
|
|
13
|
+
new RegExp(`## ${heading}\\r?\\n([\\s\\S]*?)(?=\\r?\\n## |$)`),
|
|
14
|
+
)?.[1] ?? ""
|
|
15
|
+
);
|
|
7
16
|
}
|
|
8
17
|
|
|
9
18
|
function tableAnswers(content) {
|
|
10
19
|
const answers = {};
|
|
11
20
|
for (const line of section(content, "Current Control State").split(/\r?\n/)) {
|
|
12
|
-
const cells = line
|
|
13
|
-
|
|
21
|
+
const cells = line
|
|
22
|
+
.split("|")
|
|
23
|
+
.slice(1, -1)
|
|
24
|
+
.map((cell) => cell.trim());
|
|
25
|
+
if (
|
|
26
|
+
cells.length >= 2 &&
|
|
27
|
+
cells[0] &&
|
|
28
|
+
!cells[0].startsWith("---") &&
|
|
29
|
+
cells[0] !== "Question"
|
|
30
|
+
)
|
|
31
|
+
answers[cells[0]] = cells[1];
|
|
14
32
|
}
|
|
15
33
|
return answers;
|
|
16
34
|
}
|
|
@@ -18,21 +36,52 @@ function tableAnswers(content) {
|
|
|
18
36
|
function statusCard(content) {
|
|
19
37
|
const card = {};
|
|
20
38
|
for (const line of section(content, "Run Status Card").split(/\r?\n/)) {
|
|
21
|
-
const cells = line
|
|
22
|
-
|
|
39
|
+
const cells = line
|
|
40
|
+
.split("|")
|
|
41
|
+
.slice(1, -1)
|
|
42
|
+
.map((cell) => cell.trim().replace(/^`|`$/g, ""));
|
|
43
|
+
if (
|
|
44
|
+
cells.length >= 2 &&
|
|
45
|
+
cells[0] &&
|
|
46
|
+
!cells[0].startsWith("---") &&
|
|
47
|
+
cells[0] !== "Run status"
|
|
48
|
+
)
|
|
49
|
+
card[cells[0]] = cells[1];
|
|
23
50
|
}
|
|
24
51
|
return card;
|
|
25
52
|
}
|
|
26
53
|
|
|
27
54
|
function listCell(value) {
|
|
28
|
-
return String(value ?? "")
|
|
55
|
+
return String(value ?? "")
|
|
56
|
+
.split(/\s*;\s*/)
|
|
57
|
+
.map((item) => item.trim())
|
|
58
|
+
.filter(Boolean);
|
|
29
59
|
}
|
|
30
60
|
|
|
31
61
|
export function searchInputFromControl(targetDir, options = {}) {
|
|
32
|
-
const
|
|
33
|
-
if (
|
|
62
|
+
const canonicalRoot = join(targetDir, ".agdf", "control", "runs");
|
|
63
|
+
if (existsSync(canonicalRoot)) {
|
|
64
|
+
const projection = verifyLegacyProjection(targetDir);
|
|
65
|
+
if (!["absent", "valid"].includes(projection.status)) {
|
|
66
|
+
throw new Error(`AGDF_LEGACY_PROJECTION_DRIFT:${projection.status}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const resolved = existsSync(canonicalRoot)
|
|
70
|
+
? resolveRuns(targetDir, {
|
|
71
|
+
runIdArg: options.scopeKey,
|
|
72
|
+
runIdEnv: process.env.AGDF_RUN_ID,
|
|
73
|
+
})
|
|
74
|
+
: null;
|
|
75
|
+
const runPath =
|
|
76
|
+
resolved?.run.path ?? join(targetDir, ".agdf", "control", "AGDF_RUN.md");
|
|
77
|
+
if (!existsSync(runPath)) throw new Error("missing AGDF run state");
|
|
34
78
|
const content = readFileSync(runPath, "utf8");
|
|
35
|
-
const meta = Object.fromEntries(
|
|
79
|
+
const meta = Object.fromEntries(
|
|
80
|
+
[...content.matchAll(/^- ([a-z_]+):\s*(.+)$/gm)].map((match) => [
|
|
81
|
+
match[1],
|
|
82
|
+
match[2].replace(/^`|`$/g, "").trim(),
|
|
83
|
+
]),
|
|
84
|
+
);
|
|
36
85
|
const answers = tableAnswers(content);
|
|
37
86
|
const card = statusCard(content);
|
|
38
87
|
const result = {
|
|
@@ -41,11 +90,29 @@ export function searchInputFromControl(targetDir, options = {}) {
|
|
|
41
90
|
objective: section(content, "Objective").trim(),
|
|
42
91
|
current_gate: card["Current gate"] ?? meta.current_gate ?? "unknown",
|
|
43
92
|
allowed_actions: options.allowedActions ?? listCell(card["Allowed now"]),
|
|
44
|
-
forbidden_actions:
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
93
|
+
forbidden_actions:
|
|
94
|
+
options.forbiddenActions ??
|
|
95
|
+
listCell(answers["What is explicitly forbidden right now?"]),
|
|
96
|
+
evidence_refs: [
|
|
97
|
+
...content.matchAll(
|
|
98
|
+
/\|\s*([^|]+)\s*\|\s*([^|]+)\s*\|\s*([^|]+)\s*\|\s*(direct|indirect)\s*\|/g,
|
|
99
|
+
),
|
|
100
|
+
]
|
|
101
|
+
.slice(1)
|
|
102
|
+
.map((match) => `${match[1].trim()}: ${match[2].trim()}`),
|
|
103
|
+
risks: section(content, "Risks")
|
|
104
|
+
.split(/\r?\n/)
|
|
105
|
+
.filter(
|
|
106
|
+
(line) =>
|
|
107
|
+
line.startsWith("|") &&
|
|
108
|
+
!line.includes("---") &&
|
|
109
|
+
!line.includes("| Risk |"),
|
|
110
|
+
)
|
|
111
|
+
.map((line) => line.split("|")[1].trim()),
|
|
112
|
+
enforcement: options.enforcement ?? {
|
|
113
|
+
level: "instruction_only",
|
|
114
|
+
evidence: ["surface instruction prohibits implementation during search"],
|
|
115
|
+
},
|
|
49
116
|
budgets: {
|
|
50
117
|
max_candidates: options.maxCandidates ?? 5,
|
|
51
118
|
max_depth: options.maxDepth ?? 2,
|
|
@@ -55,13 +122,14 @@ export function searchInputFromControl(targetDir, options = {}) {
|
|
|
55
122
|
stability_window: options.stabilityWindow ?? 3,
|
|
56
123
|
},
|
|
57
124
|
};
|
|
58
|
-
if (options.generation?.enabled)
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
125
|
+
if (options.generation?.enabled)
|
|
126
|
+
result.generation = {
|
|
127
|
+
enabled: true,
|
|
128
|
+
max_calls: 1,
|
|
129
|
+
max_proposals: options.generation.maxProposals ?? 5,
|
|
130
|
+
max_duration_ms: options.generation.maxDurationMs ?? 30000,
|
|
131
|
+
max_cost_units: options.generation.maxCostUnits ?? 5,
|
|
132
|
+
};
|
|
65
133
|
return result;
|
|
66
134
|
}
|
|
67
135
|
|
|
@@ -78,7 +146,10 @@ export function generatorRequestFromInput(input) {
|
|
|
78
146
|
evidence: input.evidence_refs ?? [],
|
|
79
147
|
missing_evidence: [],
|
|
80
148
|
risks: input.risks ?? [],
|
|
81
|
-
constraints: [
|
|
149
|
+
constraints: [
|
|
150
|
+
"advisory only",
|
|
151
|
+
"canonical gate-check remains authoritative",
|
|
152
|
+
],
|
|
82
153
|
enforcement: input.enforcement,
|
|
83
154
|
budgets: {
|
|
84
155
|
max_calls: input.generation.max_calls,
|