feat(kernel): loop-engineer/plan@1 Loop Plan IR + loop plan-lint CLI (#49) - #61
Conversation
…49) - schemas/plan.schema.json: plan@1 — goal/acceptance_criteria/tasks/ terminal_state_mapping required; 8 task kinds (agent tool gate approval join subloop human terminal); capability-based model_policy (#56): read/reason/write/verify -> fast_low_cost/deep_reasoning/code_generation/ independent_review; completion_policy reuses the shared all_required normalizer - loop/plan.py: validate_plan() with jsonschema mode + hand-rolled structural fallback (type-checked required core surface in both modes); cross-field rules in both modes: task-id/criterion-id uniqueness, dangling depends_on/join_on refs, invalid_dependency_entry for non-string entries, dependency-graph acyclicity (iterative DFS), per-kind required fields, approval_gates referential integrity - CLI: flat `loop plan-lint [--mode basic|strict|release] <plan-file>` reusing the doctor --mode contract; strict/release fail loud without jsonschema (exit 2, no traceback) - goldens: examples/plans/coverage-repair.plan.json (all 8 kinds) + invalid/ negatives; ci.yml plan-lint smoke (positive + 2 negatives) - docs: reference/repo-os-contract.md §15 (scope boundary: plan@1 is standalone in v1, not yet read by loop doctor) Suite: extras 586 passed / 15 skipped; pyyaml-only 563 / 38. Lane: claudex gpt-5.6-terra, accepted on attempt 2 after one productive repair (basic-mode type-check parity gap found by fresh sonnet review, reproduced by the governor, closed with both-mode regression tests). Receipts: cx_s3_plan_a1 (repair_requested), cx_s3_plan_a2 (accepted). Closes #49 Claude-Session: https://claude.ai/code/session_01EJ8zA8Cbi4o2amawpj8bZW
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a91b9d8796
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
|
||
| def _check_approval_gates(data: dict[str, Any], tasks: list[Any], path: Path, issues: list[dict]) -> None: | ||
| declared = data.get("approval_gates") | ||
| declared_set = set(declared) if isinstance(declared, list) else set() |
There was a problem hiding this comment.
Guard approval gate sets against malformed entries
When a plan has an approval task and approval_gates contains an unhashable JSON value such as a nested array, set(declared) raises TypeError before plan-lint can return a validation report. This also affects strict/release mode because the cross-field checker still runs after JSON Schema records the item-type violation, so a malformed input produces a traceback instead of the intended nonzero lint result.
Useful? React with 👍 / 👎.
| if not task.get(field): | ||
| issues.append(ContractIssue("missing_kind_field", f"task {task_id!r} (kind={kind!r}) missing required field {field!r}", path)) |
There was a problem hiding this comment.
Reject non-string kind fields in fallback mode
In structural-fallback/basic mode, this truthiness check lets malformed kind-specific fields pass, e.g. a human task with instructions: 123 or a tool task with tool_name: 1 returns ok: true when jsonschema is absent. Since the wheel-only/default no-dependency path intentionally uses this fallback, it can accept plans that the published schema would reject rather than preserving validation parity for the required per-kind surface.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Pull request overview
Introduces the standalone Loop Plan IR (loop-engineer/plan@1) plus a plan-lint CLI subcommand (python -m loop plan-lint) to validate plan documents independently of loop doctor, aligning with ADR 0001 and the capability-based model_policy vocabulary from issue #56.
Changes:
- Add
schemas/plan.schema.jsonandloop/plan.pyto validate plan@1 via jsonschema (when available) or structural fallback. - Extend the
python3 -m loopCLI to include aplan-lintcommand and update CI to smoke-test it. - Add golden and invalid plan fixtures plus contract tests covering both validation modes and CLI behaviors.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| scripts/test_wheel_selfcontained.py | Adds a wheel-only test ensuring plan-lint runs in structural-fallback without jsonschema. |
| scripts/test_plan_schema.py | Adds unit/contract tests for plan@1 validation logic and CLI exit behavior. |
| scripts/test_loop_cli.py | Adds CLI UX tests for plan-lint help, errors, and exit codes. |
| schemas/plan.schema.json | Introduces the plan@1 JSON Schema (core fields, vocabularies, and types). |
| reference/repo-os-contract.md | Documents plan@1 scope boundary and vocabulary in the repo OS contract reference. |
| loop/plan.py | Implements plan@1 validation (jsonschema + structural fallback + cross-field checks). |
| loop/main.py | Adds plan-lint command wiring, help text, and mode handling. |
| loop/init.py | Exposes plan@1 constants and validate_plan from the package surface. |
| examples/plans/invalid/missing-goal.plan.json | Adds a negative fixture for missing required core fields. |
| examples/plans/invalid/cyclic-dependency.plan.json | Adds a negative fixture for cyclic dependencies. |
| examples/plans/coverage-repair.plan.json | Adds a comprehensive golden plan exercising all task kinds and terminal states. |
| .github/workflows/ci.yml | Adds a CI smoke-test step for plan-lint (positive + negative fixtures). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| for field in _KIND_REQUIRED_FIELDS[kind]: | ||
| if not task.get(field): | ||
| issues.append(ContractIssue("missing_kind_field", f"task {task_id!r} (kind={kind!r}) missing required field {field!r}", path)) | ||
| if kind == "agent" and task.get("role") is not None and task["role"] not in MODEL_POLICY_ROLES: | ||
| issues.append(ContractIssue("invalid_task_role", f"task {task_id!r} has unknown role {task['role']!r}", path)) | ||
| if kind == "terminal" and task.get("terminal_state") is not None and task["terminal_state"] not in TERMINAL_STATES: | ||
| issues.append(ContractIssue("invalid_terminal_state", f"task {task_id!r} terminal_state {task['terminal_state']!r} is not canonical", path)) | ||
| if kind == "join": | ||
| join_on = task.get("join_on") | ||
| if isinstance(join_on, list) and len(join_on) < 2: | ||
| issues.append(ContractIssue("invalid_join", f"task {task_id!r} join_on needs at least 2 upstream task ids", path)) |
| issues.append(ContractIssue("duplicate_task_id", f"duplicate task id {task_id!r}", path)) | ||
| seen.add(task_id) | ||
| ids.append(task_id) | ||
| edges[task_id] = [d for d in depends_on if isinstance(d, str)] if isinstance(depends_on, list) else [] |
What
S3 of the Phase 0/1 roadmap run: the Loop Plan IR (
loop-engineer/plan@1) as a standalone, kernel-validated document + a flatloop plan-lintCLI command — per ADR 0001 and issue #56's capability vocabulary.loop/contract.pyis byte-unchanged; plan@1 is deliberately not yet aloop doctorartifact (scope boundary documented in reference/repo-os-contract.md §15; the execution-runtime milestone gives it an on-disk home).schemas/plan.schema.json— plan@1: required goal/acceptance_criteria/tasks/terminal_state_mapping; 8 task kinds (agent tool gate approval join subloop human terminal) with per-kind required fields; capability-based model_policy (Capability-based model routing in the portable contract #56):read/reason/write/verify→fast_low_cost/deep_reasoning/code_generation/independent_review(never a vendor model name);completion_policyreuses the sharedall_requirednormalizer.loop/plan.py—validate_plan(): jsonschema mode + type-checked structural fallback (required core surface enforced in both modes); cross-field rules in both modes: id uniqueness, danglingdepends_on/join_on,invalid_dependency_entryfor non-string entries, cycle detection (iterative DFS), per-kind required fields,approval_gatesreferential integrity.loop plan-lint [--mode basic|strict|release] <plan-file>reusing the S2--modecontract; strict/release fail loud without jsonschema (exit 2, no traceback).examples/plans/coverage-repair.plan.json(exercises all 8 kinds, all 7 terminal states) +invalid/negatives; ci.yml plan-lint smoke (1 positive + 2 negative).Verification
--mode basic), governor reproduced it live, repair closed it with both-mode regression tests + independent re-review PASS. Receiptscx_s3_plan_a1(repair_requested),cx_s3_plan_a2(accepted); codex sessions019f5c88…,019f5cd5….Closes #49
https://claude.ai/code/session_01EJ8zA8Cbi4o2amawpj8bZW