mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
* fix(board): LEARN decisions name the item, not its per-cycle index A cycle's reject reasons are rendered into the NEXT cycle's exploration prompt, but the ref recorded alongside each reason was the item's stored id (item-0/item-1) — a per-cycle index that means something different every cycle and appears nowhere the explorer can resolve. The reason survived the loop; what it was about did not. Record the item's title instead, via a shared learn_ref() helper (falls back to the id when title-less, and reads target_task_title for Scales, whose items name the live task they mutate). * chore(lint): satisfy ruff 0.16 — keyword-only signatures and markdown formatting The dev toolchain resolved ruff 0.16.0, which stabilises PLR0917 (too many positional arguments) and formats python code blocks inside markdown. Both fired repo-wide and neither had anything to do with the code they flagged. - 36 signatures gain a `*` so their tail arguments are keyword-only, and the 104 call sites that passed them positionally are converted. mypy was the safety net for the static ones; the full suite caught nine more that only bind at runtime (the MCP tool functions, whose real callers already pass named JSON arguments). - 28 markdown files reformatted by 0.16's code-block formatter. - One RUF036 (`None` mid-union) autofixed in the GitLab provider. * fix(gateway): log the reason when a verb rejects A rejected envelope rides an HTTP 200, its body is never logged, and there is no trace table — so in the access log a verb an agent could not satisfy looks identical to one that worked. On 2026-07-25 four Board Programs (Periscope, Sentinel, Scales, Barfly) each POSTed their propose verb three or four times, persisted nothing, and left their exploration tasks PENDING; the reason was unrecoverable afterwards, from the logs or from the agents' own transcripts. Log error/message/remediate/missing plus the calling agent at envelope_to_response — the one chokepoint every v1 flow and do route returns through. Success envelopes stay silent. --------- Co-authored-by: Renn F <rennf93@users.noreply.github.com>
152 lines
6.0 KiB
Markdown
152 lines
6.0 KiB
Markdown
# Preconditions and Rejection Kinds
|
|
|
|
## What are preconditions?
|
|
|
|
A **Precondition** is a declarative gate-check that the gateway verifies before allowing an action. Each precondition has four parts:
|
|
|
|
| Field | Meaning |
|
|
|-------|---------|
|
|
| `key` | Internal name (e.g., `owns_task`) |
|
|
| `check` | A function that returns True if the precondition passes |
|
|
| `remediate` | Human-readable hint surfaced when the precondition fails |
|
|
| `missing_token` | What appears in the `tracing_gap.missing[]` array when it fails (for input artifact errors) |
|
|
| `rejection_kind` | **NEW:** Controls which `error` flavor is returned on failure (see below) |
|
|
|
|
When a verb is invoked, the gateway checks all preconditions for that verb. If any fail, the agent receives a structured error envelope.
|
|
|
|
## The two rejection kinds: `tracing_gap` vs `not_authorized`
|
|
|
|
When a precondition fails, the error flavor depends on the **reason for the failure**:
|
|
|
|
### `tracing_gap` (default)
|
|
|
|
**Meaning:** A required artifact is missing — the agent needs to do something to provide it.
|
|
|
|
**Examples:**
|
|
- `PRECONDITION_COMMITS` fails if the developer hasn't made any commits yet
|
|
- `PRECONDITION_PR_EXISTS` fails if the developer hasn't opened a PR
|
|
|
|
**Agent experience:**
|
|
```json
|
|
{
|
|
"error": "tracing_gap",
|
|
"message": "Missing required commit(s)",
|
|
"missing": ["commits"],
|
|
"remediate": "commit() at least once with a non-empty message before submitting"
|
|
}
|
|
```
|
|
|
|
The `missing[]` array tells the agent exactly what artifact is missing, so they can take the right action.
|
|
|
|
### `not_authorized` (ownership / identity gates)
|
|
|
|
**Meaning:** The agent is not allowed to perform this action — a role/permission boundary, not a missing artifact.
|
|
|
|
**Examples:**
|
|
- **`PRECONDITION_OWNERSHIP`** fails if the agent is not assigned to the task
|
|
- **Self-review block** fails if the QA agent is the original developer
|
|
- **Role gate** fails if a non-PM tries to merge
|
|
|
|
**Agent experience:**
|
|
```json
|
|
{
|
|
"error": "not_authorized",
|
|
"message": "task is not assigned to you; call give_me_work() to find your work",
|
|
"remediate": "task is not assigned to you; call give_me_work() to find your work"
|
|
}
|
|
```
|
|
|
|
There is no `missing[]` array — the agent is simply not allowed, and the remediate message tells them what to do instead (usually "find your own work" or "have a different role perform this").
|
|
|
|
## How `rejection_kind` works
|
|
|
|
When a Precondition is defined, it includes a `rejection_kind` field that determines which error flavor it returns:
|
|
|
|
```python
|
|
@dataclass(frozen=True)
|
|
class Precondition:
|
|
key: str
|
|
check: Callable[[Any, Any, Any], bool]
|
|
remediate: str
|
|
missing_token: str
|
|
rejection_kind: RejectionKind = "tracing_gap" # default
|
|
```
|
|
|
|
**Built-in preconditions and their rejection kinds:**
|
|
|
|
| Precondition | `rejection_kind` | Why |
|
|
|--------------|------------------|-----|
|
|
| `PRECONDITION_OWNERSHIP` | `not_authorized` | Unowned tasks are authorization failures, not missing artifacts |
|
|
| `PRECONDITION_COMMITS` | `tracing_gap` | Commits are missing artifacts the agent can create |
|
|
| `PRECONDITION_PR_EXISTS` | `tracing_gap` | A PR is a missing artifact the agent can create |
|
|
| Most others | `tracing_gap` | Missing data artifacts the agent can provide |
|
|
|
|
## Dispatch logic in `_check_intent_preconditions`
|
|
|
|
When the gateway evaluates verb preconditions, it checks them in order and returns the first failure:
|
|
|
|
```python
|
|
def _check_intent_preconditions(
|
|
spec_intent: IntentSpec, task: Any, ctx: Context
|
|
) -> Decision | None:
|
|
"""Verb-level extra_preconditions gate.
|
|
|
|
If the first failing precondition has rejection_kind='not_authorized',
|
|
return Decision.reject(kind='not_authorized').
|
|
All other failures return Decision.tracing_gap.
|
|
"""
|
|
missing = [
|
|
p.missing_token
|
|
for p in spec_intent.extra_preconditions
|
|
if not p.check(task, None, ctx)
|
|
]
|
|
if not missing:
|
|
return None
|
|
|
|
first_missing = next(
|
|
p for p in spec_intent.extra_preconditions if p.missing_token == missing[0]
|
|
)
|
|
|
|
# Check the rejection_kind of the first failing precondition
|
|
if first_missing.rejection_kind == "not_authorized":
|
|
return Decision.reject(
|
|
kind="not_authorized",
|
|
message=first_missing.remediate,
|
|
remediate=first_missing.remediate,
|
|
)
|
|
|
|
# Default: tracing_gap with missing tokens
|
|
return Decision.tracing_gap(missing=missing, remediate=first_missing.remediate)
|
|
```
|
|
|
|
The key insight: **Only the first failing precondition's `rejection_kind` is checked.** This ensures ownership gates are checked early (they usually are in the preconditions list) so unowned tasks fail fast with `not_authorized` instead of collecting other tracing gaps.
|
|
|
|
## Agent-visible impact
|
|
|
|
When an agent tries to perform an action on a task they don't own, they now see:
|
|
|
|
```json
|
|
{
|
|
"error": "not_authorized",
|
|
"message": "task is not assigned to you; call give_me_work() to find your work",
|
|
"remediate": "task is not assigned to you; call give_me_work() to find your work"
|
|
}
|
|
```
|
|
|
|
This is semantically clearer than the previous `tracing_gap` / `owns_task` message: it's an authorization failure, not a data-collection problem. The agent cannot add a "missing" artifact to fix it — they need a different task.
|
|
|
|
## When to add a new precondition with `rejection_kind='not_authorized'`
|
|
|
|
When designing a new gate-check precondition:
|
|
|
|
- Use `rejection_kind='not_authorized'` if the failure is a **role or identity boundary** (the agent is the wrong person / role for this action)
|
|
- Use the default `rejection_kind='tracing_gap'` if the failure is a **missing artifact** (the agent can provide / create it)
|
|
|
|
**Example:** A new "task must be in this project" check would use `not_authorized` because the agent is the wrong role/team, not because they're missing data.
|
|
|
|
## See also
|
|
|
|
- [How agents are sandboxed](../company/agent-gateway.md) — the gateway, verbs, and envelope
|
|
- [REST API](../../api/rest-api.md) — error envelope schema and error flavors
|
|
- [Task model](./task-model.md) — task fields and state
|