Skip to content

feat(kernel): loop-engineer/plan@1 Loop Plan IR + loop plan-lint CLI (#49) - #61

Merged
SollanSystems merged 1 commit into
mainfrom
feat/s3-plan-ir
Jul 13, 2026
Merged

SollanSystems merged 1 commit into
mainfrom
feat/s3-plan-ir

Conversation

@SollanSystems

Copy link
Copy Markdown
Owner

What

S3 of the Phase 0/1 roadmap run: the Loop Plan IR (loop-engineer/plan@1) as a standalone, kernel-validated document + a flat loop plan-lint CLI command — per ADR 0001 and issue #56's capability vocabulary. loop/contract.py is byte-unchanged; plan@1 is deliberately not yet a loop doctor artifact (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/verifyfast_low_cost/deep_reasoning/code_generation/independent_review (never a vendor model name); completion_policy reuses the shared all_required normalizer.
  • loop/plan.pyvalidate_plan(): jsonschema mode + type-checked structural fallback (required core surface enforced in both modes); cross-field rules in both modes: id uniqueness, dangling depends_on/join_on, invalid_dependency_entry for non-string entries, cycle detection (iterative DFS), per-kind required fields, approval_gates referential integrity.
  • CLI: loop plan-lint [--mode basic|strict|release] <plan-file> reusing the S2 --mode contract; strict/release fail loud without jsonschema (exit 2, no traceback).
  • Goldens: 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).
  • Wheel test proves plan-lint runs from a jsonschema-free install in structural-fallback.

Verification

  • Extras suite: 586 passed / 15 skipped (base 544/14); pyyaml-only: 563 / 38 (base 526/32); zero regressions.
  • 4 fail-loud probes: release-without-jsonschema exit 2 clean stderr; cyclic + missing-goal fixtures rejected with correct issue codes; missing file exit 2 with plan-lint hint.
  • Lane: claudex gpt-5.6-terra; accepted on attempt 2 after one productive repair — fresh sonnet review found a basic-mode type-check parity gap (type-malformed required fields passed --mode basic), governor reproduced it live, repair closed it with both-mode regression tests + independent re-review PASS. Receipts cx_s3_plan_a1 (repair_requested), cx_s3_plan_a2 (accepted); codex sessions 019f5c88…, 019f5cd5….

Closes #49

https://claude.ai/code/session_01EJ8zA8Cbi4o2amawpj8bZW

…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
Copilot AI review requested due to automatic review settings July 13, 2026 19:10
@SollanSystems
SollanSystems enabled auto-merge (squash) July 13, 2026 19:10
@SollanSystems
SollanSystems merged commit 157c31b into main Jul 13, 2026
11 checks passed
@SollanSystems
SollanSystems deleted the feat/s3-plan-ir branch July 13, 2026 19:10

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread loop/plan.py

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()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment thread loop/plan.py
Comment on lines +142 to +143
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))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.json and loop/plan.py to validate plan@1 via jsonschema (when available) or structural fallback.
  • Extend the python3 -m loop CLI to include a plan-lint command 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.

Comment thread loop/plan.py
Comment on lines +141 to +151
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))
Comment thread loop/plan.py
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 []
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Define loop-engineer/plan@1 schema (Loop Plan IR)

2 participants