Repository navigation
2.0.0: session-bound journals - #1
Conversation
…op-hook loops Journal selection ----------------- The pointer and the nudge both decided "newest journal" by mtime alone. Whenever a filesystem hands several journals the same timestamp -- a fresh clone, a branch checkout, an unpacked archive -- bash 3.2 compares -nt at whole-second resolution, the comparison ties, and the loop keeps whichever name the glob yielded first: the OLDEST journal. A teammate cloning the repo got a January journal presented as the frontier while August's sat unread. mtime still leads, so a journal appended to across days stays live; the filename, which the spec defines as the session-start time, now settles ties. The same corrected loop lands in the spine-promotion nudge. The pointer also replayed a DONE journal's next step verbatim. Running it in this repository injected "No repository work remains" as a fresh agent's entire first minute. DONE is a finished session, so the pointer now prefers the newest journal still ACTIVE or BLOCKED and, when every journal is closed, says so instead of pretending. Next-step headings match case-insensitively: a journal written from memory routinely title-cases the heading, and an exact match silently dropped the one section the hook exists to deliver. Secrets guard ------------- The guard blocked the exact remediation its own message prescribed. `password: <redacted>` and `token: stored-in-ci-secrets` both exited 2, because the pattern flagged any 6+ non-space token after a credential key. The guard deliberately runs ahead of the stop_hook_active bail, so the runtime could not break the resulting block -> redact -> block loop. Values are now inspected rather than merely detected: a report needs credential length, a credential alphabet, and no placeholder shape. The error message finally names the <!-- journal-secrets-ok --> escape it always had. The digest-context exemption matched unanchored substrings, so "Avoid", "Android" and "paranoid" all satisfied `oid` and silently disabled hex detection for neighbouring lines. Context now compares whole words. Stop-hook robustness -------------------- stdin is drained in full instead of one line, so pretty-printed hook JSON no longer defeats stop_hook_active AND suppresses the marker fallback at once; the per-read timeout stays, because a plain `cat` would hang a Stop hook against a runner that never closes stdin. The one-shot marker gained a TTL: it used to outlive the session that armed it and silently swallow the next session's first nudge. Freshness measured a property the agent could set directly -- a bare `touch` satisfied the gate with zero characters changed. Where git can tell us, the gate now requires the journal to have actually moved when other files did. SESSION_JOURNAL_SKIP documents the escape for trivial sessions that need no journal, and JOURNAL_REQUIRE_CONTENT_CHANGE=0 restores mtime-only behaviour. The secrets guard still runs under both. Tests: 41 -> 70 checks.
…doctor The installer refused after it had already acted. An unmatched sentinel in CLAUDE.md produced "refusing to modify it" and exit 65 with both hook scripts already copied, both hooks already merged live into settings.json, and a stray .tmp file left in the repository: enforcement wired up with no instruction block to explain it. Every refusal -- invalid JSON, unmatched sentinels, a non-root git target, a journal directory that symlinks out of the repository -- now happens in a preflight, before the first byte. Re-running install.sh is the only upgrade path a clone-based project has, and it silently deleted all three things ADOPT.md instructs the adopting agent to write: the JOURNAL_FRESH_SECS override on the generated command, the adapted sessions README, and any local edit inside the instruction block. All three are preserved now, each with a line saying so, and --refresh-docs regenerates them deliberately. The installer never shipped the journal template. Both the pointer hook and the generated README told a fresh agent to start from a local template, and neither the installer nor ADOPT.md ever placed one -- ADOPT.md sends the agent to clone upstream "to a temporary location". Every first journal was therefore written from an agent's memory of a specification it had not read, which is where heading drift comes from. TEMPLATE.md now ships. Installing into a subdirectory of a git repository used to succeed and then fail forever: the generated command resolves its root at runtime with git rev-parse --show-toplevel, so it pointed at a path that did not exist. New: doctor.sh. ADOPT.md Phase C is a verification matrix the installing agent runs once and nobody can run again, yet every failure mode here is silent -- a SessionStart hook that never fires looks exactly like a session with no journal. The doctor re-runs that matrix at any time, and adds the checks adoption never had: is the installation committed, will a teammate actually receive it, does the registered command still resolve, which journal would the pointer deliver right now. Also: --plan-pointer, so the deterministic install can produce THIN mode instead of always contradicting ADOPT.md's instruction to point at the strongest governing artifact; a VERSION file stamped into the installed hooks so an adopted repo can tell what it has. Tests: 70 -> 89 checks (grep -c "^@test" tests/*.bats).
…d the discipline The project's own evidence artifact had already drifted: docs/verification.md claimed "38/38" in three places while the suite had grown past it. That number came from a journal line the next agent believed and repeated -- which is the compounding failure mode of a journaling product, and one no amount of shell hardening fixes. The record now separates a dated adoption snapshot, which is never rewritten, from live gates that tests/docs.bats holds to the code: the stated bats count, the documented environment variables, the documented installer flags, and the spec/hook agreement about DONE. The specification gains a "Selecting the live journal" section. Selection was the single most consequential behaviour in the product and the only one left entirely to the implementation, which is how it managed to be wrong in two independent ways at once. Evidence guidance is now explicit: cite something a reader can re-run, because a count frozen at write time rots and the journal is the highest-trust artifact a fresh agent reads. Dogfooding: this repository now has the discipline installed on itself. It claimed to dogfood while carrying one journal from launch day and no hooks, so commit 0016aa6 changed hook behaviour here with no journal written -- the Stop hook was either bypassed or not firing in the maintainer's own repo. Installing it settles that, and turns CI's ./doctor.sh step into a real check. CI: actions pinned by commit SHA and verified against their tags (the gitleaks tarball was already SHA-pinned, so a mutable action ref was the weaker link); a bash 3.2 leg, because that compatibility claim was the one thing untested and the selection bug lived precisely in bash 3.2's whole-second -nt comparison; and the secrets gate now walks every tracked journal instead of only the newest, which its own step name always claimed it did. Adds CHANGELOG.md with upgrade notes and SECURITY.md, which a project that copies executable shell into a stranger's repository and wires it into two lifecycle events should have had from the start. Tests: 89 -> 95 checks.
The contract ended at 'installed for you' and never reached 'your team is using this'. Nothing told the adopting agent to commit .claude/, nothing checked whether it was gitignored, and nothing mentioned that a teammate's first clone is the entire distribution mechanism. Phase D covers that now. Phase C was a matrix only the installing agent could ever run. It now leads with ./doctor.sh and keeps only the behavioural cases the doctor cannot construct -- including two new ones the old matrix could not have caught: a DONE journal must not be replayed as live work, and the redaction the secrets guard prescribes must actually clear the block. Also documents --plan-pointer, the shipped TEMPLATE.md, the non-destructive re-run as the upgrade path, and SESSION_JOURNAL_SKIP -- without a documented escape for trivial sessions, a team learns to route around the Stop hook, and the discipline dies quietly.
…fixes A four-lens review of this branch found that two of the fixes had broken what they were meant to protect. Both were the changes flagged as riskiest when the branch was written, and both were wrong in the opposite direction to the fear. Secrets guard: the rewrite made detection materially weaker while every test stayed green, because the suite only ever exercised bare, unquoted values. `PASSWORD="S3cr3tP4ssw0rdXyz"`, a backticked value, a markdown-emphasised value, and `P@ssw0rd!Secret1` all went from blocked to allowed. Two rules were responsible: quotes and backticks were read as evidence of a placeholder, and a credential-alphabet whitelist rejected any value containing punctuation -- which is backwards, since punctuation makes a value more password-like, not less. Decoration is now stripped before the check and the whitelist is gone, so only a placeholder shape can exempt a value. The matcher also stopped at the first credential per line, so a line that redacted its first value and forgot its second was exempted whole; every key on the line is examined now. An ellipsis is recognised as an elided example, which is how documentation refers to a secret without containing one. Freshness gate: it equated "the journal changed" with "the journal is dirty in the working tree", so committing the journal -- the workflow this project teaches -- turned the gate hostile. Any remaining dirty path, down to an untracked .DS_Store, produced a Stop block asserting the journal's content had not changed seconds after it changed and was committed. Untracked files are now ignored, a journal carried by the most recent commit counts as recorded, and the message states what git can actually prove. Also from the review: - A plain installer rerun forgot --sessions-dir and silently relocated the journal directory, orphaning every existing journal behind hooks that now pointed at an empty default. It reports success while doing so. The rerun adopts the configured directory unless --sessions-dir says otherwise. - preserved_env stripped quoted segments before matching, so NAME="value" collapsed to NAME= and was dropped -- exactly the override it exists to keep. It now parses the leading assignments of the matching command with quoting understood, and reads only the command registered under the relevant event, so an unrelated hook that merely mentions the script name cannot contribute text to a generated command. - doctor.sh credited a command found under any event, and a greedy sed reduced `docs/sessions` to `sessions`, failing three checks against a healthy install and then reporting "no journal to scan for secrets" over a real journal. - The stdin drain grew quadratically and had no total deadline, so a large or slow payload could stall a Stop hook. It is bounded by line count, deadline, and per-read timeout, and no longer leaks a bash read error into the message. - Both stat fallbacks validate that they got a number. - A newline in --plan-pointer is refused in the preflight, not after writing. - Uninstall checks both instruction files, because it strips both regardless of flags, and removes files it created rather than leaving them zero-byte. - The spine-promotion hook matches headings case-insensitively like the core hooks and the spec, instead of calling a title-cased THIN journal DETAILED and blocking Stop with a false statement. - The redundant bash32 CI job is replaced by an assertion in the macOS leg that the interpreter the shebangs select really is 3.2; a separate job added no coverage, and the CHANGELOG claim is corrected to match. This journal's own "Commits made" section cited a hash that no longer existed after an amend -- the precise rot the new spec section warns about. Fixed, and SECURITY.md now discloses the guard's two deliberate exemptions. Tests: 95 -> 123 checks.
A rewritten commit hash and a superseded test count -- both cited in the journal this branch added as its dogfood, and both exactly the rot the new specification section warns about. The hash rotted because amending a commit invalidates any hash recorded before the amend, which no commit can avoid for itself; the final commit is now cited by subject instead.
Replace the v1 pointer and nudge shell hooks with one stdlib Python 3 program (hooks/session-journal.py) that handles SessionStart, UserPromptSubmit, PreToolUse, PostToolUse, Stop and SessionEnd and carries a continuity CLI. A per-session lease records the journal a session first writes. Start names that journal, or ranked open candidates with IN USE marked; Stop nudges only when this turn landed work and did not touch this session's own journal, at most once per turn. Writes into a journal another live session owns, or into a linked worktree's copy, are denied, as are credential-shaped journal writes (values never echoed). Covers /clear, forks, lead/worker roles with a worker ledger, a size limit with Continues:, Codex turn_id/apply_patch input, and sweep --by-git. The v1 shell hooks and their bats suites are retired; the scenario suite in tests/test_journal.py replaces them.
The installer wires all six events per runtime with an '|| true' safety net and per-event timeouts, removes v1 scripts and hook groups in place, carries a v1 absolute SESSION_JOURNAL_DIR across as a relative path, drops v1-only knobs, installs the agent skill (Claude and Codex), and uninstalls everything it owns. The doctor checks six-event wiring, leftover v1 hooks, Python, that local state stays out of git, and runs the real hooks against a scratch state directory. Templates describe binding, IN USE and PAUSED; CI runs the scenario suite and scans every tracked journal with the hook's own rules.
The skill holds what the scripts cannot decide: where journals live, adapting roles and landing rules to a project's agents, picking or creating a journal, what never goes in one, migrating old journals, and recall. The Codex mirror carries agents/openai.yaml so Codex discovers it; a docs test keeps the two SKILL.md files identical.
Spec 2.0 replaces newest-journal selection with session binding and states the update, role and status rules. README, ADOPT.md (now routed through the skill), SECURITY.md, CHANGELOG and the verification record follow. This repository's own installation moves to 2.0.
… journal The 1.x extras/spine-promotion shell hook picked the newest journal by mtime and blocked on every Stop once a DETAILED journal passed the threshold. It now lives in session-journal.py behind SPINE_PROMOTION_THRESHOLD (off by default): it checks the journal this session is bound to, only on a landed turn, and asks once per journal. The installer removes the old Stop entry and script and carries its SPINE_PROMOTION_* settings onto the generated Stop command (threshold 6 when none was set); the doctor flags a leftover entry.
A session that committed everything had its journal edited by SessionEnd (Sessions line, ACTIVE to PAUSED), leaving an uncommitted change after the agent was gone. When the journal is clean in git, the stamp now waits in the lease: candidates show it as PAUSED (session ended), the next session that changes the journal writes the pending line when it ends, and sweep applies it. A later write supersedes a pending pause.
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
The full-history gitleaks job flags six fake values from the retired secrets-guard.bats and an earlier doctor.bats. They exist to prove the secrets check fires; the current tests build such strings at runtime.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2e66d0db5d
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if names and ctx.secrets == 'warn': | ||
| hits = scan_secrets(tool_text(tool, ti)) |
There was a problem hiding this comment.
Scan post-detected Bash writes in deny mode
When a Bash write is discovered only from the pending-file mtime check—for example, F=sessions/2026-...md; cat /tmp/credential > "$F"—PreToolUse cannot identify the target, and the secret value is not present in the command text. The changed journal is subsequently bound here, but secret scanning runs only in warn mode and scans the command rather than the resulting file, so the default deny mode silently leaves credential-shaped content in the long-lived journal. Scan the contents of journals detected after execution and instruct the agent to revert/remove any leaked content.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Valid. Fixed in #2: after any shell command that changes a journal, PostToolUse scans the journal file itself and tells the agent to remove the match (values never echoed); in deny mode Stop blocks until the journal is clean. A scenario test reproduces the F=...; cat ... > "$F" case.
Ships 1.1.0 (never released) and 2.0.0 together.
2.0.0 replaces the v1 pointer and nudge shell hooks with one stdlib Python hook (SessionStart, UserPromptSubmit, PreToolUse, PostToolUse, Stop, SessionEnd) that binds each journal to the session that writes it: ranked candidates instead of newest-file selection, IN USE locks, worktree, /clear and fork handling, lead/worker roles, a once-per-landed-turn Stop nudge, credential-shaped writes denied, a 40 KB split rule, and a continuity CLI. Codex is wired alongside Claude Code. Adds the session-journal agent skill (Claude and Codex mirror), rewrites install.sh/doctor.sh with in-place v1 migration, folds spine promotion into the hook, and updates the spec, ADOPT.md and docs.
Verification: unittest (72 runs, Python 3.8 and 3.14), bats 60/60, shellcheck, doctor on this repo and a scratch repo, live Claude Code and Codex runs. See CHANGELOG.md and docs/verification.md.