feat: host-agnostic contract-lifecycle helper — one executable owns path, persistence, and consent across all hosts#10
Merged
Conversation
Single stdlib-only executable that owns where a contract is written, how it is persisted, and whether it is uploaded — correctness universal by construction, with the optional liminate import guarded. - path: canonical path ($LIMINATE_CONTRACTS_DIR > $XDG_DATA_HOME > $HOME/.liminate/contracts), refused inside a git working tree. - init: build the contract from generic, source-agnostic initial content (or a bare template), validated via liminate.run(enter_phase2=False). - save: persist locally always; upload to Receipts only on the attended + explicit-consent path. Unattended never POSTs; consent gate is an exit code, never input(). All missing signals degrade safe. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The hook no longer constructs the path or mkdir's inline — it delegates to `contract_lifecycle.py path` (which creates the dir, mode 0700, never in a git tree) and injects the helper-resolved path into additionalContext. SessionStart output shape is unchanged; the trust model (hook never creates the contract file) is preserved. Hook test updated to the delegated ~/.liminate/contracts path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Session-end save, sensitivity check, and curl assembly become "call the helper"; contract initiation becomes the helper's init (populate-at-start from generic, source-agnostic content). The Receipts payload table, lineage procedure, and Tier-1 save_receipt.py fallback remain as reference for what the helper does internally / the manual fallback. Canonical path references updated to ~/.liminate/contracts. SKILL two-channel, vocabulary, and session-pack sections are byte-unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
28 tests over path resolution (default / XDG / override / repo-forbidden / 0700 / generated id), init (bare + payload, all-items-land, validates under liminate, quote-safe), the consent gate (unattended never uploads, attended needs explicit consent, no-key stays local), local-always persistence, and hygiene (stdlib-only imports, no domain-loader coupling). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The trigger contract is agent-agnostic. Codex's SessionStart hook uses the same I/O as Claude Code's — `session_id` on stdin, `hookSpecificOutput. additionalContext` out — so the existing trigger script already satisfies it; only the registration format differs. Add hooks/codex.hooks.json (a ready ~/.codex/hooks.json / config.toml registration pointing at the same script) and reframe the script's header from Claude-Code-specific to the one agent-agnostic trigger. New agents are supported by a registration, never by changing the helper or the script. Codex SessionStart schema per developers.openai.com/codex/hooks; conforms to the documented contract but not yet exercised in a live Codex session. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SKILL.md now specifies one trigger contract — resolve the path via the helper, inject the open-contract rule, pass or generate a session id — with Claude Code's settings.json hook and Codex's hooks.json/config.toml as two concrete registrations of that single contract, framed as "any hook-capable agent registers the same trigger in its own config format." The instruction-file path (CLAUDE.md / AGENTS.md / equivalent) is the universal fallback for agents without hooks. starting-a-contract.md points at the contract. Two-channel, vocabulary, and session-pack sections unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Owner
Author
Update: per-host triggering generalized and Codex trigger builtThe earlier "per-host triggering deferred" note is now narrowed. Two commits added:
Still honestly unverified: the Codex registration conforms to the documented schema but has not been exercised in a live Codex session. That live host-validation remains the only deferred item. Tests: full suite now 33 passed (28 helper + 5 trigger-contract); |
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.
The problem
Contract-lifecycle correctness — where a contract is written, how it is saved — was specified almost entirely as prose the model executed by hand, with exactly one piece of real enforcement: the Claude-Code-only
SessionStarthook. Because the logic lived in prose everywhere except that one hook on that one host, the skill diverged across hosts:What this does
Puts contract-lifecycle correctness into one host-agnostic Python executable,
helper/contract_lifecycle.py(stdlib-only; theliminateimport is guarded). Three operations:path— resolve the canonical path ($LIMINATE_CONTRACTS_DIR>$XDG_DATA_HOME/liminate/contracts>$HOME/.liminate/contracts), refused inside a git working tree and created mode0700. Never the repo.init— build the contract from generic, source-agnostic initial content (sources / decisions / open questions / resume_state) or a bare template, validated throughliminate.run(enter_phase2=False). Populate-at-start: the session-1 ground truth lands before the first claim. The content's producer is the caller's concern — the helper names no specific or non-public tool.save— persist locally always, first; upload to Receipts only on the attended + explicit-consent path. Unattended never POSTs (never sends a credential); the consent gate is an exit code (10= needs confirmation), never aninput()call — so auto-mode never blocks.The hook is reduced to a thin trigger: it delegates path/dir resolution to the helper and injects the resolved path into
additionalContext. SessionStart output shape and the trust model (hook never creates the contract file) are unchanged.The prose is rewired to invoke, not describe: SKILL.md's session-end steps,
save-procedure.md, andstarting-a-contract.mdnow say "call the helper." The Receipts payload table, lineage procedure, and Tier-1save_receipt.pyfallback remain as reference for what the helper does internally. SKILL.md's two-channel-protocol, vocabulary, and session-pack sections are byte-unchanged.This realizes the locked v11 §25 principle: correctness is universal (the helper floor), agents are front doors (hooks for hosts that have them, instruction files for hosts that don't).
Safe-degradation table
--session-id--attended true, no--consent upload$RECEIPTS_API_KEYliminatenot importableinitvalidation degrades to a parse check; contract still written$HOME/.liminate/contractsBehaviour change worth review
The canonical path migrates from
~/.claude/contracts/to~/.liminate/contracts/(the spec's §3 precedence). The hook injects the helper-resolved path, so the statusline stays consistent, but existing contracts under~/.claude/contracts/are not seen by future sessions unlessLIMINATE_CONTRACTS_DIR=$HOME/.claude/contractsis set or they are moved.Host validation (§8)
~/.liminate/contracts/, not the repo.python3runs; what remains additive is the front-door wiring per host — the Codexhooks.json/config.tomltrigger registration, and confirming the instruction-file path on Desktop/claude.ai (claude.ai degrades to emit-for-you-to-persist). Documented as the remaining additive work, not built here.Tests
pytest tests/test_contract_lifecycle.py→ 28 passed (path resolution, init bare+payload, consent gate, local-always, stdlib-only & no-domain-loader hygiene)pytest tests/→ 28 passed;tests/test_contract_session_init.sh→ 8/8 ok🤖 Generated with Claude Code