fix: uv sync --locked validates against pyproject.toml #1

Merged
john merged 1 commit from fix/dockerfile-uv-locked into main 2026-07-30 05:03:31 +00:00
Owner

Three changes. Each one closes a place where a surface stated something the code does not enforce.

1. The brief told every role ae's write boundary

BriefService.constraints_block took state and ignored it — noqa: ARG004 — so every dispatch, whatever its role, was told:

Do not modify tests, conftest.py, or CI workflow files — those are owned by another role and writes to them will be refused by the scope gate.

That is ae's boundary. An ate dispatch was therefore told not to write the tests that are its entire output, and a ci_fix dispatch was told the gate would refuse the workflow it was sent to repair. A false constraint is worse than a missing one: the agent either stops short of the task, or discovers one of the rules was invented and has less reason to believe the rest.

_ROLE_SCOPE in app/services/briefs.py now mirrors ROLE_POLICIES in dogsled/src/dogsled/job/policy.py. Each role carries probe paths as well as prose, and tests/test_role_scope_parity.py runs them through dogsled's own policy_for(role).permits() — prose cannot be checked, probes can. Both directions are tested: a path the brief offers that the gate refuses (expensive — the run is discarded), and a path the brief withholds that the gate permits (quiet — the agent just does less than the task asked, which is the ate case).

The block also gained a universal-deny line, derived from dogsled's UNIVERSAL_DENY: .git/, .env, *.pem, *.key, secrets* are refused for every role and the brief never said so.

Also fixed: _REPAIR_TEMPLATE. It is the ci_fix path and carried the identical false claim. Fixing only _CONSTRAINTS would have left the brief wrong for the very role named in the report. render_repair resolves with a ci_fix default rather than inheriting roles.dogsled_role's ae.

Wired into the Upstream Parity CI job beside _mirror.py and dogsled_contract.py. Unlike those two, this restatement has no runtime signal at all — a brief describing the wrong boundary is valid input to dogsled and the run simply does the wrong thing. The parity test is the entire signal.

2. layer was free-form while kind was constrained

app/graphs/investigate.py constrained kind to five values with an unclassified fallback and left layer as str. That is the same field the bug capture template already ships as a Suspected layer text box, and production returns it 88% logic — the same degeneration as type at 100% bug, for the same reason: nobody chooses.

Seven layers, each landing on a different target: route, contract, client, session, logic, store, ui. route and client both surface as a 404 and are fixed in opposite repositories; store and logic both surface as a wrong answer from a handler that ran. A label that cannot separate those is not worth filtering on.

  • listed in SYSTEM the same way KINDS is — a closed field the model is not shown is a field it cannot hit;
  • normalise_finding constrains it identically, case-folded, with a layer_means gloss beside kind_means;
  • the fast path in app/services/proposals.py wrote "layer": ""; now "unclassified", with its own gloss. It makes no model call, so there is no diagnosis to classify — that is a fact about the path, not missing data.

The test fixture's layer was prose ("backend/app/routers — no notifications module is registered"). It is now "route", and the old value is reused as the invented-layer case — the degeneration documented as a test rather than a comment.

3. eng_handle_cluster — one tool, routed mechanically

The operator had to choose between eng_reproduce_cluster and eng_investigate_cluster. The two differ only in whether the cluster's capture already contains the failing request, which is reproduction.from_reports — a function, not a judgement — and the signature does not show the answer. Measured on flexiscm 2026-07-29: 41 of 78 open reports carry a capture, 37 do not. Guessing wrong costs two model calls and 37k tokens of source listing to be told what the capture already said, or a 400.

  • reproduction.route — pure, a thin read over from_reports, deliberately not a second notion of "enough evidence". A router with its own rule would drift and send reproduce a cluster it then refuses. One test pins the agreement directly.
  • ProposalView.route_cluster — prechecks with needs_model=False, so an instance with no LLM can still route, including to the branch that needs one. require_investigator is where that branch pays.
  • POST /api/v1/items/{id}/handleeng_handle_cluster. 200 with the parked proposal on the fast path, 202 with an acknowledgement on the investigation, and path in the body either way so a caller never infers it from the status.
  • the router hands its context to the branch it chose, so the repository listing is fetched once rather than twice on a synchronous path against a 30-second gateway. forge.calls == 1 is asserted, and so is the fact that a direct reproduce caller still prechecks for itself.

Both specific tools stay as explicit overrides — forcing a diagnosis on an evidenced cluster is a legitimate thing to want — but neither is the default.

Spec Drift Callouts

  1. ci_fix has no per-role deny-list. The task brief specified "ci_fix: … Cannot touch app/, src/, tests/". dogsled's actual policy is RolePolicy(role="ci_fix") — no allow, no deny, only UNIVERSAL_DENY. Since the brief also said the constraints must agree with what dogsled enforces rather than restate it, I followed the code: ci_fix's constraint states its limit as judgement ("change what the failing check requires and nothing else; deleting the failing test is not a repair") rather than as a gate that does not exist. test_ci_fix_carries_no_per_role_denial pins both sides, so a future narrowing in dogsled forces the text to change with it.
  2. _REPAIR_TEMPLATE was in scope by implication, not by name. See above — fixed, because it is the ci_fix brief.
  3. The role guard runs after compose_brief, not before. check_role lives in enqueue_job, so an unsupported agent type reaches brief composition with a brief already rendered. scope_for therefore falls back rather than raising; raising would turn an operator park into a crash. Documented on the method.
  4. No conftest.py in this repo. The role prose still names it, because it is dogsled's pattern and every repo an agent is dispatched into may have one.
  5. mypy reports 60 pre-existing errors across 24 files. It is not a CI gate here (the Lint step runs ruff only) and this branch adds none.

Verification

uv run ruff check app/ tests/            # All checks passed
uv run ruff format --check app/ tests/   # 92 files already formatted
pytest -q                                # 775 passed, 3 skipped

The 3 skips are test_dogsled_client.py, which needs live dogsled credentials. Run against haskos_mirror_v1 locally so the smeos-table tests execute rather than skip — on haskos_eng_test the same suite reports 587 passed / 187 skipped, which is the gap CI closes by cloning smeos and running its migrations.

New parity coverage runs in Upstream Parity (informational, continue-on-error), which already clones dogsled at dev; test_role_scope_parity.py derives policy.py from the existing DOGSLED_SRC variable, so no new CI secret or variable.

🤖 Generated with Claude Code

https://claude.ai/code/session_01XFrACc19eGQYck4jd2NJWc

Three changes. Each one closes a place where a surface stated something the code does not enforce. ## 1. The brief told every role `ae`'s write boundary `BriefService.constraints_block` took `state` and ignored it — `noqa: ARG004` — so every dispatch, whatever its role, was told: > Do not modify tests, `conftest.py`, or CI workflow files — those are owned by another role and writes to them will be refused by the scope gate. That is `ae`'s boundary. An `ate` dispatch was therefore told not to write the tests that are its entire output, and a `ci_fix` dispatch was told the gate would refuse the workflow it was sent to repair. **A false constraint is worse than a missing one**: the agent either stops short of the task, or discovers one of the rules was invented and has less reason to believe the rest. `_ROLE_SCOPE` in `app/services/briefs.py` now mirrors `ROLE_POLICIES` in `dogsled/src/dogsled/job/policy.py`. Each role carries **probe paths** as well as prose, and `tests/test_role_scope_parity.py` runs them through dogsled's own `policy_for(role).permits()` — prose cannot be checked, probes can. Both directions are tested: a path the brief offers that the gate refuses (expensive — the run is discarded), and a path the brief withholds that the gate permits (quiet — the agent just does less than the task asked, which is the `ate` case). The block also gained a universal-deny line, derived from dogsled's `UNIVERSAL_DENY`: `.git/`, `.env`, `*.pem`, `*.key`, `secrets*` are refused for every role and the brief never said so. **Also fixed: `_REPAIR_TEMPLATE`.** It is the `ci_fix` path and carried the identical false claim. Fixing only `_CONSTRAINTS` would have left the brief wrong for the very role named in the report. `render_repair` resolves with a `ci_fix` default rather than inheriting `roles.dogsled_role`'s `ae`. Wired into the `Upstream Parity` CI job beside `_mirror.py` and `dogsled_contract.py`. Unlike those two, **this restatement has no runtime signal at all** — a brief describing the wrong boundary is *valid* input to dogsled and the run simply does the wrong thing. The parity test is the entire signal. ## 2. `layer` was free-form while `kind` was constrained `app/graphs/investigate.py` constrained `kind` to five values with an `unclassified` fallback and left `layer` as `str`. That is the same field the bug capture template already ships as a `Suspected layer` text box, and production returns it **88% `logic`** — the same degeneration as `type` at 100% `bug`, for the same reason: nobody chooses. Seven layers, each landing on a *different target*: `route`, `contract`, `client`, `session`, `logic`, `store`, `ui`. `route` and `client` both surface as a 404 and are fixed in opposite repositories; `store` and `logic` both surface as a wrong answer from a handler that ran. A label that cannot separate those is not worth filtering on. - listed in `SYSTEM` the same way `KINDS` is — a closed field the model is not shown is a field it cannot hit; - `normalise_finding` constrains it identically, case-folded, with a `layer_means` gloss beside `kind_means`; - the fast path in `app/services/proposals.py` wrote `"layer": ""`; now `"unclassified"`, with its own gloss. It makes no model call, so there is no diagnosis to classify — that is a fact about the path, not missing data. The test fixture's `layer` was prose (`"backend/app/routers — no notifications module is registered"`). It is now `"route"`, and the old value is reused as the invented-layer case — the degeneration documented as a test rather than a comment. ## 3. `eng_handle_cluster` — one tool, routed mechanically The operator had to choose between `eng_reproduce_cluster` and `eng_investigate_cluster`. The two differ only in whether the cluster's capture already contains the failing request, which is `reproduction.from_reports` — a function, not a judgement — and the signature does not show the answer. Measured on `flexiscm` 2026-07-29: **41 of 78 open reports carry a capture, 37 do not**. Guessing wrong costs two model calls and 37k tokens of source listing to be told what the capture already said, or a 400. - `reproduction.route` — pure, a thin read over `from_reports`, deliberately **not** a second notion of "enough evidence". A router with its own rule would drift and send `reproduce` a cluster it then refuses. One test pins the agreement directly. - `ProposalView.route_cluster` — prechecks with `needs_model=False`, so an instance with no LLM can still route, *including* to the branch that needs one. `require_investigator` is where that branch pays. - `POST /api/v1/items/{id}/handle` → `eng_handle_cluster`. **200** with the parked proposal on the fast path, **202** with an acknowledgement on the investigation, and `path` in the body either way so a caller never infers it from the status. - the router hands its context to the branch it chose, so the **repository listing is fetched once** rather than twice on a synchronous path against a 30-second gateway. `forge.calls == 1` is asserted, and so is the fact that a direct `reproduce` caller still prechecks for itself. Both specific tools stay as explicit overrides — forcing a diagnosis on an evidenced cluster is a legitimate thing to want — but neither is the default. ## Spec Drift Callouts 1. ⛔ **`ci_fix` has no per-role deny-list.** The task brief specified "`ci_fix`: … Cannot touch `app/`, `src/`, `tests/`". dogsled's actual policy is `RolePolicy(role="ci_fix")` — no `allow`, no `deny`, only `UNIVERSAL_DENY`. Since the brief also said the constraints must agree with what dogsled enforces rather than restate it, **I followed the code**: `ci_fix`'s constraint states its limit as judgement ("change what the failing check requires and nothing else; deleting the failing test is not a repair") rather than as a gate that does not exist. `test_ci_fix_carries_no_per_role_denial` pins both sides, so a future narrowing in dogsled forces the text to change with it. 2. **`_REPAIR_TEMPLATE` was in scope by implication, not by name.** See above — fixed, because it is the `ci_fix` brief. 3. **The role guard runs *after* `compose_brief`, not before.** `check_role` lives in `enqueue_job`, so an unsupported agent type reaches brief composition with a brief already rendered. `scope_for` therefore falls back rather than raising; raising would turn an operator park into a crash. Documented on the method. 4. **No `conftest.py` in this repo.** The role prose still names it, because it is dogsled's pattern and every repo an agent is dispatched *into* may have one. 5. **`mypy` reports 60 pre-existing errors** across 24 files. It is not a CI gate here (the Lint step runs `ruff` only) and this branch adds none. ## Verification ``` uv run ruff check app/ tests/ # All checks passed uv run ruff format --check app/ tests/ # 92 files already formatted pytest -q # 775 passed, 3 skipped ``` The 3 skips are `test_dogsled_client.py`, which needs live dogsled credentials. Run against `haskos_mirror_v1` locally so the smeos-table tests execute rather than skip — on `haskos_eng_test` the same suite reports 587 passed / 187 skipped, which is the gap CI closes by cloning smeos and running its migrations. New parity coverage runs in `Upstream Parity` (informational, `continue-on-error`), which already clones dogsled at `dev`; `test_role_scope_parity.py` derives `policy.py` from the existing `DOGSLED_SRC` variable, so no new CI secret or variable. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01XFrACc19eGQYck4jd2NJWc
john merged commit fa3db899ea into main 2026-07-30 05:03:31 +00:00
john deleted branch fix/dockerfile-uv-locked 2026-07-30 05:03:33 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
haskytech/jeddit!1
No description provided.