chore: rescue unlanded session docs, gitignore .codex/, de-drift AGENTS.md - #852
Conversation
Four dev sessions left their spec.md/plan.md uncommitted in agent-session worktrees; only checks.md ever reached main (and for #779/#783, nothing did). Recovered before those worktrees get pruned: - #779 durable input queues: checks.md, plan.md, spec.md - #783 synchronous hooks: spec.md - #145 steering messages: spec.md - #193 garden folders: plan.md, spec.md Empty notes.md files were skipped.
Two untracked-noise sources in the main clone: - .gitignore covered .claude/ but not .codex/, so a 3.4G directory of Codex worktrees showed up as untracked on every status. - AGENTS.md was an Aug 5 snapshot of CLAUDE.md with a blind claude->Codex find/replace run over it, which corrupted real facts: it pointed extra_skill_paths at ~/.Codex/skills instead of ~/.claude/skills, renamed the claude_code skill to Codex, and claimed .Codex/ was the gitignored worktree root. It was also missing the static-module-graph convention and metrics.py. A symlink keeps Codex on the same conventions and cannot drift. Note: the vault guide's default filename is also AGENTS.md, but that path is relative to the vault root, not the repo root - unrelated file.
There was a problem hiding this comment.
🟡 Changes recommended
Resolve the documented inconsistencies and duplicate session record, and add the missing advertised symlink.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Housekeeping PR restoring development-session documentation and adding .codex/ to .gitignore. The advertised AGENTS.md symlink is absent from the supplied patch.
Changes:
- Restores specs, plans, and checks for four sessions.
- Adds
.codex/to.gitignore. - Leaves documentation inconsistencies and duplicate records to resolve.
File summaries
| File | Summary |
|---|---|
docs/dev-sessions/20260813T191028Z-garden-folders/spec.md |
Restored specification; contraction and regression-test-path corrections are needed. |
docs/dev-sessions/20260813T191028Z-garden-folders/plan.md |
Restored implementation plan. |
docs/dev-sessions/2026-08-13-145-steering-messages/spec.md |
Restored specification; documented test path is incorrect. |
docs/dev-sessions/2026-08-10-1200-779-concurrency-durable-input-queues/spec.md |
Duplicates an existing session record and should be merged or removed. |
docs/dev-sessions/2026-08-10-1200-779-concurrency-durable-input-queues/plan.md |
Contains a SQLite/JSONL contradiction and editing aside. |
docs/dev-sessions/2026-08-10-1200-779-concurrency-durable-input-queues/checks.md |
Frozen acceptance outcomes remain marked pending. |
docs/dev-sessions/2026-08-08-0000-783-synchronous-hooks/spec.md |
Restored synchronous-hooks specification. |
.gitignore |
Adds the .codex/ ignore rule; the advertised AGENTS.md symlink is not present. |
Review details
Suppressed comments (1)
docs/dev-sessions/2026-08-10-1200-779-concurrency-durable-input-queues/spec.md:8
- This proposed implementation still says the inbox is SQLite-backed, while the acceptance criteria and design decision below specify JSONL. That contradiction could lead anyone following this recovered spec to implement the wrong persistence layer; make this bullet consistently describe JSONL storage.
- Migrate `conversation_manager.py`'s input handling from in-memory dispatch to a durable, SQLite-backed `Inbox` queue (similar to how `notifications.py` uses JSONL, but for incoming turns).
- Files reviewed: 8/9 changed files
- Comments generated: 6
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| ## C1 | ||
| CRITERION: WHEN `enqueue_turn` is called, THEN the system SHALL write the incoming turn to a durable JSONL session inbox file instead of an in-memory queue. | ||
| CHECK: `pytest tests/test_conversation_manager.py::test_enqueue_turn_writes_to_jsonl` passes. | ||
| AT FREEZE: (pending) |
| **Source issue:** https://github.com/lmorchard/decafclaw/issues/779 — **Tier:** `auto-ok` (All criteria are verifiable via unit tests without human judgment. Adding a JSONL-backed queue for incoming turns does not touch risk-gated paths.) | ||
|
|
||
| **Approach:** | ||
| - Migrate `conversation_manager.py`'s input handling from in-memory dispatch to a durable, SQLite-backed Inbox queue (similar to how `notifications.py` uses JSONL, but for incoming turns). Wait, design decisions specify "backed by JSONL files rather than SQLite". |
| @@ -0,0 +1,33 @@ | |||
| **Concept from opencode:** | |||
|
|
||
| ## Regression guards | ||
|
|
||
| - GUARD: `pytest tests/test_agent.py` passes and existing agent turn flow is preserved. |
| - Need to be conservative — dont break existing links or surprise users | ||
| - May want a dry-run mode that logs proposed moves without executing | ||
| - Should respect any user-created folder structure (dont flatten what the user organized) |
|
|
||
| ## Regression guards | ||
|
|
||
| - GUARD: `pytest tests/test_vault_tools.py tests/test_garden_recompute.py` passes — existing vault operations and importance recompute remain fully functional. |
Agent-driven development leaves a lot of git debris; this clone had 21 worktrees and 30 local branches, 25 of which held work already in origin/main. Cleaning that up by hand means checking each branch against both main's ancestry and its PR's merge state, which is exactly the kind of manual invocation that leaves nothing behind. The script classifies each branch as LANDED (ancestor of origin/main, or a merged PR whose merge postdates the branch tip, i.e. squash-merged) or UNLANDED, and only ever prunes the former. It also reports detached worktrees separately: they have no branch to key off, so a branch-driven scan misses them and 'git worktree prune' only reaps directories that are already gone. Refuses to drop a landed worktree that has tracked edits unless --force, and prints the recovery command for every branch it deletes. Dry run by default, matching the migrate-sidecars-dry / migrate-sidecars pairing. Verified against this clone: 25 LANDED / 5 UNLANDED / 2 detached, matching a hand audit of the same state. Note scripts/ sits outside 'make lint' and 'make fmt' (both scoped to src/ and tests/); ruff check and format were run on this file directly.
There was a problem hiding this comment.
🟡 Changes recommended
The pruning tool’s branch-base classification, destructive-path test coverage, and stale plan wording must be addressed.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (5)
docs/dev-sessions/2026-08-10-1200-779-concurrency-durable-input-queues/plan.md:8
- This recovered plan still says the approach is SQLite-backed, then corrects itself inline, while the goal and criteria consistently require JSONL. Replace the stale wording so the session plan is unambiguous for future readers.
- Migrate `conversation_manager.py`'s input handling from in-memory dispatch to a durable, SQLite-backed Inbox queue (similar to how `notifications.py` uses JSONL, but for incoming turns). Wait, design decisions specify "backed by JSONL files rather than SQLite".
scripts/prune_agent_worktrees.py:124
- A branch's committer timestamp is not proof that its current tip is the PR that was merged: local branches can share
headRefNamewith a fork or an older PR, and commit timestamps can be backdated or use different offsets. This can classify unrelated work as LANDED and then delete it withbranch -D; use the merged PR'sheadRefOid(and head-repository identity) and require the local tip to match before accepting the squash-merged verdict.
merged_at = prs.get(name)
if merged_at:
last = git("log", "-1", "--format=%cI", name)
if last <= merged_at:
return "LANDED", f"squash-merged {merged_at[:10]}"
scripts/prune_agent_worktrees.py:204
inspect_worktree()records untracked files, but this guard ignores them. A landed worktree with a clean index and an untrackedspec.mdwill still be removed by the unconditionalgit worktree remove --force, even when the script was not invoked with its--forceoption. Treatb.untrackedas dirty here so local files are not silently lost.
if b.dirty_tracked and not force:
print(f"SKIP {b.name}: {len(b.dirty_tracked)} tracked edit(s); use --force")
scripts/prune_agent_worktrees.py:123
%cIincludes the commit's original timezone offset, while GitHub'smergedAtis normally UTC (Z); ISO-8601 strings with different offsets are not safely orderable lexicographically. A commit at 12:00−05:00 can compare earlier than a 16:00Z merge even though it was committed at 17:00Z, so this can classify post-merge work as LANDED. Parse timestamps as aware datetimes, and preferably use the PR head/merge commit IDs instead of wall-clock ordering for a destructive decision.
if last <= merged_at:
scripts/prune_agent_worktrees.py:139
- A failed
git statusis treated as an empty status becausecheck=Falseis ignored and both result lists remain empty.apply()then callsgit worktree remove --forceand deletes the branch as if the worktree were clean; a permission error or corrupted worktree can therefore bypass the safety check. Fail closed on a nonzero status result (or mark the worktree uninspectable and skip it).
check=False,
)
- Files reviewed: 10/11 changed files
- Comments generated: 2
- Review effort level: Lite
| print("warning: gh not found; squash-merged branches will read as UNLANDED") | ||
| return {} | ||
| proc = subprocess.run( | ||
| ["gh", "pr", "list", "--state", "merged", "--limit", "500", "--json", "headRefName,mergedAt"], |
| def apply(branches: list[Branch], force: bool) -> int: | ||
| failures = 0 | ||
| for b in sorted(branches, key=lambda x: x.name): | ||
| if not b.landed: | ||
| continue |
Doc currency for #852-#858. AGENTS.md is a symlink to this file, so Codex picks these up automatically. - Running: adds check-tui, test-tui, prune-worktrees{,-dry}; corrects 'make check' (composed from install-js, check-message-types, lint, typecheck and check-js) and 'make lint' (described as 'Compile-check'; it is ruff over four directories, and was inaccurate before this session too — fixed because it sits in the same list being corrected for accuracy). - Workflow: a new convention for the defect that recurred four times in one session — a gate that exists in the Makefile and still runs nowhere. Compose targets rather than duplicating their commands, confirm CI invokes the target and read the job log, and probe every gate for teeth. Notes that pyright exits 0 when it reports only warnings, which makes a pyrightconfig.json addition report without failing. Both composition lists are checked against Makefile:105 programmatically, not by eye — an earlier draft listed only three of the five prerequisites.
Repo housekeeping from a state audit of the main clone. Three independent
cleanups, no behavior change.
1. Rescue unlanded dev-session docs
Four dev sessions left
spec.md/plan.mduncommitted inside agent-sessionworktrees. Only
checks.mdever reached main, and for #779 and #783 nothingdid. Recovered here before those worktrees get pruned:
checks.md,plan.md,spec.mdspec.mdspec.mdplan.md,spec.mdEmpty
notes.mdfiles were skipped.2. Gitignore
.codex/.gitignorecovered.claude/but not.codex/, so a 3.4G directory holding15 Codex worktrees showed up as untracked on every
git statusin the mainclone.
3.
AGENTS.mdbecomes a symlink toCLAUDE.mdAGENTS.mdwas an untracked Aug 5 snapshot ofCLAUDE.mdwith a blindclaude->Codexfind/replace run over it, which corrupted real facts:extra_skill_pathstargets~/.Codex/skills~/.claude/skillsskills/{...,Codex,...}claude_code.Codex/is the gitignored worktree root.claude/isIt was also missing the
static/module-graph convention (from #844) andmetrics.py(from #848). Since it was a verbatim copy in the first place, asymlink gives Codex the same conventions and removes the drift vector
entirely.
Unrelated note for future readers: the vault guide's default filename is also
AGENTS.md, but that path resolves against the vault root, not the repo root.Different file.
Verification
Doc, config, and symlink only -- no Python or JS touched, so this worktree
skipped
uv syncand has no local venv; CI'smake checkis the gate. Symlinkverified stored as git mode
120000->CLAUDE.md, and the new ignore patternverified matching
.codex/worktrees/....4.
scripts/prune_agent_worktrees.py+make prune-worktreesThe audit that produced this PR found 21 worktrees and 30 local branches, 25 of
which held work already in
origin/main. Doing that by hand means checking eachbranch against both main's ancestry and its PR's merge state, so it's captured
as a script rather than a one-off.
Classifies each branch as LANDED (ancestor of
origin/main, or a merged PRwhose merge postdates the branch tip -- i.e. squash-merged) or UNLANDED, and
only ever prunes the former. Detached worktrees are reported separately, since
they have no branch to key off and
git worktree pruneonly reaps directoriesthat are already gone.
Safety: refuses to drop a landed worktree with tracked edits unless
--force,prints the
git branch <name> <sha>recovery line for everything it deletes,and is dry-run by default -- matching the existing
migrate-sidecars-dry/migrate-sidecarspairing.Verified against this clone: 25 LANDED / 5 UNLANDED / 2 detached, matching a
hand audit of the same state.
ruff checkandruff formatwere run on the filedirectly, since
scripts/sits outsidemake lintandmake fmt(both scopedto
src/andtests/).