devlyn-cli 2.7.0 โ 2.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +9 -1
- package/CLAUDE.md +30 -11
- package/README.md +14 -2
- package/benchmark/auto-resolve/fixtures/F1-cli-trivial-flag/task.ko.txt +8 -0
- package/benchmark/auto-resolve/fixtures/retired/F34-cli-settle-inflight/NOTES.md +28 -0
- package/benchmark/auto-resolve/fixtures/retired/F34-cli-settle-inflight/RETIRED.md +15 -0
- package/benchmark/auto-resolve/fixtures/retired/F34-cli-settle-inflight/expected.json +57 -0
- package/benchmark/auto-resolve/fixtures/retired/F34-cli-settle-inflight/metadata.json +10 -0
- package/benchmark/auto-resolve/fixtures/retired/F34-cli-settle-inflight/setup.sh +50 -0
- package/benchmark/auto-resolve/fixtures/retired/F34-cli-settle-inflight/spec.md +59 -0
- package/benchmark/auto-resolve/fixtures/retired/F34-cli-settle-inflight/task.txt +9 -0
- package/benchmark/auto-resolve/fixtures/retired/F34-cli-settle-inflight/verifiers/ordered-settlement.js +88 -0
- package/benchmark/auto-resolve/fixtures/retired/F34-cli-settle-inflight/verifiers/reject-invalid.js +45 -0
- package/benchmark/auto-resolve/fixtures/retired/F35-cli-apply-journal/NOTES.md +30 -0
- package/benchmark/auto-resolve/fixtures/retired/F35-cli-apply-journal/RETIRED.md +16 -0
- package/benchmark/auto-resolve/fixtures/retired/F35-cli-apply-journal/expected.json +64 -0
- package/benchmark/auto-resolve/fixtures/retired/F35-cli-apply-journal/metadata.json +10 -0
- package/benchmark/auto-resolve/fixtures/retired/F35-cli-apply-journal/setup.sh +21 -0
- package/benchmark/auto-resolve/fixtures/retired/F35-cli-apply-journal/spec.md +58 -0
- package/benchmark/auto-resolve/fixtures/retired/F35-cli-apply-journal/task.txt +9 -0
- package/benchmark/auto-resolve/fixtures/retired/F35-cli-apply-journal/verifiers/dup-op.js +54 -0
- package/benchmark/auto-resolve/fixtures/retired/F35-cli-apply-journal/verifiers/priority-apply.js +82 -0
- package/benchmark/auto-resolve/fixtures/retired/F35-cli-apply-journal/verifiers/rollback.js +77 -0
- package/benchmark/auto-resolve/fixtures/retired/F36-cli-session-admission/NOTES.md +99 -0
- package/benchmark/auto-resolve/fixtures/retired/F36-cli-session-admission/RETIRED.md +18 -0
- package/benchmark/auto-resolve/fixtures/retired/F36-cli-session-admission/expected.json +64 -0
- package/benchmark/auto-resolve/fixtures/retired/F36-cli-session-admission/metadata.json +10 -0
- package/benchmark/auto-resolve/fixtures/retired/F36-cli-session-admission/setup.sh +4 -0
- package/benchmark/auto-resolve/fixtures/retired/F36-cli-session-admission/spec.md +61 -0
- package/benchmark/auto-resolve/fixtures/retired/F36-cli-session-admission/task.txt +9 -0
- package/benchmark/auto-resolve/fixtures/retired/F36-cli-session-admission/verifiers/correctness-small.js +135 -0
- package/benchmark/auto-resolve/fixtures/retired/F36-cli-session-admission/verifiers/scale.js +100 -0
- package/benchmark/auto-resolve/fixtures/retired/F36-cli-session-admission/verifiers/validation.js +75 -0
- package/benchmark/auto-resolve/fixtures/retired/F37-cli-rule-lookup/NOTES.md +93 -0
- package/benchmark/auto-resolve/fixtures/retired/F37-cli-rule-lookup/RETIRED.md +16 -0
- package/benchmark/auto-resolve/fixtures/retired/F37-cli-rule-lookup/expected.json +65 -0
- package/benchmark/auto-resolve/fixtures/retired/F37-cli-rule-lookup/metadata.json +10 -0
- package/benchmark/auto-resolve/fixtures/retired/F37-cli-rule-lookup/setup.sh +18 -0
- package/benchmark/auto-resolve/fixtures/retired/F37-cli-rule-lookup/spec.md +66 -0
- package/benchmark/auto-resolve/fixtures/retired/F37-cli-rule-lookup/task.txt +9 -0
- package/benchmark/auto-resolve/fixtures/retired/F37-cli-rule-lookup/verifiers/correctness-small.js +84 -0
- package/benchmark/auto-resolve/fixtures/retired/F37-cli-rule-lookup/verifiers/scale.js +184 -0
- package/benchmark/auto-resolve/fixtures/retired/F37-cli-rule-lookup/verifiers/validation.js +74 -0
- package/benchmark/auto-resolve/scripts/pair-rejected-fixtures.sh +12 -0
- package/benchmark/auto-resolve/scripts/run-fixture.sh +39 -1
- package/benchmark/auto-resolve/scripts/test-lint-fixtures.sh +1 -1
- package/bin/devlyn.js +42 -1
- package/config/skills/_shared/adapters/README.md +7 -0
- package/config/skills/_shared/adapters/claude.md +47 -0
- package/config/skills/_shared/adapters/omp.md +17 -0
- package/config/skills/_shared/archive_run.py +8 -2
- package/config/skills/_shared/codex-config.md +22 -6
- package/config/skills/_shared/engine-doctor.sh +107 -0
- package/config/skills/_shared/engine-preflight.md +18 -11
- package/config/skills/_shared/expected.schema.json +26 -0
- package/config/skills/_shared/runtime-principles.md +1 -1
- package/config/skills/_shared/spec-verify-check.py +773 -486
- package/config/skills/_shared/state-phase-write.py +374 -0
- package/config/skills/_shared/verify-merge-findings.py +177 -61
- package/config/skills/devlyn:engines/SKILL.md +47 -0
- package/config/skills/devlyn:ideate/SKILL.md +22 -3
- package/config/skills/devlyn:ideate/references/elicitation.md +2 -2
- package/config/skills/devlyn:ideate/references/from-spec-mode.md +2 -2
- package/config/skills/devlyn:ideate/references/project-mode.md +1 -1
- package/config/skills/devlyn:ideate/references/spec-template.md +15 -4
- package/config/skills/devlyn:queue/SKILL.md +26 -0
- package/config/skills/devlyn:resolve/SKILL.md +54 -24
- package/config/skills/devlyn:resolve/references/free-form-mode.md +1 -1
- package/config/skills/devlyn:resolve/references/phases/build-gate.md +2 -2
- package/config/skills/devlyn:resolve/references/phases/cleanup.md +1 -1
- package/config/skills/devlyn:resolve/references/phases/implement.md +1 -1
- package/config/skills/devlyn:resolve/references/phases/plan.md +3 -2
- package/config/skills/devlyn:resolve/references/phases/probe-derive.md +8 -0
- package/config/skills/devlyn:resolve/references/phases/verify.md +18 -9
- package/config/skills/devlyn:resolve/references/state-schema.md +10 -6
- package/optional-skills/devlyn:pencil-pull/SKILL.md +5 -0
- package/optional-skills/devlyn:pencil-push/SKILL.md +5 -0
- package/package.json +1 -1
- package/scripts/lint-skills.sh +48 -41
package/benchmark/auto-resolve/fixtures/retired/F37-cli-rule-lookup/verifiers/correctness-small.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// F37 hidden verifier โ one bundled scenario exercising: (1) different
|
|
3
|
+
// events in the same category resolving to different revisions by their
|
|
4
|
+
// own timestamp, (2) the inclusive effectiveAt<=timestamp boundary (an
|
|
5
|
+
// exact match is eligible), (3) the same-effectiveAt tie-break (greatest
|
|
6
|
+
// id wins), (4) unknown_category vs no_effective_rule are distinct and
|
|
7
|
+
// correctly assigned. Resets the seed so this verifier is order-independent
|
|
8
|
+
// from other verifier commands (F35 pattern).
|
|
9
|
+
//
|
|
10
|
+
// Structural equality: object key order is not part of the visible
|
|
11
|
+
// contract; array order is.
|
|
12
|
+
function deepEqual(a, b) {
|
|
13
|
+
if (a === b) return true;
|
|
14
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
15
|
+
return a.length === b.length && a.every((v, i) => deepEqual(v, b[i]));
|
|
16
|
+
}
|
|
17
|
+
if (a && b && typeof a === 'object' && typeof b === 'object' && !Array.isArray(a) && !Array.isArray(b)) {
|
|
18
|
+
const ka = Object.keys(a).sort();
|
|
19
|
+
const kb = Object.keys(b).sort();
|
|
20
|
+
return deepEqual(ka, kb) && ka.every((k) => deepEqual(a[k], b[k]));
|
|
21
|
+
}
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const fs = require('node:fs');
|
|
26
|
+
const os = require('node:os');
|
|
27
|
+
const path = require('node:path');
|
|
28
|
+
const { spawnSync } = require('node:child_process');
|
|
29
|
+
|
|
30
|
+
const work = process.env.BENCH_WORKDIR;
|
|
31
|
+
const revPath = path.join(work, 'data', 'rule-revisions.json');
|
|
32
|
+
const cli = path.join(work, 'bin', 'cli.js');
|
|
33
|
+
|
|
34
|
+
const revisions = [
|
|
35
|
+
{ id: 'rev-a2', categoryId: 'cat-A', effectiveAt: 200, discountPct: 20, minPrice: 10 },
|
|
36
|
+
{ id: 'rev-a9', categoryId: 'cat-A', effectiveAt: 300, discountPct: 90, minPrice: 10 },
|
|
37
|
+
{ id: 'rev-a1', categoryId: 'cat-A', effectiveAt: 100, discountPct: 10, minPrice: 10 },
|
|
38
|
+
{ id: 'rev-b1', categoryId: 'cat-B', effectiveAt: 50, discountPct: 0, minPrice: 5 },
|
|
39
|
+
{ id: 'rev-a3', categoryId: 'cat-A', effectiveAt: 300, discountPct: 30, minPrice: 10 },
|
|
40
|
+
];
|
|
41
|
+
fs.writeFileSync(revPath, JSON.stringify(revisions, null, 2) + '\n');
|
|
42
|
+
|
|
43
|
+
const inputPath = path.join(os.tmpdir(), `price-events-small-${process.pid}.json`);
|
|
44
|
+
fs.writeFileSync(inputPath, JSON.stringify({
|
|
45
|
+
events: [
|
|
46
|
+
{ id: 'e1', categoryId: 'cat-A', timestamp: 150, basePrice: 100 },
|
|
47
|
+
{ id: 'e2', categoryId: 'cat-A', timestamp: 250, basePrice: 100 },
|
|
48
|
+
{ id: 'e3', categoryId: 'cat-A', timestamp: 200, basePrice: 50 },
|
|
49
|
+
{ id: 'e4', categoryId: 'cat-A', timestamp: 350, basePrice: 100 },
|
|
50
|
+
{ id: 'e5', categoryId: 'cat-B', timestamp: 10, basePrice: 100 },
|
|
51
|
+
{ id: 'e6', categoryId: 'cat-C', timestamp: 1000, basePrice: 100 },
|
|
52
|
+
],
|
|
53
|
+
}));
|
|
54
|
+
|
|
55
|
+
const result = spawnSync('node', [cli, 'price-events', '--input', inputPath], {
|
|
56
|
+
cwd: work,
|
|
57
|
+
encoding: 'utf8',
|
|
58
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
let out = null;
|
|
62
|
+
try {
|
|
63
|
+
out = JSON.parse(result.stdout);
|
|
64
|
+
} catch {
|
|
65
|
+
out = null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const expected = {
|
|
69
|
+
priced: [
|
|
70
|
+
{ id: 'e1', ruleId: 'rev-a1', price: 90 },
|
|
71
|
+
{ id: 'e2', ruleId: 'rev-a2', price: 80 },
|
|
72
|
+
{ id: 'e3', ruleId: 'rev-a2', price: 40 },
|
|
73
|
+
{ id: 'e4', ruleId: 'rev-a9', price: 10 },
|
|
74
|
+
],
|
|
75
|
+
rejected: [
|
|
76
|
+
{ id: 'e5', reason: 'no_effective_rule' },
|
|
77
|
+
{ id: 'e6', reason: 'unknown_category' },
|
|
78
|
+
],
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const ok = result.status === 0 && result.stderr === '' && deepEqual(out, expected);
|
|
82
|
+
|
|
83
|
+
console.log(JSON.stringify({ ok, status: result.status, stderr: result.stderr, out, expected }));
|
|
84
|
+
process.exit(ok ? 0 : 1);
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// F37 hidden verifier โ the performance discriminator. 60,000 rule
|
|
3
|
+
// revisions spread over 4 categories (15,000 revisions/category) and
|
|
4
|
+
// 200,000 events. Every event resolves successfully (no rejections),
|
|
5
|
+
// which keeps the expected output small and the correctness check a
|
|
6
|
+
// direct array comparison; the discriminator is entirely in how the
|
|
7
|
+
// lookup is done, not in exercising the reject paths (those are covered
|
|
8
|
+
// by correctness-small.js).
|
|
9
|
+
//
|
|
10
|
+
// A per-event linear scan of a category's revisions costs on the order of
|
|
11
|
+
// events * revisions-per-category =~ 200,000 * 15,000 = 3e9 comparisons โ
|
|
12
|
+
// tens of seconds or more in Node. Grouping by category (near-free) and
|
|
13
|
+
// then binary-searching a sorted per-category list is a few million
|
|
14
|
+
// log-steps โ trivially fast. The reference below independently
|
|
15
|
+
// implements the efficient shape (not shared code with the CLI) so it can
|
|
16
|
+
// also serve as ground truth without becoming a second bottleneck. The
|
|
17
|
+
// 15-second hard kill sits far above the efficient solution's expected
|
|
18
|
+
// running time and correspondingly far below any realistic
|
|
19
|
+
// linear-scan running time at these parameters.
|
|
20
|
+
//
|
|
21
|
+
// Revisions are written to `data/rule-revisions.json` in a shuffled
|
|
22
|
+
// (non-category-grouped, non-time-sorted) order via a deterministic
|
|
23
|
+
// permutation, matching the visible "not sorted by category or by time"
|
|
24
|
+
// spec fact and closing a "trust file order" shortcut.
|
|
25
|
+
|
|
26
|
+
const fs = require('node:fs');
|
|
27
|
+
const os = require('node:os');
|
|
28
|
+
const path = require('node:path');
|
|
29
|
+
const { spawnSync } = require('node:child_process');
|
|
30
|
+
|
|
31
|
+
const work = process.env.BENCH_WORKDIR;
|
|
32
|
+
const revPath = path.join(work, 'data', 'rule-revisions.json');
|
|
33
|
+
const cli = path.join(work, 'bin', 'cli.js');
|
|
34
|
+
|
|
35
|
+
const CATS = 4;
|
|
36
|
+
const REVS_PER_CAT = 15000;
|
|
37
|
+
const TOTAL_REVS = CATS * REVS_PER_CAT;
|
|
38
|
+
const EVENTS = 200000;
|
|
39
|
+
const REV_PERMUTE_MULTIPLIER = 39989; // coprime with 60000 (2^5 * 3 * 5^4)
|
|
40
|
+
|
|
41
|
+
const revisionsInOrder = new Array(TOTAL_REVS);
|
|
42
|
+
let k = 0;
|
|
43
|
+
for (let c = 0; c < CATS; c++) {
|
|
44
|
+
for (let j = 0; j < REVS_PER_CAT; j++) {
|
|
45
|
+
revisionsInOrder[k] = {
|
|
46
|
+
id: `rev-${c}-${j}`,
|
|
47
|
+
categoryId: `cat-${c}`,
|
|
48
|
+
effectiveAt: j * 10,
|
|
49
|
+
discountPct: (j * 7) % 50,
|
|
50
|
+
minPrice: 100 + ((j * 13) % 900),
|
|
51
|
+
};
|
|
52
|
+
k += 1;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const shuffled = new Array(TOTAL_REVS);
|
|
57
|
+
for (let i = 0; i < TOTAL_REVS; i++) {
|
|
58
|
+
const pos = (i * REV_PERMUTE_MULTIPLIER) % TOTAL_REVS;
|
|
59
|
+
shuffled[pos] = revisionsInOrder[i];
|
|
60
|
+
}
|
|
61
|
+
fs.writeFileSync(revPath, JSON.stringify(shuffled));
|
|
62
|
+
|
|
63
|
+
const events = new Array(EVENTS);
|
|
64
|
+
for (let i = 0; i < EVENTS; i++) {
|
|
65
|
+
const c = i % CATS;
|
|
66
|
+
const base = (i * 131) % REVS_PER_CAT;
|
|
67
|
+
events[i] = {
|
|
68
|
+
id: `ev-${i}`,
|
|
69
|
+
categoryId: `cat-${c}`,
|
|
70
|
+
timestamp: base * 10 + 5,
|
|
71
|
+
basePrice: 1000 + (i % 500),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const inputPath = path.join(os.tmpdir(), `price-events-scale-${process.pid}.json`);
|
|
76
|
+
fs.writeFileSync(inputPath, JSON.stringify({ events }));
|
|
77
|
+
|
|
78
|
+
// Independent reference: group by category, sort each group by
|
|
79
|
+
// (effectiveAt asc, id asc), binary-search the rightmost entry with
|
|
80
|
+
// effectiveAt <= timestamp.
|
|
81
|
+
const groups = new Map();
|
|
82
|
+
for (const rev of revisionsInOrder) {
|
|
83
|
+
if (!groups.has(rev.categoryId)) groups.set(rev.categoryId, []);
|
|
84
|
+
groups.get(rev.categoryId).push(rev);
|
|
85
|
+
}
|
|
86
|
+
for (const list of groups.values()) {
|
|
87
|
+
list.sort((a, b) => a.effectiveAt - b.effectiveAt || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function findRuleIndex(sorted, timestamp) {
|
|
91
|
+
let lo = 0;
|
|
92
|
+
let hi = sorted.length - 1;
|
|
93
|
+
let ans = -1;
|
|
94
|
+
while (lo <= hi) {
|
|
95
|
+
const mid = (lo + hi) >> 1;
|
|
96
|
+
if (sorted[mid].effectiveAt <= timestamp) {
|
|
97
|
+
ans = mid;
|
|
98
|
+
lo = mid + 1;
|
|
99
|
+
} else {
|
|
100
|
+
hi = mid - 1;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return ans;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const expectedPriced = new Array(EVENTS);
|
|
107
|
+
let expectedRejectedCount = 0;
|
|
108
|
+
for (let i = 0; i < EVENTS; i++) {
|
|
109
|
+
const ev = events[i];
|
|
110
|
+
const group = groups.get(ev.categoryId);
|
|
111
|
+
if (!group) {
|
|
112
|
+
expectedRejectedCount += 1;
|
|
113
|
+
expectedPriced[i] = null;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
const idx = findRuleIndex(group, ev.timestamp);
|
|
117
|
+
if (idx < 0) {
|
|
118
|
+
expectedRejectedCount += 1;
|
|
119
|
+
expectedPriced[i] = null;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
const rule = group[idx];
|
|
123
|
+
const price = Math.max(rule.minPrice, Math.round((ev.basePrice * (100 - rule.discountPct)) / 100));
|
|
124
|
+
expectedPriced[i] = { id: ev.id, ruleId: rule.id, price };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const start = Date.now();
|
|
128
|
+
const result = spawnSync('node', [cli, 'price-events', '--input', inputPath], {
|
|
129
|
+
cwd: work,
|
|
130
|
+
encoding: 'utf8',
|
|
131
|
+
timeout: 15000,
|
|
132
|
+
killSignal: 'SIGKILL',
|
|
133
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
134
|
+
});
|
|
135
|
+
const elapsedMs = Date.now() - start;
|
|
136
|
+
|
|
137
|
+
const timedOut = result.status === null && result.signal !== null;
|
|
138
|
+
|
|
139
|
+
let out = null;
|
|
140
|
+
if (!timedOut) {
|
|
141
|
+
try {
|
|
142
|
+
out = JSON.parse(result.stdout);
|
|
143
|
+
} catch {
|
|
144
|
+
out = null;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
let correct = false;
|
|
149
|
+
let mismatch = null;
|
|
150
|
+
if (out && Array.isArray(out.priced) && Array.isArray(out.rejected)) {
|
|
151
|
+
correct =
|
|
152
|
+
out.rejected.length === expectedRejectedCount &&
|
|
153
|
+
out.priced.length === EVENTS - expectedRejectedCount &&
|
|
154
|
+
out.priced.every((row, i) => {
|
|
155
|
+
const expected = expectedPriced[i];
|
|
156
|
+
return expected && row.id === expected.id && row.ruleId === expected.ruleId && row.price === expected.price;
|
|
157
|
+
});
|
|
158
|
+
if (!correct) {
|
|
159
|
+
const firstMismatchIndex = out.priced.findIndex((row, i) => {
|
|
160
|
+
const expected = expectedPriced[i];
|
|
161
|
+
return !expected || row.id !== expected.id || row.ruleId !== expected.ruleId || row.price !== expected.price;
|
|
162
|
+
});
|
|
163
|
+
mismatch = {
|
|
164
|
+
pricedLength: out.priced.length,
|
|
165
|
+
rejectedLength: out.rejected.length,
|
|
166
|
+
expectedRejectedCount,
|
|
167
|
+
firstMismatchIndex,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const ok = !timedOut && result.status === 0 && result.stderr === '' && correct;
|
|
173
|
+
|
|
174
|
+
console.log(JSON.stringify({
|
|
175
|
+
ok,
|
|
176
|
+
timedOut,
|
|
177
|
+
elapsedMs,
|
|
178
|
+
status: result.status,
|
|
179
|
+
signal: result.signal,
|
|
180
|
+
stderrLength: result.stderr ? result.stderr.length : 0,
|
|
181
|
+
correct,
|
|
182
|
+
mismatch,
|
|
183
|
+
}));
|
|
184
|
+
process.exit(ok ? 0 : 1);
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// F37 hidden verifier โ validation happens before any pricing decision:
|
|
3
|
+
// malformed event fields use `invalid_input`, duplicate event ids use the
|
|
4
|
+
// distinct `duplicate_event_id` shape, and nothing is priced or printed to
|
|
5
|
+
// stdout in either case. Resets the seed so this verifier is
|
|
6
|
+
// order-independent from other verifier commands.
|
|
7
|
+
|
|
8
|
+
const fs = require('node:fs');
|
|
9
|
+
const os = require('node:os');
|
|
10
|
+
const path = require('node:path');
|
|
11
|
+
const { spawnSync } = require('node:child_process');
|
|
12
|
+
|
|
13
|
+
const work = process.env.BENCH_WORKDIR;
|
|
14
|
+
const revPath = path.join(work, 'data', 'rule-revisions.json');
|
|
15
|
+
const cli = path.join(work, 'bin', 'cli.js');
|
|
16
|
+
|
|
17
|
+
const revisions = [
|
|
18
|
+
{ id: 'rev-a1', categoryId: 'cat-A', effectiveAt: 100, discountPct: 10, minPrice: 10 },
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
let callCount = 0;
|
|
22
|
+
function runPriceEvents(payload) {
|
|
23
|
+
fs.writeFileSync(revPath, JSON.stringify(revisions, null, 2) + '\n');
|
|
24
|
+
callCount += 1;
|
|
25
|
+
const inputPath = path.join(os.tmpdir(), `price-events-validate-${process.pid}-${callCount}.json`);
|
|
26
|
+
fs.writeFileSync(inputPath, JSON.stringify(payload));
|
|
27
|
+
const result = spawnSync('node', [cli, 'price-events', '--input', inputPath], {
|
|
28
|
+
cwd: work,
|
|
29
|
+
encoding: 'utf8',
|
|
30
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
31
|
+
});
|
|
32
|
+
let err = null;
|
|
33
|
+
try {
|
|
34
|
+
err = JSON.parse(result.stderr);
|
|
35
|
+
} catch {
|
|
36
|
+
err = null;
|
|
37
|
+
}
|
|
38
|
+
return { result, err };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const failures = [];
|
|
42
|
+
|
|
43
|
+
function expectInvalidInput(label, payload) {
|
|
44
|
+
const { result, err } = runPriceEvents(payload);
|
|
45
|
+
const ok = result.status === 2 && result.stdout === '' && err !== null && err.error === 'invalid_input' && typeof err.reason === 'string';
|
|
46
|
+
if (!ok) {
|
|
47
|
+
failures.push({ test: label, status: result.status, stdout: result.stdout, stderr: result.stderr, err });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
expectInvalidInput('events-not-array', { events: 'nope' });
|
|
52
|
+
expectInvalidInput('missing-id', { events: [{ categoryId: 'cat-A', timestamp: 100, basePrice: 10 }] });
|
|
53
|
+
expectInvalidInput('non-integer-timestamp', { events: [{ id: 'e1', categoryId: 'cat-A', timestamp: 1.5, basePrice: 10 }] });
|
|
54
|
+
expectInvalidInput('negative-base-price', { events: [{ id: 'e1', categoryId: 'cat-A', timestamp: 100, basePrice: -1 }] });
|
|
55
|
+
expectInvalidInput('non-string-category', { events: [{ id: 'e1', categoryId: 42, timestamp: 100, basePrice: 10 }] });
|
|
56
|
+
|
|
57
|
+
// Duplicate event id โ distinct error shape.
|
|
58
|
+
{
|
|
59
|
+
const payload = {
|
|
60
|
+
events: [
|
|
61
|
+
{ id: 'dup', categoryId: 'cat-A', timestamp: 100, basePrice: 10 },
|
|
62
|
+
{ id: 'dup', categoryId: 'cat-A', timestamp: 200, basePrice: 20 },
|
|
63
|
+
],
|
|
64
|
+
};
|
|
65
|
+
const { result, err } = runPriceEvents(payload);
|
|
66
|
+
const ok = result.status === 2 && result.stdout === '' && err !== null && err.error === 'duplicate_event_id' && err.id === 'dup';
|
|
67
|
+
if (!ok) {
|
|
68
|
+
failures.push({ test: 'duplicate-event-id', status: result.status, stdout: result.stdout, stderr: result.stderr, err });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const ok = failures.length === 0;
|
|
73
|
+
console.log(JSON.stringify({ ok, failures }));
|
|
74
|
+
process.exit(ok ? 0 : 1);
|
|
@@ -69,6 +69,18 @@ rejected_pair_fixture_reason() {
|
|
|
69
69
|
F32-*|F32)
|
|
70
70
|
echo "bare 33 / solo_claude 98 in 20260512-f32-subscription-renewal-headroom"
|
|
71
71
|
;;
|
|
72
|
+
F34-*|F34)
|
|
73
|
+
echo "bare 92 / solo_claude 88 in iter-0039-headroom-f34-f35"
|
|
74
|
+
;;
|
|
75
|
+
F35-*|F35)
|
|
76
|
+
echo "bare 50 / solo_claude 97 in iter-0039-headroom-f34-f35"
|
|
77
|
+
;;
|
|
78
|
+
F36-*|F36)
|
|
79
|
+
echo "bare 50 / solo_claude 96 with solo timeout in iter-0041-headroom-f36-f37"
|
|
80
|
+
;;
|
|
81
|
+
F37-*|F37)
|
|
82
|
+
echo "bare 75 / solo_claude 96 with solo invoke failure in iter-0041-headroom-f36-f37"
|
|
83
|
+
;;
|
|
72
84
|
S2-*|S2)
|
|
73
85
|
echo "bare 33 / solo_claude 99 with solo timeout in 20260513-s2-inventory-headroom"
|
|
74
86
|
;;
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
set -euo pipefail
|
|
16
16
|
|
|
17
17
|
usage() {
|
|
18
|
-
echo "usage: $0 --fixture <FID> --arm <variant|solo_claude|bare|l2_gated|l2_risk_probes|l2_forced> --run-id <ID> [--resolve-skill new] [--dry-run]"
|
|
18
|
+
echo "usage: $0 --fixture <FID> --arm <variant|solo_claude|bare|l2_gated|l2_risk_probes|l2_forced> --run-id <ID> [--resolve-skill new] [--engines-config '<json>'] [--dry-run]"
|
|
19
19
|
exit 1
|
|
20
20
|
}
|
|
21
21
|
|
|
@@ -47,17 +47,36 @@ kill_worktree_processes() {
|
|
|
47
47
|
|
|
48
48
|
FIXTURE=""; ARM=""; RUN_ID=""; DRY_RUN=0
|
|
49
49
|
RESOLVE_SKILL="new"
|
|
50
|
+
ENGINES_CONFIG=""
|
|
50
51
|
while [ $# -gt 0 ]; do
|
|
51
52
|
case "$1" in
|
|
52
53
|
--fixture) require_value "$1" "${2:-}"; FIXTURE="$2"; shift 2;;
|
|
53
54
|
--arm) require_value "$1" "${2:-}"; ARM="$2"; shift 2;;
|
|
54
55
|
--run-id) require_value "$1" "${2:-}"; RUN_ID="$2"; shift 2;;
|
|
55
56
|
--resolve-skill) require_value "$1" "${2:-}"; RESOLVE_SKILL="$2"; shift 2;;
|
|
57
|
+
--engines-config) require_value "$1" "${2:-}"; ENGINES_CONFIG="$2"; shift 2;;
|
|
56
58
|
--dry-run) DRY_RUN=1; shift;;
|
|
57
59
|
*) usage;;
|
|
58
60
|
esac
|
|
59
61
|
done
|
|
60
62
|
[ -n "$FIXTURE" ] && [ -n "$ARM" ] && [ -n "$RUN_ID" ] || usage
|
|
63
|
+
|
|
64
|
+
# iter-0038: --engines-config stages a machine-config role pin
|
|
65
|
+
# (.devlyn/engines.json) into the arm worktree so engine COMBINATIONS are
|
|
66
|
+
# runnable as variant-class arms without new hardcoded arm names. Existing
|
|
67
|
+
# arm launch commands are untouched when the flag is absent. Fail fast on
|
|
68
|
+
# malformed JSON, and refuse bare (no skills โ the config would be dead
|
|
69
|
+
# weight that silently mis-labels the arm as configured).
|
|
70
|
+
if [ -n "$ENGINES_CONFIG" ]; then
|
|
71
|
+
if [ "$ARM" = "bare" ]; then
|
|
72
|
+
echo "--engines-config is invalid for the bare arm (no skills consume it)" >&2
|
|
73
|
+
exit 1
|
|
74
|
+
fi
|
|
75
|
+
if ! printf '%s' "$ENGINES_CONFIG" | python3 -c 'import json,sys; json.load(sys.stdin)' 2>/dev/null; then
|
|
76
|
+
echo "--engines-config is not valid JSON" >&2
|
|
77
|
+
exit 1
|
|
78
|
+
fi
|
|
79
|
+
fi
|
|
61
80
|
# iter-0019/0037: 3 smoke arms โ variant (L2: Claude orchestrator + risk-probes pair path),
|
|
62
81
|
# solo_claude (L1: Claude orchestrator, codex blocked by shim+wrapper enforcement),
|
|
63
82
|
# bare (L0: direct claude -p, no skill, no codex).
|
|
@@ -164,6 +183,14 @@ if [ "$ARM" = "variant" ] || [ "$ARM" = "solo_claude" ] \
|
|
|
164
183
|
if [ -f "$REPO_ROOT/CLAUDE.md" ]; then
|
|
165
184
|
cp "$REPO_ROOT/CLAUDE.md" "$WORK_DIR/CLAUDE.md"
|
|
166
185
|
fi
|
|
186
|
+
# iter-0038: stage the engine role pin (validated above) pre-baseline.
|
|
187
|
+
# test-repo/.gitignore covers .devlyn, so it never enters scaffold commits
|
|
188
|
+
# or arm diffs. A verbatim copy lands in RESULT_DIR as run evidence.
|
|
189
|
+
if [ -n "$ENGINES_CONFIG" ]; then
|
|
190
|
+
mkdir -p "$WORK_DIR/.devlyn"
|
|
191
|
+
printf '%s\n' "$ENGINES_CONFIG" > "$WORK_DIR/.devlyn/engines.json"
|
|
192
|
+
printf '%s\n' "$ENGINES_CONFIG" > "$RESULT_DIR/engines-config.json"
|
|
193
|
+
fi
|
|
167
194
|
# Stage the codex PATH shim. Required for both variant (route to monitored
|
|
168
195
|
# wrapper) and solo_claude (CODEX_BLOCKED enforcement at PATH layer).
|
|
169
196
|
if command -v codex >/dev/null 2>&1; then
|
|
@@ -405,6 +432,17 @@ EOF
|
|
|
405
432
|
# OLD branch and this is now the only non-F9 path.
|
|
406
433
|
mkdir -p "$WORK_DIR/docs/roadmap/phase-1"
|
|
407
434
|
cp "$SPEC" "$WORK_DIR/docs/roadmap/phase-1/$FIXTURE.md"
|
|
435
|
+
# iter-0049: fixtures keep spec.md prose-only (expected.json is the real
|
|
436
|
+
# carrier, validated separately by lint-fixtures.sh) but spec-verify-check.py
|
|
437
|
+
# now locates the visible ## Verification prose via a <!-- devlyn:verification -->
|
|
438
|
+
# sentinel rather than the English header text. Inject it into the WORK_DIR
|
|
439
|
+
# copy only, so risk-probe derived_from substring matching still finds the
|
|
440
|
+
# section during a run; the source fixture repo stays untouched.
|
|
441
|
+
if ! grep -q '<!-- devlyn:verification -->' "$WORK_DIR/docs/roadmap/phase-1/$FIXTURE.md"; then
|
|
442
|
+
sed -i.bak 's/^## Verification\b/<!-- devlyn:verification -->\n## Verification/' \
|
|
443
|
+
"$WORK_DIR/docs/roadmap/phase-1/$FIXTURE.md"
|
|
444
|
+
rm -f "$WORK_DIR/docs/roadmap/phase-1/$FIXTURE.md.bak"
|
|
445
|
+
fi
|
|
408
446
|
cat > "$PROMPT_FILE" <<EOF
|
|
409
447
|
Use the \`/devlyn:resolve --spec docs/roadmap/phase-1/$FIXTURE.md ${ENGINE_CLAUSE}\` skill to implement the spec. ${ENGINE_PROMPT_HINT}
|
|
410
448
|
|
|
@@ -387,7 +387,7 @@ status=$?
|
|
|
387
387
|
set -e
|
|
388
388
|
[ "$status" -ne 0 ]
|
|
389
389
|
grep -Fq 'spec-verify-check --check-expected failed' "$TMP/spec-verify-check-expected-fail.out"
|
|
390
|
-
grep -Fq 'unless
|
|
390
|
+
grep -Fq 'unless spec.expected.json declares "pure_design": true' "$TMP/spec-verify-check-expected-fail.out"
|
|
391
391
|
|
|
392
392
|
write_fixture "Add idempotency handling for duplicate requests."
|
|
393
393
|
|
package/bin/devlyn.js
CHANGED
|
@@ -14,7 +14,8 @@ const PKG = require('../package.json');
|
|
|
14
14
|
// directory. Single source of truth so codex/omp/pi stay in lockstep โ adding a
|
|
15
15
|
// skill here installs it everywhere. Standards skills (code-*, root-cause-*,
|
|
16
16
|
// ui-*) stay Claude-core-only, matching the pre-existing Codex bundle.
|
|
17
|
-
const DEVLYN_CORE_SKILLS = ['devlyn:resolve', 'devlyn:ideate', 'devlyn:design-ui', '_shared'];
|
|
17
|
+
const DEVLYN_CORE_SKILLS = ['devlyn:resolve', 'devlyn:ideate', 'devlyn:design-ui', 'devlyn:engines', 'devlyn:queue', '_shared'];
|
|
18
|
+
const DEVLYN_SKILL_DIR_STAMP = '__DEVLYN_SKILL_DIR__';
|
|
18
19
|
|
|
19
20
|
// Cross-agent shared skills directory read by BOTH oh-my-pi and Pi. Verified
|
|
20
21
|
// from the omp binary's skill-provider strings ("skills from .agents/skills โ
|
|
@@ -340,6 +341,40 @@ function copyRecursive(src, dest, baseDir) {
|
|
|
340
341
|
}
|
|
341
342
|
}
|
|
342
343
|
|
|
344
|
+
function shellDoubleQuoteDefault(value) {
|
|
345
|
+
if (value.includes('\n')) {
|
|
346
|
+
throw new Error(`Cannot stamp skill path with newline: ${value}`);
|
|
347
|
+
}
|
|
348
|
+
return value.replace(/[\\$"`}]/g, '\\$&');
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function stampInstalledSkillDir(rootDir, skillDir) {
|
|
352
|
+
// Stamp ONLY the assignment-default occurrence. The sentinel comparison
|
|
353
|
+
// literal on the guard line must stay intact: stamping it makes the guard
|
|
354
|
+
// compare the stamped default to itself and false-positive
|
|
355
|
+
// BLOCKED:shared-dir-unresolved on every codex/omp run that executes the
|
|
356
|
+
// block (CLAUDE_SKILL_DIR unset) โ iter-0040 R3 latent finding.
|
|
357
|
+
const assignmentStamp = '${CLAUDE_SKILL_DIR:-' + DEVLYN_SKILL_DIR_STAMP + '}';
|
|
358
|
+
const stampedAssignment = '${CLAUDE_SKILL_DIR:-' + shellDoubleQuoteDefault(skillDir) + '}';
|
|
359
|
+
|
|
360
|
+
function visit(current) {
|
|
361
|
+
const stats = fs.statSync(current);
|
|
362
|
+
if (stats.isDirectory()) {
|
|
363
|
+
for (const item of fs.readdirSync(current)) {
|
|
364
|
+
visit(path.join(current, item));
|
|
365
|
+
}
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
if (path.extname(current) !== '.md') return;
|
|
369
|
+
|
|
370
|
+
const content = fs.readFileSync(current, 'utf8');
|
|
371
|
+
if (!content.includes(assignmentStamp)) return;
|
|
372
|
+
fs.writeFileSync(current, content.split(assignmentStamp).join(stampedAssignment));
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
visit(rootDir);
|
|
376
|
+
}
|
|
377
|
+
|
|
343
378
|
// Dev artifacts that live under config/skills/ but must never ship or install.
|
|
344
379
|
// Mirrors the `!` exclusions in package.json files[].
|
|
345
380
|
const UNSHIPPED_SKILL_DIRS = new Set([
|
|
@@ -551,6 +586,11 @@ function installSkillsForCLI(cliKey) {
|
|
|
551
586
|
fs.mkdirSync(cli.skillsDir, { recursive: true });
|
|
552
587
|
}
|
|
553
588
|
|
|
589
|
+
const removed = cleanupDeprecated(path.dirname(cli.skillsDir));
|
|
590
|
+
if (removed > 0) {
|
|
591
|
+
log(`\n๐งน Cleaned up ${removed} deprecated file${removed > 1 ? 's' : ''}`, 'yellow');
|
|
592
|
+
}
|
|
593
|
+
|
|
554
594
|
let copied = 0;
|
|
555
595
|
for (const skillName of cli.skillsToInstall) {
|
|
556
596
|
const src = path.join(sourceSkillsDir, skillName);
|
|
@@ -562,6 +602,7 @@ function installSkillsForCLI(cliKey) {
|
|
|
562
602
|
fs.rmSync(dest, { recursive: true, force: true });
|
|
563
603
|
}
|
|
564
604
|
copyRecursive(src, dest, cli.skillsDir);
|
|
605
|
+
stampInstalledSkillDir(dest, dest);
|
|
565
606
|
copied++;
|
|
566
607
|
log(` โ ${cli.skillsDir.replace(os.homedir(), '~')}/${skillName}`, 'dim');
|
|
567
608
|
}
|
|
@@ -42,6 +42,13 @@ Specific patterns the official guide warns about for this model.
|
|
|
42
42
|
|
|
43
43
|
Keep each section to โค 8 lines. Adapters are deltas, not full prompts. If an adapter grows past ~80 lines, the content probably belongs in canonical body.
|
|
44
44
|
|
|
45
|
+
## Role-scoped and non-agentic adapters
|
|
46
|
+
|
|
47
|
+
Adapters may add two more sections beyond the base format:
|
|
48
|
+
|
|
49
|
+
- **`## Role eligibility`** โ fixed ASCII fields, one per line: `executor: yes|no`, `pair_judge: yes|no`. Absent section = eligible for both roles (every adapter shipped before iter-0051). Present only when a backend is structurally unable to fill a role โ e.g. a completion-only local model has no tool/file-edit access, so it declares `executor: no`. `_shared/engine-preflight.md` and `/devlyn:engines` read this section as a structured marker, not prose, so it validates mechanically.
|
|
50
|
+
- **`## Invocation`** โ for backends called over an API instead of a CLI: the concrete request shape (endpoint, JSON body, timeout) and the adapter-declared availability probe (`_shared/engine-preflight.md#role-resolution`'s "an adapter may declare a different probe" hook).
|
|
51
|
+
|
|
45
52
|
## When to add a new adapter
|
|
46
53
|
|
|
47
54
|
A new adapter file ships when:
|
|
@@ -2,6 +2,53 @@
|
|
|
2
2
|
|
|
3
3
|
> Source: <https://platform.claude.com/docs/en/build-with-claude/prompt-engineering/claude-prompting-best-practices>
|
|
4
4
|
|
|
5
|
+
## Invocation
|
|
6
|
+
|
|
7
|
+
Orchestrator-side contract for spawning a Claude judge from a non-Claude
|
|
8
|
+
orchestrator (Codex CLI, oh-my-pi) โ the reverse-direction counterpart of
|
|
9
|
+
`_shared/codex-config.md`. Source: <https://code.claude.com/docs/en/headless>
|
|
10
|
+
and <https://code.claude.com/docs/en/cli-reference>.
|
|
11
|
+
|
|
12
|
+
**Availability probe**: `command -v claude >/dev/null 2>&1`; record
|
|
13
|
+
`claude --version` as evidence. The probe is necessary, not sufficient:
|
|
14
|
+
`claude -p` needs network access to the Anthropic API, so a network-denying
|
|
15
|
+
sandbox (e.g. Codex CLI's default `workspace-write`) fails the spawn even
|
|
16
|
+
though the binary resolves. A failed spawn is the same fail-closed class as a
|
|
17
|
+
failed probe (`_shared/engine-preflight.md`): explicit route โ
|
|
18
|
+
`BLOCKED:claude-unavailable`; automatic escalation โ solo + reported skip.
|
|
19
|
+
|
|
20
|
+
**Read-only pair-JUDGE call** (bounded by the VERIFY pair contract โ at most
|
|
21
|
+
two targeted probes; the invoking phase sets `--effort`, pair-JUDGE uses
|
|
22
|
+
`medium`):
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
claude -p "<judge prompt>" \
|
|
26
|
+
--permission-mode dontAsk \
|
|
27
|
+
--allowedTools "Read,Grep,Glob,Bash(<repo test command> *)" \
|
|
28
|
+
--setting-sources project --strict-mcp-config --mcp-config '{"mcpServers":{}}' \
|
|
29
|
+
--effort medium \
|
|
30
|
+
> .devlyn/claude-judge.stdout 2> .devlyn/claude-judge.stderr
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
- Omit `--model` โ the CLI's configured default is used (zero-touch, same
|
|
34
|
+
rule as codex-config.md's "omit `-m`").
|
|
35
|
+
- `dontAsk` denies anything not allowlisted (official guide: "denies
|
|
36
|
+
anything not in your permissions.allow rules or the read-only command
|
|
37
|
+
set โ useful for locked-down CI runs"); in `-p` mode a denied tool call
|
|
38
|
+
fails without prompting, so the child can never hang on a permission ask.
|
|
39
|
+
- The `--setting-sources project --strict-mcp-config --mcp-config
|
|
40
|
+
'{"mcpServers":{}}'` trio is the hermetic-child pattern: no user-global
|
|
41
|
+
settings, hooks, or MCP servers. `--bare` is NOT used โ it skips
|
|
42
|
+
OAuth/keychain reads and requires `ANTHROPIC_API_KEY`, which
|
|
43
|
+
subscription-auth machines do not have.
|
|
44
|
+
- Findings come back as JSONL on stdout (same emission shape as the Codex
|
|
45
|
+
direction); the orchestrator writes the canonical
|
|
46
|
+
`.devlyn/verify.pair.findings.jsonl`. Raw stdout stays diagnostic at
|
|
47
|
+
`.devlyn/claude-judge.stdout`; `verify-merge-findings.py` blocks the run
|
|
48
|
+
if stdout contains findings the canonical file lacks.
|
|
49
|
+
- Do not pipe stdout (`| tail`, `| grep`); capture to file. Non-zero exit or
|
|
50
|
+
empty stdout โ spawn failure, fail closed per the probe rule above.
|
|
51
|
+
|
|
5
52
|
## Identity
|
|
6
53
|
|
|
7
54
|
You are Claude by Anthropic. Anthropic's prompt-engineering guide for this model governs your behavior on top of the canonical phase prompt below. When the canonical body and this header conflict on tactics, the canonical body wins on what to deliver; this header wins on how to deliver it.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# oh-my-pi adapter
|
|
2
|
+
|
|
3
|
+
## Identity
|
|
4
|
+
|
|
5
|
+
You are the model running behind the `omp` CLI. Follow the canonical phase prompt exactly; this adapter exists so oh-my-pi can be selected as a first-class native task worker by `/devlyn:resolve`.
|
|
6
|
+
|
|
7
|
+
## Output Discipline
|
|
8
|
+
|
|
9
|
+
Keep phase output machine-consumable and concise. Write the artifacts the canonical body names, update `.devlyn/pipeline.state.json` only where that phase owns the state write, and avoid narrative outside the required phase result.
|
|
10
|
+
|
|
11
|
+
## Validation Pattern
|
|
12
|
+
|
|
13
|
+
When the canonical body names deterministic commands, run those commands and trust their exit codes. Do not replace BUILD_GATE, phase gates, or VERIFY mechanical checks with same-context judgment.
|
|
14
|
+
|
|
15
|
+
## Fresh Context
|
|
16
|
+
|
|
17
|
+
Each native `task` tool invocation with a fresh `context` is a fresh phase worker. Do not call `/devlyn:resolve` recursively from inside a phase prompt; execute only the phase body you received.
|
|
@@ -56,7 +56,9 @@ PER_RUN_PATTERNS = (
|
|
|
56
56
|
# ("pair_judge findings archive distinguishable") would false-fail on
|
|
57
57
|
# every paired fixture without this glob.
|
|
58
58
|
"verify-judge-*.md",
|
|
59
|
-
|
|
59
|
+
# *-judge.*: every engine's pair-judge stdout/stderr capture
|
|
60
|
+
# (codex-judge.*, claude-judge.* โ adapters/claude.md ## Invocation).
|
|
61
|
+
"*-judge.*",
|
|
60
62
|
)
|
|
61
63
|
|
|
62
64
|
SAFE_RUN_ID_RE = re.compile(r"^[A-Za-z0-9_.-]+$")
|
|
@@ -140,12 +142,14 @@ def self_test() -> int:
|
|
|
140
142
|
"verify-merge.summary.json",
|
|
141
143
|
"codex-judge.stdout",
|
|
142
144
|
"codex-judge.summary.json",
|
|
145
|
+
"claude-judge.stdout",
|
|
146
|
+
"claude-judge.stderr",
|
|
143
147
|
):
|
|
144
148
|
(devlyn / name).write_text("{}\n", encoding="utf-8")
|
|
145
149
|
run_id = read_run_id(devlyn)
|
|
146
150
|
assert run_id == "run-1", run_id
|
|
147
151
|
moved = move_artifacts(devlyn, devlyn / "runs" / run_id)
|
|
148
|
-
assert moved >=
|
|
152
|
+
assert moved >= 8, moved
|
|
149
153
|
for name in (
|
|
150
154
|
"pipeline.state.json",
|
|
151
155
|
"risk-probes.jsonl",
|
|
@@ -153,6 +157,8 @@ def self_test() -> int:
|
|
|
153
157
|
"verify-merge.summary.json",
|
|
154
158
|
"codex-judge.stdout",
|
|
155
159
|
"codex-judge.summary.json",
|
|
160
|
+
"claude-judge.stdout",
|
|
161
|
+
"claude-judge.stderr",
|
|
156
162
|
):
|
|
157
163
|
assert (devlyn / "runs" / run_id / name).is_file(), name
|
|
158
164
|
|