diff options
Diffstat (limited to 'claude-templates/.ai/workflows')
| -rw-r--r-- | claude-templates/.ai/workflows/daily-prep.org | 32 | ||||
| -rw-r--r-- | claude-templates/.ai/workflows/helper-mode.org | 29 | ||||
| -rw-r--r-- | claude-templates/.ai/workflows/sentry.org | 4 | ||||
| -rw-r--r-- | claude-templates/.ai/workflows/startup.org | 43 | ||||
| -rw-r--r-- | claude-templates/.ai/workflows/triage-intake.personal-gmail.org | 4 | ||||
| -rw-r--r-- | claude-templates/.ai/workflows/triage-intake.telegram.org | 26 | ||||
| -rw-r--r-- | claude-templates/.ai/workflows/wrap-it-up.org | 32 |
7 files changed, 130 insertions, 40 deletions
diff --git a/claude-templates/.ai/workflows/daily-prep.org b/claude-templates/.ai/workflows/daily-prep.org index 3f21214..9f706d1 100644 --- a/claude-templates/.ai/workflows/daily-prep.org +++ b/claude-templates/.ai/workflows/daily-prep.org @@ -130,12 +130,33 @@ Morning Prep is where Craig reads the prep doc, mentally walks the schedule, and The Yesterday / Today / Blockers brief nests *directly under the standup meeting it's reported in* — never in a separate section. Plain section labels, no parenthetical questions in the rendered doc. -- *Blockers is always present.* When there are none, write =Blockers: None= explicitly — silence is ambiguous. +*The brief is a script, not a topic list.* The lines under each label are the words Craig says — complete first-person spoken sentences. Not fragments, not noun phrases, not a "bring these up" list. A topic list makes him compose the sentence live from a cue he wrote the night before and has since forgotten the shape of; a script is readable as-is. "Brief" invites bullets and bullets decay into topics, which is exactly the failure this rule exists to prevent. + +The script is a *draft for Craig to edit at the Phase 8 gate*, never words put in his mouth. That cuts both ways: a topic list forces him to compose and so fails safe, while a wrong script reads fluently enough to be spoken unchanged. So it gets the same gate scrutiny as the priorities. + +- *Blockers is always present.* A real blocker is stated as a full sentence, not a noun phrase. When there are none, write =Blockers: None= explicitly — silence is ambiguous. - *Outcomes, not attendance.* Never "met with <person>" — instead what came out of it: "<person> finished the branch CI/CD work." Never "went to the managers' meeting" — instead the development from it that affects this audience. - *No recurring 1:1s or ceremonies* in briefs — they're not news. - *Match the standup's altitude.* An engineering standup gets engineering-goal material only: what moved the platform or the demo forward — architecture docs, PRs, tracker tickets, partner meetings with use-case implications, integration discussions, security findings, dataset discoveries. It does NOT get: 1:1s, attending other standups, personal-tooling maintenance, profile updates, sending messages or email, meeting prep, booking travel, or interviews with non-engineering candidates. The three questions are really: (a) how have I moved us closer to the engineering goals, (b) what will I work on that moves us closer, (c) what information do I have that might impact the team or its goals. - A business-level (general) standup is different: features finished that leadership wanted, vacation/travel that affects availability or velocity, conference learnings, partner/customer decisions, cross-functional confusion worth clearing up. *Exclude routine maintenance and operational items — PR reviews don't belong here.* Foundational or strategic engineering work does; operations don't. +Worked contrast — the same day's material, written both ways: + +#+begin_example +Topics (the failure mode — a cue list, not a brief): + Bring: the freeze status, since Monday-or-later is the current answer. + The blocked review. The access request. + +Script (what belongs in the doc): + Yesterday: I fixed the bug where the selected region stayed editable + after an edit, and that's up for review along with the dependency + migration. + Today: I'm holding merges to development until the demo actually + happens, which now looks like Monday or later. + Blockers: None — the access request I raised Monday sits with the + platform team now, so it's slowing them rather than me. +#+end_example + (Drafting rules — first-person, deadline precision, recurring-meeting filters, the team-visible test — are in Phase 6.) *** Meetings @@ -315,7 +336,9 @@ Combine session history + the sweep + Day's Priorities + WAITING items into Yest - *Today*: 2-3 items max, from Day's Priorities. Include non-recurring meetings regardless of response status. - *Blockers*: the bar is "did this actually stop me from making progress?" — not "is someone else involved?" Default to under-reporting; Craig adds borderline items at the gate. FYIs come after blockers and stay loose. - *Team-visible filter*: only work that left Craig's local environment — pushed, shared, posted, changed in the tracker, or shifts what the team believes or plans. "If I didn't mention this, would someone make a worse decision or duplicate work?" If no, cut it. -- Readable aloud in under 60 seconds. +- *Write the words, not the topics.* Every line is a complete first-person sentence Craig can read aloud unchanged — see the worked contrast in the template's Standups section. No bare noun phrases, no "bring up X", and no identifier he wouldn't actually say out loud (a ticket key is fine where the team speaks in ticket keys, and wrong where they don't). +- Readable aloud in under 60 seconds — roughly 120-150 words across the three sections. Over budget means cut an item, not compress a sentence back into a fragment. +- *One script per standup.* Two standups on the same day get two separately-drafted scripts, because the altitude rules above admit different material to each. The same text under both headers is a defect, not a shortcut. *** Step 3: Capture learnings @@ -331,6 +354,8 @@ Also assemble the end-of-day block's upcoming-deadlines list: =DEADLINE:= entrie Present the assembled doc and ask whether Craig agrees with the Day's Priorities. If not, work with him to add / remove / substitute priorities and blocks until he confirms. Surface here, in one pass: meeting-goal questions, decline candidates, look-ahead flags, carry-forward decisions, and proposed schedule adjustments. +*Standup-script check (blocking).* The prep is not complete until every standup on that day's calendar carries its own script under its header, in the Phase 6 shape — spoken sentences, =Blockers:= present, altitude-matched. Check each standup individually and present the scripts at this gate for Craig to edit. Vacuous on a day with no standup, which is the common case in projects that hold none. This check exists because Phase 6's prose alone did not hold: on 2026-07-31 a prep wrote topic lists under both standup headers while every rule requiring a script was already on the page. Adding more prose to Phase 6 would not have caught that — the prose is what got skipped, so the requirement has to sit on a gate that blocks. + If the gate produces substantive rework, say so plainly: that's a =todo.org= staleness signal — the file should make Craig's current priorities obvious. Offer a task review. Update mode replaces the gate with a delta summary: what changed and why. @@ -448,3 +473,6 @@ The prep doc is born in =daily-prep/YYYY-MM-DD-daily-prep.org= and never moves; *** 2026-06-11: Full template rewrite — strict three-section doc, two run modes, mandatory priorities gate From Craig's instructive template spec (written 2026-06-10 evening, after reviewing generated preps) plus four refinements from his review of the first new-format prep. The doc is now exactly =* Heads-Up= / =* Day's Priorities= / =* Meetings / Focus Blocks=. Retired: the separate =* Standup Briefs= and =* Upcoming Deadlines= sections (briefs nest under their standup meeting; deadlines live in the end-of-day block), the =* [Day]'s Anchor Tasks= handoff (carry-forward lands directly in the next day's priorities, which are being built in the same sitting), the thin-link convention (entries mirror their todo.org task's heading and carry their own context — links in the body, never the heading), and standup-only mode (a brief refresh is an Update-mode run). New: two run modes (Create, with a MANDATORY end-of-flow priorities review gate whose disagreement signals todo.org staleness; Update, for when the world moves) both preceded by a triage-intake freshness check (no run in the last hour → run one first); event headers are the exact calendar title with ALL content nested under the event; per-event-type content rules (Morning Prep conflict-resolution strategy with drafts pre-written in the doc ready to send, standup altitude matching with =Blockers: None= explicit and operations excluded from business-level briefs, meetings carrying contribute/get/likely-questions with day-before prep blocks for "I don't know" answers and prep docs always =file:=-linked — the lesson of a prep that existed but couldn't be found in the minutes before a meeting that mattered, focus blocks as linked menus created day-before and marked free, lunch floor, the end-of-day "What Kind of Day Has It Been?" block carrying the deadlines list and generating tomorrow's prep); the look-ahead renders one day per line (=Fri 12:= …) with clear days marked =clear=; a requested-metrics Heads-Up slot rendered only when a metric is active (none yet); meetings verified against the live calendar at build and update time. + +*** 2026-07-31: Standup briefs are verbatim scripts, enforced at the Phase 8 gate +A prep wrote =Bring:= topic lists under both standup headers while every rule requiring a Yesterday/Today/Blockers brief was already on the page. The rule existed and was skipped, so the fix is a gate rather than more prose: Phase 8 now blocks until every standup on the day's calendar carries its own script, and the Standups section says plainly that the lines are the words Craig speaks — complete first-person sentences, one script per standup since the altitude rules admit different material to each, roughly 120-150 words for the under-60-seconds target. A worked topics-vs-script contrast sits with the shape rules, because prose decays back into bullets and an example doesn't. The script is a draft Craig edits at the gate, never words put in his mouth: a topic list fails safe by forcing him to compose, where a wrong script reads fluently enough to be spoken unchanged. diff --git a/claude-templates/.ai/workflows/helper-mode.org b/claude-templates/.ai/workflows/helper-mode.org index a6acfa7..b32d574 100644 --- a/claude-templates/.ai/workflows/helper-mode.org +++ b/claude-templates/.ai/workflows/helper-mode.org @@ -12,13 +12,14 @@ The governing fact behind every rule below: the session-context split isolates e * When to Use This Workflow -No operator trigger phrase. A helper reaches this contract one of three ways: +No operator trigger phrase. A helper reaches this contract one of two ways: - The =ai --helper= launcher routes here after the roster confirms a live agent (the deterministic path). -- Startup's roster check finds the session is not alone and routes here instead of running normal startup (the safety net for a raw =claude= launch). - An explicit "you are a helper, follow helper-mode.org" instruction (the manual fallback). -If none of those applies — the roster shows the session is alone — this is a primary session. Run normal [[file:startup.org][startup.org]], not this. +There is deliberately no third way, and the gap matters: *startup does not check the roster*. A bare =claude= launched into a project that already has a live session runs full primary startup — pulls, rsync, inbox processing — without ever reaching this file. That safety net is designed (see Status below) but unbuilt, so nothing catches a raw launch. Use =ai --helper=. + +If neither route applies, this is a primary session. Run normal [[file:startup.org][startup.org]], not this. * Identity @@ -92,10 +93,28 @@ A helper does not run normal startup. It runs a light version: When the helper's work is done: -1. Re-run the roster (=.ai/scripts/agent-roster=) to learn whether a primary is still live. +1. Re-run the roster to learn whether a primary is still live. Pass the project root explicitly — =agent-roster= defaults to =$PWD= and keeps only agents at or inside that root, so calling it from a subdirectory hides a primary sitting at the root and reports "alone": + + #+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 + + Read rc as =wrap-it-up.org= Step 0 does: 1 means a primary is still live, 0 means this helper is orphaned, and 2 (or an absent script) means unavailable — which takes the same archive-only path as 1, because leaving work uncommitted is recoverable and committing under a live primary is not. 2. *Primary still live (the normal case):* finalize the Summary in the helper's own =.ai/session-context.d/<id>.org=, archive it to =.ai/sessions/YYYY-MM-DD-HH-MM-<id>-<description>.org=, and stop. Do NOT commit, push, or run hygiene — the primary's next commit picks up the archived file and any scoped edits the helper left in the tree. 3. *Orphaned helper (roster shows the helper is now alone):* the primary already exited, so the helper assumes full closing duties — the git ban lifts because the concurrency that justified it is gone. Commit and push the tree (including the helper's own edits, which would otherwise strand as a dirty tree), per the normal wrap-up flow in [[file:wrap-it-up.org][wrap-it-up.org]]. * Status -Phase 1.5 of the generic-agent-runtime spec. This contract is the canonical home; the spawn paths (=ai --helper=, startup's roster branch) and the [[file:wrap-it-up.org][wrap-it-up.org]] helper branch route here. Those wiring pieces ship behind the spec's bats-then-drills-then-pilot gate and are not yet live; until then, the manual "you are a helper" instruction is how a session adopts this contract. +Phase 1.5 of the generic-agent-runtime spec. This contract is the canonical home; the spawn paths and the [[file:wrap-it-up.org][wrap-it-up.org]] helper branch route here. + +Live now: =ai --helper <project>= (roster check, id assignment, helper opener, its own tmux window), the explicit "you are a helper" instruction, and the wrap-it-up.org Step 0 helper branch. + +Not built yet, and worth knowing because it is the gap you can fall into: *startup has no roster check*. A second session launched as a bare =claude= in a project that already has one runs full primary startup — pulls, rsync, inbox processing — with no idea another agent is live. Until that safety net exists, =ai --helper= is not merely the preferred path, it is the only one that makes a helper without being told. + +Also unbuilt: the live-helper gate that pauses a primary's file-wide hygiene passes (=todo-cleanup.el=, =lint-org.el=, =wrap-org-table.el=) while a helper is mid-edit. Data-integrity rule 1 above describes the intended behavior; nothing enforces it yet, so a primary running hygiene can still clobber a helper's just-written scoped edit. diff --git a/claude-templates/.ai/workflows/sentry.org b/claude-templates/.ai/workflows/sentry.org index b25fc14..0e8f819 100644 --- a/claude-templates/.ai/workflows/sentry.org +++ b/claude-templates/.ai/workflows/sentry.org @@ -91,6 +91,10 @@ Every reclaim of a stale lock surfaces in the digest — the helper prints the r Each cycle, after acquiring the single-runner lock and verifying branch state (below), walks the pass list in order. Every pass follows the same four-step contract: +*Run the walk in a BACKGROUND subagent, not the main thread.* The pass list is roughly ten tool calls and it repeats every cycle, while the main thread's tool output lands in the Emacs buffer Craig is working in. Eleven cycles inline on 2026-08-05 filled his workspace with output that almost entirely confirmed nothing had changed. Backgrounding is the load-bearing part: a background agent's output goes to a file, while a foreground dispatch puts it straight back in his terminal. The main thread then reports only what the quiet-output rule allows: deviations, plus the one-line heartbeat at cycle-end. See the Output-Destination Override in =subagents.md= and the quiet-output rule in =interaction.md=. A recurring prompt that tells the agent to walk the list inline is overriding this. Fix the prompt rather than the workflow. + +*Where the thread boundary falls.* The dispatched agent owns the whole walk, which means the per-pass work, the =session-context.org= entries, the per-pass commits, and the between-pass lock refreshes all happen inside it. The main thread keeps what brackets the walk: the entry gates, acquiring and releasing the single-runner lock, branch-state verification, and the cycle-end digest. The lock survives the boundary because =agent-lock= is keyed by name rather than by process, so a refresh from the subagent holds the same lock the main thread acquired. + 1. *Probe* — a cheap existence check for the pass's target (named per pass below). Absent → the pass is one skip line in the digest and nothing more. This is what makes the pass list portable: passes self-activate where their target exists and stay silent elsewhere, with zero per-project configuration. 2. *Work* — run the pass under the unattended contract. Quick, solo, already-agreed mechanical actions execute. Anything destructive or requiring judgment does *not* execute — it appends to the morning-approval queue (what, why, the exact command or edit that fires on approval). A pass runs fully or not at all; there is no reduced-form pass. diff --git a/claude-templates/.ai/workflows/startup.org b/claude-templates/.ai/workflows/startup.org index 2262eea..bc89256 100644 --- a/claude-templates/.ai/workflows/startup.org +++ b/claude-templates/.ai/workflows/startup.org @@ -137,39 +137,24 @@ These calls have no dependencies on each other. Issue them all together in one m #+end_src 3. *Sync =.ai/= from templates — but only when the synced source paths in rulesets are clean.* Guard the three rsyncs behind a check that =claude-templates/.ai/{protocols.org,workflows/,scripts/}= have no uncommitted changes. Otherwise Phase A copies in-flight rulesets WIP (tracked edits or new untracked files) into this project's =.ai/workflows/= and =.ai/scripts/=, where it shows up as drift the user didn't author. Skipping once is cheap — the next session with rulesets clean catches up. The check is scoped to the synced paths, so unrelated rulesets dirt (a stray =session-context.org=, scratch files) doesn't needlessly block the sync. A second guard skips the same rsyncs when the *project* branch is behind its upstream (=git rev-list --left-right --count @{u}...HEAD= with =behind > 0=): syncing templates onto a stale committed =.ai/= baseline measures the diff against old content, so it comes out huge and conflicts when the branch later reconciles to upstream, whose history already carries the newer templates. It composes with the rulesets-clean guard — a stable rulesets source and a current project branch are both required before the sync runs. - #+begin_src bash - rs="$HOME/code/rulesets" - synced_dirty=$(cd "$rs" && git status --porcelain -- \ - claude-templates/.ai/protocols.org \ - claude-templates/.ai/workflows/ \ - claude-templates/.ai/scripts/ 2>/dev/null) - # Skip the sync when the project branch hasn't reached its upstream. Syncing - # templates onto a behind baseline measures the diff against stale committed - # .ai/, producing confusing drift that conflicts when the branch reconciles — - # the newer .ai/ is already in upstream. behind==0 (up-to-date or ahead-only) - # means HEAD contains all of upstream, so the baseline is current. No upstream - # (new/unpushed branch) → rev-list fails → proj_behind stays 0, sync runs. - proj_behind=0 - if [ -d .git ]; then - counts=$(git rev-list --left-right --count '@{u}...HEAD' 2>/dev/null) \ - && [ "$(printf '%s' "$counts" | cut -f1)" -gt 0 ] 2>/dev/null \ - && proj_behind=1 - fi + The logic lives in =.ai/scripts/sync-templates=, not inline here. It was extracted 2026-07-31 after an uncommitted edit in rulesets silently blocked all three rsyncs for a full day — five workflow files went stale in one downstream project alone, with nothing anywhere reporting it. A mechanism that distributes correctness fixes to every project needs tests, and inline bash in an org file cannot have them. The behavior is unchanged by the extraction (verified differentially, output and resulting tree both byte-identical); the guards it applies are described below. - if [ -n "$synced_dirty" ]; then - echo "rulesets has uncommitted changes under the synced template paths — skipping .ai/ sync this session (catches up when rulesets is clean):" - echo "$synced_dirty" | sed 's/^/ /' - elif [ "$proj_behind" -eq 1 ]; then - echo "project branch is behind upstream — skipping .ai/ sync this session (templates never land on a stale baseline; the sync runs once the branch is current)" + #+begin_src bash + if [ -x .ai/scripts/sync-templates ]; then + .ai/scripts/sync-templates else - rsync -a "$rs/claude-templates/.ai/protocols.org" .ai/protocols.org - rsync -a --delete "$rs/claude-templates/.ai/workflows/" .ai/workflows/ - rsync -a --delete --exclude='__pycache__' --exclude='.pytest_cache' --exclude='*.pyc' \ - "$rs/claude-templates/.ai/scripts/" .ai/scripts/ - echo ".ai/ synced from templates" + echo "sync-templates not present — .ai/ sync SKIPPED and cannot self-heal; recover with: bash ~/code/rulesets/scripts/audit.sh --apply --force" fi #+end_src + The fallback should never fire in the ordinary rollout. A project still on the pre-extraction startup.org runs the old inline block this session, which delivers both the script and this file together, and the next session finds the script in place. It covers only the split case — the =workflows/= rsync landing while the =scripts/= one didn't — where a project would otherwise hold this file with no script to call. + + *That state does not self-heal, which is why the message names a command.* The fallback runs /instead of/ the sync, so there is no later sync to deliver the missing script: the project would announce one line per session forever while its templates froze. Recovery is out-of-band, via =scripts/audit.sh --apply --force= in rulesets, which rsyncs =scripts/= directly. =--force= is there because audit skips a tracked project holding uncommitted =.ai/= changes, which a project wedged across several sessions is likely to be. + + One caveat on that recovery, worth knowing before running it: audit's =scripts/= rsync carries none of the =__pycache__= / =.pytest_cache= / =*.pyc= excludes this sync does, so it can deposit python cache artifacts that then need removing by hand. Tracked separately; it is a pre-existing gap in audit rather than something this path introduced. + + Announcing the skip loudly with its remedy is the point; a silent skip is the exact failure this extraction exists to stop. + 4. =\ls -t .ai/sessions/ 2>/dev/null | head -5= — list 5 most recent session files. The backslash bypasses any =ls= alias in the user's profile. Without it, bare =ls -t= silently returns no output under =exa= (a common =ls= replacement) — which makes a sessions directory full of files look empty, and the agent then skips Phase B step 2. 5. =\ls -la inbox/ 2>/dev/null= — inventory the inbox. Same reason for the backslash escape, applied uniformly across the Phase A =ls= calls. 6. Read =.ai/notes.org= — Project-Specific Context, Active Reminders, Pending Decisions sections (skip About This File). @@ -213,7 +198,7 @@ These calls have no dependencies on each other. Issue them all together in one m Fleet descriptions ("the fleet is ratio and velox") and runtime derivations ("run =uname -n= to find the hostname") don't match — only current-identity assertions do. Fixture-verified under bash and zsh. -Notes on the rsync commands: +Notes on what =sync-templates= does (the rsync behavior it carries): - Trailing slashes on both source and destination matter — they tell rsync to sync /contents/ rather than nest a directory inside. - =--delete= on the directory syncs lets retired template files actually disappear from each project on next startup. - protocols.org is a single file, no =--delete= needed. diff --git a/claude-templates/.ai/workflows/triage-intake.personal-gmail.org b/claude-templates/.ai/workflows/triage-intake.personal-gmail.org index 7fb1231..0bc94fc 100644 --- a/claude-templates/.ai/workflows/triage-intake.personal-gmail.org +++ b/claude-templates/.ai/workflows/triage-intake.personal-gmail.org @@ -30,6 +30,10 @@ The rule binds the *anchor* windows only. The date-slice walk below deliberately ⚠ *The MCP caps at =maxResults=100= and exposes NO =pageToken= parameter.* The response carries a =nextPageToken=, but the tool can't consume it, so a pile over 100 is silently truncated — the tail below the cap never gets classified, and every later anchored sweep skips it (it predates the new anchor). This is exactly how a 300+ backlog accumulated invisibly by 2026-07-08. Two consequences: - *Never treat a 100-row result as complete.* When a scan returns exactly 100, walk the tail in *date slices*: re-query with =before:<oldest-full-day-seen>= (day resolution), repeat until a page returns fewer than 100, dedupe by message id across slices (the day-resolution boundary overlaps). + + ⚠ *The date-slice walk is a floor, not a complete read.* It bottoms out at day resolution, so any single day carrying more than the cap still truncates — and nothing in the response signals it. Measured 2026-08-01: three slices through the MCP reported about 250 messages where the REST API, paginated properly, found 291. The walk narrows the gap; it doesn't close it. + + So pick by what the number is for. A *routine sweep* tolerates the floor, because the next sweep re-reads live unread state and picks up what was missed. A *backlog inventory* does not, and neither does any count reported to Craig as complete — go to the Gmail REST API with real =pageToken= pagination instead. No tool ships for that here, so reaching it means stepping outside this MCP; until one does, an inventory taken through the MCP must be reported as a floor ("at least N"), never as a total. - *Never report =resultSizeEstimate= as a count.* It's unreliable — observed stuck at "201" across three different queries whose real union exceeded 300. *** Backlog-residue check (every sweep — cheap, mandatory) diff --git a/claude-templates/.ai/workflows/triage-intake.telegram.org b/claude-templates/.ai/workflows/triage-intake.telegram.org index 1319da5..6ec9d66 100644 --- a/claude-templates/.ai/workflows/triage-intake.telegram.org +++ b/claude-templates/.ai/workflows/triage-intake.telegram.org @@ -50,8 +50,10 @@ server never disables the source. The setq is mandatory defense: tdlib crashed in native mode when this was set up (2026-06-09) — a separate matter from the SEGFAULT gotcha, which is about the loadChats argument — and Craig's daemon defaults to nil. - Wait ~2s for Ready, then (telega--loadChats '(:@type "chatListMain")) until telega--chats - is populated. + Wait ~2s for Ready, then (telega--loadChats '(:@type "chatListMain") #'ignore) + until telega--chats is populated. The #'ignore callback is mandatory, not + decoration: without it every call prints a spurious "Telega error 404" (see + Step 1's comment). 3. Check messages: the maphash unread scan in ** Scan Step 2 (filters the messageContactRegistered join-notice noise). 4. Send (needs the server live; /voice personal first — Telegram @@ -135,7 +137,25 @@ emacsclient -e "(progn # The `fboundp' guard matches Step 0: if the launch failed outright telega is not # loaded, and that should read as 'server-died like any other failure rather than # signalling void-function. -emacsclient -e "(progn (ignore-errors (telega--loadChats '(:@type \"chatListMain\"))) (ignore-errors (telega--loadChats '(:@type \"chatListMain\"))) (if (and (fboundp 'telega-server-live-p) (telega-server-live-p)) 'loaded 'server-died))" +# +# The `#'ignore' callback is what keeps this quiet, and it is a THIRD separate +# concern from the two above — it touches the callback, not the argument and not +# docker mode. Without it each call printed `Telega error 404: Not Found' into the +# echo area and *Messages* of Craig's daemon, two lines per scan from whichever +# project ran it. The 404 is TDLib's end-of-list marker ("Return error if all chats are +# loaded"), not a fault: `telega--loadChats' hands the callback to +# `telega-server--send-or-call', which sends fire-and-forget when it is nil, so the +# reply matches no `:@extra', falls through to `telega--on-error' — a bare catch-all +# whose whole body is that message — and gets printed. telega's own startup call +# (telega.el:290) passes a callback and is silent for exactly this reason. +# Passing a callback keeps the call asynchronous: the block-and-wait path in +# `telega-server--call' is reachable only when the callback is nil. +# +# The macro's docstring reads "Use this macro instead of `(or callback 'ignore)' +# pattern." That is guidance for whoever writes a tdlib wrapper — don't hardcode a +# callback inside the wrapper — not a bar on the caller choosing one. We are the +# caller. +emacsclient -e "(progn (ignore-errors (telega--loadChats '(:@type \"chatListMain\") #'ignore)) (ignore-errors (telega--loadChats '(:@type \"chatListMain\") #'ignore)) (if (and (fboundp 'telega-server-live-p) (telega-server-live-p)) 'loaded 'server-died))" #+end_src On a persisted session telega reaches status "Ready" within ~2s; the chat list diff --git a/claude-templates/.ai/workflows/wrap-it-up.org b/claude-templates/.ai/workflows/wrap-it-up.org index a9a5895..ecd3d22 100644 --- a/claude-templates/.ai/workflows/wrap-it-up.org +++ b/claude-templates/.ai/workflows/wrap-it-up.org @@ -29,6 +29,8 @@ The wrap-up is complete when: 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,7 +45,35 @@ This depends on three functions in =.emacs.d/modules/ai-term.el= (=cj/ai-term-qu * The Workflow -** Step 0: Refuse if sentry is live +** 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: |
