Skip to content

fix: the two things the re-run left as decisions - #208

Merged
vicenteliu merged 3 commits into
mainfrom
fix/propose-actions-wiring-and-one-local-identity
Aug 19, 2026
Merged

fix: the two things the re-run left as decisions#208
vicenteliu merged 3 commits into
mainfrom
fix/propose-actions-wiring-and-one-local-identity

Conversation

@vicenteliu

Copy link
Copy Markdown
Owner

#207 closed with two items filed as posture questions rather than defects.
Chasing them turned one into a bug and settled the other.

C — propose_actions never reached the model

Filed as "the feature is finished, nobody opts in." The interesting half is what
turned up on the way to fixing that: opting in would not have worked.

ticket_summary appended _PROPOSE_ACTIONS_PROMPT to the system prompt before
the prefetch branch, and _do_prefetch returns pb.system_prompt + addendum
rebuilt from the playbook, so the opt-in was discarded. Every playbook in the
repo is mode: "prefetch" except pb_vendor_doc_en, which is a vendor-doc
summariser and would never propose a diagnostic.

The behaviour gate has a case for exactly this behaviour and it passes, because
it builds the system prompt by hand from _PROPOSE_ACTIONS_PROMPT. It proved
the prompt works; nothing proved the prompt arrives. That is the same seam
#201#207 kept finding, one layer up: the gate covers what the model does with
an instruction, not whether the instruction is still there by the time the model
sees it.

The opt-in now applies after the branch. The new test asserts it against the
trace — which records the system prompt actually fed to the model —
parametrised over both retrieval modes; it fails on prefetch before this
change and passes after.

The default stays off. A proposal is a command somebody reads at 2am and the
approval gate behind it is a heuristic denylist that says so in its own
docstring — shipping it on is a posture change nobody asked for, and ADR-0028
says existing playbooks are unaffected. What was actually wrong is that the key
was named nowhere an operator would look, so the two incident playbooks now
carry it commented out with the reason beside it:

# Read-only diagnostics the Session may propose for a human to run (ADR-0028).
# Off by default: a proposal is a command someone will read at 2am, and the
# approval gate behind it is a heuristic denylist, not a proof. Uncomment to
# opt this playbook in — `intent` is a schema `const`, so nothing that mutates
# can be expressed either way.
# propose_actions: true

Checked that the admin model-list editor still round-trips both files
byte-identically — #204 fixed comment loss in that path, and this adds comments
to files it rewrites.

D — one human on one machine got two names

opspilot workingset status said "No working set open" while the web UI had one
open for the same person. The CLI writes cli:<osuser>, the loopback API writes
local-dev, and a Working set is per-owner by construction.

It reaches past the Working set: a Memory entry admitted from the CLI carried a
different actor than the same person's entry from the UI, and actor is the
field ADR-0030 leans on to say who decided what.

Both names are deliberate and both docstrings explain themselves. The split
between them was not. The CLI now answers local-dev under exactly the
condition auth.deps falls back on — no user account, no service token — with a
missing users table counting as no users, and the check is a read-only query
so asking does not create the schema. With a user or a token configured it goes
back to cli:<osuser>: then it genuinely has no auth context and should not
borrow a name it cannot prove.

Verified against a running server:

API  POST /api/working-set        → ws_0724248c
CLI  opspilot workingset status   → ws_0724248c — pods in staging keep restarting
CLI  opspilot workingset close    → closed
API  GET  /api/working-set        → {"working_set": null}
CLI  opspilot memory add …        → mem_a2bd3979
API  GET  /api/memory             → actor = local-dev

Verification

Full suite 1357 passed — 7 new. ruff, ruff format, mypy(154) clean. The one local
failure is test_providers_ollama.py::TestIntegration::test_chat_smoke, which
wants gemma4:e4b from a local Ollama that does not have it; CI deselects it.

behaviour-gate: 6 passed — memory injection 3/3, conflict reported 3/3, distillation keeps dead ends 3/3, proposals stay read-only 3/3, memory proposal 3/3, memory proposal restraint 3/3

🤖 Generated with Claude Code

vicenteliu and others added 3 commits August 19, 2026 05:40
…ships

ADR-0028 says playbooks opt in with `propose_actions: true`. None does, which is
how this stayed hidden: the opt-in was appended to the system prompt *before* the
prefetch branch, and `_do_prefetch` rebuilds the prompt from `pb.system_prompt`,
so the instruction was dropped on the floor. Every playbook in the repo is
`mode: "prefetch"` except `pb_vendor_doc_en`, so opting in would have changed
nothing for anyone who tried.

The behaviour gate has a case for this exact behaviour and it passes, because it
builds the system prompt by hand from `_PROPOSE_ACTIONS_PROMPT`. It proved the
prompt works. Nothing proved the prompt arrives.

The opt-in now applies after the prefetch branch, so both retrieval modes carry
it, and the new test asserts it against the trace — which records the system
prompt actually fed to the model — parametrised over `tool` and `prefetch`. It
fails on prefetch before this change.

The default stays off. A proposal is a command somebody reads at 2am and the
approval gate behind it is a heuristic denylist that says so in its own
docstring, so shipping it on would be a posture change nobody asked for. What
was wrong was discoverability: outside ROADMAP the key was named nowhere, so the
two incident playbooks now carry it commented out with the reason beside it. The
admin model-list editor still round-trips both files byte-identically.

behaviour-gate: 6 passed — memory injection 3/3, conflict reported 3/3,
distillation keeps dead ends 3/3, proposals stay read-only 3/3, memory proposal
3/3, memory proposal restraint 3/3

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`opspilot workingset status` reported "No working set open" while the web UI had
one open for the same person, on the same install. A Working set is per-owner by
construction (ADR-0032), and the two surfaces disagreed on who the owner was:
the CLI writes `cli:<osuser>`, the loopback API writes `local-dev`.

Both names are deliberate and both docstrings say why. The split between them
was not. It reaches further than the Working set — a Memory entry admitted from
the CLI carried a different actor than the same person's entry admitted from the
UI, and `actor` is the field ADR-0030 leans on to say who decided what.

The CLI now answers `local-dev` under exactly the condition `auth.deps` falls
back on: no user account and no service token. A missing `users` table counts as
no users, because it means auth was never set up. The check is a read-only
query, so asking the question does not create the schema.

The moment identity means something they diverge again, on purpose: with a user
or a token configured the CLI has no auth context and must not borrow a name it
cannot prove, so `cli:<osuser>` returns — advisory, not evidence.

Verified against a running server: the API opens a Working set, the CLI sees it,
closes it, the API confirms it closed, and a Memory entry added from the CLI
comes back over REST with `actor: local-dev`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`propose_actions` and the CLI/API identity split were both filed as posture
questions rather than bugs. Chasing them turned one into a defect (the opt-in
never reached the model in prefetch mode) and settled the other (one
unconfigured install, one operator, one name).

The proposed-actions section now says the default is off *deliberately* rather
than by omission, and records that until today opting in would not have worked
regardless.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@vicenteliu
vicenteliu merged commit c8bc67e into main Aug 19, 2026
4 checks passed
@vicenteliu
vicenteliu deleted the fix/propose-actions-wiring-and-one-local-identity branch August 19, 2026 13:15
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.

1 participant