create-agdf 0.11.3 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/README.md +5 -4
  2. package/generated/.agdf/control/templates/MASTER_BACKLOG.md +1 -1
  3. package/generated/.agdf/control/templates/artefacts/BROWNFIELD_REVIEW.md +25 -0
  4. package/generated/.github/skills/agdf-brownfield-analysis/SKILL.md +26 -0
  5. package/generated/.github/skills/agdf-gate-check/SKILL.md +40 -18
  6. package/generated/.github/skills/agdf-interaction-locales.json +50 -0
  7. package/generated/.github/skills/agdf-runtime-contract.md +1 -0
  8. package/generated/.github/skills/contracts/control-scaffold.md +2 -0
  9. package/generated/.github/skills/contracts/gate-transition.md +7 -3
  10. package/generated/.github/skills/contracts/interaction.md +22 -0
  11. package/generated/.github/skills/contracts/modes.md +81 -0
  12. package/generated/.github/skills/contracts/task-target-resolution.md +102 -0
  13. package/generated/.opencode/AGDF.md +2 -0
  14. package/generated/.opencode/agdf-interaction-locales.json +50 -0
  15. package/generated/.opencode/agdf-runtime-contract.md +1 -0
  16. package/generated/.opencode/contracts/control-scaffold.md +2 -0
  17. package/generated/.opencode/contracts/gate-transition.md +7 -3
  18. package/generated/.opencode/contracts/interaction.md +22 -0
  19. package/generated/.opencode/contracts/modes.md +81 -0
  20. package/generated/.opencode/contracts/task-target-resolution.md +102 -0
  21. package/generated/.opencode/skills/agdf-brownfield-analysis/SKILL.md +26 -0
  22. package/generated/.opencode/skills/agdf-gate-check/SKILL.md +40 -18
  23. package/generated/AGENTS.md +1 -0
  24. package/generated/plugins/agdf/.claude-plugin/plugin.json +1 -1
  25. package/generated/plugins/agdf/.codex-plugin/plugin.json +1 -1
  26. package/generated/plugins/agdf/control/templates/MASTER_BACKLOG.md +1 -1
  27. package/generated/plugins/agdf/control/templates/artefacts/BROWNFIELD_REVIEW.md +25 -0
  28. package/generated/plugins/agdf/meta/agdf-agent-router.md +16 -0
  29. package/generated/plugins/agdf/meta/agdf-interaction-locales.json +50 -0
  30. package/generated/plugins/agdf/meta/agdf-plugin.definition.json +2 -1
  31. package/generated/plugins/agdf/meta/agdf-runtime-contract.md +1 -0
  32. package/generated/plugins/agdf/meta/contracts/control-scaffold.md +2 -0
  33. package/generated/plugins/agdf/meta/contracts/gate-transition.md +7 -3
  34. package/generated/plugins/agdf/meta/contracts/interaction.md +22 -0
  35. package/generated/plugins/agdf/meta/contracts/modes.md +81 -0
  36. package/generated/plugins/agdf/meta/contracts/task-target-resolution.md +102 -0
  37. package/generated/plugins/agdf/runtime/create-agdf/generated/plugins/agdf/meta/agdf-interaction-locales.json +50 -0
  38. package/generated/plugins/agdf/runtime/create-agdf/generated/plugins/agdf/meta/agdf-plugin.definition.json +2 -1
  39. package/generated/plugins/agdf/runtime/create-agdf/lib/cli/command-registry.js +2 -2
  40. package/generated/plugins/agdf/runtime/create-agdf/lib/cli/delivery-path-search-command.js +78 -14
  41. package/generated/plugins/agdf/runtime/create-agdf/lib/control-evaluation/gate-policy.js +14 -7
  42. package/generated/plugins/agdf/runtime/create-agdf/lib/control-evaluation/shared.js +1 -0
  43. package/generated/plugins/agdf/runtime/create-agdf/lib/delivery-path-search/evaluators/claude.js +2 -11
  44. package/generated/plugins/agdf/runtime/create-agdf/lib/delivery-path-search/evaluators/codex.js +2 -11
  45. package/generated/plugins/agdf/runtime/create-agdf/lib/delivery-path-search/evaluators/opencode.js +234 -0
  46. package/generated/plugins/agdf/runtime/create-agdf/lib/delivery-path-search/evaluators/prompt.js +13 -0
  47. package/generated/plugins/agdf/runtime/create-agdf/lib/delivery-path-search/search-engine.js +1 -0
  48. package/generated/plugins/agdf/runtime/create-agdf/lib/delivery-path-search/surfaces/capabilities.js +15 -2
  49. package/generated/plugins/agdf/runtime/create-agdf/lib/interaction-presentation.js +85 -0
  50. package/generated/plugins/agdf/runtime/create-agdf/package.json +6 -2
  51. package/generated/plugins/agdf/runtime/runtime-manifest.json +2 -2
  52. package/generated/plugins/agdf/scripts/check-runtime-integrity.mjs +119 -3
  53. package/generated/plugins/agdf/skills/brownfield-analysis/SKILL.md +26 -0
  54. package/generated/plugins/agdf/skills/gate-check/SKILL.md +40 -18
  55. package/lib/cli/application.js +29 -3
  56. package/lib/cli/command-registry.js +2 -2
  57. package/lib/cli/delivery-path-search-command.js +78 -14
  58. package/lib/control-evaluation/gate-policy.js +14 -7
  59. package/lib/control-evaluation/shared.js +1 -0
  60. package/lib/delivery-path-search/evaluators/claude.js +2 -11
  61. package/lib/delivery-path-search/evaluators/codex.js +2 -11
  62. package/lib/delivery-path-search/evaluators/opencode.js +234 -0
  63. package/lib/delivery-path-search/evaluators/prompt.js +13 -0
  64. package/lib/delivery-path-search/search-engine.js +1 -0
  65. package/lib/delivery-path-search/surfaces/capabilities.js +15 -2
  66. package/lib/installers/opencode.js +341 -10
  67. package/lib/interaction-presentation.js +85 -0
  68. package/lib/lifecycle/operations.js +4 -0
  69. package/lib/live-agent/read-only-structured.js +43 -0
  70. package/lib/proportionality-benchmark/blind-prompt.js +37 -0
  71. package/lib/proportionality-benchmark/contracts.js +120 -0
  72. package/lib/proportionality-benchmark/corpus-loader.js +110 -0
  73. package/lib/proportionality-benchmark/evaluator.js +250 -0
  74. package/lib/proportionality-benchmark/index.js +7 -0
  75. package/lib/proportionality-benchmark/live-recorder.js +100 -0
  76. package/lib/proportionality-benchmark/report.js +51 -0
  77. package/lib/proportionality-benchmark/source-fingerprint.js +30 -0
  78. package/lib/skill-evals/live-recorder.js +3 -26
  79. package/opencode-plugin.js +59 -3
  80. package/package.json +6 -2
@@ -20,6 +20,7 @@ This skill must not create later artefacts such as PRD, SD, TP, CD, CR, QA, or U
20
20
  ## Runtime Contract
21
21
  Use these focused runtime-contract modules:
22
22
 
23
+ - `../../meta/contracts/task-target-resolution.md`
23
24
  - `../../meta/contracts/gate-transition.md`
24
25
  - `../../meta/contracts/interaction.md`
25
26
  - `../../meta/contracts/control-scaffold.md`
@@ -75,6 +76,12 @@ The CLI reports are validators and JSON evidence, not the primary user experienc
75
76
 
76
77
  Plugin `AGDF_*` environment variables set by the OpenCode `shell.env` hook are convenience only and must not be used as the only proof of activation; host shell-env propagation to spawned tool shells is not guaranteed and is OpenCode-owned, not AGDF-owned. A relative glob or `grep` for `.agdf/control/config.json` must not be used as proof of presence or absence of `.agdf/control/config.json`; an absolute `read` or the canonical CLI probe is required.
77
78
 
79
+ ## OpenCode Subagent Enforcement Boundary
80
+
81
+ OpenCode plugin hooks (`tool.execute.before`) do not intercept tool calls from subagents spawned via the `task` tool (anomalyco/opencode issue #5894, PR #36238 open). AGDF enforcement through plugin hooks applies to primary-agent tool calls only; in the subagent path, AGDF governance is auditing-only, not enforcement.
82
+
83
+ This is a host limitation, not an AGDF-owned defect. Do not claim that AGDF gates are technically enforced in the OpenCode subagent path. Do not route work to a subagent to bypass a primary-agent gate. When subagent work touches gate-relevant files, verify the result through the canonical validator (`doctor --json`, `gate-check --json`) after the subagent returns, not during.
84
+
78
85
  ## Native Interaction Path
79
86
 
80
87
  `../../meta/contracts/interaction.md` is the complete normative owner for interaction kinds, locale,
@@ -84,12 +91,15 @@ do not restate or infer a surface matrix in this skill.
84
91
 
85
92
  This skill owns only these operational responsibilities:
86
93
 
87
- 1. Select exactly one run and evaluate its current gate.
88
- 2. Confirm that the required durable artefact is present and ready.
89
- 3. Consume the canonical `approval_presentation` verbatim for the selected run, gate and revision.
90
- 4. Present that projection and obtain deliberate input through the contract-selected native or exact-text path.
91
- 5. Revalidate the same run, gate and revision immediately after the response and before persistence.
92
- 6. Persist only a currently valid exact approval through the existing control-state workflow.
94
+ 1. Resolve or revalidate the primary task target before selecting repository control state.
95
+ 2. Derive repository activation only from the resolved governance target; an evidence source or
96
+ working directory is not sufficient.
97
+ 3. Select exactly one run and evaluate its current gate.
98
+ 4. Confirm that the required durable artefact is present and ready.
99
+ 5. Consume the canonical `approval_presentation` verbatim for the selected run, gate and revision.
100
+ 6. Present that projection and obtain deliberate input through the contract-selected native or exact-text path.
101
+ 7. Revalidate the same target, run, gate and revision immediately after the response and before persistence.
102
+ 8. Persist only a currently valid exact approval through the existing control-state workflow.
93
103
 
94
104
  For status, blocked, read-only or rationale interactions, consume the canonical
95
105
  `status_presentation.markdown` verbatim and apply the locale contract without asking for approval.
@@ -156,18 +166,30 @@ Use what is available:
156
166
  If a status is not explicit, do not assume it is satisfied.
157
167
 
158
168
  ## Workflow
159
- 1. Check exact approvals.
160
- 2. Check artefact status.
161
- 3. Determine the earliest blocking user gate or internal mandatory step.
162
- 4. Derive allowed and forbidden outputs.
163
- 5. Name the exact missing approval, if any.
164
- 6. If consent was only implicit, say it is not yet approval and provide the exact formula.
165
- 7. Ensure the next step follows the gate transition table. In particular, never jump from `Approval: UR` to implementation.
166
- 8. Treat a generic "start", "continue" or "leg los" request as a request to perform only the current next allowed action.
167
- 9. After Brownfield Review, choose the smallest safe process path before creating later artefacts.
168
- 10. If the selected path is not visibly recorded with scope reason and evidence, keep the run at `Mode/Slice Decision`.
169
- 11. When `.agdf/control/` is missing or incomplete, make the next allowed artefact action explicit. For a fresh request, that means draft the minimal UR in the response, then request `Approval: UR`. Do not write a full control scaffold unless durable control state was explicitly requested or is already the repository's live AGDF working state.
170
- 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.
169
+ 1. Resolve or revalidate the primary target through `task-target-resolution.md`.
170
+ 2. If resolution is unresolved, consume the canonical target orientation, request only its
171
+ clarification/recovery action and stop before repository activation, gate evaluation or mutation.
172
+ 3. Derive the governance target from the primary target; never from `cwd` or an evidence source alone.
173
+ 4. Check exact approvals.
174
+ 5. Check artefact status.
175
+ 6. Determine the earliest blocking user gate or internal mandatory step.
176
+ 7. Derive allowed and forbidden outputs.
177
+ 8. Name the exact missing approval, if any.
178
+ 9. If consent was only implicit, say it is not yet approval and provide the exact formula.
179
+ 10. Ensure the next step follows the gate transition table. In particular, never jump from `Approval: UR` to implementation.
180
+ 11. Treat a generic "start", "continue" or "leg los" request as a request to perform only the current next allowed action.
181
+ 12. After Brownfield Review, choose the smallest safe process path before creating later artefacts.
182
+ 13. If the selected path is not visibly recorded with scope reason and evidence, keep the run at `Mode/Slice Decision`.
183
+ 14. When `.agdf/control/` is missing or incomplete, make the next allowed artefact action explicit. For a fresh request, that means draft the minimal UR in the response, then request `Approval: UR`. Do not write a full control scaffold unless durable control state was explicitly requested or is already the repository's live AGDF working state.
184
+ 15. If branch, workspace and durable artefacts disagree, do not choose a scope silently; report the ambiguity and ask for the smallest clarifying gate action.
185
+
186
+ ### Task Target Orientation
187
+
188
+ When target/context separation is material, the target changed, or resolution is unresolved, consume
189
+ `task_target_orientation.markdown` verbatim from `renderTaskTargetOrientation` in
190
+ `../../meta/contracts/interaction.md`. Do not reconstruct its fields or maintain a skill-local target
191
+ template. If the renderer returns `null`, fail closed to target clarification and do not proceed to
192
+ repository activation, Scope Classification or gate evaluation.
171
193
 
172
194
  ### Scope Classification Output
173
195
  When this skill classifies a fresh scope as ungated (Quick Task or Trivial Change Boundary), consume
@@ -115,9 +115,19 @@ function createHandlers({ io, env, exec, prepare }) {
115
115
  const result = runLifecyclePhase("plugin_operation", () => installOpenCodeGlobalPlugin(configDir));
116
116
  runLifecyclePhase("global_surface", () => installOpenCodeGlobalSurface(configDir));
117
117
  const report = runLifecyclePhase("verification", () => evaluateOpenCodeStatus(options.dir, configDir, result.transition));
118
+ const alignmentHealthy = ["already_matching", "aligned"].includes(result.sdk_alignment.status);
118
119
  const verificationHealthy = report.status === "configured"
119
120
  && report.package.version_status === "current"
120
- && report.global_native_surface.complete;
121
+ && report.global_native_surface.complete
122
+ && report.experimental_hooks.aggregate === "declared_supported"
123
+ && report.host_sdk_version.status === "matching"
124
+ && alignmentHealthy;
125
+ const nextAction = verificationHealthy
126
+ ? globalInstallRestartAction(options.target)
127
+ : {
128
+ kind: "recovery",
129
+ text: `Retry the OpenCode installation to align @opencode-ai/plugin to ${result.sdk_alignment.target_version || "the exact host version"}; observed SDK: ${result.sdk_alignment.installed_version || "unknown"}.`,
130
+ };
121
131
  printLifecycleResult(createLifecycleResult({
122
132
  operation: result.transition.status === "updated" ? "update" : "install",
123
133
  result: verificationHealthy ? "success" : "partial",
@@ -130,10 +140,26 @@ function createHandlers({ io, env, exec, prepare }) {
130
140
  status: report.package.version_status === "current" ? "verified" : "unknown",
131
141
  transition: result.transition.status,
132
142
  },
133
- verification: { status: verificationHealthy ? "healthy" : "degraded", evidence: [report.global_config.path, report.global_native_surface.path] },
143
+ verification: {
144
+ status: verificationHealthy ? "healthy" : "degraded",
145
+ evidence: [
146
+ report.global_config.path,
147
+ report.global_native_surface.path,
148
+ `opencode_host=${report.host.installed_version || "unknown"}`,
149
+ `plugin_sdk=${report.plugin_sdk.installed_version || "unknown"}`,
150
+ `experimental_hooks=${report.experimental_hooks.aggregate}`,
151
+ `host_sdk_version=${report.host_sdk_version.status};policy=${report.host_sdk_version.policy}`,
152
+ `sdk_alignment=${result.sdk_alignment.status};target=${result.sdk_alignment.target_version || "unknown"};installed=${result.sdk_alignment.installed_version || "unknown"}`,
153
+ ],
154
+ },
134
155
  restart: { required: true, reason: "host_reload" },
135
- next_action: globalInstallRestartAction(options.target),
156
+ next_action: nextAction,
136
157
  }), { json: options.json, io });
158
+ if (!options.json) {
159
+ io.log(`OpenCode host / plugin SDK: ${report.host.installed_version || "unknown"} / ${report.plugin_sdk.installed_version || "unknown"} (${report.host_sdk_version.status}; ${report.host_sdk_version.policy})`);
160
+ io.log(`Plugin SDK alignment: ${result.sdk_alignment.status} (target ${result.sdk_alignment.target_version || "unknown"}; installed ${result.sdk_alignment.installed_version || "unknown"})`);
161
+ io.log(`Experimental hook declarations: ${report.experimental_hooks.aggregate} (SDK declaration evidence; live invocation not observed)`);
162
+ }
137
163
  return verificationHealthy ? 0 : 1;
138
164
  } catch (error) {
139
165
  printInstallFailure(options.target, error, options, io);
@@ -27,8 +27,8 @@ export const commandRegistry = Object.freeze([
27
27
  command("gate-check", { local: [" --approval-envelope", " --json"], scaffold: [""], legacy: [" --json"] }),
28
28
  command("delivery-map", { local: [" --json"], scaffold: [""] }),
29
29
  command("delivery-path-search", {
30
- local: [" --surface codex --json", " --surface claude --json"],
31
- scaffold: [" --surface codex", " --surface claude"],
30
+ local: [" --surface codex --json", " --surface claude --json", " --surface opencode --json"],
31
+ scaffold: [" --surface codex", " --surface claude", " --surface opencode"],
32
32
  }),
33
33
  command("run-create", { local: [" --run <run_id>"] }),
34
34
  command("run-migrate", { local: [" [--run <run_id>]"] }),
@@ -3,6 +3,7 @@ import { searchInputFromControl } from '../delivery-path-search/state-adapter.js
3
3
  import { runDeliveryPathSearch } from '../delivery-path-search/search-engine.js';
4
4
  import { codexEvaluator } from '../delivery-path-search/evaluators/codex.js';
5
5
  import { claudeEvaluator } from '../delivery-path-search/evaluators/claude.js';
6
+ import { openCodeEvaluator, preflightOpenCodeEvaluator } from '../delivery-path-search/evaluators/opencode.js';
6
7
  import { fixtureEvaluator } from '../delivery-path-search/evaluators/protocol.js';
7
8
  import { codexGenerator } from '../delivery-path-search/generators/codex.js';
8
9
  import { claudeGenerator } from '../delivery-path-search/generators/claude.js';
@@ -10,12 +11,56 @@ import { fixtureGenerator } from '../delivery-path-search/generators/protocol.js
10
11
  import { enforcementForSurface } from '../delivery-path-search/surfaces/capabilities.js';
11
12
  import { persistSearchResult } from '../delivery-path-search/persistence.js';
12
13
 
13
- export async function executeDeliveryPathSearch(options, io = console) {
14
+ function unavailableOpenCodeResult({ options, preflight, input = null, error = null }) {
15
+ const mutation = error?.code === "OPENCODE_EVALUATOR_MUTATION_DETECTED";
16
+ const attempted = Boolean(input);
17
+ const nextAction = mutation
18
+ ? "Investigate and revert the detected repository mutation before any evaluator fallback or retry."
19
+ : "Repair the reported OpenCode capability and retry, or use the existing instruction-only Delivery Path Search workflow.";
20
+ return {
21
+ contract_version: "1",
22
+ surface: "opencode",
23
+ scope_key: input?.scope_key ?? options.runId ?? null,
24
+ current_gate: input?.current_gate ?? null,
25
+ status: mutation ? "evaluator_error" : "evaluator_unavailable",
26
+ recommendation: null,
27
+ enforcement: enforcementForSurface("opencode"),
28
+ evaluator: {
29
+ name: "opencode",
30
+ preflight: preflight?.code ?? "not_run",
31
+ failure_code: error?.code ?? preflight?.code,
32
+ detail: error?.message ?? preflight?.detail,
33
+ evidence: preflight?.evidence ?? [],
34
+ },
35
+ executable_evaluator: {
36
+ attempted,
37
+ preflight: preflight?.status ?? "failed",
38
+ failure_code: error?.code ?? preflight?.code,
39
+ evidence: preflight?.evidence ?? [],
40
+ },
41
+ stopping_reason: mutation ? "opencode_mutation_detected" : attempted ? "opencode_evaluator_failed" : "opencode_preflight_failed",
42
+ next_action: nextAction,
43
+ next_gate_action: nextAction,
44
+ };
45
+ }
46
+
47
+ export async function executeDeliveryPathSearch(options, io = console, dependencies = {}) {
14
48
  const fixture = options.fixture ? JSON.parse(readFileSync(options.fixture, "utf8")) : null;
15
- const enforcement = fixture?.input?.enforcement ?? enforcementForSurface(options.surface);
49
+ const preflight = !fixture && options.surface === "opencode"
50
+ ? (dependencies.preflightOpenCodeEvaluator ?? preflightOpenCodeEvaluator)({ cwd: options.dir })
51
+ : null;
52
+ if (preflight?.status === "failed") {
53
+ const result = unavailableOpenCodeResult({ options, preflight });
54
+ printDeliveryPathSearchResult(result, options, io);
55
+ return result;
56
+ }
57
+ const enforcement = fixture?.input?.enforcement
58
+ ?? (preflight
59
+ ? enforcementForSurface("opencode", preflight.evidence, { preflight })
60
+ : enforcementForSurface(options.surface));
16
61
  const fixtureGeneration = fixture?.generator_response;
17
62
  const generationEnabled = Boolean(fixtureGeneration || options.generateCandidates);
18
- const baseInput = fixture?.input ?? searchInputFromControl(options.dir, {
63
+ const baseInput = fixture?.input ?? (dependencies.searchInputFromControl ?? searchInputFromControl)(options.dir, {
19
64
  scopeKey: options.runId,
20
65
  enforcement,
21
66
  generation: generationEnabled ? {
@@ -41,9 +86,11 @@ export async function executeDeliveryPathSearch(options, io = console) {
41
86
  ? codexEvaluator({ cwd: options.dir, model: options.model })
42
87
  : options.surface === "claude"
43
88
  ? claudeEvaluator({ cwd: options.dir, model: options.model })
44
- : null;
89
+ : options.surface === "opencode"
90
+ ? (dependencies.openCodeEvaluator ?? openCodeEvaluator)({ cwd: options.dir, model: options.model, preflight })
91
+ : null;
45
92
  if (!evaluator) {
46
- throw new Error(`${options.surface} has no executable evaluator in this release. Codex and Claude are the reference adapters; --fixture is available for deterministic contract testing.`);
93
+ throw new Error(`${options.surface} has no executable evaluator in this release. Codex, Claude and preflight-qualified OpenCode are the executable adapters; --fixture is available for deterministic contract testing.`);
47
94
  }
48
95
  const generator = fixtureGeneration
49
96
  ? fixtureGenerator(fixtureGeneration)
@@ -55,21 +102,38 @@ export async function executeDeliveryPathSearch(options, io = console) {
55
102
  if (options.generateCandidates && !generator) {
56
103
  throw new Error(`${options.surface} has no executable candidate generator; Codex and Claude are tool-enforced reference transports.`);
57
104
  }
58
- const result = await runDeliveryPathSearch(input, evaluator, { candidates: fixture?.candidates, generator });
59
- if (options.persist) result.persistence = persistSearchResult(options.dir, result);
105
+ let result;
106
+ try {
107
+ result = await (dependencies.runDeliveryPathSearch ?? runDeliveryPathSearch)(input, evaluator, { candidates: fixture?.candidates, generator });
108
+ if (!fixture && options.surface === "opencode" && result.budgets?.evaluations === 0) {
109
+ result.enforcement = enforcementForSurface("opencode");
110
+ }
111
+ } catch (error) {
112
+ if (!error?.fatalEvaluator || options.surface !== "opencode") throw error;
113
+ result = unavailableOpenCodeResult({ options, preflight, input, error });
114
+ }
115
+ if (options.persist && ["recommendation", "no_safe_recommendation"].includes(result.status)) {
116
+ result.persistence = (dependencies.persistSearchResult ?? persistSearchResult)(options.dir, result);
117
+ }
118
+ printDeliveryPathSearchResult(result, options, io);
119
+ return result;
120
+ }
121
+
122
+ function printDeliveryPathSearchResult(result, options, io) {
60
123
  if (options.json) {
61
124
  io.log(JSON.stringify(result, null, 2));
62
- return result;
125
+ return;
63
126
  }
64
127
  io.log(`AGDF Delivery Path Search: ${result.status}`);
65
- io.log(`Current gate: ${result.current_gate}`);
128
+ io.log(`Current gate: ${result.current_gate ?? "unknown"}`);
66
129
  io.log(`Enforcement: ${result.enforcement.level}`);
67
130
  io.log(`Recommendation: ${result.recommendation?.action ?? "none"}`);
68
- io.log(`Evaluations: ${result.budgets.evaluations}`);
69
- io.log(`Generation: ${result.generation.status} (${result.generation.accepted}/${result.generation.returned} accepted)`);
70
- io.log(`Generation budget: ${result.generation.cost_units} cost units, ${result.generation.duration_ms} ms`);
71
- if (result.generation.failure_code) io.log(`Generation failure: ${result.generation.failure_code}`);
131
+ if (result.budgets) io.log(`Evaluations: ${result.budgets.evaluations}`);
132
+ if (result.generation) {
133
+ io.log(`Generation: ${result.generation.status} (${result.generation.accepted}/${result.generation.returned} accepted)`);
134
+ io.log(`Generation budget: ${result.generation.cost_units} cost units, ${result.generation.duration_ms} ms`);
135
+ if (result.generation.failure_code) io.log(`Generation failure: ${result.generation.failure_code}`);
136
+ }
72
137
  io.log(`Stopping reason: ${result.stopping_reason}`);
73
138
  io.log(result.next_gate_action);
74
- return result;
75
139
  }
@@ -29,8 +29,9 @@ export function isGateSatisfied(runState, gate) {
29
29
  return isDurableGateArtefactSatisfied(runState, gate);
30
30
  }
31
31
 
32
- function qaRevisionRequired(runState) {
33
- return gateArtefactStatus(runState, "QA").status === "revise";
32
+ function qaNonPassDecision(runState) {
33
+ const status = gateArtefactStatus(runState, "QA").status;
34
+ return status === "revise" || status === "block" ? status : null;
34
35
  }
35
36
 
36
37
  export function transitionDecisionForRunState(runState, verifiedChange = null) {
@@ -233,15 +234,21 @@ export function transitionDecisionForRunState(runState, verifiedChange = null) {
233
234
  };
234
235
  }
235
236
 
236
- if (qaRevisionRequired(runState)) {
237
+ const qaNonPass = qaNonPassDecision(runState);
238
+ if (qaNonPass) {
239
+ const blocked = qaNonPass === "block";
237
240
  return {
238
- status: "open",
241
+ status: blocked ? "blocked" : "open",
239
242
  current_gate: "QA",
240
- blocking_reason: "qa_revise_required",
243
+ blocking_reason: blocked ? "qa_blocked" : "qa_revise_required",
241
244
  missing_approval: "none",
242
- allowed: ["revise the implementation against the QA findings", "refresh CD+Tests and mandatory reviews", "rerun QA with refreshed evidence"],
245
+ allowed: blocked
246
+ ? ["route the blocking QA findings to their authoritative owner", "record the next permitted remediation or upstream decision", "rerun QA only after the blocker is resolved"]
247
+ : ["revise the implementation against the QA findings", "refresh CD+Tests and mandatory reviews", "rerun QA with refreshed evidence"],
243
248
  forbidden: ["request QA approval", "request UAT approval", "release", "claim delivery readiness"],
244
- next_allowed_action: "Resolve the QA revise findings, refresh CD+Tests and reviews, then rerun QA. Do not request Approval: QA from a revise report.",
249
+ next_allowed_action: blocked
250
+ ? "Resolve or route the blocking QA findings through their authoritative owner, then rerun the required delivery steps. Do not request Approval: QA from a block report."
251
+ : "Resolve the QA revise findings, refresh CD+Tests and reviews, then rerun QA. Do not request Approval: QA from a revise report.",
245
252
  };
246
253
  }
247
254
 
@@ -56,6 +56,7 @@ const backlogStatusLabels = new Map([
56
56
  ["blocked", "blocked"],
57
57
  ["awaiting qa", "awaiting_qa"],
58
58
  ["awaiting uat", "awaiting_uat"],
59
+ ["awaiting or", "awaiting_or"],
59
60
  ["completed", "completed"],
60
61
  ["superseded", "superseded"],
61
62
  ["abandoned", "abandoned"],
@@ -1,6 +1,7 @@
1
1
  import { execFileSync } from "node:child_process";
2
2
  import { evaluatorOutputSchema, validateEvaluation } from "../contracts.js";
3
3
  import { guardedExecFileSync } from "../transports/read-only-guard.js";
4
+ import { buildEvaluatorPrompt } from "./prompt.js";
4
5
 
5
6
  export function claudeEvaluator(options = {}) {
6
7
  const claudeBin = options.claudeBin ?? "claude";
@@ -13,17 +14,7 @@ export function claudeEvaluator(options = {}) {
13
14
  name: "claude",
14
15
  metadata: { name: "claude", runtime, model: options.model ?? "configured-default" },
15
16
  async evaluate(input, candidate) {
16
- const prompt = [
17
- "You are an evaluation-only component. Do not use tools, execute commands, or modify files.",
18
- `Return JSON matching contract version ${input.contract_version}.`,
19
- `Objective: ${input.objective}`,
20
- `Current gate: ${input.current_gate}`,
21
- `Candidate id: ${candidate.id}`,
22
- `Candidate action: ${candidate.action}`,
23
- "Score scope_fit, gate_readiness, risk_reduction, evidence_gain, testability, reversibility and cost from 0 to 5.",
24
- "Cost 0 is low and 5 is high. uncertainty is 0 low to 5 high.",
25
- "Keep rationale concise. child_actions must only contain actions permitted by the current gate.",
26
- ].join("\n");
17
+ const prompt = buildEvaluatorPrompt(input, candidate);
27
18
  const args = [
28
19
  "-p", "--disallowedTools", "Edit,Write,Bash",
29
20
  "--output-format", "json", "--json-schema", JSON.stringify(evaluatorOutputSchema()),
@@ -4,6 +4,7 @@ import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { evaluatorOutputSchema, validateEvaluation } from "../contracts.js";
6
6
  import { guardedExecFileSync } from "../transports/read-only-guard.js";
7
+ import { buildEvaluatorPrompt } from "./prompt.js";
7
8
 
8
9
  export function codexEvaluator(options = {}) {
9
10
  const codexBin = options.codexBin ?? "codex";
@@ -21,17 +22,7 @@ export function codexEvaluator(options = {}) {
21
22
  const outputPath = join(temp, "output.json");
22
23
  try {
23
24
  writeFileSync(schemaPath, `${JSON.stringify(evaluatorOutputSchema(), null, 2)}\n`, "utf8");
24
- const prompt = [
25
- "You are an evaluation-only component. Do not use tools, execute commands, or modify files.",
26
- `Return JSON matching contract version ${input.contract_version}.`,
27
- `Objective: ${input.objective}`,
28
- `Current gate: ${input.current_gate}`,
29
- `Candidate id: ${candidate.id}`,
30
- `Candidate action: ${candidate.action}`,
31
- "Score scope_fit, gate_readiness, risk_reduction, evidence_gain, testability, reversibility and cost from 0 to 5.",
32
- "Cost 0 is low and 5 is high. uncertainty is 0 low to 5 high.",
33
- "Keep rationale concise. child_actions must only contain actions permitted by the current gate.",
34
- ].join("\n");
25
+ const prompt = buildEvaluatorPrompt(input, candidate);
35
26
  const args = [
36
27
  "exec", "--sandbox", "read-only", "--ephemeral", "--ignore-user-config",
37
28
  "--output-schema", schemaPath, "--output-last-message", outputPath, "--color", "never",
@@ -0,0 +1,234 @@
1
+ import { execFileSync, spawnSync } from "node:child_process";
2
+ import process from "node:process";
3
+ import { validateEvaluation } from "../contracts.js";
4
+ import { guardedExecFileSync } from "../transports/read-only-guard.js";
5
+ import { buildEvaluatorPrompt } from "./prompt.js";
6
+
7
+ export const OPENCODE_EVALUATOR_AGENT = "agdf-evaluator";
8
+
9
+ export const OPENCODE_DENY_PERMISSIONS = Object.freeze({
10
+ "*": "deny",
11
+ read: "deny",
12
+ edit: "deny",
13
+ bash: "deny",
14
+ task: "deny",
15
+ webfetch: "deny",
16
+ websearch: "deny",
17
+ skill: "deny",
18
+ lsp: "deny",
19
+ todowrite: "deny",
20
+ question: "deny",
21
+ external_directory: "deny",
22
+ });
23
+
24
+ const executablePermissions = Object.freeze([
25
+ "edit", "bash", "task", "webfetch", "websearch", "skill", "lsp", "todowrite", "question",
26
+ ]);
27
+
28
+ export function openCodePermissionEnvironment(env = process.env) {
29
+ return {
30
+ ...env,
31
+ OPENCODE_PERMISSION: JSON.stringify(OPENCODE_DENY_PERMISSIONS),
32
+ };
33
+ }
34
+
35
+ function preflightFailure(code, detail, evidence = []) {
36
+ return {
37
+ surface: "opencode",
38
+ status: "failed",
39
+ code,
40
+ detail,
41
+ evidence,
42
+ };
43
+ }
44
+
45
+ function extractAgentPermissions(output, agentName) {
46
+ const marker = `${agentName} (`;
47
+ const lines = output.split(/\r?\n/);
48
+ const start = lines.findIndex((line) => line.startsWith(marker));
49
+ if (start < 0) return null;
50
+ const mode = lines[start].slice(marker.length).split(")")[0];
51
+ let jsonStart = -1;
52
+ let depth = 0;
53
+ for (let index = start + 1; index < lines.length; index += 1) {
54
+ const line = lines[index].trim();
55
+ if (jsonStart < 0 && line === "[") {
56
+ jsonStart = index;
57
+ depth = 1;
58
+ continue;
59
+ }
60
+ if (jsonStart >= 0) {
61
+ for (const character of line) {
62
+ if (character === "[") depth += 1;
63
+ if (character === "]") depth -= 1;
64
+ }
65
+ if (depth === 0) return { mode, permissions: JSON.parse(lines.slice(jsonStart, index + 1).join("\n")) };
66
+ }
67
+ }
68
+ return null;
69
+ }
70
+
71
+ function terminalPermissionDecision(permissions, permissionName) {
72
+ let decision = null;
73
+ for (const entry of permissions) {
74
+ if (entry?.pattern === "*" && (entry.permission === "*" || entry.permission === permissionName)) {
75
+ decision = entry.action;
76
+ }
77
+ }
78
+ return decision;
79
+ }
80
+
81
+ export function preflightOpenCodeEvaluator(options = {}) {
82
+ const executable = options.openCodeBin ?? process.env.AGDF_OPENCODE_BIN ?? "opencode";
83
+ const run = options.execFileSync ?? execFileSync;
84
+ const cwd = options.cwd ?? process.cwd();
85
+ const agentName = options.agentName ?? OPENCODE_EVALUATOR_AGENT;
86
+ const env = openCodePermissionEnvironment(options.env ?? process.env);
87
+ let runtime;
88
+ let help;
89
+ let agents;
90
+ try {
91
+ runtime = run(executable, ["--version"], {
92
+ cwd, env, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 5000,
93
+ }).trim();
94
+ } catch (error) {
95
+ return preflightFailure("opencode_host_unavailable", (error.stderr || error.message).toString().trim());
96
+ }
97
+ try {
98
+ if (options.execFileSync) {
99
+ help = run(executable, ["run", "--help"], {
100
+ cwd, env, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 5000,
101
+ });
102
+ } else {
103
+ const helpProbe = (options.spawnSync ?? spawnSync)(executable, ["run", "--help"], {
104
+ cwd, env, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 5000,
105
+ });
106
+ if (helpProbe.error || helpProbe.status !== 0) throw helpProbe.error ?? new Error(helpProbe.stderr || "OpenCode run help failed.");
107
+ help = `${helpProbe.stdout ?? ""}\n${helpProbe.stderr ?? ""}`;
108
+ }
109
+ } catch (error) {
110
+ return preflightFailure("opencode_run_unavailable", (error.stderr || error.message).toString().trim(), [`opencode ${runtime}`]);
111
+ }
112
+ const requiredFlags = ["--pure", "--agent", "--format", "--dir"];
113
+ const missingFlags = requiredFlags.filter((flag) => !help.includes(flag));
114
+ if (missingFlags.length) {
115
+ return preflightFailure("opencode_run_capability_missing", `Missing required flags: ${missingFlags.join(", ")}`, [`opencode ${runtime}`]);
116
+ }
117
+ try {
118
+ agents = run(executable, ["agent", "list", "--pure"], {
119
+ cwd, env, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 10000,
120
+ });
121
+ } catch (error) {
122
+ return preflightFailure("opencode_agent_probe_failed", (error.stderr || error.message).toString().trim(), [`opencode ${runtime}`]);
123
+ }
124
+ let projection;
125
+ try {
126
+ projection = extractAgentPermissions(agents, agentName);
127
+ } catch (error) {
128
+ return preflightFailure("opencode_agent_permissions_unparseable", error.message, [`opencode ${runtime}`]);
129
+ }
130
+ if (!projection) {
131
+ return preflightFailure("opencode_evaluator_agent_missing", `OpenCode agent ${agentName} is not available.`, [`opencode ${runtime}`]);
132
+ }
133
+ if (projection.mode !== "primary") {
134
+ return preflightFailure("opencode_evaluator_agent_incompatible", `OpenCode agent ${agentName} must be primary for opencode run --agent; observed ${projection.mode || "unknown"}.`, [
135
+ `opencode ${runtime}`,
136
+ `agent:${agentName}`,
137
+ ]);
138
+ }
139
+ const unsafe = executablePermissions.filter((permission) => terminalPermissionDecision(projection.permissions, permission) !== "deny");
140
+ if (unsafe.length) {
141
+ return preflightFailure("opencode_evaluator_permissions_unsafe", `Terminal deny is not effective for: ${unsafe.join(", ")}`, [
142
+ `opencode ${runtime}`,
143
+ `agent:${agentName}`,
144
+ ]);
145
+ }
146
+ return {
147
+ surface: "opencode",
148
+ status: "passed",
149
+ code: "opencode_evaluator_ready",
150
+ invocation_id: `${process.pid}-${Date.now()}`,
151
+ runtime,
152
+ agent: agentName,
153
+ evidence: [
154
+ `opencode ${runtime}`,
155
+ "opencode run --pure --agent agdf-evaluator --format json",
156
+ "effective executable permissions terminal-deny",
157
+ ],
158
+ env,
159
+ };
160
+ }
161
+
162
+ export function parseOpenCodeEvaluatorOutput(raw) {
163
+ const text = [];
164
+ for (const line of raw.split(/\r?\n/).filter(Boolean)) {
165
+ const event = JSON.parse(line);
166
+ if (event?.type === "error") {
167
+ const status = event.error?.data?.statusCode;
168
+ const detail = event.error?.data?.message || event.error?.message || "unknown OpenCode evaluator error";
169
+ const error = new Error(`OpenCode evaluator error${status ? ` (${status})` : ""}: ${detail}`);
170
+ error.code = status === 401 || /auth|credential|provider available/i.test(detail)
171
+ ? "OPENCODE_EVALUATOR_AUTHENTICATION_FAILED"
172
+ : "OPENCODE_EVALUATOR_UNAVAILABLE";
173
+ throw error;
174
+ }
175
+ if (event?.part?.type === "text" && typeof event.part.text === "string") text.push(event.part.text);
176
+ else if (event?.type === "text" && typeof event.text === "string") text.push(event.text);
177
+ }
178
+ if (!text.length) throw new Error("OpenCode evaluator returned no text event.");
179
+ if (text.length !== 1) throw new Error(`OpenCode evaluator returned ${text.length} text events; exactly one final payload is required.`);
180
+ return JSON.parse(text[0]);
181
+ }
182
+
183
+ function fatalEvaluatorError(error) {
184
+ const wrapped = new Error(error.message);
185
+ wrapped.code = typeof error.code === "string" && error.code.startsWith("OPENCODE_EVALUATOR_")
186
+ ? error.code
187
+ : error.code === "GENERATOR_MUTATION_DETECTED"
188
+ ? "OPENCODE_EVALUATOR_MUTATION_DETECTED"
189
+ : error.code === "GENERATOR_TIMEOUT"
190
+ ? "OPENCODE_EVALUATOR_TIMEOUT"
191
+ : error.code === "ENOENT"
192
+ ? "OPENCODE_EVALUATOR_UNAVAILABLE"
193
+ : /auth|unauthorized|credential/i.test(`${error.message}\n${error.stderr ?? ""}`)
194
+ ? "OPENCODE_EVALUATOR_AUTHENTICATION_FAILED"
195
+ : "OPENCODE_EVALUATOR_OUTPUT_INVALID";
196
+ wrapped.fatalEvaluator = true;
197
+ return wrapped;
198
+ }
199
+
200
+ export function openCodeEvaluator(options = {}) {
201
+ if (options.preflight?.status !== "passed" || options.preflight.surface !== "opencode") {
202
+ throw new Error("OpenCode evaluator requires a successful current-invocation preflight.");
203
+ }
204
+ const executable = options.openCodeBin ?? process.env.AGDF_OPENCODE_BIN ?? "opencode";
205
+ const agentName = options.preflight.agent ?? OPENCODE_EVALUATOR_AGENT;
206
+ return {
207
+ name: "opencode",
208
+ metadata: {
209
+ name: "opencode",
210
+ runtime: options.preflight.runtime,
211
+ agent: agentName,
212
+ preflight: options.preflight.code,
213
+ },
214
+ async evaluate(input, candidate) {
215
+ const args = [
216
+ "run", "--pure", "--agent", agentName, "--format", "json", "--dir", options.cwd,
217
+ ];
218
+ if (options.model) args.push("--model", options.model);
219
+ args.push(buildEvaluatorPrompt(input, candidate));
220
+ try {
221
+ const raw = guardedExecFileSync(executable, args, {
222
+ cwd: options.cwd,
223
+ env: options.preflight.env,
224
+ encoding: "utf8",
225
+ stdio: ["ignore", "pipe", "pipe"],
226
+ timeout: options.timeoutMs ?? 120000,
227
+ });
228
+ return validateEvaluation(parseOpenCodeEvaluatorOutput(raw), candidate.id);
229
+ } catch (error) {
230
+ throw fatalEvaluatorError(error);
231
+ }
232
+ },
233
+ };
234
+ }
@@ -0,0 +1,13 @@
1
+ export function buildEvaluatorPrompt(input, candidate) {
2
+ return [
3
+ "You are an evaluation-only component. Do not use tools, execute commands, inspect files, delegate work or modify state.",
4
+ `Return only one JSON object matching contract version ${input.contract_version}.`,
5
+ `Objective: ${input.objective}`,
6
+ `Current gate: ${input.current_gate}`,
7
+ `Candidate id: ${candidate.id}`,
8
+ `Candidate action: ${candidate.action}`,
9
+ "Score scope_fit, gate_readiness, risk_reduction, evidence_gain, testability, reversibility and cost from 0 to 5.",
10
+ "Cost 0 is low and 5 is high. uncertainty is 0 low to 5 high.",
11
+ "Keep rationale concise. child_actions must only contain actions permitted by the current gate.",
12
+ ].join("\n");
13
+ }
@@ -114,6 +114,7 @@ export async function runDeliveryPathSearch(inputValue, evaluator, options = {})
114
114
  }
115
115
  }
116
116
  } catch (error) {
117
+ if (error?.fatalEvaluator === true) throw error;
117
118
  rejected.push({ candidate_id: candidate.id, action: candidate.action, reason: `invalid_evaluation: ${error.message}` });
118
119
  }
119
120
  }