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,21 @@
1
+ #!/usr/bin/env bash
2
+ # F35 setup — seed inventory and the replay journal.
3
+ set -e
4
+
5
+ mkdir -p data
6
+
7
+ cat > data/inventory.json <<'JSON'
8
+ {
9
+ "widget": 10,
10
+ "gadget": 4,
11
+ "gizmo": 0
12
+ }
13
+ JSON
14
+
15
+ cat > data/journal.json <<'JSON'
16
+ {
17
+ "applied": ["op-900"]
18
+ }
19
+ JSON
20
+
21
+ exit 0
@@ -0,0 +1,58 @@
1
+ ---
2
+ id: "F35-cli-apply-journal"
3
+ title: "Apply command with journal replay and all-or-nothing persistence"
4
+ status: planned
5
+ complexity: high
6
+ depends-on: []
7
+ ---
8
+
9
+ # F35 Apply command with journal replay and all-or-nothing persistence
10
+
11
+ ## Context
12
+
13
+ `bench-cli` gets an `apply` command that applies a file of inventory
14
+ adjustments to `data/inventory.json`. Runs are re-deliverable: a journal at
15
+ `data/journal.json` records which ops have already been applied, so a
16
+ delivered-again op must not change inventory twice. A batch either fully
17
+ persists or leaves both files untouched.
18
+
19
+ ## Requirements
20
+
21
+ - [ ] `bench-cli apply --input <path>` reads JSON shaped as `{ "ops": [{ "op_id": string, "sku": string, "delta": number, "priority": number }] }`.
22
+ - [ ] Quantities come from `data/inventory.json`; the applied-op record comes from the journal's `applied` array in `data/journal.json`. Both are seeded by setup.
23
+ - [ ] Validation happens before any application. Invalid JSON, missing or non-string `op_id`/`sku`, unknown `sku`, non-integer `delta` or `priority`, or an `op_id` that appears more than once in the input file exits `2` and writes exactly one JSON error object to stderr with shape `{ "error": "invalid_input", "reason": string }`, printing nothing to stdout and changing neither file.
24
+ - [ ] An op whose `op_id` is already listed in the journal's `applied` array is recorded as skipped and changes nothing, regardless of its other fields.
25
+ - [ ] Non-skipped ops apply in descending `priority`; ties apply in file order.
26
+ - [ ] An op fails when applying its `delta` would take the sku's quantity below zero, evaluated at its turn in that order, with exact error shape `{ "error": "insufficient_stock", "op_id": string, "sku": string, "available": number, "requested": number }` where `available` is the quantity at that op's turn and `requested` is the absolute value of its `delta`.
27
+ - [ ] If any op fails, nothing persists: `data/inventory.json` and `data/journal.json` are byte-for-byte unchanged, the command exits `2`, and exactly one JSON error object is written to stderr with nothing on stdout.
28
+ - [ ] On success, persist the final quantities to `data/inventory.json`, append the applied `op_id`s to the journal's `applied` array in application order, and write exactly one JSON object to stdout with keys `applied` (op_ids in application order), `skipped` (op_ids in file order), and `inventory` (final quantities for every sku). Nothing is written to stderr on success.
29
+ - [ ] `tests/cli.test.js` is updated. Existing tests still pass AND at least two new tests cover `apply`: one success mixing a journaled op with different priorities, and one failing batch that leaves both files unchanged.
30
+
31
+ ## Constraints
32
+
33
+ - **No new npm dependencies.**
34
+ - **No silent catches.** If parsing or file reading fails, emit a visible JSON error to stderr and exit `2`.
35
+ - **No extra stdout/stderr text** on the success path; downstream tooling parses stdout as JSON.
36
+ - Quantities are integers; no fractional inventory.
37
+
38
+ ## Out of Scope
39
+
40
+ - Changing server routes, web UI, or journal schema.
41
+ - Concurrency across multiple `apply` processes.
42
+ - Pruning or rewriting existing journal entries.
43
+ - Touching `server/`, `web/`, or `tests/server.test.js`.
44
+
45
+ ## Verification
46
+
47
+ - `node --test tests/cli.test.js` exits 0.
48
+ - A batch mixing a journaled op, unequal priorities, and a priority tie yields exact `applied` order, exact `skipped`, exact final `inventory`, and the journal grows by exactly the applied op_ids in that order.
49
+ - A batch whose failing op is only discoverable after higher-priority ops have consumed or added stock reports `available` at that op's turn and leaves both files byte-for-byte unchanged.
50
+ - A duplicated `op_id` in the input file exits `2` with one JSON error on stderr and no stdout.
51
+ - `git diff --stat` shows only `bin/cli.js` and `tests/cli.test.js` touched (inventory and journal are seeded by setup, not the arm).
52
+
53
+ ## Solo-headroom hypothesis
54
+
55
+ A capable solo_claude baseline is expected to miss that each op's
56
+ `available` stock is evaluated at its turn in priority order rather than
57
+ against the pre-batch inventory or file order; observable command
58
+ `node "$BENCH_FIXTURE_DIR/verifiers/rollback.js"` exposes the miss.
@@ -0,0 +1,9 @@
1
+ Add an `apply` command to `bench-cli` so users can run `bench-cli apply --input <path>` with an ops JSON file of inventory adjustments. Quantities live in `data/inventory.json` and previously applied op ids live in the `applied` array of `data/journal.json` (both seeded).
2
+
3
+ Validate the whole file before applying anything: bad JSON, missing/invalid fields, unknown sku, non-integer delta or priority, or the same `op_id` appearing twice in one file exits `2` with exactly one JSON error object `{ "error": "invalid_input", "reason": string }` on stderr, nothing on stdout, and neither file changed.
4
+
5
+ An op whose `op_id` is already in the journal is recorded as skipped and changes nothing — whatever its other fields say. Everything else applies in descending `priority` (ties in file order). An op fails if its `delta` would push the sku below zero at its turn in that order; the error is the exact object `{ "error": "insufficient_stock", "op_id", "sku", "available", "requested" }` with `available` measured at that op's turn. Any failure means nothing persists — inventory and journal stay byte-for-byte identical and the command exits `2`.
6
+
7
+ On success, write the final quantities back to `data/inventory.json`, append applied op ids to the journal in application order, and print exactly one JSON object with `applied` (application order), `skipped` (file order), and `inventory` — with nothing on stderr.
8
+
9
+ Update `tests/cli.test.js`: existing tests still pass, plus at least two apply tests (one success mixing a journaled op with different priorities, one failing batch that leaves both files unchanged). No new npm dependencies. Only touch `bin/cli.js` and `tests/cli.test.js`.
@@ -0,0 +1,54 @@
1
+ 'use strict';
2
+ // F35 hidden verifier — an op_id duplicated WITHIN the input file is a
3
+ // validation error (whole file rejected, nothing changes), distinct from a
4
+ // journal replay which is a per-op skip. Exit 2, one JSON error object on
5
+ // stderr, no stdout, both files untouched.
6
+
7
+ const fs = require('node:fs');
8
+ const os = require('node:os');
9
+ const path = require('node:path');
10
+ const { spawnSync } = require('node:child_process');
11
+
12
+ const work = process.env.BENCH_WORKDIR;
13
+ const invPath = path.join(work, 'data', 'inventory.json');
14
+ const jrnPath = path.join(work, 'data', 'journal.json');
15
+
16
+ fs.writeFileSync(invPath, JSON.stringify({ widget: 10, gadget: 4, gizmo: 0 }, null, 2) + '\n');
17
+ fs.writeFileSync(jrnPath, JSON.stringify({ applied: ['op-900'] }, null, 2) + '\n');
18
+ const invBefore = fs.readFileSync(invPath, 'utf8');
19
+ const jrnBefore = fs.readFileSync(jrnPath, 'utf8');
20
+
21
+ const inputPath = path.join(os.tmpdir(), `apply-dup-${process.pid}.json`);
22
+ fs.writeFileSync(inputPath, JSON.stringify({
23
+ ops: [
24
+ { op_id: 'op-20', sku: 'widget', delta: -1, priority: 3 },
25
+ { op_id: 'op-21', sku: 'gadget', delta: 1, priority: 2 },
26
+ { op_id: 'op-20', sku: 'gizmo', delta: 2, priority: 1 }
27
+ ]
28
+ }));
29
+
30
+ const cli = path.join(work, 'bin', 'cli.js');
31
+ const result = spawnSync('node', [cli, 'apply', '--input', inputPath], {
32
+ cwd: work,
33
+ encoding: 'utf8'
34
+ });
35
+
36
+ let err;
37
+ try {
38
+ err = JSON.parse(result.stderr);
39
+ } catch {
40
+ err = null;
41
+ }
42
+
43
+ const filesUnchanged = fs.readFileSync(invPath, 'utf8') === invBefore
44
+ && fs.readFileSync(jrnPath, 'utf8') === jrnBefore;
45
+
46
+ const ok = result.status === 2
47
+ && result.stdout === ''
48
+ && err !== null
49
+ && err.error === 'invalid_input'
50
+ && typeof err.reason === 'string'
51
+ && filesUnchanged;
52
+
53
+ console.log(JSON.stringify({ ok, status: result.status, stdout: result.stdout, err, filesUnchanged }));
54
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,82 @@
1
+ 'use strict';
2
+ // F35 hidden verifier — success path mixing a journaled (replayed) op, unequal
3
+ // priorities, and a priority tie. Final quantities are order-independent
4
+ // (addition commutes), so the discriminator is the exact `applied` ORDER and
5
+ // the journal append order: priority-desc with file-order ties, replayed op
6
+ // skipped regardless of its (otherwise failing) fields.
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
+ // Structural equality: object key order is NOT part of the visible contract;
14
+ // array order IS. Codex fairness-audit fix (stringify equality made JSON
15
+ // property order a hidden oracle narrower than spec.md).
16
+ function deepEqual(a, b) {
17
+ if (a === b) return true;
18
+ if (Array.isArray(a) && Array.isArray(b)) {
19
+ return a.length === b.length && a.every((v, i) => deepEqual(v, b[i]));
20
+ }
21
+ if (a && b && typeof a === 'object' && typeof b === 'object' && !Array.isArray(a) && !Array.isArray(b)) {
22
+ const ka = Object.keys(a).sort();
23
+ const kb = Object.keys(b).sort();
24
+ return deepEqual(ka, kb) && ka.every((k) => deepEqual(a[k], b[k]));
25
+ }
26
+ return false;
27
+ }
28
+
29
+ const work = process.env.BENCH_WORKDIR;
30
+ const invPath = path.join(work, 'data', 'inventory.json');
31
+ const jrnPath = path.join(work, 'data', 'journal.json');
32
+
33
+ // Reset seeds so this verifier is order-independent from other commands.
34
+ fs.writeFileSync(invPath, JSON.stringify({ widget: 10, gadget: 4, gizmo: 0 }, null, 2) + '\n');
35
+ fs.writeFileSync(jrnPath, JSON.stringify({ applied: ['op-900'] }, null, 2) + '\n');
36
+
37
+ const inputPath = path.join(os.tmpdir(), `apply-ok-${process.pid}.json`);
38
+ fs.writeFileSync(inputPath, JSON.stringify({
39
+ ops: [
40
+ { op_id: 'op-1', sku: 'widget', delta: -4, priority: 1 },
41
+ { op_id: 'op-900', sku: 'widget', delta: -9, priority: 9 },
42
+ { op_id: 'op-2', sku: 'widget', delta: -5, priority: 5 },
43
+ { op_id: 'op-3', sku: 'gadget', delta: -4, priority: 5 },
44
+ { op_id: 'op-4', sku: 'widget', delta: 3, priority: 0 }
45
+ ]
46
+ }));
47
+
48
+ const cli = path.join(work, 'bin', 'cli.js');
49
+ const result = spawnSync('node', [cli, 'apply', '--input', inputPath], {
50
+ cwd: work,
51
+ encoding: 'utf8'
52
+ });
53
+
54
+ let out;
55
+ try {
56
+ out = JSON.parse(result.stdout);
57
+ } catch {
58
+ out = null;
59
+ }
60
+
61
+ const expected = {
62
+ applied: ['op-2', 'op-3', 'op-1', 'op-4'],
63
+ skipped: ['op-900'],
64
+ inventory: { widget: 4, gadget: 0, gizmo: 0 }
65
+ };
66
+
67
+ let inv; let jrn;
68
+ try {
69
+ inv = JSON.parse(fs.readFileSync(invPath, 'utf8'));
70
+ jrn = JSON.parse(fs.readFileSync(jrnPath, 'utf8'));
71
+ } catch {
72
+ inv = null; jrn = null;
73
+ }
74
+
75
+ const ok = result.status === 0
76
+ && result.stderr === ''
77
+ && deepEqual(out, expected)
78
+ && deepEqual(inv, expected.inventory)
79
+ && deepEqual(jrn, { applied: ['op-900', 'op-2', 'op-3', 'op-1', 'op-4'] });
80
+
81
+ console.log(JSON.stringify({ ok, status: result.status, stderr: result.stderr, out, inv, jrn }));
82
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,77 @@
1
+ 'use strict';
2
+ // F35 hidden verifier — the failing op is only discoverable AFTER a
3
+ // higher-priority op has added stock: op-12 (priority 8) raises widget
4
+ // 10 -> 15, then op-11 (priority 5) requests 20 and fails with
5
+ // available 15 — not 10 (pre-batch / file-order would report 10).
6
+ // Both files must be byte-for-byte unchanged afterward.
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
+ // Structural equality: object key order is NOT part of the visible contract;
14
+ // array order IS. Codex fairness-audit fix (stringify equality made JSON
15
+ // property order a hidden oracle narrower than spec.md).
16
+ function deepEqual(a, b) {
17
+ if (a === b) return true;
18
+ if (Array.isArray(a) && Array.isArray(b)) {
19
+ return a.length === b.length && a.every((v, i) => deepEqual(v, b[i]));
20
+ }
21
+ if (a && b && typeof a === 'object' && typeof b === 'object' && !Array.isArray(a) && !Array.isArray(b)) {
22
+ const ka = Object.keys(a).sort();
23
+ const kb = Object.keys(b).sort();
24
+ return deepEqual(ka, kb) && ka.every((k) => deepEqual(a[k], b[k]));
25
+ }
26
+ return false;
27
+ }
28
+
29
+ const work = process.env.BENCH_WORKDIR;
30
+ const invPath = path.join(work, 'data', 'inventory.json');
31
+ const jrnPath = path.join(work, 'data', 'journal.json');
32
+
33
+ fs.writeFileSync(invPath, JSON.stringify({ widget: 10, gadget: 4, gizmo: 0 }, null, 2) + '\n');
34
+ fs.writeFileSync(jrnPath, JSON.stringify({ applied: ['op-900'] }, null, 2) + '\n');
35
+ const invBefore = fs.readFileSync(invPath, 'utf8');
36
+ const jrnBefore = fs.readFileSync(jrnPath, 'utf8');
37
+
38
+ const inputPath = path.join(os.tmpdir(), `apply-fail-${process.pid}.json`);
39
+ fs.writeFileSync(inputPath, JSON.stringify({
40
+ ops: [
41
+ { op_id: 'op-10', sku: 'gadget', delta: 2, priority: 9 },
42
+ { op_id: 'op-11', sku: 'widget', delta: -20, priority: 5 },
43
+ { op_id: 'op-12', sku: 'widget', delta: 5, priority: 8 }
44
+ ]
45
+ }));
46
+
47
+ const cli = path.join(work, 'bin', 'cli.js');
48
+ const result = spawnSync('node', [cli, 'apply', '--input', inputPath], {
49
+ cwd: work,
50
+ encoding: 'utf8'
51
+ });
52
+
53
+ let err;
54
+ try {
55
+ err = JSON.parse(result.stderr);
56
+ } catch {
57
+ err = null;
58
+ }
59
+
60
+ const expectedErr = {
61
+ error: 'insufficient_stock',
62
+ op_id: 'op-11',
63
+ sku: 'widget',
64
+ available: 15,
65
+ requested: 20
66
+ };
67
+
68
+ const filesUnchanged = fs.readFileSync(invPath, 'utf8') === invBefore
69
+ && fs.readFileSync(jrnPath, 'utf8') === jrnBefore;
70
+
71
+ const ok = result.status === 2
72
+ && result.stdout === ''
73
+ && deepEqual(err, expectedErr)
74
+ && filesUnchanged;
75
+
76
+ console.log(JSON.stringify({ ok, status: result.status, stdout: result.stdout, err, filesUnchanged }));
77
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,99 @@
1
+ # F36 Session admission command with a global concurrency cap
2
+
3
+ ## Failure mode
4
+
5
+ Detects implementations that get interval-admission logic correct at small,
6
+ hand-written scale but reach for a data structure that stops scaling once
7
+ the number of concurrently active sessions grows large — most naturally, a
8
+ plain array of admitted sessions that gets linearly scanned or filtered on
9
+ every new candidate. That shape is indistinguishable from the fully correct
10
+ answer on any test an agent is likely to hand-write, and only fails once the
11
+ active set stays large across a long run (Tier-2 axis: performance/scale,
12
+ never previously used in this suite — no existing fixture verifier measures
13
+ wall-clock; grepped `verifiers/*.js` across active + retired fixtures for
14
+ `Date.now|hrtime|performance.now|elapsed`, zero hits before this one).
15
+ Compounded with four small-scale correctness sub-traps so a fast-but-wrong
16
+ shortcut and a correct-but-slow shortcut both lose points independently:
17
+ half-open boundary (touching, not overlapping), start-time eligibility
18
+ order vs. file order, per-candidate (not static) `active_at_start`, and
19
+ eviction that must be driven by actual expiry (`end`) rather than admission
20
+ order — the last one specifically closes a FIFO-queue shortcut that would
21
+ otherwise pass the scale case by coincidence, since the scale case uses a
22
+ uniform session duration and a FIFO queue's admission order happens to
23
+ coincide with its expiry order there (Codex R2 catch, see Rounds below).
24
+
25
+ ## Fairness
26
+
27
+ Every verifier assertion quotes a visible spec.md bullet via `contract_refs`
28
+ (lint-fixtures enforced). The 150,000-session performance requirement is
29
+ stated as a plain, numeric, visible spec bullet — no mechanism vocabulary
30
+ (no "sort", "heap", "priority queue", "efficient", "O(n log n)") appears
31
+ anywhere in `spec.md` or `task.txt`; only the observable behavior (must
32
+ finish within the process timeout, must not change the answer) is named.
33
+ The scale verifier's 15-second hard kill (`spawnSync` `timeout` option) sits
34
+ more than 100x above the efficient reference's actual running time and
35
+ correspondingly far below any realistic degraded-shortcut running time on
36
+ this fixture's parameters, so the gate has a wide, non-flaky margin in both
37
+ directions.
38
+
39
+ ## Pipeline phase(s) tested
40
+
41
+ BUILD_GATE / VERIFY: an agent's own test suite is overwhelmingly likely to
42
+ be small-N (a handful of hand-written sessions), so nothing in a normal
43
+ iterative dev loop surfaces the scale defect; only the hidden, harness-side
44
+ scale verifier (never visible to the arm) exercises it. This is a
45
+ structurally different discriminator from every prior fixture in this
46
+ suite, which relied entirely on a single hidden semantic trap rather than a
47
+ mechanical, orthogonal performance axis.
48
+
49
+ ## Why another fixture can't cover this
50
+
51
+ No active or retired fixture in this suite measures wall-clock/algorithmic
52
+ complexity at all (verified by grep, see Failure mode above). Every prior
53
+ headroom attempt (F27, F28, F30, F34, F35, S2-S6 — see
54
+ `pair-rejected-fixtures.sh`) relied on a single hidden ordering/rollback/
55
+ atomicity trap, and solo_claude scored 88-100 on all of them. This fixture
56
+ tests a category of miss (asymptotic complexity blindness under a full
57
+ self-review pipeline) that none of those exercise.
58
+
59
+ ## When to retire or replace
60
+
61
+ If a headroom run shows solo_claude reliably recognizes and implements the
62
+ required efficient structure even without prompting (i.e. the visible
63
+ numeric scale requirement alone is enough to trigger scale-aware design),
64
+ retire per the same honest-retirement contract used for F27/F28/F30/F34/F35
65
+ and add this id to `pair-rejected-fixtures.sh` with the observed scores.
66
+
67
+ ## Rounds — Codex GPT-5.5 collaboration (read-only, `codex-monitored.sh`)
68
+
69
+ **Round 1** (initial design: performance stated as a secondary condition on
70
+ top of a single hidden ordering trap, textbook "meeting rooms with
71
+ capacity" shape): Codex verdict **needs-rework** — "200,000 sessions" alone
72
+ is enough for a strong PLAN phase to infer scale pressure and pattern-match
73
+ the well-known interval-scheduling/heap shape; the boundary traps alone
74
+ weren't judged sufficient given solo already solved comparable explicit
75
+ order/rollback traps in F34/F35. Adopted: make performance the dominant
76
+ axis by choosing scale-case parameters (uniform long overlapping durations,
77
+ capacity held well below the overlap window) so that BOTH a brute
78
+ nested-loop AND a "linear-scan a plain active-sessions array" middle-tier
79
+ shortcut fail — only a genuinely sub-linear-per-op structure survives, not
80
+ merely "some awareness of scale."
81
+
82
+ **Round 2** (revised design presented): Codex verdict **converge, with one
83
+ fix** — flagged that because the scale case uses a uniform session
84
+ duration, admission order and expiry order coincide, so a FIFO
85
+ queue-with-front-eviction (simpler than the general answer, and not the
86
+ "real" discriminator intended) would also pass the scale case correctly and
87
+ fast. Fix adopted: added the fourth small-scale correctness sub-trap
88
+ (eviction driven by actual smallest `end` among currently active sessions,
89
+ not admission order) via a hand-authored 3-session case where a FIFO
90
+ shortcut and the correct rule disagree. This closes the loophole without
91
+ touching the scale case's parameters, since the two verifiers are
92
+ independent files/inputs.
93
+
94
+ Codex's own words, R2 answer to Q1: "F36 is much stronger now, but I see one
95
+ real shortcut left: a FIFO/sliding-window queue of admitted sessions can
96
+ pass the scale case if ends are monotonic... Add one small correctness case
97
+ where admitted end order differs from start/admission order... That defeats
98
+ FIFO while keeping the scale discriminator intact." Adopted verbatim as
99
+ correctness sub-trap 4.
@@ -0,0 +1,18 @@
1
+ # F36 retired
2
+
3
+ Retired from the active golden suite after headroom run
4
+ `iter-0041-headroom-f36-f37`.
5
+
6
+ Reason: `solo_claude` scored 96, exceeding the headroom ceiling of 80, and
7
+ timed out doing so (full 1500s budget). bare scored 50 — the bare ceiling
8
+ held (headroom 10), and bare failed on the intended traps (wrong deferred-row
9
+ output shape: reason `capacity`/key `active` instead of
10
+ `over_capacity`/`blocking`, plus a validation miss). But solo's pipeline
11
+ still produced a scalable heap-based implementation that cleared the hidden
12
+ scale verifier, so there is no pair-lift headroom to measure. The
13
+ performance/scale axis alone does not hold the solo pipeline under 80.
14
+
15
+ Future use: the small-scale correctness traps discriminated bare as designed;
16
+ the scale verifier did not discriminate solo. A future rework needs a
17
+ difficulty axis that survives a full PLAN/BUILD_GATE loop, not just a
18
+ single-pass implementation.
@@ -0,0 +1,64 @@
1
+ {
2
+ "verification_commands": [
3
+ {
4
+ "cmd": "node --test tests/cli.test.js",
5
+ "exit_code": 0,
6
+ "stdout_contains": [],
7
+ "stdout_not_contains": ["not ok "]
8
+ },
9
+ {
10
+ "cmd": "node \"$BENCH_FIXTURE_DIR/verifiers/correctness-small.js\"",
11
+ "exit_code": 0,
12
+ "stdout_contains": ["\"ok\":true"],
13
+ "stdout_not_contains": [],
14
+ "contract_refs": [
15
+ "an admitted session whose `end` equals the candidate's `start` has already ended and does not count against it",
16
+ "Sessions become eligible for admission in ascending order of `start`; ties are broken by the session's position in the input `sessions` array",
17
+ "Each deferred row reports `blocking`: the ids of the currently-active admitted sessions preventing its admission, in the order those sessions were themselves admitted"
18
+ ]
19
+ },
20
+ {
21
+ "cmd": "node \"$BENCH_FIXTURE_DIR/verifiers/validation.js\"",
22
+ "exit_code": 0,
23
+ "stdout_contains": ["\"ok\":true"],
24
+ "stdout_not_contains": [],
25
+ "contract_refs": [
26
+ "Validation happens before any admission decision.",
27
+ "a duplicate `id` uses shape `{ \"error\": \"duplicate_session_id\", \"id\": string }`"
28
+ ]
29
+ },
30
+ {
31
+ "cmd": "node \"$BENCH_FIXTURE_DIR/verifiers/scale.js\"",
32
+ "exit_code": 0,
33
+ "stdout_contains": ["\"ok\":true"],
34
+ "stdout_not_contains": [],
35
+ "contract_refs": [
36
+ "`bench-cli admit` must finish processing the 150,000-session verification input (see `## Verification`) within its process timeout, without changing which sessions are admitted or deferred compared to a smaller input processed under the same rule."
37
+ ]
38
+ }
39
+ ],
40
+ "forbidden_patterns": [
41
+ {
42
+ "pattern": "catch\\s*\\([^)]*\\)\\s*\\{[^}]*return\\s+(null|undefined|''|\\{\\})",
43
+ "description": "silent catch returning fallback in admit path",
44
+ "files": ["bin/cli.js"],
45
+ "severity": "disqualifier"
46
+ },
47
+ {
48
+ "pattern": "catch\\s*\\([^)]*\\)\\s*\\{\\s*\\}",
49
+ "description": "empty catch block",
50
+ "files": ["bin/cli.js"],
51
+ "severity": "disqualifier"
52
+ },
53
+ {
54
+ "pattern": "/\\*\\s*eslint-disable",
55
+ "description": "eslint-disable without scoped justification",
56
+ "files": ["bin/cli.js"],
57
+ "severity": "disqualifier"
58
+ }
59
+ ],
60
+ "required_files": ["bin/cli.js", "tests/cli.test.js"],
61
+ "forbidden_files": [],
62
+ "max_deps_added": 0,
63
+ "spec_output_files": ["bin/cli.js", "tests/cli.test.js"]
64
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "id": "F36-cli-session-admission",
3
+ "category": "high-risk",
4
+ "difficulty": "high",
5
+ "timeout_seconds": 1500,
6
+ "required_tools": ["node"],
7
+ "browser": false,
8
+ "deps_change_expected": false,
9
+ "intent": "Add a bench-cli admit command that admits scheduling sessions against a global concurrency capacity in start-time order with file-order tie-break, deferring sessions that would exceed capacity, and that must finish processing a 150,000-session verification input within its process timeout without changing which sessions are admitted or deferred."
10
+ }
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env bash
2
+ # F36 setup — no seed data required; sessions arrive entirely via --input.
3
+ set -e
4
+ exit 0
@@ -0,0 +1,61 @@
1
+ ---
2
+ id: "F36-cli-session-admission"
3
+ title: "Session admission command with a global concurrency cap"
4
+ status: planned
5
+ complexity: high
6
+ depends-on: []
7
+ ---
8
+
9
+ # F36 Session admission command with a global concurrency cap
10
+
11
+ ## Context
12
+
13
+ `bench-cli` gets an `admit` command that decides which of a batch of
14
+ scheduling sessions can run at all, given a single global limit on how many
15
+ sessions may be concurrently active. Downstream capacity-planning tooling
16
+ parses stdout as JSON and replays the verification input at production
17
+ scale, so the decision must be exact and the command must finish in
18
+ reasonable time even on large batches.
19
+
20
+ ## Requirements
21
+
22
+ - [ ] `bench-cli admit --input <path>` reads JSON shaped as `{ "capacity": number, "sessions": [{ "id": string, "start": number, "end": number }] }`.
23
+ - [ ] Sessions become eligible for admission in ascending order of `start`; ties are broken by the session's position in the input `sessions` array (the earlier entry is eligible first).
24
+ - [ ] Sessions are considered for admission one at a time, in that eligibility order. A session is admitted unless, at the moment it is considered, the number of already-admitted sessions whose interval has not yet ended is already equal to `capacity` — in that case it is deferred instead. A session's interval `[start, end)` is half-open: an admitted session whose `end` equals the candidate's `start` has already ended and does not count against it.
25
+ - [ ] Each deferred row reports `blocking`: the ids of the currently-active admitted sessions preventing its admission, in the order those sessions were themselves admitted (not a final or static list, and not the list for any other candidate).
26
+ - [ ] `bench-cli admit` must finish processing the 150,000-session verification input (see `## Verification`) within its process timeout, without changing which sessions are admitted or deferred compared to a smaller input processed under the same rule.
27
+ - [ ] Validation happens before any admission decision. `capacity` must be a positive integer; `sessions` must be an array; every session needs a non-empty string `id`, integer `start`, and integer `end` with `start < end`; every `id` must be unique across the whole input. Invalid input exits `2` and writes exactly one JSON error object to stderr, printing nothing to stdout: a malformed capacity/session field uses shape `{ "error": "invalid_input", "reason": string }`; a duplicate `id` uses shape `{ "error": "duplicate_session_id", "id": string }`.
28
+ - [ ] On success, write exactly one JSON object to stdout and no stderr. Keys: `admitted` (array of admitted session ids, in eligibility order) and `deferred` (array of `{ "id": string, "reason": "over_capacity", "blocking": [string, ...] }` rows, in eligibility order).
29
+ - [ ] `tests/cli.test.js` is updated. Existing tests still pass AND at least two new tests cover `admit`: one where a session listed earlier in the file but starting later must not be admitted ahead of a session listed later but starting earlier, and one deferral.
30
+
31
+ ## Constraints
32
+
33
+ - **No new npm dependencies.**
34
+ - **No silent catches.** If parsing or file reading fails, emit a visible JSON error to stderr and exit `2`.
35
+ - **No extra stdout/stderr text** on the success path; downstream tooling parses stdout as JSON.
36
+ - `data`, `server`, and `web` directories are unrelated to this command; do not add files there.
37
+
38
+ ## Out of Scope
39
+
40
+ - Persisting admission results between command invocations.
41
+ - Retrying, rescheduling, or suggesting alternate times for deferred sessions.
42
+ - Changing server routes, web UI, or any file outside `bin/cli.js` and `tests/cli.test.js`.
43
+ - Touching `server/`, `web/`, or `tests/server.test.js`.
44
+
45
+ ## Verification
46
+
47
+ - `node --test tests/cli.test.js` exits 0.
48
+ - Given three sessions where the second-admitted session's interval ends before the first-admitted session's interval ends, a fourth session starting after the second has ended but before the first has ended is correctly admitted (its admission depends on evicting the second, not the first, from the active set) — `node "$BENCH_FIXTURE_DIR/verifiers/correctness-small.js"` exposes misses on this and the eligibility-order, half-open-boundary, and `blocking` rules above.
49
+ - Malformed input, non-unique ids, and a non-positive capacity are all rejected before any admission decision, per the exact error shapes above — `node "$BENCH_FIXTURE_DIR/verifiers/validation.js"` exposes misses.
50
+ - A 150,000-session input with `capacity` set low relative to heavily overlapping session durations completes within the process timeout and produces the exact admitted/deferred sets — `node "$BENCH_FIXTURE_DIR/verifiers/scale.js"` exposes misses on both timing and correctness.
51
+ - `git diff --stat` shows only `bin/cli.js` and `tests/cli.test.js` touched.
52
+
53
+ ## Solo-headroom hypothesis
54
+
55
+ A capable solo_claude baseline is expected to reach for a data structure
56
+ that is correct on typical hand-written examples but does not stay fast as
57
+ the number of concurrently active sessions grows — for example, keeping
58
+ admitted sessions in a plain list and scanning or filtering it on every new
59
+ candidate — which is correct at small scale but does not finish the
60
+ 150,000-session input in time; observable command
61
+ `node "$BENCH_FIXTURE_DIR/verifiers/scale.js"` exposes the miss.
@@ -0,0 +1,9 @@
1
+ Add an `admit` command to `bench-cli` so users can run `bench-cli admit --input <path>` with a JSON file shaped as `{ "capacity": number, "sessions": [{ "id": string, "start": number, "end": number }] }`. Sessions become eligible for admission in ascending order of `start`; if two sessions share the same `start`, the one listed earlier in the `sessions` array is eligible first.
2
+
3
+ Consider sessions one at a time in that eligibility order. Admit a session unless the number of already-admitted sessions whose interval hasn't ended yet is already equal to `capacity` — in that case defer it instead. Treat intervals as half-open: a session that ends exactly when another starts is not overlapping it. Each deferred session should report which already-admitted sessions are still active at its own turn (their ids, in the order those sessions were admitted) — this can differ across deferred sessions depending on when each one is considered, since a session admitted earlier isn't necessarily still active later.
4
+
5
+ This command needs to stay fast: downstream capacity-planning tooling replays inputs with well over a hundred thousand sessions, and the command must still finish within a reasonable process timeout without changing which sessions get admitted or deferred versus a small input processed the same way.
6
+
7
+ Validate the whole input before making any admission decision: `capacity` must be a positive integer, `sessions` must be an array, every session needs a non-empty string `id` and integer `start`/`end` with `start` less than `end`, and every `id` must be unique. On any violation, exit 2, print exactly one JSON error object to stderr, and print nothing to stdout — use a distinct error for duplicate ids versus other malformed input. On success, print exactly one JSON object with `admitted` (ids in eligibility order) and `deferred` (rows with id/reason/active-count, in eligibility order) — nothing on stderr.
8
+
9
+ Update `tests/cli.test.js`: existing tests still pass, plus at least two new tests for `admit` (one exercising the start-time-vs-file-order tie-break, one deferral). No new npm dependencies. Only touch `bin/cli.js` and `tests/cli.test.js`.