create-agdf 0.4.9 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -3
- package/bin/create-agdf.js +74 -4
- package/generated/.github/skills/agdf-delivery-path-search/SKILL.md +14 -0
- package/generated/.github/skills/agdf-runtime-contract.md +6 -0
- package/generated/.opencode/agdf-runtime-contract.md +6 -0
- package/generated/.opencode/agents/agdf-delivery-path-search.md +14 -0
- package/generated/plugins/agdf/.codex-plugin/plugin.json +1 -1
- package/generated/plugins/agdf/meta/agdf-plugin.definition.json +1 -1
- package/generated/plugins/agdf/meta/agdf-runtime-contract.md +6 -0
- package/generated/plugins/agdf/skills/delivery-path-search/SKILL.md +14 -0
- package/lib/delivery-path-search/candidate-policy.js +67 -2
- package/lib/delivery-path-search/contracts.js +127 -0
- package/lib/delivery-path-search/evaluators/claude.js +2 -16
- package/lib/delivery-path-search/evaluators/codex.js +2 -16
- package/lib/delivery-path-search/generators/claude.js +40 -0
- package/lib/delivery-path-search/generators/codex.js +38 -0
- package/lib/delivery-path-search/generators/protocol.js +12 -0
- package/lib/delivery-path-search/persistence.js +5 -0
- package/lib/delivery-path-search/search-engine.js +64 -2
- package/lib/delivery-path-search/state-adapter.js +34 -2
- package/lib/delivery-path-search/transports/read-only-guard.js +43 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -155,17 +155,18 @@ Use `delivery-path-search` only for high-impact planning decisions with several
|
|
|
155
155
|
npx --yes @agdf/cli@latest delivery-path-search --surface codex --json
|
|
156
156
|
```
|
|
157
157
|
|
|
158
|
-
The
|
|
158
|
+
The runtime uses bounded best-first Delivery Path Search, not MCTS. It is read-only and advisory: the result must be checked by canonical `gate-check`. Codex and Claude Code are executable, tool-enforced evaluator adapters and support opt-in `--generate-candidates`; generated proposals supplement the deterministic baseline and are deterministically validated before evaluation. Copilot and OpenCode reuse the same skill and contracts as instruction-only surfaces until conforming executable adapters are available.
|
|
159
159
|
|
|
160
160
|
Requirements and boundaries:
|
|
161
161
|
|
|
162
162
|
- run it only in a repository with live `.agdf/control/AGDF_RUN.md` state
|
|
163
163
|
- the current control state must expose legal next actions
|
|
164
164
|
- Codex CLI must be installed and authenticated for `--surface codex`
|
|
165
|
-
-
|
|
165
|
+
- Claude Code CLI must be installed and authenticated for `--surface claude`
|
|
166
|
+
- `--model <id>` optionally selects the Codex or Claude evaluator model
|
|
166
167
|
- `--persist` writes redacted `DELIVERY_PATH_SEARCH.json` and `.md` evidence under the current scope
|
|
167
168
|
- `--fixture <path>` is for deterministic contract tests, not a production evaluator
|
|
168
|
-
-
|
|
169
|
+
- Copilot and OpenCode have shared workflow mappings but no executable native evaluator in this release
|
|
169
170
|
- cost units are rubric values used for bounded comparison, not measured provider currency
|
|
170
171
|
|
|
171
172
|
The result is either one recommendation or `no_safe_recommendation`. In both cases run canonical `gate-check` afterwards.
|
package/bin/create-agdf.js
CHANGED
|
@@ -11,6 +11,9 @@ import { runDeliveryPathSearch } from "../lib/delivery-path-search/search-engine
|
|
|
11
11
|
import { codexEvaluator } from "../lib/delivery-path-search/evaluators/codex.js";
|
|
12
12
|
import { claudeEvaluator } from "../lib/delivery-path-search/evaluators/claude.js";
|
|
13
13
|
import { fixtureEvaluator } from "../lib/delivery-path-search/evaluators/protocol.js";
|
|
14
|
+
import { codexGenerator } from "../lib/delivery-path-search/generators/codex.js";
|
|
15
|
+
import { claudeGenerator } from "../lib/delivery-path-search/generators/claude.js";
|
|
16
|
+
import { fixtureGenerator } from "../lib/delivery-path-search/generators/protocol.js";
|
|
14
17
|
import { enforcementForSurface } from "../lib/delivery-path-search/surfaces/capabilities.js";
|
|
15
18
|
import { persistSearchResult } from "../lib/delivery-path-search/persistence.js";
|
|
16
19
|
|
|
@@ -213,6 +216,13 @@ Options:
|
|
|
213
216
|
Use deterministic evaluator/candidate fixtures instead of a live evaluator
|
|
214
217
|
--persist Persist the redacted Delivery Path Search result under the current scope
|
|
215
218
|
--model <id> Optional Codex evaluator model
|
|
219
|
+
--generate-candidates
|
|
220
|
+
Add one bounded AI-native candidate-generation call before evaluation
|
|
221
|
+
--generator-model <id>
|
|
222
|
+
Optional candidate-generator model override
|
|
223
|
+
--max-generated-candidates <1-5>
|
|
224
|
+
--generation-timeout-ms <1-30000>
|
|
225
|
+
--generation-cost-units <1-5>
|
|
216
226
|
--help Show this help
|
|
217
227
|
`);
|
|
218
228
|
}
|
|
@@ -281,6 +291,11 @@ function parseArgs(argv) {
|
|
|
281
291
|
let fixture;
|
|
282
292
|
let persist = false;
|
|
283
293
|
let model;
|
|
294
|
+
let generateCandidates = false;
|
|
295
|
+
let generatorModel;
|
|
296
|
+
let maxGeneratedCandidates = 5;
|
|
297
|
+
let generationTimeoutMs = 30000;
|
|
298
|
+
let generationCostUnits = 5;
|
|
284
299
|
|
|
285
300
|
for (let i = 0; i < args.length; i += 1) {
|
|
286
301
|
const arg = args[i];
|
|
@@ -311,7 +326,12 @@ function parseArgs(argv) {
|
|
|
311
326
|
continue;
|
|
312
327
|
}
|
|
313
328
|
|
|
314
|
-
if (
|
|
329
|
+
if (arg === "--generate-candidates") {
|
|
330
|
+
generateCandidates = true;
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (["--surface", "--fixture", "--model", "--generator-model", "--max-generated-candidates", "--generation-timeout-ms", "--generation-cost-units"].includes(arg)) {
|
|
315
335
|
const next = args[i + 1];
|
|
316
336
|
if (!next) {
|
|
317
337
|
console.error(`Missing value for ${arg}`);
|
|
@@ -324,7 +344,19 @@ function parseArgs(argv) {
|
|
|
324
344
|
}
|
|
325
345
|
surface = next;
|
|
326
346
|
} else if (arg === "--fixture") fixture = next;
|
|
327
|
-
else model = next;
|
|
347
|
+
else if (arg === "--model") model = next;
|
|
348
|
+
else if (arg === "--generator-model") generatorModel = next;
|
|
349
|
+
else {
|
|
350
|
+
const value = Number(next);
|
|
351
|
+
const maximum = arg === "--generation-timeout-ms" ? 30000 : 5;
|
|
352
|
+
if (!Number.isInteger(value) || value < 1 || value > maximum) {
|
|
353
|
+
console.error(`${arg} must be an integer from 1 to ${maximum}.`);
|
|
354
|
+
process.exit(1);
|
|
355
|
+
}
|
|
356
|
+
if (arg === "--max-generated-candidates") maxGeneratedCandidates = value;
|
|
357
|
+
else if (arg === "--generation-timeout-ms") generationTimeoutMs = value;
|
|
358
|
+
else generationCostUnits = value;
|
|
359
|
+
}
|
|
328
360
|
i += 1;
|
|
329
361
|
continue;
|
|
330
362
|
}
|
|
@@ -395,6 +427,11 @@ function parseArgs(argv) {
|
|
|
395
427
|
fixture: fixture ? resolve(process.cwd(), fixture) : null,
|
|
396
428
|
persist,
|
|
397
429
|
model,
|
|
430
|
+
generateCandidates,
|
|
431
|
+
generatorModel,
|
|
432
|
+
maxGeneratedCandidates,
|
|
433
|
+
generationTimeoutMs,
|
|
434
|
+
generationCostUnits,
|
|
398
435
|
};
|
|
399
436
|
}
|
|
400
437
|
|
|
@@ -2198,7 +2235,27 @@ function printDeliveryMapReport(report, json) {
|
|
|
2198
2235
|
async function executeDeliveryPathSearch(options) {
|
|
2199
2236
|
const fixture = options.fixture ? JSON.parse(readFileSync(options.fixture, "utf8")) : null;
|
|
2200
2237
|
const enforcement = fixture?.input?.enforcement ?? enforcementForSurface(options.surface);
|
|
2201
|
-
const
|
|
2238
|
+
const fixtureGeneration = fixture?.generator_response;
|
|
2239
|
+
const generationEnabled = Boolean(fixtureGeneration || options.generateCandidates);
|
|
2240
|
+
const baseInput = fixture?.input ?? searchInputFromControl(options.dir, {
|
|
2241
|
+
enforcement,
|
|
2242
|
+
generation: generationEnabled ? {
|
|
2243
|
+
enabled: true,
|
|
2244
|
+
maxProposals: options.maxGeneratedCandidates,
|
|
2245
|
+
maxDurationMs: options.generationTimeoutMs,
|
|
2246
|
+
maxCostUnits: options.generationCostUnits,
|
|
2247
|
+
} : undefined,
|
|
2248
|
+
});
|
|
2249
|
+
const input = generationEnabled && !baseInput.generation ? {
|
|
2250
|
+
...baseInput,
|
|
2251
|
+
generation: {
|
|
2252
|
+
enabled: true,
|
|
2253
|
+
max_calls: 1,
|
|
2254
|
+
max_proposals: options.maxGeneratedCandidates,
|
|
2255
|
+
max_duration_ms: options.generationTimeoutMs,
|
|
2256
|
+
max_cost_units: options.generationCostUnits,
|
|
2257
|
+
},
|
|
2258
|
+
} : baseInput;
|
|
2202
2259
|
const evaluator = fixture
|
|
2203
2260
|
? fixtureEvaluator(fixture.evaluations ?? {})
|
|
2204
2261
|
: options.surface === "codex"
|
|
@@ -2209,7 +2266,17 @@ async function executeDeliveryPathSearch(options) {
|
|
|
2209
2266
|
if (!evaluator) {
|
|
2210
2267
|
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.`);
|
|
2211
2268
|
}
|
|
2212
|
-
const
|
|
2269
|
+
const generator = fixtureGeneration
|
|
2270
|
+
? fixtureGenerator(fixtureGeneration)
|
|
2271
|
+
: options.generateCandidates && options.surface === "codex"
|
|
2272
|
+
? codexGenerator({ cwd: options.dir, model: options.generatorModel, timeoutMs: options.generationTimeoutMs })
|
|
2273
|
+
: options.generateCandidates && options.surface === "claude"
|
|
2274
|
+
? claudeGenerator({ cwd: options.dir, model: options.generatorModel, timeoutMs: options.generationTimeoutMs })
|
|
2275
|
+
: null;
|
|
2276
|
+
if (options.generateCandidates && !generator) {
|
|
2277
|
+
throw new Error(`${options.surface} has no executable candidate generator; Codex and Claude are tool-enforced reference transports.`);
|
|
2278
|
+
}
|
|
2279
|
+
const result = await runDeliveryPathSearch(input, evaluator, { candidates: fixture?.candidates, generator });
|
|
2213
2280
|
if (options.persist) result.persistence = persistSearchResult(options.dir, result);
|
|
2214
2281
|
if (options.json) {
|
|
2215
2282
|
console.log(JSON.stringify(result, null, 2));
|
|
@@ -2220,6 +2287,9 @@ async function executeDeliveryPathSearch(options) {
|
|
|
2220
2287
|
console.log(`Enforcement: ${result.enforcement.level}`);
|
|
2221
2288
|
console.log(`Recommendation: ${result.recommendation?.action ?? "none"}`);
|
|
2222
2289
|
console.log(`Evaluations: ${result.budgets.evaluations}`);
|
|
2290
|
+
console.log(`Generation: ${result.generation.status} (${result.generation.accepted}/${result.generation.returned} accepted)`);
|
|
2291
|
+
console.log(`Generation budget: ${result.generation.cost_units} cost units, ${result.generation.duration_ms} ms`);
|
|
2292
|
+
if (result.generation.failure_code) console.log(`Generation failure: ${result.generation.failure_code}`);
|
|
2223
2293
|
console.log(`Stopping reason: ${result.stopping_reason}`);
|
|
2224
2294
|
console.log(result.next_gate_action);
|
|
2225
2295
|
return result;
|
|
@@ -37,6 +37,17 @@ Skip for routine questions, obvious local fixes and approved implementation with
|
|
|
37
37
|
npx --yes @agdf/cli@latest delivery-path-search --surface <surface> --json
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
+
Optional AI-native candidate generation is explicit:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npx --yes @agdf/cli@latest delivery-path-search \
|
|
44
|
+
--surface codex \
|
|
45
|
+
--generate-candidates \
|
|
46
|
+
--json
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
It supplements the deterministic baseline with at most one bounded generator call. Treat proposals as untrusted until the canonical core validates schema, gate action, scope, duplicates and material diversity. Report generation provenance, budgets, rejections and failure. Never hide deterministic fallback or switch providers automatically.
|
|
50
|
+
|
|
40
51
|
5. Treat facts, repository evidence, assumptions and model judgements separately.
|
|
41
52
|
6. Return exactly one recommendation or `no_safe_recommendation`.
|
|
42
53
|
7. Run canonical AGDF gate-check after search. Never translate a recommendation into permission.
|
|
@@ -73,3 +84,6 @@ Persist only the redacted decision summary when durable evidence is required. Do
|
|
|
73
84
|
- Do not invent default scores for invalid evaluator output.
|
|
74
85
|
- Do not implement, approve or release based on search output.
|
|
75
86
|
- Do not create agent-specific scoring, gate logic or skill copies.
|
|
87
|
+
- Do not replace deterministic candidates with generated proposals.
|
|
88
|
+
- Do not send secrets, full artefacts, source snapshots, raw prompts or hidden reasoning to a generator.
|
|
89
|
+
- Do not claim executable generation for instruction-only surfaces.
|
|
@@ -91,6 +91,12 @@ Delivery Path Search is an optional read-only planning step for high-impact deci
|
|
|
91
91
|
- Search output is evidence only. Canonical `agdf-gate-check` independently decides what may proceed.
|
|
92
92
|
- The bounded first-release algorithm must not be labelled MCTS.
|
|
93
93
|
- Surface adapters may translate transport and presentation, but must not fork scoring, search or gate semantics.
|
|
94
|
+
- Optional AI-native candidate generation supplements the deterministic candidate baseline; it never replaces it.
|
|
95
|
+
- Generated proposals are untrusted. The core must validate schema, exact canonical gate action, scope, duplicates and material diversity before evaluation.
|
|
96
|
+
- Generation is opt-in and bounded to one call, five proposals, 30 seconds and five abstract cost units by default, within whole-run budgets.
|
|
97
|
+
- External generation receives only bounded normalized control summaries and references, never secrets, full artefacts, raw prompts, hidden reasoning or source snapshots.
|
|
98
|
+
- Generator status, provenance, accepted/rejected counts, separate budget use and typed failure must remain visible. Failure retains the deterministic baseline; automatic provider fallback is forbidden.
|
|
99
|
+
- Codex and Claude Code may provide tool-enforced generator transports. Copilot, OpenCode and generic surfaces remain instruction-only without conforming evidence.
|
|
94
100
|
|
|
95
101
|
The field names above are the stable machine-readable contract used by JSON
|
|
96
102
|
reports and automation. Human-facing Markdown must present the same projection
|
|
@@ -91,6 +91,12 @@ Delivery Path Search is an optional read-only planning step for high-impact deci
|
|
|
91
91
|
- Search output is evidence only. Canonical `agdf-gate-check` independently decides what may proceed.
|
|
92
92
|
- The bounded first-release algorithm must not be labelled MCTS.
|
|
93
93
|
- Surface adapters may translate transport and presentation, but must not fork scoring, search or gate semantics.
|
|
94
|
+
- Optional AI-native candidate generation supplements the deterministic candidate baseline; it never replaces it.
|
|
95
|
+
- Generated proposals are untrusted. The core must validate schema, exact canonical gate action, scope, duplicates and material diversity before evaluation.
|
|
96
|
+
- Generation is opt-in and bounded to one call, five proposals, 30 seconds and five abstract cost units by default, within whole-run budgets.
|
|
97
|
+
- External generation receives only bounded normalized control summaries and references, never secrets, full artefacts, raw prompts, hidden reasoning or source snapshots.
|
|
98
|
+
- Generator status, provenance, accepted/rejected counts, separate budget use and typed failure must remain visible. Failure retains the deterministic baseline; automatic provider fallback is forbidden.
|
|
99
|
+
- Codex and Claude Code may provide tool-enforced generator transports. Copilot, OpenCode and generic surfaces remain instruction-only without conforming evidence.
|
|
94
100
|
|
|
95
101
|
The field names above are the stable machine-readable contract used by JSON
|
|
96
102
|
reports and automation. Human-facing Markdown must present the same projection
|
|
@@ -37,6 +37,17 @@ Skip for routine questions, obvious local fixes and approved implementation with
|
|
|
37
37
|
npx --yes @agdf/cli@latest delivery-path-search --surface <surface> --json
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
+
Optional AI-native candidate generation is explicit:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npx --yes @agdf/cli@latest delivery-path-search \
|
|
44
|
+
--surface codex \
|
|
45
|
+
--generate-candidates \
|
|
46
|
+
--json
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
It supplements the deterministic baseline with at most one bounded generator call. Treat proposals as untrusted until the canonical core validates schema, gate action, scope, duplicates and material diversity. Report generation provenance, budgets, rejections and failure. Never hide deterministic fallback or switch providers automatically.
|
|
50
|
+
|
|
40
51
|
5. Treat facts, repository evidence, assumptions and model judgements separately.
|
|
41
52
|
6. Return exactly one recommendation or `no_safe_recommendation`.
|
|
42
53
|
7. Run canonical AGDF gate-check after search. Never translate a recommendation into permission.
|
|
@@ -73,4 +84,7 @@ Persist only the redacted decision summary when durable evidence is required. Do
|
|
|
73
84
|
- Do not invent default scores for invalid evaluator output.
|
|
74
85
|
- Do not implement, approve or release based on search output.
|
|
75
86
|
- Do not create agent-specific scoring, gate logic or skill copies.
|
|
87
|
+
- Do not replace deterministic candidates with generated proposals.
|
|
88
|
+
- Do not send secrets, full artefacts, source snapshots, raw prompts or hidden reasoning to a generator.
|
|
89
|
+
- Do not claim executable generation for instruction-only surfaces.
|
|
76
90
|
|
|
@@ -91,6 +91,12 @@ Delivery Path Search is an optional read-only planning step for high-impact deci
|
|
|
91
91
|
- Search output is evidence only. Canonical `gate-check` independently decides what may proceed.
|
|
92
92
|
- The bounded first-release algorithm must not be labelled MCTS.
|
|
93
93
|
- Surface adapters may translate transport and presentation, but must not fork scoring, search or gate semantics.
|
|
94
|
+
- Optional AI-native candidate generation supplements the deterministic candidate baseline; it never replaces it.
|
|
95
|
+
- Generated proposals are untrusted. The core must validate schema, exact canonical gate action, scope, duplicates and material diversity before evaluation.
|
|
96
|
+
- Generation is opt-in and bounded to one call, five proposals, 30 seconds and five abstract cost units by default, within whole-run budgets.
|
|
97
|
+
- External generation receives only bounded normalized control summaries and references, never secrets, full artefacts, raw prompts, hidden reasoning or source snapshots.
|
|
98
|
+
- Generator status, provenance, accepted/rejected counts, separate budget use and typed failure must remain visible. Failure retains the deterministic baseline; automatic provider fallback is forbidden.
|
|
99
|
+
- Codex and Claude Code may provide tool-enforced generator transports. Copilot, OpenCode and generic surfaces remain instruction-only without conforming evidence.
|
|
94
100
|
|
|
95
101
|
The field names above are the stable machine-readable contract used by JSON
|
|
96
102
|
reports and automation. Human-facing Markdown must present the same projection
|
|
@@ -37,6 +37,17 @@ Skip for routine questions, obvious local fixes and approved implementation with
|
|
|
37
37
|
npx --yes @agdf/cli@latest delivery-path-search --surface <surface> --json
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
+
Optional AI-native candidate generation is explicit:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npx --yes @agdf/cli@latest delivery-path-search \
|
|
44
|
+
--surface codex \
|
|
45
|
+
--generate-candidates \
|
|
46
|
+
--json
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
It supplements the deterministic baseline with at most one bounded generator call. Treat proposals as untrusted until the canonical core validates schema, gate action, scope, duplicates and material diversity. Report generation provenance, budgets, rejections and failure. Never hide deterministic fallback or switch providers automatically.
|
|
50
|
+
|
|
40
51
|
5. Treat facts, repository evidence, assumptions and model judgements separately.
|
|
41
52
|
6. Return exactly one recommendation or `no_safe_recommendation`.
|
|
42
53
|
7. Run canonical AGDF gate-check after search. Never translate a recommendation into permission.
|
|
@@ -73,3 +84,6 @@ Persist only the redacted decision summary when durable evidence is required. Do
|
|
|
73
84
|
- Do not invent default scores for invalid evaluator output.
|
|
74
85
|
- Do not implement, approve or release based on search output.
|
|
75
86
|
- Do not create agent-specific scoring, gate logic or skill copies.
|
|
87
|
+
- Do not replace deterministic candidates with generated proposals.
|
|
88
|
+
- Do not send secrets, full artefacts, source snapshots, raw prompts or hidden reasoning to a generator.
|
|
89
|
+
- Do not claim executable generation for instruction-only surfaces.
|
|
@@ -1,12 +1,37 @@
|
|
|
1
1
|
import { validateCandidate } from "./contracts.js";
|
|
2
2
|
|
|
3
3
|
function normalized(value) {
|
|
4
|
-
return String(value).toLowerCase().replace(/[`*_]/g, "").replace(/\s+/g, " ").trim();
|
|
4
|
+
return String(value).normalize("NFKC").toLowerCase().replace(/[`*_]/g, "").replace(/[^\p{L}\p{N}\s-]/gu, " ").replace(/\s+/g, " ").trim();
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const STOP_WORDS = new Set(["a", "an", "and", "der", "die", "das", "ein", "eine", "the", "to", "und", "zu"]);
|
|
8
|
+
|
|
9
|
+
function tokens(value) {
|
|
10
|
+
return new Set(normalized(value).split(/\s+/).filter((item) => item && !STOP_WORDS.has(item)));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function jaccard(left, right) {
|
|
14
|
+
const union = new Set([...left, ...right]);
|
|
15
|
+
if (!union.size) return 1;
|
|
16
|
+
let intersection = 0;
|
|
17
|
+
for (const item of left) if (right.has(item)) intersection += 1;
|
|
18
|
+
return intersection / union.size;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function signature(candidate) {
|
|
22
|
+
return JSON.stringify({
|
|
23
|
+
gate_action: normalized(candidate.gate_action ?? candidate.action),
|
|
24
|
+
affected_boundaries: [...(candidate.affected_boundaries ?? [])].map(normalized).sort(),
|
|
25
|
+
evidence: [...(candidate.expected_evidence ?? [])].map(normalized).sort(),
|
|
26
|
+
tests: [...(candidate.tests ?? [])].map(normalized).sort(),
|
|
27
|
+
risk_strategy: normalized(candidate.risk_strategy ?? ""),
|
|
28
|
+
reversibility: normalized(candidate.reversibility ?? ""),
|
|
29
|
+
});
|
|
5
30
|
}
|
|
6
31
|
|
|
7
32
|
export function candidateLegality(candidateValue, input) {
|
|
8
33
|
const candidate = validateCandidate(candidateValue);
|
|
9
|
-
const action = normalized(candidate.action);
|
|
34
|
+
const action = normalized(candidate.gate_action ?? candidate.action);
|
|
10
35
|
const forbiddenMatch = input.forbidden_actions.find((item) => {
|
|
11
36
|
const forbidden = normalized(item);
|
|
12
37
|
return forbidden && action === forbidden;
|
|
@@ -32,3 +57,43 @@ export function candidatesFromInput(input) {
|
|
|
32
57
|
parent_id: null,
|
|
33
58
|
}));
|
|
34
59
|
}
|
|
60
|
+
|
|
61
|
+
export function generatedCandidatesFromResponse(response, input, existingCandidates = []) {
|
|
62
|
+
const accepted = [];
|
|
63
|
+
const rejected = [];
|
|
64
|
+
for (const proposal of response.proposals) {
|
|
65
|
+
const candidate = {
|
|
66
|
+
id: `generated-${proposal.proposal_id}`,
|
|
67
|
+
action: proposal.intent,
|
|
68
|
+
source: "generated",
|
|
69
|
+
gate_action: proposal.gate_action,
|
|
70
|
+
intent: proposal.intent,
|
|
71
|
+
expected_evidence: proposal.expected_evidence,
|
|
72
|
+
tests: proposal.tests,
|
|
73
|
+
assumptions: proposal.assumptions,
|
|
74
|
+
affected_boundaries: proposal.affected_boundaries,
|
|
75
|
+
risk_strategy: proposal.risk_strategy,
|
|
76
|
+
reversibility: proposal.reversibility,
|
|
77
|
+
generator_proposal_id: proposal.proposal_id,
|
|
78
|
+
depth: 0,
|
|
79
|
+
parent_id: null,
|
|
80
|
+
};
|
|
81
|
+
const legality = candidateLegality(candidate, input);
|
|
82
|
+
if (!legality.legal) {
|
|
83
|
+
rejected.push({ proposal_id: proposal.proposal_id, reason: legality.reason });
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
const peers = [...existingCandidates, ...accepted].filter((item) => normalized(item.gate_action ?? item.action) === normalized(candidate.gate_action));
|
|
87
|
+
const duplicate = peers.find((item) => {
|
|
88
|
+
const same = normalized(item.intent ?? item.action) === normalized(candidate.intent);
|
|
89
|
+
const sameDecision = signature(item) === signature(candidate);
|
|
90
|
+
return same || sameDecision || jaccard(tokens(item.intent ?? item.action), tokens(candidate.intent)) >= 0.8;
|
|
91
|
+
});
|
|
92
|
+
if (duplicate) {
|
|
93
|
+
rejected.push({ proposal_id: proposal.proposal_id, reason: "cosmetic_or_material_duplicate" });
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
accepted.push(candidate);
|
|
97
|
+
}
|
|
98
|
+
return { accepted, rejected };
|
|
99
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export const CONTRACT_VERSION = "1";
|
|
2
|
+
export const GENERATOR_CONTRACT_VERSION = "1";
|
|
2
3
|
export const SCORING_POLICY_VERSION = "1";
|
|
3
4
|
export const SCORE_DIMENSIONS = [
|
|
4
5
|
"scope_fit",
|
|
@@ -25,6 +26,39 @@ function stringArray(value, label) {
|
|
|
25
26
|
return value.map((item) => item.trim()).filter(Boolean);
|
|
26
27
|
}
|
|
27
28
|
|
|
29
|
+
function optionalBoundedString(value, label, maxLength = 1000) {
|
|
30
|
+
if (value === undefined) return undefined;
|
|
31
|
+
const result = requireString(value, label);
|
|
32
|
+
if (result.length > maxLength) throw new Error(`${label} must be at most ${maxLength} characters`);
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function rejectUnknownKeys(value, allowed, label) {
|
|
37
|
+
const unknown = Object.keys(value).filter((key) => !allowed.includes(key));
|
|
38
|
+
if (unknown.length) throw new Error(`${label} contains unknown fields: ${unknown.join(", ")}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function generationConfig(value, wholeRunBudgets) {
|
|
42
|
+
if (value === undefined) return undefined;
|
|
43
|
+
const generation = requireObject(value, "generation");
|
|
44
|
+
if (typeof generation.enabled !== "boolean") throw new Error("generation.enabled must be a boolean");
|
|
45
|
+
const limits = {
|
|
46
|
+
max_calls: 1,
|
|
47
|
+
max_proposals: 5,
|
|
48
|
+
max_duration_ms: 30000,
|
|
49
|
+
max_cost_units: 5,
|
|
50
|
+
};
|
|
51
|
+
for (const [key, maximum] of Object.entries(limits)) {
|
|
52
|
+
if (!Number.isInteger(generation[key]) || generation[key] < 1 || generation[key] > maximum) {
|
|
53
|
+
throw new Error(`generation.${key} must be an integer from 1 to ${maximum}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (generation.max_calls !== 1) throw new Error("generation.max_calls must equal 1 in this contract version");
|
|
57
|
+
if (generation.max_duration_ms > wholeRunBudgets.max_duration_ms) throw new Error("generation.max_duration_ms cannot exceed budgets.max_duration_ms");
|
|
58
|
+
if (generation.max_cost_units > wholeRunBudgets.max_cost_units) throw new Error("generation.max_cost_units cannot exceed budgets.max_cost_units");
|
|
59
|
+
return structuredClone(generation);
|
|
60
|
+
}
|
|
61
|
+
|
|
28
62
|
export function validateSearchInput(value) {
|
|
29
63
|
const input = requireObject(value, "search input");
|
|
30
64
|
if (input.contract_version !== CONTRACT_VERSION) throw new Error(`unsupported search input contract_version: ${input.contract_version}`);
|
|
@@ -43,6 +77,7 @@ export function validateSearchInput(value) {
|
|
|
43
77
|
for (const key of ["max_candidates", "max_depth", "max_evaluations", "max_duration_ms", "max_cost_units", "stability_window"]) {
|
|
44
78
|
if (!Number.isInteger(budgets[key]) || budgets[key] < 1) throw new Error(`budgets.${key} must be a positive integer`);
|
|
45
79
|
}
|
|
80
|
+
generationConfig(input.generation, budgets);
|
|
46
81
|
return structuredClone(input);
|
|
47
82
|
}
|
|
48
83
|
|
|
@@ -53,9 +88,101 @@ export function validateCandidate(value) {
|
|
|
53
88
|
stringArray(candidate.expected_evidence ?? [], "candidate.expected_evidence");
|
|
54
89
|
stringArray(candidate.tests ?? [], "candidate.tests");
|
|
55
90
|
stringArray(candidate.assumptions ?? [], "candidate.assumptions");
|
|
91
|
+
if (candidate.source !== undefined && !["deterministic", "generated", "expanded"].includes(candidate.source)) {
|
|
92
|
+
throw new Error("candidate.source must be deterministic, generated or expanded");
|
|
93
|
+
}
|
|
94
|
+
optionalBoundedString(candidate.gate_action, "candidate.gate_action");
|
|
95
|
+
optionalBoundedString(candidate.intent, "candidate.intent");
|
|
96
|
+
stringArray(candidate.affected_boundaries ?? [], "candidate.affected_boundaries");
|
|
97
|
+
optionalBoundedString(candidate.risk_strategy, "candidate.risk_strategy");
|
|
98
|
+
optionalBoundedString(candidate.reversibility, "candidate.reversibility");
|
|
99
|
+
optionalBoundedString(candidate.generator_proposal_id, "candidate.generator_proposal_id", 200);
|
|
56
100
|
return structuredClone(candidate);
|
|
57
101
|
}
|
|
58
102
|
|
|
103
|
+
export function validateGeneratorRequest(value) {
|
|
104
|
+
const request = requireObject(value, "generator request");
|
|
105
|
+
rejectUnknownKeys(request, ["contract_version", "scope_key", "objective", "scope_summary", "current_gate", "allowed_actions", "forbidden_actions", "artefact_refs", "evidence", "missing_evidence", "risks", "constraints", "enforcement", "budgets"], "generator request");
|
|
106
|
+
if (request.contract_version !== GENERATOR_CONTRACT_VERSION) throw new Error(`unsupported generator contract_version: ${request.contract_version}`);
|
|
107
|
+
requireString(request.scope_key, "generator scope_key");
|
|
108
|
+
optionalBoundedString(request.objective, "generator objective", 2000);
|
|
109
|
+
optionalBoundedString(request.scope_summary, "generator scope_summary", 2000);
|
|
110
|
+
requireString(request.current_gate, "generator current_gate");
|
|
111
|
+
for (const key of ["allowed_actions", "forbidden_actions", "artefact_refs", "evidence", "missing_evidence", "risks", "constraints"]) {
|
|
112
|
+
const items = stringArray(request[key] ?? [], `generator ${key}`);
|
|
113
|
+
if (items.length > 20 || items.some((item) => item.length > 500)) throw new Error(`generator ${key} exceeds context bounds`);
|
|
114
|
+
}
|
|
115
|
+
validateEnforcement(request.enforcement);
|
|
116
|
+
const budgets = requireObject(request.budgets, "generator budgets");
|
|
117
|
+
generationConfig({ enabled: true, ...budgets }, { max_duration_ms: 120000, max_cost_units: 20 });
|
|
118
|
+
const serialized = JSON.stringify(request);
|
|
119
|
+
if (/(secret|credential|environment|raw_prompt|hidden_reasoning|source_snapshot)/i.test(serialized)) {
|
|
120
|
+
throw new Error("generator request contains disallowed context");
|
|
121
|
+
}
|
|
122
|
+
return structuredClone(request);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function validateGeneratorResponse(value) {
|
|
126
|
+
const response = requireObject(value, "generator response");
|
|
127
|
+
rejectUnknownKeys(response, ["contract_version", "proposals", "cost_units", "metadata"], "generator response");
|
|
128
|
+
if (response.contract_version !== GENERATOR_CONTRACT_VERSION) throw new Error(`unsupported generator contract_version: ${response.contract_version}`);
|
|
129
|
+
if (!Array.isArray(response.proposals) || response.proposals.length > 5) throw new Error("generator proposals must be an array with at most 5 items");
|
|
130
|
+
if (!Number.isInteger(response.cost_units) || response.cost_units < 0 || response.cost_units > 5) throw new Error("generator cost_units must be an integer from 0 to 5");
|
|
131
|
+
if (response.metadata !== undefined) {
|
|
132
|
+
const metadata = requireObject(response.metadata, "generator metadata");
|
|
133
|
+
rejectUnknownKeys(metadata, ["name", "runtime", "model"], "generator metadata");
|
|
134
|
+
for (const [key, item] of Object.entries(metadata)) optionalBoundedString(item, `generator metadata.${key}`, 500);
|
|
135
|
+
}
|
|
136
|
+
const proposals = response.proposals.map((proposal, index) => {
|
|
137
|
+
const item = requireObject(proposal, `proposal ${index}`);
|
|
138
|
+
rejectUnknownKeys(item, ["proposal_id", "gate_action", "intent", "expected_evidence", "tests", "assumptions", "affected_boundaries", "risk_strategy", "reversibility"], `proposal ${index}`);
|
|
139
|
+
const normalized = {
|
|
140
|
+
proposal_id: requireString(item.proposal_id, `proposal ${index}.proposal_id`),
|
|
141
|
+
gate_action: requireString(item.gate_action, `proposal ${index}.gate_action`),
|
|
142
|
+
intent: requireString(item.intent, `proposal ${index}.intent`),
|
|
143
|
+
expected_evidence: stringArray(item.expected_evidence ?? [], `proposal ${index}.expected_evidence`),
|
|
144
|
+
tests: stringArray(item.tests ?? [], `proposal ${index}.tests`),
|
|
145
|
+
assumptions: stringArray(item.assumptions ?? [], `proposal ${index}.assumptions`),
|
|
146
|
+
affected_boundaries: stringArray(item.affected_boundaries ?? [], `proposal ${index}.affected_boundaries`),
|
|
147
|
+
risk_strategy: requireString(item.risk_strategy, `proposal ${index}.risk_strategy`),
|
|
148
|
+
reversibility: requireString(item.reversibility, `proposal ${index}.reversibility`),
|
|
149
|
+
};
|
|
150
|
+
if (JSON.stringify(normalized).length > 5000) throw new Error(`proposal ${index} exceeds size limit`);
|
|
151
|
+
if (/(^|\s)(rm|sudo|curl|wget|bash|sh|exec)\s/i.test(normalized.intent)) throw new Error(`proposal ${index} contains executable instructions`);
|
|
152
|
+
return normalized;
|
|
153
|
+
});
|
|
154
|
+
return {
|
|
155
|
+
contract_version: response.contract_version,
|
|
156
|
+
proposals,
|
|
157
|
+
cost_units: response.cost_units,
|
|
158
|
+
metadata: response.metadata ? structuredClone(response.metadata) : {},
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function generatorOutputSchema() {
|
|
163
|
+
return {
|
|
164
|
+
type: "object",
|
|
165
|
+
additionalProperties: false,
|
|
166
|
+
required: ["contract_version", "proposals", "cost_units"],
|
|
167
|
+
properties: {
|
|
168
|
+
contract_version: { type: "string", const: GENERATOR_CONTRACT_VERSION },
|
|
169
|
+
cost_units: { type: "integer", minimum: 0, maximum: 5 },
|
|
170
|
+
proposals: {
|
|
171
|
+
type: "array", maxItems: 5, items: {
|
|
172
|
+
type: "object", additionalProperties: false,
|
|
173
|
+
required: ["proposal_id", "gate_action", "intent", "expected_evidence", "tests", "assumptions", "affected_boundaries", "risk_strategy", "reversibility"],
|
|
174
|
+
properties: {
|
|
175
|
+
proposal_id: { type: "string", minLength: 1 }, gate_action: { type: "string", minLength: 1 }, intent: { type: "string", minLength: 1 },
|
|
176
|
+
expected_evidence: { type: "array", items: { type: "string" } }, tests: { type: "array", items: { type: "string" } },
|
|
177
|
+
assumptions: { type: "array", items: { type: "string" } }, affected_boundaries: { type: "array", items: { type: "string" } },
|
|
178
|
+
risk_strategy: { type: "string", minLength: 1 }, reversibility: { type: "string", minLength: 1 },
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
59
186
|
export function validateEvaluation(value, candidateId) {
|
|
60
187
|
const evaluation = requireObject(value, "evaluation");
|
|
61
188
|
if (evaluation.contract_version !== CONTRACT_VERSION) throw new Error(`unsupported evaluator contract_version: ${evaluation.contract_version}`);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
2
|
import { evaluatorOutputSchema, validateEvaluation } from "../contracts.js";
|
|
3
|
+
import { guardedExecFileSync } from "../transports/read-only-guard.js";
|
|
3
4
|
|
|
4
5
|
export function claudeEvaluator(options = {}) {
|
|
5
6
|
const claudeBin = options.claudeBin ?? "claude";
|
|
@@ -8,18 +9,6 @@ export function claudeEvaluator(options = {}) {
|
|
|
8
9
|
runtime = execFileSync(claudeBin, ["--version"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
9
10
|
} catch {}
|
|
10
11
|
|
|
11
|
-
function gitState() {
|
|
12
|
-
try {
|
|
13
|
-
return execFileSync("git", ["status", "--porcelain=v1", "-z"], {
|
|
14
|
-
cwd: options.cwd,
|
|
15
|
-
encoding: "utf8",
|
|
16
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
17
|
-
});
|
|
18
|
-
} catch {
|
|
19
|
-
return null;
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
12
|
return {
|
|
24
13
|
name: "claude",
|
|
25
14
|
metadata: { name: "claude", runtime, model: options.model ?? "configured-default" },
|
|
@@ -41,15 +30,12 @@ export function claudeEvaluator(options = {}) {
|
|
|
41
30
|
];
|
|
42
31
|
if (options.model) args.push("--model", options.model);
|
|
43
32
|
args.push(prompt);
|
|
44
|
-
const
|
|
45
|
-
const raw = execFileSync(claudeBin, args, {
|
|
33
|
+
const raw = guardedExecFileSync(claudeBin, args, {
|
|
46
34
|
cwd: options.cwd,
|
|
47
35
|
encoding: "utf8",
|
|
48
36
|
stdio: ["ignore", "pipe", "pipe"],
|
|
49
37
|
timeout: options.timeoutMs ?? 120000,
|
|
50
38
|
});
|
|
51
|
-
const after = gitState();
|
|
52
|
-
if (before !== null && after !== before) throw new Error("repository mutation detected during read-only evaluator run");
|
|
53
39
|
const response = JSON.parse(raw);
|
|
54
40
|
if (response.is_error) throw new Error(`claude evaluator returned an error: ${response.result ?? "unknown error"}`);
|
|
55
41
|
return validateEvaluation(JSON.parse(response.result), candidate.id);
|
|
@@ -3,6 +3,7 @@ import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { evaluatorOutputSchema, validateEvaluation } from "../contracts.js";
|
|
6
|
+
import { guardedExecFileSync } from "../transports/read-only-guard.js";
|
|
6
7
|
|
|
7
8
|
export function codexEvaluator(options = {}) {
|
|
8
9
|
const codexBin = options.codexBin ?? "codex";
|
|
@@ -11,18 +12,6 @@ export function codexEvaluator(options = {}) {
|
|
|
11
12
|
runtime = execFileSync(codexBin, ["--version"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
12
13
|
} catch {}
|
|
13
14
|
|
|
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
15
|
return {
|
|
27
16
|
name: "codex",
|
|
28
17
|
metadata: { name: "codex", runtime, model: options.model ?? "configured-default" },
|
|
@@ -49,15 +38,12 @@ export function codexEvaluator(options = {}) {
|
|
|
49
38
|
];
|
|
50
39
|
if (options.model) args.push("--model", options.model);
|
|
51
40
|
args.push(prompt);
|
|
52
|
-
|
|
53
|
-
execFileSync(codexBin, args, {
|
|
41
|
+
guardedExecFileSync(codexBin, args, {
|
|
54
42
|
cwd: options.cwd,
|
|
55
43
|
encoding: "utf8",
|
|
56
44
|
stdio: ["ignore", "pipe", "pipe"],
|
|
57
45
|
timeout: options.timeoutMs ?? 120000,
|
|
58
46
|
});
|
|
59
|
-
const after = gitState();
|
|
60
|
-
if (before !== null && after !== before) throw new Error("repository mutation detected during read-only evaluator run");
|
|
61
47
|
return validateEvaluation(JSON.parse(readFileSync(outputPath, "utf8")), candidate.id);
|
|
62
48
|
} finally {
|
|
63
49
|
rmSync(temp, { recursive: true, force: true });
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { generatorOutputSchema, validateGeneratorRequest, validateGeneratorResponse } from "../contracts.js";
|
|
3
|
+
import { guardedExecFileSync } from "../transports/read-only-guard.js";
|
|
4
|
+
|
|
5
|
+
export function claudeGenerator(options = {}) {
|
|
6
|
+
const claudeBin = options.claudeBin ?? "claude";
|
|
7
|
+
const execute = options.execute ?? guardedExecFileSync;
|
|
8
|
+
let runtime = options.runtime ?? "claude version unavailable";
|
|
9
|
+
if (!options.runtime) try { runtime = execFileSync(claudeBin, ["--version"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim(); } catch {}
|
|
10
|
+
return {
|
|
11
|
+
name: "claude",
|
|
12
|
+
metadata: { name: "claude", runtime, model: options.model ?? "configured-default" },
|
|
13
|
+
async generate(requestValue) {
|
|
14
|
+
const request = validateGeneratorRequest(requestValue);
|
|
15
|
+
const prompt = [
|
|
16
|
+
"Generate bounded advisory delivery-path proposals. Do not use tools, execute commands, or modify files.",
|
|
17
|
+
"Return only JSON matching the supplied schema. Do not include hidden reasoning.",
|
|
18
|
+
`Normalized request: ${JSON.stringify(request)}`,
|
|
19
|
+
].join("\n");
|
|
20
|
+
const args = ["-p", "--disallowedTools", "Edit,Write,Bash", "--output-format", "json", "--json-schema", JSON.stringify(generatorOutputSchema())];
|
|
21
|
+
if (options.model) args.push("--model", options.model);
|
|
22
|
+
args.push(prompt);
|
|
23
|
+
let raw;
|
|
24
|
+
try {
|
|
25
|
+
raw = execute(claudeBin, args, { cwd: options.cwd, timeout: options.timeoutMs ?? request.budgets.max_duration_ms });
|
|
26
|
+
} catch (error) {
|
|
27
|
+
const providerOutput = String(error?.stdout ?? "");
|
|
28
|
+
if (/not logged in|unauthorized|authentication/i.test(providerOutput)) {
|
|
29
|
+
const authentication = new Error("claude generator authentication failed");
|
|
30
|
+
authentication.code = "GENERATOR_AUTHENTICATION_FAILED";
|
|
31
|
+
throw authentication;
|
|
32
|
+
}
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
const response = JSON.parse(raw);
|
|
36
|
+
if (response.is_error) throw new Error(`claude generator returned an error: ${response.result ?? "unknown error"}`);
|
|
37
|
+
return validateGeneratorResponse(JSON.parse(response.result));
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
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 { generatorOutputSchema, validateGeneratorRequest, validateGeneratorResponse } from "../contracts.js";
|
|
6
|
+
import { guardedExecFileSync } from "../transports/read-only-guard.js";
|
|
7
|
+
|
|
8
|
+
export function codexGenerator(options = {}) {
|
|
9
|
+
const codexBin = options.codexBin ?? "codex";
|
|
10
|
+
const execute = options.execute ?? guardedExecFileSync;
|
|
11
|
+
let runtime = options.runtime ?? "codex version unavailable";
|
|
12
|
+
if (!options.runtime) try { runtime = execFileSync(codexBin, ["--version"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim(); } catch {}
|
|
13
|
+
return {
|
|
14
|
+
name: "codex",
|
|
15
|
+
metadata: { name: "codex", runtime, model: options.model ?? "configured-default" },
|
|
16
|
+
async generate(requestValue) {
|
|
17
|
+
const request = validateGeneratorRequest(requestValue);
|
|
18
|
+
const temp = mkdtempSync(join(tmpdir(), "agdf-dps-generator-"));
|
|
19
|
+
const schemaPath = join(temp, "schema.json");
|
|
20
|
+
const outputPath = join(temp, "output.json");
|
|
21
|
+
try {
|
|
22
|
+
writeFileSync(schemaPath, `${JSON.stringify(generatorOutputSchema(), null, 2)}\n`, "utf8");
|
|
23
|
+
const prompt = [
|
|
24
|
+
"Generate bounded advisory delivery-path proposals. Do not use tools, execute commands, or modify files.",
|
|
25
|
+
"Return only JSON matching the supplied schema. Do not include hidden reasoning.",
|
|
26
|
+
`Normalized request: ${JSON.stringify(request)}`,
|
|
27
|
+
].join("\n");
|
|
28
|
+
const args = ["exec", "--sandbox", "read-only", "--ephemeral", "--ignore-user-config", "--output-schema", schemaPath, "--output-last-message", outputPath, "--color", "never"];
|
|
29
|
+
if (options.model) args.push("--model", options.model);
|
|
30
|
+
args.push(prompt);
|
|
31
|
+
execute(codexBin, args, { cwd: options.cwd, timeout: options.timeoutMs ?? request.budgets.max_duration_ms });
|
|
32
|
+
return validateGeneratorResponse(JSON.parse(readFileSync(outputPath, "utf8")));
|
|
33
|
+
} finally {
|
|
34
|
+
rmSync(temp, { recursive: true, force: true });
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { validateGeneratorRequest, validateGeneratorResponse } from "../contracts.js";
|
|
2
|
+
|
|
3
|
+
export function fixtureGenerator(response, metadata = { name: "fixture-generator" }) {
|
|
4
|
+
return {
|
|
5
|
+
name: metadata.name,
|
|
6
|
+
metadata,
|
|
7
|
+
async generate(request) {
|
|
8
|
+
validateGeneratorRequest(request);
|
|
9
|
+
return validateGeneratorResponse(response);
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
}
|
|
@@ -25,6 +25,11 @@ export function persistSearchResult(targetDir, result) {
|
|
|
25
25
|
`- recommendation: ${recommendation}`,
|
|
26
26
|
`- stopping_reason: ${safe.stopping_reason}`,
|
|
27
27
|
`- evaluations: ${safe.budgets.evaluations}`,
|
|
28
|
+
`- generation_status: ${safe.generation?.status ?? "legacy_deterministic"}`,
|
|
29
|
+
`- generated_candidates: ${safe.generation ? `${safe.generation.accepted}/${safe.generation.returned}` : "0/0"}`,
|
|
30
|
+
`- generation_cost_units: ${safe.generation?.cost_units ?? 0}`,
|
|
31
|
+
`- generation_duration_ms: ${safe.generation?.duration_ms ?? 0}`,
|
|
32
|
+
...(safe.generation?.failure_code ? [`- generation_failure: ${safe.generation.failure_code}`] : []),
|
|
28
33
|
`- next_gate_action: ${safe.next_gate_action}`,
|
|
29
34
|
"",
|
|
30
35
|
"This summary contains model judgements, not measured facts. Canonical AGDF gate-check remains authoritative.",
|
|
@@ -1,11 +1,24 @@
|
|
|
1
1
|
import { CONTRACT_VERSION, SCORING_POLICY_VERSION, validateEvaluation, validateSearchInput } from "./contracts.js";
|
|
2
|
-
import { candidateLegality, candidatesFromInput } from "./candidate-policy.js";
|
|
2
|
+
import { candidateLegality, candidatesFromInput, generatedCandidatesFromResponse } from "./candidate-policy.js";
|
|
3
3
|
import { scoreEvaluation } from "./scoring.js";
|
|
4
|
+
import { generatorRequestFromInput } from "./state-adapter.js";
|
|
5
|
+
|
|
6
|
+
function generatorFailureCode(error) {
|
|
7
|
+
if (error?.code === "GENERATOR_MUTATION_DETECTED") return "generator_mutation_detected";
|
|
8
|
+
if (error?.code === "GENERATOR_TIMEOUT") return "generator_timeout";
|
|
9
|
+
if (error?.code === "GENERATOR_AUTHENTICATION_FAILED") return "generator_authentication_failed";
|
|
10
|
+
if (error?.code === "ENOENT") return "generator_unavailable";
|
|
11
|
+
if (/auth/i.test(error?.message ?? "")) return "generator_authentication_failed";
|
|
12
|
+
if (/context|disallowed/i.test(error?.message ?? "")) return "generator_context_rejected";
|
|
13
|
+
if (/budget/i.test(error?.message ?? "")) return "generator_budget_exceeded";
|
|
14
|
+
return "generator_schema_invalid";
|
|
15
|
+
}
|
|
4
16
|
|
|
5
17
|
export async function runDeliveryPathSearch(inputValue, evaluator, options = {}) {
|
|
6
18
|
const input = validateSearchInput(inputValue);
|
|
7
19
|
const startedAt = Date.now();
|
|
8
|
-
const
|
|
20
|
+
const baseline = [...(options.candidates ?? candidatesFromInput(input))];
|
|
21
|
+
const queue = [...baseline];
|
|
9
22
|
const rejected = [];
|
|
10
23
|
const evaluated = [];
|
|
11
24
|
let evaluations = 0;
|
|
@@ -13,6 +26,53 @@ export async function runDeliveryPathSearch(inputValue, evaluator, options = {})
|
|
|
13
26
|
let lastLeaderId = null;
|
|
14
27
|
let stableComparisons = 0;
|
|
15
28
|
let stoppingReason = "candidate_queue_exhausted";
|
|
29
|
+
const generation = {
|
|
30
|
+
status: input.generation?.enabled ? "failed" : "disabled",
|
|
31
|
+
adapter: options.generator?.metadata ?? null,
|
|
32
|
+
budgets: input.generation ?? null,
|
|
33
|
+
returned: 0,
|
|
34
|
+
accepted: 0,
|
|
35
|
+
rejected: [],
|
|
36
|
+
cost_units: 0,
|
|
37
|
+
duration_ms: 0,
|
|
38
|
+
failure_code: null,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
if (input.generation?.enabled) {
|
|
42
|
+
if (!options.generator) {
|
|
43
|
+
generation.failure_code = "generator_unavailable";
|
|
44
|
+
} else {
|
|
45
|
+
const generationStartedAt = Date.now();
|
|
46
|
+
try {
|
|
47
|
+
const response = await options.generator.generate(generatorRequestFromInput(input));
|
|
48
|
+
const elapsed = Date.now() - generationStartedAt;
|
|
49
|
+
generation.returned = response.proposals.length;
|
|
50
|
+
generation.cost_units = response.cost_units;
|
|
51
|
+
costUnits += response.cost_units;
|
|
52
|
+
if (elapsed > input.generation.max_duration_ms) {
|
|
53
|
+
const error = new Error("generator duration budget exceeded");
|
|
54
|
+
error.code = "GENERATOR_TIMEOUT";
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
if (response.cost_units > input.generation.max_cost_units || response.proposals.length > input.generation.max_proposals) {
|
|
58
|
+
const error = new Error("generator response exceeds configured budget");
|
|
59
|
+
error.code = "GENERATOR_BUDGET_EXCEEDED";
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
const filtered = generatedCandidatesFromResponse(response, input, baseline);
|
|
63
|
+
generation.accepted = filtered.accepted.length;
|
|
64
|
+
generation.rejected = filtered.rejected;
|
|
65
|
+
queue.push(...filtered.accepted.slice(0, input.generation.max_proposals));
|
|
66
|
+
generation.status = filtered.accepted.length === response.proposals.length ? "success" : "partial";
|
|
67
|
+
if (!filtered.accepted.length) generation.failure_code = "generator_no_diverse_proposals";
|
|
68
|
+
} catch (error) {
|
|
69
|
+
generation.failure_code = generatorFailureCode(error);
|
|
70
|
+
if (generation.failure_code === "generator_mutation_detected") throw error;
|
|
71
|
+
} finally {
|
|
72
|
+
generation.duration_ms = Date.now() - generationStartedAt;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
16
76
|
|
|
17
77
|
while (queue.length > 0) {
|
|
18
78
|
if (evaluations >= input.budgets.max_evaluations) { stoppingReason = "evaluation_budget_exhausted"; break; }
|
|
@@ -46,6 +106,7 @@ export async function runDeliveryPathSearch(inputValue, evaluator, options = {})
|
|
|
46
106
|
parent_id: candidate.id,
|
|
47
107
|
depth: (candidate.depth ?? 0) + 1,
|
|
48
108
|
action,
|
|
109
|
+
source: "expanded",
|
|
49
110
|
expected_evidence: [],
|
|
50
111
|
tests: [],
|
|
51
112
|
assumptions: evaluation.assumptions,
|
|
@@ -85,6 +146,7 @@ export async function runDeliveryPathSearch(inputValue, evaluator, options = {})
|
|
|
85
146
|
rejected,
|
|
86
147
|
enforcement: input.enforcement,
|
|
87
148
|
evaluator: evaluator.metadata ?? { name: evaluator.name ?? "unknown" },
|
|
149
|
+
generation,
|
|
88
150
|
budgets: { configured: input.budgets, evaluations, cost_units: costUnits, stable_comparisons: stableComparisons, duration_ms: Date.now() - startedAt },
|
|
89
151
|
stopping_reason: stoppingReason,
|
|
90
152
|
next_gate_action: "Run canonical AGDF gate-check; search does not grant permission.",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { CONTRACT_VERSION } from "./contracts.js";
|
|
3
|
+
import { CONTRACT_VERSION, GENERATOR_CONTRACT_VERSION, validateGeneratorRequest } from "./contracts.js";
|
|
4
4
|
|
|
5
5
|
function section(content, heading) {
|
|
6
6
|
return content.match(new RegExp(`## ${heading}\\r?\\n([\\s\\S]*?)(?=\\r?\\n## |$)`))?.[1] ?? "";
|
|
@@ -35,7 +35,7 @@ export function searchInputFromControl(targetDir, options = {}) {
|
|
|
35
35
|
const meta = Object.fromEntries([...content.matchAll(/^- ([a-z_]+):\s*(.+)$/gm)].map((match) => [match[1], match[2].replace(/^`|`$/g, "").trim()]));
|
|
36
36
|
const answers = tableAnswers(content);
|
|
37
37
|
const card = statusCard(content);
|
|
38
|
-
|
|
38
|
+
const result = {
|
|
39
39
|
contract_version: CONTRACT_VERSION,
|
|
40
40
|
scope_key: options.scopeKey ?? meta.run_id ?? "unknown-scope",
|
|
41
41
|
objective: section(content, "Objective").trim(),
|
|
@@ -55,4 +55,36 @@ export function searchInputFromControl(targetDir, options = {}) {
|
|
|
55
55
|
stability_window: options.stabilityWindow ?? 3,
|
|
56
56
|
},
|
|
57
57
|
};
|
|
58
|
+
if (options.generation?.enabled) result.generation = {
|
|
59
|
+
enabled: true,
|
|
60
|
+
max_calls: 1,
|
|
61
|
+
max_proposals: options.generation.maxProposals ?? 5,
|
|
62
|
+
max_duration_ms: options.generation.maxDurationMs ?? 30000,
|
|
63
|
+
max_cost_units: options.generation.maxCostUnits ?? 5,
|
|
64
|
+
};
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function generatorRequestFromInput(input) {
|
|
69
|
+
return validateGeneratorRequest({
|
|
70
|
+
contract_version: GENERATOR_CONTRACT_VERSION,
|
|
71
|
+
scope_key: input.scope_key,
|
|
72
|
+
objective: input.objective,
|
|
73
|
+
scope_summary: input.objective,
|
|
74
|
+
current_gate: input.current_gate,
|
|
75
|
+
allowed_actions: input.allowed_actions,
|
|
76
|
+
forbidden_actions: input.forbidden_actions,
|
|
77
|
+
artefact_refs: [],
|
|
78
|
+
evidence: input.evidence_refs ?? [],
|
|
79
|
+
missing_evidence: [],
|
|
80
|
+
risks: input.risks ?? [],
|
|
81
|
+
constraints: ["advisory only", "canonical gate-check remains authoritative"],
|
|
82
|
+
enforcement: input.enforcement,
|
|
83
|
+
budgets: {
|
|
84
|
+
max_calls: input.generation.max_calls,
|
|
85
|
+
max_proposals: input.generation.max_proposals,
|
|
86
|
+
max_duration_ms: input.generation.max_duration_ms,
|
|
87
|
+
max_cost_units: input.generation.max_cost_units,
|
|
88
|
+
},
|
|
89
|
+
});
|
|
58
90
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
export function repositoryState(cwd) {
|
|
4
|
+
try {
|
|
5
|
+
return execFileSync("git", ["status", "--porcelain=v1", "-z"], {
|
|
6
|
+
cwd,
|
|
7
|
+
encoding: "utf8",
|
|
8
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
9
|
+
});
|
|
10
|
+
} catch {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function guardedExecFileSync(file, args, options = {}) {
|
|
16
|
+
const before = repositoryState(options.cwd);
|
|
17
|
+
let output;
|
|
18
|
+
let failure;
|
|
19
|
+
try {
|
|
20
|
+
output = execFileSync(file, args, {
|
|
21
|
+
...options,
|
|
22
|
+
encoding: options.encoding ?? "utf8",
|
|
23
|
+
stdio: options.stdio ?? ["ignore", "pipe", "pipe"],
|
|
24
|
+
timeout: options.timeout ?? 30000,
|
|
25
|
+
killSignal: "SIGTERM",
|
|
26
|
+
});
|
|
27
|
+
} catch (error) {
|
|
28
|
+
failure = error;
|
|
29
|
+
}
|
|
30
|
+
const after = repositoryState(options.cwd);
|
|
31
|
+
if (before !== null && after !== before) {
|
|
32
|
+
const error = new Error("repository mutation detected during read-only run");
|
|
33
|
+
error.code = "GENERATOR_MUTATION_DETECTED";
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
if (failure?.code === "ETIMEDOUT" || failure?.signal === "SIGTERM") {
|
|
37
|
+
const timeout = new Error("read-only process timed out");
|
|
38
|
+
timeout.code = "GENERATOR_TIMEOUT";
|
|
39
|
+
throw timeout;
|
|
40
|
+
}
|
|
41
|
+
if (failure) throw failure;
|
|
42
|
+
return output;
|
|
43
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-agdf",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
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": [
|
|
@@ -23,8 +23,9 @@
|
|
|
23
23
|
"sync-package-assets": "node ./scripts/sync-package-assets.js",
|
|
24
24
|
"test:delivery-path-search": "node ./scripts/delivery-path-search-test.js",
|
|
25
25
|
"test:delivery-path-search-unit": "node ./scripts/delivery-path-search-unit-test.js",
|
|
26
|
+
"test:delivery-path-search-generator": "node ./scripts/delivery-path-search-generator-test.js",
|
|
26
27
|
"test:routing": "npm run sync-package-assets && node ./scripts/test-routing.js",
|
|
27
|
-
"smoke-test": "npm run sync-package-assets && npm run test:delivery-path-search && npm run test:delivery-path-search-unit && node ./scripts/smoke-test.js && node ./scripts/test-routing.js",
|
|
28
|
+
"smoke-test": "npm run sync-package-assets && 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",
|
|
28
29
|
"prepack": "npm run sync-package-assets"
|
|
29
30
|
},
|
|
30
31
|
"keywords": [
|