diff options
Diffstat (limited to '.ai/workflows/wrap-it-up.org')
| -rw-r--r-- | .ai/workflows/wrap-it-up.org | 122 |
1 files changed, 105 insertions, 17 deletions
diff --git a/.ai/workflows/wrap-it-up.org b/.ai/workflows/wrap-it-up.org index 5ce88a5..ecd3d22 100644 --- a/.ai/workflows/wrap-it-up.org +++ b/.ai/workflows/wrap-it-up.org @@ -24,11 +24,13 @@ The wrap-up is complete when: 2. *File is archived.* =.ai/session-context.org= has been renamed to =.ai/sessions/YYYY-MM-DD-HH-MM-description.org=. The old path no longer exists. 3. *todo.org is clean.* Cleanup script ran. Any auto-fixes are staged for the wrap-up commit. Orphan planning lines surfaced for manual fix if there are any. 4. *Linear board is honest* (skip if project doesn't use Linear). Any Dev-Review ticket whose PR has merged was moved to Done or PM Acceptance per the classification rule. -5. *Git state is clean.* All changes committed + pushed to all remotes. Working tree clean. +5. *Git state is certified clean.* All changes are committed + pushed to all remotes, =git-worktree-gate certify= succeeded at the current HEAD, and the working tree has no staged, unstaged, untracked, submodule, or in-progress-operation state. 6. *Valediction delivered.* Brief, warm closing with key accomplishments and reminders, ending with =session wrapped.= on its own line as the signoff marker. The absence of =.ai/session-context.org= is the signal that the last session wrapped up cleanly. Its presence at session start means the previous session was interrupted. +*A helper session meets a shorter list.* Criteria 1 and 2 apply to its own context file (archived under =.ai/sessions/YYYY-MM-DD-HH-MM-<id>-<description>.org=), and 6 applies. Criteria 3, 4, and 5 do not: hygiene, the Linear pass, and all git mutation belong to the primary, so a helper that satisfied criterion 5 would have violated its contract to get there. Step 0 routes this. + * Teardown mode (set from the trigger phrase) The wrap itself — Steps 1 through 5 — is identical in every mode. The trigger phrase only decides what Step 6 does once commit + push and the valediction are done. Resolve the mode from the phrase before starting: @@ -43,8 +45,58 @@ This depends on three functions in =.emacs.d/modules/ai-term.el= (=cj/ai-term-qu * The Workflow +** Step 0: Helper branch — a helper wraps only itself + +Resolve first whether this session is a helper, because a helper's wrap is a different and much shorter workflow. Everything from Step 1 down — the hygiene passes, the inbox check, the commit, the push, the clean-tree certificate — is primary-only under the role contract in [[file:helper-mode.org][helper-mode.org]], and running any of it from a helper is exactly the concurrency failure that contract exists to prevent. + +A session is a helper when =AI_HELPER=1= in its environment (=ai --helper= sets it) or when it adopted helper-mode.org this session by instruction. If neither holds, this is a primary: skip to Step 0.5 and wrap normally. + +#+begin_src bash +echo "AI_HELPER=${AI_HELPER:-unset} AI_AGENT_ID=${AI_AGENT_ID:-unset}" +#+end_src + +For a helper, re-run the roster — the answer decides which wrap applies: + +#+begin_src bash +root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)" +if [ -x "$root/.ai/scripts/agent-roster" ]; then + "$root/.ai/scripts/agent-roster" "$root"; rc=$? +else + rc=2 +fi +echo "roster rc=$rc" +#+end_src + +Pass the project root explicitly. =agent-roster= defaults to =$PWD= and keeps only agents whose cwd is at or inside that root, so running it from a subdirectory hides a primary sitting at the root — and the "alone" that produces is read below as *orphaned*, which is the one branch that commits and pushes. Capture =rc= inside the branch too: =[ -x … ] && …; echo $?= reports the status of the whole list, so an absent script reads as 1 (others live) rather than 2 (unavailable). + +- *Primary still live (rc 1)* — the normal case. Finalize the =* Summary= in the helper's own context file — same contract as Step 1, KB receipt line included (resolve it with =AI_AGENT_ID=<id> .ai/scripts/session-context-path=), archive it to =.ai/sessions/YYYY-MM-DD-HH-MM-<id>-<description>.org= so it can't collide with the primary's archive name, deliver the valediction, and stop. Do NOT commit, push, or run any hygiene pass. The helper's scoped edits stay in the tree and the primary's next commit carries them along with the archived file — say so in the valediction, so Craig knows the work is real but not yet pushed. +- *Alone (rc 0) — orphaned helper* — the primary exited first, so the git ban lifts: the concurrency that justified it is gone, and stopping here would strand the helper's edits as a dirty tree nobody owns. Run the full wrap below starting at Step 0.5, exactly as a primary would. +- *Roster unavailable (rc 2, or the script absent)* — take the archive-only path, the same as primary-still-live. Leaving work for the next session to commit is recoverable; guessing "orphaned" and committing underneath a live primary is not. + +** Step 0.5: Refuse if sentry is live + +Before anything else, check whether sentry is running in this project. Sentry holds the working tree on its =sentry/<date>-<host>= branch and commits unattended; wrapping underneath it would archive the session anchor and tear down the buffer while the loop is still firing into it. If sentry's single-runner lock is held, stop and point at the shutdown path: + +#+begin_src bash +proj="$(basename "$(git rev-parse --show-toplevel 2>/dev/null || pwd)")" +if [ -x .ai/scripts/agent-lock ] && .ai/scripts/agent-lock status "sentry-$proj" | grep -q '^held'; then + echo "sentry is active — say 'stop sentry' first" + exit 1 +fi +#+end_src + +The stop-sentry operation (defined in =sentry.org=) owns the shutdown: it cancels the loop, disposes of the branch, and walks the approval queue. Wrap-up carries only this one guard; a =stale= lock (a crashed cycle) doesn't block — only a live =held= lock does. + ** Step 1: Finalize the Summary +*** Work the Before-Close Queue (before the Summary) + +If the session anchor (=.ai/session-context.org=) carries a =* Before-Close Queue= heading with items, work them now, oldest-first, before writing the Summary, so any resulting edits ride this wrap's commit and get described in it. The queue is the "put X on the list" shorthand (see =protocols.org=, Colloquialisms and Expansions): session-scoped work Craig deferred to wrap time. + +Per item: do it if it's clear and bounded, or promote it to a =todo.org= task if it turns out to need its own session. Never drop an item silently. Remove each line as it's handled; if one can't be finished, surface it in the valediction (Step 5) and either leave a follow-up task or state why it's dropped. + +If there's no =* Before-Close Queue= heading, or it's empty, this step is a silent no-op. + *** Early KB reflection (capture while fresh, before the Summary) Before distilling the Summary, while the session is still fresh, ask: what did this session learn worth remembering, for yourself or a future agent? Reflect and stage any candidate durable facts — a decision and its why, an environment gotcha, a reference pointer, a transferable lesson. Self-answer silently; this adds no interactive turn (Craig already authorized the wrap). The candidates flow straight into the KB promotion check below, which does the actual writing and the receipt — this is the capture half, that is the commit half, one pipeline, one receipt. Reflecting here rather than reconstructing learnings after the Summary is the point: the early ask is what keeps the receipt from defaulting to "promoted 0" out of fatigue. @@ -167,6 +219,16 @@ Preview the moves without writing: emacs --batch -q -l .ai/scripts/todo-cleanup.el --archive-done --check todo.org #+end_src +*** Clear temp/ + +#+begin_src bash +[ -d temp ] && find temp -mindepth 1 -delete && echo "temp/ cleared" +#+end_src + +=temp/= holds throwaway artifacts — discarded prototypes, scratch output, intermediate data (see =working-files.md=). It's gitignored in every project, so nothing here rides a commit and nothing is recoverable from git once deleted. Clearing it at wrap is what keeps ephemeral work from silting up across sessions, and it's the counterpart to =working/=, which is tracked and *never* cleared here. + +Two guards. Confirm before deleting if =temp/= holds anything a reasonable reader would call in-progress rather than throwaway — misfiled work belongs in =working/=, so move it there instead of deleting it. And skip the step entirely in a project where =temp/= is not gitignored, since that means the project is using the directory for something else. + *** Sync child priorities #+begin_src bash @@ -241,7 +303,7 @@ For an interactive walk of the judgments mid-day, run =/lint-org todo.org=. *** Inbox sanity check (surface unprocessed handoffs) -If the project has an =inbox/= directory, verify it holds nothing but =.gitkeep=, =lint-followups.org= (the lint-org pipeline file the next morning's daily-prep consumes), and any explicitly-deferred =PROCESSED-*= files before the wrap completes. An inbox that arrived at session start with handoffs from other projects, or that received handoffs mid-session, needs =inbox.org= process mode to run and apply its value-gate dispositions. Wrapping with a dirty inbox silently defers the work to next session and accumulates handoff debt that the sender can't see. +If the project has an =inbox/= directory, verify it holds nothing but =.gitkeep=, =lint-followups.org= (the lint-org pipeline file the next morning's daily-prep consumes), and =PROCESSED-*= files before the wrap completes. An inbox that arrived at session start with handoffs from other projects, or that received handoffs mid-session, needs =inbox.org= process mode to run and apply its value-gate dispositions. Wrapping with an unprocessed inbox silently defers the work to next session and accumulates handoff debt that the sender can't see. #+begin_src bash unprocessed=$(find inbox -maxdepth 1 -type f \ @@ -250,7 +312,7 @@ unprocessed=$(find inbox -maxdepth 1 -type f \ ! -name 'PROCESSED-*' \ 2>/dev/null | wc -l) if [ "$unprocessed" -gt 0 ]; then - echo "wrap-up: inbox/ has $unprocessed unprocessed item(s). Run inbox.org process mode before wrapping, or explicitly defer each item with a one-line reason in the valediction." + echo "wrap-up blocked: inbox/ has $unprocessed unprocessed item(s). Run inbox.org process mode before wrapping." find inbox -maxdepth 1 -type f \ ! -name '.gitkeep' \ ! -name 'lint-followups.org' \ @@ -259,7 +321,7 @@ if [ "$unprocessed" -gt 0 ]; then fi #+end_src -If the count is zero or the project has no =inbox/= directory, the check is a silent no-op. If non-zero, the wrap is incomplete by default. The user resolves each item (process now, defer with reason in the valediction, or delete with rationale) before the validation checklist passes. +If the count is zero or the project has no =inbox/= directory, the check is a silent no-op. If non-zero, the wrap is blocked. Process each item through its value-gate disposition, or delete it only when that workflow's rationale authorizes deletion, before continuing. The check exempts =lint-followups.org= explicitly because lint-org runs earlier in the same wrap-up workflow and writes its judgment items to that file in =inbox/= by design. The file is a pipeline artifact for the next morning's =daily-prep=, not a handoff that needs the value gate. @@ -469,17 +531,17 @@ Behavior: git status --short #+end_src -*Default policy: end every session with an empty =git status=.* The wrap is incomplete while anything remains dirty. There is no "leave it alone" default — every leftover gets an active resolution. The only way for a file to stay dirty across the wrap is the user explicitly saying "defer this one, leave it dirty." Surface each leftover with a concrete recommendation; the user has to actively opt out for the dirt to persist. +*Hard policy: end every session with an empty =git status=.* The wrap is incomplete while anything remains dirty. There is no deferral exception and no "wrapped with known changes" state: unresolved dirt means the session remains open and wrap-up does not occur. This inverts the older "intentional carryover" default, which let pre-existing dirty state accumulate across sessions silently. Carryover that lives for days or weeks is almost always one of: a forgotten commit from a prior wrap, a stale change that should be discarded, or genuine in-flight work that needs an explicit stash/branch home. None of those should default to "leave it dirty." **** Three kinds of leftover -| Pattern | What it is | Recommended action (apply unless user defers) | +| Pattern | What it is | Recommended action | |---+---+---| | Generated, runtime, or lock files that no human edits — e.g., =.claude/scheduled_tasks.lock=, =.pytest_cache/=, build outputs, IDE state, editor swap files | *Runtime artifact* — created by tooling or the harness, not by the user, and shouldn't be tracked | Add the matching pattern to =.gitignore= (project-level, not =~/.gitignore_global=). For tracked files, =git rm --cached <path>=. Stage =.gitignore= and any =rm --cached= changes in *one* follow-up commit (=chore: gitignore X=), push. Re-run =git status= to confirm clean. | | Modified or created during the session but not staged into the wrap-up commit | *Forgotten change* — real session work that should have been in the wrap commit but missed it | Stage and create a follow-up commit. Don't =--amend= the wrap-up commit once pushed (diverging history without a clear win). Push the follow-up to all remotes. | -| Was dirty at session start and still dirty at session end — work this session deliberately didn't touch | *Pre-existing dirt that needs a decision* — could be a missed commit from a prior wrap, stale abandoned work, or real in-flight work without a home | Investigate (show diff + check the originating session). Recommend one of: (a) commit now if the work is complete, (b) stash with a descriptive message if it's genuine WIP, (c) =git checkout -- <path>= / =git clean -f <path>= if stale and unwanted, (d) move to a feature branch if it's longer-running, (e) user explicitly defers and accepts the dirt. Do not silently leave dirty. | +| Was dirty at session start and still dirty at session end — work this session deliberately didn't touch | *Pre-existing dirt that needs a decision* — could be a missed commit from a prior wrap, stale abandoned work, or real in-flight work without a home | Investigate (show diff + check the originating session). Recommend one of: (a) commit now if the work is complete, (b) stash with a descriptive message if it's genuine WIP, (c) =git checkout -- <path>= / =git clean -f <path>= if stale and unwanted, or (d) move to a feature branch if it's longer-running. Do not silently leave dirty. | **** Per-file flow @@ -487,18 +549,40 @@ For each leftover line in =git status --short=: 1. Identify which of the three kinds above it matches. 2. State what the file is (one line) and the recommended action. -3. Apply the action unless the user explicitly defers. -4. Re-run =git status --short= after each follow-up commit until empty (or until every remaining line is an explicit user-deferred entry). +3. Apply the action when it is safe and authorized. +4. Re-run =git status --short= after each follow-up commit until empty. The pre-existing-dirt case (third row) is the one this rule most cares about. Treat each pre-existing-dirty file as a question that must get an answer this session, not as "carryover that's fine to inherit." A file that was dirty for a week before this session probably isn't going to get cleaner by waiting another week. Look at the diff, check the originating session's notes, and recommend a real resolution. -**** When the user defers +**** When cleanup cannot be completed -If the user does say "leave this one dirty for now" after seeing the recommendation, that is fine — log the deferral in the valediction so the next session knows it was an explicit choice, not a miss. Format: "Deferred (per Craig's decision today): =path/to/file= — <one-line reason>". Without that note, the next session can't distinguish "we agreed to defer" from "we forgot again." +Stop the wrap. Do not deliver the valediction, print =session wrapped.=, drop a teardown/shutdown sentinel, or describe the session as complete. Report: + +1. Every remaining path and its exact Git state. +2. What the file is and why the agent cannot safely resolve it alone. +3. The concrete action or decision Craig needs to provide to make the tree clean. + +An explicit decision to keep a file dirty changes the outcome from "wrapping" to "leaving the session interrupted." It never satisfies this workflow. + +*** Final clean-tree certificate — hard gate + +After all commits are pushed and every leftover appears resolved, run the shared gate: + +#+begin_src bash +gate="$(command -v git-worktree-gate 2>/dev/null || true)" +[ -n "$gate" ] || gate="$HOME/code/rulesets/claude-templates/bin/git-worktree-gate" +if [ ! -x "$gate" ]; then + echo "wrap blocked: git-worktree-gate is unavailable; install rulesets tooling and retry" + exit 1 +fi +"$gate" certify "$PWD" +#+end_src + +The certificate lives inside the Git directory, so it does not dirty the worktree. It records the exact verified HEAD. A non-zero result is a hard stop governed by "When cleanup cannot be completed" above. Step 5 is unreachable until certification succeeds. ** Step 5: Valediction -Brief, warm closing. 3-4 sentences max. +Only after the final clean-tree certificate succeeds, deliver a brief, warm closing. 3-4 sentences max. Include: - What was accomplished (specific, not generic) @@ -535,7 +619,7 @@ Do nothing. The buffer, the =aiv-<project>= tmux session, and =claude= all stay *** Teardown mode (default) -Confirm commit + push succeeded (Exit Criteria 5 — never tear down over unpushed work), then drop the sentinel: +Confirm commit + push and the final clean-tree certificate succeeded (Exit Criteria 5 — never tear down over unpushed or dirty work), then drop the sentinel: #+begin_src bash touch "/tmp/ai-wrap-teardown-$(basename "$PWD")" @@ -543,6 +627,8 @@ touch "/tmp/ai-wrap-teardown-$(basename "$PWD")" That is the whole step. Don't run any =tmux kill-session=, =emacsclient=, or buffer kill inline — the =Stop= hook reads the sentinel when this response ends and runs =cj/ai-term-quit=, which kills the =aiv-<project>= session (taking =claude= with it), kills the vterm buffer, and restores geometry. The basename of =$PWD= is the key the hook matches, so the sentinel names the session it tears down. +*The sentinel is session-scoped.* If certification fails, the =Stop= hook blocks and leaves the sentinel armed on purpose, so a wrap blocked by a dirty tree retries on a later stop without re-running this workflow. It does *not* survive the session: =session-start-disarm.sh= clears it at =SessionStart=, because a wrap that never certified is not a pending teardown once its session is gone. Before that hook existed, an uncertified sentinel sat armed indefinitely and fired in whatever session next reached a clean tree — work's 2026-07-27 11:37 wrap killed the 13:20 session mid-work, and archsetup's sat armed on a live terminal for two days. If teardown is still wanted in a new session, run this workflow again. + *** Shutdown mode Confirm commit + push succeeded, then evaluate the safety gate *before* committing to the shutdown — never power the box off out from under another live session: @@ -573,7 +659,8 @@ If =emacsclient= isn't resolvable or the daemon is down, the gate can't run — 7. *Leaving =.ai/session-context.org= in place* — its presence means "interrupted session", confuses next startup 8. *Long preachy valediction* — brief beats thorough 9. *Leaving runtime/generated files dirty without gitignoring them* — pollutes every future =git status= and erodes trust in "working tree clean" as a signal. Fix =.gitignore= during the wrap, not later. -10. *Treating "was dirty at session start, still dirty now" as fine by default* — that's how a forgotten commit from two sessions ago turns into "carryover" for two weeks. Every pre-existing dirty file needs an active resolution recommendation this session. Deferral is allowed only with an explicit user choice, logged in the valediction. +10. *Treating "was dirty at session start, still dirty now" as fine* — that's how a forgotten commit from two sessions ago turns into "carryover" for two weeks. Every pre-existing dirty file must be resolved or the wrap remains blocked. +11. *Calling a blocked cleanup a wrap* — if the strict gate fails, report the paths and needed decisions; do not valedict, certify completion, or tear down. * Validation Checklist @@ -586,19 +673,20 @@ Before considering wrap-up complete: - [ ] =todo-cleanup.el= ran — hygiene pass + =--convert-subtasks= + =--archive-done= + =--sync-child-priority= (if =todo.org= exists at project root) - [ ] =lint-org.el= ran on =todo.org= — mechanical fixes applied, judgments appended to follow-ups file (if =todo.org= exists) - [ ] Any orphan-planning-line warnings reviewed (fix or accept) -- [ ] Inbox carries nothing but expected pipeline artifacts (=.gitkeep=, =lint-followups.org=, =PROCESSED-*= prefixes), OR each remaining handoff has an explicit deferral logged in the valediction +- [ ] Inbox carries nothing but expected committed or ignored pipeline artifacts (=.gitkeep=, =lint-followups.org=, =PROCESSED-*= prefixes); any untracked inbox delivery was processed before wrap - [ ] Linear Dev-Review sweep ran; any merged-PR tickets moved to Done or PM Acceptance (skip if project doesn't use Linear) - [ ] Template-sync churn committed as its own =chore: sync .ai tooling from templates= (consuming projects only; skipped in rulesets), or surfaced if a synced path didn't match canonical -- [ ] After wrap-up commit + push, =git status --short= is empty OR every remaining line has an explicit user-deferred decision logged in the valediction +- [ ] After wrap-up commit + push, =git-worktree-gate certify "$PWD"= succeeded at the current HEAD - [ ] Each leftover was investigated and the user saw a concrete resolution recommendation - [ ] Runtime artifacts added to =.gitignore=, follow-up commit pushed, =git status= re-verified - [ ] Forgotten changes committed in a follow-up and pushed -- [ ] Pre-existing dirty files resolved (committed / stashed / discarded / moved to a feature branch) or explicitly deferred with a one-line reason in the valediction +- [ ] Pre-existing dirty files resolved (committed / stashed / discarded / moved to a feature branch); otherwise wrap stopped with an actionable blocker report - [ ] Current branch pushed to ALL remotes (verified with =git remote -v=) - [ ] All other local branches with a tracking upstream pushed to their remote - [ ] Any untracked-upstream branches surfaced for manual =git push -u= - [ ] Step 6 teardown matches the trigger phrase: no-teardown leaves the buffer; teardown drops only =/tmp/ai-wrap-teardown-<project>=; shutdown gates on =cj/ai-term-live-count= = 1 and drops only =/tmp/ai-wrap-shutdown-<project>= - [ ] No teardown/shutdown sentinel was dropped before commit + push was verified +- [ ] The teardown hook can re-verify the clean-tree certificate before consuming a sentinel - [ ] Shutdown aborted (fell back to normal wrap, logged in the valediction) when another =aiv-*= session was live or the gate couldn't run - [ ] Commit message follows format (no =session:=, no Claude attribution) - [ ] Valediction delivered (brief, specific, warm) |
