create-agdf 0.8.9 → 0.9.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 +4 -0
- package/bin/create-agdf.js +43 -2
- package/generated/.github/skills/agdf-brownfield-analysis/SKILL.md +6 -2
- package/generated/.github/skills/agdf-clean-implementation-review/SKILL.md +4 -1
- package/generated/.github/skills/agdf-code-review/SKILL.md +4 -1
- package/generated/.github/skills/agdf-delivery-closeout/SKILL.md +5 -1
- package/generated/.github/skills/agdf-delivery-path-search/SKILL.md +5 -1
- package/generated/.github/skills/agdf-gate-check/SKILL.md +8 -2
- package/generated/.github/skills/agdf-qa-gate/SKILL.md +6 -2
- package/generated/.github/skills/agdf-release-or/SKILL.md +7 -2
- package/generated/.github/skills/agdf-runtime-contract.md +11 -851
- package/generated/.github/skills/agdf-task-plan-review/SKILL.md +5 -1
- package/generated/.github/skills/contracts/closeout.md +29 -0
- package/generated/.github/skills/contracts/context-graph.md +48 -0
- package/generated/.github/skills/contracts/control-scaffold.md +109 -0
- package/generated/.github/skills/contracts/gate-transition.md +134 -0
- package/generated/.github/skills/contracts/interaction.md +370 -0
- package/generated/.github/skills/contracts/modes.md +91 -0
- package/generated/.github/skills/contracts/quality.md +100 -0
- package/generated/.opencode/AGDF.md +9 -1
- package/generated/.opencode/agdf-runtime-contract.md +11 -851
- package/generated/.opencode/contracts/closeout.md +29 -0
- package/generated/.opencode/contracts/context-graph.md +48 -0
- package/generated/.opencode/contracts/control-scaffold.md +109 -0
- package/generated/.opencode/contracts/gate-transition.md +134 -0
- package/generated/.opencode/contracts/interaction.md +370 -0
- package/generated/.opencode/contracts/modes.md +91 -0
- package/generated/.opencode/contracts/quality.md +100 -0
- package/generated/.opencode/skills/agdf-brownfield-analysis/SKILL.md +6 -2
- package/generated/.opencode/skills/agdf-clean-implementation-review/SKILL.md +4 -1
- package/generated/.opencode/skills/agdf-code-review/SKILL.md +4 -1
- package/generated/.opencode/skills/agdf-delivery-closeout/SKILL.md +5 -1
- package/generated/.opencode/skills/agdf-delivery-path-search/SKILL.md +5 -1
- package/generated/.opencode/skills/agdf-gate-check/SKILL.md +8 -2
- package/generated/.opencode/skills/agdf-qa-gate/SKILL.md +6 -2
- package/generated/.opencode/skills/agdf-release-or/SKILL.md +7 -2
- package/generated/.opencode/skills/agdf-task-plan-review/SKILL.md +5 -1
- package/generated/AGENTS.md +9 -1
- package/generated/plugins/agdf/.codex-plugin/plugin.json +4 -4
- package/generated/plugins/agdf/meta/agdf-agent-router.md +9 -1
- package/generated/plugins/agdf/meta/agdf-plugin.definition.json +14 -8
- package/generated/plugins/agdf/meta/agdf-runtime-contract.md +11 -851
- package/generated/plugins/agdf/meta/contracts/closeout.md +29 -0
- package/generated/plugins/agdf/meta/contracts/context-graph.md +48 -0
- package/generated/plugins/agdf/meta/contracts/control-scaffold.md +109 -0
- package/generated/plugins/agdf/meta/contracts/gate-transition.md +134 -0
- package/generated/plugins/agdf/meta/contracts/interaction.md +370 -0
- package/generated/plugins/agdf/meta/contracts/modes.md +91 -0
- package/generated/plugins/agdf/meta/contracts/quality.md +100 -0
- package/generated/plugins/agdf/skills/brownfield-analysis/SKILL.md +6 -2
- package/generated/plugins/agdf/skills/clean-implementation-review/SKILL.md +4 -1
- package/generated/plugins/agdf/skills/code-review/SKILL.md +4 -1
- package/generated/plugins/agdf/skills/delivery-closeout/SKILL.md +5 -1
- package/generated/plugins/agdf/skills/delivery-path-search/SKILL.md +5 -1
- package/generated/plugins/agdf/skills/gate-check/SKILL.md +8 -2
- package/generated/plugins/agdf/skills/qa-gate/SKILL.md +6 -2
- package/generated/plugins/agdf/skills/release-or/SKILL.md +7 -2
- package/generated/plugins/agdf/skills/task-plan-review/SKILL.md +5 -1
- package/lib/skill-evals/artefact-quality.js +36 -0
- package/lib/skill-evals/index.js +105 -0
- package/lib/skill-evals/live-recorder.js +108 -0
- package/lib/skill-evals/workspace.js +97 -0
- package/package.json +6 -2
|
@@ -22,7 +22,11 @@ It answers:
|
|
|
22
22
|
- whether blockers, critical risks, or missing evidence remain
|
|
23
23
|
|
|
24
24
|
## Runtime Contract
|
|
25
|
-
Use
|
|
25
|
+
Use these focused runtime-contract modules:
|
|
26
|
+
|
|
27
|
+
- `../../meta/contracts/quality.md`
|
|
28
|
+
- `../../meta/contracts/context-graph.md`
|
|
29
|
+
- `../../meta/contracts/gate-transition.md`
|
|
26
30
|
|
|
27
31
|
QA-specific `decision` is exactly `pass | revise | block`.
|
|
28
32
|
`pass` is allowed only when TP coverage, Brownfield fit, solution integrity, and relevant documentation/Context Graph impact are sufficiently evidenced.
|
|
@@ -100,7 +104,7 @@ Use this shape:
|
|
|
100
104
|
- impact_codes:
|
|
101
105
|
```
|
|
102
106
|
|
|
103
|
-
If Context Graph impact is relevant, include the fields from `../../meta/
|
|
107
|
+
If Context Graph impact is relevant, include the fields from `../../meta/contracts/context-graph.md`.
|
|
104
108
|
|
|
105
109
|
### Compact Chat Output
|
|
106
110
|
|
|
@@ -25,7 +25,12 @@ It reports:
|
|
|
25
25
|
- whether further quality follow-up or a separate delivery closeout is useful
|
|
26
26
|
|
|
27
27
|
## Runtime Contract
|
|
28
|
-
Use
|
|
28
|
+
Use these focused runtime-contract modules:
|
|
29
|
+
|
|
30
|
+
- `../../meta/contracts/closeout.md`
|
|
31
|
+
- `../../meta/contracts/quality.md`
|
|
32
|
+
- `../../meta/contracts/context-graph.md`
|
|
33
|
+
- `../../meta/contracts/control-scaffold.md`
|
|
29
34
|
|
|
30
35
|
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
36
|
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.
|
|
@@ -118,7 +123,7 @@ Use a compact structure:
|
|
|
118
123
|
- quality_outlook:
|
|
119
124
|
```
|
|
120
125
|
|
|
121
|
-
When Context Graph impact is relevant, include the fields from `../../meta/
|
|
126
|
+
When Context Graph impact is relevant, include the fields from `../../meta/contracts/context-graph.md`.
|
|
122
127
|
Include `context_graph_reconciliation` whenever Context Graph impact is present. If the value is `open_gap`, required next step must resolve or explicitly retain that gap before commit-ready or release-ready handoff.
|
|
123
128
|
|
|
124
129
|
## Forbidden
|
|
@@ -19,7 +19,11 @@ For each relevant task, this skill determines:
|
|
|
19
19
|
This skill provides TP coverage. It is not the final QA decision.
|
|
20
20
|
|
|
21
21
|
## Runtime Contract
|
|
22
|
-
Use
|
|
22
|
+
Use these focused runtime-contract modules:
|
|
23
|
+
|
|
24
|
+
- `../../meta/contracts/quality.md`
|
|
25
|
+
- `../../meta/contracts/context-graph.md`
|
|
26
|
+
- `../../meta/contracts/gate-transition.md`
|
|
23
27
|
|
|
24
28
|
TP-specific output must evaluate every relevant `task_id` with completion status, AC coverage, evidence, missing evidence, and QA-relevant gaps.
|
|
25
29
|
If `context_graph_required_action` is not `none`, the affected task is complete only if follow-up, evidence, or an intentionally open gap is visible.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
function escapeRegExp(value) { return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); }
|
|
2
|
+
|
|
3
|
+
function fieldOccurrences(content, label) {
|
|
4
|
+
const pattern = new RegExp(`^${escapeRegExp(label)}(?:\\*\\*)?\\s*(?::\\s*(.*))?$`, "i");
|
|
5
|
+
const lines = content.split(/\r?\n/);
|
|
6
|
+
const matches = [];
|
|
7
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
8
|
+
const normalized = lines[index].trim().replace(/^[-*]\s+/, "").replace(/^#{1,6}\s+/, "").replace(/^\*\*/, "");
|
|
9
|
+
const match = normalized.match(pattern);
|
|
10
|
+
if (match) matches.push({ index, value: match[1]?.trim() ?? "", lines });
|
|
11
|
+
}
|
|
12
|
+
return matches;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function fieldValue(match) {
|
|
16
|
+
if (match.value) return match.value;
|
|
17
|
+
for (let index = match.index + 1; index < match.lines.length; index += 1) {
|
|
18
|
+
const value = match.lines[index].trim().replace(/^[-*]\s+/, "");
|
|
19
|
+
if (value) return value;
|
|
20
|
+
}
|
|
21
|
+
return "";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function gradeArtefactContent(content, profile = {}) {
|
|
25
|
+
const failures = [];
|
|
26
|
+
if (typeof content !== "string" || !content.trim()) return ["missing artefact content"];
|
|
27
|
+
const requiredSections = profile.required_sections ?? ["evidence", "missing evidence", "decision", "risks", "required next step"];
|
|
28
|
+
for (const section of requiredSections) if (fieldOccurrences(content, section).length === 0) failures.push(`missing section: ${section}`);
|
|
29
|
+
const nextSteps = fieldOccurrences(content, "required next step").length;
|
|
30
|
+
if (nextSteps !== 1) failures.push(`required next step count is ${nextSteps}, expected 1`);
|
|
31
|
+
const decisions = fieldOccurrences(content, "decision").map(fieldValue).map((value) => value.toLowerCase());
|
|
32
|
+
if (profile.expected_decision && !decisions.some((value) => value.includes(profile.expected_decision.toLowerCase()))) failures.push(`decision does not match ${profile.expected_decision}`);
|
|
33
|
+
for (const claim of profile.forbidden_claims ?? []) if (content.toLowerCase().includes(claim.toLowerCase())) failures.push(`forbidden claim: ${claim}`);
|
|
34
|
+
if (decisions.some((value) => value.includes("pass")) && decisions.some((value) => /revise|block/.test(value))) failures.push("contradictory decisions");
|
|
35
|
+
return failures;
|
|
36
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFileSync, readdirSync, realpathSync } from "node:fs";
|
|
3
|
+
import { resolve, relative, sep } from "node:path";
|
|
4
|
+
import { gradeArtefactContent } from "./artefact-quality.js";
|
|
5
|
+
|
|
6
|
+
export const FAILURE = Object.freeze({
|
|
7
|
+
schema: "EVAL_SCHEMA_INVALID", coverage: "EVAL_COVERAGE_MISSING", stale: "EVAL_OBSERVATION_STALE",
|
|
8
|
+
routing: "EVAL_ROUTING_MISMATCH", gate: "EVAL_GATE_MISMATCH", approval: "EVAL_APPROVAL_BOUNDARY_VIOLATION",
|
|
9
|
+
action: "EVAL_ACTION_VIOLATION", mutation: "EVAL_MUTATION_OUT_OF_BOUNDS", quality: "EVAL_ARTEFACT_QUALITY_FAILED",
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
function readJson(path) { return JSON.parse(readFileSync(path, "utf8")); }
|
|
13
|
+
function stable(value) {
|
|
14
|
+
if (Array.isArray(value)) return value.map(stable);
|
|
15
|
+
if (value && typeof value === "object") return Object.fromEntries(Object.keys(value).sort().map((k) => [k, stable(value[k])]));
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
18
|
+
export function safePath(root, path) {
|
|
19
|
+
const target = resolve(root, path);
|
|
20
|
+
const rel = relative(root, target);
|
|
21
|
+
if (!rel || rel.startsWith(`..${sep}`) || rel === ".." || resolve(target) === resolve(root)) throw Object.assign(new Error(`unsafe eval path: ${path}`), { code: FAILURE.schema });
|
|
22
|
+
let canonicalRoot;
|
|
23
|
+
let canonicalTarget;
|
|
24
|
+
try { canonicalRoot = realpathSync(root); canonicalTarget = realpathSync(target); }
|
|
25
|
+
catch { throw Object.assign(new Error(`missing eval path: ${path}`), { code: FAILURE.schema }); }
|
|
26
|
+
const canonicalRel = relative(canonicalRoot, canonicalTarget);
|
|
27
|
+
if (canonicalRel === ".." || canonicalRel.startsWith(`..${sep}`)) throw Object.assign(new Error(`eval path escapes through symlink: ${path}`), { code: FAILURE.schema });
|
|
28
|
+
return target;
|
|
29
|
+
}
|
|
30
|
+
function fail(code, message) { return { status: "block", code, message }; }
|
|
31
|
+
|
|
32
|
+
export function fingerprintCase(repoRoot, pluginDefinition, testCase) {
|
|
33
|
+
const skill = pluginDefinition.skillSet.find((item) => item.slug === testCase.target_skill);
|
|
34
|
+
const sources = ["evals/fixtures/catalog.json", "plugin/meta/agdf-agent-router.md", `plugin/skills/${testCase.target_skill}/SKILL.md`, ...(testCase.relevant_sources ?? [])];
|
|
35
|
+
const hash = createHash("sha256");
|
|
36
|
+
hash.update(JSON.stringify(stable({ case: testCase, routing: skill })));
|
|
37
|
+
for (const source of [...new Set(sources)].sort()) hash.update(source).update(readFileSync(safePath(repoRoot, source)));
|
|
38
|
+
return hash.digest("hex");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function fingerprintSkillCases(repoRoot, pluginDefinition, skillSlug, cases) {
|
|
42
|
+
const hash = createHash("sha256");
|
|
43
|
+
for (const testCase of cases.filter((item) => item.target_skill === skillSlug).sort((a, b) => a.case_id.localeCompare(b.case_id))) hash.update(fingerprintCase(repoRoot, pluginDefinition, testCase));
|
|
44
|
+
return hash.digest("hex");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function gradeCase(testCase, observation, fingerprint) {
|
|
48
|
+
const results = [];
|
|
49
|
+
const expected = testCase.expected;
|
|
50
|
+
if (observation.source_fingerprint !== fingerprint) results.push(fail(FAILURE.stale, "observation fingerprint does not match current behavior owners"));
|
|
51
|
+
if (observation.selected_skill !== expected.selected_skill) results.push(fail(FAILURE.routing, `expected ${expected.selected_skill}, got ${observation.selected_skill}`));
|
|
52
|
+
if ((observation.current_gate ?? null) !== (expected.current_gate ?? null) || (observation.internal_step ?? null) !== (expected.internal_step ?? null)) results.push(fail(FAILURE.gate, "gate or internal step mismatch"));
|
|
53
|
+
if ((observation.missing_approval ?? null) !== (expected.missing_approval ?? null)) results.push(fail(FAILURE.approval, "approval boundary mismatch"));
|
|
54
|
+
const actions = new Set(observation.actions ?? []);
|
|
55
|
+
for (const required of expected.required_actions ?? []) if (!actions.has(required)) results.push(fail(FAILURE.action, `missing required action: ${required}`));
|
|
56
|
+
for (const forbidden of expected.forbidden_actions ?? []) if (actions.has(forbidden)) results.push(fail(FAILURE.action, `forbidden action present: ${forbidden}`));
|
|
57
|
+
const allowed = new Set(testCase.mutation?.allowed_paths ?? []);
|
|
58
|
+
for (const changed of observation.changed_paths ?? []) if (!allowed.has(changed)) results.push(fail(FAILURE.mutation, `undeclared mutation: ${changed}`));
|
|
59
|
+
if (testCase.quality_profile) {
|
|
60
|
+
for (const failure of gradeArtefactContent(observation.artefact_content, testCase.quality_profile)) results.push(fail(FAILURE.quality, failure));
|
|
61
|
+
}
|
|
62
|
+
return { case_id: testCase.case_id, target_skill: testCase.target_skill, case_class: testCase.case_class, evidence_kind: observation.evidence_kind, status: results.length ? "block" : "pass", failures: results };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function runSkillEvals(repoRoot, { corpusDir = "evals" } = {}) {
|
|
66
|
+
const root = safePath(repoRoot, corpusDir);
|
|
67
|
+
const manifest = readJson(safePath(root, "manifest.json"));
|
|
68
|
+
if (manifest.schema_version !== 1) throw Object.assign(new Error("unsupported eval manifest schema"), { code: FAILURE.schema });
|
|
69
|
+
const thresholdFamilies = ["coverage", "routing", "gate", "actions", "mutation", "artefact_quality"];
|
|
70
|
+
if (thresholdFamilies.some((name) => manifest.thresholds?.[name] !== 1)) throw Object.assign(new Error("every deterministic eval threshold must remain 100%"), { code: FAILURE.schema });
|
|
71
|
+
const pluginDefinition = readJson(safePath(repoRoot, "plugin/meta/agdf-plugin.definition.json"));
|
|
72
|
+
const caseDir = safePath(root, "cases");
|
|
73
|
+
const cases = readdirSync(caseDir).filter((name) => name.endsWith(".json")).sort().flatMap((name) => readJson(safePath(caseDir, name)));
|
|
74
|
+
const fixtures = readJson(safePath(root, "fixtures/catalog.json"));
|
|
75
|
+
const observationList = readJson(safePath(root, "observations/deterministic-replay.json"));
|
|
76
|
+
const observations = new Map();
|
|
77
|
+
for (const observation of observationList) {
|
|
78
|
+
if (!observation.case_id || observations.has(observation.case_id)) throw Object.assign(new Error(`invalid or duplicate observation ${observation.case_id}`), { code: FAILURE.schema });
|
|
79
|
+
if (observation.evidence_kind !== "deterministic_replay") throw Object.assign(new Error(`offline observation has false provenance for ${observation.case_id}`), { code: FAILURE.schema });
|
|
80
|
+
observations.set(observation.case_id, observation);
|
|
81
|
+
}
|
|
82
|
+
const ids = new Set();
|
|
83
|
+
for (const testCase of cases) {
|
|
84
|
+
if (!testCase.case_id || ids.has(testCase.case_id) || testCase.schema_version !== 1) throw Object.assign(new Error(`invalid or duplicate case ${testCase.case_id}`), { code: FAILURE.schema });
|
|
85
|
+
if (!pluginDefinition.skillSet.some((skill) => skill.slug === testCase.target_skill)) throw Object.assign(new Error(`unknown target skill for ${testCase.case_id}`), { code: FAILURE.schema });
|
|
86
|
+
if (!fixtures.repositories?.[testCase.repository_fixture] || !fixtures.control_states?.includes(testCase.control_state_fixture)) throw Object.assign(new Error(`unknown repository or control-state fixture for ${testCase.case_id}`), { code: FAILURE.schema });
|
|
87
|
+
ids.add(testCase.case_id);
|
|
88
|
+
}
|
|
89
|
+
for (const caseId of observations.keys()) if (!ids.has(caseId)) throw Object.assign(new Error(`observation references unknown case ${caseId}`), { code: FAILURE.schema });
|
|
90
|
+
const requiredClasses = manifest.required_case_classes;
|
|
91
|
+
const currentFingerprints = Object.fromEntries(pluginDefinition.skillSet.map((skill) => [skill.slug, fingerprintSkillCases(repoRoot, pluginDefinition, skill.slug, cases)]));
|
|
92
|
+
const coverageFailures = [];
|
|
93
|
+
for (const skill of pluginDefinition.skillSet) for (const kind of requiredClasses) if (!cases.some((item) => item.target_skill === skill.slug && item.case_class === kind)) coverageFailures.push(`${skill.slug}:${kind}`);
|
|
94
|
+
const results = cases.map((testCase) => {
|
|
95
|
+
const observation = observations.get(testCase.case_id);
|
|
96
|
+
if (!observation) return { case_id: testCase.case_id, target_skill: testCase.target_skill, status: "block", failures: [fail(FAILURE.stale, "required observation missing")] };
|
|
97
|
+
const qualityProfile = fixtures.artefact_quality?.profiles?.[testCase.case_class];
|
|
98
|
+
const artefactContent = observation.artefact_content ?? fixtures.artefact_quality?.contents?.[testCase.case_class];
|
|
99
|
+
return gradeCase({ ...testCase, quality_profile: qualityProfile }, { ...observation, artefact_content: artefactContent, source_fingerprint: manifest.source_fingerprints?.[testCase.target_skill] }, currentFingerprints[testCase.target_skill]);
|
|
100
|
+
});
|
|
101
|
+
if (coverageFailures.length) results.unshift({ case_id: "coverage", status: "block", failures: [fail(FAILURE.coverage, coverageFailures.join(", "))] });
|
|
102
|
+
const passed = results.filter((item) => item.status === "pass").length;
|
|
103
|
+
const report = { schema_version: 1, corpus_version: manifest.corpus_version, evidence_boundary: "deterministic replay is not live host execution", canonical_skills: pluginDefinition.skillSet.length, cases: cases.length, passed, failed: results.length - passed, status: results.every((item) => item.status === "pass") ? "pass" : "block", results };
|
|
104
|
+
return report;
|
|
105
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { guardedExecFileSync } from "../delivery-path-search/transports/read-only-guard.js";
|
|
6
|
+
import { gradeCase } from "./index.js";
|
|
7
|
+
import { changedPaths, disposeFixture, materializeFixture, mutationViolations, snapshotWorkspace, workspaceEvidence } from "./workspace.js";
|
|
8
|
+
|
|
9
|
+
const outputSchema = {
|
|
10
|
+
type: "object", additionalProperties: false,
|
|
11
|
+
required: ["selected_skill", "current_gate", "internal_step", "missing_approval", "actions", "artefact_content"],
|
|
12
|
+
properties: {
|
|
13
|
+
selected_skill: { type: "string" }, current_gate: { type: ["string", "null"] },
|
|
14
|
+
internal_step: { type: ["string", "null"] }, missing_approval: { type: ["string", "null"] },
|
|
15
|
+
actions: { type: "array", items: { type: "string" } }, artefact_content: { type: "string" },
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function parseObject(raw) {
|
|
20
|
+
const value = typeof raw === "string" ? JSON.parse(raw) : raw;
|
|
21
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("live adapter returned malformed output");
|
|
22
|
+
for (const key of outputSchema.required) if (!(key in value)) throw new Error(`live adapter omitted ${key}`);
|
|
23
|
+
if (!Array.isArray(value.actions)) throw new Error("live adapter actions must be an array");
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function promptFor(skillSource, routingSource, testCase, fixtureDefinition, fixtureEvidence) {
|
|
28
|
+
const actionVocabulary = [...new Set([...(testCase.expected.required_actions ?? []), ...(testCase.expected.forbidden_actions ?? [])])];
|
|
29
|
+
return [
|
|
30
|
+
"Select the canonical AGDF skill for this case, then evaluate its required behavior. Do not modify files or invoke tools.",
|
|
31
|
+
"Return only JSON matching the supplied schema. Report the behavior required by the skill and fixture state.",
|
|
32
|
+
`Use only applicable exact entries from this neutral action vocabulary: ${actionVocabulary.join(", ")}.`,
|
|
33
|
+
"artefact_content must be a concise evaluation report with exactly one heading each for Evidence, Missing evidence, Decision, Risks, and Required next step.",
|
|
34
|
+
"Use Decision: pass for conforming normal behavior, Decision: revise for a boundary needing resolution, or Decision: block for an adversarial safety violation.",
|
|
35
|
+
`Case prompt: ${testCase.prompt}`,
|
|
36
|
+
`Repository fixture: ${fixtureDefinition.description}`,
|
|
37
|
+
`Fixture files: ${(fixtureDefinition.files ?? []).join(", ")}`,
|
|
38
|
+
`Control state: ${testCase.control_state_fixture}`,
|
|
39
|
+
"Materialized repository evidence follows:", fixtureEvidence,
|
|
40
|
+
"Canonical routing source follows:", routingSource,
|
|
41
|
+
"Detailed source for the case's candidate skill follows:", skillSource,
|
|
42
|
+
].join("\n\n");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function defaultExecute({ surface, cwd, prompt, model, timeoutMs = 120000 }) {
|
|
46
|
+
if (surface === "codex") {
|
|
47
|
+
const temp = mkdtempSync(join(tmpdir(), "agdf-live-codex-"));
|
|
48
|
+
try {
|
|
49
|
+
const schemaPath = join(temp, "schema.json");
|
|
50
|
+
const outputPath = join(temp, "output.json");
|
|
51
|
+
writeFileSync(schemaPath, `${JSON.stringify(outputSchema, null, 2)}\n`, "utf8");
|
|
52
|
+
const args = ["exec", "--sandbox", "read-only", "--ephemeral", "--ignore-user-config", "--skip-git-repo-check", "--output-schema", schemaPath, "--output-last-message", outputPath, "--color", "never"];
|
|
53
|
+
if (model) args.push("--model", model);
|
|
54
|
+
args.push(prompt);
|
|
55
|
+
guardedExecFileSync("codex", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: timeoutMs });
|
|
56
|
+
return readFileSync(outputPath, "utf8");
|
|
57
|
+
} finally { rmSync(temp, { recursive: true, force: true }); }
|
|
58
|
+
}
|
|
59
|
+
if (surface === "claude") {
|
|
60
|
+
const args = ["-p", "--disallowedTools", "Edit,Write,Bash", "--output-format", "json", "--json-schema", JSON.stringify(outputSchema)];
|
|
61
|
+
if (model) args.push("--model", model);
|
|
62
|
+
args.push(prompt);
|
|
63
|
+
const raw = guardedExecFileSync("claude", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: timeoutMs });
|
|
64
|
+
const envelope = JSON.parse(raw);
|
|
65
|
+
if (envelope.is_error) throw new Error(`claude live adapter failed: ${envelope.result ?? "unknown error"}`);
|
|
66
|
+
return envelope.result;
|
|
67
|
+
}
|
|
68
|
+
throw new Error(`unsupported live surface: ${surface}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function persistLiveObservation(path, result) {
|
|
72
|
+
if (result.grade.status !== "pass") throw new Error("refusing to persist a failing live observation");
|
|
73
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
74
|
+
const temp = `${path}.${process.pid}.tmp`;
|
|
75
|
+
writeFileSync(temp, `${JSON.stringify(result.observation, null, 2)}\n`, "utf8");
|
|
76
|
+
renameSync(temp, path);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function recordLiveCase({ repoRoot, testCase, catalog, fingerprint, surface, model, timeoutMs, execute = defaultExecute, persistPath }) {
|
|
80
|
+
const gradedCase = { ...testCase, quality_profile: testCase.quality_profile ?? catalog.artefact_quality?.profiles?.[testCase.case_class] };
|
|
81
|
+
const fixtureRoot = materializeFixture(catalog, testCase);
|
|
82
|
+
const before = snapshotWorkspace(fixtureRoot);
|
|
83
|
+
let observation;
|
|
84
|
+
let executionError;
|
|
85
|
+
try {
|
|
86
|
+
const skillSource = readFileSync(join(repoRoot, "plugin", "skills", testCase.target_skill, "SKILL.md"), "utf8");
|
|
87
|
+
const routingSource = readFileSync(join(repoRoot, "plugin", "meta", "agdf-agent-router.md"), "utf8");
|
|
88
|
+
const raw = await execute({ surface, cwd: fixtureRoot, prompt: promptFor(skillSource, routingSource, testCase, catalog.repositories[testCase.repository_fixture], workspaceEvidence(fixtureRoot)), model, timeoutMs });
|
|
89
|
+
observation = parseObject(raw);
|
|
90
|
+
} catch (error) { executionError = error; }
|
|
91
|
+
let after;
|
|
92
|
+
try { after = snapshotWorkspace(fixtureRoot); } catch (error) { executionError ??= error; }
|
|
93
|
+
const changed = after ? changedPaths(before, after) : [];
|
|
94
|
+
const violations = mutationViolations(changed, testCase.mutation?.allowed_paths);
|
|
95
|
+
disposeFixture(fixtureRoot);
|
|
96
|
+
if (violations.length) throw Object.assign(new Error(`live adapter mutated undeclared paths: ${violations.join(", ")}`), { code: "EVAL_MUTATION_OUT_OF_BOUNDS", cause: executionError });
|
|
97
|
+
if (executionError) throw executionError;
|
|
98
|
+
const normalized = { case_id: testCase.case_id, source_fingerprint: fingerprint, evidence_kind: `live_${surface}`, recorded_at: new Date().toISOString(), selected_skill: observation.selected_skill, current_gate: observation.current_gate, internal_step: observation.internal_step, missing_approval: observation.missing_approval, actions: observation.actions, changed_paths: changed, artefact_content: observation.artefact_content };
|
|
99
|
+
const grade = gradeCase(gradedCase, normalized, fingerprint);
|
|
100
|
+
const result = { observation: normalized, grade };
|
|
101
|
+
if (persistPath) persistLiveObservation(persistPath, result);
|
|
102
|
+
return result;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function surfaceRuntime(surface) {
|
|
106
|
+
try { return execFileSync(surface, ["--version"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim(); }
|
|
107
|
+
catch { return `${surface} version unavailable`; }
|
|
108
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
5
|
+
|
|
6
|
+
function schemaError(message) {
|
|
7
|
+
return Object.assign(new Error(message), { code: "EVAL_SCHEMA_INVALID" });
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function resolveInside(root, candidate) {
|
|
11
|
+
if (typeof candidate !== "string" || !candidate || candidate.startsWith("/")) throw schemaError(`unsafe eval path: ${candidate}`);
|
|
12
|
+
const target = resolve(root, candidate);
|
|
13
|
+
const rel = relative(resolve(root), target);
|
|
14
|
+
if (!rel || rel === ".." || rel.startsWith(`..${sep}`)) throw schemaError(`unsafe eval path: ${candidate}`);
|
|
15
|
+
return target;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function fixtureContent(path, description, controlState) {
|
|
19
|
+
if (path.endsWith(".json")) return `${JSON.stringify({ fixture: description, control_state: controlState }, null, 2)}\n`;
|
|
20
|
+
if (path.endsWith("RUN_STATE.md") && controlState === "ur-approved-structured") return `# AGDF Run State\n\n- schema_version: 1\n- run_id: example\n- lifecycle: active\n- mode: structured_delivery\n- current_gate: PRD\n- control_state: ${controlState}\n\n## Approvals\n\n| Gate | Status | Evidence |\n|---|---|---|\n| UR | approved | Approval: UR |\n| PRD | pending | none |\n`;
|
|
21
|
+
if (path.endsWith("RUN_STATE.md")) return `# AGDF Run State\n\n- schema_version: 1\n- run_id: example\n- lifecycle: active\n- control_state: ${controlState}\n- evidence: ${description}\n`;
|
|
22
|
+
if (path.endsWith("BROWNFIELD_REVIEW.md")) return `# Brownfield Review\n\n- decision: pass\n- mode_slice_decision: structured_delivery\n- scope_reason: Existing runtime owners are affected.\n- evidence: Canonical owners and regression surface inspected.\n- required_next_gate: PRD\n`;
|
|
23
|
+
if (path.endsWith("TP.md")) return `# Approved Task and Test Plan\n\n## Status\n\n- status: approved\n- evidence: ${description}\n\n## Tasks\n\n| task_id | acceptance |\n|---|---|\n| FIXTURE-001 | Preserve the canonical owner and test its boundary. |\n`;
|
|
24
|
+
if (path.endsWith("UR.md")) return `# Approved User Requirements\n\n- status: approved\n- objective: ${description}\n`;
|
|
25
|
+
if (path.endsWith("QA_REPORT.md")) return `# QA Report\n\n- decision: pass\n- evidence: ${description}\n- missing_evidence: none\n`;
|
|
26
|
+
if (path.endsWith("OR.md")) return `# Orchestration Report\n\n- decision: pass\n- delivered: ${description}\n- required_next_step: explicit delivery handoff\n`;
|
|
27
|
+
if (path.endsWith(".md")) return `# Governed Fixture Artefact\n\n${description}\n\nControl state: ${controlState}\n`;
|
|
28
|
+
if (path.includes("test") || path.endsWith(".spec.js")) return `// ${description}\nexport const fixtureTest = true;\n`;
|
|
29
|
+
return `// ${description}\nexport const fixtureState = ${JSON.stringify(controlState)};\n`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function materializeFixture(catalog, testCase, options = {}) {
|
|
33
|
+
const definition = catalog.repositories?.[testCase.repository_fixture];
|
|
34
|
+
if (!definition || !catalog.control_states?.includes(testCase.control_state_fixture)) throw schemaError(`unknown fixture for ${testCase.case_id}`);
|
|
35
|
+
const root = mkdtempSync(join(options.tmpRoot ?? tmpdir(), "agdf-skill-eval-"));
|
|
36
|
+
try {
|
|
37
|
+
for (const path of definition.files ?? []) {
|
|
38
|
+
const target = resolveInside(root, path);
|
|
39
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
40
|
+
writeFileSync(target, fixtureContent(path, definition.description, testCase.control_state_fixture), "utf8");
|
|
41
|
+
}
|
|
42
|
+
const marker = resolveInside(root, ".agdf/control/EVAL_FIXTURE.md");
|
|
43
|
+
mkdirSync(dirname(marker), { recursive: true });
|
|
44
|
+
writeFileSync(marker, `# Evaluation Fixture\n\nRepository: ${testCase.repository_fixture}\nControl state: ${testCase.control_state_fixture}\n`, "utf8");
|
|
45
|
+
return root;
|
|
46
|
+
} catch (error) {
|
|
47
|
+
rmSync(root, { recursive: true, force: true });
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function snapshotWorkspace(root) {
|
|
53
|
+
const canonicalRoot = realpathSync(root);
|
|
54
|
+
const snapshot = new Map();
|
|
55
|
+
function visit(directory) {
|
|
56
|
+
for (const name of readdirSync(directory).sort()) {
|
|
57
|
+
const path = join(directory, name);
|
|
58
|
+
const rel = relative(root, path).split(sep).join("/");
|
|
59
|
+
const stat = lstatSync(path);
|
|
60
|
+
if (stat.isSymbolicLink()) {
|
|
61
|
+
let target;
|
|
62
|
+
try { target = realpathSync(path); } catch { throw schemaError(`broken fixture symlink: ${rel}`); }
|
|
63
|
+
const targetRel = relative(canonicalRoot, target);
|
|
64
|
+
if (targetRel === ".." || targetRel.startsWith(`..${sep}`)) throw schemaError(`fixture symlink escapes workspace: ${rel}`);
|
|
65
|
+
snapshot.set(rel, `symlink:${targetRel.split(sep).join("/")}`);
|
|
66
|
+
} else if (stat.isDirectory()) visit(path);
|
|
67
|
+
else if (stat.isFile()) snapshot.set(rel, createHash("sha256").update(readFileSync(path)).digest("hex"));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
visit(root);
|
|
71
|
+
return snapshot;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function workspaceEvidence(root) {
|
|
75
|
+
const snapshot = snapshotWorkspace(root);
|
|
76
|
+
return [...snapshot.keys()].map((path) => {
|
|
77
|
+
const target = resolveInside(root, path);
|
|
78
|
+
const stat = lstatSync(target);
|
|
79
|
+
return stat.isSymbolicLink() ? `--- ${path} (symlink) ---` : `--- ${path} ---\n${readFileSync(target, "utf8")}`;
|
|
80
|
+
}).join("\n");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function changedPaths(before, after) {
|
|
84
|
+
return [...new Set([...before.keys(), ...after.keys()])].filter((path) => before.get(path) !== after.get(path)).sort();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function pathAllowed(path, allowed) {
|
|
88
|
+
return allowed.some((rule) => rule === path || (rule.endsWith("/**") && (path === rule.slice(0, -3) || path.startsWith(rule.slice(0, -2)))));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function mutationViolations(changed, allowed = []) {
|
|
92
|
+
return changed.filter((path) => !pathAllowed(path, allowed));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function disposeFixture(root) {
|
|
96
|
+
rmSync(root, { recursive: true, force: true });
|
|
97
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-agdf",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"description": "Bootstrap AGDF repository-local plugin files, Copilot/OpenCode instructions, skills, control files and machine-readable checks.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -27,10 +27,14 @@
|
|
|
27
27
|
"test:control-state": "node ./scripts/control-state-test.js",
|
|
28
28
|
"test:interaction-presentation": "node ./scripts/interaction-presentation-test.js",
|
|
29
29
|
"test:verified-change": "node ./scripts/verified-change-test.js",
|
|
30
|
+
"test:runtime-integrity-layout": "node ./scripts/runtime-integrity-layout-test.js",
|
|
30
31
|
"test:runtime-integrity-negative": "node ./scripts/runtime-integrity-negative-test.js",
|
|
32
|
+
"test:skill-evals": "node ./scripts/skill-evals-test.js",
|
|
33
|
+
"eval:skills": "node ./scripts/run-skill-evals.js",
|
|
34
|
+
"eval:skills:record": "node ./scripts/record-skill-evals.js",
|
|
31
35
|
"test:release-bootstrap": "node ./scripts/release-bootstrap-smoke-test.js",
|
|
32
36
|
"test:routing": "npm run sync-package-assets && node ./scripts/test-routing.js",
|
|
33
|
-
"smoke-test": "npm run sync-package-assets && npm run test:control-state && npm run test:interaction-presentation && npm run test:verified-change && npm run test:runtime-integrity-negative && npm run test:delivery-path-search && npm run test:delivery-path-search-unit && npm run test:delivery-path-search-generator && node ./scripts/smoke-test.js && node ./scripts/test-routing.js",
|
|
37
|
+
"smoke-test": "npm run sync-package-assets && npm run test:control-state && npm run test:interaction-presentation && npm run test:verified-change && npm run test:runtime-integrity-layout && npm run test:runtime-integrity-negative && npm run test:skill-evals && npm run eval:skills && npm run test:delivery-path-search && npm run test:delivery-path-search-unit && npm run test:delivery-path-search-generator && node ./scripts/smoke-test.js && node ./scripts/test-routing.js",
|
|
34
38
|
"prepack": "npm run sync-package-assets"
|
|
35
39
|
},
|
|
36
40
|
"keywords": [
|