devlyn-cli 2.10.0 → 3.0.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 (82) hide show
  1. package/AGENTS.md +4 -9
  2. package/CLAUDE.md +11 -15
  3. package/README.md +31 -7
  4. package/benchmark/auto-resolve/results/20260507-f10-f11-tier1-full-pipeline/headroom-gate.json +25 -0
  5. package/benchmark/auto-resolve/results/20260507-f10-f11-tier1-full-pipeline/headroom-gate.md +10 -0
  6. package/benchmark/auto-resolve/results/20260508-f22-exact-error-headroom/headroom-gate.json +18 -0
  7. package/benchmark/auto-resolve/results/20260508-f22-exact-error-headroom/headroom-gate.md +9 -0
  8. package/benchmark/auto-resolve/results/20260508-f26-headroom/headroom-gate.json +18 -0
  9. package/benchmark/auto-resolve/results/20260508-f26-headroom/headroom-gate.md +9 -0
  10. package/benchmark/auto-resolve/results/20260510-f16-f23-f25-combined-proof/F16-cli-quote-tax-rules/l2_risk_probes/result.json +28 -0
  11. package/benchmark/auto-resolve/results/20260510-f16-f23-f25-combined-proof/F23-cli-fulfillment-wave/l2_risk_probes/result.json +28 -0
  12. package/benchmark/auto-resolve/results/20260510-f16-f23-f25-combined-proof/F25-cli-cart-promotion-rules/l2_risk_probes/result.json +30 -0
  13. package/benchmark/auto-resolve/results/20260511-f12-webhook-headroom/headroom-gate.json +28 -0
  14. package/benchmark/auto-resolve/results/20260511-f12-webhook-headroom/headroom-gate.md +15 -0
  15. package/benchmark/auto-resolve/results/20260511-f15-concurrency-headroom/headroom-gate.json +28 -0
  16. package/benchmark/auto-resolve/results/20260511-f15-concurrency-headroom/headroom-gate.md +15 -0
  17. package/benchmark/auto-resolve/results/20260511-f21-current-riskprobes-v1/F21-cli-scheduler-priority/l2_risk_probes/result.json +32 -0
  18. package/benchmark/auto-resolve/results/20260511-f21-current-riskprobes-v1/full-pipeline-pair-gate.json +44 -0
  19. package/benchmark/auto-resolve/results/20260511-f21-current-riskprobes-v1/full-pipeline-pair-gate.md +16 -0
  20. package/benchmark/auto-resolve/results/20260511-f21-current-riskprobes-v1/headroom-gate.json +18 -0
  21. package/benchmark/auto-resolve/results/20260511-f21-current-riskprobes-v1/headroom-gate.md +9 -0
  22. package/benchmark/auto-resolve/results/20260511-f3-http-error-headroom/headroom-gate.json +28 -0
  23. package/benchmark/auto-resolve/results/20260511-f3-http-error-headroom/headroom-gate.md +15 -0
  24. package/benchmark/auto-resolve/results/20260512-f2-medium-headroom/headroom-gate.json +28 -0
  25. package/benchmark/auto-resolve/results/20260512-f2-medium-headroom/headroom-gate.md +15 -0
  26. package/benchmark/auto-resolve/results/20260512-f31-seat-rebalance-headroom/headroom-gate.json +28 -0
  27. package/benchmark/auto-resolve/results/20260512-f31-seat-rebalance-headroom/headroom-gate.md +15 -0
  28. package/benchmark/auto-resolve/results/20260512-f32-subscription-renewal-headroom/headroom-gate.json +28 -0
  29. package/benchmark/auto-resolve/results/20260512-f32-subscription-renewal-headroom/headroom-gate.md +15 -0
  30. package/benchmark/auto-resolve/results/20260512-f4-web-headroom/headroom-gate.json +28 -0
  31. package/benchmark/auto-resolve/results/20260512-f4-web-headroom/headroom-gate.md +15 -0
  32. package/benchmark/auto-resolve/results/20260512-f5-fixloop-headroom/headroom-gate.json +28 -0
  33. package/benchmark/auto-resolve/results/20260512-f5-fixloop-headroom/headroom-gate.md +15 -0
  34. package/benchmark/auto-resolve/results/20260512-f6-checksum-headroom/headroom-gate.json +28 -0
  35. package/benchmark/auto-resolve/results/20260512-f6-checksum-headroom/headroom-gate.md +15 -0
  36. package/benchmark/auto-resolve/results/20260512-f7-scope-headroom/headroom-gate.json +28 -0
  37. package/benchmark/auto-resolve/results/20260512-f7-scope-headroom/headroom-gate.md +15 -0
  38. package/benchmark/auto-resolve/results/20260512-f9-e2e-headroom/headroom-gate.json +28 -0
  39. package/benchmark/auto-resolve/results/20260512-f9-e2e-headroom/headroom-gate.md +15 -0
  40. package/benchmark/auto-resolve/scripts/run-fixture.sh +11 -1
  41. package/benchmark/auto-resolve/scripts/test-benchmark-arg-parsing.sh +83 -1
  42. package/bin/devlyn.js +59 -9
  43. package/config/skills/_shared/.ruff.toml +2 -0
  44. package/config/skills/_shared/adapters/claude.md +9 -1
  45. package/config/skills/_shared/adapters/codex.md +5 -0
  46. package/config/skills/_shared/adapters/grok.md +37 -13
  47. package/config/skills/_shared/archive_run.py +738 -56
  48. package/config/skills/_shared/codex-config.md +19 -4
  49. package/config/skills/_shared/codex-monitored.sh +81 -12
  50. package/config/skills/_shared/collect-codex-findings.py +419 -60
  51. package/config/skills/_shared/engine-preflight.md +8 -7
  52. package/config/skills/_shared/expected.schema.json +68 -0
  53. package/config/skills/_shared/finish-gate.py +12 -11
  54. package/config/skills/_shared/grok-anchor-guard.py +78 -0
  55. package/config/skills/_shared/invocation-receipt.py +1041 -0
  56. package/config/skills/_shared/judge-output-parser.py +200 -0
  57. package/config/skills/_shared/judge-role-evidence.py +273 -0
  58. package/config/skills/_shared/phase-prompt-render.py +181 -0
  59. package/config/skills/_shared/process-evidence.py +909 -0
  60. package/config/skills/_shared/resolve-bootstrap.py +776 -84
  61. package/config/skills/_shared/resolve-stop-hook.py +33 -1
  62. package/config/skills/_shared/role-config.py +552 -0
  63. package/config/skills/_shared/run-bounded.py +1 -1
  64. package/config/skills/_shared/spec-verify-check.py +1037 -192
  65. package/config/skills/_shared/state-phase-write.py +2551 -83
  66. package/config/skills/_shared/terminal-claim-check.py +15 -0
  67. package/config/skills/_shared/verify-merge-findings.py +1127 -50
  68. package/config/skills/devlyn:engines/SKILL.md +8 -16
  69. package/config/skills/devlyn:ideate/SKILL.md +1 -1
  70. package/config/skills/devlyn:ideate/references/spec-template.md +1 -0
  71. package/config/skills/devlyn:queue/SKILL.md +7 -4
  72. package/config/skills/devlyn:resolve/SKILL.md +94 -35
  73. package/config/skills/devlyn:resolve/references/outer-loop.md +15 -0
  74. package/config/skills/devlyn:resolve/references/phases/build-gate.md +33 -4
  75. package/config/skills/devlyn:resolve/references/phases/implement.md +4 -3
  76. package/config/skills/devlyn:resolve/references/phases/plan.md +1 -5
  77. package/config/skills/devlyn:resolve/references/phases/probe-derive.md +4 -2
  78. package/config/skills/devlyn:resolve/references/phases/verify.md +120 -80
  79. package/config/skills/devlyn:resolve/references/state-schema.md +23 -9
  80. package/package.json +5 -1
  81. package/scripts/lint-fixtures.sh +4 -1
  82. package/scripts/lint-skills.sh +447 -290
@@ -6,6 +6,67 @@
6
6
  "type": "object",
7
7
  "additionalProperties": false,
8
8
  "properties": {
9
+ "process_evidence": {
10
+ "type": "array",
11
+ "description": "Declared process obligations captured during implementation. Each id is unique within its phase; the shared process-evidence runner records raw streams and validates the expectation before IMPLEMENT can pass.",
12
+ "items": {
13
+ "type": "object",
14
+ "additionalProperties": false,
15
+ "required": ["id", "phase"],
16
+ "oneOf": [
17
+ { "required": ["cmd"], "not": { "required": ["argv"] } },
18
+ { "required": ["argv"], "not": { "required": ["cmd"] } }
19
+ ],
20
+ "properties": {
21
+ "id": {
22
+ "type": "string",
23
+ "pattern": "^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$",
24
+ "description": "Stable evidence id, unique within its phase and safe as one path component."
25
+ },
26
+ "phase": {
27
+ "const": "implement",
28
+ "description": "The phase that must produce this expected-contract obligation."
29
+ },
30
+ "cmd": {
31
+ "type": "string",
32
+ "minLength": 1,
33
+ "description": "Exact shell command executed by the shared runner."
34
+ },
35
+ "argv": {
36
+ "type": "array",
37
+ "minItems": 1,
38
+ "items": { "type": "string" },
39
+ "description": "Exact argv executed without a shell; argv[0] must be non-empty."
40
+ },
41
+ "exit_code": {
42
+ "type": "integer",
43
+ "minimum": 0,
44
+ "description": "Required exit code. Default 0 if omitted.",
45
+ "default": 0
46
+ },
47
+ "timeout_sec": {
48
+ "type": "integer",
49
+ "minimum": 1,
50
+ "maximum": 600,
51
+ "description": "Per-command timeout in seconds. Default 60 if omitted.",
52
+ "default": 60
53
+ },
54
+ "stdout_contains": {
55
+ "type": "array",
56
+ "items": { "type": "string", "minLength": 1 },
57
+ "description": "Each UTF-8 substring must occur in the concatenated raw stdout and stderr bytes.",
58
+ "default": []
59
+ },
60
+ "stdout_not_contains": {
61
+ "type": "array",
62
+ "items": { "type": "string", "minLength": 1 },
63
+ "description": "No UTF-8 substring may occur in the concatenated raw stdout and stderr bytes.",
64
+ "default": []
65
+ }
66
+ }
67
+ },
68
+ "default": []
69
+ },
9
70
  "verification_commands": {
10
71
  "type": "array",
11
72
  "description": "Each command is executed against the post-BUILD code. Each pass/fail contributes to verify_score. At least one entry is required when the spec has any observable runtime check (CLI, test command, HTTP request).",
@@ -24,6 +85,13 @@
24
85
  "description": "Required exit code. Default 0 if omitted.",
25
86
  "default": 0
26
87
  },
88
+ "timeout_sec": {
89
+ "type": "integer",
90
+ "minimum": 1,
91
+ "maximum": 600,
92
+ "description": "Per-command timeout in seconds. Default 60 if omitted; maximum 600.",
93
+ "default": 60
94
+ },
27
95
  "stdout_contains": {
28
96
  "type": "array",
29
97
  "description": "Each substring must appear verbatim in (stdout + stderr) for pass.",
@@ -277,13 +277,11 @@ def run_gate(work: pathlib.Path, devlyn_dir: pathlib.Path) -> int:
277
277
  return 0
278
278
 
279
279
  findings: list[dict] = []
280
- failed = False
281
280
  reverted = 0
282
281
  revert_failed = 0
283
282
  for seq, path in enumerate(offenders, start=1):
284
283
  ok, detail, existed_at_base = revert_offender(work, base_sha, path)
285
284
  status = "reverted" if ok else "revert-failed"
286
- failed = failed or not ok
287
285
  if ok:
288
286
  reverted += 1
289
287
  else:
@@ -305,7 +303,10 @@ def run_gate(work: pathlib.Path, devlyn_dir: pathlib.Path) -> int:
305
303
  ),
306
304
  ))
307
305
  write_findings(devlyn_dir, findings)
308
- exit_code = 2 if failed else 0
306
+ # Exit 0 let a real run treat reverted orchestrator commits as a clean pass.
307
+ # Any offender is therefore unclean even when every automatic revert succeeds;
308
+ # the caller maps exit 1/2 to BLOCKED:finish-gate-unclean.
309
+ exit_code = 2
309
310
  write_summary(devlyn_dir, {
310
311
  "mode": state.get("mode"),
311
312
  "checked": len(checked),
@@ -386,25 +387,25 @@ def self_test() -> int:
386
387
 
387
388
  work, devlyn, _base = make_fixture(root, "tracked-mutation")
388
389
  write_text(work / "notes.txt", "polluted\n")
389
- assert checked_run_gate(work, devlyn) == 0
390
+ assert checked_run_gate(work, devlyn) == 2
390
391
  assert (work / "notes.txt").read_text(encoding="utf-8") == "base notes\n"
391
392
  findings = read_findings(devlyn)
392
393
  assert findings[0]["rule_id"] == "scope.finish-unaudited-file"
393
394
  assert findings[0]["status"] == "reverted"
394
395
  assert_summary(devlyn, {
395
396
  "mode": "full", "checked": 1, "offenders": 1,
396
- "reverted": 1, "revert_failed": 0, "exit": 0,
397
+ "reverted": 1, "revert_failed": 0, "exit": 2,
397
398
  })
398
399
 
399
400
  work, devlyn, _base = make_fixture(root, "added-file")
400
401
  write_text(work / "runtime.txt", "late\n")
401
402
  git_check(work, "add", "runtime.txt")
402
- assert checked_run_gate(work, devlyn) == 0
403
+ assert checked_run_gate(work, devlyn) == 2
403
404
  assert not (work / "runtime.txt").exists()
404
405
  assert "runtime.txt" not in git_check(work, "diff", "--name-only", _base).splitlines()
405
406
  assert_summary(devlyn, {
406
407
  "mode": "full", "checked": 1, "offenders": 1,
407
- "reverted": 1, "revert_failed": 0, "exit": 0,
408
+ "reverted": 1, "revert_failed": 0, "exit": 2,
408
409
  })
409
410
 
410
411
  work, devlyn, _base = make_fixture(root, "devlyn-owned-tracked-mutation")
@@ -414,7 +415,7 @@ def self_test() -> int:
414
415
  git_check(work, "-c", "user.email=t@t", "-c", "user.name=t", "commit", "-q", "-m", "track devlyn state")
415
416
  write_text(archived, "mutated devlyn state\n")
416
417
  write_text(work / "notes.txt", "polluted\n")
417
- assert checked_run_gate(work, devlyn) == 0
418
+ assert checked_run_gate(work, devlyn) == 2
418
419
  assert archived.read_text(encoding="utf-8") == "mutated devlyn state\n"
419
420
  assert (work / "notes.txt").read_text(encoding="utf-8") == "base notes\n"
420
421
  findings = read_findings(devlyn)
@@ -422,7 +423,7 @@ def self_test() -> int:
422
423
  assert findings[0]["status"] == "reverted"
423
424
  assert_summary(devlyn, {
424
425
  "mode": "full", "checked": 1, "offenders": 1,
425
- "reverted": 1, "revert_failed": 0, "exit": 0,
426
+ "reverted": 1, "revert_failed": 0, "exit": 2,
426
427
  })
427
428
 
428
429
  work, devlyn, base = make_fixture(root, "cleanup-window")
@@ -469,12 +470,12 @@ def self_test() -> int:
469
470
 
470
471
  work, devlyn, _base = make_fixture(root, "deleted-file")
471
472
  (work / "notes.txt").unlink()
472
- assert checked_run_gate(work, devlyn) == 0
473
+ assert checked_run_gate(work, devlyn) == 2
473
474
  assert (work / "notes.txt").read_text(encoding="utf-8") == "base notes\n"
474
475
  assert read_findings(devlyn)[0]["status"] == "reverted"
475
476
  assert_summary(devlyn, {
476
477
  "mode": "full", "checked": 1, "offenders": 1,
477
- "reverted": 1, "revert_failed": 0, "exit": 0,
478
+ "reverted": 1, "revert_failed": 0, "exit": 2,
478
479
  })
479
480
 
480
481
  work, devlyn, _base = make_fixture(root, "git-diff-failure")
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env python3
2
+ """Deny pair-judge shell outside the probe anchor, with a model-readable reason.
3
+
4
+ Registered as a native grok `PreToolUse` hook. It fires at step 1 of the
5
+ authorization pipeline, before the permission rules and before the mode policy
6
+ whose auto-deny silently terminates the review at exit 0 (iter-0081).
7
+
8
+ The anchor is supplied via DEVLYN_PROBE_ANCHOR, derived by the orchestrator from
9
+ the same source as the --allow rule.
10
+
11
+ Measured (iter-0081 M-C6 v2, external isolation, admissible cell): Line A 6/6 —
12
+ the denial arrives as a model-visible tool_result, the judge re-routes to
13
+ read_file, the anchor still executes inside the grok process, EndTurn with zero
14
+ PermissionCancelled. Line A-chain 1/1 — `<anchor> && <write>` is denied
15
+ model-visibly, neither segment executes, and the judge then runs the bare anchor.
16
+ """
17
+ import json
18
+ import os
19
+ import sys
20
+
21
+ REASON = (
22
+ "DEVLYN-0081-HOOK-DENY: this shell command is outside the permitted probe "
23
+ "anchor. Use the read_file tool for file reads. The only permitted shell "
24
+ "command is the anchor named in your prompt, unchained."
25
+ )
26
+
27
+ SHELL_TOOLS = {"run_terminal_command", "run_terminal_cmd", "Bash"}
28
+
29
+ # A conservative, quote-OBLIVIOUS lexical veto — deliberately not a shell parser.
30
+ # It rejects these characters even inside quotes, over-denying rather than ever
31
+ # under-denying a chain. Adding quote-, escape-, or operator-aware exceptions is
32
+ # how this becomes a parser; that is a registered falsifier, not a polish.
33
+ FORBIDDEN = set(";&|`$()<>") | {"\n", "\r"}
34
+
35
+
36
+ def reject_json_constant(token):
37
+ raise ValueError(f"invalid JSON numeric constant: {token}")
38
+
39
+
40
+ def reject_duplicate_keys(pairs):
41
+ result = {}
42
+ for key, value in pairs:
43
+ if key in result:
44
+ raise ValueError(f"duplicate JSON key: {key}")
45
+ result[key] = value
46
+ return result
47
+
48
+
49
+ def main():
50
+ anchor = os.environ.get("DEVLYN_PROBE_ANCHOR", "")
51
+ payload = json.load(
52
+ sys.stdin,
53
+ parse_constant=reject_json_constant,
54
+ object_pairs_hook=reject_duplicate_keys,
55
+ )
56
+
57
+ if payload.get("toolName") not in SHELL_TOOLS or not anchor:
58
+ print(json.dumps({"decision": "allow"}))
59
+ return
60
+
61
+ # The anchor, or the anchor plus argv, and nothing that could start a second
62
+ # command. A bare prefix rule would allow `<anchor> && <other>` — the vendor's
63
+ # own documented footgun. Exact match would instead deny `<anchor> --n 5`,
64
+ # mechanically destroying the "bounded input variations" that verify.md
65
+ # requires of every engine. Both were rejected by both seats (iter-0081 v2).
66
+ command = (payload.get("toolInput") or {}).get("command", "").strip()
67
+ if command == anchor or (
68
+ command.startswith(anchor + " ")
69
+ and not any(ch in FORBIDDEN for ch in command)
70
+ ):
71
+ print(json.dumps({"decision": "allow"}))
72
+ return
73
+
74
+ print(json.dumps({"decision": "deny", "reason": REASON}))
75
+
76
+
77
+ if __name__ == "__main__":
78
+ main()