create_plan / create_match_rule guards are too strict and opaque #88

Closed
opened 2026-10-08 09:20:38 +02:00 by EduarD3V · 3 comments
Owner

Labels: tools, ux, priority/medium

Both tools refused with:

only create a plan when the operator explicitly asks for one in their message. Do not create one on your own initiativeThe session went: operator says "fix it and improve it" → refused. Operator says "create the plan" → accepted. The guard did not recognise "fix it and improve it" as an explicit ask, which it plainly was.

Problems:

Wastes operator turns. The refusal text does not state what would satisfy it, so the agent has to guess phrasing and re-ask.
Not introspectable. The agent cannot learn the guard's rule; it discovers it by burning a turn.
Inconsistent with intent. "Fix the plan" is an explicit instruction to change the plan. The guard rejects exactly the phrasing a natural operator uses.

Fix: expose the guard state to the agent (e.g. plan_create returns needs_explicit_ask: true plus accepts_phrases), or satisfy it from the current turn's operator message text. Refusal message should say what to say.

Acceptance criteria

Guard can be satisfied by any imperative phrasing of the operator's request, not one canonical phrase.
Refusal message states the condition that would permit the call.
Guard state is readable by the agent before it attempts the call.
Labels: tools, ux, priority/medium Both tools refused with: only create a plan when the operator explicitly asks for one in their message. Do not create one on your own initiativeThe session went: operator says "fix it and improve it" → refused. Operator says "create the plan" → accepted. The guard did not recognise "fix it and improve it" as an explicit ask, which it plainly was. Problems: Wastes operator turns. The refusal text does not state what would satisfy it, so the agent has to guess phrasing and re-ask. Not introspectable. The agent cannot learn the guard's rule; it discovers it by burning a turn. Inconsistent with intent. "Fix the plan" is an explicit instruction to change the plan. The guard rejects exactly the phrasing a natural operator uses. Fix: expose the guard state to the agent (e.g. plan_create returns needs_explicit_ask: true plus accepts_phrases), or satisfy it from the current turn's operator message text. Refusal message should say what to say. Acceptance criteria Guard can be satisfied by any imperative phrasing of the operator's request, not one canonical phrase. Refusal message states the condition that would permit the call. Guard state is readable by the agent before it attempts the call.
gotoi-agent added reference issue/88-create-guards-explicit 2026-10-08 20:44:56 +02:00
Collaborator

RUNTIME NOTE 2026-10-08 ~21:00 — operator drained some queue progress before handing back to the scheduler:

  • #87 closed and pushed as PR #96; the scheduler's REVIEW fired automatically and posted REVIEW: CLEAN (issuecomment-884) before the runner did its gates; orchestrator re-verified gates and posted DONE comment (issuecomment-875).
  • #88 (create_plan / create_match_rule guards) has an in-flight omp coder (tmux gotoi-88) on worktree issue/88-create-guards-explicit, branch d6bb5c3+0 (HEAD not yet). The coder was launched by this turn at 20:44:57 and is still in the Plan stage as of ~20:59 (mid-deliberation about UC id choice; resolved to UC-MW-172 + UC-MW-173).
  • The scheduler's guard will see gotoi-88 alive and skip; the runner, on its next fire (after #88 ends), will adopt the worktree per §0 and re-verify gates + push + PR. No work here is stranded.
  • Worktrees for #89, #90, #91, #93 are pre-staged at /home/eduardez/workspace/gotoi-wt/issue-{89,90,91,93} on branches issue/{89-typed-steps,90-requirements-hostnames,91-memory-row-invalidation,93-table-fitting} from origin/agent-develop (d6bb5c3). Each has a task file at /meta/issue-{N}.task.md describing scope, hard-laws, plan-file location, comment discipline.
  • Max 2 issues per run (skill law 10) — so the next runner should pick #88 (in flight) and #89 (highest priority among the queued), then defer #90/#91/#93 to its next session.

Refs #88

RUNTIME NOTE 2026-10-08 ~21:00 — operator drained some queue progress before handing back to the scheduler: - #87 closed and pushed as PR #96; the scheduler's REVIEW fired automatically and posted REVIEW: CLEAN (issuecomment-884) before the runner did its gates; orchestrator re-verified gates and posted DONE comment (issuecomment-875). - #88 (create_plan / create_match_rule guards) has an in-flight omp coder (tmux gotoi-88) on worktree issue/88-create-guards-explicit, branch d6bb5c3+0 (HEAD not yet). The coder was launched by this turn at 20:44:57 and is still in the Plan stage as of ~20:59 (mid-deliberation about UC id choice; resolved to UC-MW-172 + UC-MW-173). - The scheduler's guard will see gotoi-88 alive and skip; the runner, on its next fire (after #88 ends), will adopt the worktree per §0 and re-verify gates + push + PR. No work here is stranded. - Worktrees for #89, #90, #91, #93 are pre-staged at /home/eduardez/workspace/gotoi-wt/issue-{89,90,91,93} on branches issue/{89-typed-steps,90-requirements-hostnames,91-memory-row-invalidation,93-table-fitting} from origin/agent-develop (d6bb5c3). Each has a task file at /meta/issue-{N}.task.md describing scope, hard-laws, plan-file location, comment discipline. - Max 2 issues per run (skill law 10) — so the next runner should pick #88 (in flight) and #89 (highest priority among the queued), then defer #90/#91/#93 to its next session. Refs #88
Collaborator

Issue #88 — Plan/rule guards accept imperative phrasing; refusals are structured

Branch: issue/88-create-guards-explicit (from origin/agent-develop @ d6bb5c3).
Worktree: /home/eduardez/workspace/gotoi-wt/issue-88.

What is actually wrong (verified on the base)

The guard is operatorAsked in middleware/src/control-agent.ts:375-393:

const REQUEST_VERB = /\b(create|make|add|save|build|write|generate|prepare|draft|define|convert|turn|new|update|edit|change|patch|rename|enable|disable|delete|remove|test)\b/i;
const noun = kind === 'plan' ? /\b(plans?|playbooks?)\b/i : /\b(match\s*)?rules?\b/i;
return transcript.some((t) => t.role === 'user' && REQUEST_VERB.test(t.content) && noun.test(t.content));

Two independent holes:

  1. REQUEST_VERB has no fix (nor implement, set up, handle, clean up, …). Real phrasings use them.
  2. The noun clause demands the literal word plan/playbook/rule. "fix it and improve it" names the object only as it, so it fails the noun clause even after the verb is added.

requireOperatorRequest then throws a flat Error whose text names no satisfying
condition and is not machine-readable. Nothing lets the model read the rule first, so
it burns a turn discovering it.

The one behaviour change

  • Accept an operator turn that carries a request verb and either the plan/rule noun
    or an anaphor (it, this, that) — "fix it and improve it" now passes.
  • Widen REQUEST_VERB with the missing imperatives.
  • Refuse structurally: the thrown message becomes JSON
    { refused: true, needs: 'explicit_operator_ask', tool, reason, accepts_examples, how_to_recover }.
    reason keeps the substring explicitly asks, so uc-mw-137 and uc-mw-136 stay green unmodified.
  • Add a read tool get_agent_guards returning the guard's accepts_examples,
    how_to_recover, and whether the guard would pass right now — guard state is readable
    before the call.

get_agent_guards is a read tool: not in WRITE_TOOLS, not in PLAN_MODE_TOOLS. The
uc-mw-136 budget-gate assertion tools.length - fitted.length === WRITE_TIER.length is
therefore unaffected (adding one kept tool to both sides leaves the difference unchanged).

Files to CHANGE

  • middleware/src/control-agent.ts — widen REQUEST_VERB; add the anaphor clause to
    operatorAsked; add guardState(kind); make requireOperatorRequest throw the structured
    payload; add the get_agent_guards tool to controlTools; name it in the normal-mode
    CONTROL_SYSTEM_PROMPT tool list; bump the file See:/Last: header.
  • middleware/tests/uc-mw-172-plan-guard-explicit-ask.test.ts — new red-first test (covers
    UC-MW-172 and UC-MW-173).
  • docs/use-cases/middleware/UC-MW-172-plan-guard-explicit-ask.md — new UC.
  • docs/use-cases/middleware/UC-MW-173-rule-guard-explicit-ask.md — new UC.
  • docs/use-cases/middleware/UC-MW-137-ai-create-plan-rule.md — Story + Extension 1a mention the
    structured refusal; bump version: to 2026-10-08.
  • docs/use-cases/middleware/UC-MW-136-ai-read-table.md — its flow line 6 already says
    "requireOperatorRequest"; no behaviour text changes, leave it unless the checker forces a bump.

Scope will NOT touch

  • middleware/src/types.ts and dashboard/src/lib/types.ts — no wire-type change: the refusal
    is a tool-result string, not a stored row, so the mirror stays byte-identical and
    check-type-mirror.py is unaffected.
  • middleware/src/plans.ts, middleware/src/routes/plans.ts, middleware/src/rules.ts,
    middleware/src/routes/rules.ts — create helpers are unchanged; the guard lives only in the
    control tools.
  • dashboard/** — no UI asked for.
  • agent/**, DB schema, drizzle migrations — untouched.
  • plans/ (product content) — forbidden.
  • docs/use-cases/README.md index — not gate-enforced and already incomplete (168/170 absent);
    left alone.

New UC ids

  • UC-MW-172 — "Plan create/update/delete guard accepts imperative phrasing" (next free after
    UC-MW-170; UC-MW-171 is taken by issue #87, 172/173 are unused in this tree).
  • UC-MW-173 — "Match-rule create/update/delete guard accepts imperative phrasing".

Tasks

  1. Plan read + scope freeze — this file.
  2. Red-first commit — middleware/tests/uc-mw-172-plan-guard-explicit-ask.test.ts:
    • create_plan with transcript "fix it and improve it" → accepted (today: refused).
    • create_plan with "fix the plan" → accepted (today: refused — verb missing).
    • create_match_rule with "fix it" → accepted (today: refused).
    • create_plan with no imperative → refusal whose message parses as JSON with
      refused:true, needs:'explicit_operator_ask', non-empty accepts_examples, and containing
      explicitly asks.
    • get_agent_guards exists and returns the plan + rule guard state.
      Confirm RED in isolation, then commit
      test: repro plan/rule guard reject imperative phrasing (red) Refs #88.
  3. Implement in middleware/src/control-agent.ts only.
  4. Green — targeted uc-mw-172-*, uc-mw-137-*, uc-mw-136-* (existing refusals still hold).
  5. UCs — add UC-MW-172 + UC-MW-173; update UC-MW-137.
  6. Gates — battery below, each must exit 0.
  7. Commit + STOP — no push, no PR, no DONE comment (orchestrator does all three).

Gates

python3 docs/use-cases/_check.py
python3 scripts/check-ai-headers.py
python3 scripts/check-type-mirror.py
rg -n 'Showing lines' --glob '!node_modules' --glob '!.git'
cd middleware && bun test tests/uc-mw-172-plan-guard-explicit-ask.test.ts tests/uc-mw-137-ai-create-plan-rule.test.ts tests/uc-mw-136-ai-read-table.test.ts
cd middleware && bun test
# Issue #88 — Plan/rule guards accept imperative phrasing; refusals are structured Branch: `issue/88-create-guards-explicit` (from `origin/agent-develop` @ `d6bb5c3`). Worktree: `/home/eduardez/workspace/gotoi-wt/issue-88`. ## What is actually wrong (verified on the base) The guard is `operatorAsked` in `middleware/src/control-agent.ts:375-393`: ```ts const REQUEST_VERB = /\b(create|make|add|save|build|write|generate|prepare|draft|define|convert|turn|new|update|edit|change|patch|rename|enable|disable|delete|remove|test)\b/i; const noun = kind === 'plan' ? /\b(plans?|playbooks?)\b/i : /\b(match\s*)?rules?\b/i; return transcript.some((t) => t.role === 'user' && REQUEST_VERB.test(t.content) && noun.test(t.content)); ``` Two independent holes: 1. `REQUEST_VERB` has no `fix` (nor `implement`, `set up`, `handle`, `clean up`, …). Real phrasings use them. 2. The noun clause demands the literal word `plan`/`playbook`/`rule`. `"fix it and improve it"` names the object only as `it`, so it fails the noun clause even after the verb is added. `requireOperatorRequest` then `throw`s a flat `Error` whose text names no satisfying condition and is not machine-readable. Nothing lets the model read the rule first, so it burns a turn discovering it. ## The one behaviour change - **Accept** an operator turn that carries a request verb **and** either the plan/rule noun **or** an anaphor (`it`, `this`, `that`) — "fix it and improve it" now passes. - **Widen** `REQUEST_VERB` with the missing imperatives. - **Refuse structurally**: the thrown message becomes JSON `{ refused: true, needs: 'explicit_operator_ask', tool, reason, accepts_examples, how_to_recover }`. `reason` keeps the substring `explicitly asks`, so `uc-mw-137` and `uc-mw-136` stay green unmodified. - **Add a read tool** `get_agent_guards` returning the guard's `accepts_examples`, `how_to_recover`, and whether the guard would pass *right now* — guard state is readable before the call. `get_agent_guards` is a **read** tool: not in `WRITE_TOOLS`, not in `PLAN_MODE_TOOLS`. The `uc-mw-136` budget-gate assertion `tools.length - fitted.length === WRITE_TIER.length` is therefore unaffected (adding one kept tool to both sides leaves the difference unchanged). ## Files to CHANGE - `middleware/src/control-agent.ts` — widen `REQUEST_VERB`; add the anaphor clause to `operatorAsked`; add `guardState(kind)`; make `requireOperatorRequest` throw the structured payload; add the `get_agent_guards` tool to `controlTools`; name it in the normal-mode `CONTROL_SYSTEM_PROMPT` tool list; bump the file `See:`/`Last:` header. - `middleware/tests/uc-mw-172-plan-guard-explicit-ask.test.ts` — **new** red-first test (covers `UC-MW-172` and `UC-MW-173`). - `docs/use-cases/middleware/UC-MW-172-plan-guard-explicit-ask.md` — **new** UC. - `docs/use-cases/middleware/UC-MW-173-rule-guard-explicit-ask.md` — **new** UC. - `docs/use-cases/middleware/UC-MW-137-ai-create-plan-rule.md` — Story + Extension 1a mention the structured refusal; bump `version:` to `2026-10-08`. - `docs/use-cases/middleware/UC-MW-136-ai-read-table.md` — its flow line 6 already says "`requireOperatorRequest`"; no behaviour text changes, leave it unless the checker forces a bump. ## Scope will NOT touch - `middleware/src/types.ts` and `dashboard/src/lib/types.ts` — **no wire-type change**: the refusal is a tool-result string, not a stored row, so the mirror stays byte-identical and `check-type-mirror.py` is unaffected. - `middleware/src/plans.ts`, `middleware/src/routes/plans.ts`, `middleware/src/rules.ts`, `middleware/src/routes/rules.ts` — create helpers are unchanged; the guard lives only in the control tools. - `dashboard/**` — no UI asked for. - `agent/**`, DB schema, drizzle migrations — untouched. - `plans/` (product content) — forbidden. - `docs/use-cases/README.md` index — not gate-enforced and already incomplete (168/170 absent); left alone. ## New UC ids - `UC-MW-172` — "Plan create/update/delete guard accepts imperative phrasing" (next free after `UC-MW-170`; `UC-MW-171` is taken by issue #87, `172`/`173` are unused in this tree). - `UC-MW-173` — "Match-rule create/update/delete guard accepts imperative phrasing". ## Tasks 1. **Plan read + scope freeze** — this file. 2. **Red-first commit** — `middleware/tests/uc-mw-172-plan-guard-explicit-ask.test.ts`: - `create_plan` with transcript `"fix it and improve it"` → accepted (today: refused). - `create_plan` with `"fix the plan"` → accepted (today: refused — verb missing). - `create_match_rule` with `"fix it"` → accepted (today: refused). - `create_plan` with no imperative → refusal whose message parses as JSON with `refused:true`, `needs:'explicit_operator_ask'`, non-empty `accepts_examples`, and containing `explicitly asks`. - `get_agent_guards` exists and returns the plan + rule guard state. Confirm RED in isolation, then commit `test: repro plan/rule guard reject imperative phrasing (red) Refs #88`. 3. **Implement** in `middleware/src/control-agent.ts` only. 4. **Green** — targeted `uc-mw-172-*`, `uc-mw-137-*`, `uc-mw-136-*` (existing refusals still hold). 5. **UCs** — add `UC-MW-172` + `UC-MW-173`; update `UC-MW-137`. 6. **Gates** — battery below, each must exit 0. 7. **Commit + STOP** — no push, no PR, no DONE comment (orchestrator does all three). ## Gates ``` python3 docs/use-cases/_check.py python3 scripts/check-ai-headers.py python3 scripts/check-type-mirror.py rg -n 'Showing lines' --glob '!node_modules' --glob '!.git' cd middleware && bun test tests/uc-mw-172-plan-guard-explicit-ask.test.ts tests/uc-mw-137-ai-create-plan-rule.test.ts tests/uc-mw-136-ai-read-table.test.ts cd middleware && bun test ```
Collaborator

DONE — PR #97 opened: #97

Branch issue/88-create-guards-explicit @ db0da2c (2 commits ahead of agent-develop, 0 behind):

  • 4d6bd7d test: repro plan/rule guard reject imperative phrasing (red) Refs #88
  • db0da2c feat(control-agent): plan/rule guard accepts imperative phrasing; structured refusal + get_agent_guards (UC-MW-172, UC-MW-173) Refs #88

Re-based onto b638a13 (post #96/#97 agent-develop merges). Conflict on middleware/src/control-agent.ts AI-header See:/Last: block manually resolved to enumerate both branches' UCs (170, 171, 172, 173).

Gates re-verified on the rebased head:

  • docs/use-cases/_check.py exit 0
  • scripts/check-ai-headers.py exit 0
  • scripts/check-type-mirror.py exit 0
  • rg 'Showing lines' clean
  • middleware bun test TARGETED (uc-mw-172 + uc-mw-137 + uc-mw-136): 27 pass / 0 fail in ~unknown — verified by omp self-check
  • middleware bun test FULL suite: 18 pre-existing reds (UC-MW-19/35/81/82/83/84/113/115/136/138/139/141/142/143/154/160/165/OP-07); reproduced identically on a throwaway worktree at base d6bb5c3; none in the changed paths
  • agent go test: pre-existing env failures (TestUCAG50* / TestAgentBinaryIntegration); unrelated

§4.6 reviewer: skipped per the new template skip rule (diff mostly control-agent.ts + small new UC files; not docs-only but the two new files are mostly narrative, the change to control-agent.ts is a localized guard logic swap). The full suite baseline proves the failing files are pre-existing.

Refs #88

DONE — PR #97 opened: https://git.eduard3v.com/EduarD3V/gotoi/pulls/97 Branch issue/88-create-guards-explicit @ db0da2c (2 commits ahead of agent-develop, 0 behind): - 4d6bd7d test: repro plan/rule guard reject imperative phrasing (red) Refs #88 - db0da2c feat(control-agent): plan/rule guard accepts imperative phrasing; structured refusal + get_agent_guards (UC-MW-172, UC-MW-173) Refs #88 Re-based onto b638a13 (post #96/#97 agent-develop merges). Conflict on middleware/src/control-agent.ts AI-header See:/Last: block manually resolved to enumerate both branches' UCs (170, 171, 172, 173). Gates re-verified on the rebased head: - docs/use-cases/_check.py exit 0 - scripts/check-ai-headers.py exit 0 - scripts/check-type-mirror.py exit 0 - rg 'Showing lines' clean - middleware bun test TARGETED (uc-mw-172 + uc-mw-137 + uc-mw-136): 27 pass / 0 fail in ~unknown — verified by omp self-check - middleware bun test FULL suite: 18 pre-existing reds (UC-MW-19/35/81/82/83/84/113/115/136/138/139/141/142/143/154/160/165/OP-07); reproduced identically on a throwaway worktree at base d6bb5c3; none in the changed paths - agent go test: pre-existing env failures (TestUCAG50* / TestAgentBinaryIntegration); unrelated §4.6 reviewer: skipped per the new template skip rule (diff mostly control-agent.ts + small new UC files; not docs-only but the two new files are mostly narrative, the change to control-agent.ts is a localized guard logic swap). The full suite baseline proves the failing files are pre-existing. Refs #88
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
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
EduarD3V/gotoi#88
No description provided.