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.
- package/AGENTS.md +4 -9
- package/CLAUDE.md +11 -15
- package/README.md +31 -7
- package/benchmark/auto-resolve/results/20260507-f10-f11-tier1-full-pipeline/headroom-gate.json +25 -0
- package/benchmark/auto-resolve/results/20260507-f10-f11-tier1-full-pipeline/headroom-gate.md +10 -0
- package/benchmark/auto-resolve/results/20260508-f22-exact-error-headroom/headroom-gate.json +18 -0
- package/benchmark/auto-resolve/results/20260508-f22-exact-error-headroom/headroom-gate.md +9 -0
- package/benchmark/auto-resolve/results/20260508-f26-headroom/headroom-gate.json +18 -0
- package/benchmark/auto-resolve/results/20260508-f26-headroom/headroom-gate.md +9 -0
- package/benchmark/auto-resolve/results/20260510-f16-f23-f25-combined-proof/F16-cli-quote-tax-rules/l2_risk_probes/result.json +28 -0
- package/benchmark/auto-resolve/results/20260510-f16-f23-f25-combined-proof/F23-cli-fulfillment-wave/l2_risk_probes/result.json +28 -0
- package/benchmark/auto-resolve/results/20260510-f16-f23-f25-combined-proof/F25-cli-cart-promotion-rules/l2_risk_probes/result.json +30 -0
- package/benchmark/auto-resolve/results/20260511-f12-webhook-headroom/headroom-gate.json +28 -0
- package/benchmark/auto-resolve/results/20260511-f12-webhook-headroom/headroom-gate.md +15 -0
- package/benchmark/auto-resolve/results/20260511-f15-concurrency-headroom/headroom-gate.json +28 -0
- package/benchmark/auto-resolve/results/20260511-f15-concurrency-headroom/headroom-gate.md +15 -0
- package/benchmark/auto-resolve/results/20260511-f21-current-riskprobes-v1/F21-cli-scheduler-priority/l2_risk_probes/result.json +32 -0
- package/benchmark/auto-resolve/results/20260511-f21-current-riskprobes-v1/full-pipeline-pair-gate.json +44 -0
- package/benchmark/auto-resolve/results/20260511-f21-current-riskprobes-v1/full-pipeline-pair-gate.md +16 -0
- package/benchmark/auto-resolve/results/20260511-f21-current-riskprobes-v1/headroom-gate.json +18 -0
- package/benchmark/auto-resolve/results/20260511-f21-current-riskprobes-v1/headroom-gate.md +9 -0
- package/benchmark/auto-resolve/results/20260511-f3-http-error-headroom/headroom-gate.json +28 -0
- package/benchmark/auto-resolve/results/20260511-f3-http-error-headroom/headroom-gate.md +15 -0
- package/benchmark/auto-resolve/results/20260512-f2-medium-headroom/headroom-gate.json +28 -0
- package/benchmark/auto-resolve/results/20260512-f2-medium-headroom/headroom-gate.md +15 -0
- package/benchmark/auto-resolve/results/20260512-f31-seat-rebalance-headroom/headroom-gate.json +28 -0
- package/benchmark/auto-resolve/results/20260512-f31-seat-rebalance-headroom/headroom-gate.md +15 -0
- package/benchmark/auto-resolve/results/20260512-f32-subscription-renewal-headroom/headroom-gate.json +28 -0
- package/benchmark/auto-resolve/results/20260512-f32-subscription-renewal-headroom/headroom-gate.md +15 -0
- package/benchmark/auto-resolve/results/20260512-f4-web-headroom/headroom-gate.json +28 -0
- package/benchmark/auto-resolve/results/20260512-f4-web-headroom/headroom-gate.md +15 -0
- package/benchmark/auto-resolve/results/20260512-f5-fixloop-headroom/headroom-gate.json +28 -0
- package/benchmark/auto-resolve/results/20260512-f5-fixloop-headroom/headroom-gate.md +15 -0
- package/benchmark/auto-resolve/results/20260512-f6-checksum-headroom/headroom-gate.json +28 -0
- package/benchmark/auto-resolve/results/20260512-f6-checksum-headroom/headroom-gate.md +15 -0
- package/benchmark/auto-resolve/results/20260512-f7-scope-headroom/headroom-gate.json +28 -0
- package/benchmark/auto-resolve/results/20260512-f7-scope-headroom/headroom-gate.md +15 -0
- package/benchmark/auto-resolve/results/20260512-f9-e2e-headroom/headroom-gate.json +28 -0
- package/benchmark/auto-resolve/results/20260512-f9-e2e-headroom/headroom-gate.md +15 -0
- package/benchmark/auto-resolve/scripts/run-fixture.sh +11 -1
- package/benchmark/auto-resolve/scripts/test-benchmark-arg-parsing.sh +83 -1
- package/bin/devlyn.js +59 -9
- package/config/skills/_shared/.ruff.toml +2 -0
- package/config/skills/_shared/adapters/claude.md +9 -1
- package/config/skills/_shared/adapters/codex.md +5 -0
- package/config/skills/_shared/adapters/grok.md +37 -13
- package/config/skills/_shared/archive_run.py +738 -56
- package/config/skills/_shared/codex-config.md +19 -4
- package/config/skills/_shared/codex-monitored.sh +81 -12
- package/config/skills/_shared/collect-codex-findings.py +419 -60
- package/config/skills/_shared/engine-preflight.md +8 -7
- package/config/skills/_shared/expected.schema.json +68 -0
- package/config/skills/_shared/finish-gate.py +12 -11
- package/config/skills/_shared/grok-anchor-guard.py +78 -0
- package/config/skills/_shared/invocation-receipt.py +1041 -0
- package/config/skills/_shared/judge-output-parser.py +200 -0
- package/config/skills/_shared/judge-role-evidence.py +273 -0
- package/config/skills/_shared/phase-prompt-render.py +181 -0
- package/config/skills/_shared/process-evidence.py +909 -0
- package/config/skills/_shared/resolve-bootstrap.py +776 -84
- package/config/skills/_shared/resolve-stop-hook.py +33 -1
- package/config/skills/_shared/role-config.py +552 -0
- package/config/skills/_shared/run-bounded.py +1 -1
- package/config/skills/_shared/spec-verify-check.py +1037 -192
- package/config/skills/_shared/state-phase-write.py +2551 -83
- package/config/skills/_shared/terminal-claim-check.py +15 -0
- package/config/skills/_shared/verify-merge-findings.py +1127 -50
- package/config/skills/devlyn:engines/SKILL.md +8 -16
- package/config/skills/devlyn:ideate/SKILL.md +1 -1
- package/config/skills/devlyn:ideate/references/spec-template.md +1 -0
- package/config/skills/devlyn:queue/SKILL.md +7 -4
- package/config/skills/devlyn:resolve/SKILL.md +94 -35
- package/config/skills/devlyn:resolve/references/outer-loop.md +15 -0
- package/config/skills/devlyn:resolve/references/phases/build-gate.md +33 -4
- package/config/skills/devlyn:resolve/references/phases/implement.md +4 -3
- package/config/skills/devlyn:resolve/references/phases/plan.md +1 -5
- package/config/skills/devlyn:resolve/references/phases/probe-derive.md +4 -2
- package/config/skills/devlyn:resolve/references/phases/verify.md +120 -80
- package/config/skills/devlyn:resolve/references/state-schema.md +23 -9
- package/package.json +5 -1
- package/scripts/lint-fixtures.sh +4 -1
- 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
|
-
|
|
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) ==
|
|
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":
|
|
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) ==
|
|
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":
|
|
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) ==
|
|
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":
|
|
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) ==
|
|
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":
|
|
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()
|