Skip to content

Move the nightly mechanism into an app-owned operating contract - #20

Merged
miljanm merged 2 commits into
mobius-os:mainfrom
miljanm:feat/operating-contract-split
Jul 30, 2026
Merged

Move the nightly mechanism into an app-owned operating contract#20
miljanm merged 2 commits into
mobius-os:mainfrom
miljanm:feat/operating-contract-split

Conversation

@miljanm

@miljanm miljanm commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Problem

The reflection skill is agent-editable by design — app updates never touch it, so the nightly agent can evolve its own judgment. But that same property makes it the wrong home for mechanism: any operational rule written there (who sends the morning push, where the brief and state.json go, the question-carrier format) drifts the moment the app updates. That drift is not hypothetical: the skill's old "send the morning notification" rule survived the wrapper taking over the send, and both fired on 2026-07-28 — the duplicate push #13 then guarded against. By that point the "exactly one push per brief" invariant was defended by four stacked layers (the wrapper send, a skill-text warning, the runner's tool denylist, and the #13 dedupe guard) — a structural smell, not four independent bugs.

Change

Make the split structural — one rule, one home:

  • operating-contract.md (new) holds the mechanism: the storage-vs-source split and canonical settings path, brief path + template + hand-written fallback, the state.json shape (also the push body), push ownership, the question-carrier format, and the commit helper. It ships beside the runner, so it updates atomically with every app update.
  • reflection_runner.py appends the contract to the system prompt after the skill each run (build_system_prompt()), failing loudly when the contract is missing — the same posture as a missing skill. skill_text parameters are renamed system_prompt to match what now flows through.

The skill keeps judgment only. Migration is self-serve: the contract's preamble instructs the nightly agent to delete stale mechanical duplicates from its own evolved skill, so existing instances converge without a forced notebook rewrite — and the authoritative statement always reads last in the composed prompt either way.

Auditing for this bug class found a second live instance: the skill still pointed at the legacy /data/apps/reflection/settings.json path after the runner canonicalized settings to numeric storage. The contract now states the canonical path.

Kept deliberately

The runner's tool denylist (one cheap line) and the #13 dedupe guard (delivery over a network should be idempotent as a property) stay — they are correct on their own merits, not compensations for skill drift. The skill-text warnings are the layer this removes.

Follow-up (separate)

The seed skill in the platform image (seed-skills/reflection.md) still carries the old mechanical text; trimming it to judgment-only belongs in a mobius-os/mobius PR once this lands. Until then a fresh install sees the mechanics twice (skill + contract) with the contract authoritative — harmless and self-correcting via the preamble.

Testing

  • python3 -m py_compile reflection_runner.py
  • Composition dry-run: build_system_prompt() returns skill + contract, the contract resolves beside the runner, and every mechanic removed from a judgment-trimmed skill is still present in the composed prompt.
  • Installed on my instance with the local skill trimmed to judgment-only; tonight's run is the first live pass of the composed prompt.

🤖 Generated with Claude Code

The agent-editable reflection skill is deliberately never touched by app
updates, so any mechanic written there (output paths, the state.json
recipe, who sends the morning push, the question-carrier format)
inevitably drifts from the code — that drift caused the 2026-07-28
duplicate morning-brief push that mobius-os#13 then had to guard against.

This makes the split structural: mechanics now live in
operating-contract.md, shipped beside the runner and appended to the
system prompt fresh every run, so they update atomically with the app.
The skill keeps judgment only. The contract's preamble instructs the
nightly agent to delete stale mechanical duplicates from its own evolved
skill, so existing instances migrate themselves without a forced
notebook rewrite. The tool denylist and the mobius-os#13 dedupe guard stay — they
are correct on their own merits, not compensations.

Co-authored-by: Möbius Agent <mobius-agent@users.noreply.github.com>
@miljanm miljanm added the enhancement New feature or request label Jul 30, 2026
The timeout subtest copies the runner into a bare fake scripts dir; the
runner now resolves operating-contract.md beside itself and fails loudly
without it, so the subtest died at exit 1 before reaching the timeout
path it exercises. A relocated runner home must carry the contract, as a
real install does.

Co-authored-by: Möbius Agent <mobius-agent@users.noreply.github.com>
@miljanm
miljanm merged commit 688db23 into mobius-os:main Jul 30, 2026
1 check passed
@miljanm
miljanm deleted the feat/operating-contract-split branch July 30, 2026 18:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant