devlyn-cli 2.7.0 โ†’ 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/AGENTS.md +9 -1
  2. package/CLAUDE.md +28 -11
  3. package/benchmark/auto-resolve/fixtures/F1-cli-trivial-flag/task.ko.txt +8 -0
  4. package/benchmark/auto-resolve/fixtures/retired/F34-cli-settle-inflight/NOTES.md +28 -0
  5. package/benchmark/auto-resolve/fixtures/retired/F34-cli-settle-inflight/RETIRED.md +15 -0
  6. package/benchmark/auto-resolve/fixtures/retired/F34-cli-settle-inflight/expected.json +57 -0
  7. package/benchmark/auto-resolve/fixtures/retired/F34-cli-settle-inflight/metadata.json +10 -0
  8. package/benchmark/auto-resolve/fixtures/retired/F34-cli-settle-inflight/setup.sh +50 -0
  9. package/benchmark/auto-resolve/fixtures/retired/F34-cli-settle-inflight/spec.md +59 -0
  10. package/benchmark/auto-resolve/fixtures/retired/F34-cli-settle-inflight/task.txt +9 -0
  11. package/benchmark/auto-resolve/fixtures/retired/F34-cli-settle-inflight/verifiers/ordered-settlement.js +88 -0
  12. package/benchmark/auto-resolve/fixtures/retired/F34-cli-settle-inflight/verifiers/reject-invalid.js +45 -0
  13. package/benchmark/auto-resolve/fixtures/retired/F35-cli-apply-journal/NOTES.md +30 -0
  14. package/benchmark/auto-resolve/fixtures/retired/F35-cli-apply-journal/RETIRED.md +16 -0
  15. package/benchmark/auto-resolve/fixtures/retired/F35-cli-apply-journal/expected.json +64 -0
  16. package/benchmark/auto-resolve/fixtures/retired/F35-cli-apply-journal/metadata.json +10 -0
  17. package/benchmark/auto-resolve/fixtures/retired/F35-cli-apply-journal/setup.sh +21 -0
  18. package/benchmark/auto-resolve/fixtures/retired/F35-cli-apply-journal/spec.md +58 -0
  19. package/benchmark/auto-resolve/fixtures/retired/F35-cli-apply-journal/task.txt +9 -0
  20. package/benchmark/auto-resolve/fixtures/retired/F35-cli-apply-journal/verifiers/dup-op.js +54 -0
  21. package/benchmark/auto-resolve/fixtures/retired/F35-cli-apply-journal/verifiers/priority-apply.js +82 -0
  22. package/benchmark/auto-resolve/fixtures/retired/F35-cli-apply-journal/verifiers/rollback.js +77 -0
  23. package/benchmark/auto-resolve/fixtures/retired/F36-cli-session-admission/NOTES.md +99 -0
  24. package/benchmark/auto-resolve/fixtures/retired/F36-cli-session-admission/RETIRED.md +18 -0
  25. package/benchmark/auto-resolve/fixtures/retired/F36-cli-session-admission/expected.json +64 -0
  26. package/benchmark/auto-resolve/fixtures/retired/F36-cli-session-admission/metadata.json +10 -0
  27. package/benchmark/auto-resolve/fixtures/retired/F36-cli-session-admission/setup.sh +4 -0
  28. package/benchmark/auto-resolve/fixtures/retired/F36-cli-session-admission/spec.md +61 -0
  29. package/benchmark/auto-resolve/fixtures/retired/F36-cli-session-admission/task.txt +9 -0
  30. package/benchmark/auto-resolve/fixtures/retired/F36-cli-session-admission/verifiers/correctness-small.js +135 -0
  31. package/benchmark/auto-resolve/fixtures/retired/F36-cli-session-admission/verifiers/scale.js +100 -0
  32. package/benchmark/auto-resolve/fixtures/retired/F36-cli-session-admission/verifiers/validation.js +75 -0
  33. package/benchmark/auto-resolve/fixtures/retired/F37-cli-rule-lookup/NOTES.md +93 -0
  34. package/benchmark/auto-resolve/fixtures/retired/F37-cli-rule-lookup/RETIRED.md +16 -0
  35. package/benchmark/auto-resolve/fixtures/retired/F37-cli-rule-lookup/expected.json +65 -0
  36. package/benchmark/auto-resolve/fixtures/retired/F37-cli-rule-lookup/metadata.json +10 -0
  37. package/benchmark/auto-resolve/fixtures/retired/F37-cli-rule-lookup/setup.sh +18 -0
  38. package/benchmark/auto-resolve/fixtures/retired/F37-cli-rule-lookup/spec.md +66 -0
  39. package/benchmark/auto-resolve/fixtures/retired/F37-cli-rule-lookup/task.txt +9 -0
  40. package/benchmark/auto-resolve/fixtures/retired/F37-cli-rule-lookup/verifiers/correctness-small.js +84 -0
  41. package/benchmark/auto-resolve/fixtures/retired/F37-cli-rule-lookup/verifiers/scale.js +184 -0
  42. package/benchmark/auto-resolve/fixtures/retired/F37-cli-rule-lookup/verifiers/validation.js +74 -0
  43. package/benchmark/auto-resolve/scripts/pair-rejected-fixtures.sh +12 -0
  44. package/benchmark/auto-resolve/scripts/run-fixture.sh +39 -1
  45. package/benchmark/auto-resolve/scripts/test-lint-fixtures.sh +1 -1
  46. package/bin/devlyn.js +42 -1
  47. package/config/skills/_shared/adapters/README.md +7 -0
  48. package/config/skills/_shared/adapters/claude.md +47 -0
  49. package/config/skills/_shared/adapters/omp.md +17 -0
  50. package/config/skills/_shared/archive_run.py +8 -2
  51. package/config/skills/_shared/codex-config.md +22 -6
  52. package/config/skills/_shared/engine-doctor.sh +107 -0
  53. package/config/skills/_shared/engine-preflight.md +18 -11
  54. package/config/skills/_shared/expected.schema.json +26 -0
  55. package/config/skills/_shared/runtime-principles.md +1 -1
  56. package/config/skills/_shared/spec-verify-check.py +773 -486
  57. package/config/skills/_shared/state-phase-write.py +374 -0
  58. package/config/skills/_shared/verify-merge-findings.py +177 -61
  59. package/config/skills/devlyn:engines/SKILL.md +47 -0
  60. package/config/skills/devlyn:ideate/SKILL.md +22 -3
  61. package/config/skills/devlyn:ideate/references/elicitation.md +2 -2
  62. package/config/skills/devlyn:ideate/references/from-spec-mode.md +2 -2
  63. package/config/skills/devlyn:ideate/references/project-mode.md +1 -1
  64. package/config/skills/devlyn:ideate/references/spec-template.md +15 -4
  65. package/config/skills/devlyn:queue/SKILL.md +26 -0
  66. package/config/skills/devlyn:resolve/SKILL.md +54 -24
  67. package/config/skills/devlyn:resolve/references/free-form-mode.md +1 -1
  68. package/config/skills/devlyn:resolve/references/phases/build-gate.md +2 -2
  69. package/config/skills/devlyn:resolve/references/phases/cleanup.md +1 -1
  70. package/config/skills/devlyn:resolve/references/phases/implement.md +1 -1
  71. package/config/skills/devlyn:resolve/references/phases/plan.md +3 -2
  72. package/config/skills/devlyn:resolve/references/phases/probe-derive.md +8 -0
  73. package/config/skills/devlyn:resolve/references/phases/verify.md +18 -9
  74. package/config/skills/devlyn:resolve/references/state-schema.md +10 -6
  75. package/optional-skills/devlyn:pencil-pull/SKILL.md +5 -0
  76. package/optional-skills/devlyn:pencil-push/SKILL.md +5 -0
  77. package/package.json +1 -1
  78. package/scripts/lint-skills.sh +48 -41
@@ -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 sibling spec.md declares all Requirements are pure-design' "$TMP/spec-verify-check-expected-fail.out"
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
- "codex-judge.*",
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 >= 6, 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