Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,18 @@ jobs:
python -B -m loop doctor --mode release examples/coverage-repair
python -B -m loop inspect examples/coverage-repair

- name: Plan lint smoke test
run: |
python -B -m loop plan-lint --mode release examples/plans/coverage-repair.plan.json
if python -B -m loop plan-lint --mode release examples/plans/invalid/cyclic-dependency.plan.json; then
echo "::error::cyclic-dependency.plan.json unexpectedly passed plan-lint"
exit 1
fi
if python -B -m loop plan-lint --mode release examples/plans/invalid/missing-goal.plan.json; then
echo "::error::missing-goal.plan.json unexpectedly passed plan-lint"
exit 1
fi

recipe-langgraph:
name: recipe (langgraph)
runs-on: ubuntu-latest
Expand Down
32 changes: 32 additions & 0 deletions examples/plans/coverage-repair.plan.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"schema": "loop-engineer/plan@1",
"goal": "Eliminate the known flaky-test cluster in the coverage-repair example without weakening any existing assertion.",
"constraints": ["no new third-party test dependencies", "must not increase full-suite runtime by more than 10%"],
"acceptance_criteria": [
{ "id": "AC1", "description": "All previously flaky tests pass 20/20 consecutive runs" },
{ "id": "AC2", "description": "No regression in the existing suite" }
],
"tasks": [
{ "id": "collect-logs", "kind": "agent", "title": "Collect recent CI failure logs", "depends_on": [], "role": "read", "verify": "scripts/verify-fast" },
{ "id": "diagnose-flakiness", "kind": "agent", "title": "Diagnose the flaky-test cluster", "depends_on": ["collect-logs"], "role": "reason", "verify": "scripts/verify-fast" },
{ "id": "run-flaky-detector", "kind": "tool", "title": "Run the flaky-test detector", "depends_on": ["diagnose-flakiness"], "tool_name": "pytest-flaky-detector", "verify": "scripts/verify-fast" },
{ "id": "repair-branch-a", "kind": "agent", "title": "Repair candidate A", "depends_on": ["run-flaky-detector"], "role": "write", "verify": "scripts/verify-full" },
{ "id": "repair-branch-b", "kind": "agent", "title": "Repair candidate B", "depends_on": ["run-flaky-detector"], "role": "write", "verify": "scripts/verify-full" },
{ "id": "join-repairs", "kind": "join", "title": "Join repair candidates", "depends_on": ["repair-branch-a", "repair-branch-b"], "join_on": ["repair-branch-a", "repair-branch-b"] },
{ "id": "verify-fast-gate", "kind": "gate", "title": "Fast deterministic gate", "depends_on": ["join-repairs"], "verify": "scripts/verify-fast" },
{ "id": "independent-review", "kind": "agent", "title": "Independent adversarial review", "depends_on": ["verify-fast-gate"], "role": "verify", "verify": "scripts/verify-safety" },
{ "id": "ship-approval", "kind": "approval", "title": "Release sign-off", "depends_on": ["independent-review"], "approval_gate": "release-sign-off" },
{ "id": "manual-changelog-entry", "kind": "human", "title": "Add CHANGELOG entry", "depends_on": ["ship-approval"], "instructions": "Add a one-line CHANGELOG entry summarizing the flaky-test fix once ship-approval is granted." },
{ "id": "regression-sweep", "kind": "subloop", "title": "Full regression sweep", "depends_on": ["manual-changelog-entry"], "subloop_ref": "loop-engineer/plan@1:regression-sweep-v1" },
{ "id": "succeeded", "kind": "terminal", "title": "Succeeded", "depends_on": ["regression-sweep"], "terminal_state": "Succeeded" }
],
"completion_policy": { "mode": "all_required" },
"terminal_state_mapping": {
"success": "Succeeded", "budget_exhausted": "FailedBudget", "verifier_veto": "FailedSafety", "unresolvable_flake": "FailedUnverifiable", "external_ci_outage": "FailedBlocked", "spec_ambiguous": "FailedSpecGap", "operator_abort": "AbortedByHuman"
},
"approval_gates": ["release-sign-off"],
"verifiers": ["scripts/verify-fast", "scripts/verify-full", "scripts/verify-safety"],
"budgets": { "max_iterations": 12, "wall_clock_minutes": 90 },
"defaults": { "retry": { "max_attempts": 2, "backoff": "exponential", "timeout_seconds": 600 } },
"model_policy": { "read": "fast_low_cost", "reason": "deep_reasoning", "write": "code_generation", "verify": "independent_review" }
}
11 changes: 11 additions & 0 deletions examples/plans/invalid/cyclic-dependency.plan.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"schema": "loop-engineer/plan@1",
"goal": "Deliberately invalid fixture: exercises loop/plan.py's cycle detector.",
"acceptance_criteria": [{ "id": "AC1", "description": "unreachable — this plan is intentionally cyclic" }],
"tasks": [
{ "id": "task-a", "kind": "tool", "title": "A", "depends_on": ["task-c"], "tool_name": "noop", "verify": "true" },
{ "id": "task-b", "kind": "tool", "title": "B", "depends_on": ["task-a"], "tool_name": "noop", "verify": "true" },
{ "id": "task-c", "kind": "tool", "title": "C", "depends_on": ["task-b"], "tool_name": "noop", "verify": "true" }
],
"terminal_state_mapping": { "success": "Succeeded", "failure": "FailedUnverifiable" }
}
6 changes: 6 additions & 0 deletions examples/plans/invalid/missing-goal.plan.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"schema": "loop-engineer/plan@1",
"acceptance_criteria": [{ "id": "AC1", "description": "n/a" }],
"tasks": [{ "id": "only-task", "kind": "human", "title": "Manual step", "depends_on": [], "instructions": "n/a" }],
"terminal_state_mapping": { "success": "Succeeded" }
}
4 changes: 4 additions & 0 deletions loop/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,16 @@

from .paths import LoopPaths, resolve_loop_paths
from .contract import TERMINAL_STATES, VALIDATION_MODES, doctor_report, validate_contract
from .plan import PLAN_SCHEMA_ID, TASK_KINDS, validate_plan

__all__ = [
"LoopPaths",
"PLAN_SCHEMA_ID",
"TASK_KINDS",
"TERMINAL_STATES",
"VALIDATION_MODES",
"doctor_report",
"resolve_loop_paths",
"validate_contract",
"validate_plan",
]
41 changes: 28 additions & 13 deletions loop/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,24 @@
from pathlib import Path

from .contract import VALIDATION_MODES, ValidationModeError, doctor_report
from .plan import validate_plan

_PROG = "python3 -m loop"

_COMMANDS = ("scaffold", "doctor", "validate", "verify", "inspect", "metrics")
_COMMANDS = ("scaffold", "doctor", "validate", "verify", "inspect", "metrics", "plan-lint")

# Read commands operate on an EXISTING contract dir; scaffold CREATES one, so it
# is exempt from the "target must exist" guard.
_READ_COMMANDS = ("doctor", "validate", "verify", "inspect", "metrics")
_READ_COMMANDS = ("doctor", "validate", "verify", "inspect", "metrics", "plan-lint")

_USAGE = f"usage: {_PROG} <scaffold|doctor|validate|verify|inspect|metrics> <workspace-or-.loop>"
_USAGE = f"usage: {_PROG} <scaffold|doctor|validate|verify|inspect|metrics|plan-lint> <target>"

_HELP = f"""{_PROG} — validate, inspect, and measure a portable repo-OS loop contract.

{_USAGE}
{_PROG} metrics [--baseline] <workspace-or-.loop>
{_PROG} doctor|validate|verify [--mode basic|strict|release] <workspace-or-.loop>
{_PROG} plan-lint [--mode basic|strict|release] <plan-file>

commands:
scaffold Write a fresh, doctor-clean loop contract into <target>.
Expand All @@ -34,14 +36,18 @@
real .loop/ evidence (RUNLOG, verify bundles, held-out gate, repair
records) and emit a JSON scorecard. With --baseline, write a
checked-in baseline scorecard — refused unless the run is gate-backed.
plan-lint Validate a loop-engineer/plan@1 Loop Plan IR document: task-kind
fields, dependency-graph acyclicity, and the terminal-state
mapping. --mode selects validation strength, same as doctor.

arguments:
<target> A workspace root or its .loop/ directory.
<target> A workspace root or its .loop/ directory (all commands except plan-lint).
<plan-file> A single loop-engineer/plan@1 JSON file (plan-lint only).

options:
--mode {{basic,strict,release}}
(doctor/validate/verify only) basic forces structural checks;
strict/release require jsonschema. Default: auto-detect.
(doctor/validate/verify/plan-lint only) basic forces structural
checks; strict/release require jsonschema. Default: auto-detect.
--baseline (metrics only) write docs/metrics-baseline.json over a gate-backed
run; exits non-zero and writes nothing otherwise.
-h, --help Show this help and exit.
Expand Down Expand Up @@ -155,7 +161,7 @@ def main(argv: list[str] | None = None) -> int:
return 2

mode = None
if command in {"doctor", "validate", "verify"}:
if command in {"doctor", "validate", "verify", "plan-lint"}:
try:
mode, argv = _extract_mode_flag(argv)
except ValueError as exc:
Expand All @@ -175,12 +181,14 @@ def main(argv: list[str] | None = None) -> int:
target = Path(argv[0])

if command in _READ_COMMANDS and not target.exists():
print(
f"{command}: target path does not exist: {target}\n"
f" pass an existing workspace root or its .loop/ directory "
f"(run `{_PROG} scaffold {target}` to create a new contract).",
file=sys.stderr,
)
if command == "plan-lint":
hint = "pass an existing loop-engineer/plan@1 JSON file"
else:
hint = (
f"pass an existing workspace root or its .loop/ directory "
f"(run `{_PROG} scaffold {target}` to create a new contract)"
)
print(f"{command}: target path does not exist: {target}\n {hint}.", file=sys.stderr)
return 2

if command == "scaffold":
Expand All @@ -201,6 +209,13 @@ def main(argv: list[str] | None = None) -> int:
print(f"{command}: {exc}", file=sys.stderr)
return 2

if command == "plan-lint":
try:
return _print_json(validate_plan(target, mode=mode))
except ValidationModeError as exc:
print(f"{command}: {exc}", file=sys.stderr)
return 2

# command == "inspect": keep the historical inspector script as the scoring
# UI over the same contract artifacts; import lazily to avoid making
# scripts/ a package.
Expand Down
Loading