create-agdf 0.4.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +37 -0
  2. package/bin/create-agdf.js +249 -8
  3. package/generated/.agdf/control/templates/AGDF_RUN.md +1 -0
  4. package/generated/.agdf/control/templates/artefacts/OR.md +1 -0
  5. package/generated/.github/copilot-instructions.md +1 -0
  6. package/generated/.github/skills/README.md +1 -0
  7. package/generated/.github/skills/agdf-delivery-closeout/SKILL.md +9 -4
  8. package/generated/.github/skills/agdf-delivery-path-search/SKILL.md +75 -0
  9. package/generated/.github/skills/agdf-release-or/SKILL.md +8 -4
  10. package/generated/.github/skills/agdf-runtime-contract.md +24 -0
  11. package/generated/.opencode/AGDF.md +3 -0
  12. package/generated/.opencode/README.md +3 -0
  13. package/generated/.opencode/agdf-runtime-contract.md +24 -0
  14. package/generated/.opencode/agents/agdf-delivery-closeout.md +9 -4
  15. package/generated/.opencode/agents/agdf-delivery-path-search.md +76 -0
  16. package/generated/.opencode/agents/agdf-release-or.md +8 -4
  17. package/generated/AGENTS.md +2 -0
  18. package/generated/plugins/agdf/.codex-plugin/plugin.json +1 -1
  19. package/generated/plugins/agdf/control/templates/AGDF_RUN.md +1 -0
  20. package/generated/plugins/agdf/control/templates/artefacts/OR.md +1 -0
  21. package/generated/plugins/agdf/meta/agdf-agent-router.md +2 -0
  22. package/generated/plugins/agdf/meta/agdf-plugin.definition.json +6 -1
  23. package/generated/plugins/agdf/meta/agdf-runtime-contract.md +24 -0
  24. package/generated/plugins/agdf/skills/delivery-closeout/SKILL.md +9 -4
  25. package/generated/plugins/agdf/skills/delivery-path-search/SKILL.md +75 -0
  26. package/generated/plugins/agdf/skills/delivery-path-search/help.md +5 -0
  27. package/generated/plugins/agdf/skills/release-or/SKILL.md +8 -4
  28. package/lib/delivery-path-search/candidate-policy.js +34 -0
  29. package/lib/delivery-path-search/contracts.js +110 -0
  30. package/lib/delivery-path-search/evaluators/codex.js +67 -0
  31. package/lib/delivery-path-search/evaluators/protocol.js +13 -0
  32. package/lib/delivery-path-search/persistence.js +34 -0
  33. package/lib/delivery-path-search/scoring.js +21 -0
  34. package/lib/delivery-path-search/search-engine.js +92 -0
  35. package/lib/delivery-path-search/state-adapter.js +58 -0
  36. package/lib/delivery-path-search/surfaces/capabilities.js +14 -0
  37. package/opencode-plugin.js +17 -5
  38. package/package.json +4 -2
@@ -0,0 +1,110 @@
1
+ export const CONTRACT_VERSION = "1";
2
+ export const SCORING_POLICY_VERSION = "1";
3
+ export const SCORE_DIMENSIONS = [
4
+ "scope_fit",
5
+ "gate_readiness",
6
+ "risk_reduction",
7
+ "evidence_gain",
8
+ "testability",
9
+ "reversibility",
10
+ "cost",
11
+ ];
12
+
13
+ function requireObject(value, label) {
14
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${label} must be an object`);
15
+ return value;
16
+ }
17
+
18
+ function requireString(value, label) {
19
+ if (typeof value !== "string" || !value.trim()) throw new Error(`${label} must be a non-empty string`);
20
+ return value.trim();
21
+ }
22
+
23
+ function stringArray(value, label) {
24
+ if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) throw new Error(`${label} must be a string array`);
25
+ return value.map((item) => item.trim()).filter(Boolean);
26
+ }
27
+
28
+ export function validateSearchInput(value) {
29
+ const input = requireObject(value, "search input");
30
+ if (input.contract_version !== CONTRACT_VERSION) throw new Error(`unsupported search input contract_version: ${input.contract_version}`);
31
+ requireString(input.scope_key, "scope_key");
32
+ if (!/^[a-z0-9][a-z0-9._-]*$/i.test(input.scope_key) || input.scope_key.includes("..")) {
33
+ throw new Error("scope_key must be a safe path segment");
34
+ }
35
+ requireString(input.objective, "objective");
36
+ requireString(input.current_gate, "current_gate");
37
+ stringArray(input.allowed_actions, "allowed_actions");
38
+ stringArray(input.forbidden_actions, "forbidden_actions");
39
+ stringArray(input.evidence_refs ?? [], "evidence_refs");
40
+ stringArray(input.risks ?? [], "risks");
41
+ validateEnforcement(input.enforcement);
42
+ const budgets = requireObject(input.budgets, "budgets");
43
+ for (const key of ["max_candidates", "max_depth", "max_evaluations", "max_duration_ms", "max_cost_units", "stability_window"]) {
44
+ if (!Number.isInteger(budgets[key]) || budgets[key] < 1) throw new Error(`budgets.${key} must be a positive integer`);
45
+ }
46
+ return structuredClone(input);
47
+ }
48
+
49
+ export function validateCandidate(value) {
50
+ const candidate = requireObject(value, "candidate");
51
+ requireString(candidate.id, "candidate.id");
52
+ requireString(candidate.action, "candidate.action");
53
+ stringArray(candidate.expected_evidence ?? [], "candidate.expected_evidence");
54
+ stringArray(candidate.tests ?? [], "candidate.tests");
55
+ stringArray(candidate.assumptions ?? [], "candidate.assumptions");
56
+ return structuredClone(candidate);
57
+ }
58
+
59
+ export function validateEvaluation(value, candidateId) {
60
+ const evaluation = requireObject(value, "evaluation");
61
+ if (evaluation.contract_version !== CONTRACT_VERSION) throw new Error(`unsupported evaluator contract_version: ${evaluation.contract_version}`);
62
+ if (evaluation.candidate_id !== candidateId) throw new Error(`evaluation candidate_id must be ${candidateId}`);
63
+ for (const dimension of SCORE_DIMENSIONS) {
64
+ if (!Number.isInteger(evaluation[dimension]) || evaluation[dimension] < 0 || evaluation[dimension] > 5) {
65
+ throw new Error(`${dimension} must be an integer from 0 to 5`);
66
+ }
67
+ }
68
+ if (!Number.isInteger(evaluation.uncertainty) || evaluation.uncertainty < 0 || evaluation.uncertainty > 5) {
69
+ throw new Error("uncertainty must be an integer from 0 to 5");
70
+ }
71
+ requireString(evaluation.rationale, "evaluation.rationale");
72
+ stringArray(evaluation.risks ?? [], "evaluation.risks");
73
+ stringArray(evaluation.assumptions ?? [], "evaluation.assumptions");
74
+ stringArray(evaluation.child_actions ?? [], "evaluation.child_actions");
75
+ if (/(^|\s)(rm|sudo|curl|wget|bash|sh|exec)\s/i.test(evaluation.rationale)) {
76
+ throw new Error("evaluation rationale contains executable instructions");
77
+ }
78
+ return structuredClone(evaluation);
79
+ }
80
+
81
+ export function validateEnforcement(value) {
82
+ const enforcement = requireObject(value, "enforcement");
83
+ if (!["full", "tool_enforced", "instruction_only"].includes(enforcement.level)) {
84
+ throw new Error("enforcement.level must be full, tool_enforced or instruction_only");
85
+ }
86
+ const evidence = stringArray(enforcement.evidence ?? [], "enforcement.evidence");
87
+ if (enforcement.level !== "instruction_only" && evidence.length === 0) {
88
+ throw new Error(`${enforcement.level} requires enforcement evidence`);
89
+ }
90
+ return { level: enforcement.level, evidence };
91
+ }
92
+
93
+ export function evaluatorOutputSchema() {
94
+ const scoreProperties = Object.fromEntries(SCORE_DIMENSIONS.map((key) => [key, { type: "integer", minimum: 0, maximum: 5 }]));
95
+ return {
96
+ type: "object",
97
+ additionalProperties: false,
98
+ required: ["contract_version", "candidate_id", ...SCORE_DIMENSIONS, "uncertainty", "rationale", "risks", "assumptions", "child_actions"],
99
+ properties: {
100
+ contract_version: { type: "string", const: CONTRACT_VERSION },
101
+ candidate_id: { type: "string" },
102
+ ...scoreProperties,
103
+ uncertainty: { type: "integer", minimum: 0, maximum: 5 },
104
+ rationale: { type: "string", minLength: 1, maxLength: 1000 },
105
+ risks: { type: "array", items: { type: "string" } },
106
+ assumptions: { type: "array", items: { type: "string" } },
107
+ child_actions: { type: "array", items: { type: "string" }, maxItems: 5 },
108
+ },
109
+ };
110
+ }
@@ -0,0 +1,67 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { evaluatorOutputSchema, validateEvaluation } from "../contracts.js";
6
+
7
+ export function codexEvaluator(options = {}) {
8
+ const codexBin = options.codexBin ?? "codex";
9
+ let runtime = "codex version unavailable";
10
+ try {
11
+ runtime = execFileSync(codexBin, ["--version"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
12
+ } catch {}
13
+
14
+ function gitState() {
15
+ try {
16
+ return execFileSync("git", ["status", "--porcelain=v1", "-z"], {
17
+ cwd: options.cwd,
18
+ encoding: "utf8",
19
+ stdio: ["ignore", "pipe", "ignore"],
20
+ });
21
+ } catch {
22
+ return null;
23
+ }
24
+ }
25
+
26
+ return {
27
+ name: "codex",
28
+ metadata: { name: "codex", runtime, model: options.model ?? "configured-default" },
29
+ async evaluate(input, candidate) {
30
+ const temp = mkdtempSync(join(tmpdir(), "agdf-dps-"));
31
+ const schemaPath = join(temp, "schema.json");
32
+ const outputPath = join(temp, "output.json");
33
+ try {
34
+ writeFileSync(schemaPath, `${JSON.stringify(evaluatorOutputSchema(), null, 2)}\n`, "utf8");
35
+ const prompt = [
36
+ "You are an evaluation-only component. Do not use tools, execute commands, or modify files.",
37
+ `Return JSON matching contract version ${input.contract_version}.`,
38
+ `Objective: ${input.objective}`,
39
+ `Current gate: ${input.current_gate}`,
40
+ `Candidate id: ${candidate.id}`,
41
+ `Candidate action: ${candidate.action}`,
42
+ "Score scope_fit, gate_readiness, risk_reduction, evidence_gain, testability, reversibility and cost from 0 to 5.",
43
+ "Cost 0 is low and 5 is high. uncertainty is 0 low to 5 high.",
44
+ "Keep rationale concise. child_actions must only contain actions permitted by the current gate.",
45
+ ].join("\n");
46
+ const args = [
47
+ "exec", "--sandbox", "read-only", "--ephemeral", "--ignore-user-config",
48
+ "--output-schema", schemaPath, "--output-last-message", outputPath, "--color", "never",
49
+ ];
50
+ if (options.model) args.push("--model", options.model);
51
+ args.push(prompt);
52
+ const before = gitState();
53
+ execFileSync(codexBin, args, {
54
+ cwd: options.cwd,
55
+ encoding: "utf8",
56
+ stdio: ["ignore", "pipe", "pipe"],
57
+ timeout: options.timeoutMs ?? 120000,
58
+ });
59
+ const after = gitState();
60
+ if (before !== null && after !== before) throw new Error("repository mutation detected during read-only evaluator run");
61
+ return validateEvaluation(JSON.parse(readFileSync(outputPath, "utf8")), candidate.id);
62
+ } finally {
63
+ rmSync(temp, { recursive: true, force: true });
64
+ }
65
+ },
66
+ };
67
+ }
@@ -0,0 +1,13 @@
1
+ import { validateEvaluation } from "../contracts.js";
2
+
3
+ export function fixtureEvaluator(evaluations) {
4
+ return {
5
+ name: "fixture",
6
+ metadata: { name: "fixture", runtime: "deterministic-test" },
7
+ async evaluate(_input, candidate) {
8
+ const value = evaluations[candidate.id];
9
+ if (!value) throw new Error(`missing fixture evaluation for ${candidate.id}`);
10
+ return validateEvaluation(value, candidate.id);
11
+ },
12
+ };
13
+ }
@@ -0,0 +1,34 @@
1
+ import { mkdirSync, writeFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+
4
+ function safeResult(result) {
5
+ return JSON.parse(JSON.stringify(result, (key, value) => {
6
+ if (/(prompt|hidden_reasoning|secret|credential|source_snapshot)/i.test(key)) return undefined;
7
+ return value;
8
+ }));
9
+ }
10
+
11
+ export function persistSearchResult(targetDir, result) {
12
+ const root = join(targetDir, ".agdf", "control", "artefacts", result.scope_key);
13
+ const jsonPath = join(root, "DELIVERY_PATH_SEARCH.json");
14
+ const markdownPath = join(root, "DELIVERY_PATH_SEARCH.md");
15
+ mkdirSync(dirname(jsonPath), { recursive: true });
16
+ const safe = safeResult(result);
17
+ writeFileSync(jsonPath, `${JSON.stringify(safe, null, 2)}\n`, "utf8");
18
+ const recommendation = safe.recommendation?.action ?? "No safe recommendation";
19
+ writeFileSync(markdownPath, [
20
+ "# Delivery Path Search Decision",
21
+ "",
22
+ `- status: ${safe.status}`,
23
+ `- current_gate: ${safe.current_gate}`,
24
+ `- enforcement: ${safe.enforcement.level}`,
25
+ `- recommendation: ${recommendation}`,
26
+ `- stopping_reason: ${safe.stopping_reason}`,
27
+ `- evaluations: ${safe.budgets.evaluations}`,
28
+ `- next_gate_action: ${safe.next_gate_action}`,
29
+ "",
30
+ "This summary contains model judgements, not measured facts. Canonical AGDF gate-check remains authoritative.",
31
+ "",
32
+ ].join("\n"), "utf8");
33
+ return { jsonPath, markdownPath };
34
+ }
@@ -0,0 +1,21 @@
1
+ import { SCORING_POLICY_VERSION } from "./contracts.js";
2
+
3
+ export const DEFAULT_WEIGHTS = Object.freeze({
4
+ scope_fit: 3,
5
+ gate_readiness: 3,
6
+ risk_reduction: 2,
7
+ evidence_gain: 2,
8
+ testability: 2,
9
+ reversibility: 1,
10
+ cost: -1,
11
+ uncertainty: -2,
12
+ });
13
+
14
+ const enforcementPenalty = { full: 0, tool_enforced: 1, instruction_only: 4 };
15
+
16
+ export function scoreEvaluation(evaluation, enforcementLevel, weights = DEFAULT_WEIGHTS) {
17
+ const contributions = Object.fromEntries(Object.entries(weights).map(([key, weight]) => [key, evaluation[key] * weight]));
18
+ const penalty = enforcementPenalty[enforcementLevel] ?? 4;
19
+ const score = Object.values(contributions).reduce((sum, value) => sum + value, 0) - penalty;
20
+ return { score, contributions, enforcement_penalty: penalty, scoring_policy_version: SCORING_POLICY_VERSION };
21
+ }
@@ -0,0 +1,92 @@
1
+ import { CONTRACT_VERSION, SCORING_POLICY_VERSION, validateEvaluation, validateSearchInput } from "./contracts.js";
2
+ import { candidateLegality, candidatesFromInput } from "./candidate-policy.js";
3
+ import { scoreEvaluation } from "./scoring.js";
4
+
5
+ export async function runDeliveryPathSearch(inputValue, evaluator, options = {}) {
6
+ const input = validateSearchInput(inputValue);
7
+ const startedAt = Date.now();
8
+ const queue = [...(options.candidates ?? candidatesFromInput(input))];
9
+ const rejected = [];
10
+ const evaluated = [];
11
+ let evaluations = 0;
12
+ let costUnits = 0;
13
+ let lastLeaderId = null;
14
+ let stableComparisons = 0;
15
+ let stoppingReason = "candidate_queue_exhausted";
16
+
17
+ while (queue.length > 0) {
18
+ if (evaluations >= input.budgets.max_evaluations) { stoppingReason = "evaluation_budget_exhausted"; break; }
19
+ if (costUnits >= input.budgets.max_cost_units) { stoppingReason = "cost_budget_exhausted"; break; }
20
+ if (Date.now() - startedAt >= input.budgets.max_duration_ms) { stoppingReason = "duration_budget_exhausted"; break; }
21
+ const candidate = queue.shift();
22
+ const legality = candidateLegality(candidate, input);
23
+ if (!legality.legal) {
24
+ rejected.push({ candidate_id: candidate.id, action: candidate.action, reason: legality.reason });
25
+ continue;
26
+ }
27
+
28
+ try {
29
+ const evaluation = validateEvaluation(await evaluator.evaluate(input, candidate), candidate.id);
30
+ evaluations += 1;
31
+ costUnits += evaluation.cost;
32
+ const scored = scoreEvaluation(evaluation, input.enforcement.level);
33
+ evaluated.push({ candidate, evaluation, ...scored });
34
+ const currentLeader = [...evaluated].sort((a, b) => b.score - a.score || a.candidate.id.localeCompare(b.candidate.id))[0];
35
+ if (currentLeader?.candidate.id === lastLeaderId) stableComparisons += 1;
36
+ else stableComparisons = 1;
37
+ lastLeaderId = currentLeader?.candidate.id ?? null;
38
+ if (evaluated.length > 1 && stableComparisons >= input.budgets.stability_window) {
39
+ stoppingReason = "stable_leader";
40
+ break;
41
+ }
42
+ if ((candidate.depth ?? 0) + 1 < input.budgets.max_depth) {
43
+ for (const [index, action] of evaluation.child_actions.slice(0, input.budgets.max_candidates).entries()) {
44
+ queue.push({
45
+ id: `${candidate.id}.${index + 1}`,
46
+ parent_id: candidate.id,
47
+ depth: (candidate.depth ?? 0) + 1,
48
+ action,
49
+ expected_evidence: [],
50
+ tests: [],
51
+ assumptions: evaluation.assumptions,
52
+ });
53
+ }
54
+ }
55
+ } catch (error) {
56
+ rejected.push({ candidate_id: candidate.id, action: candidate.action, reason: `invalid_evaluation: ${error.message}` });
57
+ }
58
+ }
59
+
60
+ evaluated.sort((a, b) => b.score - a.score || a.candidate.id.localeCompare(b.candidate.id));
61
+ const leader = evaluated[0];
62
+ if (!leader) stoppingReason = rejected.length ? "no_safe_candidate" : stoppingReason;
63
+ return {
64
+ contract_version: CONTRACT_VERSION,
65
+ scoring_policy_version: SCORING_POLICY_VERSION,
66
+ scope_key: input.scope_key,
67
+ current_gate: input.current_gate,
68
+ status: leader ? "recommendation" : "no_safe_recommendation",
69
+ recommendation: leader ? {
70
+ candidate_id: leader.candidate.id,
71
+ action: leader.candidate.action,
72
+ score: leader.score,
73
+ rationale: leader.evaluation.rationale,
74
+ expected_evidence: leader.candidate.expected_evidence,
75
+ tests: leader.candidate.tests,
76
+ risks: leader.evaluation.risks,
77
+ assumptions: leader.evaluation.assumptions,
78
+ } : null,
79
+ alternatives: evaluated.slice(1).map((item) => ({
80
+ candidate_id: item.candidate.id,
81
+ action: item.candidate.action,
82
+ score: item.score,
83
+ rejection_reason: `ranked_below_${leader?.candidate.id ?? "safety_threshold"}`,
84
+ })),
85
+ rejected,
86
+ enforcement: input.enforcement,
87
+ evaluator: evaluator.metadata ?? { name: evaluator.name ?? "unknown" },
88
+ budgets: { configured: input.budgets, evaluations, cost_units: costUnits, stable_comparisons: stableComparisons, duration_ms: Date.now() - startedAt },
89
+ stopping_reason: stoppingReason,
90
+ next_gate_action: "Run canonical AGDF gate-check; search does not grant permission.",
91
+ };
92
+ }
@@ -0,0 +1,58 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { CONTRACT_VERSION } from "./contracts.js";
4
+
5
+ function section(content, heading) {
6
+ return content.match(new RegExp(`## ${heading}\\r?\\n([\\s\\S]*?)(?=\\r?\\n## |$)`))?.[1] ?? "";
7
+ }
8
+
9
+ function tableAnswers(content) {
10
+ const answers = {};
11
+ for (const line of section(content, "Current Control State").split(/\r?\n/)) {
12
+ const cells = line.split("|").slice(1, -1).map((cell) => cell.trim());
13
+ if (cells.length >= 2 && cells[0] && !cells[0].startsWith("---") && cells[0] !== "Question") answers[cells[0]] = cells[1];
14
+ }
15
+ return answers;
16
+ }
17
+
18
+ function statusCard(content) {
19
+ const card = {};
20
+ for (const line of section(content, "Run Status Card").split(/\r?\n/)) {
21
+ const cells = line.split("|").slice(1, -1).map((cell) => cell.trim().replace(/^`|`$/g, ""));
22
+ if (cells.length >= 2 && cells[0] && !cells[0].startsWith("---") && cells[0] !== "Run status") card[cells[0]] = cells[1];
23
+ }
24
+ return card;
25
+ }
26
+
27
+ function listCell(value) {
28
+ return String(value ?? "").split(/\s*;\s*/).map((item) => item.trim()).filter(Boolean);
29
+ }
30
+
31
+ export function searchInputFromControl(targetDir, options = {}) {
32
+ const runPath = join(targetDir, ".agdf", "control", "AGDF_RUN.md");
33
+ if (!existsSync(runPath)) throw new Error("missing .agdf/control/AGDF_RUN.md");
34
+ const content = readFileSync(runPath, "utf8");
35
+ const meta = Object.fromEntries([...content.matchAll(/^- ([a-z_]+):\s*(.+)$/gm)].map((match) => [match[1], match[2].replace(/^`|`$/g, "").trim()]));
36
+ const answers = tableAnswers(content);
37
+ const card = statusCard(content);
38
+ return {
39
+ contract_version: CONTRACT_VERSION,
40
+ scope_key: options.scopeKey ?? meta.run_id ?? "unknown-scope",
41
+ objective: section(content, "Objective").trim(),
42
+ current_gate: card["Current gate"] ?? meta.current_gate ?? "unknown",
43
+ allowed_actions: options.allowedActions ?? listCell(card["Allowed now"]),
44
+ forbidden_actions: options.forbiddenActions ?? listCell(answers["What is explicitly forbidden right now?"]),
45
+ evidence_refs: [...content.matchAll(/\|\s*([^|]+)\s*\|\s*([^|]+)\s*\|\s*([^|]+)\s*\|\s*(direct|indirect)\s*\|/g)]
46
+ .slice(1).map((match) => `${match[1].trim()}: ${match[2].trim()}`),
47
+ risks: section(content, "Risks").split(/\r?\n/).filter((line) => line.startsWith("|") && !line.includes("---") && !line.includes("| Risk |")).map((line) => line.split("|")[1].trim()),
48
+ enforcement: options.enforcement ?? { level: "instruction_only", evidence: ["surface instruction prohibits implementation during search"] },
49
+ budgets: {
50
+ max_candidates: options.maxCandidates ?? 5,
51
+ max_depth: options.maxDepth ?? 2,
52
+ max_evaluations: options.maxEvaluations ?? 8,
53
+ max_duration_ms: options.maxDurationMs ?? 120000,
54
+ max_cost_units: options.maxCostUnits ?? 20,
55
+ stability_window: options.stabilityWindow ?? 3,
56
+ },
57
+ };
58
+ }
@@ -0,0 +1,14 @@
1
+ import { validateEnforcement } from "../contracts.js";
2
+
3
+ export function enforcementForSurface(surface, evidence = []) {
4
+ const defaults = {
5
+ codex: { level: "tool_enforced", evidence: ["codex exec --sandbox read-only --ephemeral"] },
6
+ claude: { level: "instruction_only", evidence: [] },
7
+ copilot: { level: "instruction_only", evidence: [] },
8
+ opencode: { level: "instruction_only", evidence: [] },
9
+ generic: { level: "instruction_only", evidence: [] },
10
+ };
11
+ const selected = structuredClone(defaults[surface] ?? defaults.generic);
12
+ if (evidence.length) selected.evidence = evidence;
13
+ return validateEnforcement(selected);
14
+ }
@@ -1,26 +1,38 @@
1
- import { existsSync } from "node:fs";
1
+ import { existsSync, readFileSync } from "node:fs";
2
+
3
+ const packageJson = JSON.parse(readFileSync(new URL("./package.json", import.meta.url), "utf8"));
2
4
 
3
5
  export const AGDFPlugin = async ({ directory, client }) => {
4
6
  const controlDir = `${directory}/.agdf/control`;
5
7
  const hasRepositorySurface = () => existsSync(`${directory}/.opencode/AGDF.md`) && existsSync(`${directory}/.opencode/agents`);
8
+ const status = () => ({
9
+ active: true,
10
+ version: packageJson.version,
11
+ controlDir,
12
+ repositorySurface: hasRepositorySurface(),
13
+ });
6
14
 
7
15
  return {
8
16
  event: async ({ event }) => {
9
17
  if (event?.type === "session.created") {
10
- const repositorySurface = hasRepositorySurface();
18
+ const currentStatus = status();
11
19
  await client.app.log({
12
20
  body: {
13
21
  service: "agdf",
14
22
  level: "info",
15
- message: repositorySurface ? "AGDF OpenCode surface active" : "AGDF OpenCode plugin loaded without repository surface",
16
- extra: { controlDir, repositorySurface },
23
+ message: currentStatus.repositorySurface ? "AGDF OpenCode active" : "AGDF OpenCode global hook active without repository surface",
24
+ extra: currentStatus,
17
25
  },
18
26
  });
19
27
  }
20
28
  },
21
29
 
22
30
  "shell.env": async (_input, output) => {
31
+ const currentStatus = status();
32
+ output.env.AGDF_PLUGIN_ACTIVE = "1";
33
+ output.env.AGDF_PLUGIN_VERSION = currentStatus.version;
23
34
  output.env.AGDF_CONTROL_DIR = controlDir;
35
+ output.env.AGDF_OPENCODE_REPOSITORY_SURFACE = currentStatus.repositorySurface ? "1" : "0";
24
36
  },
25
37
 
26
38
  "experimental.session.compacting": async (_input, output) => {
@@ -38,7 +50,7 @@ export const AGDFPlugin = async ({ directory, client }) => {
38
50
  "## AGDF Runtime Reminder",
39
51
  "Use `.opencode/AGDF.md`, `.opencode/agents/` and `.agdf/control/` as the AGDF source for this OpenCode session.",
40
52
  "For new build/change intent or unclear approval, route first to `agdf-gate-check` before later artefacts or implementation.",
41
- "Use `npx --yes @agdf/cli@latest gate-check --json` only as deterministic proof, not as a replacement for AGDF judgement.",
53
+ "Use `npx --yes @agdf/cli@latest gate-check --status-card` for compact interactive status. Use `--json` only as deterministic proof for automation or audit evidence, and summarize it instead of mirroring full JSON into chat.",
42
54
  ].join("\n"));
43
55
  },
44
56
  };
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "create-agdf",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
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": [
7
7
  "bin",
8
+ "lib",
8
9
  "generated",
9
10
  "opencode-plugin.js",
10
11
  "README.md",
@@ -20,8 +21,9 @@
20
21
  },
21
22
  "scripts": {
22
23
  "sync-package-assets": "node ./scripts/sync-package-assets.js",
24
+ "test:delivery-path-search": "node ./scripts/delivery-path-search-test.js",
23
25
  "test:routing": "npm run sync-package-assets && node ./scripts/test-routing.js",
24
- "smoke-test": "npm run sync-package-assets && node ./scripts/smoke-test.js && node ./scripts/test-routing.js",
26
+ "smoke-test": "npm run sync-package-assets && npm run test:delivery-path-search && node ./scripts/smoke-test.js && node ./scripts/test-routing.js",
25
27
  "prepack": "npm run sync-package-assets"
26
28
  },
27
29
  "keywords": [