Move the nightly mechanism into an app-owned operating contract - #20
Merged
Merged
Conversation
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>
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.jsongo, 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, thestate.jsonshape (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.pyappends 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_textparameters are renamedsystem_promptto 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.jsonpath 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.pybuild_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.🤖 Generated with Claude Code