devlyn-cli 3.1.2 → 3.1.3
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 +6 -6
- package/CLAUDE.md +7 -7
- package/README.md +20 -5
- package/bin/devlyn.js +10 -52
- package/bin/instruction-templates.json +52 -0
- package/bin/instructions.js +137 -0
- package/config/skills/_shared/adapters/claude.md +1 -5
- package/config/skills/_shared/adapters/codex.md +1 -5
- package/config/skills/_shared/adapters/omp.md +0 -4
- package/config/skills/_shared/expected.schema.json +2 -2
- package/config/skills/_shared/platform-support.py +4 -1
- package/config/skills/_shared/resolve-bootstrap.py +22 -2
- package/config/skills/_shared/spec-verify-check.py +424 -97
- package/config/skills/_shared/state-phase-write.py +202 -32
- package/config/skills/_shared/task-complete.py +9 -1
- package/config/skills/devlyn:ideate/SKILL.md +1 -1
- package/config/skills/devlyn:ideate/references/from-spec-mode.md +1 -1
- package/config/skills/devlyn:ideate/references/spec-template.md +2 -2
- package/config/skills/devlyn:resolve/SKILL.md +4 -4
- package/config/skills/devlyn:resolve/references/free-form-mode.md +2 -2
- package/config/skills/devlyn:resolve/references/phases/build-gate.md +3 -2
- package/config/skills/devlyn:resolve/references/state-schema.md +1 -1
- package/package.json +1 -1
- package/scripts/lint-skills.sh +2 -2
|
@@ -308,6 +308,36 @@ def bind_process_evidence(
|
|
|
308
308
|
raise runner.EvidenceError(
|
|
309
309
|
"spec-verify.results.json commands must be an array"
|
|
310
310
|
)
|
|
311
|
+
preflight_failure = "preflight_failure" in results
|
|
312
|
+
if preflight_failure:
|
|
313
|
+
failure = results["preflight_failure"]
|
|
314
|
+
if (
|
|
315
|
+
not isinstance(failure, dict)
|
|
316
|
+
or set(failure) != {"run_id", "phase", "round", "findings"}
|
|
317
|
+
or failure["run_id"] != state.get("run_id")
|
|
318
|
+
or failure["phase"] != phase
|
|
319
|
+
or type(failure["round"]) is not int
|
|
320
|
+
or failure["round"] != runner.phase_round(state, phase)
|
|
321
|
+
):
|
|
322
|
+
raise runner.EvidenceError("BUILD_GATE preflight failure identity is invalid")
|
|
323
|
+
record = failure["findings"]
|
|
324
|
+
if not isinstance(record, dict) or set(record) != {"path", "sha256"}:
|
|
325
|
+
raise runner.EvidenceError("BUILD_GATE preflight failure findings binding is invalid")
|
|
326
|
+
path = runner._checked_file(work, record["path"], "preflight failure findings")
|
|
327
|
+
raw = path.read_bytes()
|
|
328
|
+
if hashlib.sha256(raw).hexdigest() != record["sha256"]:
|
|
329
|
+
raise runner.EvidenceError("BUILD_GATE preflight failure findings digest mismatch")
|
|
330
|
+
findings = [loads_strict_json(line) for line in raw.decode("utf-8").splitlines() if line.strip()]
|
|
331
|
+
if len(findings) != 1 or not isinstance(findings[0], dict) or (
|
|
332
|
+
findings[0].get("phase") != phase
|
|
333
|
+
or findings[0].get("severity") != "CRITICAL"
|
|
334
|
+
or findings[0].get("rule_id") not in {
|
|
335
|
+
"correctness.spec-verify-malformed", "correctness.risk-probe-integrity",
|
|
336
|
+
}
|
|
337
|
+
):
|
|
338
|
+
raise runner.EvidenceError("BUILD_GATE preflight failure requires its CRITICAL finding")
|
|
339
|
+
if verdict not in {"FAIL", "BLOCKED"}:
|
|
340
|
+
raise runner.EvidenceError("BUILD_GATE preflight failure requires FAIL or BLOCKED verdict")
|
|
311
341
|
carrier = results.get("process_evidence")
|
|
312
342
|
if carrier is None:
|
|
313
343
|
if commands:
|
|
@@ -315,43 +345,42 @@ def bind_process_evidence(
|
|
|
315
345
|
"BUILD_GATE commands exist without a process-evidence carrier"
|
|
316
346
|
)
|
|
317
347
|
manifest_path = work / runner.manifest_relative_path(state, phase)
|
|
318
|
-
if
|
|
348
|
+
if os.path.lexists(manifest_path):
|
|
319
349
|
raise runner.EvidenceError(
|
|
320
350
|
"BUILD_GATE manifest exists without a process-evidence carrier"
|
|
321
351
|
)
|
|
322
|
-
if runner.mechanical_evidence_required(work, state):
|
|
352
|
+
if not preflight_failure and runner.mechanical_evidence_required(work, state):
|
|
323
353
|
raise runner.EvidenceError(
|
|
324
354
|
"required BUILD_GATE evidence has no process-evidence carrier"
|
|
325
355
|
)
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
)
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
)
|
|
356
|
+
else:
|
|
357
|
+
round_ = runner.phase_round(state, phase)
|
|
358
|
+
manifest = carrier.get("manifest") if isinstance(carrier, dict) else None
|
|
359
|
+
if (
|
|
360
|
+
not isinstance(carrier, dict)
|
|
361
|
+
or carrier.get("phase") != phase
|
|
362
|
+
or carrier.get("round") != round_
|
|
363
|
+
or not isinstance(manifest, dict)
|
|
364
|
+
or manifest.get("path") != runner.manifest_relative_path(state, phase)
|
|
365
|
+
):
|
|
366
|
+
raise runner.EvidenceError(
|
|
367
|
+
"BUILD_GATE process-evidence carrier does not match the active run/round"
|
|
368
|
+
)
|
|
369
|
+
outcome = runner.validate_summary_commands(work, commands, carrier)
|
|
370
|
+
if outcome["verdict"] == "BLOCKED" and verdict != "BLOCKED":
|
|
371
|
+
denial = outcome["capability_denials"][0]
|
|
372
|
+
raise runner.EvidenceError(
|
|
373
|
+
"BUILD_GATE capability denial requires BLOCKED verdict: "
|
|
374
|
+
f"{denial['id']}:{denial['operation']}"
|
|
375
|
+
)
|
|
376
|
+
if (
|
|
377
|
+
outcome["verdict"] == "NEEDS_WORK"
|
|
378
|
+
and verdict in {"PASS", "PASS_WITH_ISSUES"}
|
|
379
|
+
):
|
|
380
|
+
raise runner.EvidenceError(
|
|
381
|
+
"BUILD_GATE process evidence mismatch cannot complete as "
|
|
382
|
+
f"{verdict}: {','.join(outcome['failed_ids'])}"
|
|
383
|
+
)
|
|
355
384
|
except (runner.EvidenceError, OSError, UnicodeError, ValueError) as exc:
|
|
356
385
|
raise SystemExit(f"BLOCKED:process-evidence-invalid: {exc}") from exc
|
|
357
386
|
existing = state.get("process_evidence")
|
|
@@ -362,8 +391,11 @@ def bind_process_evidence(
|
|
|
362
391
|
try:
|
|
363
392
|
for prior in existing:
|
|
364
393
|
runner.validate_bound_carrier(work, prior)
|
|
365
|
-
except runner.EvidenceError as exc:
|
|
394
|
+
except (runner.EvidenceError, OSError, UnicodeError, ValueError) as exc:
|
|
366
395
|
raise SystemExit(f"BLOCKED:process-evidence-invalid: {exc}") from exc
|
|
396
|
+
if carrier is None:
|
|
397
|
+
state.setdefault("process_evidence", None)
|
|
398
|
+
return
|
|
367
399
|
if any(
|
|
368
400
|
isinstance(item, dict)
|
|
369
401
|
and item.get("phase") == carrier["phase"]
|
|
@@ -2996,6 +3028,144 @@ def self_test() -> int:
|
|
|
2996
3028
|
|
|
2997
3029
|
test_interrupted_build_gate()
|
|
2998
3030
|
|
|
3031
|
+
def test_preflight_build_gate() -> None:
|
|
3032
|
+
work = (devlyn / "preflight-build-gate").resolve()
|
|
3033
|
+
active = work / ".devlyn"
|
|
3034
|
+
active.mkdir(parents=True)
|
|
3035
|
+
state_file = active / "pipeline.state.json"
|
|
3036
|
+
results_file = active / "spec-verify.results.json"
|
|
3037
|
+
checker = pathlib.Path(__file__).with_name("spec-verify-check.py")
|
|
3038
|
+
env = {key: value for key, value in os.environ.items() if key not in {
|
|
3039
|
+
"BENCH_WORKDIR", "SPEC_VERIFY_PHASE", "SPEC_VERIFY_FINDINGS_FILE",
|
|
3040
|
+
"SPEC_VERIFY_FINDING_PREFIX",
|
|
3041
|
+
} and not key.startswith("DEVLYN_INVOCATION_")}
|
|
3042
|
+
runner = process_evidence_module()
|
|
3043
|
+
|
|
3044
|
+
def complete(verdict: str) -> subprocess.CompletedProcess[str]:
|
|
3045
|
+
return subprocess.run(
|
|
3046
|
+
[sys.executable, str(pathlib.Path(__file__).resolve()),
|
|
3047
|
+
"--devlyn-dir", ".devlyn", "--phase", "build_gate", "complete",
|
|
3048
|
+
"--verdict", verdict, "--findings-file", ".devlyn/spec-verify-findings.jsonl"],
|
|
3049
|
+
cwd=work, env=env, capture_output=True, text=True, encoding="utf-8",
|
|
3050
|
+
)
|
|
3051
|
+
|
|
3052
|
+
history = []
|
|
3053
|
+
for round_, scenario in enumerate(("fresh", "language", "stale", "denied")):
|
|
3054
|
+
fixture = {
|
|
3055
|
+
"version": "3.0", "run_id": "rs-preflight-build-gate",
|
|
3056
|
+
"source": {"type": "generated", "criteria_path": ".devlyn/criteria.generated.md"},
|
|
3057
|
+
"phases": {"build_gate": None}, "process_evidence": copy.deepcopy(history) or None,
|
|
3058
|
+
}
|
|
3059
|
+
do_spawn(fixture, "build_gate", round_, None, None, "claude", None)
|
|
3060
|
+
write_state(state_file, fixture)
|
|
3061
|
+
original_state = state_file.read_bytes()
|
|
3062
|
+
results_file.unlink(missing_ok=True)
|
|
3063
|
+
relative = runner.manifest_relative_path(fixture, "build_gate")
|
|
3064
|
+
manifest = work / relative
|
|
3065
|
+
obligation = {"id": "type-check", "phase": "build_gate",
|
|
3066
|
+
"argv": [sys.executable, "-c", "raise SystemExit(7)"]}
|
|
3067
|
+
if scenario == "language":
|
|
3068
|
+
runner.capture_process(work, manifest, fixture["run_id"], "build_gate", round_, obligation)
|
|
3069
|
+
elif scenario == "denied":
|
|
3070
|
+
runner.record_capability_denial(
|
|
3071
|
+
work, manifest, fixture["run_id"], "build_gate", round_, obligation,
|
|
3072
|
+
"subprocess", b"route denied subprocess",
|
|
3073
|
+
)
|
|
3074
|
+
elif scenario == "stale":
|
|
3075
|
+
results_file.write_bytes(prior_results)
|
|
3076
|
+
observed = {path: path.read_bytes() for path in manifest.parent.iterdir()} if manifest.exists() else {}
|
|
3077
|
+
rejected = subprocess.run(
|
|
3078
|
+
[sys.executable, str(checker), "--include-risk-probes"],
|
|
3079
|
+
cwd=work, env=env, capture_output=True, text=True, encoding="utf-8",
|
|
3080
|
+
)
|
|
3081
|
+
assert rejected.returncode == 1 and "source.criteria_path" in rejected.stderr, rejected.stderr
|
|
3082
|
+
results = loads_strict_json(results_file.read_text(encoding="utf-8"))
|
|
3083
|
+
assert results["preflight_failure"]["round"] == round_
|
|
3084
|
+
prior_results = results_file.read_bytes()
|
|
3085
|
+
finding = active / "spec-verify-findings.jsonl"
|
|
3086
|
+
finding_bytes = finding.read_bytes()
|
|
3087
|
+
for verdict in ("PASS", "PASS_WITH_ISSUES", "NEEDS_WORK"):
|
|
3088
|
+
denied = complete(verdict)
|
|
3089
|
+
assert denied.returncode != 0 and "preflight failure requires" in denied.stderr, denied.stderr
|
|
3090
|
+
assert state_file.read_bytes() == original_state
|
|
3091
|
+
if scenario == "denied":
|
|
3092
|
+
denied = complete("FAIL")
|
|
3093
|
+
assert denied.returncode != 0 and "capability denial requires BLOCKED" in denied.stderr
|
|
3094
|
+
assert state_file.read_bytes() == original_state
|
|
3095
|
+
completed = complete("BLOCKED" if scenario == "denied" else "FAIL")
|
|
3096
|
+
assert completed.returncode == 0, completed.stderr
|
|
3097
|
+
bound = read_state(state_file)
|
|
3098
|
+
assert bound["phases"]["build_gate"]["verdict"] == ("BLOCKED" if scenario == "denied" else "FAIL")
|
|
3099
|
+
assert finding.read_bytes() == finding_bytes and results_file.read_bytes() == prior_results
|
|
3100
|
+
assert {path: path.read_bytes() for path in observed} == observed
|
|
3101
|
+
if observed:
|
|
3102
|
+
assert bound["process_evidence"] == [*history, results["process_evidence"]]
|
|
3103
|
+
else:
|
|
3104
|
+
assert bound["process_evidence"] == (history or None) and results["commands"] == []
|
|
3105
|
+
history = bound["process_evidence"] or []
|
|
3106
|
+
for mutation in ("run", "phase", "round", "bool-round", "digest", "escape", "finding", "null"):
|
|
3107
|
+
state_file.write_bytes(original_state)
|
|
3108
|
+
altered = copy.deepcopy(results)
|
|
3109
|
+
failure = altered["preflight_failure"]
|
|
3110
|
+
if mutation in {"run", "phase", "round", "bool-round"}:
|
|
3111
|
+
field, value = {"run": ("run_id", "rs-other"), "phase": ("phase", "verify"),
|
|
3112
|
+
"round": ("round", round_ + 1), "bool-round": ("round", True)}[mutation]
|
|
3113
|
+
failure[field] = value
|
|
3114
|
+
elif mutation == "null":
|
|
3115
|
+
altered["preflight_failure"] = None
|
|
3116
|
+
elif mutation == "finding":
|
|
3117
|
+
finding.write_bytes(b"{}\n")
|
|
3118
|
+
failure["findings"]["sha256"] = hashlib.sha256(finding.read_bytes()).hexdigest()
|
|
3119
|
+
else:
|
|
3120
|
+
failure["findings"]["path" if mutation == "escape" else "sha256"] = (
|
|
3121
|
+
"../outside.jsonl" if mutation == "escape" else "0" * 64
|
|
3122
|
+
)
|
|
3123
|
+
results_file.write_text(json.dumps(altered), encoding="utf-8")
|
|
3124
|
+
result = complete("FAIL")
|
|
3125
|
+
assert result.returncode != 0 and "BLOCKED:process-evidence-invalid" in result.stderr, (mutation, result.stderr)
|
|
3126
|
+
assert state_file.read_bytes() == original_state
|
|
3127
|
+
finding.write_bytes(finding_bytes)
|
|
3128
|
+
results_file.write_bytes(prior_results)
|
|
3129
|
+
if scenario in {"fresh", "stale"}:
|
|
3130
|
+
state_file.write_bytes(original_state)
|
|
3131
|
+
malformed_prior = read_state(state_file)
|
|
3132
|
+
malformed_prior["process_evidence"] = {}
|
|
3133
|
+
write_state(state_file, malformed_prior)
|
|
3134
|
+
before = state_file.read_bytes()
|
|
3135
|
+
invalid = complete("FAIL")
|
|
3136
|
+
assert invalid.returncode != 0 and "null or an array" in invalid.stderr, invalid.stderr
|
|
3137
|
+
assert state_file.read_bytes() == before
|
|
3138
|
+
state_file.write_bytes(original_state)
|
|
3139
|
+
if scenario == "stale":
|
|
3140
|
+
prior_stream = work / history[0]["streams"][0]["stdout"]["path"]
|
|
3141
|
+
saved = prior_stream.read_bytes()
|
|
3142
|
+
prior_stream.write_bytes(b"altered prior observation")
|
|
3143
|
+
invalid = complete("FAIL")
|
|
3144
|
+
assert invalid.returncode != 0 and "mismatch" in invalid.stderr, invalid.stderr
|
|
3145
|
+
assert state_file.read_bytes() == original_state
|
|
3146
|
+
prior_stream.write_bytes(saved)
|
|
3147
|
+
from unittest.mock import patch
|
|
3148
|
+
read_bytes = pathlib.Path.read_bytes
|
|
3149
|
+
|
|
3150
|
+
def unreadable_prior(path):
|
|
3151
|
+
if path == prior_stream:
|
|
3152
|
+
raise PermissionError("prior stream unreadable")
|
|
3153
|
+
return read_bytes(path)
|
|
3154
|
+
|
|
3155
|
+
fixture = read_state(state_file)
|
|
3156
|
+
unchanged = copy.deepcopy(fixture)
|
|
3157
|
+
with patch.object(pathlib.Path, "read_bytes", unreadable_prior):
|
|
3158
|
+
try:
|
|
3159
|
+
bind_process_evidence(fixture, "build_gate", "FAIL", active, work)
|
|
3160
|
+
except SystemExit as exc:
|
|
3161
|
+
assert str(exc) == "BLOCKED:process-evidence-invalid: prior stream unreadable", exc
|
|
3162
|
+
else:
|
|
3163
|
+
raise AssertionError("unreadable prior evidence completed")
|
|
3164
|
+
assert fixture == unchanged and state_file.read_bytes() == original_state
|
|
3165
|
+
print("PASS preflight rejection: actual checker-to-completion, success floors, identity/digest controls")
|
|
3166
|
+
|
|
3167
|
+
test_preflight_build_gate()
|
|
3168
|
+
|
|
2999
3169
|
# complete() before spawn() must fail loudly, not silently invent data.
|
|
3000
3170
|
write_state(state_path, {"phases": {}})
|
|
3001
3171
|
state = read_state(state_path)
|
|
@@ -779,6 +779,14 @@ else:
|
|
|
779
779
|
raise SystemExit('unexpected gh arguments: '+str(a))
|
|
780
780
|
'''
|
|
781
781
|
|
|
782
|
+
GIT_DISPATCH = r'''#!/bin/sh
|
|
783
|
+
for arg do
|
|
784
|
+
case "$arg" in push|fetch|ls-remote|remove) exec python3 "$0.py" "$@";; esac
|
|
785
|
+
done
|
|
786
|
+
exec "$REAL_GIT" "$@"
|
|
787
|
+
'''
|
|
788
|
+
|
|
789
|
+
|
|
782
790
|
GIT_WRAPPER = r'''#!/usr/bin/env python3
|
|
783
791
|
import json, os, pathlib, subprocess, sys
|
|
784
792
|
a = sys.argv[1:]
|
|
@@ -822,7 +830,7 @@ class CompletionTests(unittest.TestCase):
|
|
|
822
830
|
for key in list(self.env):
|
|
823
831
|
if key in {"GIT_DIR", "GIT_WORK_TREE", "GIT_INDEX_FILE", "GIT_COMMON_DIR"}:
|
|
824
832
|
del self.env[key]
|
|
825
|
-
for name, body in (("gh", FAKE_GH), ("git", GIT_WRAPPER)):
|
|
833
|
+
for name, body in (("gh", FAKE_GH), ("git.py", GIT_WRAPPER), ("git", GIT_DISPATCH)):
|
|
826
834
|
path = self.bin / name
|
|
827
835
|
path.write_text(body, encoding="utf-8")
|
|
828
836
|
path.chmod(0o755)
|
|
@@ -110,7 +110,7 @@ Structural lint (inline check, no script needed):
|
|
|
110
110
|
After lint passes:
|
|
111
111
|
1. Write `<spec-dir>/<id>-<slug>/spec.md` (the spec).
|
|
112
112
|
2. Generate `<spec-dir>/<id>-<slug>/spec.expected.json` following `references/spec-template.md` § "Sibling file: `spec.expected.json`" for constraint coverage, diff scope and guard controls.
|
|
113
|
-
3. Run `python3 "$DEVLYN_SHARED_DIR/spec-verify-check.py" --check <spec-path>` to validate the
|
|
113
|
+
3. Run `python3 "$DEVLYN_SHARED_DIR/spec-verify-check.py" --check <spec-path>` to validate the actual spec's carrier shape and supported `complexity` frontmatter. Sibling `spec.expected.json` takes precedence over the legacy inline `## Verification` JSON carrier; any present actionable solo-headroom hypothesis command must match that carrier's `verification_commands[].cmd`. If exit 2, fix the file named in the error and re-run.
|
|
114
114
|
4. Run `python3 "$DEVLYN_SHARED_DIR/spec-verify-check.py" --check-expected <expected-path>` to validate sibling `spec.expected.json` against `_shared/expected.schema.json` plus sibling spec `complexity` frontmatter and any present actionable solo-headroom hypothesis; if the spec has a solo-headroom hypothesis, its observable command must match `spec.expected.json.verification_commands[].cmd`. If exit 2, fix the JSON/frontmatter/hypothesis and re-run.
|
|
115
115
|
5. Print: `spec ready — /devlyn:resolve --spec <spec-path>`.
|
|
116
116
|
|
|
@@ -37,7 +37,7 @@ You must NOT:
|
|
|
37
37
|
3. For each missing/malformed piece: apply the smallest allowed fix.
|
|
38
38
|
4. Write the normalized spec. Default location: `<spec-dir>/<id>-<slug>/spec.md`. With `--in-place` flag: write to `<path>` directly (overwrites the original).
|
|
39
39
|
5. Generate or fix `spec.expected.json` per the rules above. Same dir as the spec.
|
|
40
|
-
6. Run `python3 "$DEVLYN_SHARED_DIR/spec-verify-check.py" --check <spec-path>` to validate the spec carrier and supported `complexity` frontmatter
|
|
40
|
+
6. Run `python3 "$DEVLYN_SHARED_DIR/spec-verify-check.py" --check <spec-path>` to validate the actual spec's carrier shape and supported `complexity` frontmatter. Sibling `spec.expected.json` takes precedence over the legacy inline `## Verification` JSON carrier; any present actionable solo-headroom hypothesis command must match that carrier's `verification_commands[].cmd`. If exit 2, fix the file named in the error and re-run.
|
|
41
41
|
7. Run `python3 "$DEVLYN_SHARED_DIR/spec-verify-check.py" --check-expected <expected-path>` to validate sibling `spec.expected.json` plus sibling spec `complexity` frontmatter and any present solo-headroom hypothesis command against `spec.expected.json.verification_commands[].cmd`.
|
|
42
42
|
8. If lint still fails after allowed fixes (e.g. Requirements section is empty in the source), surface the issue and exit non-zero — do NOT invent Requirements.
|
|
43
43
|
9. If the preserved Requirements combine state mutation with ordering/priority,
|
|
@@ -95,10 +95,10 @@ Schema: `_shared/expected.schema.json`. Required when Requirements have observab
|
|
|
95
95
|
python3 "$DEVLYN_SHARED_DIR/spec-verify-check.py" --check-expected <expected-path>
|
|
96
96
|
```
|
|
97
97
|
|
|
98
|
-
Map mechanically decidable rules from both Requirements and Constraints into the existing carriers. Validate each new guard with a violating control that fails and an allowed control that passes, preserving permitted pre-existing occurrences:
|
|
98
|
+
Map mechanically decidable rules from both Requirements and Constraints into the existing carriers. Preserve specific file constraints even when narrower than PLAN's scope. During BUILD_GATE, with `state.base_ref.sha` recorded, the existing `authorized_surface` check enforces PLAN's scope over the run diff and newly created untracked paths (`.devlyn/` exempt). Do not duplicate this general scope check with a whole-checkout scan in `verification_commands`. Validate each new guard with a violating control that fails and an allowed control that passes, preserving permitted pre-existing occurrences:
|
|
99
99
|
- `verification_commands` ← `## Verification` + commands the conversation surfaced. While drafting both Verification and its carrier, omit a standalone check only when another command in that contract runs the same assertions under the required execution conditions and propagates failure; describe that shared coverage in Verification and keep both carriers aligned. Preserve distinct scenarios and required independent verification; execute approved commands unchanged without reusing results across phases. For semantic annotation or exception-handling rules, use a suitable syntax-aware/executable check, preferring existing checks; explicitly retain uncovered semantics as source-review obligations.
|
|
100
100
|
- `forbidden_patterns` ← literal syntax constraints only; regex does not prove semantics. It scans the whole file-filtered diff, including context, deletions and headers; additions-only guards must exclude those lines.
|
|
101
|
-
- `required_files` / `forbidden_files` ← file
|
|
101
|
+
- `required_files` / `forbidden_files` ← literal file paths from Requirements, Constraints and Out of Scope; `forbidden_files` matches exact Git-relative paths (forward slashes, no leading `./`), including both names of a rename. Globs and directory-wide exclusions need a suitable `verification_commands` guard, not wildcard-looking entries in these lists.
|
|
102
102
|
- `max_deps_added` ← dependency limits in Requirements or Constraints (default 0 unless the spec licenses a new dep).
|
|
103
103
|
- `required_risk_probe_requirements` ← for specs where a Requirement combines ordering/priority, rollback, idempotency, auth/signature, concurrency, atomic-batch, or exact output shape, declare the risk categories PHASE 1.5 risk-probe derivation must cover directly, e.g. `[{"tag": "rollback_state", "derived_from": "<exact ## Verification bullet>"}]`. This replaces guessing the required categories from prose — declare them explicitly so the check works in any language. `tag` must be one of the risk-probe tags in `_shared/spec-verify-check.py:RISK_PROBE_TAGS`; `derived_from` must be an exact substring of `## Verification`. Omit entirely when the spec has no ordering/rollback/idempotency/auth/concurrency/atomic-batch/exact-shape risk.
|
|
104
104
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: devlyn:resolve
|
|
3
|
-
description:
|
|
3
|
+
description: Default to direct work after inspecting scope and verification, including bounded multi-file changes. Use full resolve for explicit resolve/spec-mode requests, queue drains, or concrete interacting requirements or verification too complex for the current context. Domain labels, file count, spec documents and quoted skill paths alone do not trigger it. Investigate or clarify missing intent/access first. Explicit small resolve retains all phases and independent verification. Verify dual-judge is default-when-available.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
The current CLI owns PHASE 0, state transitions, Git checkpoints and final report/archive. Spawn the canonical phase workers from this process; never delegate the whole run to another parent orchestrator. VERIFY uses a fresh, findings-only worker.
|
|
7
7
|
|
|
8
8
|
<pipeline_config>
|
|
9
9
|
$ARGUMENTS
|
|
@@ -297,7 +297,7 @@ the BUILD_GATE process-evidence manifest and complete the phase as
|
|
|
297
297
|
`BLOCKED:build-env-underprovisioned`, not as a product finding or substitute
|
|
298
298
|
command. The worker:
|
|
299
299
|
1. Detects language/framework via project files (`package.json`, `pyproject.toml`, etc.).
|
|
300
|
-
2. Runs language-specific gates (tsc / lint / test).
|
|
300
|
+
2. Runs language-specific gates (tsc / lint / test), deferring exact literal overlaps under `references/phases/build-gate.md`'s current-contract/current-round rule; task-context prompts must preserve that rule.
|
|
301
301
|
3. Always runs `python3 "$DEVLYN_SHARED_DIR/spec-verify-check.py" --include-risk-probes` (verification_commands literal-match plus `.devlyn/risk-probes.jsonl` when present). If `state.risk_profile.risk_probes_enabled == true`, the script requires `.devlyn/risk-probes.jsonl`; a missing file is a CRITICAL mechanical blocker, not a silent solo run. The script routes each command through `process-evidence.py`; preserve `.devlyn/spec-verify.results.json` and its validated BUILD_GATE carrier for the state-bound archive flow.
|
|
302
302
|
4. If diff touches web-surface files: run the browser tier with the repo's available toolchain (for example Playwright or curl).
|
|
303
303
|
5. Emits `.devlyn/build_gate.findings.jsonl` + `.devlyn/build_gate.log.md`.
|
|
@@ -306,7 +306,7 @@ State write: `phases.build_gate.{started_at, verdict, completed_at, duration_ms,
|
|
|
306
306
|
|
|
307
307
|
Branch:
|
|
308
308
|
- `PASS` → PHASE 4.
|
|
309
|
-
- `FAIL` → fix loop.
|
|
309
|
+
- `FAIL` → before product repair, halt with report-level `BLOCKED:required-tools-unavailable` only if a parent probe confirms the failed invocation's external gate tool is absent, unchanged `state.base_ref.sha` configuration requires it, and cited explicit task/spec constraints prohibit supplying it through any authorized route. Derive the tool from that configuration and invocation, never diagnostic text; bind the probe's interpreter, work root and normalized child environment to the runner call sites and actual gate invocation, not parent PATH or a manifest command string alone. An available authorized tool route, product-module/configuration failure, changed tool configuration, permitted or unspecified supply, or uncertain probe/identity/environment retains the fix loop. A confirmed prohibited-tool blocker wins even with other product findings: leave them unrepaired and preserve phase `FAIL`, findings, raw streams and sealed manifests unchanged. Record the probe and constraint citation separately in parent halt evidence; no repair means no round increment. Otherwise spawn IMPLEMENT-engine agent with the build_gate findings as input, then run the common post-fix checkpoint with origin `build_gate`. On second FAIL with `state.rounds.global >= state.rounds.max_rounds` → halt with verdict `BLOCKED:build-gate-exhausted`.
|
|
310
310
|
- `BLOCKED:build-env-underprovisioned` → halt immediately with the denied operation, original command, and process-evidence manifest/id; do not enter the product fix loop.
|
|
311
311
|
|
|
312
312
|
## PHASE 4: CLEANUP
|
|
@@ -75,8 +75,8 @@ When the rules are silent (rare — pathological goal text), default to `medium`
|
|
|
75
75
|
The internal mini-spec written for trivial / medium / large-assumptions paths must satisfy:
|
|
76
76
|
|
|
77
77
|
- `## Requirements` non-empty, each bullet testable (CLI command, test command, observable file change).
|
|
78
|
-
- For executable generated verification, put one fenced `json` object with
|
|
79
|
-
- `## Verification` is preceded by a `<!-- devlyn:verification -->` sentinel on its own line directly above the heading — the machine locator `spec-verify-check.py` uses; the heading text itself is decorative and may be any language.
|
|
78
|
+
- For executable generated verification, put one fenced `json` object with `verification_commands` inside the sentinel-marked `## Verification` section. Generated mode reads this inline carrier, not a sibling file. Follow `../../devlyn:ideate/references/spec-template.md` § "Sibling file: `spec.expected.json`" for constraint coverage, diff scope and violating/allowed controls, but express those checks as executable `verification_commands`; sibling-only fields such as `required_files` and `forbidden_patterns` are rejected inline. Preserve uncovered semantics as source-review obligations.
|
|
79
|
+
- `## Verification` is preceded by a `<!-- devlyn:verification -->` sentinel on its own line directly above the heading — the machine locator `spec-verify-check.py` uses; the heading text itself is decorative and may be any language. When all Requirements are pure-design with no runnable acceptance check, put `{"pure_design": true, "verification_commands": []}` in the fenced `json` block and retain semantic source-review obligations. This explicit declaration is required; a missing block, an unmarked empty list, or `pure_design: true` with commands is invalid.
|
|
80
80
|
- If a free-form goal includes pair-evidence intent and already includes an actionable solo-headroom hypothesis, preserve that literal hypothesis in `.devlyn/criteria.generated.md` unchanged enough for VERIFY to detect `solo-headroom hypothesis`, `solo_claude`, `miss`, and the backticked observable command line that itself contains `miss`, emit the canonical `spec.solo_headroom_hypothesis` pair trigger reason, and satisfy regenerated-evidence checks such as `benchmark audit --require-hypothesis-trigger`.
|
|
81
81
|
- If a free-form goal includes unmeasured pair-candidate intent and already includes solo ceiling avoidance, preserve that literal note in `.devlyn/criteria.generated.md` unchanged enough for reviewers to see `solo ceiling avoidance`, `solo_claude`, and the concrete difference from rejected or solo-saturated controls such as `S2`-`S6`.
|
|
82
82
|
- Free-form mode mini-specs are written to `.devlyn/criteria.generated.md` (not to a roadmap path) — this is run-scoped artifact, not a documented spec.
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Per-engine adapter header is prepended at runtime. BUILD_GATE is mechanical / deterministic — same commands CI / Docker / production run.
|
|
4
4
|
|
|
5
5
|
<role>
|
|
6
|
-
Run language-specific gates and the spec literal-match verification. Emit findings; the orchestrator's fix loop consumes them.
|
|
6
|
+
Run language-specific gates and the spec literal-match verification. Emit findings; the orchestrator's fix loop consumes them. Git staging and commits belong to the parent IMPLEMENT checkpoint; verify any staging prerequisite with read-only Git inspection, never repeat `git add` or forward task staging instructions as worker actions.
|
|
7
7
|
</role>
|
|
8
8
|
|
|
9
9
|
<capability_contract>
|
|
@@ -46,7 +46,7 @@ Detect the project shape from files in `state.base_ref.sha`:
|
|
|
46
46
|
</detection>
|
|
47
47
|
|
|
48
48
|
<gates>
|
|
49
|
-
Run in
|
|
49
|
+
Resolve the concrete commands for steps 1–3; detection examples are not command identities. Run them in order unless step 4's current valid literal contract, using its source precedence and checker defaults, schedules the exact command with the same source/test inputs, working directory and effective environment. Establish the work root and normalized child environment from the checker/runner call sites and the current invocation; a manifest's command string alone does not establish them. The literal must expect exit 0, cover every gate assertion and use the same effective timeout as the planned gate invocation; uncertain matches run normally. Defer a matching gate to step 4 and derive its usual per-error findings and severity from that invocation's validated current BUILD_GATE run/round raw streams, citing the evidence id. A completed nonzero exit still supplies the gate's usual failure findings. If step 4 leaves no validated exit-kind result for a deferred gate, run that gate normally and append its findings, preserving step 4's failures; capability denials retain the stop contract above. Keep every literal invocation and independent post-CLEANUP VERIFY. Each gate emits findings into `.devlyn/build_gate.findings.jsonl`:
|
|
50
50
|
|
|
51
51
|
1. **Type check** (TypeScript / mypy / etc.). Each error → one finding, severity `HIGH`, rule `correctness.type-check`.
|
|
52
52
|
2. **Lint**. Run a recognized configured language linter (for example, ESLint with a config, `ruff`, or `clippy`) and/or the exact `package.json` `scripts.lint` command. A declared specialized `lint:*` script (for example, `lint:json`) is not the general language lint gate on the strength of its declaration alone. If the spec's verification commands explicitly name such a specialized command, step 4 executes it and preserves its real failure severity. If no linter is applicable, log an explicit SKIP in `.devlyn/build_gate.log.md` and emit zero lint findings. Each error → finding, severity `MEDIUM`, rule `quality.lint`. Warnings stay LOW unless the spec elevates them.
|
|
@@ -59,6 +59,7 @@ Append all findings; do not stop on the first failure.
|
|
|
59
59
|
|
|
60
60
|
<output>
|
|
61
61
|
- `.devlyn/build_gate.findings.jsonl` — JSONL stream, one finding per line. Schema: `{id, rule_id, severity, file, line, message, fix_hint, criterion_ref}`.
|
|
62
|
+
- A completed checker preflight rejection writes `spec-verify.results.json` with `preflight_failure` and the original CRITICAL finding's path/digest. Retain that source (default `.devlyn/spec-verify-findings.jsonl`) and append its finding to `.devlyn/build_gate.findings.jsonl`. Earlier gate observations remain bound, but no spec command is claimed to have run. An absent results file still follows the interrupted-observation `BLOCKED` path.
|
|
62
63
|
- `.devlyn/build_gate.log.md` — human-readable gate results with raw output or links to complete raw-output files, using sealed artifacts where available. Retain complete inventories/hash maps in `.devlyn/` files; report their paths, digests and relevant deltas instead of dumping them into the conversation.
|
|
63
64
|
- Report `PASS` if zero CRITICAL/HIGH findings, `FAIL` for genuine product findings, or `BLOCKED:build-env-underprovisioned` only for the evidence-backed capability path above. A capability blocker leaves no product finding. Do not edit `pipeline.state.json` yourself — the orchestrator records it via `state-phase-write.py` from these artifacts.
|
|
64
65
|
</output>
|
|
@@ -54,7 +54,7 @@ Single authoritative verdict source for `/devlyn:resolve`. The orchestrator bran
|
|
|
54
54
|
- **engine** — any engine name with a shipped adapter (`_shared/adapters/<name>.md`); `"claude"` and `"codex"` today. A required unavailable engine stops the run with `BLOCKED:<engine>-unavailable`.
|
|
55
55
|
- **engine_source** — `"flag" | "engines.json" | "default"` — provenance of the resolved executor engine (`_shared/engine-preflight.md#role-resolution`). Optional on archived pre-iter-0038 runs; absent means `default`.
|
|
56
56
|
- **source** — downstream contract provenance. Spec/verify-only set `type: "spec"`, `spec_path`, and `spec_sha256`. Free-form sets `type: "generated"`, `goal_path: ".devlyn/goal.raw.txt"` + exact-byte `goal_sha256`, and `criteria_path: ".devlyn/criteria.generated.md"` + raw-byte `criteria_sha256`. VERIFY re-checks its contract hash.
|
|
57
|
-
- **`.devlyn/external-diff.patch` artifact** — verify-only
|
|
57
|
+
- **`.devlyn/external-diff.patch` artifact** — verify-only Git patch with `a/` and `b/` path prefixes (`git diff --binary --src-prefix=a/ --dst-prefix=b/`). A zero-byte file means no changes; file-path checks reject malformed or other-prefix input. File exclusions cover both names in rename/copy headers. The verifier fails closed if the artifact exists outside `"verify-only"` mode.
|
|
58
58
|
- **risk_profile** — PHASE 0 routing state. `high_risk` controls automatic risk probes; `risk_probes_enabled` / `risk_probes_explicit` preserve their current semantics; `pair_default_enabled` is false only for explicit `--no-pair`. `risk_profile` must remain an object with boolean `high_risk`, `risk_probes_enabled`, `risk_probes_explicit`, and `pair_default_enabled` fields when present, plus `reasons` as a string list. Malformed state blocks VERIFY because it can hide required routes.
|
|
59
59
|
- **risk_probes_digest** — top-level sha256 written by PHASE 1.5 after probe validation; BUILD_GATE/VERIFY replay semantics live in `phases/build-gate.md` step 4.
|
|
60
60
|
- **process_evidence** — absent or JSON null for legacy runs and new runs with no captured obligation; otherwise an append-only array of `{phase, round, manifest: {path, sha256}, streams: [{id, stdout: {path, sha256}, stderr: {path, sha256}}]}` carriers. Paths are worktree-relative and live under `.devlyn/process-evidence/<run_id>/<phase>/round-<round>/`. A successful IMPLEMENT completion/transition validates every sibling `spec.expected.json.process_evidence[]` item declared for `phase: "implement"`, rehashes both raw streams (including zero-byte files), and appends the carrier before accepting the checkpoint. Missing, altered, escaping, duplicate-id, or expectation-mismatched evidence blocks without a state mutation. Later phases reuse the same carrier shape; no schema-major bump is required because readers must accept absent/null legacy state.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "devlyn-cli",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.3",
|
|
4
4
|
"description": "Loop & harness engineering for AI coding agents (Claude Code, Codex, and any AGENTS.md-driven agent) — take intent to shipped, engineer-quality software hands-free with phase-gated pipelines and agent orchestration",
|
|
5
5
|
"homepage": "https://github.com/fysoul17/devlyn-cli#readme",
|
|
6
6
|
"bin": {
|
package/scripts/lint-skills.sh
CHANGED
|
@@ -356,7 +356,7 @@ PY
|
|
|
356
356
|
|
|
357
357
|
incomplete="$tmp_install_marker/incomplete"
|
|
358
358
|
mkdir -p "$incomplete/package" "$incomplete/home/.codex/skills" "$incomplete/claude/.claude/skills" "$incomplete/codex"
|
|
359
|
-
cp -R bin config package.json "$incomplete/package/"
|
|
359
|
+
cp -R bin config package.json AGENTS.md CLAUDE.md "$incomplete/package/"
|
|
360
360
|
rm -rf "$incomplete/package/config/skills/devlyn:queue"
|
|
361
361
|
printf '{"version":"stale"}\n' > "$incomplete/home/.codex/skills/.devlyn-install.json"
|
|
362
362
|
printf '{"version":"stale"}\n' > "$incomplete/claude/.claude/skills/.devlyn-install.json"
|
|
@@ -1298,7 +1298,7 @@ done
|
|
|
1298
1298
|
for pattern in \
|
|
1299
1299
|
'def stage_from_expected' \
|
|
1300
1300
|
'stage_from_expected(' \
|
|
1301
|
-
'
|
|
1301
|
+
'contract_found, _staged, expected_error, expected_path' \
|
|
1302
1302
|
'def expected_contract_findings' \
|
|
1303
1303
|
'correctness.forbidden-pattern' \
|
|
1304
1304
|
'scope.max-deps-added-exceeded' \
|