aboutsummaryrefslogtreecommitdiff
path: root/todo.org
diff options
context:
space:
mode:
Diffstat (limited to 'todo.org')
-rw-r--r--todo.org2881
1 files changed, 1254 insertions, 1627 deletions
diff --git a/todo.org b/todo.org
index 8eb0b02..9d47566 100644
--- a/todo.org
+++ b/todo.org
@@ -30,23 +30,522 @@ Optional *effort and autonomy tags* — orthogonal to type, both can apply on th
- =:quick:= — likely to take ≤30 minutes from start through verification.
- =:solo:= — Claude can complete the work end to end, including verification, without input from Craig.
+Optional *dependency tags* — cross-project, both plain tags with the which-project detail in the task body (per =todo-format.md=):
+
+- =:blocked:= — the task can't advance until another project delivers the work named in its body. =open-tasks.org= pulls =:blocked:= tasks out of the cascade and surfaces them on their own. Distinct from =VERIFY= (which waits on Craig).
+- =:blocker:= — this task owes work that's blocking another project (named in its body). =open-tasks.org= surfaces =:blocker:= tasks first, since clearing one unblocks the other project.
+
Tags are assigned and refreshed by =task-audit=; =task-review= keeps them honest in passing.
* Rulesets Open Work
-** DOING [#B] Wrap-up inbox/transcript routing to destination projects :feature:spec:
+** DOING [#B] Hostile subagent review before every agent commit :feature:
+:PROPERTIES:
+:LAST_REVIEWED: 2026-07-28
+:END:
+From the roam inbox, 2026-07-28: "code reviews must occur before every commit an agent does, and they should be hostile reviews from a subagent without the agent's context."
+
+Two asks, and only the second is new. The publish flow already mandates a review before every commit (Step 1). What changes is *who reviews*: today the reviewing agent is the one that wrote the change, so it inherits the author's mental model, and =review-code= only *suggests* subagent dispatch, and only "for substantive reviews on large diffs".
+
+Decisions settled with Craig, 2026-07-28, and shipped:
+- *Scope* — every commit. The reviewer's own Phase 0 rules a diff trivial and returns Skipped, which satisfies the gate; the author never rules on their own diff.
+- *Stance* — "adversarial", not "hostile" (Craig's call). An agent told to attack manufactures findings, so the stance carries a substantiation floor: a finding not substantiated against the diff is dropped.
+- *What the reviewer gets* — the diff, a one-line claim of what it does, and the requirement source (ticket, plan, task body) where one exists. Withheld: the conversation, the exploration, the author's rationale. The requirement source stays *in* because it is the only artifact that can contradict the author's claim; withholding it makes the claim self-certifying.
+- *Loop* — re-review until the reviewer approves, turning on blocking findings rather than the verdict token. Bounded at three rounds, and stopped early on a finding that recurs after being reported fixed. Both bounds hand the decision to Craig; the unattended callers park instead.
+- *Adjudication* — Craig, never the author overruling the reviewer.
+- *Home* — the =publish= skill Step 1, with =review-code= carrying the adversarial contract and re-review mode.
+- *The =subagents.md= tension* — resolved with an Isolation Override section: the size heuristics assume the main thread could do the task equally well, and they lapse when its own context is what makes its answer untrustworthy.
+
+Remaining: nothing on the design. The change shipped in this session.
+
+** TODO [#B] wrap-org-table splits logical rows, and lint drives it :bug:
+:PROPERTIES:
+:LAST_REVIEWED: 2026-07-29
+:END:
+Reported by work 2026-07-28 against =arch-00-deepsat-platform-spec-draft.org=. Reproduced here.
+
+*** Verified
+
+Each of these is a measurement, re-run under adversarial review. The analysis I built on top of them was wrong three times, so this section is deliberately separated from the open questions below.
+
+- *The defect.* =wrap-org-table.el= turns one logical table row into two or more, by writing a rule between its continuation lines. Content survives; structure does not.
+- *Root cause.* =wot--continuation-group-p= (=wrap-org-table.el:168=) requires every line past the first to carry at least one empty cell. When a row overflows in every column its continuation line is fully populated, the predicate rejects the group, and =wot--logical-rows= appends each physical line as its own row (=:197=).
+- *Controlled A/B.* Rules present in both, continuation line's middle cell the only variable: blank merges, populated splits.
+- *Idempotence is broken.* Running the tool twice on its own correct output corrupts it. Pass 1 emits a properly rule-delimited three-line row; pass 2 splits it into three rows. The docstring at =:203-204= asserts the opposite, and =wot-reformat-is-idempotent= passes because its fixture overflows only one column.
+- *lint doesn't just miss it, it causes it.* =lint-org.el:424= calls the same predicate. Given the tool's own correct output — a rule after every logical row — lint reports "missing rule between rows — wrap-org-table.el reflows it". Nothing is missing. Follow that advice and the tool splits the row; lint then reports the result 0 mechanical, 0 judgment. Control: a conformant table whose continuation keeps an empty cell returns 0 and 0. So the loop is not two independent green lights, it is the linter manufacturing a false violation and certifying the damage it caused.
+- *A second path.* With no hlines at all, =wot--logical-rows= short-circuits (=:184=) before the predicate is reached, so every physical line becomes a row.
+- *Nothing invokes the tool unattended.* =todo-cleanup.el= names it in comments only; the entry-script guard from the 2026-07-09 incident holds.
+- *rulesets is exposed*: =todo.org= carries a four-row attachment-sanitization table with no rules between rows. Don't reflow it until the tool is fixed — reflowing is the trigger.
+
+*** Two fixes that were verified to work
+
+- *Conformant round-trip.* Replacing the predicate body with =(> (length group) 1)= — pure rule-delimited grouping, no emitter marker, no new field — makes the corrupting pass-1 output round-trip cleanly.
+- *Telling a continuation group from two real rows at runtime.* Provenance isn't available (=wot-reformat-table-string= receives a string), but the test is cheap: merge the group, re-wrap at the allocated widths, compare to the group as given. A continuation group reproduces itself under merge-then-wrap; two genuinely distinct short rows don't, because merged they fit on one line.
+
+Start with a red test from the double-run repro — it needs no unusual input and falsifies the docstring and the passing idempotence test together. Emission is already correct (=:222-224= emits one rule per element of =rows=), so the repair is entirely in how =rows= is computed.
+
+*Prefer the round-trip check to any detector.* work implemented the predicate-based detection I recommended and demonstrated it cannot discriminate at any threshold (2026-07-29, worked examples from their repo). Run bare it flags 144 files — essentially every table — because an ordinary header-plus-body table is one multi-line fully-populated group and rejecting it is the predicate working correctly. Add a per-row-ruled precondition and it cuts to 9, but at 9 it still mixes a genuinely wrapped row (=arch-09:40=, a header row whose second line continues the sentence) with two distinct rows legitimately sharing a rule (their =todo.org:117=, where splitting is the *right* answer). Same structural signature, opposite correct verdicts. The difference is whether one line continues the other as prose, which is semantics and not in the parse.
+
+So =lint-org.el:424= cannot simply inherit the repair, and a checker keyed on the predicate would train people to ignore it. The idempotence property is the checkable one: reflow twice and diff. It sidesteps detection entirely, because round-tripping the tool's own output correctly is true by construction and needs nobody to decide what a group means.
+
+*** Open questions
+
+- *What should no-hline input do?* My "refuse to reflow" instruction was wrong: adding rules to a ruleless table is the tool's main job, and =test-wrap-org-table.el:147= asserts exactly that. The real danger is narrower — a no-hline table where some line carries an empty cell, so a physical line might be a continuation. Needs a decision on refuse, ask, or heuristic.
+- *Which path bit work?* One question settles it: did the =arch-00= Document Status table have rules between its rows before the reflow? I inferred "probably secondary" from a file-level scan, which can't resolve a table-level incident.
+- *How exposed is work?* Partly resolved by their own implementation, 2026-07-29. *Four secondary-path files are exact.* The primary-path number is 9 under a per-row-ruled precondition, which they correctly label a floor on a population they cannot cleanly define rather than a measurement — the precondition also drops the minimal =a_full= fixture, which has only two groups. My earlier "24 secondary-path files" certification was not sound: their original signature also matches every correctly-reflowed table, so it never partitioned.
+- *Does the grade go up?* Currently Major x most users frequently = P2 = [#B] (any table you reflow that overflows every column, which is what a width violation sends you to the tool to fix). The lint-drives-it finding arrived after that grading and may lift it.
+
+*** Provenance
+
+work reverted their table and left it over-budget at 134 columns, correctly: an over-wide table is cosmetic, a table that says something else is not. I hit this same failure on 2026-07-27, wrote "reflowed a table into a worse shape and lint-org then passed it" into a session summary, and never filed it — which is why work met it three weeks later. Their framing beats the apology: a correct observation recorded and then read as fine is the same failure as a green check on a corrupted table.
+
+Process note for whoever picks this up. This task bounded out of its review loop at three rounds. Every finding across all three landed on inference, never on a measurement — the reproductions held throughout while the reasoning built on them was refuted repeatedly. Trust the Verified section; re-derive anything else.
+
+
+** TODO [#B] Synced workflows link outside the synced set with ../../ :bug:
+:PROPERTIES:
+:LAST_REVIEWED: 2026-07-28
+:END:
+Seven link sites across four synced workflows, three distinct targets, all escaping the =.ai/= boundary into rulesets repo-root paths. From a consuming project's =.ai/workflows/=, =../../= is that project's root, where none of these exist:
+
+- → =../../claude-rules/todo-format.md= (five sites): =open-tasks.org:163=, =task-audit.org:84=, =task-review.org:60=, =task-review.org:64=, =task-review.org:99=
+- → =../../docs/design/task-review.org= (one site): =task-review.org:11=
+- → =../../flush/SKILL.md= (one site): =suspend.org:22=
+
+Verified dead in both home and =.emacs.d=; they resolve only in rulesets, which is why nobody noticed.
+
+Grading: Minor severity (a documented reference an agent can't follow, workaround is to search) x every user every time (every consuming project, every sync) = P2 = [#B]. Same grade as the =references/= link this came from, and the same mechanism — a synced file linking a path the sync doesn't deliver — with seven sites instead of one.
+
+Fix direction, two halves. Rewrite the seven as prose references naming the file, the same move taken for the credential paths: a link that resolves in one repo shouldn't be a link in a file that ships to two dozen. Then close the gate, or the eighth arrives unnoticed.
+
+The gate already exists and is nearly right. =scripts/lint.sh='s =check_md_links= was written for this exact class — its comment says so ("Validate cross-references to =claude-rules/= — the install-layout problem"). It misses these for two concrete reasons: it matches only markdown link syntax (=grep -oE '\[[^]]*\]\([^)]+\)'=), so org =[[file:...]]= links are invisible to it, and its driver loop only feeds it =claude-rules/*.md= and the language rule files, never =.ai/workflows/*.org=. Extending it on both axes is a smaller and more durable fix than a manual sweep.
+
+Found by the adversarial reviewer on the =references/= fix, 2026-07-28, as the sibling class the original report missed.
+
+** TODO [#C] start-work Phase 7 still summarizes the old publish flow :chore:solo:
+:PROPERTIES:
+:LAST_REVIEWED: 2026-07-28
+:END:
+=.claude/commands/start-work.md:336-339= hands off with "Follow =commits.md= exactly" and "Run =/review-code --staged= before each commit" — no isolated reviewer, no re-review loop. It was already stale before the 2026-07-28 review change, because =commits.md= moved the publish flow into the =publish= skill in an earlier commit, so the pointer names a file that no longer holds the flow.
+
+Grading: Cosmetic severity (a stale summary beside a correct canonical, and start-work is attended so the escalation target exists) x most users frequently = P3 = [#C].
+
+Fix is to point Phase 7 at the =publish= skill rather than restate the flow, which is what let it drift in the first place. Worth a sweep for other files that restate the flow instead of pointing at it.
+
+Found by the adversarial reviewer during the 2026-07-28 review-flow change, and correctly filed rather than fixed there: the line was untouched by that diff.
+
+** TODO [#C] Two lint defects at the template source :chore:
+:PROPERTIES:
+:LAST_REVIEWED: 2026-07-28
+:END:
+Both verified at the rulesets source, so every project seeded from them inherits the defect.
+
+=retrospectives/PRINCIPLES.org:38= violates the org-table standard (no closing rule). =lint-org= flags it as =org-table-standard=, and =wrap-org-table.el= reflows it mechanically. This half is purely tool-driven.
+
+=protocols.org= lints at 8 mechanical + 19 judgment =misplaced-heading= hits, all from Markdown =**bold**= in an org file, which org reads as a level-2 heading when it starts a line. 48 bold spans total, 14 of them line-initial. This half is not mechanical: converting to org =*bold*= is 48 edits, and =lint-org --fix= would rewrite the line-initial ones without knowing they are emphasis rather than headings.
+
+Grading: Cosmetic severity x every user every time = P3 = [#C]. It is most of the linter's noise on protocols.org, which is the real cost — noise that trains the reader to skip the report.
+
+Not =:solo:= — both files are synced templates.
+
+Source: winvm link-integrity pass, 2026-07-28.
+
+** DOING [#B] Finish context-engineering rightsizing :refactor:
+:PROPERTIES:
+:LAST_REVIEWED: 2026-07-27
+:END:
+Started 2026-07-27 from three Anthropic posts Craig supplied. Always-loaded rules went from ~57,800 tokens to ~28,949, plus 13,461 path-scoped. Shipped: =paths:= frontmatter on the three file-type rules, the per-project generic-rule de-duplication, =commits.md= split into a 2,804-token invariant core plus the =publish= skill, =testing.md= split into a 347-word directive plus the =testing-standards= skill, the approval-gate signal fixed from =.ai/=-tracking to remote host, and the first-person directive.
+
+*What remains is Craig's decisions, not execution.* Each of these needs him:
+
+1. *C1 — =verification.md= (3,388 tok).* The Opus 5 guide says explicit verification instructions cause over-verification and should be removed. His standing direction is never guess, always check. My read: the honesty core (don't claim a green suite you didn't run) stays and shortens, the process injection (green baseline, suite-as-its-own-step) moves to the publish skill. His call — and it's the rule closest to a preference he's stated outright.
+2. *=interaction.md= (3,828 tok, now the largest).* Carries genuinely universal output constraints (no popup menus, no reverse-video markup) plus the new peer-reasoning contract. Splitting it means deciding which parts must fire on every turn.
+3. *The TDD rationalization table.* Moved to =testing-standards= rather than cut. The posts argue that kind of over-argument is counterproductive on current models. Deleting his defense against me skipping TDD is his call.
+4. *D3 — the gate separation.* Which approval gates are preference (he wants to see what goes out under his name) versus guardrail (written when the worst case was worse). They read identically in the files; only he can tell them apart. Highest-leverage input remaining.
+
+*Do first:* the three docs in =working/context-engineering-rightsizing/= are one commit behind — they were corrected at d74d98d, before the two splits and the gate fix. Reconcile before deciding anything from them.
+
+Risk on the record: =testing.md='s margin is thinner than =commits.md='s. If =testing-standards= fails to trigger mid-test-writing the mocking-boundary rules are lost — a quality regression, visible in review, but a real bet where =commits.md='s moved half was purely procedural.
+
+** TODO [#B] Recurring-loop mechanics as a shared rule :feature:
+:PROPERTIES:
+:LAST_REVIEWED: 2026-07-27
+:END:
+Work proposes promoting the pattern behind its 2026-07-27 auto-mode triage-intake into a standing rule covering all recurring agent tasks: fixed interval via =CronCreate= rather than dynamic self-pacing, a subagent per firing so raw scan output never reaches the orchestrator, silence with no heartbeat when subagent-backed, and accumulate-don't-mutate between closes with explicit "close the X" / "stop the X" verbs. Likely touches =triage-intake.org= auto mode, =inbox.org= monitor mode, and a new short rule in =claude-rules/= so individual workflows point at one definition of cadence, isolation, and silence.
+
+Points 1, 2, and 4 mostly promote existing practice. Point 3 changes documented behavior and needs a real decision. Three findings from the skeptical review, all to resolve before this ships:
+
+1. *Point 1 is too absolute.* Fixed interval is the right default, but not the right universal. A loop waiting on unpredictable external state (a CI run, a deploy queue) should pace to how fast that state actually changes, which is what dynamic scheduling exists for. Write it as "default to a fixed interval; use dynamic pacing only when polling external state whose timing you can't predict."
+2. *Point 2 collides with a standing instruction.* Craig's harness prompt says not to spawn subagents unless he requested it. Making subagent-per-firing the standing pattern needs that reconciled explicitly — the honest reading is that asking for the loop *is* the request, and the rule should say so rather than leaving two instructions to fight. The isolation argument itself is sound and matches the Opus 5 guidance (delegate for genuinely independent, sizeable work).
+3. *Point 3 has a silent-failure hole.* Removing the heartbeat means a loop that died looks exactly like a loop quietly finding nothing. "The subagent completing is proof it ran" only holds if a *failed* subagent still surfaces. Either keep a rare heartbeat (hourly, not per-fire) or specify that failure always breaks silence. Suppressing success is fine; suppressing failure is not.
+
+Also underspecified: what counts as "signal" for a subagent-backed loop. And the silent-until-signal spec (=docs/specs/2026-07-20-silent-until-signal-monitors-spec.org=, IMPLEMENTED) documents the per-fire heartbeat, so it needs a superseding history line rather than a silent contradiction.
+
+*** 2026-07-27 Mon @ 16:55 Work accepted all three findings and sharpened point 3
+
+Work agreed without pushback and corrected my either/or on point 3, which was too weak. Both mechanisms are needed, not one: a failed or hung subagent breaking silence covers a *scan-level* failure, but only a periodic heartbeat catches the *scheduler* dying — because in that case nothing spawns at all and there is no failure to report. Heartbeat rare (hourly), not per-fire.
+
+The live consequence makes it urgent rather than theoretical: =CronCreate= auto-expires recurring jobs after seven days, so any fully-silent loop is *guaranteed* to die quietly. Work's auto-triage loop is running under exactly that contract right now.
+
+Work also added the reason that makes point 2's carve-out principled and should go in the rule text: the subagent exists to keep N sources' worth of raw scan output out of the orchestrator across a multi-hour loop, not to parallelize.
+
+Craig's call on point 3 is pending; work is surfacing it and will send the answer.
+
+Source: work handoff 2026-07-27 14:51, reply 16:55.
+
+** TODO [#B] Sentry triage split — work mail and messengers in, personal mail out :bug:
+:PROPERTIES:
+:LAST_REVIEWED: 2026-07-27
+:END:
+Craig's 2026-07-27 correction supersedes his 2026-07-21 ruling: sentry should scan DeepSat work mail and every active messenger source, excluding only personal email. =sentry.org= currently excludes all mail and all messengers (overview line and pass 3), so work's first 2026-07-27 fire reported a quiet pass while a Hayk DM and a Kostya PR-review request sat unread. Work's live at-prompt carries the override in the meantime; the canonical workflow, the source-activation probe, and the tests still encode the old policy.
+
+Grading: Major severity (the pass reports "nothing" while real work items sit — the failure is silent, which is what makes it worse than a visible miss) × most users, frequently (every fire in any project declaring a work-mail or messenger source) = P2 = =[#B]=.
+
+The blocker is not wording. The current rule excludes by *category* — "mail", "messenger" — and the new rule is a *work-vs-personal* split that category cannot express. Shipping general plugins are =cmail=, =personal-gmail=, =personal-calendar=, =telegram=, =github-prs=; there is no general work-mail plugin, so work's source is project-specific. Naming the personal plugins in a denylist fails open the moment a new personal source is added. Decide the classification mechanism first — the durable shape is a per-plugin eligibility declaration inside each plugin file, so a new plugin classifies itself and the probe reads the declaration rather than pattern-matching a name.
+
+Scope once decided: the =sentry.org= overview line and pass 3, the activation probe's "survives that exclusion" test, and the pass-3 tests.
+
+Source: work handoff 2026-07-27 06:21.
+
+** TODO [#B] Repository publish-lock for two sessions sharing one clone :feature:
+:PROPERTIES:
+:LAST_REVIEWED: 2026-07-27
+:END:
+Craig approved this in archsetup on 2026-07-26 after weighing the competing approaches recorded there. Two agent sessions in one clone share the working tree *and* =.git/index=, so one can sweep the other's hunks into a commit; a pathspec commit doesn't help, because =git commit -- <path>= reads the current working-tree state for that path. The approved design: one repository-scoped publish lock keyed on the real Git common-directory path (so worktrees sharing a clone contend, and unrelated clones with the same basename don't), acquired before any publish-flow operation that can mutate the shared index and held across reconcile, staging, =/review-code --staged=, message approval, and =git commit=. Ownership tracks =AI_AGENT_ID=, then a stable harness id. The reviewed staged-tree fingerprint (=git write-tree= after the review) is re-compared immediately before commit; a lost lock, a vanished lock, or a changed fingerprint forces reacquire and a fresh staged review. Ordinary edits stay concurrent — the lock serializes only the publish mutation window.
+
+Skeptical review — the design is sound and the acceptance checks are testable. Three gaps to close during the build:
+
+1. *TTL sizing across a human wait.* The lock is held across the commit-message approval gate, which is an unbounded human pause. "Refresh on conversational re-entry" covers an agent that keeps working; it does not cover Craig stepping away for an hour mid-review. Size the staleness horizon for that, or refresh on a timer while the gate is open — sentry's TTL is nowhere near long enough.
+2. *Blocked-session behavior is unspecified.* The proposal says a second session cannot stage or commit through the guarded flow, but not what it should then do — wait on =--wait=, defer and report, or stop and surface. Pick one and test it.
+3. *Size.* This is a real build (key derivation, owner support in =agent-lock=, the fingerprint gate, the review-restart path, and the acceptance battery), not a wording change.
+
+Canonical touchpoints: =claude-rules/commits.md= (add the lock to the pre-commit flow; state explicitly that an approval waiver never waives the staged review, since the review is the gate that reads the hunks entering the commit), =claude-templates/.ai/scripts/agent-lock= (backward-compatible caller ownership — existing sentry and roam-write callers must stay green), and =claude-templates/.ai/scripts/tests/agent-lock.bats=.
+
+Non-goals from the approval: no per-session =GIT_INDEX_FILE=, no worktree requirement for live stow-managed dotfiles, no locking of ordinary edits, no replacement for remote pre-push reconciliation.
+
+Source: archsetup handoff 2026-07-26 10:55.
+
+** VERIFY [#B] Parked: reusable Claude-to-Codex MCP registry sync :spec:
+:PROPERTIES:
+:LAST_REVIEWED: 2026-07-25
+:END:
+Two independent 2026-07-25 handoffs (work and home) report the same successful local migration: expose Claude's global =~/.claude.json= MCP registrations to Codex without duplicating secrets. Stdio definitions launch through a mode-0700 =claude-mcp-for-codex SERVER_NAME= wrapper that reads the mode-0600 Claude config at process start; Streamable HTTP entries remain native Codex entries; the loopback-only legacy Slack SSE endpoint bridges through =mcp-remote=. Both senders verified initialize + tools/list, not merely =codex mcp list=.
+
+Recommendation: accept the need, but spec it before adding an installer. "Every project" is misleading because these are machine-global registries; the reusable unit is a host-level, idempotent reconciler. It must:
+
+1. Preserve Codex-only and plugin-provided entries and distinguish active global registrations from inactive marketplace templates and broken project-local definitions.
+2. Inventory and report without printing environment values, secret-bearing arguments, tokens, or auth material.
+3. Back up and update atomically; preserve secure file modes; handle malformed input, name collisions, upstream removals, missing dependencies, and repeated runs.
+4. Classify stdio, Streamable HTTP, and legacy SSE explicitly. Never register SSE as HTTP, and allow cleartext only for loopback or an equivalently trusted private endpoint.
+5. Health-check each locally executable server with initialize + tools/list and report only redacted names/counts. Verify both machines and document restart/new-session plus OAuth reauthentication behavior.
+6. Keep the Figma token finding separate: rotate it and move it out of command-line arguments without ever recording its value.
+
+The proposed Claude-memory migration auditor is related only by runtime portability and should be a separate task/spec. It should classify guidance, useful context, duplicates, stale/ephemeral material, and secret-bearing denylist entries; it must not bulk-copy generated memory files.
+
+No prepared diff: this is a shared configuration design decision, and the current local wrappers/configs remain machine-owned evidence rather than canonical source. Say "spec the MCP registry sync" to start the decisions walk.
+
+** VERIFY [#B] A SCAN FAILED source must not advance its sentinel (engine, not the plugin)
+:PROPERTIES:
+:LAST_REVIEWED: 2026-07-24
+:END:
+Promoted to top-level 2026-07-28 when its parent closed — it is a separate engine question and would have been buried under a DONE parent.
+
+Secondary finding from the 2026-07-24 handoff, flagged for your judgment because it's *engine* behavior in =triage-intake.org=, not the telegram plugin. work reported that after a SCAN FAILED, "the marker still advanced" — and telegram is =:ANCHOR: none=, so something advanced a cursor it shouldn't have. The compounding harm: a source that reports SCAN FAILED but advances its window means the next sweep believes it already covered that window, so the blind-sweep hole persists across sweeps rather than self-healing. Not touched .emacs.d-side; needs a look at the engine's per-source last-run/anchor advance.
+
+** TODO [#B] cj-remove-block still can delete the WRONG cj block :bug:
+:PROPERTIES:
+:LAST_REVIEWED: 2026-07-24
+:END:
+The 2026-07-24 range fix (17f5d48) closed the *over-deletion* case: a range spanning two blocks is now refused. It did NOT close the underlying class. The validator proves the range is a well-formed single block; it never proves it is the block that was *scanned*.
+
+An adversarial reviewer demonstrated this on the fixed code: passing the second block's range while intending the first deletes the second, exit 0, no warning. Drift by exactly one whole block still slips through — which is the failure the validator exists to prevent.
+
+Grading: Major severity (silent deletion of the wrong annotation in Craig's org files, no warning, success exit) x rare edge case (needs drift landing exactly on another well-formed block) = P3 = [#C]... graded up to [#B] because it is the *same* failure the fix was believed to have closed, so the current state carries false confidence.
+
+Fix direction needs a decision, which is why this is not :solo:. The range alone cannot identify the intended block, so the caller must assert something about content — a hash of the expected block, or the expected first body line, passed alongside the range and verified before deletion. That changes the CLI contract and the calling skill, so it is a design call.
+
+** TODO [#D] Atomic-write residuals in cj-remove-block :bug:
+:PROPERTIES:
+:LAST_REVIEWED: 2026-07-24
+:END:
+Three narrower gaps a reviewer found in the atomic write, all verified, none fixed in the 2026-07-24 repair round:
+
+1. =shutil.copymode= runs before =write_text=, and writing clears setuid/setgid — a 2755 file comes back 755.
+2. ACLs are not carried across, and copying the ACL mask into the group bits can *widen* group permission on a file carrying a named ACL entry.
+3. Hard links are broken the same way symlinks were: =os.replace= gives the path a new inode, so a second hard link keeps stale content. Introduced by the atomic write itself (17f5d48), which did not exist on main.
+4. No =fsync= before =os.replace=, so the "never a partial" guarantee holds against process failure but not a system crash. Two lines to close.
+
+Grading: Minor severity (each needs an unusual file mode, an ACL, a hard link, or a crash mid-write) x rare edge case = P4 = [#D].
+
+** TODO [#D] Test suites leak backup files into shared /tmp :test:
+:PROPERTIES:
+:LAST_REVIEWED: 2026-07-24
+:END:
+The elisp todo-cleanup suite writes roughly 81 backup files into the shared temp dir per =make test= run, named after randomized fixtures (=tc-test-XXXXXX.org.before-todo-cleanup.*=). 1598 had accumulated by 2026-07-24. Not data loss and not production-named, so nothing masquerades as a real backup and nothing is deleted — this is clutter that grows every run.
+
+The python sibling was fixed with an autouse fixture (f850ad6) that sets =TMPDIR= and overrides =tempfile.tempdir=. ERT has no autouse equivalent, so the elisp fix wants a load-time rebind of =temporary-file-directory= plus a =kill-emacs-hook= cleanup.
+
+Also unaddressed: =test-lint-org.el= scans a hardcoded =/tmp=, and =lint-org.el= hardcodes =/tmp/= in =lo--backup=, so that pair cannot be isolated the same way without touching the script. And =lint-org.el= carries the same second-resolution backup-overwrite flaw that todo-cleanup and cj-remove-block were fixed for.
+
+Grading: Minor severity (clutter in a directory cleared on reboot) x every user, every time (every suite run) = P2... graded [#D] on the no-harm read.
+
+** VERIFY [#B] Should rulesets run shellcheck on its own shell?
+:PROPERTIES:
+:LAST_REVIEWED: 2026-07-24
+:END:
+Surfaced by the lint-coverage finding above, and a bigger question than that task should decide.
+
+rulesets ships shellcheck enforcement to *other* projects — the bash bundle's =githooks/pre-commit= scans staged shell, and =validate-bash.sh= blocks an edit that fails it. rulesets runs neither on itself. Its own =githooks/pre-commit= calls =sync-check.sh= and nothing else; =lint.sh='s =check_hook= validates only shebang and exec bit; =make test= has no shellcheck step. So the repo asks consumers for a standard it does not apply to its own shell.
+
+Why this needs your call rather than an overnight fix: turning shellcheck on today would surface the findings I dispositioned as false positives during this session's sweeps — =SC2094= on =install-ai.sh= and =sweep-gitignore-tooling.sh= (append-with-stat, not a read-write race), =SC2088= on =doctor.sh= and =audit.sh= (tilde in a *display* string, not a path), =SC2164= on =lint.sh= and =status.sh=. Enabling the gate means either fixing those or adding =# shellcheck disable== directives with justifications, and which of those you want is a preference, not a fact.
+
+Options: (1) shellcheck in =make lint= as a warning, (2) in =githooks/pre-commit= as a hard gate matching what consumers get, (3) leave it, on the grounds that the repo's shell is small and reviewed. My lean is 2, since the asymmetry is the odd part — but it is your repo's bar to set.
+
+** TODO [#C] Attachment filenames from email are only partly sanitized :bug:
+:PROPERTIES:
+:LAST_REVIEWED: 2026-07-24
+:END:
+Both attachment writers derive on-disk filenames from attacker-controlled input (the =filename= an email declares for its attachment), and both sanitize incompletely — in *different* ways, so neither covers the other's gaps.
+
+=eml-view-and-extract-attachments.py= runs the name through =_clean_for_filename= but interpolates the *extension* raw: =name, ext = os.path.splitext(...)= then =f"{basename}-ATTACH-{clean_name}{ext}"=. =gmail-fetch-attachments.py='s =safe_filename= handles path separators and leading =..= and nothing else.
+
+Verified 2026-07-24 by probing both with the same adversarial set:
+
+| input | eml-view result | gmail-fetch result |
+|------------------------+------------------------------------+--------------------|
+| =report.pdf; rm -rf ~= | keeps =; rm -rf ~= in the filename | keeps it |
+| =x.p\ndf= | keeps a literal newline | keeps it |
+| =a.= + 300 chars | 314-char filename | unbounded |
+| =../../../etc/passwd= | neutralized | neutralized |
+
+What this is NOT, checked so it isn't over-graded later: not remote code execution (files are written through Python =open=, never a shell) and not path traversal (=os.path.splitext= only returns an extension when the last dot follows the last separator, so =ext= can never contain a slash — the traversal case above is neutralized in both). The real harms are narrower: a newline in a filename breaks any downstream tool that reads the output directory as a newline-delimited list, and an unbounded extension exceeds the 255-byte filesystem limit so a crafted attachment aborts the extraction.
+
+Grading: Major severity (untrusted input reaches a filesystem name unsanitized, and the newline case breaks real tooling today) x rare edge case (needs an attachment name with unusual characters *after* the last dot, which ordinary mail doesn't produce) = P3 = [#C].
+
+Fix direction: sanitize the extension with the same rules as the name, cap the *whole* filename rather than just the stem, and reject or replace control characters in both scripts. The open question is where the sanitizer lives — see the VERIFY below.
+
+** VERIFY [#B] One sanitizer or two for the attachment filename fix?
+:PROPERTIES:
+:LAST_REVIEWED: 2026-07-24
+:END:
+Deferred from pass 12 on 2026-07-24: the bug above is well-specified, but *where* the shared sanitizer lives is a design call with real tradeoffs, and the unattended loop has no one to ask.
+
+The two scripts are standalone stdlib-only CLI tools with kebab-case names, so neither is importable as a normal module. Three options, none obviously right:
+
+1. *A shared helper module* in =.ai/scripts/=. Cleanest single source of truth, but it becomes another synced template file, and the kebab-named callers need =importlib= gymnastics to reach it — the same trick =route_recommend.py= already uses to load =inbox-send.py=, so there is precedent.
+2. *Duplicate a small sanitizer in each script.* Zero import machinery and each tool stays self-contained, which is the current house style for these scripts. Costs drift, and sibling drift is exactly the defect class that produced three separate findings this session.
+3. *Fix only the demonstrated gaps in place* (extension sanitizing in one, length and control chars in both) without unifying. Smallest diff, leaves the two sanitizers permanently different.
+
+I lean 1 given how much drift has bitten tonight, but it changes the shape of =.ai/scripts/= and that is Craig's call, not an overnight one.
+
+** VERIFY [#B] Parked: question-capture pattern — ask async, answer live (from archsetup, Craig's idea)
+:PROPERTIES:
+:LAST_REVIEWED: 2026-07-24
+:END:
+What arrived: your own idea, relayed via archsetup's roam inbox. Drop a question (not a build task) into the roam inbox as a capture; the agent retrieves it during a sentry/inbox pass but does NOT auto-answer; it holds the question and answers it at the next live conversation, closing on your acknowledgement. Distinct from VERIFY: a VERIFY blocks the agent on your input, here the agent owes you the answer and you owe only the ack. The instance that spawned it: "why does the cursor not appear over the desktop when the world-clock wallpaper is on?", answered live in archsetup.
+
+Recommendation: adopt, as a small spec rather than a direct build. The value is real — a captured question has no home today (not a task, not quite a VERIFY), and decoupling async-capture from sync-answer stops the agent burning cycles guessing at something you'd rather discuss. It clears the value gate on your authorship. But the shape carries decisions I shouldn't guess, which is why it parks rather than lands:
+
+1. How is a question item identified — an explicit marker (a =:question:= filetag, a =Q:= prefix) or phrasing (ends in "?")? Phrasing is fuzzy (a real task can contain a question); a marker is reliable. My lean: explicit marker.
+2. Where does the "answer owed" state live between capture and the live session? Options: the roam item stays put with an answered-pending tag, or it routes to the owning project as an "answer owed" item that startup surfaces. My lean: surface at startup, since that's the next live moment.
+3. Does it warrant a new keyword (the inverse of VERIFY — agent-owes-answer) or a tagged VERIFY variant? The proposal draws the VERIFY distinction sharply enough that a distinct marker may be cleaner.
+
+Prepared artifact: none — this is a rule-design decision, not a mechanical edit, so there's no diff to stage. The proposal and the concrete instance are in [[file:working/question-capture-pattern/proposal-from-archsetup.org]].
+Say "spec the question-capture pattern" (answering 1-3, or leaving them to the spec's decisions walk) and it becomes a spec-create.
+
+** VERIFY [#A] Parked: account-binding guard for the personal-gmail triage plugin (from home)
+:PROPERTIES:
+:LAST_REVIEWED: 2026-07-23
+:END:
+What arrived: home hit a real cross-account hazard on 2026-07-23 — a sentry triage fire used =mcp__claude_ai_Gmail= (bound to the DeepSat work account, name gives no hint) instead of =google-docs-personal=, pulled 201 unread work messages, and would have run every hygiene action against the wrong inbox. The fix adds one guard block to the =triage-intake.personal-gmail.org= Scan phase: confirm a sample result's =to:= is =craigmartinjennings@gmail.com= before classifying, and on mismatch or unavailability fall back to the local mu mirror rather than another MCP.
+
+Recommendation: accept as-is. The diff is a single clean addition between the promotions-filter warning and the maxResults-cap warning — nothing else in the plugin changes. The bug is reproduced and dated, the account→tool mapping was verified by home 2026-07-23 (google-docs-personal=personal, google-docs-work + claude_ai_Gmail=work-bound), and the mu-mirror fallback is the right escape (it's account-fixed by maildir, not by an ambiguous MCP name). I agree with home's judgment that cmail needs no analogous note — it's a bridge script fixed to c@cjennings.net by construction, no tool-binding ambiguity.
+
+Graded [#A] on severity alone, per the todo-format security/privacy carve-out: acting on the wrong person's mailbox is a privacy exposure, and one occurrence with work mail misrouted through a personal-hygiene close is a showstopper regardless of frequency.
+
+Prepared diff: [[file:working/triage-account-guard/proposed.diff]] — apply is mechanical (home's file becomes canonical) on your go. Companion note: [[file:working/triage-account-guard/companion-note-from-home.org]].
+Say "approve the parked account guard" (or adjust / reject) and it gets applied.
+
+** VERIFY [#B] Parked: term-translation-density voice pattern #48 (from work)
+:PROPERTIES:
+:LAST_REVIEWED: 2026-07-23
+:END:
+What arrived: you proposed a new voice pattern in the work session — a sentence forcing the reader to translate more than one specialized term is too dense and gets rewritten. You supplied the rule text and a worked before/after (the ViT/VLM/detect-then-contextualize email sentence), and delegated number, placement, and mode-tag to me.
+
+Recommendation: accept, as #48, general mode. Your reasoning holds — it's a universal clarity rule in the Orwell/Plain English family, distinct from #7 (which words) and #30 (fragments), so it reads for anyone's prose, not just Craig's. The diff is prepared and verified: both files consistent, #47 intact, all mode enumerations and counts updated.
+
+The one wrinkle I resolved, worth a glance before you approve: general mode was defined as exactly "patterns #1-31," a contiguous block. A general pattern at #48 breaks that, so the prepared diff updates every "general walks #1-31" line to "#1-31 and #48" and notes the number is an artifact of when it was added, not a scope signal. The alternative was to scope it [prose · personal] to keep general clean, but that costs the pattern its reach over third-party prose, which is exactly where jargon density bites. I went with your general-mode call.
+
+Prepared diffs: [[file:working/voice-term-density/skill.diff]] and [[file:working/voice-term-density/profile.diff]] — apply is mechanical on your go.
+Say "approve the parked term-density pattern" (or adjust / reject) and it gets applied.
+
+** VERIFY [#B] Parked: auto-empty working/ when a task closes (from work)
+:PROPERTIES:
+:LAST_REVIEWED: 2026-07-23
+:END:
+What arrived: your roam capture relayed via work — every project carries working/ and temp/, and when a task completes its working/ files are automatically ("via a soft hook if possible") archived or moved to temp/. The sender flagged that most of this may already exist and asked for a check before treating it as net-new.
+
+Recommendation: accept the intent, reject the mechanism. Three of the four asks already shipped — working/ tracked from creation and temp/ gitignored (2026-07-20), and temp/ cleared at wrap (today, 10ea44b, from this same capture). Only the auto-empty is new, and I'd not build it as described.
+
+Two reasons. Moving a closed task's artifacts to temp/ destroys them, because temp/ is cleared at the next wrap, while working-files.md says those artifacts get renamed individually and filed flat into assets/ with meaningful names. And filing is deliberately a judgment step: it forces a review of each artifact's permanent value, which is what keeps assets/ searchable and catches the ones not worth keeping. Automating it removes exactly that review. Tonight is the live case — I closed four parked VERIFY tasks and filed their working/ dirs by hand, checking for inbound links and confirming the content stayed recoverable in git first. An automatic sweep would have destroyed four prepared diffs at the next wrap.
+
+The real gap is narrower: orphan detection exists only inside a sentry fire (pass 6), so a project that never runs sentry never gets the nudge. The prepared diff adds that detection to wrap-up as a report-only step, which serves the intent without the destruction. Verified against this repo's live tree.
+
+Prepared diff: [[file:working/working-dir-orphan-check/proposed.diff]] — apply is mechanical on your go.
+Say "approve the parked orphan check" (or adjust / reject) and it gets applied.
+
+** TODO [#B] No two language bundles compose any more — polyglot is now refused :feature:spec:
+:PROPERTIES:
+:LAST_REVIEWED: 2026-07-23
+:END:
+Completing the python and typescript bundles (the [#A] above, fixed 2026-07-23) had a consequence worth deciding on deliberately: all five bundles now ship =claude/settings.json= and =githooks/pre-commit=, so *every* pair collides and =install-lang= refuses the second without =FORCE=1=.
+
+This reopens a decision that was closed on a premise that turned out to be a bug. On 2026-07-17 the call was "polyglot: case-by-case, no option-2 machinery — bundles already compose; only coverage-makefile.txt collides, and it's a manual paste." Bundles appeared to compose only because python and typescript were missing the two files everything else collides on. The composition was the defect, not a design property.
+
+Live impact: =clock-panel= is a python + typescript project. It can install one bundle's hooks or the other's, not both. Today it has neither, so nothing regressed underneath it, but the polyglot path it would have wanted is now closed. =work= is python-only and unaffected.
+
+The real question is what a polyglot project should get. Both files are single-owner by construction: =settings.json= would need its =PostToolUse= arrays merged, and =pre-commit= would need each language's checks concatenated behind one secret scan (which is already identical across all five). Neither merge is hard; the reason it was never built is that nobody had a project that needed it. Now one does.
+
+Options, roughly: (1) build the merge — settings arrays union, pre-commit composes per-language phases; (2) split the shared half out, so the secret scan is one file every bundle sources and only the language phase is per-bundle; (3) leave =FORCE=1= as the polyglot answer and document that the last install wins; (4) do nothing, since only one project is affected. Option 2 is the one that would also have prevented the [#A] — a shared secret scan can't go missing from a bundle that never had its own copy.
+
+Not [#A] because nothing is currently broken: no project is running with a bundle it lost. Not [#D] because a live project wants it and the decision is now forced rather than hypothetical.
+
+Pinned by =scripts/tests/install-lang-collision.bats= so the trade can't drift back unnoticed.
+
+** TODO [#C] Two Signal-channel gaps: unbounded send, second account never received :bug:
+:PROPERTIES:
+:LAST_REVIEWED: 2026-07-23
+:END:
+Both found 2026-07-23 reading =claude-templates/bin/agent-text= and =scripts/signal-receive.sh=. Shellcheck is clean on all four =claude-templates/bin/= scripts; these are logic gaps, not style.
+
+1. *The direct send has no timeout.* =agent-text='s relay path passes =-o ConnectTimeout=10= to ssh, but the local-account path calls =signal-cli send= with no bound at all. A stalled network or a lock contended by the receive timer hangs the call indefinitely. That matters more than it looks: agent-text is invoked *by agents*, so an unbounded call blocks the calling turn with no output, and the caller can't distinguish a hang from a slow send. The receive path already takes ~16s of wall clock every cadence, so the two contending on the same account is not hypothetical. Fix: wrap the direct send in =timeout=, matching the relay's bound, and let the existing non-zero path print the desktop-fallback message.
+
+2. *ratio's second Signal account is never received.* =signal-cli listAccounts= on ratio warns "Messages have been last received 8 days ago". Established by elimination, not inference: ratio holds two accounts (=+15045173983= pager, =+15103169357= Craig's personal), =signal-receive.sh= hardcodes the pager as its default and takes no others, and the timer's journal shows it draining the pager cleanly every 15 minutes (last run 2 minutes before the warning appeared). So the 8-day-stale account is the personal one, and nothing on the machine keeps it warm.
+
+ Honest limit on item 2: the staleness is verified, the *harm* is not. That registration is described in the session history as note-to-self with no push, so it may be fine to leave cold, and Signal's own tolerance for a quiet linked account isn't something I established. Filed so the question gets answered rather than rediscovered. The script already accepts an account argument, so if it does matter the fix is a second timer instance rather than a code change — which makes it a dotfiles handoff (that repo owns the unit) with rulesets owning only the script.
+
+Grading: Minor severity for both (one is a latent hang in a tool with a documented fallback, the other a warning on an account nothing currently depends on) x most users, frequently (the stale warning is a standing condition on this host; the hang needs a stall) = P3 = [#C].
+
+** TODO Manual testing and validation
+*** Sentry — entry gates fire with Craig present
+What we're verifying: the interactive entry gates stop for the right states and start the loop only on a clean, green baseline.
+- On rulesets (ratio), with a clean tree and green suite, say "start sentry hourly".
+- Confirm it reads =:COMMIT_AUTONOMY: yes= and doesn't decline.
+#+begin_src sh :results output
+# Dirty the tree on purpose, then re-arm — the dirty-tree gate should stop and offer finish/stash/rollback.
+echo "# scratch" >> README.org
+git diff --stat
+#+end_src
+- Re-run "start sentry"; confirm it surfaces the dirty README and the three numbered options rather than starting.
+- Discard the scratch edit (=git checkout -- README.org=), re-arm, and confirm it reconciles, creates =sentry/$(date +%F)-$(uname -n)=, and arms the loop.
+Expected: sentry declines on the dirty tree with numbered options, and on a clean+green tree it creates the host-suffixed branch and starts the hourly loop.
+*** 2026-07-20 Mon @ 11:22:56 -0500 Sentry — one fire runs end to end, verified in live trial
+Verified across the 8-fire live trial on =sentry/2026-07-19-ratio= (ratio, 2026-07-20). Each fire wrote one digest block with a ran-or-skipped line per pass (no silent gaps), parked judgment/destructive items under the =* Sentry approval queue= heading with their exact commands (never executed unattended), committed writing passes to the branch, left the tracked tree clean, and freed the single-runner lock between fires. Fires 1-2 productive (2 handoffs processed + 1 parked, 3 design considerations filed); fires 3-8 clean idle. Digest lives in the session anchor.
+*** Sentry — wrap-up guard and stop-sentry
+What we're verifying: wrap-up refuses while sentry is live, and stop-sentry cleanly ends the loop.
+- With sentry live, say "wrap it up"; confirm it refuses with "sentry is active — say 'stop sentry' first."
+- Say "stop sentry"; confirm the loop cancels and it offers the branch disposition (merge now / leave named) and the approval-queue walk.
+Expected: wrap-up blocks on the held single-runner lock; stop-sentry cancels the loop and walks branch + queue disposition.
+*** Sentry — morning branch review
+What we're verifying: the morning teardown reviews and merges cleanly, nothing reached main unpushed.
+- =git log main..sentry/<date>-<host>= and read the diff.
+- Squash-merge what's wanted, delete the branch, revert any stale Emacs buffers.
+- Confirm main was never pushed to during the night and the approval queue was walked.
+Expected: the night's work is reviewable as one branch, merges by choice, and main stayed clean throughout.
+*** Silent-until-signal — empty fire heartbeats, real item full turn
+What we're verifying: an in-session monitor fire collapses to a one-line heartbeat when nothing changed, and still does the full turn on a real item. Covers sentry, auto triage-intake, and auto inbox-zero (spec: docs/specs/2026-07-20-silent-until-signal-monitors-spec.org).
+- Run a sentry fire with nothing pending. Expected: one line, =sentry at HH:MM: nothing=, no per-pass digest block, tree clean.
+- Run a sentry fire after planting an inbox handoff. Expected: the full per-pass digest block, the handoff processed or queued.
+- Start "auto triage-intake" and let an empty sweep run. Expected: one line, =triage intake at HH:MM: nothing=, no three-section output.
+- Plant a real change in a triage source, let the next sweep run. Expected: the full three-section output (deltas / unacked / timestamp).
+- Start "auto inbox zero" with an empty roam inbox, let a cycle run. Expected: one line, =inbox zero at HH:MM: nothing=.
+- Add a roam inbox item, let the next cycle run. Expected: the item summarized, filed, and queued.
+Expected: every empty fire is a single labelled heartbeat; every fire with real work does its full turn unchanged.
+*** Triage source activation — declared sources pulled, undeclared dormant
+What we're verifying: triage-intake pulls only the sources a project declares (spec: docs/specs/2026-07-20-triage-source-activation-spec.org).
+- In rulesets (no =:TRIAGE_SOURCES:=, no project plugin), run "triage intake". Expected: Phase 0 announces every general plugin as "inactive (undeclared)", loads zero sources, and the sweep no-ops.
+- In a project with =:TRIAGE_SOURCES: personal-gmail cmail=, run "triage intake". Expected: Phase 0 loads personal-gmail and cmail as active general sources, names the rest inactive, and scans only the two.
+- In a project with a project-specific plugin (=.ai/project-workflows/triage-intake.*.org=) and no declaration, run "triage intake". Expected: the project plugin is active by presence; general plugins stay inactive.
+- Under sentry, confirm pass 3 (triage) probe-skips in a project with no active source, and runs where one is declared.
+Expected: undeclared general plugins never scan; declared ones and project-specific ones do; sentry's triage pass activates only where a source is active.
+
+** TODO [#D] Document polyglot coverage-makefile namespacing :chore:
+The one manual step the polyglot decision (2026-07-20) left: when a project installs two bundles that both ship =coverage-makefile.txt= (elisp/go/python/typescript), their =coverage:= / =coverage-summary:= Makefile targets duplicate. Document the fix — rename to per-language =coverage-<lang>:= targets and add a =coverage:= aggregate that depends on them — where a polyglot user would meet it (install-lang docs, or a short note in the bundle README / languages/ overview). Low urgency: clock-panel is polyglot today and unaffected because it has no assembled Makefile. Do it when the coverage-makefile fragments are next actually pasted, or opportunistically.
+
+** TODO [#D] Cross-host agent coordination — a lock across ratio and velox :feature:
+Foundational primitive several deferred features wait on. =agent-lock= serializes agents on ONE host (its locks live on tmpfs, =$XDG_RUNTIME_DIR/agent-locks/=), so nothing coordinates the two daily drivers. When ratio and velox both act on shared state at once, there's no lock to stop them — the roam-write lock and sentry's single-runner lock are all host-local. Design a cross-machine lock (a lock node committed to a shared repo, a tailnet lock service, or a designated-primary election) that the host-local locks escalate to when the operation touches cross-machine-shared state (roam, a sibling-freshness push). Take up when cross-host contention proves real in practice, not speculatively.
+
+Blocks (the shared-prerequisite half of the sentry cluster):
+- [[file:todo.org::*Cross-host roam conflict surfacing][Cross-host roam conflict surfacing]].
+- [[file:todo.org::*Sentry vNext passes][Sentry vNext passes]] items #2 (system-health) and #3 (sibling-freshness) — both need ratio/velox not to run the pass simultaneously.
+
+** TODO [#D] Cross-host roam conflict surfacing :feature:
+Deferred from the sentry spec (Decision: host-local lock only). The
+roam-write lock serializes same-host agents; a true cross-host race still
+forks a sync-conflict file, surfaced only by roam-sync's rebase abort.
+Investigate making the conflict loud (roam-sync retry-once, or a
+cross-host lock node in the repo) when it proves frequent in practice.
+Shared prerequisite: [[file:todo.org::*Cross-host agent coordination][Cross-host agent coordination]] (the missing cross-machine lock).
+
+** TODO [#D] KB lesson-detection heuristic :spec:
+Deferred from the sentry spec review (2026-07-14): sentry's KB promotion pass was cut from v1 because "promote recent durable lessons" had no defined lesson source, and an unattended judgment pass writing to the shared KB needs one that doesn't spam or go silent. Design the detection heuristic — candidate source: Session Log entries and memory-dir changes since the last fire, judged against knowledge-base.md's inclusion bar — and define how precision gets verified before the pass ships in a sentry vNext. Blocks re-adding the pass.
+
+** TODO [#D] Unattended /schedule cron contract — no-session variant :feature:
+The design problem shared by two deferred features: a =/schedule= cron pass that fires with *no live session* needs its own contract, distinct from the interactive =/loop= shape. The open questions are the same for both consumers — mutation rights (read-only vs may-mutate =todo.org= / =~/org/roam/inbox.org=), how a find surfaces asynchronously when Craig isn't at the session, how dedup state persists across runs that don't share a session, and what session/auth context a cron run carries (the MCP-auth wall: a detached run can't reach Gmail/Slack/Linear, the same constraint the silent-until-signal spec turned on).
+
+Two consumers, one contract:
+- *Sentry* (deferred from the sentry spec Non-Goal): v1 is interactive, a live session Craig starts, /loop-driven. A cron variant firing with no session is out of v1 scope.
+- *Auto inbox zero* (vNext from the inbox-consolidation spec, Codex finding 1; [[id:a7fe2a10-dfa8-4ba3-a11a-e7b1288b7573][spec]]): v1 =auto inbox zero= is the interactive /loop check that waits for Craig's yes. Its "design after v1 consolidation lands" precondition cleared 2026-06-28 (the inbox engine consolidation 24ca58d and monitor-inbox loop edb545d shipped), so this is actionable backlog.
+
+Design the one contract; both features consume it. Merged 2026-07-20 from the separate "Sentry unattended /schedule variant" and "Fully-unattended scheduled inbox check" tasks — same no-session design problem.
+
+** TODO [#D] Research: MCP for device locations shared with you :feature:
+From .emacs.d (2026-07-20, rulesets-owned research). Is there a Google Maps MCP (or similar) that reports the locations of devices sharing their location with you? If none exists, research how hard it would be to build one. (Google's location-sharing has no official public API; likely needs investigation of unofficial routes or a different provider.)
+
+** TODO [#C] Sentry vNext passes — from live-trial design input :feature:spec:
+:PROPERTIES:
+:LAST_REVIEWED: 2026-07-25
+:END:
+Three considerations captured by .emacs.d's own hand-run sentry trial (routed via the shared roam inbox, 2026-07-20), for folding into the sentry workflow. Design input, needs deliberation — not applied to the shared workflow unattended.
+
+1. Bug/enhancement logging pass. Only where the project owns a codebase: file bugs by default, enhancements only if asked; review-and-accept/decline during the morning reconciliation. Live-tested — .emacs.d ran exactly this by hand and logged findings to its session anchor. Strongest candidate of the three; overlaps the deferred KB lesson-detection pass ([[file:todo.org::*KB lesson-detection heuristic][KB lesson-detection heuristic]]).
+2. System-health pass. Decide which checks are safe unattended, and how to keep ratio and velox from running it simultaneously (a cross-driver lock/coordination question — agent-lock is host-local tmpfs, so it doesn't coordinate across machines).
+3. Sibling-machine freshness pass. Keep the other daily driver (velox/ratio) up to date during sentry (see daily-drivers.md; same cross-driver coordination question as #2).
+
+Take up with the sentry Living Document refinements once the trial has quiet nights behind it. Passes #2 and #3 are blocked on [[file:todo.org::*Cross-host agent coordination][Cross-host agent coordination]] (the missing ratio↔velox lock). Related: [[file:todo.org::*Unattended /schedule cron contract][Unattended /schedule cron contract]] (the no-session variant).
+
+** TODO [#B] Extend ui-prototyping rule with build-to-prototype :feature:
:PROPERTIES:
-:CREATED: [2026-06-13 Sat]
-:LAST_REVIEWED: 2026-06-13
+:CREATED: [2026-07-11 Sat]
+:LAST_REVIEWED: 2026-07-25
:END:
-Optional wrap-up step that surfaces filed keepers belonging to another project, recommends a destination, and batch-moves them into that project's =todo.org= Open Work section (transcript filing deferred to vNext). All six decisions resolved (Reading B: the router acts on session-filed keepers, separate from the inbox gate and from defer-and-stage). Spec ready for review.
+.emacs.d proposal (2026-07-11, Craig-approved promotion), extending the ui-prototyping rule shipped tonight (=claude-rules/ui-prototyping.md=, 53f6ce6). The base rule covers research → prototypes → iterate → decisions-backed-by-a-prototype, but not the "build to the prototype" half. Add: after prototyping, fold what settled back into the spec and make the *build target* the prototype rather than the original spec text — the built feature should match the prototype, and any deviation is documented in a "Prototype & deviations" addendum section the build keeps current. Wire into four touch points: =brainstorm= Phase 3 (a UI design isn't "accepted" until it's been through the prototype loop; Next Steps say "build to the prototype"), =spec-create= (emit the deviations-addendum section for UI specs), =spec-response= (a UI spec decomposes into a prototype loop first, then build-to-prototype tasks), =start-work= (its verify phase drives the UI end-to-end; the bar becomes "matches the prototype," deviations logged). Worked example: the takuzu Emacs game — colored tiles read as all-black in the real terminal frame (dark faces + GUI-only box cursor), caught by a prototype loop on the first screenshot where build-to-spec would have shipped an unusable board. Multi-asset synced-rule change, so review-gated and needs a focused session; decide brainstorm-vs-lifecycle placement with Craig. Source: =inbox/PROCESSED-2026-07-11-0222-from-.emacs.d-ui-prototype-rule-proposal.org=.
-Spec: [[file:docs/design/wrapup-routing-spec.org]]. Source proposal: [[file:docs/design/2026-06-13-wrapup-inbox-transcript-routing-proposal.org]] (archsetup handoff 2026-06-13). Next: =spec-review=.
+** TODO [#C] Roam-only startup for .ai projects — investigate :spec:
+:PROPERTIES:
+:CREATED: [2026-07-11 Sat]
+:LAST_REVIEWED: 2026-07-25
+:END:
+Open question from the roam inbox (2026-07-11): could the startup sequence for .ai projects use org-roam as the single store instead of local files? Potential gain is near-guaranteed shared information across projects — lessons and proven techniques on a common thread, scannable across similar projects. It would need a way to isolate a project from the rest. Weigh the pros/cons, the risk, and whether it's worth it before any build. Exploratory, no commitment yet.
-** TODO [#B] Helper-instance support — concurrent same-project Claude :feature:spec:
+** TODO [#C] Keep WIP from blocking the template sync gate :feature:
+:PROPERTIES:
+:CREATED: [2026-07-11 Sat]
+:LAST_REVIEWED: 2026-07-25
+:END:
+From the roam inbox (2026-07-11): work in progress in one project shouldn't stop the sync gate. Idea: keep all diffs/changes in a =working/= directory and exclude it (and its subdirectories) from the sync gate. Many projects run at once, so their WIP files need to be grouped. Also add a per-project count of when the gate tripped, tracked as a metric to investigate. Distinct from the 2026-07-02 policy (untracked/gitignored changes already pass — this is about *tracked* WIP under =working/=). Verify how the gate detects dirtiness today before designing.
+
+** TODO [#C] KB orphan-node review pass :chore:
+:PROPERTIES:
+:CREATED: [2026-07-01 Wed]
+:LAST_REVIEWED: 2026-07-25
+:END:
+The 2026-07-01 kb-hygiene report listed 42 agent KB nodes with no inbound id: links (of 53 agent nodes; 0 conflicts, no duplicate titles). Orphan-ness alone isn't a defect — agent nodes are found by rg, not only by links — but a periodic pass is worth doing: prune nodes that aged out, merge near-duplicates, add id: links where clusters exist. Regenerate the list with the kb-hygiene script rather than trusting the snapshot. Propose deletions/merges to Craig before applying (auto-cleanup allowed only for :agent:-tagged nodes after approval, per knowledge-base.md).
+
+** TODO [#B] Helper-agent instance support — concurrent same-project Claude :feature:spec:
:PROPERTIES:
:CREATED: [2026-06-11 Thu]
-:LAST_REVIEWED: 2026-06-12
+:LAST_REVIEWED: 2026-07-25
:END:
SPEC REVIEWED 2026-06-12: [[file:docs/design/2026-05-28-generic-agent-runtime-spec-review.org][Codex review]] now rates Phase 1.5 =Ready with caveats=. Before any build, keep the Emacs integration as a cross-project handoff to =~/.emacs.d=, preserve the three-ring gate (bats → sandbox drills → pilot project), and do not let startup/helper changes reach synced template paths until the live drills pass.
@@ -64,111 +563,217 @@ Implement Phase 1.5 of the generic-agent-runtime spec ([[file:docs/design/2026-0
Independent of the spec's phases 2-6 (runtime-neutral refactor), which stay gated on their own go/no-go.
-** DOING [#C] Check that memories are sync'd across machines via git :spec:
+*** 2026-06-15 progress — detection + contract landed (inert); wiring is what's left
+RESUME NOTE. Before picking this back up as the big item, re-read what it is, does, and why first — don't dive straight into the next slice. The orientation, in one breath: this lets Craig open a second Claude in the *same project* while a primary session is running (a second terminal, to look something up or make a small task edit) without the two sessions clobbering each other's files. The whole risk it manages is two Edit-tool writers on one shared file (todo.org, notes.org) losing each other's writes, last-write-wins, silently. The design's answer: a singleton primary keeps the unisolated session-context.org; helpers get their own session-context.d/<id>.org, make only scoped single-heading edits to shared files, and never touch git. Full why-and-how in the spec ([[file:docs/design/2026-05-28-generic-agent-runtime-spec.org]], "Concurrent same-project agents" amendment) and the contract ([[file:.ai/workflows/helper-mode.org]]). A helper is NOT a subagent — subagents are for bounded dispatched lookups; this is for interactive parallel work Craig drives himself.
+
+Done so far (shipped, pushed, inert until wiring routes to them):
+- agent-roster (commit f8bdf30): the detection primitive. pgrep -x claude → /proc cwd → keep in-project → drop own ancestry. Exit 0 alone / 1 others / 2 unavailable. Injectable boundary (ROSTER_PGREP/PROC/SELF_PID), 11 bats, live-verified against 4 real sessions. [[file:.ai/scripts/agent-roster]].
+- helper-mode.org (commit 0b681dc): the canonical contract — read/write tiers, four data-integrity rules, light startup, helper wrap-up. Triggerless INDEX entry, protocols.org pointer.
+- Already shipped earlier: the AI_AGENT_ID + session-context.d/ split, and (2026-06-14) the epoch-on-the-tail id convention.
+
+What's next — the WIRING, all behind the spec's three-ring gate (bats → sandbox drills → live pilot), none of it sync'd to live template paths until the two-session drill passes:
+- Startup roster-detection branch: roster runs first; not-alone routes to helper-mode.org, alone keeps crashed-vs-fresh anchor logic. (Edits startup.org — synced, gated.)
+- wrap-it-up.org helper branch (archive own file, skip hygiene+commit; orphaned-helper lifts the git ban).
+- ai --helper launcher: roster → assign+export id → launch with helper opener. Plus the Emacs surface (ai-term.el) via an .emacs.d cross-project handoff.
+- Hygiene-pass live-helper gate: todo-cleanup.el / lint-org.el / wrap-org-table.el check session-context.d/ and pause+ask on live helper files.
+- todo-cleanup.el backup-to-/tmp backstop (lint-org and wrap-org-table already conform).
+- Manual validation drills with Craig (the live two-session test + the corruption drill).
+
+Stand up a drill rig before the gated work; build against it, don't touch synced paths until the live drill passes.
+
+DEPENDENCY QUESTION (Craig, 2026-06-15, resolved 2026-06-24 — see below): doesn't helper-instance support depend on generic agent runtime support? Starting point: the spec frames this work as Phase 1.5, "Independent of the spec's phases 2-6 (runtime-neutral refactor), which stay gated on their own go/no-go," and the body claims it sits only on the already-shipped session-context split. The separate =Generic agent runtime support — Codex spec v0= task (#C, below) is that phases-2-6 arc. So the spec's stated answer is "no, 1.5 is independent" — but confirm that's actually true for every wiring slice (does ai --helper, the roster branch, or helper-mode routing secretly assume any runtime-manifest / multi-runtime machinery from 2-6?), or whether helper-instance should be sequenced after, or merged into, the generic-runtime task.
+
+*** 2026-06-24 Wed @ 00:30:32 -0400 RESOLVED — independent, unblocked (keyword VERIFY → TODO)
+Craig's call (2026-06-24): helper-instance is independent of the generic-runtime refactor and builds on its own. The shipped pieces and the remaining wiring are all shared-file concurrency-safety (two Edit writers, one file), orthogonal to which LLM runtime launches — none of it assumes the runtime-manifest / multi-runtime machinery of phases 2-6. One caveat: =ai --helper= overlaps the launcher refactor the generic-runtime arc plans, so keep that launcher change small and contained so the later refactor doesn't fight it. Now a buildable [#B] task behind its own three-ring gate (bats → sandbox drills → live pilot).
+
+** TODO [#B] Wrap-up routing — manual end-to-end validation :test:
:PROPERTIES:
-:LAST_REVIEWED: 2026-06-12
+:LAST_REVIEWED: 2026-07-25
:END:
-v1 implemented end-to-end 2026-06-10 (Phases 0-4 below, no-approvals batch). Remaining before DONE: the manual testing and validation child, plus the other personal machines' one-time clone + timer setup (archsetup handoff).
-*** 2026-05-14 Thu @ 19:14:11 -0500 Investigate current memory storage
+What we're verifying: a real keeper routes through a live wrap and the destination actually files it. The task-routing build shipped IMPLEMENTED 2026-07-04 (spec [[id:00b47414-2213-4a99-be35-48ceb266fc08][wrapup-routing]]); this confirms it works end to end across a real cross-project wrap. A failed check promotes to a bug.
+- In a project session, let process-inbox file a handoff whose home is a different project; confirm the local task carries =:ROUTE_CANDIDATE: <dest>=.
+- Run wrap-it-up; at the router sub-step, confirm the candidate is surfaced with the right destination + confidence, then choose "go".
+- Confirm a =from-<thisproject>= handoff landed in the destination's =inbox/= and the keeper was removed from the local =todo.org=.
+- Open the destination project; confirm its startup/process-inbox files the handoff into its =todo.org= per its own conventions.
+Expected: the task ends up in the destination's =todo.org=, gone from the source, with no foreign =todo.org= written directly. Not =:solo:= — needs a real cross-project wrap and the destination's next session.
-Memory files live at
-[[file:/home/cjennings/.claude/projects/-home-cjennings-code-rulesets/memory/][~/.claude/projects/-home-cjennings-code-rulesets/memory/]]
-— four files including =MEMORY.md= and three individual entries
-(=feedback_never_guess.md=, =project_ai_scripts_canonical_source.md=,
-=reference_pdftools_venv.md=). The directory is a plain unmanaged dir
-(no symlink, no enclosing git checkout). Neither
-[[file:/home/cjennings/.claude/][~/.claude/]] itself nor any subtree
-containing the project-memory dirs is tracked in
-[[file:/home/cjennings/code/archsetup/][archsetup]] or
-[[file:/home/cjennings/code/rulesets/][rulesets]]. Without a symlink
-into a stowed or tracked location, memory files don't survive a new
-machine setup or a dotfiles restore.
+** TODO [#D] Wrap-up routing — transcript filing (vNext) :feature:no-sync:
+File a meeting recording into the destination =assets/= per =working-files.md=, batch go/skip mirroring the task router. Gated on the source-location decision (spec D4). Spec: [[id:00b47414-2213-4a99-be35-48ceb266fc08][wrapup-routing-spec.org]] (Phase 5).
-Proposed setup: stow =~/.claude/projects= →
-=archsetup/dotfiles/common/.claude/projects/= (path doesn't exist yet
-— it's the target location pending VERIFY).
-Create the destination in archsetup, move existing per-project
-=projects/<encoded-cwd>/memory/= dirs there, run =stow= to link, then
-commit + push archsetup. After that, every machine running =stow=
-picks up the same memory tree.
+** TODO [#C] Multiple agent-source improvements :spec:
+:PROPERTIES:
+:CREATED: [2026-06-23 Tue]
+:LAST_REVIEWED: 2026-07-13
+:END:
+Make the tooling agent-agnostic instead of Claude-specific. Three threads from Craig (roam 2026-06-23): (1) give the agent a name so workflows don't say "Claude" everywhere — a non-Claude agent (Codex) reading "you are Claude" gets confused; evaluate whether naming resolves the confusion or whether other spots also leak Claude-specificity. (2) Pull agent-neutral content out of Anthropic-specific files (=CLAUDE.md=) into a shared source that each agent's own entry file points to, so Codex (which runs more literal) reads the same rules; or link =CLAUDE.md= and the Codex equivalent to one source. Have Codex review the workflows for literal-reading wording gaps. (3) Send =.emacs.d= a note (inbox-send) to let =ai-term= launch Claude / Codex / a local ollama LLM, switchable seamlessly at session start. Spec-shaped — needs design before build. From the roam inbox 2026-06-23 (deferred from the 2026-06-21 session).
-*** 2026-05-23 Sat @ 16:12:48 -0500 Decided: dedicated private repo, not stow
-Worked through dotfiles → rulesets → dedicated repo. Dropped stow/dotfiles (machine config, wrong cadence) and rulesets (it's pulled first in every session, so memory edits would dirty its tree and skip the startup =git pull --ff-only=). Chose a dedicated private repo on cjennings.net: storage is unified there while recall stays per-project (the encoded-cwd subdirs), since pooling recall would hurt relevance and risk work-private facts surfacing in personal-project artifacts.
+*** 2026-06-24 Wed @ 00:21:20 -0400 Partial — agent-neutral wording sweep + thread-3 note landed
+Thread 2's wording half shipped in 6ad0442 (=refactor(rules): use agent-neutral language in shared rules=): agent-as-actor phrasing replaced with "the agent" across interaction.md, cross-project.md, triggers.md, working-files.md. Thread 3's note reached =.emacs.d=, whose 2026-06-23 inbox FYI confirms it received and filed the "multi-LLM support" ai-term handoff. Remaining and still TODO: thread 1 (give the agent a name), and thread 2's structural half (extract agent-neutral content into a shared source with a Codex entry-file pointer, then have Codex review the workflows for literal-reading gaps).
-*** 2026-05-23 Sat @ 16:12:48 -0500 Shipped: claude-memory.git + folded symlinks
-Created bare =git@cjennings.net:claude-memory.git=, cloned to =~/.claude-memory= (later deleted in the reversal below), moved all 7 per-project =memory/= dirs in (54 files; work has 40) and replaced each live =~/.claude/projects/<enc>/memory= with a folded dir-symlink so new memory lands in the clone and a push syncs it. Added =link-claude-memory.sh= (idempotent — recreates the symlinks on a new machine after clone) + README. Private repo, never GitHub (carries work/DeepSat memory). Initial import pushed (=f496370=).
+*** 2026-07-13 Mon @ 16:04:04 -0500 Thread 2's entry-file half landed via the runtime-portability build
+The Codex entry-file pointer now exists: =claude-templates/AGENTS.md= (thin pointer at protocols.org + rules + /name resolution), linked to =~/.codex/AGENTS.md= by =make install= and seeded per-project by =install-ai.sh= (see the generic-agent-runtime parent's 2026-07-13 children). Remaining here: thread 1 (agent naming — the entry file's "you are this project's agent" phrasing is a start, not the whole answer) and the Codex literal-reading review of workflows.
-*** 2026-05-24 Sun @ 01:53:35 -0500 Reversed the migration — back to unmanaged per-project memory
-Cancelled the follow-up brainstorm and undid the dedicated-repo migration at Craig's call. Moved all 7 memory dirs back to =~/.claude/projects/<enc>/memory/= (content preserved), deleted the =~/.claude-memory= clone, and deleted the bare =claude-memory.git= on the server. Memory is back to its original at-risk state, so the task reopens at [#C] pending a direction. The brainstorm landed on a two-tier idea for whenever this resumes: promote general lessons into a rulesets-tracked file symlinked into =~/.claude/rules/= (loaded into every project natively, one repo), and keep project-specific memory under each project's own =.ai/memory/= (committed where =.ai/= is tracked, at-risk where it's gitignored). Not implemented.
+** TODO [#C] Flashcard tooling improvements :feature:
+:PROPERTIES:
+:CREATED: [2026-06-28 Sun]
+:LAST_REVIEWED: 2026-07-13
+:END:
+Three flashcard-tooling tasks that all edit =flashcard-to-anki.py= and/or =flashcard-stats.py=, grouped so they get built together instead of colliding on the same files (prior sessions flagged the conflict risk). The Anki =#+TITLE= deck-name fix already landed (commit 060a938), so any preserved pre-fix script copy gets re-derived against the current canonical, never copied wholesale. The three children each ship independently.
-*** 2026-06-05 Fri @ 05:57:35 -0500 Pivot: adopt the existing org-roam KB as the shared agent substrate
-Pressure-tested the two-tier idea, then Craig redirected: a shared org-roam knowledge base any project can read and write makes this simpler. Ground truth verified: =~/sync/org/roam/= already exists (484 org files, curated since 2023, Syncthing-synced, not git). So cross-machine sync is already solved, and the task stops being "build a memory-sync system" and becomes "point agents at the KB that already syncs." The dedicated-repo and two-tier approaches are both superseded for the storage+sync half.
+*** 2026-07-19 Sun @ 18:40:36 -0500 Built apkg-to-orgdrill.py, the inverse converter
+Commit a143679. Reads an Anki =.apkg= (stdlib =zipfile= + =sqlite3=, no genanki) and emits org-drill =.org= in the house shape: deck → =#+TITLE=, note Front → =** heading :drill:=, Back HTML → body (=<br>=→newlines, entities unescaped amp-last, answer =<hr>= stripped), tag → =* section=, fresh =:ID:= per card. 17 tests incl. the round-trip through =flashcard-to-anki.py='s own parse(); verified end-to-end against a real genanki apkg. Grounded the schema by generating + inspecting a real apkg first. Speedrun task 1 of 2.
-Wrote a one-page spec: [[file:docs/agent-knowledge-base-spec.org][agent-knowledge-base-spec.org]] (originally docs/design/2026-06-05-org-roam-knowledge-base-spec.org; superseded by the 2026-06-10 spec-create rewrite at the new path). Five decisions, mechanics recommended: (1) KB is a queried substrate accessed as files (ripgrep + follow =[[id:]]= by grep), not via the org-roam package; (2) capture in harness memory, promote durable facts into the KB (same cadence as the pattern catalog) — resolves the at-risk problem since the valuable knowledge moves to the synced KB; (3) a =claude-rules/knowledge-base.md= pointer rule carries path/query/write-schema/boundary; (4) write schema = roam-valid node + =:agent:= filetag so agent notes stay distinguishable and index on the next =org-roam-db-sync=. The rules layer (=claude-rules/=, =CLAUDE.md=) is untouched — the KB replaces the memory tier, not the rules tier.
+*** TODO [#C] flashcard-stats refutation / claim-prompt mode :feature:
+:PROPERTIES:
+:CREATED: [2026-06-22 Mon]
+:LAST_REVIEWED: 2026-06-28
+:END:
+A refutation card (heading is a bare false claim, body is the rebuttal) is valid org-drill but trips two BLOCKING =flashcard-stats.py= checks as false positives: non-prompt-heading (a declarative claim has no =?= or imperative verb) and answer-leakage (claim words reappear in the rebuttal). =flashcard-sync='s gate then blocks the whole deck.
-*** 2026-06-10 Wed @ 14:29:20 -0500 Spec ratified — write boundary is option C; rewritten to spec-create format
-Craig answered via cj annotations in the spec (2026-06-10): DECISION 5 is option C (read-shared, write-scoped — work agents never write the KB). Syncthing does replicate ~/sync/ to a work machine and Craig is fine with how C handles it. Node granularity: per-fact nodes. Write review: agent writes land freely in the KB only — explicitly not permission to post to email, Linear, or any public channel without review and consent. The spec was rewritten into the spec-create format at [[file:docs/agent-knowledge-base-spec.org][agent-knowledge-base-spec.org]] (old draft removed). Implementation explicitly held pending Craig's go-ahead; one decision still open (D7, next VERIFY).
+Design (Craig, 2026-06-28, supersedes the two proposed options): make the exemption *generic*, not refutation-specific — more card kinds like this will come. When the org header declares the relevant info, the gate honors it rather than blocking. So a general file-level header keyword (a card-kind / check-exemption declaration) tells =flashcard-stats.py= which checks not to apply, instead of a hardcoded =#+DECK_KIND: refutation= keyword or a per-card =:claim:= tag. Document the mechanism in =flashcard-review.org= and add tests (a header-declared exemption file passes despite declarative headings + claim/answer overlap). Edits =flashcard-stats.py= — coordinate with the multi-tag reconcile, same file. Proposal: [[file:docs/design/2026-06-21-flashcard-stats-refutation-proposal.org][proposal]] (its two-option fix is superseded by this generic header approach). Backlog. From home 2026-06-21.
-*** 2026-06-10 Wed @ 14:35:40 -0500 Spec review — not ready
-Review written at docs/agent-knowledge-base-spec-review.org (deleted on disposition completion; content summarized in the spec's Review dispositions). Rubric: =Not ready=. Blockers: resolve D7 (keep vs retire harness memory) and define the executable personal/work/unknown write-boundary classifier plus work-side write/refusal destination. Medium notes: use concrete ripgrep commands that exclude =*.sync-conflict-*= files, and define seed-node approval/rollback.
+*** 2026-07-19 Sun @ 18:41:00 -0500 Reconciled the flashcard multi-tag tooling into canonical
+Commit a14e43b. Broadened =CARD_RE= in both =flashcard-to-anki.py= and =flashcard-stats.py= so a heading is a card when =drill= is among its tag block (=:fundamental:drill:=), bounded card bodies by any L1/L2 heading (=HEADING_RE=), and added =--tag-filter= (subset emit) + =--guid-salt= (distinct GUID space) to to-anki, plus a drill-membership guard to stats. Re-derived against the current canonical rather than copying the preserved 2026-06-17 files, which predated the =#+TITLE= fix (060a938) and would have reverted it. parse()'s 3rd element is now the Anki-tag list; existing parse tests moved to that contract, new tests cover multi-tag / tag-filter / guid-salt / stats count, and a =flashcard-sync.bats= case guards the count. The real 465/100 check needs the work deck; verified end-to-end on a synthetic deck (2 cards → 1 with =--tag-filter fundamental=) through real genanki. Speedrun task 2 of 2.
-*** 2026-06-10 Wed @ 14:44:00 -0500 D7 resolved — keep harness memory as the capture layer
-Craig ratified "keep" in chat (2026-06-10). Harness memory stays the ephemeral, auto-recalled capture layer; the KB holds promoted durable facts; Phase 3's wrap-up promotion cadence is mandatory. Spec D7 flipped to accepted; D2 stands as written.
+** TODO [#C] Agent-KB / memory-sync — work + unknown-project write refusal :test:
+:PROPERTIES:
+:LAST_REVIEWED: 2026-07-13
+:END:
+Residual manual validation from the memory-sync task (closed 2026-07-04, implementation IMPLEMENTED). Two checks need live sessions in other project contexts:
+- In the work project, a durable-storage request produces no KB write and the refusal report names the fact.
+- In an unknown project (outside =~/code/=, =~/projects/=, =~/.emacs.d=), the agent refuses or asks rather than guessing.
+Expected: both refusal checks behave per the spec; any miss promotes to a bug. Not =:solo:= — needs sessions in the work and an unknown project.
-*** 2026-06-10 Wed @ 14:44:00 -0500 Project classification defined — work-root denylist, unknown refuses
-Resolved in the spec-response pass: =knowledge-base.md= carries an explicit work-root denylist (initially =~/projects/work=) as the source of truth. Personal = under a known project parent (=~/code/=, =~/projects/=, =~/.emacs.d=) and not denylisted → KB writes allowed. Work or unknown → no KB write; the agent reports the refusal with a one-line redacted summary of the fact. v1 adds no new work-side store — work projects keep their existing project-tree conventions. See the "Project classification and write routing" section of [[file:docs/agent-knowledge-base-spec.org][the spec]]. Denylist completeness is the one open caveat (next VERIFY).
+** TODO [#C] Token-rotation helper for =@a-bonus/google-docs-mcp= OAuth refresh :feature:quick:
+:PROPERTIES:
+:LAST_REVIEWED: 2026-07-13
+:END:
-*** 2026-06-10 Wed @ 14:44:00 -0500 Codex review incorporated — spec ready with caveats
-Spec-response pass processed the 2026-06-10 Codex review with D7 = keep as a pre-agreed input. Both blockers cleared (D7 accepted; classification/write-routing section added). Mediums accepted: canonical rg commands with conflict-file exclusion, Phase 2 seed-node approval/rollback mechanics, Makefile no-change note, Testing/Verification section. Three recommendations modified, none rejected — see the spec's Review dispositions. Review file deleted per the workflow. Rubric: ready with caveats (denylist confirmation). Implementation tasks broken out below; implementation itself awaits Craig's go.
+When a Google refresh token gets revoked (re-grant scopes, removed Connected App, account password reset), recovery is currently manual: run =npx -y @a-bonus/google-docs-mcp= with the right env, follow the URL in a browser, kill the process, base64-encode the new =token.json=, decrypt =secrets.env.gpg=, replace the var, re-encrypt. A small =mcp/refresh-google-docs-token.sh <profile>= would chain that into one command.
-*** 2026-06-10 Wed @ 17:29:37 -0500 Work-root denylist confirmed — ~/projects/work only
-Craig confirmed (2026-06-10, in chat): the denylist is just =~/projects/work=. Archangel is not work-scoped. The spec's one caveat clears — status now ready. Phase 1 is unblocked, but implementation still awaits Craig's explicit go.
+*** Sketch
-*** 2026-06-10 Wed @ 17:57:08 -0500 Spec amended — D8 git transport + migration/metrics/docs/maintenance folds
-Craig's five design questions answered and folded into the spec, and D8 ratified (Shape A): the KB moves out of the =~/sync/org= Syncthing share into its own git repo on cjennings.net, with an =agents/= subdirectory for agent writes, a systemd auto-sync timer for Craig's edits, opt-in-by-clone replication (work machine doesn't clone), and the phone staying on the on-demand =~/sync/phone= pattern. Folded in: inclusion criteria + a Phase 1.5 guided memory sweep, a Success metrics section with a 30-day checkpoint, the seed node redefined as the KB's own documentation, and Phase 4 maintenance automation. Phases renumbered 0-4; tasks below updated. Implementation still held.
+#+begin_src bash
+# usage: mcp/refresh-google-docs-token.sh personal
+profile="$1"
+gpg -d ... | grep -v "GOOGLE_DOCS_${profile^^}_TOKEN_B64" > /tmp/secrets.env.tmp
+GOOGLE_MCP_PROFILE="$profile" npx -y @a-bonus/google-docs-mcp &
+xdg-open <captured-url>
+# wait for ~/.config/google-docs-mcp/$profile/token.json to land
+kill %1
+echo "GOOGLE_DOCS_${profile^^}_TOKEN_B64=$(base64 -w0 ~/.config/google-docs-mcp/$profile/token.json)" >> /tmp/secrets.env.tmp
+gpg -c --cipher-algo AES256 -o mcp/secrets.env.gpg.new /tmp/secrets.env.tmp
+mv mcp/secrets.env.gpg.new mcp/secrets.env.gpg
+rm /tmp/secrets.env.tmp
+#+end_src
-*** 2026-06-10 Wed @ 18:21:33 -0500 Phase 0 done — roam migrated to git
-Backed up (~/roam-backup-2026-06-10.tar.gz), copied to =~/org/roam=, 63 conflict files deleted (424 org files), git repo with origin =git@cjennings.net:roam.git= (initial commit 515693d), old location replaced with a transition symlink. Emacs =roam-dir= updated in user-constants.el + live-reloaded (db rebuilt, 416 nodes); handoff to .emacs.d for the commit. =roam-sync.sh= (6 bats green) on a 15-min systemd user timer, installed + enabled + round-trip verified. Old-path references repointed (protocols task-list pointer, journal workflow, notes template). archsetup handoff covers dotfiles adoption + other-machine clones. rulesets commit fcf554a.
+The flow tonight worked but took a handful of manual steps. One script collapses it.
-*** 2026-06-10 Wed @ 18:21:33 -0500 Phase 1 done — knowledge-base.md rule live
-=claude-rules/knowledge-base.md= written (path, git discipline, query commands, agents/ write schema, denylist + refusal contract, inclusion criteria, capture-then-promote). =make install= linked it machine-wide; verified the link, a known-note query, and conflict-glob exclusion with a planted file. Commit d071f1f.
+Decision (Craig, 2026-05-31): *hold until a token rotation is imminent.* The OAuth re-grant is a browser step that can't be triggered without revoking a live token, so the script can't be verified in isolation. Not marked =:solo:= — when a token actually needs rotating, write and verify in one pass (solo at that point).
-*** 2026-06-10 Wed @ 18:21:33 -0500 Phase 1.5 done — rulesets swept, 10 projects broadcast
-Rulesets' 6 memories classified: 3 promoted as =agents/= nodes (notify-attention pattern, pdftools venv, gpg-agent SSH TTL trap), 2 kept local (rule-encoded in verification.md / interaction.md), 1 kept + de-staled (ai-scripts-canonical updated for the claude-templates subtree fold). Sweep handoff broadcast to the 10 other memory-bearing projects (archsetup, org-drill, pearl, .emacs.d, elibrary, finances, health, home, jr-estate, kit); work skipped by the boundary; the orphaned =linear-emacs= memory dir (project retired, likely pearl's predecessor) noted for Craig.
+** TODO [#D] Generic agent runtime support — Codex spec v0 :spec:design:
+:PROPERTIES:
+:LAST_REVIEWED: 2026-06-28
+:END:
+Codex drafted a v0 design doc for making rulesets runtime-neutral rather than Claude-Code-specific. Motivating cases: offline operation with a local LLM, and two LLMs running in the same project at the same time without trampling each other's session-context.
-*** 2026-06-10 Wed @ 18:21:33 -0500 Phase 2 done — seed/doc node written and indexed
-=agents/20260610181640-how-the-agent-knowledge-base-works.org= written: the KB's user-facing guide (what agents do, how it syncs, finding/pruning agent content, the rule pointer). Index verified programmatically: =org-roam-node-from-title-or-alias= resolves it with tags (agent reference); node count 416 → 420. Craig's visual check remains in the manual-testing child.
+Spec at [[file:docs/design/2026-05-28-generic-agent-runtime-spec.org]] (moved here from inbox on intake).
-*** 2026-06-10 Wed @ 18:21:33 -0500 Phase 3 done — wrap-up promotes + records the KB receipt
-wrap-it-up.org Step 1 gains the promotion check (inclusion-criteria bar) and the mandatory "KB: promoted N / consulted yes-no" Summary line; validation checklist enforces it. Mirror synced, integrity OK (44), parse OK. Commit 242b95e.
+Immediate correctness issue Codex flagged: the singleton .ai/session-context.org is unsafe under simultaneous agents. Codex recommends starting with Phase 1 only — add AI_AGENT_ID + session-context.d/<id>.org without renaming the rest.
-*** 2026-06-11 Thu @ 19:26:26 -0500 .emacs.d memory sweep complete (first broadcast response)
-First of the 10 broadcast projects to report Phase 1.5 done (handoff 18:23). Inventory 7: promoted 3 to KB (no-make-frame-in-live-daemon, proton-bridge-headless-cert-mismatch, open-images-with-imv — roam commit a915760), kept 3 local at Craig's call (commit-flow-no-approval-gate per-project-scoped; two theme-scoped ones possibly superseded by the palette-columns spec), deleted 1 (superseded by canonical interaction.md rule). 9 projects' sweeps outstanding.
+Broader refactor proposes runtimes/ adapter manifests, generic install commands, language-bundle split (common/ + runtimes/<runtime>/), launcher refactor, local model service via llama.cpp/ollama. Big surface area, six phases.
-*** 2026-06-12 Fri @ 02:25:12 -0500 Five more sweeps complete via the home folds
-Overnight handoffs from home closed five more broadcast targets, each swept at fold-time triage with Craig's approval: jr-estate 2 promoted (forms name-with-number, PDF-editing tooling split; roam 45d8e6c) / 3 kept with area attribution / 2 deleted as rule-encoded or duplicate; finances 0/1/0 (rosalea-daly contact fact kept local); elibrary 0/0/2, health 0/0/1, kit 1/0/2 (hand-prep-items-to-work-inbox promoted into home's memory; the rest duplicated rules or home memories). Nothing from these five met the KB bar that wasn't already encoded. All folded projects' session archives merged area-prefixed into home's .ai/sessions/, so session-harvest's first run sees them. Home covers its own and remaining areas' sweeps through ongoing discipline; still pending from the broadcast: archsetup and work.
+2026-06-12 spec review complete: [[file:docs/design/2026-05-28-generic-agent-runtime-spec-review.org][Codex review]] rubric for the whole spec is =Not ready=. Phase 1 is already shipped, and Phase 1.5 is tracked separately as the helper-instance task. Before any phases 2-5 implementation, decide whether to commit to the larger arc and answer the blocker decisions: generic instruction-file strategy, default local runtime/server, first supported local editing CLI, adapter scope, and compatibility behavior for existing =CLAUDE.md= / =.claude/= projects.
-*** TODO Agent KB — manual testing and validation :test:
-What we're verifying: the v1 acceptance surface that needs Craig's eyes or a live cross-project session. Run after Phases 0-2 land.
-- Seed node appears in org-roam (autosync) and in the =rg '#\+filetags:.*:agent:'= inventory.
-- In the work project, a durable-storage request produces no write in the KB and the refusal report names the fact.
-- In an unknown project (outside =~/code/=, =~/projects/=, =~/.emacs.d=), the agent refuses or asks rather than guessing.
-- After Phase 0: an edit made on one machine appears on another within the auto-sync timer interval, no new sync-conflict files appear, and the work machine has no KB clone.
-Expected: all four behave per the spec; any miss promotes to a bug task. (Agent-runnable checks — make install link, rg finds a known note, conflict-file exclusion — are verified inside Phases 0-2.)
+*** 2026-06-10 Wed @ 14:13:55 -0500 Noted Phase 1 already shipped; narrowed scope to the phases 2-6 decision
+Phase 1 (the correctness fix) is live: protocols.org documents the AI_AGENT_ID-scoped session-context path (=.ai/session-context.d/<id>.org=) and =.ai/scripts/session-context-path= resolves it. The singleton race Codex flagged is closed. What remains is the spec review plus a go/no-go on the broader runtime-neutral refactor: runtimes/ adapter manifests, generic install commands, language-bundle split, launcher refactor, local model service.
+
+*** 2026-06-11 Thu @ 19:26:26 -0500 Spec amended with the helper-instance slice; implementation split out
+Craig's motivating case (a second Claude in the same project for lookups and safe task updates) was under-specified in v0 — it had identity and message targeting but no spawn mechanics and no write-safety contract for the shared files the session-context split doesn't isolate. Added the "Concurrent same-project agents (helper instances)" section (subagent boundary, identity/spawn via =ai --helper=, the tiered read/write contract, light startup, helper wrap-up) and Phase 1.5 to the migration plan. Implementation filed as its own [#B] task ("Helper-instance support"); this task stays scoped to the phases 2-6 go/no-go.
+
+*** 2026-06-12 Fri @ 02:09:10 -0500 Independent spec review complete
+Codex ran the spec-review workflow. Outcome: the combined spec is =Not ready= because phases 2-5 still require product decisions and current external-runtime/model verification. Phase 1.5 can proceed only as the already-split helper task, with rollout/manual-validation caveats accepted and no accidental template-wide release before sandbox/pilot drills pass. Review file: [[file:docs/design/2026-05-28-generic-agent-runtime-spec-review.org]].
+
+*** 2026-06-12 Fri @ 02:39:38 -0500 Second review after response pass
+Codex re-ran spec-review after the dispositions were folded in. Outcome by arc: Phase 1.5 helper instances =Ready with caveats=; phases 2-5 remain =Not ready= behind the explicit decisions/reverification gate. No new blocking findings for the helper slice. Review file updated in place: [[file:docs/design/2026-05-28-generic-agent-runtime-spec-review.org]].
+
+*** 2026-07-13 Mon @ 13:26:57 -0500 Gap assessment decomposed into child tasks
+Craig asked what's left to run ChatGPT or a local LLM as the agent. Assessment: the =.ai/= layer (protocols, workflows, scripts, inbox, todo, session anchors) is already runtime-neutral — plain org + bash, and a Codex session has run in it (2026-06-13). The Claude-bound remainder decomposed into the child tasks below; each overlapping spec-blocker decision is named in its body. The phases 2-5 go/no-go above still gates any big build, but several children are useful standalone.
+
+*** 2026-07-13 Mon @ 16:04:04 -0500 Instruction bootstrap built — thin-pointer AGENTS.md, both install paths
+Craig picked the thin-pointer shape (Decisions: option 1, definitively). Shipped TDD (bats red → green): canonical =claude-templates/AGENTS.md= (you-are-this-project's-agent + read protocols.org + rules locations + the /name resolution rule from the skill-parity finding + degrade-per-fallback, never skip gates); =make install= links it to =~/.codex/AGENTS.md= (new CODEX_DIR stanza, house skip/WARN/link idiom, covered by NEW =scripts/tests/install-agents-entry.bats=, 3 tests); =install-ai.sh= seeds a project-owned copy at bootstrap, never overwriting (+2 tests in install-ai.bats); rulesets root gets a tracked symlink as dogfood. Resolves the spec's "generic instruction-file strategy" blocker. velox picks up the global link automatically — startup Phase A.0 runs =make install= every session. Existing projects get seeded on demand (no auto-sweep in v1).
+
+*** 2026-07-13 Mon @ 14:52:38 -0500 Skill parity resolved — one resolution rule, no per-skill matrix
+Analysis in [[file:docs/design/2026-07-13-runtime-portability-inventories.org]] (Skill and command parity section). All 29 artifacts (11 skills + 18 commands) are markdown bodies; a single resolution sentence in the bootstrap entry file ("a /name reference resolves to the skill/command file — read and follow it") makes the library portable to any file-reading harness. Auto-invocation degrades to by-name invocation (the publish flow already invokes by name), native slash registration is an optional install nicety, flush is explicitly excluded (session-plumbing child owns it). Folds into the instruction-bootstrap child's build.
+
+*** 2026-07-13 Mon @ 13:34:17 -0500 Hook parity inventoried — only two hooks carry real porting work
+Full mapping in [[file:docs/design/2026-07-13-runtime-portability-inventories.org]]. AskUserQuestion deny is moot off Claude (no popup tool to deny); PostToolUse validators survive via the bundles' git pre-commit hooks; clear-resume folds into the session-plumbing child; session-title is cosmetic. Real gaps: PreCompact priority-save (prose downgrade) and Stop wrap-teardown (Codex notify / manual elsewhere) — decisions in the VERIFY below.
+
+*** 2026-07-13 Mon @ 23:21:08 -0500 Interactive agent picker — bare =ai= asks which brain first
+Craig's ask: the fzf flow should offer the LLMs before the projects. Bare =ai= now runs a runtime picker (claude first so Enter-Enter keeps the old muscle memory; codex labeled ChatGPT; one =local:<model>= line per ollama model, live-queried with a 3s timeout — a dead server just drops the local lines), then the familiar annotated project multi-select. =--runtime= / =AI_RUNTIME= skip the pick; single-dir mode unchanged. New =--print-runtimes= test seam; 9/9 launcher bats, suite 441/0. This substantially delivers the "easy lightweight way to change agents / agentically democratic" roam ask — the launcher-hardening task keeps the deeper refactor.
+
+*** 2026-07-13 Mon @ 23:04:21 -0500 Local runtime wired — ai --runtime local runs codex --oss over ollama
+The reserved lane went live the same day: =local= maps to =codex --oss --local-provider=ollama -m $AI_LOCAL_MODEL= (default gpt-oss:120b; qwen3-coder:30b via the env var). Verified end to end: =codex exec --oss --local-provider=ollama -m gpt-oss:120b= returned a correct completion through the local model (~7K tokens). The explicit provider flag avoids config.toml dependence (a root-level =oss_provider= key would also work; appending to the file lands in the last TOML table and silently does nothing — learned live). Dependency check keys on AGENT_BIN (codex) now that AGENT_CMD carries flags. This partially answers the spec's "first supported local editing CLI" blocker: codex-over-ollama is the de-facto v1. The model-floor child's live trial (a takuzu session under =ai --runtime local=) is the next step.
-*** 2026-06-10 Wed @ 18:21:33 -0500 Phase 4 done — monthly hygiene automation live
-=scripts/kb-hygiene.sh= (6 bats green, shellcheck clean, read-only by design) inventories =:agent:= nodes, flags orphans / duplicate titles / conflict files, and writes an org report into the rulesets inbox; =roam-hygiene.timer= (monthly, Persistent) installed + enabled. Live run against the real KB verified (4 agent nodes, 428 files, 0 conflicts). Conditional vNext stays in the spec's scope tiers: a =/promote= command if the wrap-up prompt proves insufficient, an =:agent:inbox:= staging tag if free writes prove too noisy. Commit b014095.
+*** 2026-07-13 Mon @ 16:39:41 -0500 Launcher runtime flag built — ai --runtime claude|codex
+Shipped TDD (new =scripts/tests/ai-launcher-runtime.bats=, 6 tests red→green): =--runtime= flag + =AI_RUNTIME= env on =bin/ai=, runtime→CLI mapping (claude default, codex 1:1 — both take the opening line as a positional prompt), runtime-aware dependency check, =local= reserved with a clear not-wired-yet error (pending the model-floor evaluation), and a =--print-launch= mode as the test seam (prints the pane launch command, no tmux/fzf). Kept small per the 2026-06-24 helper-task caveat — dispatch pre-parse only, no launcher restructure. The Emacs-side equivalent (ai-term multi-LLM) remains .emacs.d's June handoff. Live smoke: correct commands for both runtimes against real projects.
-** TODO [#C] Morning ops orchestrator pilot — read-only :feature:
+*** 2026-07-13 Mon @ 17:05:00 -0500 Roam capture folded — the agent-switching ask
+Two roam items (2026-07-13) asked for lightweight agent selection (claude, qwen, chatgpt, ollama — "agentically democratic"). Partly shipped the same day: =--runtime claude|codex= (04c3b29; codex is the ChatGPT-side CLI). The ollama/qwen choice is the reserved =local= runtime, landing with the model-floor eval below. An interactive runtime picker rides the new launcher-hardening task ([#C] :refactor:solo:, filed from the same capture).
+
+*** 2026-07-13 Mon @ 16:41:14 -0500 Session plumbing assessed — no build needed
+Analysis in [[file:docs/design/2026-07-13-runtime-portability-inventories.org]] (Session plumbing section). session-context-path is runtime-aware by design; the anchor cycle is plain files any agent drives identically; self-inject.sh is harness-agnostic (only its PAYLOAD is Claude-shaped — a codex auto-flush is a payload variant whose second injected line carries the resume instruction itself, no hook needed); session-clear-resume.sh stays Claude-only and the payload variant obsoletes it off Claude. Wire the codex variant the day a codex session wants it.
+
+*** 2026-07-13 Mon @ 13:34:17 -0500 Memory story confirmed — KB is already the cross-agent store
+Details in [[file:docs/design/2026-07-13-runtime-portability-inventories.org]]. Auto-memory stays Claude-owned; non-Claude agents use the org-roam KB + file artifacts (todo/notes/session anchors), all runtime-neutral already. One wording gap: knowledge-base.md's capture-then-promote names harness memory as the capture layer — a one-sentence addition (session log as the capture layer for agents without harness memory) closes it, pending approval in the VERIFY below.
+
+*** 2026-07-13 Mon @ 13:34:17 -0500 MCP portability checked — portable except paging
+Details in [[file:docs/design/2026-07-13-runtime-portability-inventories.org]]. Nine locally-configured servers (linear, notion, figma, slack-deepsat, google-calendar, google-docs x2, drawio, google-keep) port mechanically to any MCP-speaking harness. claude.ai-managed connectors (Gmail + claude.ai Calendar/Drive) don't travel but are redundant with local servers / cmail-action. The real finding: signal-mcp is NOT locally configured anywhere — the protocols.org paging path is claude.ai-side only, so off Claude there is no page channel. The Signal-pager [#C] task's signal-cli runbook is the fix; add runtime-portability as motivation there (VERIFY below).
+
+*** 2026-07-13 Mon @ 14:40:00 -0500 Four inventory decisions — Craig approved all four
+Recorded in [[file:docs/design/2026-07-13-runtime-portability-inventories.org][the inventories doc]]: prose-only PreCompact downgrade off Claude; Stop-teardown via Codex notify where available, manual elsewhere; runtime-portability note added to the Signal-pager task; capture-layer sentence added to knowledge-base.md (non-Claude runtimes capture into the session log and promote from there).
+
+*** TODO Local model floor evaluation
+The workflows assume long-context instruction-following (startup's multi-file read; the commits.md publish chain). Establish the minimum viable local tier (likely strong-70B+/MoE, 100k+ context), and what compensations a weaker model needs: shortened protocols, more checklist gates, more hook-level enforcement. Feeds the spec's "default local runtime/server" and "first supported local editing CLI" blocker decisions.
+
+Environment inventory done 2026-07-13 (KB node "Local LLM inference inventory — daily drivers"): ratio is the inference box — Strix Halo iGPU + 125 GiB unified RAM. velox is out of scope (Iris Xe, 60 GiB, no ollama).
+
+Environment BUILT and verified, evening of 2026-07-13: ollama 0.31.2 as a systemd service, Vulkan/RADV backend forced via systemd override (ROCm loaded a 61 GB model at <25 MB/s — 40+ min, unusable; Vulkan loads it in 44 s), models swapped to the MoE shape this platform wants: gpt-oss:120b (44 s load, 37.9 tok/s, 100% GPU) and qwen3-coder:30b (5 s load, 76 tok/s). All four legacy dense models dropped; the orphaned ~/.ollama user store purged. Gotcha for the eval harness: pass num_ctx 8-32K — gpt-oss's native 128K default balloons the load. Remaining work is now purely the evaluation: drive a scripted startup read + a publish-flow transcript through gpt-oss:120b (and qwen3-coder as the fast comparator), grade instruction-following against the protocol stack, and answer the spec's default-local-runtime + first-supported-CLI blockers.
+
+** TODO [#C] Docs-lifecycle convention — manual validation :test:
:PROPERTIES:
-:CREATED: [2026-06-11 Thu]
-:LAST_REVIEWED: 2026-06-11
+:LAST_REVIEWED: 2026-07-13
:END:
-A scheduled headless morning run chaining the existing pieces: startup checks, the triage-intake scan, a system health check — producing the prep doc plus a report and a notify ping, with all remediation propose-only. Staged adoption from the 2026-06-11 insights report's "Self-Healing Daily Ops Orchestrator": read-only first; promote individual routine remediations to auto only after each has a track record. Known blockers to design around: headless MCP auth (interactively-authenticated servers are absent in cron runs) and the consent boundary (triage Phase D, anything destructive).
+The human-eyes half of the docs-lifecycle acceptance surface. The convention shipped IMPLEMENTED 2026-07-04 (spec [[id:80b0787b-4a60-4c82-8a16-b383d3e3c8f2][docs-lifecycle]]); these checks confirm the human-facing behavior. A failed check promotes to a bug.
+
+*** Startup nudge appears and clears
+What we're verifying: the Phase A probe + Phase C nudge fire exactly once per project.
+- Open a session in a project with an unsorted docs/design (before its sort) and read the startup output.
+Expected: one line offering "run spec-sort"; after the pilot stamps :LAST_SPEC_SORT:, the next session shows nothing.
-** TODO [#C] Build =create-documentation= skill for high-quality project/product docs :feature:
+*** Moved-spec links click through in Emacs
+What we're verifying: the pilot's relink pass left todo.org and docs links working for the human reader, not just the residue grep.
+- After the Phase 3 pilot, open todo.org in Emacs and click three links that point at moved specs (including one from a dated log entry).
+Expected: each opens the spec at its new docs/specs/ path.
+
+** TODO [#D] Docs lifecycle vNext — org-agenda spec-status view :feature:no-sync:
+Once specs carry lifecycle TODO keywords under =docs/specs/=, add a custom org-agenda view that lists =DRAFT= / =READY= / =DOING= / terminal specs by status. Deferred from [[id:80b0787b-4a60-4c82-8a16-b383d3e3c8f2][the docs-lifecycle spec]]; not part of v1 because the grep board is sufficient until the status headings exist.
+
+** TODO [#C] Wrap-it-up summary mode — keep or cut :feature:
+:PROPERTIES:
+:LAST_REVIEWED: 2026-07-13
+:END:
+From Craig via the roam inbox (2026-07-02, routed by archsetup). Teardown-by-default already shipped (bare "wrap it up" closes the window; "with summary" keeps it). Craig's follow-on: "maybe we cut the summary altogether. help me think through when I'd want a summary and how I would recognize it before confirming and then having it close." Run that think-through with him (brainstorm-shaped, not solo), then adjust wrap-it-up.org's Step 6 + trigger phrases to the outcome.
+
+** TODO [#D] Warn-only pre-commit hook for tooling-path enumeration :feature:
:PROPERTIES:
-:LAST_REVIEWED: 2026-06-12
+:CREATED: [2026-06-22 Mon]
+:END:
+Optional enforcement teeth for the no-attribution / no-tooling-artifacts tightening landed 2026-06-22 (commit 91217d9), which is documentation-only. A warn-only (not blocking) pre-commit hook could scan the commit subject + body for tooling-path enumeration (=CLAUDE.md=, =.claude/=, =.ai/=, =todo.org=, =notes.org=, =session-context=) and AI-attribution language, with the two exemptions baked in: a commit whose change IS one of those files, and private single-user repos. Must warn, not block — a rigid grep false-positives on legit subject mentions. Deferred: Craig chose docs-only for now.
+
+** TODO [#D] Build =create-documentation= skill for high-quality project/product docs :feature:
+:PROPERTIES:
+:LAST_REVIEWED: 2026-06-15
:END:
Create a Claude skill named =create-documentation= that can plan, write,
@@ -803,9 +1408,9 @@ The skill should reject:
public/library/API docs: =llms.txt= or markdown export is valuable, but normal
human navigation remains primary.
-** TODO [#C] Build /research-writer — clean-room synthesis for research-backed long-form :feature:
+** TODO [#D] Build /research-writer — clean-room synthesis for research-backed long-form :feature:
:PROPERTIES:
-:LAST_REVIEWED: 2026-06-12
+:LAST_REVIEWED: 2026-06-15
:END:
Gap in current rulesets: between =brainstorm= (idea refinement → design doc)
@@ -862,7 +1467,7 @@ Upstream reference (do not vendor): ComposioHQ/awesome-claude-skills
** TODO [#D] Revisit =c4-*= rename if a second notation skill ships :chore:
:PROPERTIES:
-:LAST_REVIEWED: 2026-06-10
+:LAST_REVIEWED: 2026-06-15
:END:
Current naming keeps =c4-analyze= and =c4-diagram= as-is (framework prefix
@@ -1006,1701 +1611,723 @@ having a skill to generate or check OV-1-shaped artifacts. Don't build
speculatively — defense-specific notations are narrow enough that each
skill should be driven by a concrete contract need, not aspiration.
-** TODO [#C] Token-rotation helper for =@a-bonus/google-docs-mcp= OAuth refresh :feature:quick:
+* Rulesets Resolved
+** CANCELLED [#C] ntfy phone channel as general two-way agent-comms :feature:spec:
+CLOSED: [2026-07-13 Mon]
:PROPERTIES:
-:LAST_REVIEWED: 2026-06-12
+:CREATED: [2026-06-20 Sat]
+:LAST_REVIEWED: 2026-06-24
:END:
-
-When a Google refresh token gets revoked (re-grant scopes, removed Connected App, account password reset), recovery is currently manual: run =npx -y @a-bonus/google-docs-mcp= with the right env, follow the URL in a browser, kill the process, base64-encode the new =token.json=, decrypt =secrets.env.gpg=, replace the var, re-encrypt. A small =mcp/refresh-google-docs-token.sh <profile>= would chain that into one command.
-
-*** Sketch
-
-#+begin_src bash
-# usage: mcp/refresh-google-docs-token.sh personal
-profile="$1"
-gpg -d ... | grep -v "GOOGLE_DOCS_${profile^^}_TOKEN_B64" > /tmp/secrets.env.tmp
-GOOGLE_MCP_PROFILE="$profile" npx -y @a-bonus/google-docs-mcp &
-xdg-open <captured-url>
-# wait for ~/.config/google-docs-mcp/$profile/token.json to land
-kill %1
-echo "GOOGLE_DOCS_${profile^^}_TOKEN_B64=$(base64 -w0 ~/.config/google-docs-mcp/$profile/token.json)" >> /tmp/secrets.env.tmp
-gpg -c --cipher-algo AES256 -o mcp/secrets.env.gpg.new /tmp/secrets.env.tmp
-mv mcp/secrets.env.gpg.new mcp/secrets.env.gpg
-rm /tmp/secrets.env.tmp
-#+end_src
-
-The flow tonight worked but took a handful of manual steps. One script collapses it.
-
-Decision (Craig, 2026-05-31): *hold until a token rotation is imminent.* The OAuth re-grant is a browser step that can't be triggered without revoking a live token, so the script can't be verified in isolation. Not marked =:solo:= — when a token actually needs rotating, write and verify in one pass (solo at that point).
-
-** TODO [#C] Generic agent runtime support — Codex spec v0 :spec:design:
+Killed at the 2026-07-13 task review: home retired and tore down the ntfy channel on 2026-07-04, so this proposal's transport no longer exists. Its living successors are the Signal pager ([#B] task above — one identity on velox, runbook pending) and agent-page (shipped 2026-07-13), which cover the send half; two-way (read-replies) rides the Signal runbook.
+Proposal from the home project (2026-06-17): promote the self-hosted ntfy-over-Tailscale phone channel it built and verified on ratio into a general two-way agent-comms tool rulesets owns. Full proposal: [[file:docs/design/2026-06-17-ntfy-agent-comms-proposal.org]] (as-built runbook stays in the home project at =working/phone-notifications/spec.org=). What rulesets would decide: canonicalize =phone-notify= (send) plus a new =phone-recv= (check-since) as synced bin scripts; the per-machine config/secret convention (token in =~/.config/phone-notify/config= chmod 600 today, vs GPG-encrypted in dotfiles); a reference =ntfy-inbound-handler= plus systemd user-unit for event-driven delivery (Tier A subscriber routes inbound to inbox/notify, Tier B inbound spawns an agent session, Tier C notify a live session — harness research); approval-button workflows for the commits.md gates when Craig is away from the desk (tap-to-approve, the high-value concrete use); and the relationship to the retired cross-agent-comms scripts (ntfy may be the transport they lacked). Worked via =spec-create=. Blocks the triage-intake phone-push task below.
+** DONE [#B] Org-table helpers corrupt example blocks :bug:solo:
+CLOSED: [2026-07-14 Tue]
:PROPERTIES:
-:LAST_REVIEWED: 2026-06-12
+:CREATED: [2026-07-11 Sat]
+:LAST_REVIEWED: 2026-07-13
:END:
-Codex drafted a v0 design doc for making rulesets runtime-neutral rather than Claude-Code-specific. Motivating cases: offline operation with a local LLM, and two LLMs running in the same project at the same time without trampling each other's session-context.
-
-Spec at [[file:docs/design/2026-05-28-generic-agent-runtime-spec.org]] (moved here from inbox on intake).
-
-Immediate correctness issue Codex flagged: the singleton .ai/session-context.org is unsafe under simultaneous agents. Codex recommends starting with Phase 1 only — add AI_AGENT_ID + session-context.d/<id>.org without renaming the rest.
+Fixed in 951b6fc, test-first. Both defects landed as filed (block-type-aware scanning in both helpers; lint-org CLI report-only by default, writes behind --fix), plus the true corruption path found during the work: wrap-org-table's load-time CLI dispatch fired on lint-org's require and reformatted lint-org's file arguments. Entry-script guard added. The named regression test (example block byte-identical) is in the suite.
+=wrap-org-table.el= and =lint-org.el= both scan for =/^\s*|/= lines and rewrite them as org tables without skipping =#+begin_example=/=src=/=quote= regions, so ASCII art using pipe characters gets mangled into bordered tables. Reproduced 2026-07-09 in the work project against an architecture doc with a pipe/=v= flow diagram; a plain indented block became a table with =|---|= rules between every line. Two separable defects: (1) table detection is line-based — both helpers should use =org-element-at-point= / =org-in-block-p= to skip example/src/quote/verse blocks; (2) =lint-org.el= mutates its input on disk with no confirmation — passing five files reformatted all five (one by 1949 lines). A linter must report, not write; put the reformat behind an explicit =--fix= flag.
-Broader refactor proposes runtimes/ adapter manifests, generic install commands, language-bundle split (common/ + runtimes/<runtime>/), launcher refactor, local model service via llama.cpp/ollama. Big surface area, six phases.
+Grading (severity × frequency, per todo-format.md): Critical severity (silent org data loss; recoverable here only because the content was git-staged) × rare-edge-case frequency (fires only when a mixed table+example file is passed to the helper) = P2 = [#B].
-2026-06-12 spec review complete: [[file:docs/design/2026-05-28-generic-agent-runtime-spec-review.org][Codex review]] rubric for the whole spec is =Not ready=. Phase 1 is already shipped, and Phase 1.5 is tracked separately as the helper-instance task. Before any phases 2-5 implementation, decide whether to commit to the larger arc and answer the blocker decisions: generic instruction-file strategy, default local runtime/server, first supported local editing CLI, adapter scope, and compatibility behavior for existing =CLAUDE.md= / =.claude/= projects.
-
-*** 2026-06-10 Wed @ 14:13:55 -0500 Noted Phase 1 already shipped; narrowed scope to the phases 2-6 decision
-Phase 1 (the correctness fix) is live: protocols.org documents the AI_AGENT_ID-scoped session-context path (=.ai/session-context.d/<id>.org=) and =.ai/scripts/session-context-path= resolves it. The singleton race Codex flagged is closed. What remains is the spec review plus a go/no-go on the broader runtime-neutral refactor: runtimes/ adapter manifests, generic install commands, language-bundle split, launcher refactor, local model service.
-
-*** 2026-06-11 Thu @ 19:26:26 -0500 Spec amended with the helper-instance slice; implementation split out
-Craig's motivating case (a second Claude in the same project for lookups and safe task updates) was under-specified in v0 — it had identity and message targeting but no spawn mechanics and no write-safety contract for the shared files the session-context split doesn't isolate. Added the "Concurrent same-project agents (helper instances)" section (subagent boundary, identity/spawn via =ai --helper=, the tiered read/write contract, light startup, helper wrap-up) and Phase 1.5 to the migration plan. Implementation filed as its own [#B] task ("Helper-instance support"); this task stays scoped to the phases 2-6 go/no-go.
-
-*** 2026-06-12 Fri @ 02:09:10 -0500 Independent spec review complete
-Codex ran the spec-review workflow. Outcome: the combined spec is =Not ready= because phases 2-5 still require product decisions and current external-runtime/model verification. Phase 1.5 can proceed only as the already-split helper task, with rollout/manual-validation caveats accepted and no accidental template-wide release before sandbox/pilot drills pass. Review file: [[file:docs/design/2026-05-28-generic-agent-runtime-spec-review.org]].
-
-*** 2026-06-12 Fri @ 02:39:38 -0500 Second review after response pass
-Codex re-ran spec-review after the dispositions were folded in. Outcome by arc: Phase 1.5 helper instances =Ready with caveats=; phases 2-5 remain =Not ready= behind the explicit decisions/reverification gate. No new blocking findings for the helper slice. Review file updated in place: [[file:docs/design/2026-05-28-generic-agent-runtime-spec-review.org]].
-
-** DONE [#C] Session title hostname-project, no space :feature:quick:
-CLOSED: [2026-06-13 Sat]
+Regression test: run =wrap-org-table.el= against a file containing a =#+begin_example= block whose lines start with =|= and assert the block is byte-identical afterward. Source: work handoff 2026-07-09 (=inbox/2026-07-09-1341-from-work-bug-data-loss-wrap-org-table-el-and.org=).
+** DONE [#B] Sentry workflow — build from spec :feature:
+CLOSED: [2026-07-19 Sun]
:PROPERTIES:
-:CREATED: [2026-06-13 Sat]
-:LAST_REVIEWED: 2026-06-13
+:SPEC_ID: f6c51f27-d7a2-4b63-9ff9-5ba005a66dfb
:END:
-Routed from the roam global inbox via inbox-zero 2026-06-13. The SessionStart hook (=hooks/session-title.sh=) emitted =<host> <project>= with a space; Craig wanted =<host>-<project>= with a hyphen and no space. Changed the =sessionTitle= join to ="$host-$project"= plus the header comments, and updated the three =session-title-hook.bats= expectations (test-first; 6/6 green).
-
-* Rulesets Resolved
-** DONE [#C] Fix =cj-scan= false positives on cj fences nested inside other =#+begin_*= blocks :bug:
-CLOSED: [2026-05-15 Fri]
-
-=cj-scan.py= was matching =#+begin_src cj:= / =#+end_src= line-by-line
-without awareness of enclosing block scopes. A cj fence embedded inside a
-=#+begin_example= block (typically when documenting what the =<cj= yasnippet
-emits) or inside =#+begin_src snippet= (the yasnippet definition itself) was
-misclassified as a live cj annotation. Surfaced from a /respond-to-cj-comments
-run against the dotemacs =todo.org= that reported two false positives in the
-=<cj= yasnippet documentation.
-
-Fix: track an active =wrapper_type= state. When the scanner sees =#+begin_<type>=
-(for any =<type>= other than =cj:= via the more-specific cj-open regex, which
-is checked first), it enters a wrapper state where every line is treated as
-content until the matching =#+end_<type>= closer fires. Inside a wrapper, cj
-fence patterns and legacy inline =cj:= lines are both suppressed.
-
-Tests: added =TestCjScanNestedFencesIgnored= (6 tests) to
-=claude-templates/.ai/scripts/tests/test_cj_scan.py= covering nesting inside
-=#+begin_example=, =#+begin_src <other-lang>=, and =#+begin_quote=, plus
-regression guards that a wrapper closes cleanly (a subsequent real cj fence
-is still detected) and that an unclosed wrapper doesn't silently swallow
-later content into false-positive cj blocks.
-
-Full =make test-scripts= equivalent (=python3 -m pytest=): 302 passed, 1
-skipped, 0 failures.
-
-** DONE [#A] Add =make doctor= — verify ~/.claude/ matches repo + settings.json :feature:
-
-A drift detector that scans =~/.claude/= and reports anything inconsistent with what the repo expects. Single-command answer to "is my machine consistent with rulesets?"
-
-*** Why this matters
-
-A 2026-05-06 sweep found =~/.claude/hooks/= didn't exist on this machine even though =settings.json= referenced =~/.claude/hooks/precompact-priorities.sh= as a PreCompact hook. Compaction would have silently failed to invoke the hook. The fix was =make install-hooks=, but the breakage was invisible until I happened to grep for it. =make doctor= run regularly (or even as part of session start) would catch this kind of drift in seconds instead of after the fact.
-
-*** Checks
-
-- Every entry in =settings.json= ="hooks"= block points at a file that exists.
-- Every entry in =enabledPlugins= has a matching install under =~/.claude/plugins/data/=.
-- Every skill in =$(SKILLS)= has a working symlink at =~/.claude/skills/<name>=.
-- Every rule in =$(RULES)= has a working symlink at =~/.claude/rules/<name>=.
-- Every default hook has a symlink at =~/.claude/hooks/<name>= (warn-only — opt-out is legitimate).
-- =settings.json= and =.mcp.json= symlinks resolve to the rulesets versions.
-- =mcp/install.py= state matches =claude mcp list= (every server in =servers.json= is registered).
-- No dangling symlinks anywhere under =~/.claude/=.
-
-*** Output
-
-One line per check: =ok= / =WARN= / =FAIL=. Final summary: =N ok, M warnings, K failures=. Exit non-zero on any failure so it can ride a pre-flight check.
-
-** DONE [#A] Build =voice= skill — combine =humanizer= with universal + personal style passes :feature:
-
-Combine =humanizer= with universal good-writing passes (Strunk & White, Orwell, Plain English) and the personal-style passes from =commits.md=. Two modes — =general= for arbitrary writing, =personal= for commits/PRs/comments — share a foundation and diverge on register.
-
-Built and shipped 2026-05-07: =voice/SKILL.md= with 39 numbered patterns walked sequentially. Patterns 1-25 carried over from humanizer, 26-31 are universal good-writing additions, 32-39 are personal-only. Migrated three callers (=commits.md=, =respond-to-cj-comments.md=, =start-work.md=). Removed the standalone =humanizer= skill since voice supersedes it.
-
-*** Why this matters
-
-Three transformations want to run together for personal-mode artifacts (commits, PR titles + bodies, PR comments) but lived in three places: =humanizer= as a skill, S&W-style universal rules nowhere (applied ad-hoc), and the personal-style passes as prose steps in =commits.md= that got re-applied by hand each time. Costs: (1) the "I forgot pass (e)" failure mode — skipping a pass without flagging is a defect but happens in practice. (2) No single-call invocation of the full transform. (3) General-mode writing (research notes, philosophy, history) got only humanizer with no universal-prose pass at all. Combining brings them under one skill with one invocation.
-
-*** Design
-
-Two modes:
-
-- *general* (default) — for arbitrary writing not bound for commit/PR/comment publishing (research notes, philosophy/history essays, emails, README prose). Runs:
- - humanizer (current behavior — strip AI-generated-writing fingerprints)
- - tier-1 universal passes (canonical good-writing rules)
- - the 2 personal-style passes that have no register conflict (jargon-fragment rewrite, noun-ified verbs)
-
-- *personal* — for commits, PR titles + bodies, PR comments. Runs general PLUS:
- - 8 personal-only passes (first-person rewrite, semicolons, contractions, sentence-split, felt-experience, sentence fragments, terse cut, public-artifact scope check)
-
-The 8 personal-only passes are explicitly *not* in general mode. They conflict with academic / literary / philosophical register. Forcing first-person on a Foucault essay or stripping felt-experience from a journal entry would damage the writing.
-
-*** Tier 1 universals (v1)
-
-From Strunk & White, Orwell's "Politics and the English Language", Plain English Campaign, and Garner's Modern English Usage. Each is a detection-pattern + rewrite-rule pair, mechanical enough to apply consistently across runs.
-
-- *Omit needless words* — curated phrase list (=the fact that= → =that=/=because=, =in order to= → =to=, =at this point in time= → =now=, =due to the fact that= → =because=, =for the purpose of= → =to=, =in spite of= → =despite=, etc.)
-- *Long word → short word* — Plain English wordlist (~150 entries: =utilize=→=use=, =commence=→=start=, =terminate=→=end=, =facilitate=→=help=, =demonstrate=→=show=, =sufficient=→=enough=, =prior to=→=before=, =subsequent to=→=after=, =in the event that=→=if=, =a great deal of=→=much=)
-- *Active over passive voice* — detect "to be + past-participle" patterns. Suggestion-only in v1 (auto-rewrite is risky in technical contexts where passive is appropriate); graduate to auto-rewrite for unambiguous cases in v2.
-- *Comma splices* — detect independent clauses joined only by comma; rewrite to period or semicolon-then-period.
-- *Cliché flag* — small curated list (=at the end of the day=, =moving forward=, =going forward=, =at this juncture=, =circle back=, =low-hanging fruit=, =deep dive=, =leverage= as verb).
-
-*** Tier 2 universals (v2)
-
-- *Positive over negative form* (S&W) — =not unlike= → =like=, =do not fail to= → =remember to=, =did not pay any attention= → =ignored=
-- *Garner-style word-pair corrections* — comprise/compose, less/fewer, that/which (restrictive vs nonrestrictive), affect/effect, principal/principle
-- *Parallelism in lists* — detect mismatched grammar in bullet items
-- *Tense consistency* — flag mid-paragraph tense shifts
-- *Acronym definition on first use* — detect uppercase tokens used before being expanded
-
-*** Tier 3 (v3, may not land)
-
-- *Concrete-over-abstract* preference
-- *Emphatic word at sentence end* (S&W rule 18)
-- *Vary sentence length / rhythm*
-- *Reading-grade-level scoring* (Hemingway-style)
-
-*** Personal-style pass placement
-
-| # | Pass | Mode | Why |
-|----+-------------------------------------+-------------------------------------+-------------------------------------|
-| 1 | First-person voice rewrite | personal only | Forces "I" voice; wrong for |
-| | | | academic prose where third-person |
-| | | | and "we" are conventional |
-|----+-------------------------------------+-------------------------------------+-------------------------------------|
-| 2 | Jargon-fragment → complete sentence | both | Universal clarity, no genre |
-| | | | conflict |
-|----+-------------------------------------+-------------------------------------+-------------------------------------|
-| 3 | Semicolon → period/comma | personal only | Semicolons are conventional in |
-| | | | long-form / academic prose |
-|----+-------------------------------------+-------------------------------------+-------------------------------------|
-| 4 | Contractions ("it's", "don't") | personal only | Academic and formal writing |
-| | | | typically avoids contractions |
-|----+-------------------------------------+-------------------------------------+-------------------------------------|
-| 5 | Sentence split on conjunctions | personal only | Foucault, Hegel, Adorno |
-| | | | deliberately use long compound |
-| | | | sentences |
-|----+-------------------------------------+-------------------------------------+-------------------------------------|
-| 6 | Felt-experience narration ("I'll | personal only | Personal essays *use* |
-| | feel this every time") | | felt-experience as content |
-|----+-------------------------------------+-------------------------------------+-------------------------------------|
-| 7 | Noun-ified verbs ("the ask", "a | both | Targets corporate-speak with |
-| | learn", "the spend") | | curated wordlist; doesn't catch |
-| | | | philosophical nominalizations like |
-| | | | "the becoming" |
-|----+-------------------------------------+-------------------------------------+-------------------------------------|
-| 8 | Sentence fragments → complete (in | personal only | Fragments are valid stylistic |
-| | prose) | | devices in literary prose |
-|----+-------------------------------------+-------------------------------------+-------------------------------------|
-| 9 | Terse cut (rhetorical padding: | personal only | Tier 1 omit-needless-words covers |
-| | "worth noting", "it's important to | | the worst offenders universally; |
-| | understand") | | aggressive cut conflicts with |
-| | | | academic register |
-|----+-------------------------------------+-------------------------------------+-------------------------------------|
-| 10 | Public-artifact scope check (local | personal only — *flag-only*, no | Operational/safety check, not |
-| | paths, private repos, personal | auto-rewrite | stylistic; auto-masking risks |
-| | tooling) | | silently editing meaningful text |
-|----+-------------------------------------+-------------------------------------+-------------------------------------|
-
-*** Inclusive-language pass — explicitly excluded
-
-Considered and rejected. Conflicts with planned writing on philosophy/history topics (Foucault on sexuality and gender, history of slavery in New Orleans). Wordlist substitutions would override deliberate vocabulary choices in those genres.
-
-*** V1 scope
-
-- [ ] Skill at =~/code/rulesets/voice/= with =SKILL.md=
-- [ ] Frontmatter with positive triggers (commit, PR, comment, "humanize", "voice pass") and negative triggers (code, structured data, plain bullet lists)
-- [X] Mode invocation: default = =general= when invoked bare; =personal= invoked explicitly by publish-context callers
-- [X] humanizer content migrated from =humanizer/= → =voice/=
-- [X] Tier 1 universal passes implemented (5 patterns: #26-30, plus #31 noun-ified verbs as a universal personal addition)
-- [X] 2 personal passes that run in both modes (#30 jargon-fragment, #31 noun-ified verbs)
-- [X] 8 personal passes that run in personal mode only (#32 first-person, #33 semicolons, #34 contractions, #35 sentence-split, #36 felt-experience, #37 fragments, #38 terse cut, #39 scope check)
-- [X] Each pass = detection-pattern + rewrite-rule pair (#39 is detection + flag-only)
-- [X] Total v1 pattern count: 31 in general mode (humanizer's 25 + 4 tier-1 + 2 universal personal); +8 personal-only = 39 in personal mode
-- [X] Update =commits.md= to invoke =/voice personal= instead of "run =humanizer= and apply five passes manually"
-- [X] Remove the existing =humanizer/= skill (no callers outside this repo, all migrated)
-- [X] =make doctor= still passes
-- [X] =make lint= clean
-
-*** v2 (deferred)
-
-- [ ] Tier 2 universals (positive form, word-pair corrections, parallelism, tense consistency, acronym definition)
-- [ ] Per-pass severity flags for Tier 1 active-voice (suggestion-only when actor is implicit; auto-rewrite when actor is named)
-- [ ] Reporting mode: list which passes fired and which were no-ops
-
-*** v3 (aspirational, may not land)
-
-- [ ] Tier 3 (concrete-over-abstract, emphatic-word position, sentence-length variation, reading-grade scoring)
-- [ ] Progressive disclosure split: =voice/SKILL.md= orchestrator + =voice/passes/<pass-name>.md= per pass with worked examples
-
-*** Migration (resolved)
-
-Decision: deleted =humanizer/= entirely. Three callers (=commits.md=, =respond-to-cj-comments.md=, =start-work.md=) all updated to invoke =/voice= directly. No alias needed since nothing outside the repo invoked humanizer.
-
-*** Naming alternatives considered
-
-- =voice= — chosen. Captures both modes; broad enough.
-- =polish= — descriptive of multi-pass nature; less prescriptive about whose voice.
-- =house-style= — signals "this is the house style"; appropriate for personal repo.
-- =commit-voice= — too narrow (passes apply to research notes, emails, etc. in general mode).
-- =humanize= (extending current) — undersells the universal + personal additions.
-
-*** Open questions before implementation
-
-Resolved during implementation:
-- Default mode when =/voice= is invoked bare: =general=. Personal-context callers (=commits.md= publish flow, =respond-to-cj-comments.md=) invoke =/voice personal= explicitly. Avoids accidentally first-person-ifying research notes.
-- Reporting: skill prints "Summary of changes" listing which patterns fired (audit value).
-- Public-artifact scope check (#39): flag-only, user resolves manually. Blocking would frustrate on legitimate path mentions.
-- Tier 1 active-voice detection: suggestion-only in v1. Auto-rewrite for unambiguous cases deferred to v2.
-
-** DONE [#B] Add =--archive-done= mode to =.ai/scripts/todo-cleanup.el= :feature:
-
-Opt-in mode that moves every level-2 subtree whose TODO state is DONE or CANCELLED out of the "Open Work" section and into the "Resolved" section of the same org file, subtree intact.
-
-- *Section matching.* Key on a top-level heading containing "Open Work" and one containing "Resolved" — that pairing is the only naming consistent across projects (=Work Open Work= / =Work Resolved= here; bare =Open Work= / =Resolved= elsewhere). Require exactly one match for each; otherwise skip with a clear message, no crash.
-- *Modes.* =--check= previews and writes nothing, same as the existing hygiene pass. Idempotent. Not run by default in the wrap-up flow — archiving is consequential, so it stays opt-in: =emacs --batch -q -l todo-cleanup.el --archive-done FILE=.
-- *Edge cases.* Source or target section missing; subtree at EOF; nested DONE subtree under an open parent stays put (only level-2 entries move); nothing to move → clean no-op.
-- *Tests.* TDD with ERT — the project's first elisp tests. Fixtures (synthetic) under =.ai/scripts/tests/=; run via =make test= (rulesets) or =make test-scripts= (claude-templates), which run pytest + every =tests/test-*.el= ERT suite. Cases: one DONE level-2 moves; multiple; CANCELLED also moves; structural (no-state) headings don't move; nested DONE under an open parent stays; level-2 DONE with open level-3 children moves intact; subtree at EOF; missing source/target section; ambiguous "Resolved"; lowercase headings; nothing-to-do; idempotency; =--check= preview + its idempotency; realistic-sample integration.
-
-Origin: came up while scrubbing a project's todo.org on 2026-05-11 — moving a big completed PROJECT subtree (plus a few smaller ones) into the Resolved section by hand was the cue to build a reusable tool.
-
-Built and shipped 2026-05-11: =--archive-done= added to =.ai/scripts/todo-cleanup.el= test-first; 13-test ERT suite (=tests/test-todo-cleanup.el=) + realistic synthetic fixture (=tests/fixtures/todo-sample.org=), wired into =make test= / =make test-scripts= alongside pytest. The CLI dispatch moved into =tc-main= behind a guard so the suite can =require= the file without firing it. Section matching is case-insensitive and tolerates the =<Project> Open Work= / =<Project> Resolved= naming variants. Opt-in only — not wired into the wrap-up flow. Source of truth is =~/projects/claude-templates/=; rsync'd into this repo.
-
-** DONE [#B] Encode follow-up filing rules into =/start-work=
-CLOSED: [2026-05-15 Fri]
-
-Phase 4 step 5 of =/start-work= ("refactor audit") says any candidate that isn't fix-now must land in one of three buckets: fold-into-related-commit, separate =refactor:= commit, or "file a ticket or todo.org entry." The third disposition doesn't say *where* — which leaves the orchestrator picking a location ad-hoc. Result: follow-ups buried under children of an epic parent get orphaned when the parent closes, or follow-ups for standalone tasks scatter across the file with no convention.
-
-Proposed placement rule (already memorized for this project as =feedback_followups_as_siblings.md=, generalizing):
-
-- *Epic-style parent task* (level-2 with multiple level-3 children) → follow-ups file as level-2 *siblings* of the parent. Stays visible after parent closure.
-- *Standalone task* (level-2 with no children, or a level-3 inside another structure) → follow-up files as a new level-2 top-level entry in the same =* Open Work= section. Don't nest under the originating task.
-
-Both cases: include a "Triggered by: <date> <task or commit>" line so a future reader sees what surfaced it.
-
-Update =.claude/commands/start-work.md= Phase 4 step 5's "Disposition for each candidate" section to spell this out. Update any cross-references in =commits.md= or other files that touch the discipline.
-
-Triggered by: 2026-05-15 fold-epic session — Craig flagged the gap mid-flight after I'd surfaced a follow-up but hadn't filed it.
-** DONE [#A] Consolidate =.ai/= template infrastructure (fold + audit + install-ai + ratio) :feature:
-CLOSED: [2026-05-15 Fri]
-
-End-state: one repo (=rulesets=) is the single source of truth for =.ai/= template content. =make audit= verifies and applies drift across every =.ai/=-using project on the machine. =make install-ai= bootstraps new projects. Same setup propagated to ratio so both machines run the same way.
-
-Today (2026-05-15) the canonical-source rule got violated again: rulesets commit =372fb76= added a wrap-up subsection to =rulesets= without going through =claude-templates= first, and the next session's startup rsync was about to silently undo it. Two-repo coordination is the root cause; fold solves it.
-
-Build order: fold first (others depend on the new canonical path), then audit + install-ai in parallel, then test, then propagate to ratio.
-
-*** DONE [#A] Fold =claude-templates= into rulesets
-CLOSED: [2026-05-15 Fri]
-
-Two repos, one source of truth. =~/projects/claude-templates/= is the canonical =.ai/= template that gets rsync'd into every project at session start. Keeping it standalone means a second =git pull= in startup Phase A.0, a second remote to push to at wrap-up, and a split history any time a change touches both. Folding it into =rulesets/claude-templates/= gives one repo to clone on a fresh machine and one place to edit templates.
-
-**** Open design choices
-
-- *History.* =git subtree add --prefix=claude-templates ~/projects/claude-templates main= preserves the 84-commit history under the new prefix. Plain content copy (=cp -a= + =git add=) is simpler but loses history. Either is fine since the standalone repo stays archived on =cjennings.net=.
-- *Layout.* =rulesets/claude-templates/= mirrors the old repo name and sits next to =claude-rules/= cleanly. Alternative: absorb =.ai/= directly under a different name (=rulesets/.ai-template/= or similar). First option is clearer.
-- *bin/ai.* The standalone Makefile symlinks =$HOME/.local/bin/ai → bin/ai=. After the move, fold that into rulesets' Makefile as another install target.
-
-**** Mechanical steps
-
-1. Subtree-merge or copy =~/projects/claude-templates/= into =rulesets/claude-templates/=.
-2. Update 3 references in rulesets:
- - =.ai/protocols.org= line 163 — pointer in the "Let's run/do the X workflow" section.
- - =.ai/workflows/cross-agent-comms.org= line 8 — promotion-target path.
- - =.ai/workflows/startup.org= lines 22, 96-98 — Phase A.0 pull + Phase A rsync sources.
-3. Update Phase A.0 of =startup.org= to pull rulesets instead of claude-templates. Inside rulesets sessions, the existing project-repo pull already covers it. Outside rulesets (every other project's session), Phase A.0 needs an explicit =git pull= on =~/code/rulesets/= before the rsync — otherwise the templates will be stale.
-4. Replace =~/projects/claude-templates/= with a symlink to =~/code/rulesets/claude-templates/= for transition continuity.
-5. After every active project has had one session start (and rsync'd the new =startup.org=), drop the symlink and archive =cjennings.net:git/claude-templates.git=.
-
-**** Bootstrap gap
-
-Every project on the machine has a =.ai/workflows/startup.org= that rsyncs from =~/projects/claude-templates/=. Until each project's startup.org gets refreshed (which happens via the rsync itself), the old path needs to keep resolving. The symlink at step 4 is the bridge: old paths resolve into the new location, the rsync delivers the updated startup.org, next session uses the new path directly.
-
-*** DONE [#A] Add =make audit= — drift detector across all =.ai/=-using projects
-CLOSED: [2026-05-15 Fri]
-
-Companion to =make doctor= (single-machine scope, checks =~/.claude/=). =audit= is cross-project scope: walks every directory on the machine that has a =.ai/=, diffs the synced template files against the canonical source, and reports drift. =--apply= flag rsyncs the drift into the project's working tree (no auto-commit). Catches stale projects without forcing a session start in each one.
-
-**** Open design choices
-
-- *Scope.* Template-sync drift is the useful flavor: for each project, diff =.ai/protocols.org=, =.ai/workflows/=, =.ai/scripts/= against the canonical source.
-- *Source path.* Post-fold: =~/code/rulesets/claude-templates/.ai/=. Build =audit= against the new path from day one.
-- *Project discovery.* Walk =~/code/=, =~/projects/=, =~/.emacs.d/= up to depth 3 for any directory containing =.ai/=. Skip the canonical source itself.
-- *Default mode is report-only.* =--apply= triggers rsync; =--force= overrides the dirty-skip safety.
-
-**** Per-project flow (designed 2026-05-15)
-
-For each discovered project, in order:
-
-1. Verify =.ai/= exists (path probe). If missing → =FAIL=, skip, continue loop.
-2. Detect git tracking via =git check-ignore .ai/= → =tracked= or =gitignored=.
-3. Verify no uncommitted =.ai/= changes (=git status --porcelain .ai/=). Dirty → =WARN=, skip rsync unless =--force=.
-4. Verify content matches canonical via three =rsync -a --dry-run --itemize-changes= calls (=protocols.org=, =workflows/=, =scripts/=). Zero items = clean.
-5. Action (=--apply= only, drift detected): three =rsync -a [--delete]= calls.
-6. Verify rsync converged (re-run the dry-runs; zero now).
-7. Verify working-tree state after rsync (tracked projects). Report deltas. Do not auto-commit.
-8. Verify no unpushed =.ai/= commits (=git log @{u}..HEAD -- .ai/=). Informational only.
-
-**** Output format (mirrors =doctor=)
-
-#+begin_example
-Claude-templates source:
- ok rulesets/claude-templates is current (origin/main)
-
-Per-project .ai/ drift:
- ok ~/projects/work
- applied ~/projects/homelab 3 files changed
- skipped ~/code/winvm uncommitted .ai/ (use --force)
- ok ~/projects/clipper
-
-Summary: 18 ok, 3 applied, 1 skipped, 0 failed
-#+end_example
-
-Exit code: =0= if all clean, no skips, no failures. =1= otherwise.
-
-**** Why not extend =make doctor= instead
-
-=doctor= has a clean meaning today: "is this machine's =~/.claude/= consistent with rulesets?" Mixing in cross-project =.ai/= drift muddies the exit code. Keep them separate. =audit= can optionally invoke =doctor= as its last check since both ask "did the symlinks keep up with the source?". A future =make all-checks= can wrap both.
-
-*** DONE [#A] Add =make install-ai PROJECT=<path>= — bootstrap =.ai/= in a fresh project
-CLOSED: [2026-05-15 Fri]
-
-Separate target from =audit= because operating on projects that lack =.ai/= is a distinct action. The absence might be intentional, so =audit= skips them. Bootstrap is explicit opt-in.
-
-**** Flow
-
-1. Refuse if =.ai/= already exists in =PROJECT=. Message: "already installed; use =make audit --apply= to update."
-2. Verify =PROJECT= is a git checkout (warn if not — works without git, loses some lifecycle benefits).
-3. Create =PROJECT/.ai/= directory.
-4. Rsync canonical content: =protocols.org=, =workflows/=, =scripts/= (same three rsyncs as =audit=).
-5. Seed =PROJECT/.ai/notes.org= from a canonical template with project-name placeholder.
-6. Create empty =PROJECT/.ai/sessions/= (with =.gitkeep= for tracked projects).
-7. Track or gitignore =.ai/=? Default: ask. Flag: =--track= / =--gitignore=.
-8. Print next-steps banner: =make install-lang LANG=<lang> PROJECT=<path>=; open Claude Code in the project.
-
-**** Symmetry with existing install targets
-
-#+begin_example
-make install-lang LANG=python PROJECT=/path # language bundle (existing)
-make install-ai PROJECT=/path # .ai/ template (new)
-make install-lang # no args → fzf-pick
-make install-ai # no args → fzf-pick from
- # ~/projects/* + ~/code/* dirs
- # without an existing .ai/
-#+end_example
-
-*** DONE [#A] Test plan for audit + install-ai before propagating to ratio
-CLOSED: [2026-05-15 Fri]
-
-Test against the current state of this machine before pushing changes to ratio.
-
-**** =make audit= tests
-
-1. Dry-run report only (no =--apply=). Should show: claude-templates current; per-project drift; correct =ok=/=drift= classifications; summary line and exit code match.
-2. After the fold lands, every project should be reported as drift (their =startup.org= still points at the old path). Run =--apply= → rsync converges. Re-run audit → all =ok=.
-3. Manually edit one =.ai/workflows/foo.org= in a tracked project. Re-run audit → should report =skipped: uncommitted .ai/=. Run =--apply --force= → rsync clobbers the edit. Verify the edit is gone.
-4. Manually delete one =.ai/= dir. Re-run audit → =FAIL: .ai/ missing=. Loop continues.
-5. Idempotency: =--apply= twice in a row converges to all =ok= on the second pass.
-
-**** =make install-ai= tests
-
-1. Create =/tmp/test-fresh-project= as a git repo. Run =make install-ai PROJECT=/tmp/test-fresh-project=. Verify =.ai/= structure matches canonical, =notes.org= has placeholder, =sessions/= exists.
-2. Run =make install-ai PROJECT=/tmp/test-fresh-project= again → should refuse (=.ai/= already exists).
-3. Open Claude Code in the new project. Startup workflow runs cleanly (Phase A.0 + Phase A rsync should be a no-op since the install just ran).
-4. fzf form: =make install-ai= with no args. Lists candidate dirs (=~/projects/*=, =~/code/*= without =.ai/=).
-
-**** Pass criteria
-
-- =audit= behavior matches the per-project flow spec for every classification path.
-- =install-ai= produces a project indistinguishable from one that's been running sessions for a while.
-- =make doctor= still passes 36/0/0 after all the work.
-- =make test= (pytest + ERT) passes.
-
-*** DONE [#A] Migrate projects on ratio (second machine)
-CLOSED: [2026-05-15 Fri]
-
-After local fold + audit + install-ai are working, propagate to ratio.
-
-**** Steps
-
-1. On ratio: =git -C ~/code/rulesets pull= — picks up the folded =claude-templates/= subdir and updated =Makefile= targets.
-2. On ratio: archive or =mv= the standalone =~/projects/claude-templates/= aside, replace with symlink to =~/code/rulesets/claude-templates/= (same bridge mechanic as local).
-3. On ratio: =make audit= → see drift across ratio's projects.
-4. On ratio: =make audit --apply= → rsync into each tracked/gitignored project. Surface projects with uncommitted =.ai/= drift for manual handling.
-5. On ratio: =make doctor= → catch any =~/.claude/= install drift (likely some, since ratio hasn't seen recent rulesets updates).
-6. Verify by opening Claude Code in a few ratio projects. Startup should be a no-op or near-zero rsync.
-
-**** Known unknowns
-
-- Ratio may have its own project list overlapping with this machine's but not identical. =audit= discovers projects via the walk, so this is automatic.
-- Ratio might have uncommitted =.ai/= work in some projects that this machine doesn't. =audit= surfaces them; handle case-by-case.
-- If anything goes wrong, ratio's archived =~/projects/claude-templates/= is the safety net — restore the symlink target and re-run audit.
-
-**** Adjacent: cross-machine memory sync
-
-The =[#A] DOING= memory-sync investigation (todo.org:10) is adjacent. Both involve "make my Claude setup portable across machines." Coordinate so the memory-sync stow approach (if approved) doesn't conflict with this fold's symlink mechanics.
-** DONE [#B] Document startup pull-ordering rule in protocols.org
-CLOSED: [2026-05-15 Fri]
-
-Phase A.0 of =startup.org= now pulls rulesets ff-only before the project repo
-(shipped 2026-05-15 as part of the claude-templates fold — after the subtree
-merge, there's no separate claude-templates pull, just rulesets-then-project).
-The protocols.org paragraph stating the ordering and "resolve any issues
-before proceeding" rule shipped 2026-05-15 in the =** Startup Pull Ordering=
-subsection under =IMPORTANT - MUST DO=.
-** DONE [#A] Build =/lint-org= skill + wrap-up integration
-CLOSED: [2026-05-14 Thu]
-
-Spec: [[file:.ai/specs/lint-org-skill-spec.md]]
-
-A two-mode skill (=interactive=, =mechanical-only=) that runs =org-lint=,
-auto-fixes safe categories (item-number, missing-language-in-src-block,
-misplaced-planning-info, markdown-bold → single-asterisk), and walks judgment
-items (broken local-file links, invalid fuzzy links, verbatim-asterisk false
-positives, suspicious-language blocks) inline.
-
-Wrap-up integration: =wrap-it-up.org= invokes
-=/lint-org todo.org --mode=mechanical-only= after the existing
-=todo-cleanup.el --archive-done= pass. Judgment items defer to a
-carry-forward file that the next morning's daily-prep merges in, so
-wrap-up never blocks on a judgment call.
-
-Baseline that motivated this: the 2026-05-14 manual pass took =todo.org=
-from 55 → 1 lint warnings across two commits (=0d10458= signal,
-=9ad5b30= cosmetic). A nightly mechanical sweep keeps the count near
-zero forever — each day's drift is small.
-** DONE [#C] Test harness for =make audit= + =make install-ai= edge cases :test:
-CLOSED: [2026-05-15 Fri]
-
-Three edge cases from the fold-epic test plan were not exercised because they're destructive on real projects:
-
-- =audit --force= clobbers uncommitted =.ai/= work — needs a project with intentionally dirty =.ai/= to verify the override path.
-- =audit= reports =FAIL= when =.ai/= is missing — needs a project where the directory was deleted to verify the loop continues past the failure.
-- =install-ai= fzf-pick form (no =PROJECT= arg) — needs interactive testing.
-
-Build a self-contained test harness under =.ai/scripts/tests/= that spins up =/tmp/audit-test-projects/= with a known matrix of project states (clean, dirty, missing =.ai/=, pristine, etc.), runs the audit + install-ai targets against it, and asserts expected outputs. The harness should clean up after itself.
-
-Pattern reference: bats or shell-based assertions (similar to the elisp ERT suites for =todo-cleanup= and =lint-org=, but for shell scripts).
-
-Triggered by: 2026-05-15 fold-epic, child 4 test plan; commits =94782ee= (audit) + =d364cf2= (install-ai).
-** DONE [#A] wrap it up mentions github, which isn't the remote for many projects. :chore:
-CLOSED: [2026-05-16 Sat]
-For many of them, git.cjennings.net mirrors to github.com, and github.com isn't the remote.
-For many others, git.cjennings.net is the remote with no mirror.
-Remove or replace the reference to github.com
-** DONE [#B] Phase A startup blind to =claude-templates/inbox/= post-fold :bug:fold:
-CLOSED: [2026-05-19 Tue]
-
-Resolved on inspection: the bug is moot in current state. =inbox-send.py='s discovery scans =~/code/*= and =~/projects/*= single-level only, so =claude-templates/= (two levels under =~/code/=) is never a routable target; the 2026-05-15 incident was a one-time manual workaround because =rulesets/inbox/= didn't exist yet, and that root inbox was added in =470085f=. =claude-templates/inbox/= was removed 2026-05-15 and is no longer on disk.
-
-Phase A's inbox check at =startup.org:107= runs =\ls -la inbox/= against the project root. Post-fold, the canonical's inbox sits inside the subtree at =claude-templates/inbox/= and never gets scanned. A 2026-05-15 cross-project handoff from a dotemacs session dropped a record there; the next rulesets session (this one) missed it at startup entirely. Picked up only when the working-tree drift surfaced during the publish flow.
-
-Fix: extend Phase A's discovery to also scan =claude-templates/inbox/= when the canonical lives in-repo (i.e., when =claude-templates/.ai/= exists alongside =./.ai/=). The Phase B/C inbox-processing flow already handles per-file routing once a file is surfaced; the gap is only in discovery.
-
-Adjacent question worth answering at the same time: should cross-project handoffs file into =./inbox/= at the project root (matching what Phase A already scans), or stay in =claude-templates/inbox/= and rely on the discovery fix? The =inbox-send= script's target-project logic is the place to settle that.
-
-Triggered by: 2026-05-15 evening session, surfaced when committing the test-harness work.
-** DONE [#A] Implement task-review daily-habit per spec
-CLOSED: [2026-05-20 Wed]
-:PROPERTIES:
-:LAST_REVIEWED: 2026-05-20
-:END:
-Spec: [[file:docs/design/task-review.org]]
-
-Retires =wrap-it-up.org='s date-coverage scan and replaces it with a daily list-hygiene review (N=7 oldest-unreviewed top-level =[#A]= / =[#B]= / =[#C]= tasks per session, ~12-day rotation). Built as a pure Claude workflow — Shape B, no elisp; see the spec's Revision section for why the elisp approach was dropped.
-
-Status:
-1. [X] =task-review-staleness.sh= + bats (count + =--list= modes).
-2. [X] =wrap-it-up.org= health check (threshold 30).
-3. [-] =task-review.el= — dropped (Shape B is a pure workflow, not an Emacs mode).
-4. [X] New =task-review.org= workflow + INDEX entry (the existing listing workflow was renamed to =open-tasks.org= to free the name).
-5. [X] Startup nudge in template =startup.org= (threshold 7), not the project-only startup-extras layer.
-6. [X] Smoke test against live =todo.org= — first cycle run 2026-05-20 (7 tasks reviewed: 3 re-grades, 1 cancellation, 1 bump-and-tag).
-
-Triggered by: 2026-05-16 brainstorm on retiring the date-coverage scan.
-** CANCELLED [#B] Build =ov-1= skill for DoDAF OV-1 (High-Level Operational Concept Graphic)
-CLOSED: [2026-05-20 Wed]
-
-Cancelled during the 2026-05-20 task review.
-
-Triggered by SOFWeek (May 2026, Tampa) — DeepSat attending; DoD attendees
-may ask for architecture diagrams. OV-1 is the universal informal
-currency in DoD briefings ("show me the architecture" → OV-1 by default).
-
-Priority upgrades to =[#A]= if Craig confirms scenario 2 below (personal
-load-bearing need at the event); stays =[#B]= or drops to =[#C]= if
-scenario 1 (team already covers it, future asset only).
-
-*** Prior art (searched 2026-04-19)
-
-No existing Claude Code skill exists for DoDAF / OV-1 / SV-1 / SysML.
-
-- =anthropics/skills= — 17 skills, zero DoDAF/SysML/defense coverage.
-- =awesome-claude-code= list — zero hits for DoDAF/OV-1/SysML/UAF.
-- =mfsgr/sysml2dodaf= — empty repo (0 stars, no code). Vapor.
-- =HowardKao-1130/mini-NEXEN= — broad SE methodology skill that
- name-drops DoDAF as a trigger keyword; no artifact generation. 0 stars.
-- =gaphor/gaphor= (Apache-2.0, 2.2k stars) — mature UML/SysML GUI
- modeler. Not a skill; not a pipeline. Useful reference only.
-
-Nearest prior art to lean on when building:
-- DoDAF 2.02 Viewpoints & Models reference (dodcio.defense.gov) —
- canonical OV-1 exemplars. Embed 3-5 layouts as skill =references/=.
-- Pattern from existing =c4-diagram= skill — same shape (prose → diagram
- spec), swap the viewpoint vocabulary to DoDAF.
-- PlantUML for SV-1 (when that skill comes later); Mermaid or draw.io
- XML for OV-1 lightweight visuals.
-
-*** Build scope (when triggered)
-
-*In scope:*
-- Input: prose description of a system + its operational context.
-- Output: structured OV-1 *spec* — performers, external actors (other
- systems, forces, adversaries), relationships (data/control flows),
- narrative captions, classification marking, legend requirements.
-- DoDAF 2.02 completeness checklist as a quality gate — verify the
- produced spec contains every element a correct OV-1 requires.
-- Optional lightweight visual: draw.io XML or Mermaid approximation for
- quick review; NOT a finished rendering.
-
-*Out of scope:*
-- Icon libraries, pictorial assets, finished PowerPoint export. OV-1
- final art belongs to a designer or Craig in Visio/PowerPoint; the
- skill's job is the spec and the check, not the slide.
-- SV-1, SV-2, UAF, IDEF1X, other viewpoints. Build only when a
- concrete need triggers each.
-
-Estimate: 4-6 hours.
-
-*** Craig's investigation before kickoff
-
-1. Does DeepSat's systems-engineering or marketing team already have an
- OV-1 (or the equivalent briefing artifact) for SOFWeek?
-2. If yes (scenario 1) — skill is a future asset, not event-load-bearing.
- Ship after SOFWeek. Priority drops to =[#C]=.
-3. If no, or if the scenario is "Craig may need to produce/iterate an
- OV-1 on the fly during the event" (scenario 2) — skill is load-bearing
- for the event. Priority upgrades to =[#A]=; build before SOFWeek.
-4. Confirm the classification level the skill needs to handle
- (unclassified-only? or FOUO markings? affects the classification
- block in the spec).
-5. Confirm the target rendering format DeepSat uses for OV-1
- deliverables (PowerPoint slide? Cameo? Visio? affects whether the
- skill emits draw.io XML vs Mermaid vs pure structured spec).
-
-*** Related
-
-See also the DoD-specific notations section under the later TODO
-(=c4-*= rename revisit) — OV-1 is flagged there as the highest-value
-starting point across the DoD notation landscape (SysML, DoDAF/UAF,
-IDEF1X). This entry is the execution plan for that starting point.
-** DONE [#A] Split team-specific publishing rules out of commits.md :commits:
-CLOSED: [2026-05-22 Fri]
-Shipped 3cb467e. Moved the DeepSat publishing steps (Linear ticket-state, the Slack notification protocol + channel ID, the GHE host, the team merge norm, the Linear ticket-body structure) out of the global =claude-rules/commits.md= into =teams/deepsat/claude/rules/publishing.md=. The global file keeps the universal skeleton and uses seams ("run the project's publishing overlay here if present") like startup-extras. Added =install-team= (targeted per-project copy, keyed on PROJECT, never globally symlinked) and generalized =sync-language-bundle.sh= to keep team overlays fresh at startup (3 new bats; make test green).
-
-Remaining deploy step (cross-project, surfaced to Craig): install the overlay into the DeepSat work project — =make install-team TEAM=deepsat PROJECT=<deepsat-path>= — so it actually loads there.
-** DONE [#A] Define a /voice-unavailable fallback in the commits.md publish flow :commits:
-CLOSED: [2026-05-22 Fri]
-Added an "If =/voice= is unavailable" paragraph to the Single-skill gate in =commits.md=: walk the same patterns inline (the flow already names which matter), state the skill was unavailable and the pass was applied by hand ("/voice unavailable — patterns walked inline"), and flag the missing skill for install. The gate is the pattern walk, not the tooling. The original "=humanizer= unavailable" framing was moot (humanizer → /voice).
-** DONE [#A] wrap-it-up Step 3.5 assumes GitHub-family remote :chore:quick:
-CLOSED: [2026-05-22 Fri]
-:PROPERTIES:
-:LAST_REVIEWED: 2026-05-20
-:END:
-Documented the assumption inline at =wrap-it-up.org= Step 3.5 (chose the lightweight path over a provider-agnostic rewrite): the =gh= lookup expects a GitHub-family host, holds today via DeepSat on GHE, flagged for update if a future Linear project lands on GitLab/Gitea/Bitbucket.
-Triggered by: 2026-05-16 wrap-it-up github.com cleanup (audit of the same file).
-
-Step 3.5 (Linear ticket-state hygiene) at =wrap-it-up.org:207= says "the project's GitHub remote — use =gh pr list ...=". Currently fine in practice: the step is Linear-gated, and the only Linear-using project is DeepSat (on =deepsat.ghe.com=, a GitHub-family host where =gh= works). Would break if a future Linear-using project lived on a non-GitHub host (gitlab, gitea, bitbucket). Either drop the GitHub-family assumption (provider-agnostic lookup, harder) or document the assumption explicitly so future projects know the step needs an update if they don't fit.
-** DONE [#C] Review pass: tighten skills and rulesets after 2026-05-04 audit
-CLOSED: [2026-05-22 Fri]
-:PROPERTIES:
-:LAST_REVIEWED: 2026-05-20
-:END:
-All 55 grouped-index items dispositioned (2026-05-22): ~49 edited across skills, commands, rule files, hooks, and the two playwright skills; several came out moot post-audit (humanizer→voice, skills→commands, typescript ruleset added); the two commits.md items shipped as the team-overlay split + /voice fallback. Freshness-checked each item against current reality before editing.
-
-Source notes used in this pass:
-- C4 official docs: C4 is notation-independent; System Context and Container
- diagrams are enough for most teams; every diagram needs title, key/legend,
- explicit element types, and audience-appropriate abstraction.
- [[https://c4model.com/diagrams][C4 diagrams]],
- [[https://c4model.com/diagrams/notation][C4 notation]],
- [[https://c4model.com/abstractions/component][C4 component]]
-- arc42 docs: quality requirements need measurable scenarios; section 10
- should reference top quality goals and capture lesser quality requirements
- with specific measures. [[https://docs.arc42.org/section-10/][arc42 section 10]],
- [[https://quality.arc42.org/articles/specify-quality-requirements][specifying quality requirements]]
-- ADR references: ADRs capture one justified architecturally significant
- decision and its rationale; Nygard's original guidance emphasizes short,
- numbered, repository-stored records and superseding rather than rewriting old
- decisions. [[https://adr.github.io/][adr.github.io]],
- [[https://cognitect.com/blog/2011/11/15/documenting-architecture-decisions][Nygard ADR article]]
-- Playwright docs: prefer user-visible locators and web assertions; locators
- auto-wait and retry; =networkidle= is discouraged for testing readiness.
- [[https://playwright.dev/docs/best-practices][Playwright best practices]],
- [[https://playwright.dev/docs/locators][Playwright locators]],
- [[https://playwright.dev/docs/next/api/class-page][Playwright page API]]
-- OWASP references: Top 10 2021 includes Broken Access Control,
- Cryptographic Failures, Injection, Insecure Design, Security
- Misconfiguration, Vulnerable and Outdated Components, Identification and
- Authentication Failures, Software and Data Integrity Failures, Security
- Logging and Monitoring Failures, and SSRF; WSTG adds a broader testing map
- across configuration, identity, authn/z, sessions, input validation, error
- handling, cryptography, business logic, client-side, and API testing.
- [[https://owasp.org/Top10/2021/][OWASP Top 10 2021]],
- [[https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/][OWASP WSTG]]
-- V2MOM references: Salesforce calls the last M "Measures" and emphasizes a
- simple alignment document with prioritized Methods, explicit Obstacles, and
- measurable outcomes. [[https://trailhead.salesforce.com/content/learn/modules/selfmotivation/get-focused-with-your-personal-v2mom][Salesforce Trailhead personal V2MOM]],
- [[https://www.salesforce.com/blog/?p=12][Salesforce V2MOM alignment]]
-- Prompt research: the cited Meincke paper is titled "Call Me A Jerk:
- Persuading AI to Comply with Objectionable Requests"; its scope is
- persuasion increasing compliance with objectionable requests, not a general
- proof that persuasion framing improves prompt quality.
- [[https://papers.ssrn.com/sol3/papers.cfm?abstract_id=5357179][SSRN paper]]
-- Combinatorial testing references: NIST supports t-way combinatorial testing
- and notes pairwise is one covering strength, with higher-strength arrays
- useful for failures requiring more interacting factors.
- [[https://www.nist.gov/publications/practical-combinatorial-testing-beyond-pairwise][NIST beyond pairwise]],
- [[https://www.nist.gov/publications/combinatorial-software-testing][NIST combinatorial testing]]
-
-*** Grouped index (for batching by area)
-
-Each item below is a one-line summary of a sub-TODO further down. Tick the box when the matching sub-TODO is moved to =DONE=. Items are grouped by area so they can be batched (e.g., "do all Playwright items in one session").
-
-**** Browser testing
-- [X] [#A] =playwright-js=: locator/assertion-first guidance (replace raw CSS, =networkidle=)
-- [X] [#B] =playwright-js= + =playwright-py=: reconcile headless/visible defaults
-- [X] [#B] =playwright-js= + =playwright-py=: remove emoji console markers from examples
-
-**** Frontend / UI
-- [X] [#B] =frontend-design=: WCAG 2.2 alignment, accessibility non-optional
-- [X] [#B] =frontend-design=: harmonize aesthetic guidance with anti-pattern rules
-
-**** Security
-- [X] [#A] =security-check=: OWASP 2021 + WSTG coverage
-- [X] [#B] =security-check=: tooling and offline/network caveats
-
-**** Combinatorial testing
-- [X] [#B] =pairwise-tests=: t-way escalation guidance beyond pairwise
-- [X] [#B] =pairwise-tests=: clarify negative value syntax + generator availability
-
-**** V2MOM
-- [X] [#A] =create-v2mom=: rename Metrics → Measures (Salesforce alignment)
-- [X] [#B] =create-v2mom=: prevent task migration from turning V2MOM into a backlog
-- [X] [#B] =create-v2mom=: mitigation/owner fields for Obstacles
-
-**** Prompt engineering
-- [X] [#A] =prompt-engineering=: correct/narrow Meincke citation
-- [X] [#B] =prompt-engineering=: eval-harness requirement for production prompts
-
-**** Codify
-- [X] [#B] =codify=: stale-entry review + privacy checks before writing project =CLAUDE.md=
-
-**** Code review
-- [X] [#A] =review-code=: resolve local-verification vs CI boundary
-- [X] [#B] =review-code=: =CLAUDE.md= citation scope for public artifacts
-- [X] [#B] =review-code=: relax three-strengths rule for tiny/failing diffs
-
-**** PR / review responses
-- [X] [#A] =respond-to-review=: remove review-process language from commit messages
-- [X] [#B] =respond-to-review=: use unresolved threads + resolution state
-- [X] [#B] =respond-to-cj-comments=: drop personal absolute paths from public-writing (moot — already clean)
-- [X] [#B] =respond-to-cj-comments=: fallback when =humanizer= or =emacsclient= unavailable (moot — superseded by /voice + VERIFY pattern)
-
-**** Branch workflow
-- [X] [#A] =finish-branch=: fix base-branch detection
-- [X] [#B] =finish-branch=: worktree-aware pull/merge safety
-- [X] [#B] =start-work=: tool-availability + ceremony-scaling rules
-- [X] [#B] =start-work=: claim-before-justify rollback risk
-
-**** Tests / TDD
-- [X] [#B] =add-tests=: fix missing =typescript-testing.md= reference or add ruleset (moot — ruleset now exists)
-- [X] [#B] =add-tests=: explicit exceptions to "all three categories per function"
-
-**** Debugging / RCA
-- [X] [#B] =debug=: capture environment + recent-change context before hypotheses
-- [X] [#B] =root-cause-trace=: constrain defense-in-depth to trust boundaries
-- [X] [#B] =five-whys=: require evidence + counterfactual validation per why
-
-**** Brainstorming
-- [X] [#B] =brainstorm=: timebox + research/source rules for high-stakes designs
-
-**** Architecture
-- [X] [#B] =arch-decide=: timeless examples, drop unverifiable claims
-- [X] [#B] =arch-decide=: standardize statuses + immutability language
-- [X] [#B] =arch-design=: threat modeling + privacy/compliance as first-class inputs
-- [X] [#B] =arch-design=: separate paradigms from tactical patterns
-- [X] [#B] =arch-document=: arc42/Q42 quality scenarios
-- [X] [#B] =arch-document=: staleness + ownership metadata for generated docs
-- [X] [#B] =arch-evaluate=: confidence levels for framework-agnostic findings
-- [X] [#B] =arch-evaluate=: report skipped tool checks explicitly
-
-**** C4 modeling
-- [X] [#A] =c4-analyze= + =c4-diagram=: notation/output fallback (not draw.io-only)
-- [X] [#B] =c4-analyze= + =c4-diagram=: clarify abstraction boundaries
-
-**** Global rules
-- [X] [#B] =commits.md=: split DeepSat/Linear/Slack-specific from global rules → promoted to a top-level task (deferred for Craig)
-- [X] [#A] =commits.md= + publish flows: =humanizer=-unavailable fallback → promoted to a top-level task (deferred; humanizer premise moot)
-- [X] [#B] =verification.md=: explicit "unable to verify" reporting standard
-- [X] [#B] =testing.md=: property-based + mutation testing as escalation paths
-- [X] [#B] =testing.md=: soften absolute TDD with explicit spike protocol
-- [X] [#B] =subagents.md=: capability/availability + cost checks
-
-**** Languages
-- [X] [#A] =python-testing.md=: revisit in-memory SQLite guidance
-- [X] [#B] =python-testing.md=: separate "never mock ORM" from unit-test boundaries
-- [X] [#B] =elisp.md=: drop tool-specific advice
-- [X] [#B] =elisp-testing.md=: batch-mode + native-comp caveats
-
-**** Hooks
-- [X] [#A] =hooks/README.md=: include =destructive-bash-confirm.py= in install/settings snippets
-- [X] [#A] =hooks/git-commit-confirm.py= + =gh-pr-create-confirm.py=: inspect message/body files referenced by =-F= / =--body-file=
-- [X] [#B] =hooks/destructive-bash-confirm.py=: shell-aware command parsing (not regex)
-
-*** 2026-05-22 Fri @ 15:47:10 -0500 Made playwright guidance locator/assertion-first, dropped networkidle-as-readiness
-
-Rewrote the readiness guidance in both =playwright-js/SKILL.md= and =playwright-py/SKILL.md=: reconnaissance now waits for a visible app landmark via a web assertion or locator (=expect(...).toBeVisible()= / =get_by_role(...).wait_for()=), not =networkidle= (which Playwright discourages). Updated the login/form examples to =getByLabel=/=getByRole= + web assertions, the API_REFERENCE.md waiting section, and =lib/helpers.js= defaults (=waitForPageReady= now defaults to =load= and prefers a caller-supplied landmark; =authenticate= races the success indicator over a =load= navigation). node --check passes.
-
-*** 2026-05-22 Fri @ 14:23:02 -0500 Added headed/headless decision tables to both playwright skills
-
-Added matching purpose-based decision tables to =playwright-js/SKILL.md= (was "always visible") and =playwright-py/SKILL.md= Best Practices (was "always headless"). Each names its own default and points at the other skill, so the difference is deliberate, not a habit-flip: headed for interactive debugging, headless for CI/pytest. Also softened the absolutist "Always launch... headless" comment in the py example.
-
-*** 2026-05-22 Fri @ 15:47:10 -0500 Removed emoji console markers from the playwright skills
-
-Replaced every emoji status marker with a plain ASCII prefix across =playwright-js/= (run.js, lib/helpers.js, SKILL.md) and =playwright-py/= (SKILL.md, examples/*.py): 📦/⚡/📄/📥/🎭/🚀/📋/✅/❌/🔍/📸/✓/✗ → =[setup]=/=[run]=/=[ok]=/=[error]=/=[fail]= etc. Post-change emoji grep is clean (excluding node_modules); node --check and py_compile pass.
-
-*** 2026-05-22 Fri @ 14:35:16 -0500 Made accessibility a non-optional WCAG 2.2 gate in frontend-design
-
-Added an "Accessibility Gate (required before handoff)" section to =frontend-design/SKILL.md= covering keyboard operation, focus visibility, focus-not-obscured (2.2), target size (2.2), contrast, reduced motion, labels, and semantic structure — a baseline for all frontend work, not just interactive components. Rewrote the Build/Review phases to build accessibly as you go and clear the gate before handoff, and bumped =references/accessibility.md= from WCAG 2.1 to 2.2 with backing detail for the new criteria.
-
-*** 2026-05-22 Fri @ 14:35:16 -0500 Added a "creative but bounded" section to frontend-design
-
-Added a subsection under Frontend Aesthetics framing the bold/maximalist directions as tools, not obligations: domain fit, readability first, responsive stability, and no decorative effect that degrades the workflow. Reconciles rather than contradicts the maximalist encouragement (maximalism stays on the table as deliberate usable density), and ties the readability bullet to the new accessibility gate.
-
-*** 2026-05-22 Fri @ 14:35:16 -0500 Updated security-check to OWASP Top 10 2021 + WSTG mapping
-
-Replaced the older six-category list in =.claude/commands/security-check.md= with the full Top 10 2021 set, each finding mapped to a 2021 category or WSTG area. Added the four missing categories (Insecure Design, Software and Data Integrity Failures, Security Logging and Monitoring Failures, SSRF) plus explicit checks for object/function-level authorization, SSRF on URL-fetch paths, update/plugin/dependency integrity, and logging/monitoring gaps.
-
-*** 2026-05-22 Fri @ 14:35:16 -0500 Added scanner tooling + network caveats to security-check
-
-Added an optional configured-scanners step (=gitleaks=/=trufflehog= secrets, =semgrep= source patterns, OSV scanner, lockfile-diff review) that supplements the manual scans, plus a network caveat: dependency audits that can't run (offline, tool absent, DB unreachable) must report "not run" naming the tool and reason, never read as a pass. Carried that into the no-issues summary.
-
-*** 2026-05-22 Fri @ 14:35:16 -0500 Added t-way escalation guidance to pairwise-tests
-
-Added an "Escalating Beyond Pairwise (t-way)" subsection: start with pairwise across the whole space, then escalate specific high-risk clusters to 3-way+ when history, safety, security, or domain coupling says a fault needs more than two interacting factors. Lists escalation triggers and shows the sub-model order syntax (={ A, B, C } @ 3=) vs a blanket =/o:3= bump, stressing targeted not uniform escalation. Cites NIST combinatorial-testing work.
-
-*** 2026-05-22 Fri @ 14:35:16 -0500 Clarified PICT ~ syntax + honest generator-availability path in pairwise-tests
-
-Added a "~ prefix" explanation (PICT marker tagging a value as negative/invalid, not an arithmetic operator; PICT pairs negatives with valid values once and strips the marker before the SUT) and a stop-at-the-model rule: if neither the =pict= binary nor =pypict= is present, produce the model and stop rather than hand-writing a table and passing it off as PICT output.
-
-*** 2026-05-22 Fri @ 14:43:17 -0500 Renamed Metrics → Measures throughout create-v2mom
-
-Full rename across =.claude/commands/create-v2mom.md= (acronym expansions, Phase 7 heading, the "Measures must be measurable" principle, exit criteria, review questions, red flags, examples) to match Salesforce's official term. Kept the "vanity metrics" idiom intact — it's the anti-pattern term, not a section reference.
-
-*** 2026-05-22 Fri @ 14:43:17 -0500 Split strategy from execution in create-v2mom task migration
-
-Rewrote Phase 8 (and tightened Phase 5.5): tasks stay in the backlog grouped by method, and each method gains a one-line link to where its tasks live, instead of transplanting the task tree into the V2MOM. Strategy (V2MOM) and execution (backlog) are now explicitly separate sources of truth, keeping the V2MOM concise.
-
-*** 2026-05-22 Fri @ 14:43:17 -0500 Made create-v2mom obstacles operational (mitigation/owner/cadence)
-
-Phase 6 now captures, per obstacle: name, manifestation, stakes, mitigation, owner, and review cadence — with a worked example per domain (health/finance/software), a "good obstacle" characteristic, a Phase 9 review question, and a red flag for candid-but-not-operational obstacles. An obstacle without a countermove is now flagged as an observation, not a plan.
-
-*** 2026-05-22 Fri @ 14:43:17 -0500 Corrected and narrowed the Meincke citation in prompt-engineering
-
-Fixed the title to "Call Me A Jerk: Persuading AI to Comply with Objectionable Requests" (SSRN abstract_id=5357179) in all three spots (frontmatter, Seven Principles intro, References). Reframed the ~33%→72% result as what it is — a prompt-safety caution that persuasion raises compliance with objectionable requests — explicitly not evidence that persuasion framing improves engineering prompt quality. Kept the seven principles as a tone vocabulary.
-
-*** 2026-05-22 Fri @ 14:43:17 -0500 Added an eval-harness requirement to prompt-engineering critique mode
-
-Added critique step 7 + a checklist line: for fragile or reusable/production prompts, write 3-5 adversarial/edge inputs, run both the old and new prompt against each, and record the behavioral delta. A throwaway prompt can ship on the rewrite alone; a discipline/reused/production one can't. Without examples, "the rewrite is better" is an assertion, not a result.
-
-*** 2026-05-22 Fri @ 14:43:17 -0500 Added mandatory stale-entry + privacy pre-write checks to codify
-
-Added a "Mandatory pre-write checks" block at the top of Phase 3 (Write) in =.claude/commands/codify.md=: a stale-entry scan (update/remove no-longer-true entries in place, don't append contradictions around them) and a privacy/leak check carrying both questions verbatim — "safe if the project were public?" and "belongs in private memory instead?" — routing private content to auto-memory. Gates, not background guidance.
-
-*** 2026-05-22 Fri @ 14:06:41 -0500 Scoped review-code's CI-trust rule to reviewing, not shipping
-
-Expanded the False-Positive Filter bullet in =review-code/SKILL.md=: "trust CI, don't run builds" applies to reading a diff, not producing one. A pre-commit/pre-push flow still owes the local verification =verification.md= requires (run the suite or state "not run because..."). Closes the apparent contradiction with =verification.md= / =finish-branch=.
-
-*** 2026-05-22 Fri @ 14:06:41 -0500 Added private-vs-public CLAUDE.md citation modes to review-code
-
-Expanded the Content scope section in =review-code/SKILL.md= with two modes: a private/internal review cites =CLAUDE.md= directly; a public/team review translates the rule into the engineering reason it encodes and doesn't name the rules file (a teammate can act on the reason, not on a file they can't reach). Same principle =commits.md= states for personal tooling in public artifacts.
-
-*** 2026-05-22 Fri @ 13:48:14 -0500 Relaxed review-code "three strengths" to up-to-three-or-none
-
-Changed all three "three minimum" spots in =review-code/SKILL.md= (Strengths section, Critical Rules DO list, Anti-Patterns) to "up to three specific; say none found on a tiny or weak diff." Reframed the old "No Strengths section" anti-pattern as "Skipping strengths out of laziness" so a substantive diff still demands them while a weak one can honestly report nothing notable. Landed alongside Craig's adjacent edit telling reviewers not to explain why a strength is good (sycophantic padding).
-
-*** 2026-05-22 Fri @ 14:12:24 -0500 Removed review-process language from respond-to-review commit guidance
-
-Replaced the =fix: Address review — [description]= example (and the matching description-line phrasing) in =.claude/commands/respond-to-review.md= with "name the actual fix (=fix: validate export filename=), not the review that prompted it." Killed the non-ASCII dash and the process-in-commit pattern that conflicted with =commits.md=.
-
-*** 2026-05-22 Fri @ 14:12:24 -0500 Made respond-to-review fetch unresolved threads + resolve after verification
-
-Rewrote section 1 (Gather) in =.claude/commands/respond-to-review.md= to pull =reviewThreads= via =gh api graphql= with =isResolved=, skipping already-resolved threads so settled feedback isn't re-processed; top-level conversation comments still come from REST. Added a section-4 step: reply and resolve a thread only after the fix is verified, never before.
-
-*** 2026-05-22 Fri @ 14:12:24 -0500 Verified respond-to-cj-comments no longer embeds an absolute path (moot)
-
-Already resolved by a prior migration: =grep= for =/home/= and =/Users/= in =.claude/commands/respond-to-cj-comments.md= returns nothing. The public-writing section refers to the rules by name, not by local path. No edit needed.
-
-*** 2026-05-22 Fri @ 14:12:24 -0500 Closed respond-to-cj-comments humanizer/emacsclient fallback (largely moot)
-
-Overtaken by two later changes: =/humanizer= was replaced by =/voice personal= (no =/humanizer= invocation remains), and the mandatory =emacsclient= summary-open was replaced by the in-place VERIFY-task pattern (workflow line ~262, Craig's 2026-05-12 standing instruction). Only a stale descriptive phrase remained — tidied "humanizer's signs of AI writing" to "the signs of AI writing." The original fresh-environment-fallback concern no longer applies as written.
-
-*** 2026-05-22 Fri @ 14:51:37 -0500 Fixed finish-branch base-branch detection
-
-Rewrote Phase 2: resolve the base *branch name* in priority order (open PR's =baseRefName=, then =git symbolic-ref --short refs/remotes/origin/HEAD= stripped, then ask), and compute the merge-base *SHA* separately only where a commit range is needed. Made the branch-name-vs-merge-base distinction explicit, since the old command returned a SHA where a branch name was needed.
-
-*** 2026-05-22 Fri @ 14:51:37 -0500 Made finish-branch merge safer + worktree-aware
-
-Added pre-flight checks to Option 1 (Merge Locally): dirty-tree refusal with no auto-stash, protected-branch awareness, upstream-gated =git pull --ff-only=, and merge-commit-vs-rebase as a team-policy choice instead of a hardcoded =--no-ff=. Replaced the fragile =git worktree list | grep <branch>= detection with a =git rev-parse --git-dir= vs =--git-common-dir= comparison plus =git worktree list --porcelain= for the path.
-
-*** 2026-05-22 Fri @ 14:51:37 -0500 Added tool-availability + ceremony-scale paths to start-work
-
-Added a "Tool availability" section (graceful degradation when Linear MCP / =gh= / =/voice= / Playwright are missing — do what's available, surface what isn't, don't block) and a "Ceremony scale" section (trivial / small / standard tiers so a two-line fix skips ticket+branch+gates unless asked). The =humanizer= reference in the original item is moot — the file already uses =/voice= throughout.
-
-*** 2026-05-22 Fri @ 14:51:37 -0500 Resolved start-work claim-before-justify rollback risk
-
-Split the claim by tracker type: personal todo.org claims defer to after the Justify gate (a killed task needs no rollback), while team trackers (Linear/GitHub) still claim first to signal intent but record prior state (status, assignee, label) so the Phase 2 rollback restores exactly it. Updated the per-tracker rollback steps and the matching anti-pattern.
-
-*** 2026-05-22 Fri @ 14:28:41 -0500 Verified add-tests typescript-testing.md reference resolves (moot)
-
-Resolved since the audit: =languages/typescript/claude/rules/typescript-testing.md= now exists, and =add-tests/SKILL.md:68= references it by bare filename, the same way it references =python-testing.md= (both get copied into a project's =.claude/rules/=). The "missing file" premise no longer holds. No edit needed.
-
-*** 2026-05-22 Fri @ 14:28:41 -0500 Added a category-exception protocol to add-tests
-
-Added an exception note to step 7 (proposal) in =add-tests/SKILL.md=: pure adapters, generated code, tiny pass-through wrappers, and framework glue may skip a category that would only re-test the framework, but the skip must be stated and justified in the plan and the behavior covered at integration/E2E level — never a silent omission. Step 12 (write) now points back to "honor documented category exceptions."
-
-*** 2026-05-22 Fri @ 14:25:37 -0500 Added environment + recent-change capture to debug Phase 1
-
-Added a fourth Phase-1 step in =debug/SKILL.md=: record versions, feature-flag/config state, dataset/fixture, seed/clock, concurrency, and recent commits/config-infra changes. Noted that intermittent bugs usually live in environment/state transitions (and "what changed recently" is often the fastest route), while a deterministic local bug only needs a one-liner. Updated the phase's closing recap to include the context.
-
-*** 2026-05-22 Fri @ 14:25:37 -0500 Constrained root-cause-trace defense-in-depth to boundaries
-
-Rewrote step b in =root-cause-trace/SKILL.md=: instead of "add a check at each layer that could have caught it," add one only at a layer that owns a boundary or invariant — ingress/trust, persistence, invariant-owning service, final render. Added the explicit rule that a pass-through function owning neither shouldn't get a duplicate null check (validation spam). Recast the three example layers as the boundary types.
-
-*** 2026-05-22 Fri @ 14:25:37 -0500 Required evidence + counterfactual per why in five-whys
-
-Expanded step 2 in =five-whys/SKILL.md=: each link now owes an evidence field (a log/commit/metric/config you can point to) and a counterfactual check (remove this cause — does the symptom above plausibly not happen?). Framed the counterfactual as the main guard against monocausal storytelling, and updated the worked example to show both fields.
-
-*** 2026-05-22 Fri @ 15:51:59 -0500 Added timebox + fresh-sources rules to brainstorm
-
-Phase 1 gained a "Timebox the dialogue" rule (aim for the one-sentence restatement in ~5-8 questions, then move on and park the rest as open questions). Phase 2 gained "Ground high-stakes claims in fresh sources" (check load-bearing claims about markets/regulations/tools/vendors/APIs against a current source; mark unverified ones as assumptions). The design-doc skeleton gained an "## Assumptions" section that distinguishes researched facts (with source) from assumptions (to confirm before building).
-
-*** 2026-05-22 Fri @ 14:59:32 -0500 Made arch-decide examples timeless + required citations
-
-Dated the MongoDB multi-document-transaction example (scoped to 2024-01) with a backing reference, and added a "Cite, don't assert" Do: every concrete technical claim about a tool/version/platform carries a link, doc, version, or "checked YYYY-MM" date, or gets a domain-neutral placeholder — so unsourced "X can't do Y" doesn't rot into stale fact.
-
-*** 2026-05-22 Fri @ 14:59:32 -0500 Standardized arch-decide ADR statuses + immutability rule
-
-Declared a canonical five-status set (Proposed, Accepted, Rejected, Deprecated, Superseded) with an explicit "no synonyms" line, and spelled out the immutability rule in the Don'ts: an accepted ADR's body is frozen, only status/link metadata changes, a changed decision gets a new superseding ADR and the old one stays as the historical record.
-
-*** 2026-05-22 Fri @ 14:59:32 -0500 Added Trust/Data/Compliance phase to arch-design
-
-Added a new Phase 4 (Trust, Data, and Compliance) before the paradigm shortlist: trust boundaries, data classification, abuse/misuse cases, privacy constraints, compliance evidence, and operational ownership — surfaced early so the architecture is drawn around them, not retrofitted by a downstream =security-check=. Threaded into the workflow list, brief template (new §6), review checklist, and anti-patterns.
-
-*** 2026-05-22 Fri @ 14:59:32 -0500 Split paradigms from tactical patterns in arch-design
-
-Split Phase 5's single mixed table into Step 1 (pick one paradigm: monolith/microservices/layered/event-driven/serverless/pipeline/space-based) and Step 2 (compose tactical patterns: DDD, hexagonal, CQRS, event sourcing — several or none, often per-module), with composition examples and an anti-pattern against treating DDD/CQRS as alternatives to a paradigm. Recommendation + brief now name a paradigm plus composed patterns.
-
-*** 2026-05-22 Fri @ 14:59:32 -0500 Expanded arch-document quality scenarios to the Q42 six-part template
-
-Replaced §10's thin "Under [condition]..." template with the arc42/Q42 six-part structure (source, stimulus, environment, artifact, response, response measure), each glossed, with the cart-checkout example rewritten across all six parts. A one-line prose form stays acceptable once all six parts are recoverable.
-
-*** 2026-05-22 Fri @ 14:59:32 -0500 Added staleness/ownership metadata to arch-document output
-
-Added a per-section metadata block (owner, generated-against SHA + date, review cadence, "stale-when" conditions) as an HTML-comment header plus a visible Doc-status note, with field-fill guidance, and a whole-document Doc Status table replacing the README's "Last Updated" stub. Wired into the review checklist and an "Undated docs" anti-pattern.
-
-*** 2026-05-22 Fri @ 14:59:32 -0500 Added confidence levels to arch-evaluate findings
-
-Added a "Confidence and Provenance" subsection: every framework-agnostic finding carries High/Medium/Low + how it was determined, with a required "Not fully checked because..." note when scale, runtime imports, reflection, or dynamic dispatch cap certainty. Updated the example findings and review checklist; a finding with no note now asserts a full read.
-
-*** 2026-05-22 Fri @ 14:59:32 -0500 Made arch-evaluate report skipped tool checks explicitly
-
-Replaced "skip silently" with explicit reporting: for each detected language whose tool isn't configured or can't run, emit an Info "tool not configured / not run" finding (with an example) so the audit shows what was and wasn't verified. A check that didn't run no longer reads as a pass. Updated workflow step 4 and the review checklist.
-
-*** 2026-05-22 Fri @ 14:51:37 -0500 Added notation/output fallback to c4-analyze + c4-diagram
-
-Both commands now treat C4 as notation-independent: a "Choosing a notation" section (draw.io XML, Structurizr DSL, Mermaid with native C4 types, PlantUML/C4-PlantUML) and a headless fallback that emits a text notation (Mermaid or Structurizr DSL) and skips PNG-export/desktop-open when =drawio= or a GUI is absent, rather than failing. draw.io is now one option, not the only one.
-
-*** 2026-05-22 Fri @ 14:51:37 -0500 Clarified C4 abstraction boundaries in c4-analyze + c4-diagram
-
-Added an "Abstraction boundaries" section to both: a Container is a separately deployable/runnable unit (not synonymous with a Docker container — a SPA or managed DB counts), a Component lives inside one Container and isn't separately deployable. Added a 4e "Verify single abstraction level" check that walks every element and relationship to confirm it stays at the diagram's level, notation-independent.
-
-*** 2026-05-22 Fri @ 15:10:35 -0500 Added "When You Cannot Verify" standard to verification.md
-
-Added a section requiring, when a verification command can't run, a four-part report: command attempted, why it couldn't run, risk left unverified, and the smallest next command for the user. States the principle that a check that didn't run is never reported as a pass — "unable to verify" is a required honest outcome, not silence. Placed after Red Flags.
-
-*** 2026-05-22 Fri @ 15:10:35 -0500 Added property-based + mutation testing escalation to testing.md
-
-Added an "Escalation Beyond Category and Pairwise" section: property-based testing for invariants over a broad input domain (round-trips, idempotence, ordering — Hypothesis/fast-check/proptest) and mutation testing for when high line coverage hides thin assertions (mutmut/cosmic-ray/Stryker). Both framed as escalation paths to reach for on a gap, not gates on every unit.
-
-*** 2026-05-22 Fri @ 15:10:35 -0500 Added a disciplined spike protocol to testing.md
-
-Formalized the existing "I need to spike first" excuse-table row into a "Spike Exception (Disciplined)" subsection under TDD Discipline: TDD stays the default, but a spike is sanctioned when all three hold — timeboxed, spike code not committed, and the first failing test written before productionizing the discovered approach. Built on the existing row rather than contradicting it.
-
-*** 2026-05-22 Fri @ 15:10:35 -0500 Added pre-dispatch availability + cost checks to subagents.md
-
-Added a "Pre-Dispatch Checks" section with two gates: Availability (no Agent capability → do the work in the main thread under the same scope/constraints/output discipline the contract would enforce) and Cost (when writing the full contract costs more than the task, do it inline). Cross-references the existing "Don't Subagent At All" section and "Subagenting trivial work" anti-pattern rather than duplicating.
-
-*** 2026-05-22 Fri @ 15:06:04 -0500 Revised python-testing SQLite guidance toward production-like DBs
-
-Replaced "prefer in-memory SQLite for speed" with: run ORM/query tests against a production-like DB (same engine as prod, often containerized), since SQLite diverges from Postgres/MySQL on query semantics, constraints, transactions, JSON, time zones, and indexes (a test can pass on SQLite and fail in prod). SQLite stays only for pure unit tests with no DB-semantics dependency.
-
-*** 2026-05-22 Fri @ 15:06:04 -0500 Clarified python-testing ORM-mocking boundary
-
-Changed the "never mock" bullet from "ORM queries" to "ORM internals (querysets, sessions, model internals)" and added a paragraph: domain services use real model methods/validation, but a thin orchestration unit can inject a fake at a deliberate data-access port (a repository/interface the code owns). That's still mocking at a boundary, not at ORM internals.
-
-*** 2026-05-22 Fri @ 15:06:04 -0500 Made elisp.md editing advice tool-agnostic
-
-Rephrased the "prefer Write over repeated Edits" bullet around intent: land nontrivial Elisp as one cohesive change rather than dribbling it in over tiny partial edits (which accumulate paren mismatches), and run paren-balance + byte-compile checks immediately after, whatever editing mechanism the environment uses.
-
-*** 2026-05-22 Fri @ 15:06:04 -0500 Added batch-mode + native-comp caveats to elisp-testing.md
-
-Added three sections: Batch-Mode Reproducibility (=emacs --batch= as source of truth, no interactive-session state, no blocking prompts, deterministic), Isolating Emacs State (temp =user-emacs-directory=, explicit load-path, declared deps only, with an unwind-protect sandbox example), and Byte-Compile/Native-Comp Warnings (=byte-compile-error-on-warn=, native-comp gated on =native-comp-available-p= and kept opt-in/version-aware).
-
-*** 2026-05-22 Fri @ 15:16:22 -0500 Synced hooks/README install snippets with the destructive hook (opt-in)
-
-Brought the README's manual-install and settings-JSON snippets in line with the canonical =hooks/settings-snippet.json= (which already wires all three) and the Makefile's opt-in design: added the destructive-bash-confirm.py symlink as an opt-in step, added its settings entry, and reworded the note to say all three are no-op-safe but the destructive gate is opt-in (=make install-hooks= excludes it by default — link manually before relying on the snippet entry).
-
-*** 2026-05-22 Fri @ 15:35:06 -0500 Hooks now scan file-backed commit/PR messages
-
-Added =read_referenced_file()= to =_common.py= (safe local read: missing/oversize/non-UTF-8 → None) and wired it in: =git-commit-confirm.py= =extract_commit_message= now handles =-F=/=--file=/=--file===<path>= (reads + scans the file, falls through to UNPARSEABLE → asks if unreadable), and =gh-pr-create-confirm.py= reads =--body-file= content instead of a placeholder. Attribution scanning now sees the real committed/posted text. Built a pytest harness (=hooks/tests/=, importlib-by-path loader for the hyphen-named hooks) and wired =hooks/tests= into =make test=. 54 hook tests pass; full suite green.
-
-*** 2026-05-22 Fri @ 15:35:06 -0500 Rewrote destructive-bash rm parsing on shlex
-
-=detect_rm_rf= now tokenizes with =shlex.split= instead of a whitespace split, so quoted/spaced paths and combined/separate/reordered flags (=-rf=, =-r -f=, =-fr=, =--recursive=/=--force=) all parse. Fails toward asking — returns a sentinel that still fires the modal — on unbalanced quotes or when a forced recursive rm coexists with a compound/pipeline/substitution/redirect construct. Documented the supported/unsupported shell constructs in the docstrings, and extended the dangerous-path banner to =$HOME=-prefixed and wildcard targets. Covered by 25 new tests. (Pre-existing, out-of-scope: path-prefixed =rm= like =/bin/rm= still isn't matched.)
-** DONE [#B] Add =make remove= for interactive ruleset removal via fzf
-CLOSED: [2026-05-22 Fri]
-Shipped: =scripts/remove.sh= (three modes — =--list=, =--remove-selected= reading stdin, and the default fzf-multi interactive flow) + =make remove= target + =scripts/tests/remove.bats= (5 cases). Lists only symlinks resolving into the repo (foreign links left alone); rm's picked links while leaving repo sources untouched; reports-and-continues on a missing target; quiet no-op on empty selection. shellcheck clean, make test green. Dropped the stale =bridge= entry per the note below.
-
-Add a Makefile target that lists every currently-installed ruleset entry
-and lets me pick one or more to remove via fzf. Granular alternative to
-=make uninstall= (removes everything) and =make uninstall-hooks= (removes
-only hooks).
-
-*** Why this matters
-
-Tearing down a single skill, rule, hook, or config file currently means
-either running =make uninstall= and re-installing what I want to keep,
-or =rm=ing the symlink directly and remembering the exact path. Both are
-friction. An interactive picker lets me filter, multi-select with Tab,
-and confirm with Enter — the typical fzf flow. Costs about 3-5 seconds
-per teardown instead of 15+ seconds of "what's the exact name?".
-
-*** Design
-
-The recipe builds a tab-separated list of every currently-installed item,
-categorized by type, and pipes it to =fzf --multi=. The user filters,
-marks with Tab, and confirms with Enter. The recipe parses the selections
-and =rm=s the matching symlinks.
-
-#+begin_example
- skill debug
- rule commits.md
- hook destructive-bash-confirm.py
- config settings.json
- commands commands
- bridge claude-rules
-#+end_example
-
-Each line is =<kind>\t<name>=. The recipe maps =<kind>= to the right path:
-
-- =skill= → =$(SKILLS_DIR)/<name>=
-- =rule= → =$(RULES_DIR)/<name>=
-- =hook= → =$(HOOKS_DIR)/<name>=
-- =config= → =$(CLAUDE_DIR)/<name>=
-- =commands= → =$(CLAUDE_DIR)/commands=
-- =bridge= → =$(SKILLS_DIR)/claude-rules=
-
-Source files in =rulesets/= stay untouched. =make install= re-creates the
-removed links if needed (the install loop is idempotent).
-
-*** Edge cases
-
-- Esc instead of Enter → empty selection → clean exit, no removal.
-- Filter to nothing then Enter → same as Esc.
-- Selected item already gone → =rm= fails visibly, processing continues
- on the rest.
-- =fzf= not installed → fail fast with a clear error (matches the pattern
- used by =install-lang=).
-
-*** Possible extensions
-
-- Parallel =make pick-install= target that lists not-yet-installed items
- and installs the chosen ones. Symmetric UX, same fzf flow.
-- Confirmation prompt when more than N items selected (defense against
- accidental select-all).
-- =--source= flag that also runs =git rm= against the rulesets source for
- the selected item. Probably bad idea — too easy to lose work.
-- The =bridge → $(SKILLS_DIR)/claude-rules= entry above is stale — the
- bridge symlink got removed in a later commit. Drop that bullet when the
- recipe lands.
-** DONE [#B] Document the =mcp/= install pipeline in =mcp/README.org=
-CLOSED: [2026-05-22 Fri]
-Wrote =mcp/README.org= covering everything in the "what to cover" list: the file layout (tracked vs gitignored), the secrets-bundle shape (plain =${VAR}= secrets + base64-bundled OAuth artifacts, AES256 symmetric =gpg -c=), the install flow (decrypt → materialize keys/token caches at mode 600 → expand → register unregistered, idempotent), the http/sse-vs-stdio transport split, token rotation when a Google refresh token is revoked, and adding a new server. Grounded in a read of the actual =install.py= + =servers.json=.
-
-=mcp/= has =install.py=, =servers.json=, =secrets.env.gpg=, =gcp-oauth.keys.json= (gitignored, regenerated at install). No README. Coming back to this in three months I'll re-discover how the bundle is structured, what =install.py= does, and how to rotate tokens. Saving that re-discovery is the whole point.
-
-*** What to cover
-
-- Layout: what each file is, which are tracked vs gitignored.
-- Secrets bundle shape: how vars are listed in =secrets.env=, the symmetric-encryption pattern (=gpg -c --cipher-algo AES256=), the base64-bundled OAuth artifacts (=GCP_OAUTH_KEYS_JSON_B64=, =GOOGLE_DOCS_PERSONAL_TOKEN_B64=, =GOOGLE_DOCS_WORK_TOKEN_B64=).
-- Install flow: =make install-mcp= → =install.py= decrypts, writes the keys file and Google Docs token caches at mode 600, expands =${VAR}= in =servers.json=, calls =claude mcp add --scope user= for unregistered servers. Idempotent.
-- Token rotation: when a refresh token gets revoked, the recovery flow (re-auth on one machine, re-bundle, recommit).
-- Adding a new server: edit =servers.json=, add any new =${VAR}= placeholders to the bundle, re-encrypt.
-- The OAuth dance for HTTP-transport servers (linear, notion) versus stdio (google-docs-*) — different paths, different gotchas.
-** DONE [#C] Add =make uninstall-mcp= + =mcp/install.py --check= for symmetry :feature:solo:quick:
-CLOSED: [2026-05-28 Thu]
+Built the sentry supervisor workflow from the spec
+([[file:docs/specs/2026-07-14-sentry-workflow-spec.org][sentry workflow spec]], now IMPLEMENTED). Four phases, each committed and
+pushed in no-approvals + auto-flush mode; full suite green throughout. The overnight
+live trial is handed to Craig as a manual-testing task (below); its findings file as
+follow-ups.
+*** 2026-07-19 Sun @ 04:52:00 -0500 Built the agent-lock helper + 18 bats tests
+=.ai/scripts/agent-lock= (canonical =claude-templates/.ai/scripts/=, mirror synced):
+mkdir-atomic acquire, PID/host/ISO-timestamp metadata, mtime-based staleness reclaim
+(atomic-rename claim so two acquirers can't double-acquire — caught by the pre-commit
+review), heartbeat refresh, acquire/release/status/path subcommands, XDG_RUNTIME_DIR
+home with =~/.cache= fallback. Commit a8b6cf4.
+*** 2026-07-19 Sun @ 04:56:00 -0500 Built the sentry.org engine + INDEX entry
+=.ai/workflows/sentry.org= (mirror synced): :COMMIT_AUTONOMY: entry ticket, the
+interactive entry gates, ff-only reconcile, =sentry/<date>-<host>= branch mechanics,
+the ten-pass probe→work→session-context→commit runner, digest + morning-approval
+queue, skip-not-degrade safety, spine-excluded dirty checks + fire-end digest commit,
+multi-day stall notify, the stop-sentry operation. All 10 decisions and 12 findings
+reflected. Commit ccc9c26.
+*** 2026-07-19 Sun @ 05:00:00 -0500 Wired the roam writers + wrap-up guard
+knowledge-base.md and inbox.org core §5 acquire the roam-write lock and edit-plus-
+trigger (roam-sync stays sole committer); roam-sync.sh header updated to match;
+wrap-it-up.org gained a Step 0 active-sentry guard; triage-intake.org notes its
+sentry-pass role. Graceful degradation when agent-lock is absent. Commit c6383e9.
+*** 2026-07-19 Sun @ 05:04:00 -0500 Verified suite green + flipped spec to IMPLEMENTED
+=make test= green at HEAD (pytest 393, ERT + bats all pass, exit 0). Flipped the spec
+keyword DOING → IMPLEMENTED with a dated history line and mirrored the Metadata Status.
+Filed the overnight live trial as a structured manual-testing task.
+** DONE [#C] ai launcher hardening — bug hunt + refactor pass :refactor:solo:
+CLOSED: [2026-07-19 Sun]
:PROPERTIES:
-:LAST_REVIEWED: 2026-05-28
+:CREATED: [2026-07-13 Mon]
+:LAST_REVIEWED: 2026-07-19
:END:
+Resolved across two commits (113e8d8 net, 2b619f1 refactor). Brought the 17 uncovered functions under characterization tests (launcher tests 9 → 42), extracted the git/tmux decision logic into four pure cores (_git_prep_action, _order_windows, _match_window_id, _git_is_dirty) each with a Normal/Boundary/Error set, and dispositioned the footgun audit + /refactor pass. Objective floor met: shellcheck clean, shfmt -i 2 -ci consistent, make test green before and after, live black-box smoke correct for claude and codex. Honest limit: attach_session and the full end-to-end of single/multi/fetch stay partially covered (their terminal step attaches to tmux or blocks on fzf, can't run headless). Their decision logic was extracted into the netted cores. The interactive runtime picker stayed out of scope (a design call, filed on the generic-agent-runtime parent).
+Harden =claude-templates/bin/ai= (the agent-session launcher, 540 lines, 22 functions). Origin: roam inbox 2026-07-13, phrased open-endedly ("find bugs until none visible, refactor until nothing worthwhile remains"). Rescoped 2026-07-19 with measurable acceptance criteria per =todo-format.md='s "Making an open-ended task measurable," which is what makes it =:solo:=. The four moves:
-Currently the MCP install pipeline only flows one direction. No way to remove rulesets-managed MCP servers in one command. No way to ask "what's the drift between =servers.json= and =claude mcp list=" without eyeballing.
+1. *Bound the surface.* The 22 functions are the done-set. *Covered* (behaviorally, via =scripts/tests/ai-launcher-runtime.bats=, 9 tests over the runtime path): =resolve_agent_cmd=, =build_runtime_choices=, =pick_runtime=, =build_instructions=, the print modes. *Uncovered* (the 17 to bring under test): =usage=, =check_deps=, =attach_session=, =create_window=, =maybe_add_candidate=, =build_candidates=, =fetch_candidates=, =git_status_indicator=, =annotate_candidates=, =auto_pull_if_clean=, =read_selections=, =sort_windows=, =find_window_id=, =prep_git_single=, =attach_mode=, =single_mode=, =multi_mode=, =print_launch_mode=.
-*** =make uninstall-mcp=
+2. *Net the behavior.* Characterization tests (Normal/Boundary/Error per unit, per =testing.md=) over the uncovered surface. The pure/near-pure ones take the category set directly: =git_status_indicator=, =maybe_add_candidate= (dedup), =annotate_candidates= (formatting), =read_selections= (selection parse), =usage=. The =tmux=/=git=-coupled ones (=sort_windows='s ordering, =create_window=, =attach_session=, =find_window_id=, =prep_git_single=, =auto_pull_if_clean=) get their pure decision logic extracted into helpers that take plain inputs and return plain results — that extraction *is* the hardening — with the I/O calls left as thin wrappers.
-Iterate over =servers.json=, run =claude mcp remove <name> -s user= for each. Ignore "not registered" errors. Idempotent.
+3. *Disposition every finding.* (a) A bash-footgun audit per function, each cell fixed / n-a / filed: unquoted expansions + word-splitting, =set -euo pipefail= gaps and where errexit is intentionally off, subshell state loss, exit-code propagation, ordering/races in =sort_windows= + window creation, and the git-prep error paths (=prep_git_single= / =auto_pull_if_clean= on a dirty tree, detached HEAD, no upstream). (b) A =/refactor= pass, each finding applied (tests green) or declined with a one-line reason.
-*** =mcp/install.py --check=
+4. *Objective floor.* =shellcheck= clean, =shfmt=-consistent, =make test= green before and after, and every uncovered function above has its characterization set (a per-function checklist — =kcov= isn't installed; install it if a single coverage number is wanted). Plus ~3 functional tests over the launch pipelines (=single_mode=, =multi_mode=, =attach_mode=) against a throwaway =tmux= session, for the composition bugs no per-function unit can see.
-Dry-run mode. Decrypt secrets, but instead of registering, print the drift report:
+*Qualifying answer:* a dispositioned report — surface split covered/uncovered, tests before → after, =shellcheck=/=shfmt= result, the footgun matrix fully dispositioned, the =/refactor= findings fully dispositioned, all green. Not "no bugs remain" (unprovable) — "every enumerated path passes its characterization set and clears the audit."
-- Servers in =servers.json= not in =claude mcp list= → =MISSING=
-- Servers in =claude mcp list= not in =servers.json= → =EXTRA=
-- Servers in both → =ok=
-
-Useful for diagnosing connection failures and for the eventual =make doctor= integration.
-** DONE [#C] Update =README.org= with MCP install pipeline section :chore:solo:quick:
-CLOSED: [2026-05-28 Thu]
+*Out of this task's =:solo:= scope:* an interactive runtime picker. It's a feature carrying a design/preference call (does Craig want it, what shape), which is deliberation, not hardening — file it separately if wanted. The runtime-selection arc (claude/codex shipped; ollama/qwen pending the model-floor eval) stays on the generic-agent-runtime parent.
+** DONE [#C] Put install-ai on PATH, launchable as =install-ai= :chore:quick:solo:
+CLOSED: [2026-07-18 Sat]
:PROPERTIES:
-:LAST_REVIEWED: 2026-05-28
+:CREATED: [2026-07-11 Sat]
+:LAST_REVIEWED: 2026-07-13
:END:
+From the roam inbox (2026-07-11): make install-ai launchable as =install-ai= (no =.sh=) from PATH. dotfiles needs a copy that stays in sync with the rulesets canonical — decide whether the startup script-sync already covers it or a dedicated mechanism is needed.
-=README.org= covers global install, per-project language bundles, and design principles, but doesn't mention =make install-mcp= or the =mcp/= directory. Add a short section after "Per-project language bundles" describing the user-scope MCP install pattern (decrypt → expand → register) and pointing at the eventual =mcp/README.org=.
-** DONE [#C] Consolidate =claude-templates/Makefile= after fold :chore:quick:solo:
-CLOSED: [2026-05-28 Thu]
+Resolved: added =claude-templates/bin/install-ai=, a thin launcher that resolves its own path through the symlink chain and execs =scripts/install-ai.sh=. =make install='s existing bin loop symlinks it into =~/.local/bin/install-ai= (same mechanism as =ai= and =agent-page=), so no dedicated sync and no dotfiles copy — the symlink always points at the canonical. 3 launcher bats added (incl. symlink-invocation resolution). Verified live: =install-ai --help= runs from PATH.
+** DONE [#C] coverage-summary.el documented as a local-only helper :chore:quick:solo:
+CLOSED: [2026-07-18 Sat]
:PROPERTIES:
-:LAST_REVIEWED: 2026-05-28
+:CREATED: [2026-06-22 Mon]
+:LAST_REVIEWED: 2026-07-13
:END:
-
-Sibling follow-up from the fold child (2026-05-15). After the subtree merge, =rulesets/claude-templates/Makefile= still has its standalone =install= / =uninstall= / =list= / =test-scripts= targets. The =install= target's =bin/ai= logic is now duplicated in =rulesets/Makefile=. Both work; the redundancy is harmless but worth cleaning up.
-
-Options:
-- *Delete* =claude-templates/Makefile= entirely — forces all install through rulesets root. Cleaner.
-- *Strip down* to just =test-scripts= — the one piece not redundant with =rulesets/Makefile=.
-- *Leave it* — slight redundancy, no functional harm.
-
-Triggered by: 2026-05-15 fold session's refactor audit (commit =2d645fc=).
-** DONE [#C] Run =--archive-done= sweep at start of =open-tasks.org= Phase A :chore:quick:solo:
-CLOSED: [2026-05-28 Thu]
+The elisp bundle installs =coverage-summary.el= into =.claude/scripts/=, gitignored in code projects, so CI can't run =make coverage-summary= against it. Decision (Craig, 2026-06-28): keep it in =.claude/scripts/= and document it as a local-only helper — don't ship it to a tracked =scripts/= dir, don't expect CI to run it. Remaining work (docs only, no move): state the local-only status in the script's header comment and wherever =make coverage-summary= is described, so the gitignored install reads as intentional rather than a gap. Note: emacs-wttrin rewrote its copy's header to claim a tracked =scripts/= home, which now contradicts this decision and should be reverted on their side. Surfaced 2026-06-21 during the coverage-summary autoloads bugfix (commit fb86736).
+
+Resolved: documented the local-only status in the =coverage-summary.el= commentary header and in =elisp-testing.md='s "Measuring it" section — the gitignored install now reads as intentional, not a coverage gap. Sent emacs-wttrin a handoff to revert its contradicting header claim.
+** DONE [#B] todo-cleanup.el dated-seal archiving :feature:solo:
+CLOSED: [2026-07-18 Sat]
+Redefine =--archive-done= aging from a 7-day roll-into-one-file model to a
+one-month retention with dated seals. Craig ratified the design in the work
+project 2026-07-17; origin handoff preserved at
+[[file:docs/design/2026-07-17-todo-cleanup-dated-seal-proposal.md]].
+
+Changes to =.ai/scripts/todo-cleanup.el= (canonical in
+=claude-templates/.ai/scripts/=):
+- =tc-archive-retain-days= default 7 → 31.
+- Aging predicate: archive a level-2 DONE/CANCELLED subtree when CLOSED is
+ older than the retain window OR CLOSED is unparseable. The unparseable case
+ already archives per the docstring; make it explicit in the contract.
+- New seal step (flag, e.g. =--seal=): rename the working =tc-archive-file=
+ (=task-archive.org=) → =resolved-YYYY-MM-DD.org= beside it; next aging
+ recreates a fresh working file. Auto-seal-at-quarter vs manual flag is an
+ open call — default manual (the per-project rotation task covers cadence).
+- Update the file-aging tests (=test-todo-cleanup.el= lines 362, 378, 491,
+ 513, 518) for the new retain default and the seal/rename behavior. The
+ gitignore-inheritance logic (=tc--ensure-archive-gitignored=) applies to both
+ names unchanged.
+
+TDD, canonical-then-mirror per the sync-check invariant. Home's planning-line
+strip proposal (filed alongside) also touches todo-cleanup.el =--convert-subtasks=
+— the two can be built as one batch.
+** DONE [#B] Strip stale planning lines on dated completion + lint backstop :feature:solo:
+CLOSED: [2026-07-18 Sat]
+Two linked fixes so a closed sub-task can't keep polluting the org agenda. Origin:
+home handoff 2026-07-17; design preserved at
+[[file:docs/design/2026-07-17-dated-log-planning-line-strip-proposal.md]]. A dated-log
+heading (no TODO keyword) that still carries an active =SCHEDULED:= renders as
+weeks-overdue on the agenda forever — invisible to a keyword scan and surviving
+=--archive-done=. Root cause: the completion rewrite strips keyword/priority/tags
+but nothing strips the planning line, and an interactive org close only stamps
+=CLOSED:=.
+
+1. =claude-rules/todo-format.md= (canonical rule) — in the "=***= and deeper —
+ rewrite to a dated event-log entry" section, add a step: remove any
+ =SCHEDULED:=/=DEADLINE:= line when rewriting to the dated form. Apply the same
+ to the VERIFY dated-completion path.
+2. =.ai/scripts/lint-org.el= (canonical in =claude-templates/.ai/scripts/=) — new
+ checker =dated-log-heading-active-timestamp=: flag any dated-log heading (the
+ =YYYY-MM-DD ... @ ...= form, no TODO keyword) carrying an active =<...>=
+ SCHEDULED or DEADLINE. The mechanical backstop for #1, mirroring
+ =subtask-done-not-dated=.
+3. =todo-cleanup.el= =--convert-subtasks= — drop the planning line alongside its
+ existing CLOSED-timestamp pull.
+
+Build notes: the checker's regex must key on "no TODO keyword" so it never flags a
+live TODO that legitimately carries a SCHEDULED. TDD, canonical-then-mirror. Batches
+with the dated-seal task above (both touch todo-cleanup.el).
+** DONE [#B] Enforce the task-boundary inbox check via a hook :feature:
+CLOSED: [2026-07-19 Sun]
+Resolved with the soft-nudge design (94e54f6). New =hooks/inbox-boundary-check.sh= Stop hook blocks the yield once + injects the pending count when =inbox-status -q= exits 1, steps aside on the harness re-entry (=stop_hook_active=) so a mid-task pause never wedges, self-skips on no-inbox/no-inbox-status/clean. Wired ahead of =ai-wrap-teardown= in =.claude/settings.json= (which the live =~/.claude/settings.json= symlinks to) + the snippet; glob-installed by =make install-hooks=. 6 bats (pending/clean/re-entry/no-inbox/absent-status/project-name). protocols.org Inbox Monitoring Cadence now notes the enforcement. Live-verified on ratio (clean inbox no-ops, pending fixture blocks); velox synced + hook linked. The UserPromptSubmit visibility complement stayed unbuilt (optional in the design) — file separately if wanted.
+Today the "check =inbox/= at every task boundary" rule (protocols.org, "Inbox
+Monitoring Cadence") is prose-only — present in all 27 projects' synced
+protocols.org, but nothing mechanically enforces it, so it holds only as well as
+the agent's adherence. Convert it to a hook so a pending handoff can't slip past a
+turn unseen. Design worked out with Craig 2026-07-18.
+
+*** The one decision to settle at kickoff
+Hard-block vs soft-nudge (this is why the task isn't =:solo:= — it's a preference
+call). My recommendation: *soft-nudge*. Answer this first, then the rest is
+mechanical.
+- *Hard-block*: the Stop hook blocks EVERY yield while items are pending, forcing
+ the agent to process before it can return control. Strongest guarantee, but it
+ also fires when the agent pauses mid-task to ask Craig a clarifying question
+ (that pause is also a Stop), pushing inbox processing ahead of the question.
+- *Soft-nudge*: use the =stop_hook_active= flag to inject the reason once per turn,
+ then let the turn end if the agent chooses not to act. Stronger than today's
+ prose rule, no clarifying-question hijack.
+
+*** Why the Stop event
+The harness has no "task boundary" event. But the rule's own definition — "after
+finishing a unit of work, before reporting back or asking what's next" — maps onto
+the Stop event (agent finishing its turn, about to yield). Every "reporting back"
+is a Stop. PostToolUse is wrong granularity (fires per tool call); UserPromptSubmit
+is the START of the next task, useful only as a complement (below).
+
+*** Mechanism
+A Stop hook returns =\{"decision":"block","reason":"N pending handoffs — process
+per inbox.org before yielding."\}= when =inbox-status -q= exits 1. The harness then
+keeps the agent going with that reason injected instead of returning to Craig. The
+loop self-terminates: once items are dispositioned (deleted or renamed
+=PROCESSED-=, which =inbox-status= excludes), the next Stop exits 0 and the turn
+ends. The harness passes =stop_hook_active: true= on the re-entry — check it to
+nudge once rather than wedge on an item the agent genuinely can't process.
+
+*** The complement (optional, pair with the Stop hook)
+A =UserPromptSubmit= hook that runs =inbox-status= and injects a one-line "N
+pending handoffs" note into context at the start of Craig's next instruction.
+Non-blocking; guarantees the agent sees pending items the moment a new task starts.
+Catches the "arrived while away" case from the other direction. Can ship alone
+(pure visibility) or alongside the Stop hook. Recommended: ship both.
+
+*** Placement + files
+- Global, in =~/.claude/settings.json= (the =Stop= array already holds
+ =ai-wrap-teardown.sh= — add a second entry, or a combined script).
+- Ship the script from =claude-templates/.claude/hooks/= (e.g.
+ =inbox-boundary-check.sh=); =make install-hooks= links it; wire it in the tracked
+ =settings.json= so it travels. Mirror the reference pattern in
+ =hooks/ai-wrap-teardown.sh= — reads =cwd= from stdin JSON via =jq=, basenames to
+ the project.
+- Self-skips where inapplicable: =inbox-status= exits 2 with no =inbox/= dir, so
+ the hook no-ops in any project without an inbox. One hook, every project, no
+ config.
+
+*** Verify
+- bats around the hook script (=hooks/tests/=): exit-1 inbox → block JSON emitted;
+ exit-0 → no output; =stop_hook_active: true= input → single-nudge path; no
+ =inbox/= → clean no-op. Follow the existing hook-test pattern.
+- Live: drop a test handoff, confirm the agent is pushed to process it at turn end.
+
+*** Honest limitation to carry forward
+No hook can perfectly tell "a unit of work finished" from "the agent paused for any
+other reason" — the harness exposes only "the turn is ending," not "a task is
+ending." The design leans on those two being usually the same in Craig's workflow.
+That's the tradeoff hard-block vs soft-nudge is really about.
+** DONE [#B] "Colloquialisms and Expansions" + "the list" before-close-queue convention :feature:
+CLOSED: [2026-07-19 Sun]
+Resolved (approved in the speedrun). New =* Colloquialisms and Expansions= section in =protocols.org= documents both shorthands: "put X on the list" → append to a session-scoped Before-Close Queue (=* Before-Close Queue= heading in the session anchor, resets on archive, todo.org for must-outlive items); "tell <project> <msg>" → =inbox-send=. =wrap-it-up.org= Step 1 gained a "Work the Before-Close Queue (before the Summary)" sub-step so queued work rides the wrap commit, unfinished items surfaced in the valediction. Design calls: reference in protocols.org not per-project notes.org (synced = shared norm); queue in the session anchor as home did; wrap step at the front of Step 1, not a new half-step (keeps the "Steps 1-5" framing). 4 documentation-integrity bats (=before-close-queue.bats=). Canonical + mirror synced. The UserPromptSubmit-style variant wasn't in scope.
+Home proposes two linked cross-project norms; Craig recommends rolling them out.
+Origin: home handoff 2026-07-18, design preserved at
+[[file:docs/design/2026-07-18-colloquialisms-and-the-list-proposal.md]]. Needs Craig's
+adoption decision + a small design pass before implementation — that's why it's
+filed, not applied.
+
+The two parts:
+1. *"the list" = a before-close FIFO queue.* "Put X on the list" / "add X to the
+ list" appends X to a session-scoped Before-Close Queue, worked oldest-first at
+ wrap-up before teardown, unfinished items surfaced rather than dropped. Resets
+ when the session anchor archives. Anything that must outlive the session is a
+ =todo.org= task instead.
+2. *"Colloquialisms and Expansions" shorthand dictionary.* A per-project (or
+ shared) map of Craig shorthand → expansion the agent applies without asking.
+ Seed entries: "the list" → the queue above; "tell <project> <msg>" → drop the
+ message in that project's inbox via =inbox-send= (already the sanctioned
+ handoff, so this entry is near-documentation).
+
+Durable wiring (why rulesets, not home-local): (a) a colloquialisms reference in
+the template — a =protocols.org= section or a shipped reference file; (b) a
+=wrap-it-up.org= step that processes the Before-Close Queue before teardown.
+=wrap-it-up.org= is a synced rulesets-owned workflow, so home can't wire (b)
+durably from downstream — it stubbed the norm via its local =notes.org= for now.
+
+Design pass to settle first: where the colloquialisms reference lives (protocols.org
+section vs a new shipped reference file); where the queue itself lives (home used a
+=* Before-Close Queue= heading in =session-context.org=); and the exact wrap-it-up
+insertion point (before the teardown/valediction, alongside the roam-inbox
+sub-step). Then it's a synced-file change (canonical-then-mirror) + a test that the
+wrap step drains the queue.
+** DONE [#B] working/ tracked-from-creation + gitignored temp/ :feature:
+CLOSED: [2026-07-20 Mon]
+Craig's ruling relayed from .emacs.d (2026-07-19): working/ is version-controlled from creation (not excluded until graduation); ephemeral artifacts go in a gitignored temp/ or /tmp; graduation reorganizes durable artifacts into permanent homes rather than marking when they become durable.
+
+Implemented (Shape A) during the morning sentry review, 2026-07-20:
+- =claude-rules/working-files.md=: added "working/ Is Version-Controlled From Creation" section (tracked-from-creation, graduation-is-a-move, temp/ for ephemeral).
+- =.ai/protocols.org= (canonical + mirror): one-paragraph mirror in the Working-Files Convention section.
+- =scripts/install-ai.sh=: emits a =temp/= ignore block in BOTH track and gitignore modes; working/ never ignored. Idempotent.
+- =scripts/sweep-gitignore-tooling.sh=: separate mode-independent temp/ backfill pass (a distinct loop, not an IGNORE_SET member — that would skip track-mode projects, the set that most needs it).
+- Tests: 3 new install-ai bats + 4 new sweep bats (temp/ in both modes, never working/, idempotent). Suite green, 384 bats ok.
+
+Finding confirmed at implementation: the canonical machinery already never ignored working/ (tooling set is only =.ai/ .claude/ CLAUDE.md AGENTS.md=), so .emacs.d's =/working/= ignore was a purely local deviation. The staging proposal dir was removed after shipping; its content lives in the feat commit and this body.
+** DONE [#C] Polyglot projects — supported, or refused? :spec:
+CLOSED: [2026-07-20 Mon]
+DECISION (2026-07-20, scouting with Craig): *case-by-case, and it already composes — no option-2 machinery.* The evidence: bundle contents split into namespaced/additive files (rules =<lang>.md=, =validate-<lang>.sh= hooks, =coverage-summary.<ext>= scripts, appended =gitignore-add.txt=) that compose cleanly, and exactly three colliding files — =claude/settings.json= and =githooks/pre-commit= (full bundles only: bash/elisp/go) and =coverage-makefile.txt= (elisp/go/python/typescript). Of the three, only =coverage-makefile.txt= occurs in the fleet, and clock-panel (the one real polyglot, python+typescript) proves it's benign: both bundles installed, no breakage, because that fragment is a hand-pasted Makefile block nobody pasted twice. So: keep the install-lang collision guard (it blocks the destructive full+full settings/githooks clobber, which no project actually hits), and document the =coverage-<lang>:= + =coverage:= aggregate namespacing as the one manual step when going polyglot (filed below). No two-full-co-equal-bundle project exists, so the settings.json/githooks merge is unwarranted. Follow-up doc: [[file:todo.org::*Document polyglot coverage-makefile namespacing][Document polyglot coverage-makefile namespacing]].
+
+Do we support more than one language bundle per project? The honest answer today
+is "partly, by accident." The collision guard added 2026-07-16 refuses a
+*colliding* second bundle rather than silently replacing the first's config, but
+a non-overlapping pair still installs fine: bash ships =settings.json= +
+githooks and no coverage fragment, python ships only a coverage fragment, so
+=bash= + =python= composes cleanly today and yields a real polyglot project with
+both rule sets. So the line isn't polyglot-vs-not, it's overlap-vs-not — and
+nobody chose that line, it fell out of which bundle happens to ship what. Origin:
+home's report after scaffolding clock-panel with python + typescript,
+[[file:docs/design/2026-07-16-polyglot-bundle-collision.txt][docs/design/2026-07-16-polyglot-bundle-collision.txt]].
+
+Pair this with the subproject scouting below — it's the same question in a
+different costume ("which projects would actually be polyglot, and why"), so
+they should be one conversation.
+
+The three options, in the order they'd be weighed:
+
+1. *Unsupported, explicitly.* Keep the guard as the answer. Cheapest, and
+ matches how little polyglot exists (one project, clock-panel).
+2. *Supported.* Needs per-bundle filenames, a merged =settings.json= (the hooks
+ arrays compose rather than clobber), composed githooks, and namespaced
+ Makefile targets with a =coverage= aggregate. This is the real work.
+3. *Case-by-case.* Support the pairs that come up, refuse the rest.
+
+What the decision needs to know:
+
+- *The target-name collision is the deeper half* (home's point, and it's right).
+ Every bundle's fragment defines =coverage:= and =coverage-summary:=, so even
+ with both files present a polyglot project can't paste both into one Makefile.
+ Renaming files doesn't fix it.
+- *Only three of five shared filenames actually collide.* =gitignore-add.txt=
+ (5 bundles) appends deduped and composes. =CLAUDE.md= (3) is seed-only, and
+ its fallback comment shows multi-bundle was already considered there.
+ =claude/settings.json= (3), =githooks/*= (3), and =coverage-makefile.txt= (4)
+ are the real ones.
+- *=FORCE=1= is a poor escape hatch* (home's catch): it also re-seeds
+ =CLAUDE.md=, which is destructive on a customized project. If polyglot
+ becomes supported, the override wants to be its own flag.
+** DONE [#C] Subproject pattern — promote to claude-rules? :spec:
+CLOSED: [2026-07-20 Mon]
+DECISION (2026-07-20, scouting with Craig): *don't promote — keep it local at home.* The scouting confirmed N=1: across all 27 =.ai= scopes, home (9 subprojects, all from the single 2026-06-11 fold) is the only real user. The nearest neighbors aren't the pattern — rulesets folds claude-templates in as a git *subtree* (different mechanism, not shared-=.ai/=-scope), archsetup's dotfiles/ likewise; every other project is a focused single package with no subproject structure or need. Promoting a 282-line convention into the always-on claude-rules layer for one project fails the thin-always-on principle (the precedent is patterns.md at 29 lines + docs-lifecycle.md's depth-in-a-spec). Keep the convention as home's local instance. Revisit only if a genuine second case appears, and then as a thin pointer + a spec, never 282 lines always-on.
+
+home proposes promoting its subproject pattern (a former standalone project
+folded into a parent, living as a self-contained subdir sharing the parent's
+=.ai/= scope) into the rules layer: vocabulary, the read-first
+=<subproject>/<subproject>-brief.org= convention, the parent-vs-subproject
+content criterion ("one fact, one home"), and create/archive criteria.
+Proposal + home's full instance:
+[[file:docs/design/2026-07-15-subproject-pattern-proposal.org][proposal]],
+[[file:docs/design/2026-07-15-subprojects-convention-home-instance.org][home's convention doc]].
+
+Deferred 2026-07-16 rather than promoted. *Craig's framing:* he wants to scout
+which projects would actually get subprojects, and why, before we shape a rule.
+If he hasn't done that scouting by the time this comes up, offer to do it
+together — brainstorm the candidates, then explore the reasons behind each. That
+evidence decides it: either we drop the pattern, or we know enough to adjust it
+so it's effective. Don't shape the rule before the scouting.
+
+*Review findings from the 2026-07-16 pass* (the inputs the decision needs):
+
+- *N=1.* home is the only project with subprojects, across all 27 =.ai= scopes;
+ its nine all came from the single 2026-06-11 fold. This is the fact the
+ scouting tests.
+- *Placement contradicts the proposal's own principle.* =claude-rules/*.md=
+ loads into every session of every project. home's doc argues the always-on
+ layer is "a tax paid whether or not it's relevant today" and depth belongs
+ "one open away". At 282 lines the doc would be the third-largest rule and add
+ ~11% to the always-on layer, so every .emacs.d / takuzu / chime session would
+ carry a one-project convention.
+- *Precedent for the shape:* =patterns.md= (29 lines, explicit "don't carry the
+ catalog in context") and =docs-lifecycle.md= (75 lines, depth in a spec).
+ Thin rule + on-demand depth is the established answer.
+- *Dangling reference:* the doc cites =claude-rules/git-hosting-privacy-model=
+ as authority for its shared-scope-safety criterion. No such file exists — the
+ real content is the gitignore-vs-track and public-reachability decision in
+ =protocols.org=. Fix before any promotion.
+- *Instance vs rule:* the metrics, self-improvement log, kill criteria, rollout
+ dates, and adoption table are home's instance, not rule content.
+** DONE [#B] Triage source activation — per-project source declaration :feature:spec:
+CLOSED: [2026-07-20 Mon]
+Spec: [[file:docs/specs/2026-07-20-triage-source-activation-spec.org][docs/specs/2026-07-20-triage-source-activation-spec.org]] (IMPLEMENTED, ID af73ef0b-cd1d-46f1-9e1d-62695733a4de). Craig approved after his read, both open decisions resolved via cj comments; built same session.
+
+From the sentry live trial (2026-07-20): triage-intake self-activated in every project because the general (personal-account) plugins are template-synced everywhere, so sentry's pass-3 probe misfired and would pull Craig's personal inboxes into whatever project the fire ran in. Fixed with an activation gate in triage-intake Phase 0 — general plugins gate on a per-project =:TRIAGE_SOURCES:= declaration; project-specific plugins stay active by presence. Applies to interactive and unattended alike. sentry pass-3 probe reads the same signal. Migration handoffs sent to home + work. Supersedes Fire 1's narrower probe-only approval-queue item.
+** DONE [#B] Silent-until-signal for in-session monitor loops :feature:spec:
+CLOSED: [2026-07-20 Mon]
+Spec: [[file:docs/specs/2026-07-20-silent-until-signal-monitors-spec.org][docs/specs/2026-07-20-silent-until-signal-monitors-spec.org]] (IMPLEMENTED, ID af592bd6-d3e6-47e2-8804-2a287b4d9303). Craig approved without changes 2026-07-20; all 5 phases shipped that day. Sentry, auto triage-intake, and auto inbox-zero all collapse an empty fire to =<workflow> at HH:MM: nothing=; a manual-testing entry covers the live-loop verification.
+
+Craig-approved proposal from .emacs.d (2026-07-20), demonstrated by the sentry live trial (fires 3-8 were walls of no-op lines). Reframed live from a watcher *mechanism* to a *policy*: an in-session monitor fire detects first, and on an empty check collapses to one labelled heartbeat line (=<workflow> at HH:MM: nothing=) instead of a full turn; only a real item earns the full surface-and-judge turn. Keeping detection in-session dissolves the MCP-auth split (triage's Gmail/Slack/Linear need session auth), so it applies uniformly to sentry, auto triage-intake, and auto inbox-zero. No external watcher, no new seen-list. Five build phases in the spec. Phase 1 (sentry quiet-fire heartbeat) shipped 2026-07-20 ahead of the full READY gate at Craig's direction — a quiet fire now collapses to =sentry at HH:MM: nothing=. Phases 2-4 (auto triage-intake, auto inbox-zero, shared-policy home) + Phase 5 verification pending Craig's spec read. Overlaps the sentry cluster ([[file:todo.org::*Sentry vNext passes][Sentry vNext passes]], [[file:todo.org::*Triage source activation][Triage source activation]]).
+** DONE [#C] Apply suspend.org detach-on-suspend change to canonical :feature:
+CLOSED: [2026-07-20 Mon]
+Craig's change relayed from archsetup (2026-07-20): add Step 6 to suspend.org — detach the tmux client (=tmux detach-client -s "$sess"=) as the final action of every suspend, so a suspended session parks in the re-attachable set instead of cluttering the alt-space rotation. Also reworded the neighbors bullet and the "does NOT do" teardown bullet to draw the detach-vs-teardown line.
+
+Applied to canonical 2026-07-20 (verified the diff was exactly the intended change, canonical + mirror synced, lint clean bar one pre-existing flush/SKILL.md link). Replied to archsetup that its local stopgap is now canonical.
+** DONE [#B] Document (and own) the Signal pager :feature:spec:
+CLOSED: [2026-07-20 Mon]
:PROPERTIES:
-:CREATED: [2026-05-28 Thu]
-:LAST_REVIEWED: 2026-05-28
+:CREATED: [2026-07-11 Sat]
+:LAST_REVIEWED: 2026-07-13
:END:
+home retired the ntfy phone-notification channel (phone-notify/phone-recv, self-hosted ntfy on ratio) on 2026-07-04 in favor of paging over Signal, and tore ntfy down. The Signal pager it replaced ntfy with is undocumented: no pager script in =~/.local/bin=, and =notify= doesn't reference Signal. What exists on ratio: signal-cli 0.14.5, account 404211. Deliverable: a documented Signal pager (send + read-replies), the signal-cli setup/account notes, and the sync path — the Signal equivalent of the retired ntfy runbook. Cross-machine tooling, so canonical home + docs belong in rulesets.
-From pearl handoff 2026-05-28. =open-tasks.org= Next Mode reads =* Project Open Work= and skips =* Project Resolved= correctly, but a level-2 task that completed during a session sits as =** DONE= under Open Work until something archives it. Between cleanups, a freshly-DONE task can surface as a "what's next" candidate.
+RECONCILE FIRST: =protocols.org= "Paging Craig" already documents an agent-paging path via the *signal-mcp* tool (=send_message_to_user=, pager account +15045173983, Craig's UUID =b1b5601e-…=, verified 2026-06-30). home's handoff is about a *different* mechanism (signal-cli / account 404211, home's own paging). Decide whether these are one channel or two, and whether the signal-cli side needs its own runbook or should route through signal-mcp. Source: home handoff 2026-07-04 (=inbox/2026-07-04-1302-from-home-task-for-rulesets-document-and-decide.org=). Successor to the 2026-06-17 two-way-comms proposal.
-Proposed fix: as the first step of =open-tasks.org= Phase A, run =emacs --batch -q -l .ai/scripts/todo-cleanup.el --archive-done todo.org=, then read =todo.org=. The cleanup tool already exists; this is wiring it into the workflow.
+*** 2026-07-13 Mon @ 05:16:37 -0500 Folded home's ownership ack + current-state report into the reconcile scope
+home confirmed (2026-07-11 reply) rulesets owns this task and the two-paths reconciliation is the right first step. New facts for the reconcile: from a home session on 2026-07-09, signal-mcp was NOT connected, and the local signal-cli is registered as Craig's own number — =send --note-to-self= returns a message id but produces no phone push. So home currently has no working ad-hoc page channel at all; whatever the runbook lands on must give home a live path.
-Cost: a few hundred ms at the start of every "what's next" invocation. Win: recommendations never include DONE work.
+*** 2026-07-13 Mon @ 14:40:00 -0500 Added runtime-portability as a second motivation (Craig approved)
+The MCP portability inventory ([[file:docs/design/2026-07-13-runtime-portability-inventories.org]]) found signal-mcp exists only claude.ai-side — no local config anywhere — so a non-Claude agent (Codex-style or local LLM) has no paging path at all. The signal-cli runbook this task produces is therefore also the runtime-neutral page channel, not just home's replacement for ntfy.
-Optional refinement: gate behind a check for read-only / dry-run mode if that's ever introduced. The default invocation archives.
-** DONE [#C] Triage Codex enhancement backlog :spec:
-CLOSED: [2026-05-28 Thu]
-:PROPERTIES:
-:CREATED: [2026-05-28 Thu]
-:LAST_REVIEWED: 2026-05-28
-:END:
-
-Triaged interactively 2026-05-28. Disposition table for all 14 items lives at [[file:docs/design/2026-05-28-rulesets-enhancement-backlog.org][2026-05-28-rulesets-enhancement-backlog.org]] under "Triage Dispositions": 3 accepted (filed below as TODOs), 3 pilot/scope-limited (filed below), 2 marked as conventions rather than tracked tasks, 6 rejected with rationale. Items #1 and #2 already had homes (#16 and the Phase-1 codex TODO).
-** DONE [#C] Canonical/mirror drift detection via pre-commit hook or =make sync-check= :feature:quick:solo:
-CLOSED: [2026-05-28 Thu]
-:PROPERTIES:
-:CREATED: [2026-05-28 Thu]
-:LAST_REVIEWED: 2026-05-28
-:END:
-
-From the codex enhancement backlog (item #7), reframed: don't dedupe the dual source — the canonical-in-=claude-templates/= + mirror-in-=.ai/= pattern is a feature (other projects rsync from the canonical; the mirror lets rulesets-as-a-project have a working copy). The real pain is sync-discipline overhead — every workflow edit needs both copies updated, and forgetting one leaves the next startup's rsync to surface the drift.
+*** 2026-07-13 Mon @ 18:30:19 -0500 agent-page shipped — every project now knows both channels
+Craig's call: make paging universal and rename it the agent pager. NEW =claude-templates/bin/agent-page= (runs signal-cli directly on velox, ssh-relays from anywhere else, desktop-fallback hint on failure; 4 bats tests; live-verified from ratio through the real script). protocols.org "Paging Craig" rewritten around the two channels (notify desktop + agent-page phone; signal-mcp demoted to a velox-local nicety); page-me.org gained the phone section + fire-both guidance; work-the-backlog's end-of-set page names both surfaces; INDEX updated. Every project inherits via the startup sync + make install. Remaining here: the runbook proper, the receive timer, ssh-only vs linked-device.
-Scope: write a small =scripts/sync-check.sh= (or fold into the existing Makefile) that diffs =claude-templates/.ai/workflows/= against =.ai/workflows/=, exits non-zero on drift. Wire as a pre-commit hook (=githooks/pre-commit= or equivalent) so the discipline is enforced before publish, not at the next startup. =make sync-check= as a manual entry point.
+*** 2026-07-13 Mon @ 18:13:07 -0500 RECONCILED — one channel, on velox; live page verified end to end
+The two-paths question is answered: there is ONE pager identity, +15045173983, registered in velox's signal-cli (account file 465310) — and signal-mcp is a locally-configured MCP server in velox's global ~/.claude.json (not claude.ai-side as the 14:40 entry inferred; it's just invisible from ratio, which is why home and this session couldn't find it). ratio's signal-cli holds only Craig's personal number (note-to-self, no push). Verified live today: =ssh velox 'signal-cli -a +15045173983 send -m … b1b5601e-6126-47f8-afaa-0a59f5188fde'= buzzed Craig's phone — his remembered CLI page was this same account on velox. Reliability findings for the runbook: both accounts throw receive-staleness warnings (velox 40 days, ratio 26; the signal protocol wants regular receives — a systemd receive timer on velox is the roam-sync-shaped fix), and the channel requires velox to be up. Remaining deliverables sharpened: the runbook (send + read-replies + receive timer + account notes); decide ssh-over-tailnet-only vs registering ratio as a linked device of the pager account; update protocols.org "Paging Craig" (it names signal-mcp as the only supported path — true only on velox; the ssh recipe is the cross-machine path) — shared-asset edit, own review pass. Interim recipe sent to home so it's unblocked today.
-Verification: introduce a deliberate diff, commit, hook should block. Restore parity, hook should pass.
-** DONE [#C] Add =make status= — compose audit + doctor + open-task count :feature:quick:solo:
-CLOSED: [2026-05-28 Thu]
+*** 2026-07-20 Mon @ 15:56:56 -0500 Runbook shipped; ratio linked as a device; receive timer on both machines
+All four remaining deliverables landed. (1) Runbook: [[file:docs/design/2026-07-20-signal-pager-runbook.org]] — send, read-replies, receive timer, signal-cli account/setup notes, the resolved topology decision. (2) Receive timer: =scripts/signal-receive.sh= + =scripts/systemd/signal-receive.{service,timer}= (roam-sync-shaped, 15-min cadence, 3 bats), stowed via dotfiles =common=, enabled + verified on ratio; a manual drain also cleared the 47-day staleness live. (3) Topology decision (Craig, option 2): register daily drivers as linked devices rather than ssh-relay-only — ratio linked as Device 2 "ratio-pager", direct send verified, =agent-page= generalized from a velox-only check to "any machine holding the account sends directly, else relay" (bats updated). (4) protocols.org "Paging Craig": verified accurate, no edit — already describes both channels and the caveats, and its generic runbook pointer is correct to leave un-pathed since it syncs into every project. Remaining one-time step: enable the timer on velox after it pulls dotfiles.
+** DONE [#C] triage-intake auto mode — push signal sweeps to phone via agent-text :feature:solo:
+CLOSED: [2026-07-20 Mon]
:PROPERTIES:
-:CREATED: [2026-05-28 Thu]
-:LAST_REVIEWED: 2026-05-28
+:CREATED: [2026-06-20 Sat]
+:LAST_REVIEWED: 2026-07-13
:END:
+Send half shipped 2026-07-20. Folded a "Phone delivery" subsection into canonical =triage-intake.org= auto mode: a full-three-section sweep pushes to Craig's phone over Signal via =agent-text=, with a pointer from "End-of-sweep output" and a Living Document note. Signal-only by Craig's 2026-07-20 ruling — a quiet sweep's =nothing= heartbeat never reaches the phone, so silent-until-signal governs the phone channel too and the in-session heartbeat stays as proof-of-life. Falls back to inline when =agent-text= is absent.
-From the codex enhancement backlog (item #12), scope-limited: =make status= only. Reject the rest of #12 (=make sync= duplicates the existing sync flow; =make health= wraps existing checks without adding signal; =make bootstrap-project= duplicates =install-ai= + =install-lang=).
+Reply-polling half (the old =phone-recv=) deferred to the reply-correlation follow-up: with the Signal account linked on more than one device a reply fans out to every device and neither knows which page it answers, so auto mode pushes but does not poll until that's resolved. The recv wiring is owned by that spec, not this task.
-Scope: one Makefile target that prints a compact summary of:
+Origin: the work project's 2026-06-18 fold-in request; preserved bundle [[file:docs/design/2026-06-18-triage-intake-phone-push-note.org][note]] + [[file:docs/design/2026-06-18-triage-intake-phone-push-workflow.org][edited workflow]]. Transport re-pointed off retired ntfy → agent-text (renamed from agent-page 2026-07-20).
+** CANCELLED [#D] Fully-unattended scheduled inbox check (/schedule cron pass) :feature:
+CLOSED: [2026-07-20 Mon]
+Merged 2026-07-20 into [[file:todo.org::*Unattended /schedule cron contract][Unattended /schedule cron contract — no-session variant]]. Same no-session design problem as the sentry /schedule variant (mutation rights, async surfacing, cross-run dedup, cron auth context); its inbox-specific context was absorbed there.
+** CANCELLED [#B] lint-org resolves file: links against cwd, not the linted file :bug:solo:
+CLOSED: [2026-07-23 Thu]
+Not a defect. I filed this on a bad comparison and retracted it the same night.
-- Install audit state (clean / drift, calling =make audit=).
-- Machine-global doctor state (calling =make doctor=).
-- Open-task count (top-level entries in =todo.org= under =* Rulesets Open Work=).
-- Inbox count (files in =inbox/= excluding =.gitkeep= and =PROCESSED-= prefixes).
-- Git working-tree status (clean / dirty, ahead/behind upstream).
+The claimed evidence was that linting =claude-templates/.ai/notes.org= from the repo root reports =protocols.org= and =workflows/first-session.org= missing, while linting it from its own directory reports neither. The first half of that was never run. The findings came from a =/tmp= copy of the template made while preparing the smoke diff, and in =/tmp= those two siblings genuinely are missing, so org-lint was right. I compared two different files and read the difference as a cwd bug.
-Output should be roughly 10 lines, scannable in one glance. Composes the existing checks; no new logic except the summary formatting.
-** DONE [#C] Iteration-history backfill for spec-review and spec-response :docs:followup:
-CLOSED: [2026-05-28 Thu]
+Retested three ways: the real file from the repo root gives zero link findings, the real file from its own directory gives zero, and only the =/tmp= copy gives two. A direct probe confirms =find-file-noselect= already sets =default-directory= to the linted file's directory, so the mechanism I proposed could not have been the cause either.
+
+Worth keeping from the episode: =link-to-local-file= is org-lint's own checker, not one lint-org.el implements, so a real fix here would mean pre- or post-filtering upstream output rather than editing a local checker.
+** DONE [#A] Python and TypeScript bundles ship no secret-scan hook :bug:
+CLOSED: [2026-07-23 Thu]
:PROPERTIES:
-:LAST_REVIEWED: 2026-05-28
+:LAST_REVIEWED: 2026-07-23
:END:
-Source: org-drill inbox 2026-05-28.
-
-Once the in-flight WIP lands (the requirement that specs carry a bottom =Review and iteration history= section, with iteration / date / contributor / role / what / why / artifacts), backfill the two workflow files themselves using rulesets' session history as evidence.
+Fixed 2026-07-23. Both bundles now ship all four components: =githooks/pre-commit= (the secret scan, shared verbatim with the other bundles, plus a language-appropriate syntax gate), =claude/hooks/validate-python.sh= / =validate-typescript.sh=, =claude/settings.json= wiring the PostToolUse hook, and a seed =CLAUDE.md=. 53 new tests (13 + 13 hook, 13 + 14 pre-commit), suite green.
-Files to update:
-- =claude-templates/.ai/workflows/spec-review.org=
-- =claude-templates/.ai/workflows/spec-response.org=
+Two findings from the build worth keeping. First, =node --check= must never be used on TypeScript: it ignores =--experimental-strip-types=, so it rejects valid TS (an =interface= reads as a syntax error) and accepts broken TS. Measured on node v26.4.0. The hook uses =tsc= filtered to TS1xxx (syntactic) diagnostics instead, which also keeps type errors out of scope — those need the whole project graph. Second, the install now warns when a bundle lacks a documented component, which is the half that stops this recurring: nobody will remember the seventh bundle either, so the installer says so.
-Investigation: search =.ai/sessions/=, =.ai/notes.org=, inbox archive, and git log for mentions of these workflow docs. Identify review/response/design iterations, dates, and contributors (including agents where known: Claude Code, Codex, local models). Distinguish high-confidence history (commits, dated session entries) from inferred (chat-only context). Recommend whether enough evidence exists to populate the section, and draft the entries if so.
+Left undone deliberately: re-running =make install-lang= on =work= and =clock-panel=. Both are other projects, so that's a cross-project action for Craig. See the polyglot task below — =clock-panel= is python + typescript and can no longer install both.
+The =python= and =typescript= language bundles carry rules, a coverage script, and a gitignore fragment. They carry no =githooks/pre-commit=, no =claude/hooks/=, no =claude/settings.json=, and no =CLAUDE.md=. The =bash=, =elisp=, and =go= bundles carry all four.
-Dependency: spec-review.org and spec-response.org have uncommitted edits in flight. Wait for those to land before writing to the files. The read-only research portion (search sessions, identify iterations, draft entries to a scratch file) can run in parallel without conflict.
-** DONE [#B] Startup Phase A rsync propagates dirty rulesets WIP into downstream projects :feature:
-CLOSED: [2026-05-30 Sat]
-:PROPERTIES:
-:CREATED: [2026-05-29 Fri]
-:LAST_REVIEWED: 2026-05-29
-:END:
-Fixed via option 1 (skip-when-dirty), scoped to the synced source paths: startup.org Phase A now guards the protocols/workflows/scripts rsyncs behind a =git status --porcelain= check on =claude-templates/.ai/{protocols.org,workflows/,scripts/}=, skipping the sync when any are dirty. The propagation anomaly (cross-project-broadcast.org / page-signal.org not reaching jr-estate) was a timeline artifact: both files were added in 664bf01 on 2026-05-29, after jr-estate's Phase A rsync had already run — correct behavior, not a bug.
+The pre-commit hook is the secret scanner. So a project installing the Python or TypeScript bundle gets no credential scan on commit, no validate-on-edit hook, and no settings — while README's "Bundle structure" section documents all four as what each bundle follows, and notes.org describes the bundles as "rules + hooks + settings".
-From jr-estate handoff 2026-05-29. When rulesets has uncommitted WIP at the moment a downstream project starts a session, Phase A.0 reports "dirty, skipping pull" and proceeds. Phase A's =rsync -a --delete= then runs against the dirty rulesets working tree and copies the WIP state into the downstream project's =.ai/workflows/= and =.ai/scripts/=. The downstream project's =git status= then shows drift the user did not author. Two bad recovery paths: commit the drift as "chore: sync .ai tooling from templates" (creates fake commit history about template state) or leave it dirty (noisy wrap-ups, pressure to commit anyway).
+Live as of 2026-07-23, verified by scanning every project carrying =.claude/rules/=: =work= (python) and =clock-panel= (python + typescript) both have no =githooks/= and no =.claude/settings.json=. The four elisp projects all have both. =work= is the one that matters — a work repo is where a leaked credential is most costly and most likely to reach a company remote.
-Three options proposed in the handoff:
-1. *Skip-when-dirty.* Make Phase A's workflows/ and scripts/ rsync no-op when Phase A.0 reports rulesets dirty. Simplest defense.
-2. *Clean-files-only.* Restrict the rsync to files git considers unmodified in rulesets. Untracked files in rulesets do not propagate. Most precise.
-3. *Clean-ref-based.* Cache the last-known-clean state as a git tag or ref and rsync from that ref rather than the working tree. Most decoupled, also the most infrastructure.
+Not a design choice. Both bundles were added 2026-05-31; =go='s githooks landed 2026-06-02 and =bash='s 2026-06-23, so the hook rollout swept the two bundles added *after* these and skipped these. =install-lang.sh= guards its copy with =[ -d "$SRC/githooks" ]=, so the install succeeds silently and reports nothing missing — which is why this stayed invisible for nearly two months.
-Recommendation (mine): option 1. The downstream impact of skipping a sync once is small (the next session with rulesets clean catches up), and the implementation is one =if [ "$dirty" -eq 0 ]= guard around the existing rsync block. Option 2 adds shellout complexity per file; option 3 requires tagging discipline that has no other reason to exist.
+Grading: Major severity (a documented security control absent, silently, with the install reporting success) x every user, every time (every install of either bundle, standing on 2 of the 6 bundle-using projects) = P1 = [#A].
-The original handoff also noted a related anomaly: even with =--delete=, two files that DO exist in rulesets canonical (=cross-project-broadcast.org=, =page-signal.org=) did NOT propagate to jr-estate. Worth confirming whether that was a transient rsync issue or evidence of a deeper Phase A bug. Could be ordering: those files were added to rulesets AFTER the jr-estate Phase A rsync ran, in which case the behavior is correct and the report is misreading the timeline.
-
-Source: =inbox/2026-05-29-0832-from-jr-estate-investigate-startup-rsync-carried-dirty.org= (processed and deleted).
-** DONE [#B] Codex Phase 1 — AI_AGENT_ID + session-context.d/<id>.org :feature:
-CLOSED: [2026-05-30 Sat]
+Fix direction: port =githooks/pre-commit= to both bundles, adapting the language-specific half (the secret scan is common; =gofmt=/=shellcheck= becomes a formatter/linter check per language), add =claude/hooks/validate-*.sh= and =claude/settings.json=, and seed =CLAUDE.md=. Then make the gap loud: =install-lang= should warn when a bundle lacks a component the README documents, so the next partial bundle announces itself instead of installing quietly. Re-run =make install-lang= on =work= and =clock-panel= afterward.
+** DONE [#C] Two language bundles' pre-commit hooks lost the cd guard :bug:quick:solo:
+CLOSED: [2026-07-23 Thu]
+:PROPERTIES:
+:LAST_REVIEWED: 2026-07-23
+:END:
+Fixed 2026-07-23. =go= and =elisp= now read =cd "$REPO_ROOT" || exit 1=, matching =bash=; the two new bundles were written with the guard. All five siblings agree, all five shellcheck clean.
+=languages/bash/githooks/pre-commit= line 8 reads =cd "$REPO_ROOT" || exit 1=. The =go= and =elisp= copies of the same line read a bare =cd "$REPO_ROOT"=. Three siblings of one file, one hardened and two not — the guard landed in bash and never propagated.
+
+Low impact, stated honestly: git chdirs to the working-tree root before running a hook, so if =cd= fails the cwd is already correct and the checks still run against the right tree. Neither script sets =-e=, so a failure wouldn't abort them either. It takes a =GIT_WORK_TREE= or bare-repo edge case for =rev-parse --show-toplevel= to disagree with the cwd at all.
+
+Grading: Minor severity (belt-and-braces guard, no silent-pass path found — the checks run regardless) x rare edge case (needs a git configuration that makes toplevel differ from the hook's cwd) = P4... graded [#C] rather than [#D] because it's a two-character fix in a security-relevant gate and the drift pattern is the real signal: a fix landed in one bundle copy and stopped there, which is the same shape as the [#A] above.
+** DONE [#B] Parked: zero markup in chat output, fences included (from org-drill)
+CLOSED: [2026-07-23 Thu]
+Craig approved 2026-07-23. Applied to =claude-rules/interaction.md=: the fenced-code-block carve-out is gone, replaced with a zero-markup-always statement citing his 2026-05-30 direction. The rule's factual claim stays honest — fences don't invert the way inline spans do, so the text says they read as markup he didn't ask for rather than inventing a rendering problem, and points at plain indented text or a named file path as the way to hand over something copyable. It was the only copy of the carve-out.
+** DONE [#B] Parked: sentry Living Document updates from two dogfood runs (from takuzu + archangel)
+CLOSED: [2026-07-23 Thu]
+Craig approved 2026-07-23. Four folds applied to =sentry.org=: =--archive-done= touches =.gitignore= on its first run so an "org-only" pass can still produce a commit, a mirror-only project's quiet fires leave no commits so the anchor's heartbeat list is the only record, randomized property sweeps are good quiet-fire work, and the task-audit pass splits into a mechanical hourly subset plus a nightly judgment half. The fifth finding (make the bug hunt official) had already landed as pass 11, so it became a corroboration note. Takuzu's lint-org finding stayed a filed bug task rather than a workflow edit, since it's a defect not a design change.
+** DONE [#B] Parked: clear four lint flags in the notes.org template (from smoke)
+CLOSED: [2026-07-23 Thu]
+Craig approved 2026-07-23. Applied to =claude-templates/.ai/notes.org= and synced to the mirror. The template now lints with zero mechanical and zero judgment findings, down from four. Two fixes beyond what smoke reported: two more column-0 bold lines, and the real cause of the block flags — a literal =** Feature Name= inside the example block that org parses as a heading, cleared by comma-escaping that one line. Worth remembering: those heading flags were mechanical, not judgment, so =lint-org --fix= in any project would have rewritten the template locally and drifted it from canonical.
+** DONE [#B] Parked: clear temp/ during wrap-up teardown (from your roam capture)
+CLOSED: [2026-07-23 Thu]
+Craig approved 2026-07-23. Added =*** Clear temp/= to =wrap-it-up.org= Step 3, after the archive pass, and synced to the mirror. Two guards: confirm before deleting anything that reads as in-progress rather than throwaway (that belongs in =working/=, so move it there), and skip entirely where =temp/= isn't gitignored, since that means the project uses the directory for something else. This closes the last open clause of his 2026-07-20 working/temp capture.
+** DONE [#B] inbox-send leaves a phantom empty handoff in the target inbox :bug:solo:
+CLOSED: [2026-07-23 Thu]
:PROPERTIES:
-:CREATED: [2026-05-28 Thu]
-:LAST_REVIEWED: 2026-05-28
+:LAST_REVIEWED: 2026-07-23
:END:
-Shipped backward-compatibly. New =.ai/scripts/session-context-path= helper resolves the active path from =AI_AGENT_ID=: unset → the legacy =.ai/session-context.org= singleton (one-agent default unchanged, per the spec's compatibility rule), set → =.ai/session-context.d/<sanitized-id>.org=. startup.org's existence check and wrap-it-up.org's rename now resolve through the helper (with a singleton fallback for older checkouts); wrap folds the agent id into the archive name. protocols.org documents the rule. Verified: 5 bats cases + a two-agent simulation showing distinct paths per id. Larger runtime-neutral arc (runtimes/ manifests, launcher refactor) stays parked under the parent spec.
+Fixed 2026-07-23 (speedrun, 0f91a8e). Both send paths write to a .inbox-send-* temp sibling and os.replace it into place, so a mid-write failure leaves no phantom; utf-8 pinned on the write and the roots read; inbox-status skips the in-flight temp. 5 new tests (atomic write, no-partial-on-failure, no-temp-on-success, both paths) + 1 inbox-status bats. Review found and fixed one Important: the temp was discoverable by inbox-status during the write window.
+=inbox-send.py= writes straight to the destination path in the target project's =inbox/=. =Path.write_text= opens with mode =w=, which creates and truncates before any content is written, so *any* failure between opening and finishing leaves a zero-byte =.org= file sitting in another project's inbox.
-Lifted from the broader codex runtime spec ([[file:docs/design/2026-05-28-generic-agent-runtime-spec.org]]) as the immediate-correctness slice independent of the larger arc. The singleton =.ai/session-context.org= is unsafe under simultaneous agents — two LLMs running in the same project at the same time would overwrite each other's session state.
+That file is not inert. =inbox-status= counts it as a pending handoff, so it trips the receiving project's =inbox-boundary-check= Stop hook and blocks a turn there. The receiving agent then has to resolve a handoff with no content and no sender context, which it cannot do from the file. Meanwhile the sender saw an error and will most likely retry, so the target gets a second file too.
-Scope: introduce an =AI_AGENT_ID= environment variable and split the single =session-context.org= into a per-agent =session-context.d/<id>.org= directory. No other phases of the runtime refactor are in this task — keep the surface small, fix the race, ship.
+Reproduced 2026-07-23 end to end: a send whose text contains non-ASCII under =LC_ALL=C PYTHONUTF8=0 PYTHONCOERCECLOCALE=0= fails on the ASCII codec, leaves a zero-byte file in the destination inbox, and =inbox-status= in that project then reports it as pending.
-Touches: =.ai/protocols.org= (rename rule + recovery anchor), =.ai/workflows/startup.org= (Phase A check), wrap-up workflow (rename target), per-project session record discoverability.
+The encoding case is one trigger, not the defect. =write_text= and =read_text= are both called with no =encoding= argument, so they follow the locale; passing =encoding="utf-8"= closes that trigger. The defect underneath is the non-atomic write, which a full disk, a revoked permission, or an interrupted process reaches just as easily.
-Verification: simulate two agents sharing a project (separate AI_AGENT_ID values) and confirm session-context writes land in distinct files without interleaving.
+Grading: Major severity (a phantom handoff in a *different* project's inbox, unresolvable from its own content, that blocks a turn there via the boundary hook) x some users, sometimes (any mid-write failure, of which the locale case is only the one reproduced) = P2 = [#B].
-Parent: see [[Generic agent runtime support — Codex spec v0]] above for the larger arc this is sliced from.
-** DONE [#C] Decide on category-3 rule copies in the deepsat tree :chore:quick:solo:
-CLOSED: [2026-05-31 Sun]
+Fix direction: write to a temp file in the destination directory and =os.replace= into place, so the inbox only ever sees a complete file. Pin =encoding="utf-8"= on both =write_text= and the =read_text= in =resolve_roots=. Same treatment for =send_file=, whose =shutil.copy2= has the same shape. Test by forcing a mid-write failure and asserting the destination directory is unchanged.
+** DONE [#C] Two smaller inbox-send defects: uncaught traceback, duplicate roots :bug:quick:solo:
+CLOSED: [2026-07-23 Thu]
:PROPERTIES:
-:LAST_REVIEWED: 2026-05-28
+:LAST_REVIEWED: 2026-07-23
:END:
-Diffed 2026-05-31. Both copies (coding-rulesets vendored + orchestration_dashboard_mvp) are byte-identical to each other and stale against canonical: =testing.md= 221 lines behind with 5 lines unique to the copies (older wording or a small team tweak), =verification.md= 40 behind with nothing unique. Same older vendored version in both spots. Left untouched per the A1 decision — team-owned, and canonicalizing would create a cross-repo dependency on the private rulesets (the orchestration_dashboard_mvp pair is team-visible from Vrezh's PR thread). No files modified.
+Fixed 2026-07-23 (speedrun, a053e9d). main now catches OSError, so an unreadable source gives the clean inbox-send: error rather than a traceback. discover_projects dedupes on the resolved path. 2 tests.
+Both verified 2026-07-23, both in =.ai/scripts/inbox-send.py=, both low-harm. Grouped because they're the same file and the same fix session.
-While symlinking personal-project =.claude/rules/= mirrors to the rulesets canonical on 2026-05-07, two locations didn't fit the "personal mirror → symlink" pattern and were left untouched pending judgment:
+1. *Uncaught =PermissionError=.* =main= catches =(ValueError, FileNotFoundError)= around the send, but =send_file= reaches =shutil.copy2=, which raises =PermissionError= on an unreadable source. That is an =OSError=, not caught, so the user gets a raw Python traceback instead of the clean =inbox-send: <message>= error every other failure path produces. Reproduced with a =chmod 000= source. No partial file is left in this case. Fix: catch =OSError= alongside the other two.
-- =~/projects/work/deepsat/code/coding-rulesets/claude-rules/{testing,verification}.md= — looks like a vendored team-shared copy.
-- =~/projects/work/deepsat/code/orchestration_dashboard_mvp/.claude/rules/{testing,verification}.md= — could be project-specific overrides.
+2. *Duplicate roots list a project twice.* =discover_projects= appends without deduping, so a roots config naming both a parent and one of its children (=/x= and =/x/proj=) lists =proj= at two different numeric indices. Reproduced via =INBOX_SEND_ROOTS=. Harmless today — both indices resolve to the same project, so no message goes to the wrong place, and Craig's current =~/.claude/inbox-roots.txt= has no overlap. Fix: dedupe on =resolve()= before returning.
-For each: read the file, diff against the rulesets canonical, decide whether it's an intentional diverge (leave alone), stale (sync content), or should canonicalize (replace with symlink and accept the cross-repo dependency). The orchestration_dashboard_mvp pair is the project where Vrezh's PR review surfaced this whole thread, so any decision there has team-visibility implications.
-
-Decision (Craig, 2026-05-31): *leave team-tree copies alone.* Personal rulesets does not reach into team repos — canonicalizing would create a cross-repo dependency on the private rulesets, and the orchestration_dashboard_mvp copy is team-visible. This makes the task solo: diff each copy against canonical, record whether it's identical / drifted / overridden in the disposition, and close as "left alone (team-owned)" without modifying the team-tree files.
-** DONE [#C] Audit language-specific rule files for cross-project duplication :chore:solo:
-CLOSED: [2026-05-31 Sun]
+Grading: Minor severity (one is cosmetic output noise, the other an ugly but accurate failure message; neither loses or misroutes a message) x rare edge case (an unreadable source file, or a roots config with an overlap) = P4... graded up to [#C] rather than [#D] because both fixes are one line each and sit in a script every project depends on for cross-project messaging.
+** DONE [#C] lint-org todo-format checkers fire on spec files :bug:solo:
+CLOSED: [2026-07-24 Fri]
:PROPERTIES:
-:LAST_REVIEWED: 2026-05-28
+:LAST_REVIEWED: 2026-07-23
:END:
-Audited 2026-05-31. Findings: in sync with canonical (=languages/<lang>/claude/rules/=) — work =python-testing.md=, deepsat =typescript-testing.md=, =.emacs.d= =elisp-testing.md= + =elisp.md=. Drifted — =gloss= and =chime= (byte-identical to each other): =elisp-testing.md= 44 lines behind (canonical added Batch-Mode Reproducibility + Isolating Emacs State; zero lines unique to the copies), =elisp.md= one line behind (canonical expanded the edit-cohesively guidance). No project-specific additions anywhere — every copy is either current or purely stale.
+Fixed 2026-07-24 (speedrun, c38bab9). A lo--spec-file-p path guard skips all five todo-format-family checkers on files under docs/specs/. Verified: the nine repo specs go from 100 todo-format findings to zero, todo.org keeps its full checker set. 7 ERT tests.
+=level2-done-without-closed= and =level-2-dated-header= encode =todo.org= completion conventions, but they run against every org file, including =docs/specs/=. A spec's Decisions section legitimately uses =** DONE <decision>= with no =CLOSED:= cookie, and its Review-and-iteration-history section legitimately uses =** <dated> — <who> — <role>= headings. Neither is a completion defect there.
-Disposition: *leave them project-local* (the task's own option). The language-rule copies in code projects are the bundle's deliberate copy-and-sync model, not the symlink pattern the generic rules (commits/testing/verification/subagents) use in personal doc-projects. =sync-language-bundle.sh= auto-fixes drifted bundle rules on each startup, so gloss/chime self-heal the moment those projects next boot — no canonicalize/symlink needed, and symlinking would fight the bundle model. Did not reach into work/deepsat/gloss/chime/.emacs.d from here (cross-project boundary; team copies left alone per the 2026-05-31 category-3 decision).
+Reproduced 2026-07-23 across rulesets' own specs: 100 findings over 7 of the 9 files in =docs/specs/= (docs-lifecycle 27, sentry-workflow 25, autonomous-batch 11, wrapup-routing 11, inbox-consolidation 10, agent-kb 8, encourage-kb 8; the two 2026-07-20 specs are clean). Every project carrying =docs/specs/= inherits the same noise on every sweep. Reported independently by takuzu's first sentry dogfood run.
-The four canonical rules (=commits=, =testing=, =verification=, =subagents=) are now symlinked across the five personal-project mirrors as of 2026-05-07. But several language-specific rule files exist in multiple project mirrors and may be duplicated or drifted:
+Grading: Minor severity (judgment-kind output, so nothing mutates; the cost is noise that trains the reader to skim) x every user, every time (fires on every sweep in every project with specs) = P2... graded down to [#C] because the two inputs disagree: the frequency row is genuinely universal, but Minor severity with zero mutation risk and a known cause reads as P3. Recorded so the read can be argued.
-- =python-testing.md= in =~/projects/work/.claude/rules/=
-- =typescript-testing.md= in =~/projects/work/deepsat/code/.claude/rules/=
-- =elisp-testing.md= and =elisp.md= in =~/.emacs.d/=, =~/code/gloss/=, =~/code/chime/=
+Fix direction: scope the todo-format-family checkers away from =docs/specs/= by path. Correction (2026-07-23 speedrun): the earlier note that these are org-lint's own checkers was wrong — =lo--check-level2-dated-headers= (line 413) and =lo--check-level2-done-without-closed= (line 507) are lint-org.el's own functions, so the fix is a direct local guard, not upstream filtering.
-The Elisp pair is the most suspicious — three repos using essentially the same rules. Audit: diff these across the projects, check for drift, then decide whether to canonicalize them under =~/code/rulesets/claude-rules/languages/<lang>/= and symlink, or leave them as project-local.
-** DONE [#C] Refactor =daily-prep.org= to delegate to =triage-intake.org= for the triage section :chore:solo:
-CLOSED: [2026-05-31 Sun]
+Decision (Craig, 2026-07-23 speedrun pre-flight): scope *all four* todo-format-family checkers, not just the two that fired — the two named plus =subtask-done-not-dated= and =dated-log-heading-active-timestamp=, which would misfire the same way on a spec's dated review-history headings. Path-based (any file under a =docs/specs/= segment), matching the docs-lifecycle canon that specs live there. Takuzu's spec-create alternative fixes new specs only and leaves the nine existing ones noisy, so path-scoping is the general fix.
+** DONE [#C] notes.org template trips four lint-org flags in every project :bug:quick:solo:
+CLOSED: [2026-07-24 Fri]
:PROPERTIES:
-:LAST_REVIEWED: 2026-05-28
+:LAST_REVIEWED: 2026-07-23
:END:
-Collapsed Phase 3's inline source scans (sub-steps 3b email / 3c mark-read / 3d Slack / 3e Linear / 3f PRs / 3g dedup, ~280 lines) into four: 3b runs the triage-intake engine, 3c surfaces today's reactive items as Day's Priorities thin links, 3d re-sorts by urgency, 3e writes the audit footer from the engine's coverage. Source coverage carries via the engine's Phase 0 two-dir glob (general + .ai/project-workflows/ plugins), so the work account's Gmail/Slack/Linear/GHE plugins still get scanned. Adapted the downstream refs (Prep Doc Structure rule, Heads-up FYI source, Recommended Approach Pattern reframed as engine-applied), removed the orphaned Linear-digest note, added a Living Document entry. Verified: workflow-integrity clean (no dangling script refs), sync-check clean, full suite green. daily-prep.org went 825 → 576 lines.
+Already satisfied. The canonical claude-templates/.ai/notes.org was fixed in 10ea44b (2026-07-23) when Craig approved the smoke proposal — the two column-0 bold lines rephrased, the example block's marker comma-escaped. Verified 2026-07-24: the template lints with zero mechanical and zero judgment findings, down from four. This TODO and the applied smoke VERIFY were duplicate work items for the same fix; closing DONE citing the commit rather than filing a no-op VERIFY, since the end-state is verifiable now with nothing to ask.
+The synced =claude-templates/.ai/notes.org= carries two boilerplate lines that open with markdown-style bold at column 0 (=**Session history is NOT in this file.**=, =**For protocols and conventions, see:**=). Org parses a line-initial =**= as a level-2 heading, so =misplaced-heading= flags both, and the =#+begin_example= block in the Pending Decisions instructions trips =invalid-block= twice. Every project inherits all four on every sweep.
-=daily-prep.org= still does its own inline triage (Gmail × 3 accounts, Slack, Linear, GHE PRs, calendars) as part of the full prep flow. =triage-intake.org= is now a source-agnostic engine that loads =triage-intake.<source>.org= plugins (refactored 2026-05-26), so daily-prep could call the engine and consume its synthesis instead of duplicating the source-scan logic. That DRYs up a large workflow and keeps both flows in sync when sources change — a source change now lives in one plugin that both flows pick up.
+Reproduced 2026-07-23. Confirms smoke's proposal. Additional finding from the same run: these two are =mechanical-fixed=, not judgment, so =lint-org --fix= silently rewrites the template's =**bold**= to org =*bold*=. The rewrite is correct org, but it lands on a synced template, so whichever project runs =--fix= first creates drift against canonical.
-Scope:
-- Identify the sections in =daily-prep.org= that do the inline triage (the email / Slack / Linear / PR / calendar fan-out, plus the "Sources checked: ..." footer at the top of each generated prep doc).
-- Replace those sections with "run the =triage-intake.org= engine" and adapt the downstream sections (Heads-up, Day's Priorities, Carry-forwards) to read the engine's synthesis output rather than the inline scan results.
-- Verify the generated prep doc still has the same shape (Heads-up + Day's Priorities + Carry-forwards + Sources checked).
-- Reconcile source coverage: daily-prep's inline triage scans work accounts (3 Gmail, Slack, Linear, GHE PRs) that are project-specific plugins under =.ai/project-workflows/=, not general plugins. The delegation must ensure the engine loads those project plugins (Phase 0 globs both dirs) so nothing daily-prep currently scans drops out.
+Grading: Minor severity (cosmetic noise; the mechanical rewrite is correct org and harmless in isolation) x every user, every time (every project, every sweep) = P2... same disagreement as the checker task above; graded [#C] on the no-real-harm read.
-Origin: came up while authoring =triage-intake.org= on 2026-05-11; body refreshed after the engine/plugin refactor on 2026-05-26.
-** DONE [#C] Templatize =make coverage-summary= into the language bundles (Elisp pilot) :feature:solo:
-CLOSED: [2026-05-31 Sun]
+Fix direction: rephrase both lines in the canonical template so no line starts with =**= (a list dash, or move the bold off column 0), and comma-escape the example block's own markers. One canonical fix clears it everywhere.
+** DONE [#B] cj-remove-block silently deletes content between two cj blocks :bug:solo:
+CLOSED: [2026-07-24 Fri]
:PROPERTIES:
-:LAST_REVIEWED: 2026-05-28
+:LAST_REVIEWED: 2026-07-24
:END:
+Fixed 2026-07-24 (sentry fire 2, 17f5d48). The range validation now looks inside the range and refuses when a second fence appears before the end. Verified it doesn't over-tighten: indented blocks, interior blank lines, legacy single-line annotations, and prose mentioning a fence mid-line all still validate. Second defect fixed in the same commit: remove_range now backs up to /tmp (matching lint-org.el) and writes atomically through a temp sibling, utf-8 pinned. 6 new tests.
+=looks_like_cj_range= validates only that the *first* line of the range opens a =#+begin_src cj:= fence and the *last* line closes with =#+end_src=. It never checks that the range holds exactly one block. A range spanning one block's opening fence to a *later* block's closing fence passes validation, and =remove_range= then deletes everything between — real prose, headings, whole tasks — silently, exit 0.
-Done 2026-05-31 (Elisp pilot, the scoped milestone): ported the kernel into the elisp bundle as a self-contained =languages/elisp/claude/scripts/coverage-summary.el= (no coverage-core dependency), proven end-to-end against the real dotemacs SimpleCov report (93 tracked, 27 untested modules surfaced, project number 66.4%). The missing-file-as-0% + unit-weighted number is the kernel. Delivery: the script ships under =.claude/scripts/= (gitignored, auto-fixed on drift by =sync-language-bundle.sh=); =languages/elisp/coverage-makefile.txt= holds the project-owned Makefile fragment, seeded at project root by =install-lang.sh= and dropped into =.ai/inbox/= by sync when that convention exists. Tests: 12 ERT (=languages/elisp/tests/=, wired into =make test=), 5 new sync bats, 2 new install-lang bats. The fan-out to Python/Go/TS is the follow-up below.
+That is precisely the failure the validation exists to prevent. Its docstring says it "protects against accidentally trimming the wrong block when line numbers drift between a cj-scan call and a remove call", and drift is the normal operating mode: =respond-to-cj-comments= edits the file as it processes each item, and a file being processed for cj comments generally holds several of them.
-Borrow dotemacs's =make coverage-summary= into the language bundles. After =make coverage= writes a coverage file, =coverage-summary= prints per-unit covered/total with percentages, a unit-weighted project number, and a list of source files present on disk but missing from the coverage report.
+Reproduced 2026-07-24 on a fixture with two cj blocks separated by real content. =looks_like_cj_range(lines, 2, 10)= returned =ok=True=, and the removal reduced a 10-line file to its first line. A heading and two content lines were destroyed with no warning and a zero exit.
-*The kernel — the only part worth building.* Weight the project number by file/module rather than by line, and count a source file absent from the report as 0% instead of omitting it. A module no test imports just doesn't appear in coverage.py or nyc output, so it silently fails to drag the number down. That missing-file detection is the value; everything else (per-file table, total) the built-in reporters already print, so don't reimplement those.
+Grading: Major severity (silent destruction of real content in the file that holds Craig's tasks and notes, with no warning and a success exit; git recovers only to the last commit, so intra-session work is lost) x some users, sometimes (needs drift plus multiple cj blocks, which together are the skill's normal operating mode) = P2 = [#B].
-*Scope Elisp-first.* Port the proven dotemacs version into the elisp bundle, prove the pattern end-to-end, then fan out. Don't open all four bundles at once.
+Fix direction: =looks_like_cj_range= must confirm the range contains exactly one block — scan lines start..end and reject when any =#+end_src= appears before the final line, or when any second =#+begin_src cj:= appears after the first. Test with the two-block fixture above asserting the validation refuses.
-*Delivery (settled 2026-05-25).* Two rulesets-owned pieces per language:
-- The summary *script* ships in the bundle under =.claude/= (inside the now-gitignored tooling footprint), copied in on install and auto-fixed on drift by =sync-language-bundle.sh=, never committed by the project.
-- One *text file per language* holding the Makefile fragment (the =coverage-summary= target plus its =coverage= prerequisite) and a block recommending how to set up coverage for that language. The bundle never edits the project's own Makefile.
- - *New project:* install copies that file in for the project to own.
- - *Existing project:* sync drops the fragment into the project's =inbox/= rather than touching its Makefile — the project adopts it deliberately.
+Second defect, same file, same fix session: =remove_range= writes the mutated org file with a bare =path.write_text=, which truncates the target on open, and it takes no backup first. A mid-write failure leaves Craig's =todo.org= truncated. =lint-org.el= — the other tool that mutates these files — copies a backup to =/tmp/<basename>.before-lint-pass.<timestamp>= before touching anything. cj-remove-block should match that: back up first, then write atomically via a temp file and =os.replace=, the same shape shipped for =inbox-send= in 0f91a8e. Pin =encoding="utf-8"= on the read and write while there.
+** DONE [#D] route_recommend downgrades strong to weak on a duplicate basename :bug:quick:solo:
+CLOSED: [2026-07-24 Fri]
+:PROPERTIES:
+:LAST_REVIEWED: 2026-07-24
+:END:
+Fixed 2026-07-24 (sentry fire 2, 1b0f284). The dedupe went into recommend rather than discover_destination_names as the task proposed, so every caller of the pure core is protected, not just the CLI path. Identical names collapse; genuine ambiguity between two different projects still downgrades to weak, pinned by a test. 3 new tests.
+=discover_destination_names= collapses discovered projects to bare basenames (=[p.name for p in ...]=). Two projects sharing a basename across roots (=~/code/notes= and =~/projects/notes=) therefore appear twice in the candidate list, both literal-match the same item, and =recommend= reads =len(strong) > 1= as an ambiguous tie — downgrading a correct strong match to weak.
-*Prerequisite caveat.* The summary presumes a coverage harness exists (undercover, coverage.py, nyc, =go cover=). Several bundles may have no =make coverage= yet, so for those this task implies adding the harness first — or the per-language file documents it as a prereq.
+Reproduced 2026-07-24 by direct probe: =recommend("fix the notes thing", ["notes", "other"])= returns =('notes', 'strong')=, while the same call with =["notes", "notes", "other"]= returns =('notes', 'weak')=.
-Per-language parser (the script is ~40 lines over each tool's output):
-- Elisp: undercover SimpleCov JSON (=.coverage/simplecov.json=) — dotemacs/auto-dim scripts already parse this.
-- Go: =go test -coverprofile=cover.out=; parse =cover.out= (simple text), or lean on =go tool cover -func=.
-- Python: =coverage json= per-file JSON, or lean on =coverage report=.
-- TypeScript/JS: nyc/Istanbul =coverage-final.json= / json-summary.
+Latent, not live: the current project set is 27 projects with 27 distinct basenames, so no collision exists today. The destination stays correct either way; only the confidence tier is wrong, which costs an unnecessary routing prompt rather than a misroute.
-Reference (dotemacs): =scripts/coverage-summary.el=, =modules/coverage-core.el=, and the =coverage= / =coverage-summary= Makefile targets.
+Grading: Minor severity (right destination, wrong tier, cost is one extra prompt) x rare edge case (needs a basename collision across roots, which doesn't currently exist) = P4 = [#D].
-Origin: handoff from the .emacs.d session, 2026-05-25.
-** DONE [#C] Fan out coverage-summary across all language bundles :feature:
-CLOSED: [2026-05-31 Sun]
+Fix direction: dedupe names in =discover_destination_names= (=list(dict.fromkeys(names))=, order-preserving). A duplicate basename is one addressable name as far as routing goes, since =inbox-send='s =find_target= resolves a name to its first match anyway. Add a test with a duplicated candidate asserting the tier stays strong.
+** DONE [#C] audit.bats has a flaky teardown that produces false suite reds :bug:test:solo:
+CLOSED: [2026-07-24 Fri]
:PROPERTIES:
-:CREATED: [2026-05-31 Sun]
+:LAST_REVIEWED: 2026-07-24
:END:
+Fixed 2026-07-24 (sentry fire 3, 7f45d4b). The fixture now sets =maintenance.auto false= and =gc.auto 0= before staging.
+
+Cause, traced not guessed: =git commit= spawns =git maintenance run --auto --quiet --detach= on git 2.55. The commit returns while that detached process is still writing a pack, and teardown's =rm -rf= races it. Every failed run left a =tmp_pack_*= behind, which is what pointed at it.
+
+*My original lead in this task was wrong* and worth recording as such. It blamed =gc.auto='s loose-object threshold; the object counts kill that outright (a fixture holds five objects against a default threshold of 6700). The right knob on modern git is =maintenance.auto=, and the mechanism is the =--detach=, not the threshold. Filing the theory as an explicitly-labelled lead rather than a finding is what kept it from being implemented as fact.
-Done 2026-05-31: coverage-summary now ships in all four bundles. Elisp pilot, then Python, Go, and TypeScript. Each parses its tool's report (SimpleCov / coverage.py JSON / Go cover.out / Istanbul json-summary), counts on-disk source files absent from the report as 0%, and file-weights the project number. The plumbing proved generic: =install-lang.sh= seeds the project-owned =coverage-makefile.txt= and ships the script into the gitignored =.claude/scripts/=; =make test= discovers ERT (=test-*.el=), pytest (=test_*.py=), =go test= (=*_test.go=), and =node --test= (=*.test.js=) under =languages/*/tests/=, each guarded on its toolchain. TypeScript and Go scripts were dogfooded (Go against a live profile, TS against the CLI); Python and TS weren't run against a live coverage tool (coverage.py / nyc not installed) — proven against faithful fixtures matching each tool's stable schema.
+Validation: 20 consecutive runs clean with no leftover fixture directories, against a baseline of roughly one failure in eight. Stated honestly, 20 clean runs alone would be about 7% likely by luck at that rate, so the statistics confirm rather than prove — the trace is the evidence. Disabled the background writer rather than retrying the delete, since a retry loop hides a live process instead of removing it.
+=scripts/tests/audit.bats= test 4 ("tracked project with dirty .ai/ is skipped") intermittently fails in its =teardown=, not its assertions: =rm -rf "$TEST_HOME"= exits non-zero with =rm: cannot remove '/tmp/audit-bats.XXXX/code/alpha/.git/objects': Directory not empty=. The test body passes; only the cleanup fails, and bats reports the whole test as failed.
-Remaining follow-ups (not blockers):
-- Go is a coverage-only slice — =languages/go/= has no rule file, so =sync-language-bundle.sh= can't fingerprint it and won't sync-maintain the script. Build out the real Go bundle (=go.md= / =go-testing.md= + =CLAUDE.md=) to close that.
-- First real adopters of the Python and TS scripts should sanity-check against a live =coverage json= / nyc =coverage-summary.json= run.
+Verified intermittent 2026-07-24: it failed twice during a sentry fire (once in a full =make test=, once running the file alone), then passed three consecutive runs immediately afterward with an identical tree. That intermittency is the defect — it makes =make test= return a false red.
-Original notes retained below for the next person.
+This matters more than a normal flaky test because the green suite is load-bearing for sentry: it gates entry, and the fire-end conditional run gates whether a night's commits are trusted. A spurious red there either blocks a fire or flags a clean night for morning review.
-The Elisp pilot proved the pattern; Python and Go followed. The plumbing is generic: =install-lang.sh= seeds the fragment, and =make test= now discovers ERT (=test-*.el=), pytest (=test_*.py=), and =go test= (=*_test.go=) under =languages/*/tests/=. TypeScript is the last one.
+Grading: Minor severity (a teardown-only failure, no production code implicated, and the assertions themselves pass) x some users, sometimes (fired twice in roughly six runs tonight) = P3 = [#C].
-- TypeScript/JS: nyc/Istanbul =coverage-final.json= / =coverage-summary.json=. Same kernel: file-weighted project number, on-disk =*.ts=/=*.js= absent from the report counted as 0%. nyc prints its own table, so the script focuses on the missing-file list and the number. Needs a vitest/jest (or =node --test=) discovery path in =make test=, mirroring the go-test block.
+Lead, not a verified cause: =audit.bats= line 42 runs =git init -q= in each fixture project and sets no =gc.auto=, while =audit.sh= runs five git commands against them. Git can fork background maintenance (=gc --auto=) that keeps writing into =.git/objects= after the foreground command returns, which would race the teardown's =rm -rf=. That fits the symptom exactly but is untested. Confirm before fixing.
-Notes for the next person, from the Python + Go runs:
-- Python: parses coverage.py's =files[path].summary.{covered_lines,num_statements}= (stable since coverage 5.x), resolves report paths against the report's parent dir. Proven against a synthetic report, not a live =coverage json= run (coverage.py wasn't installed). Sanity-check against a real one.
-- Go: =languages/go/= is a coverage-only slice with no rule file, so =sync-language-bundle.sh= can't fingerprint it (detection keys on a bundle's own =.claude/rules/*.md=). The script is delivered by =make install-lang LANG=go= but is not sync-maintained until the Go bundle gets a real rule file + =CLAUDE.md=. Building out that bundle is the natural companion task. Also: modern =go test ./...= already lists every module package in the profile at 0%, so the missing-file list is usually empty for in-module code; it earns its keep on build-tagged files and dirs outside =./...=.
-** DONE [#C] Enumerate implementation tasks in =spec-review.org= Phase 6 :feature:solo:
-CLOSED: [2026-05-31 Sun]
+Fix direction (once the cause is confirmed): set =git config gc.auto 0= (and =maintenance.auto false=) in the fixture setup so no background writer exists. Failing that, make teardown resilient rather than papering over it — a retry loop hides a real writer instead of removing it, so prefer killing the writer.
+** DONE [#C] todo-cleanup rewrites todo.org with no backup, unlike its siblings :bug:solo:
+CLOSED: [2026-07-24 Fri]
:PROPERTIES:
-:CREATED: [2026-05-28 Thu]
-:LAST_REVIEWED: 2026-05-28
+:LAST_REVIEWED: 2026-07-24
:END:
-Added a Phase 6 step that lifts the spec's =Implementation phases= into a drop-in =todo.org= block (one =[#B]= per phase + a test-surface entry mirroring =Acceptance criteria=); a spec lacking phase decomposition raises that as a finding instead. Added Exit Criterion 6 and a review-history entry. Pure workflow-doc change.
+Fixed 2026-07-24 (sentry fire 4, 0686784). Copies to =/tmp/<basename>.before-todo-cleanup.<stamp>= before the first mutation, matching lint-org.el's convention, once per invocation and skipped under =--check=. 2 ERT tests. The two non-findings recorded in this task's original body stand: the archive move was already fail-safe, and missing-file behavior is unchanged (exit 255, nothing created) — both re-verified against the pre-change version.
+=todo-cleanup.el= rewrites =todo.org= in place (hygiene fixes, =--convert-subtasks=, =--archive-done=, =--sync-child-priority=) and leaves no copy behind. The two sibling tools that mutate the same files both do: =lint-org.el= copies to =/tmp/<basename>.before-lint-pass.<stamp>= and =wrap-org-table.el= does the equivalent. =cj-remove-block= joined them in 17f5d48. todo-cleanup is the outlier, and it is the one that runs most often — wrap-up calls it, and every sentry fire's pass 4 calls it three times (four runs tonight alone).
-From pearl handoff 2026-05-28. =spec-review.org= Phase 6 currently says "log deferred work to =todo.org=: v1 implementation = [#B] ... vNext/someday = [#D]." That covers deferred and v1 in passing but doesn't lift the spec's =Implementation phases= section into a drop-in =todo.org= block.
+Verified 2026-07-24: after a real =--convert-subtasks= mutation on a fixture, the directory holds only =todo.org=. Emacs's own backup mechanism does not fire under =--batch -q=, so there is genuinely no undo short of git, which recovers only to the last commit and loses intra-session work.
-Proposed addition to Phase 6: a structured step that reads the spec's =Implementation phases= section and produces a =[#B] TODO= entry per phase (subject line, tags, one-line body, pointer back to spec), plus a final entry for the test surface (unit / integration / e2e / manual-verify mirroring the spec's =Acceptance criteria= when present). Emit under a new section "Implementation tasks (drop-in for todo.org)" in the review file. Format follows =todo-format.md= (terse heading, body holds context, tags on heading).
+Two things this is NOT, both checked so they don't get re-investigated:
-Three wins: handoff is one paste not a re-read; forces specs to be implementable in pieces (a spec without a phase decomposition fails this step, surfacing the shape problem); closes the loop on =Acceptance criteria= as manual-verify entries.
+- The archive move is *fail-safe*, not lossy. It deletes subtrees from the buffer, writes the archive file, and only saves =todo.org= at the very end, so an archive-write failure aborts before the save. Verified by making the archive directory unwritable: exit 255, =todo.org= byte-identical, content intact. My initial hypothesis that a mid-move failure could lose a subtree from both files was wrong.
+- No error swallowing on the mutation path. The only =ignore-errors= in the file wrap =call-process "git"=, not any write.
-If the spec lacks an =Implementation phases= section, the step is the prompt to ask the author to add one before =Ready=.
-** DONE [#C] Add =.aiignore= for agent inventory exclusions :chore:solo:
-CLOSED: [2026-05-31 Sun]
-:PROPERTIES:
-:CREATED: [2026-05-28 Thu]
-:LAST_REVIEWED: 2026-05-28
-:END:
-Shipped a gitignore-syntax =.aiignore= at the rulesets root (deps, build output, language caches, editor cruft, token artifacts, lockfiles-as-agent-read-skip) and documented the convention + defaults + lockfile policy in protocols.org ("Recursive Reads"). Per Craig's scope call (2026-05-31): did NOT wire audit.sh / diff-lang.sh / sync-language-bundle.sh — they do targeted finds over .ai/.claude/bundle dirs, never naive whole-tree walks, so honoring .aiignore there would be dead code. Script-side honoring belongs in a future catalog/inventory tool if one ships; the real consumer today is agent recursive reads (the protocols guidance).
+So this is a hardening gap rather than an active bug: a future defect in a mechanical rewriter would have no undo.
-From the codex enhancement backlog (item #8). Filesystem scans by agents and helper scripts pick up =node_modules=, =__pycache__=, =.pytest_cache=, lockfiles, generated OAuth artifacts, and test caches, even when those are gitignored. Token waste during exploration and skewed project summaries.
+Grading: Minor severity (no known active defect; the exposure is that any future one is unrecoverable within a session) x every user, every time (it runs on every wrap and every sentry fire) = P2 = [#C].
-Scope: add a shared =.aiignore= file (or =rulesets-ignore.json= if a more structured format helps) listing default exclusions. Teach the scripts that walk the project (=audit.sh=, =diff-lang.sh=, =sync-language-bundle.sh=, future =catalog= work if any) to honor it. Document in =protocols.org= so agents know to consult it before naive recursive reads.
-
-Keep the lockfile policy explicit: ignored when a local skill dependency cache, tracked when reproducibility matters.
-** DONE [#C] Workflow test harness — drift + integrity tests :feature:solo:
-CLOSED: [2026-05-31 Sun]
+Fix direction: back up before the first mutation, matching =lint-org.el='s convention exactly — =/tmp/<basename>.before-todo-cleanup.<YYYYMMDD-HHMMSS>=. One copy per invocation, not per pass, and skip it under =--check= (which writes nothing). Test by asserting the backup exists and holds the pre-edit content after a real mutation.
+** DONE [#C] claude-templates/bin/ gets no lint coverage at all :bug:quick:solo:
+CLOSED: [2026-07-24 Fri]
:PROPERTIES:
-:CREATED: [2026-05-28 Thu]
-:LAST_REVIEWED: 2026-05-28
+:LAST_REVIEWED: 2026-07-24
:END:
+Fixed 2026-07-24 (sentry fire 8, f91feef). lint.sh now sweeps =claude-templates/bin/*= through =check_hook=, matching the extensionless-file shape =languages/*/githooks/*= already uses. Added =scripts/tests/lint-coverage.bats=, which pins the *coverage* rather than current cleanliness: it plants a broken file in each swept location and asserts lint.sh complains, so a location that silently stops being swept fails the suite. The broader question this surfaced — whether rulesets should run shellcheck on its own shell at all — is the VERIFY below and stays Craig's call.
+=scripts/lint.sh= sweeps =scripts/*.sh=, =languages/*/claude/hooks/*.sh=, and =languages/*/githooks/*= through =check_hook= (shebang present, executable bit set). It never touches =claude-templates/bin/=. Verified: zero references to that path in the file.
-From the codex enhancement backlog (item #10). Startup's drift check catches index-vs-directory mismatches but not deeper integrity: a workflow that references a script that's been renamed, a plugin whose parent engine has been deleted, a required section missing from a newly-added workflow.
+Those four scripts — =ai=, =agent-text=, =agent-page=, =install-ai= — are the ones =make install= symlinks into =~/.local/bin=, so they run on Craig's PATH on every machine. They are the *most* exposed shell in the repo and the only shell with no gate over it.
-Scope: add =scripts/tests/workflow-integrity.bats= (or pytest equivalent) verifying:
+All four are clean today (shebangs present, mode 755, shellcheck-clean when run by hand), so nothing is broken. This is a missing gate, not an active defect: the =ai= launcher was hardened to 42 tests recently and that cleanliness is not enforced going forward.
-- Every =.org= file in =.ai/workflows/= is either indexed in =INDEX.org= or classifiable as a source plugin under an indexed engine.
-- Every indexed workflow file actually exists.
-- Every =file:= or shell-command reference inside a workflow to a script under =.ai/scripts/= or =scripts/= resolves to an existing file.
-- Every source plugin maps to a parent workflow that exists and is indexed.
-- Required sections (Overview, When to Use, the workflow's main phases) are present in each workflow.
-- Workflow trigger phrases are unique enough to route — no two workflows claim the same exact trigger.
+Grading: Minor severity (nothing broken now; the exposure is a future regression in a PATH-installed script going uncaught) x every user, every time (every =make lint= silently skips them) = P2 = [#C].
-Wire into =make test=. Run on the canonical =claude-templates/.ai/workflows/= as the source of truth.
-** DONE [#C] Token-tier pilot on largest workflows :feature:solo:
-CLOSED: [2026-05-31 Sun]
+Fix: add =claude-templates/bin/*= to the =check_hook= loop, the same shape =languages/*/githooks/*= already uses for extensionless files. A no-op today by design — it passes immediately — which is exactly what a guard should do.
+** DONE [#A] Applied: the secret-scan pre-commit fails open in ALL FIVE bundles (from .emacs.d)
:PROPERTIES:
-:CREATED: [2026-05-28 Thu]
-:LAST_REVIEWED: 2026-05-28
+:LAST_REVIEWED: 2026-07-24
:END:
+CLOSED: [2026-07-24 Fri]
+Applied 2026-07-24 across all five bundles (11 sites: the secret-scan input in each, plus each bundle's staged-file list, typescript having two). Verified on all five: refuses when the diff can't be read, still blocks a real staged secret, still passes a clean commit. Adopted .emacs.d's elisp bats suite (8 tests) and extended the repo-level cross-bundle suite with two fail-closed assertions.
-Done 2026-05-31: restructured both =startup.org= and =triage-intake.org= into the four-lane structure (Summary / Execution / Reference / History), preserving every existing instruction. triage-intake's reorder ran through a content-preservation guard (the multiset of content lines is unchanged; only heading depth and lane grouping moved). workflow-integrity, sync-check, and the full test suite pass.
+Separate defect found while wiring that up: the cross-bundle suite's VARIANTS list read "elisp bash go" while python and typescript also shipped hooks, so every "in every variant" assertion had silently skipped two bundles since I added them. VARIANTS is now discovered from the tree rather than enumerated — the same failure class the tests exist to catch.
-From the codex enhancement backlog (item #5), scope-limited to a pilot rather than a universal template change.
+What arrived: .emacs.d found that =languages/elisp/githooks/pre-commit= builds its scan input as =added_lines="$(git diff --cached -U0 ... | grep '^+' | grep -v '^+++' || true)"=. With no pipefail and =|| true= swallowing everything, any git failure yields an empty string, so the scan searches nothing, finds nothing, reports clean, and the commit proceeds with the secret in it. The staged-file list feeding the paren check has the identical hole.
-Apply a standardized section structure to the largest workflow files first — =startup.org= and =triage-intake.org= are the prime candidates. Sections:
+*Verified independently, and it is worse than reported.* I reproduced the fail-open with a stub git that fails only the staged-diff call: exit 0 with an AWS-shaped key staged. Then I checked the other bundles, which the sender did not: *bash, go, python, and typescript all carry the same pattern and all fail open the same way.* Confirmed live on all five. Two of those (python, typescript) are hooks I wrote on 2026-07-24 by copying the bash one, so I propagated the defect while closing a different gap.
-- *Summary* / *Quick Contract* — one-screen purpose and outputs.
-- *Execution* — the steps an agent must follow.
-- *Reference* — examples, edge cases, rationale, old decisions.
-- *History* / *Design Notes* — durable context not needed every run.
+Graded [#A] on severity alone, per the todo-format security carve-out: a credential-scanning gate that reports clean without having looked is a showstopper regardless of how rarely git fails.
-Decision (Craig, 2026-05-31): *approved the four-lane structure (Summary/Execution/Reference/History) and the scope — restructure both =startup.org= and =triage-intake.org= now.* Makes the task solo: apply the lanes to both, preserving every existing instruction (reorganize, don't rewrite), verify the workflows still read coherently and the drift/integrity checks pass.
+The sender's fix is correct and I verified all three axes on it: refuses to proceed when the diff cannot be read (exit 1), still blocks a real staged secret (exit 1), still passes a clean commit (exit 0). It splits the git read from the greps so a git failure aborts while "grep matched nothing" stays the ordinary case.
-Teach startup/routing to read =Summary= only at routing time, then =Execution= only for the selected workflow. Other sections become opt-in.
+Decision needed: the fix as sent covers elisp only. It should be applied to all five bundles, which is my scope expansion rather than the sender's proposal — hence a VERIFY rather than a silent apply. Also unresolved: where the two attached bats suites live, since the hooks are rulesets-owned and the tests currently sit in the consuming project.
-After the pilot, evaluate: did the savings show up in real session token use? Did the structure constrain the workflow expressiveness too much? If yes to savings and no to constraint, expand to the next-largest workflows. If not, document why and stop. Don't templatize universally — shorter workflows don't need tiering.
-** DONE [#B] Add Signal MCP server (rymurr/signal-mcp) :feature:
-CLOSED: [2026-06-02 Tue]
+Prepared: [[file:working/hook-fail-open/pre-commit.diff]], plus =test-pre-commit-hook.bats= (8 tests) in the same dir.
+** DONE [#B] Applied: remove the validate-el auto-test cap (from .emacs.d, Craig's call)
:PROPERTIES:
-:CREATED: [2026-05-29 Fri]
-:LAST_REVIEWED: 2026-05-29
+:LAST_REVIEWED: 2026-07-24
:END:
-Done 2026-06-02. Registered signal-cli to the Google Voice pager account, added the signal-mcp entry to servers.json, installed via make install-mcp (claude mcp list shows it connected), and documented the signal-cli + GV dependency in mcp/README.org. The GV-registration dependency this task flagged is resolved. Shipped in cfaff12 (page-signal routing) and this commit (README).
-
-Install [[https://github.com/rymurr/signal-mcp][rymurr/signal-mcp]] so Claude can call =send_message_to_user=, =send_message_to_group=, and =receive_message= natively rather than shelling out to the =page-signal= wrapper. Python, MCP framework, depends on =signal-cli= being configured locally.
-
-Two-way capability is the differentiator over the CLI: =receive_message= lets the agent listen for replies on the phone, enabling page-as-confirm flows, "should I proceed?" loops over Signal, and structured Q&A across devices.
+CLOSED: [2026-07-24 Fri]
+Applied 2026-07-24. Cap and the unreachable notice helper removed; the gate is now count -ge 1. Adopted the rewritten bats suite (6 tests), with its hook path corrected from the installed .claude/hooks/ layout to the bundle's claude/hooks/ — the re-homing mismatch the sender flagged.
-*** Dependency
+What arrived: a superseding handoff. The first proposed a loud notice when the test count exceeds =MAX_AUTO_TEST_FILES=20= (above the cap the block was skipped, nothing printed, exit 0 — indistinguishable from a pass). Craig chose in the .emacs.d session to remove the cap instead.
-This depends on the Google Voice account being registered with =signal-cli= first. Sending from Craig's primary number to itself doesn't notify (Signal treats it as one account on linked devices). The MCP server takes =--user-id= at startup, one account per instance, so it has to point at the GV account, with the primary as the per-send recipient.
+The reasoning is the valuable part. Measured, a whole family runs in under two seconds (calendar-sync 63 files / 633 tests / 0.9s), so the cap bought nothing. Worse, it was *concealing* a real cross-test pollution bug: calendar-sync exits 1 when its 63 files run in one process, because one test marks a calendar as syncing and never resets it, and a sibling file's test then hits the stale guard. Invisible from both directions — =make test= runs each file in its own Emacs, and the hook skipped the family for being over the cap.
-If GV registration is still pending when this task runs, block here and surface that.
+Verified the superseding file drops the cap entirely (zero references, gate is now =count -ge 1=) and removes the now-unreachable notice helper.
-*** Implementation
+Since Craig already made this call, the remaining decision is only adoption scope: removing the cap means other consuming projects run every stem-matched test file per edit, and one may go red on first use by surfacing pollution that per-file runs hid. The sender flags that as intended.
-- =mcp/servers.json= — add =signal-mcp= entry under stdio transport (=command=, =args=, optional =env= for the user-id pointer).
-- =mcp/README.org= — document the signal-cli + GV-registration dependency and the user-id pattern.
-- =mcp/secrets.env.gpg= — only if the MCP server's user-id needs to be encrypted (probably not; the GV number isn't a secret beyond being personal).
-- Verify: =make install-mcp= followed by =make check-mcp= shows =signal-mcp ok=; smoke-test via a Claude tool call sending a message + waiting on =receive_message=.
-
-*** Why this matters
-
-=page-signal= is the fast path (a hook, a script, a make recipe can call it without an MCP round-trip). The MCP server is the smart path. When Claude wants to send and then *react to the reply*, the CLI can't do that — only the MCP server can. The two complement each other; this task adds the second half.
-** DONE [#C] task-review pass at end of task-audit :chore:solo:
-CLOSED: [2026-06-02 Tue]
-:PROPERTIES:
-:LAST_REVIEWED: 2026-06-02
-:END:
-Have the =task-audit= workflow chain a =task-review= pass as its final phase, so a freshly-audited list also gets the lighter staleness/honesty sweep without a second invocation. The legend already notes the division of labor — task-audit assigns and refreshes tags, task-review keeps them honest in passing — so running task-review at the tail of task-audit closes the loop in one pass. Edit =claude-templates/.ai/workflows/task-audit.org= (and the synced mirror) to add the final phase; check whether =open-tasks.org= already invokes task-review so the chaining stays consistent.
-** DONE [#C] lint-followups drift — reconcile-on-write + audit dead-link reaping :feature:solo:
-CLOSED: [2026-06-02 Tue]
+Prepared: [[file:working/hook-fail-open/validate-el.diff]], plus =test-validate-el-hook.bats= (6 tests, two of which stage a deliberately failing test so a quiet pass proves the run happened).
+** DONE [#C] lint-org invalid-block false-positives inside example/src blocks :bug:solo:
+CLOSED: [2026-07-24 Fri]
:PROPERTIES:
-:LAST_REVIEWED: 2026-06-02
+:LAST_REVIEWED: 2026-07-24
:END:
-From an .emacs.d handoff (2026-06-02): running task-audit against a large todo.org proved several =.ai/lint-followups.org= entries stale (four dead-link flags pointed at docs that now exist; three near-duplicate dated lint runs had piled up). Two fixes, scoped separately.
+Fixed 2026-07-24, test-first. =lo--matched-block-regions= scans lines for correctly paired blocks under org's real rule — once a block is open, only its own =#+end_TYPE= closes it — and =lo--handle-item= drops an =invalid-block= finding whose line falls in one, delimiters included (org-lint reports at the delimiters themselves). Line-scanning rather than asking org is the point: org's parser is what mis-reads these blocks. 4 ERT tests: the heading-in-example case, a src block holding a literal =#+end_example=, a genuinely unterminated block that must still report, and a file with one of each proving the suppression is per-block not per-file.
-1. =lint-org= workflow/script (the real fix): reconcile-on-write. Before appending a run, drop entries whose finding no longer reproduces (dead link now resolves, flagged block/timestamp now clean) and dedupe against the prior run instead of re-logging. Key entries by content/finding rather than line number, so they survive edits to the target file (line numbers go stale immediately).
-2. =task-audit.org= (small, narrow): in the Phase C link-hygiene step, when fixing/verifying a =file:= link, also reap any matching dead-link entry in the project's lint-followups file so the two artifacts don't drift. Scope explicitly to dead-link entries — do NOT pull general lint cleanup into the audit; that mixes two concerns and slows the audit.
-** DONE [#C] start-work Justify gate: explicit "reasons not to do this" item :feature:quick:solo:
-CLOSED: [2026-06-02 Tue]
-:PROPERTIES:
-:LAST_REVIEWED: 2026-06-02
-:END:
-From a work handoff (2026-06-02, surfaced running /start-work on a clean low-risk refactor). The Phase 2 Justify gate has "Downsides" and "Alternatives considered" but no forced devil's-advocate verdict on "should we even do this?" Add a "top reasons not to do this" item: surface the top three objections if any exist; when none rise to a real objection, state one line instead of manufacturing three (e.g. "Nothing material argues against this; no reason to defer or drop it"). Building the case against the work before committing is cheapest exactly at this gate, which is its purpose. Edit the start-work skill's Justify-gate phase.
-** DONE [#C] start-work Approach gate: spec-needed check :feature:quick:solo:
-CLOSED: [2026-06-02 Tue]
-:PROPERTIES:
-:LAST_REVIEWED: 2026-06-02
-:END:
-From Craig (2026-06-02). The Approach phase should consider whether the work needs a spec when one doesn't already exist. For a big task, this isn't a silent skip — the pre-confirmation summary must explicitly report why a spec isn't needed, so the decision is visible and challengeable at the gate rather than assumed. Small tasks can pass without comment. Edit the start-work skill's Approach-gate phase to add the spec-needed consideration and the big-task report-why-not requirement.
-** DONE [#B] Cross-project pattern catalog :spec:thinking:
-CLOSED: [2026-06-05 Fri]
-:PROPERTIES:
-:LAST_REVIEWED: 2026-06-02
-:END:
+Verified against home's fixture end to end: =/home/cjennings/projects/home/.ai/notes.org= produced exactly the two reported findings (lines 386, 398) under the pre-change script and zero under the new one, file untouched. Full suite green.
-From pearl handoffs [[file:docs/design/2026-05-27-pattern-catalog-pearl-notes.org][2026-05-27]] + [[file:docs/design/2026-05-28-pattern-catalog-no-empty-input.org][2026-05-28 follow-up]].
+Left alone deliberately: the =,**= comma-escape in =claude-templates/.ai/notes.org= line 53. The task noted the fix "lets that escape be reverted," but the escape is the documented org convention for a literal =**= inside a verbatim block, so reverting it would trade correct org for no gain now that the finding is suppressed either way.
-Meta-question: how do good patterns travel from project A to project B? Pearl shipped three worked examples worth capturing — one-prompt picker with typed prefix (pearl-pick-source), magit-transient state buttons, and "no empty input as meaningful" (none-sentinel as first candidate). Each is a small principle with wide surface area; without a catalog, every project re-derives them from scratch.
+home reported it, and I verified it: an =#+begin_example= block whose body contains a line beginning =** = (or any heading-shaped line) makes =invalid-block= flag *both* delimiters as "Possible incomplete block", even though the block is correctly paired. The checker reads the heading line inside the verbatim body as a structural break and loses the open block.
-Open design questions before any implementation:
-- Catalog format — structured (one pattern per file with frontmatter) vs free-form doc
-- Surfacing mechanism — agent-driven (model spots opportunity) vs human-driven (Craig grep-searches)
-- Anti-patterns included or only what worked
-- Intake cadence — every time one lands, or batch review
-- Home — rulesets repo (agent visibility) vs Linear doc vs per-project cross-links
+Reproduced 2026-07-24 on a three-line example block: 2 judgment findings, both false. This is a docs-file-common shape — any org file documenting org syntax inside an example block hits it (home's notes.org PENDING DECISIONS section does).
-Pearl recommends a one-page spec (problem + design + open questions + acceptance) before implementation. Pearl available to come back for spec-review iterations.
+*This is the root cause behind a workaround I already shipped.* During fire 1 last night I comma-escaped exactly this =** Feature Name= line in =claude-templates/.ai/notes.org= to silence the two findings. That fixed the symptom in one file; the checker bug it worked around is still live and recurs everywhere. Fixing the checker lets that escape be reverted.
-*** 2026-05-28 Thu @ 08:12:55 -0500 Pearl shipped patterns 4-6, filed alongside the prior two
-Three more pearl handoffs landed and were filed during this audit. Filed: [[file:docs/design/2026-05-28-pattern-catalog-prompt-labels-and-defaults.org][prompt-labels-and-defaults]] (patterns 4-5: label-matches-behavior, default-most-common with friction-proportional-to-consequence) and [[file:docs/design/2026-05-28-pattern-catalog-prompt-collapse.org][prompt-collapse]] (pattern 6: collapse N orthogonal prompts into one enriched prompt). The catalog's evidence base is now four pearl notes in =docs/design/= covering six patterns plus the synthesizing principle Pearl articulated — "choices on screen, accurately labeled, ordered by what the user most often wants, friction sized to the cost of being wrong."
+Grading: Minor severity (judgment output, nothing mutates; pure noise that trains the reader to skim) x most users, frequently (every org file documenting org syntax in a verbatim block) = P3 = [#C].
-*** 2026-06-05 Fri @ 00:47:59 -0500 Spec approved as written — all 5 decisions + 3 open questions accepted
-Craig approved the spec ([[file:docs/design/2026-06-02-pattern-catalog-spec.org][2026-06-02-pattern-catalog-spec.org]]) as written. Confirmed: one file per pattern with frontmatter; home =patterns/= in rulesets; thin =claude-rules/patterns.md= pointer, agent-driven; anti-patterns as a per-pattern field; capture-on-landing/promote-on-review intake. Open questions resolved to the spec's leans: directory name =patterns/=; concrete-now, generalize-on-second-use; manual promote flow first, no =/pattern= skill yet. Built as =.org= files with =#+KEYWORD= frontmatter (Craig's call over the initial =.md= draft); the =claude-rules/patterns.md= pointer stays =.md= since the rules layer and the Makefile glob require it.
+Fix direction (per home): while inside a =begin_example= / =begin_src= block, skip structural parsing of the body until the matching =#+end_= line — verbatim blocks contain no headings, timestamps, or delimiters by definition. The same class hits a src block containing =#+end_example= as literal text. Tagged :solo: because the fix and its test are mechanical.
-*** 2026-06-05 Fri @ 00:47:59 -0500 Built the catalog — 6 seed patterns + pointer + README
-Created =patterns/= with the six seed patterns (one-prompt-picker-typed-prefix, transient-state-buttons, no-empty-input-as-meaningful, label-matches-behavior, default-most-common-friction-proportional, collapse-orthogonal-prompts), each carrying the frontmatter contract (name/principle/problem/tags/source/examples) plus Problem/Do/Anti-pattern/Applicability/Related sections. =patterns/README.org= states the root principle, the frontmatter contract, and the intake cadence. =claude-rules/patterns.md= is the agent-facing pointer, auto-installed via the Makefile RULES glob. Sourced from the four pearl notes in =docs/design/=.
-** CANCELLED [#C] Try Skill Seekers on a real DeepSat docs-briefing need :chore:
-CLOSED: [2026-06-10 Wed]
+*** 2026-07-24 Fri @ 20:10:00 -0500 Open question settled — invalid-block is org-lint's, so the fix is a filter
+Home answered it and I re-verified both halves here: =grep invalid-block= over =lint-org.el= returns nothing, and =org-lint--checkers= enumerates =invalid-block= in batch Emacs alongside =link-to-local-file=, =invalid-babel-call-block=, and =missing-language-in-src-block=. So this is the same shape as the =link-to-local-file= episode: suppress an =invalid-block= finding whose line falls inside a verbatim block, on our side of org-lint's output. No local checker to edit.
+
+Regression fixture, ready to use: home's own =.ai/notes.org= PENDING DECISIONS example block (lines 386-398) holds an unescaped =** Feature Name or Topic= and trips both delimiters — findings at lines 386 and 398. Home is deliberately leaving its copy unescaped so it stays a fixture, and asked to be pinged when the filter lands so it can re-run. The filter should take those two findings to zero without touching the file.
+** DONE [#C] sentry.org calls one loop cycle a "fire" :chore:solo:
+CLOSED: [2026-07-28 Tue]
:PROPERTIES:
-:LAST_REVIEWED: 2026-05-28
+:LAST_REVIEWED: 2026-07-28
:END:
+Done 2026-07-28. 72 noun-sense instances in =sentry.org= became "cycle"; the four verb-sense uses stayed. Three more lived outside the file — =wrap-it-up.org= ("a crashed fire"), =todo-cleanup.el= and its test ("every sentry fire") — all naming a sentry cycle, so they moved too. home's proposal was scoped to =sentry.org= alone, so the leak would have split the vocabulary across files.
-=Skill Seekers= ([[https://github.com/yusufkaraaslan/Skill_Seekers]]) is a Python
-CLI + MCP server that ingests 18 source types (docs sites, PDFs, GitHub
-repos, YouTube videos, Confluence, Notion, OpenAPI specs, etc.) and
-exports to 20+ AI targets including Claude skills. MIT licensed, 12.9k
-stars, active as of 2026-04-12.
+Craig read home's "nine fires" as nine emergencies and went looking for what was burning (2026-07-28): "I assume you mean nine crises, not nine loop cycles and I begin to get scared." The term reaches him directly — digest headings render as =** Fire 11 — 08:32 CDT= in the anchor he reads every morning. 35 instances in =sentry.org=.
-*Evaluated: 2026-04-19 — not adopted for rulesets.* Generates
-*reference-style* skills (encyclopedic dumps of scraped source material),
-not *operational* skills (opinionated how-we-do-things content). Doesn't
-fit the rulesets curation pattern.
+Grading: Minor severity (a user-facing artifact that miscommunicates, no data loss) x most users frequently (every digest, on every project running sentry) = P3 = [#C].
-*Next-trigger experiment (this TODO):* the next time a DeepSat task needs
-Claude briefed deeply on a specific library, API, or docs site — try:
-#+begin_src bash
-pip install skill-seekers
-skill-seekers create <url> --target claude
-#+end_src
-Measure output quality vs hand-curated briefing. If usable, consider
-installing as a persistent tool. If output is bloated / under-structured,
-discard and stick with hand briefing.
+*Take the problem, not home's proposed term.* home proposed "pass", reasoning that it already lives in the file's vocabulary. That is exactly what disqualifies it. =sentry.org= already uses "pass" as a precise numbered noun: "the pass list", "the Pass Runner", "eleven finding/hygiene passes", "pass 12" for the implementation pass. There are exactly eleven hygiene passes, so home's proposed digest heading =** Pass 11= collides with an existing real referent. Renaming would trade a term Craig misreads as urgent for one that is genuinely ambiguous.
-*Candidate first experiments (pick one from an actual need, don't invent):*
-- A Django ORM reference skill scoped to the version DeepSat pins
-- An OpenAPI-to-skill conversion for a partner-vendor API
-- A React hooks reference skill for the frontend team's current patterns
-- A specific AWS service's docs (e.g. GovCloud-flavored)
+Counter-proposal: *cycle*. It appears zero times in =sentry.org=, so there is nothing to collide with. It is also Craig's own word from the very quote that surfaced this ("not nine loop cycles"). =** Cycle 11 — 08:32 CDT= reads cleanly. Checked and rejected: "sweep" (already used for hygiene and property sweeps) and "run" (already used as a noun, "first live run").
-*Patterns worth borrowing into rulesets even without adopting the tool:*
-- Enhancement-via-agent pipeline (scrape raw → LLM pass → structured
- SKILL.md). Applicable if we ever build internal-docs-to-skill tooling.
-- Multi-target export abstraction (one knowledge extraction → many output
- formats). Clean design for any future multi-AI-tool workflow.
+Keep the verb sense of fire throughout ("the notify fires", "the path never fires") — only the noun meaning one loop cycle changes. Past session anchors are historical records and stay as written.
-*Concerns to verify on actual use:*
-- =LICENSE= has an unfilled =[Your Name/Username]= placeholder (MIT is
- unambiguous, but sloppy for a 12k-star project)
-- Default branch is =development=, not =main= — pin with care
-- Heavy commercialization signals (website at skillseekersweb.com,
- Trendshift promo, branded badges) — license might shift later; watch
-- Companion =skill-seekers-configs= community repo has only 8 stars
- despite main's 12.9k — ecosystem thinner than headline adoption
-** DONE [#C] Promote meeting-prep to a template workflow :feature:solo:
-CLOSED: [2026-06-10 Wed]
-:PROPERTIES:
-:LAST_REVIEWED: 2026-06-10
-:END:
-meeting-prep lives in the work project's =project-workflows/= and is general-purpose — it builds a per-meeting prep doc — but its body carries project-specific references: =deepsat/assets/= transcript paths, Linear as the tracker, =knowledge.org=. Promoting to =claude-templates= means generalizing those to project-neutral terms (the project's transcript home, the project's tracker), adding it plus its =meeting-prep.pre-wire.org= supporting doc to the =.ai/= mirror and INDEX.org, and a workflow-integrity pass. Once promoted, the daily-prep 5-Day Look-Ahead's conditional "where the project has one" reference can become a direct link.
+Craig approved "cycle" on 2026-07-28. That settles the only judgment the task carried, so it is =:solo:= now: the surface is the 35 noun-sense instances in =sentry.org=, and the completion check is objective — zero noun-sense "fire" left, verb sense untouched, lint clean, suite green, canonical and mirror in sync.
-Out of the 2026-06-10 daily-prep handoff from the work project.
-** DONE [#C] Build Craig's writing voice profile from real corpora :spec:
-CLOSED: [2026-06-10 Wed]
+Source: home handoff, 2026-07-28.
+** DONE [#B] references/ is linked from protocols.org but never synced :bug:
+CLOSED: [2026-07-28 Tue]
:PROPERTIES:
-:CREATED: [2026-05-29 Fri]
-:LAST_REVIEWED: 2026-05-29
+:LAST_REVIEWED: 2026-07-28
:END:
-Shipped across 2026-05-29 → 2026-06-10. =voice/references/voice-profile.org= is the canonical paired file: Phases 1-2 corpora measured (commit bodies 128k words + email/PR/review registers), all 45 patterns carry entries with basis and history, and every reconciliation delta landed in =voice/SKILL.md= (#13/#33 self-discipline reframing, #7 soft flag, new corpus-derived #43-#45). Extension corpora (Slack, long-form, syntactic fragment detection) deliberately not pursued.
-
-Build a grounded profile of Craig's actual writing voice by mining the corpora he's produced over time. The =voice/SKILL.md= patterns today are observation-derived (em-dash zero-tolerance, semicolon → period, contractions kept, sentence-fragment rewrite, felt-experience cut, etc.). Some are spot-on; others are intuition. A real corpus pass would tell us which patterns are genuinely Craig's voice and which were guesses, plus surface idioms, sentence structures, and vocabulary the current ruleset misses.
+Craig picked option 1 on 2026-07-28: drop the link, point at the calendar workflows instead. The four of them sync already and carry the MCP tool names, both account ids, the gcalcli fallback, and the conflict-check discipline — everything the reference was cited for.
-*** Sources to mine
+The adversarial review then returned =Needs Discussion= and widened the fix twice, both correctly:
-- *Email* — sent folders across all three accounts (=gmail=, =dmail/DeepSat=, =cmail/Proton=). Filter to Craig-authored (not forwards or replies-just-quoting). Separate work voice (=dmail=) from personal voice (=gmail=, =cmail=) since they're likely distinct registers.
-- *Commit messages* — =git log --author= across his repos. Captures terse-imperative voice.
-- *PR descriptions and review comments* — same corpora. More deliberate prose than commits.
-- *Org files he authored* — =notes.org=, todo bodies he typed, design docs in =docs/design/=, journal entries. Heavier on first-person voice than emails.
-- *Slack/messages* — DeepSat work slack, family group, friends. Casual register.
-- *Long-form artifacts* — résumé, proposals, white papers, blog posts (if any).
+- My first replacement said credentials "live in the rulesets repo" without naming a file. The only calendar-named document there was =calendar-reference.org=, whose three credential paths have all been dead since the OAuth keys moved into the encrypted MCP bundle in May. So the prose sent a reader to a stale file, which fails more quietly than the dead link it replaced. Now names =mcp/README.org=, the real authority, verified to document =gcp-oauth.keys.json= as gitignored and regenerated at install.
+- =calendar-reference.org= was left orphaned by the link removal: zero live inbound references, two copies, and =references/= is not in =sync-check.sh='s gate (=paths=(protocols.org workflows scripts)=), so the copies could drift silently. Deleted both, and the empty =references/= directories with them. Its operational content is fully covered by the four workflows and its credential paths were all stale, so nothing live was lost.
-Skip session-context files, which are Claude-co-written and would muddy the signal.
+The review also found the same defect class at seven other sites, filed separately.
-*** Output
+One fact died with the file, dropped deliberately rather than by accident: that the Google Cloud app runs in production mode, so tokens don't expire after seven days. It's checkable in the console, and it was the last live line in a document whose other credential facts had all gone stale.
-- =voice/references/voice-profile.org= (or =.md=) — the canonical reference doc:
- - Vocabulary tendencies (preferred verbs, avoided cliché classes, technical-vs-plain word choice).
- - Sentence structures (typical length, conjunction patterns, parenthetical use).
- - Punctuation patterns (em-dash actual frequency, semicolon vs period split, contraction rate).
- - Register markers (signs of formal vs casual mode, work vs personal).
- - Idioms and recurring phrasings.
- - "Anti-patterns" — phrasings Craig consistently avoids that show up in AI-generated prose.
-- Updated =voice/SKILL.md= patterns grounded in evidence rather than intuition. Patterns that the corpus confirms get strengthened; patterns the corpus contradicts get rewritten or removed.
+=protocols.org:273= links to =references/calendar-reference.org=, but startup.org's rsync copies only =protocols.org=, =workflows/=, and =scripts/=. So the link is dead in every consuming project. Confirmed: the file exists at =claude-templates/.ai/references/=, and neither home nor =.emacs.d= has a =.ai/references/= at all.
-Each finding should cite at least two evidence samples from the corpora so the basis for a rule is reviewable.
+Grading: Minor severity (a documented reference an agent can't follow, workaround is to search or ask) x every user every time (every consuming project, every sync) = P2 = [#B].
-*** Approach
+Pinned 2026-07-28 for Craig's decision. Analysis below is complete; only the choice is open.
-Phase 1 (corpus assembly) — pull the relevant slices: sent-mail dumps, =git log --author --no-merges --pretty=format:'%B'=, =gh pr list --author= bodies, org-file extracts. Strip headers, replies-quoted blocks, signatures. Land in =voice/corpus/= (gitignored if the project's =.ai/= is gitignored, tracked if private repo with private remote).
+*Revised recommendation: drop the link, don't add the sync.* My first read was add =references/= to the rsync. Looking at what the directory actually holds reversed it:
-Phase 2 (analysis) — pass over the corpus with focused queries: distribution of em-dashes per 1000 words, semicolon count, contraction frequency by register, sentence-length histogram, top-N adjectives/adverbs, etc. Subagent dispatch fits here.
+- =references/= holds exactly one file, and =protocols.org:273= is its only citation anywhere in the tree.
+- The four calendar workflows (=add-=, =edit-=, =delete-=, =read-calendar-event(s).org=) already sync to every project, and already carry the operational detail: the MCP server name, both account IDs, the gcalcli fallback, conflict-checking. None of them cites =calendar-reference.org=; they are self-sufficient.
+- What the reference uniquely adds is credential *file locations* (an OAuth keys path, a GPG-encrypted gcalcli secret) — no secret values. Re-auth is a rare operation Craig performs himself, not something an agent in another project needs a pointer to.
-Phase 3 (draft profile) — write =voice-profile.org= with findings + evidence. Surface contradictions with the current ruleset.
+So adding a synced directory carrying =--delete= semantics, to deliver one mostly-redundant file, is a poor trade. It also cuts against the rightsizing work: =protocols.org= is read into context every session, and a dead link is noise in a file being slimmed.
-Phase 4 (reconcile with voice/SKILL.md) — present the deltas to Craig. Each delta is one of: confirm existing rule with evidence, strengthen rule, weaken rule, add new pattern, remove unsupported pattern. Apply approved deltas.
+Preferred fix: replace the link with a one-line pointer to the calendar workflows, which travel and are current. The reference file stays rulesets-only for the credential locations.
-*** Privacy
+Alternatives if Craig prefers: add =references/= to the rsync (the =--delete= hazard is not present today — work is the only project with a =.ai/references/= and its copy is byte-identical), or fold the credential locations into the calendar workflows and then drop the link (most complete, but spreads local absolute paths across four more synced files).
-Email and Slack content is private. The corpus must NOT enter any commit unless rulesets stays on the private cjennings.net remote (which it does today). If a future move to a public remote is on the table, the corpus and any direct quotes have to go before that happens. The profile doc itself can stay (it's analysis, not raw content), but cite by pattern not by verbatim quote.
+Not =:solo:= — it changes a synced template, which needs Craig's approval by the inbox rule, and the sync-vs-drop choice is his.
-*** Why this matters
-
-The voice skill earns its place when Craig sees the rewrite and recognizes it as his own voice rather than a "clean" AI voice that approximates him. Today the skill catches common AI tells (em-dashes, semicolons, the felt-experience tic), which is useful. Corpus-grounding would make it catch the absence of *Craig-specific positive traits* — the phrasings he actually reaches for — not just the AI traits he doesn't.
-
-Likely improves =/voice personal= output quality on PR bodies, commit messages, and email drafts. Compound interest over the long run.
-** DONE [#C] Wide org-table handling — helper/lint/standard :spec:
-CLOSED: [2026-06-11 Thu]
-:PROPERTIES:
-:LAST_REVIEWED: 2026-06-11
-:END:
-The org-table standard keeps project-doc tables <=120 cols with multi-line wrapped cells and a rule between rows, but nothing enforces it and hand-wrapping a wide cell into multi-row form is tedious and error-prone. Decide among: (a) a helper that auto-wraps a wide table into multi-row cells at a target width, (b) a lint check that flags tables over the width budget, (c) tighten the written standard with a worked before/after example. Likely some combination. A worked before/after example exists in a work-project prep doc (a 6-col table reformatted by hand to a 4-col multi-row-cell version), to be reproduced generically when this lands.
-
-Out of a work-project handoff 2026-06-09.
-
-Resolution 2026-06-11: all three shipped. (c) The standard, generalized from the work project's notes.org local copy, is now claude-rules/org-tables.md (globally loaded; render-width semantics — links measure at their visible label, never split a link) with the worked wrapped-table example. (a) .ai/scripts/wrap-org-table.el reflows tables mechanically: render-width measurement, link-atomic tokenizing, column shrink-to-floor allocation, continuation rows, rules between logical rows; idempotent (rule-delimited continuation groups merge back before re-wrapping); 23 ERT tests. (b) lint-org.el gained an org-table-standard judgment check (width overruns, missing rules; conformant wrapped tables not false-flagged); 5 new ERT tests, 32 total. Verified end-to-end on a demo file: 150-col table reflowed to budget, idempotent second pass, lint clean on the result.
-** DONE [#C] SessionStart-on-clear hook for auto-resume :feature:
-CLOSED: [2026-06-11 Thu]
-:PROPERTIES:
-:LAST_REVIEWED: 2026-06-11
-:END:
-Add a SessionStart hook (matcher: clear) in settings.json that auto-injects "read .ai/session-context.org and resume if present, else run startup.org". Today /flush prompts the user to /clear and the next session relies on the model re-reading session-context; the hook makes resume automatic on /clear. Keep full startup.org for genuine fresh starts (new day, other machine, been away). Likely lands as claude-templates workflow notes plus the hook in settings.json.
-
-The checkpoint+resume halves already shipped as /flush. This is the remaining automation piece. Out of a work-project handoff 2026-06-09 (process tooling, belongs in rulesets not the work project).
-
-Resolution 2026-06-11: the hook itself had already shipped 2026-06-02 (hooks/session-clear-resume.sh + the SessionStart clear entry in the tracked settings.json — this task duplicated it). What was actually broken: make install didn't cover hooks, so the symlink never reached machines that hadn't run make install-hooks by hand, and the hook errored silently on every /clear. Fixed by folding default-hook linking into make install (startup's Phase A.0 now propagates hooks machine-wide), with bats coverage in scripts/tests/install-hooks-link.bats. Both hook branches verified on ratio; the live /clear fire is a one-keystroke manual test.
-*** TODO Manual testing and validation :test:
-**** /clear mid-session resumes from the anchor
-What we're verifying: the SessionStart(clear) hook fires and the fresh context resumes instead of cold-starting.
-- In any project session with a live .ai/session-context.org (this rulesets session qualifies), type /clear
-- Send any short message (the injected context loads but the model waits for your next keystroke)
-Expected: the reply starts with "flushed." on its own line, restates the Active Goal and immediate Next Step, and does NOT run the startup workflow.
-** 2026-06-12 Fri @ 02:56:58 -0500 New personal projects are home regroupings — no mechanism needed
-Craig's call (2026-06-12): new personal projects will live in home, and there's no project-creation mechanism to build — he'll be working in home and simply decide to group some things differently. Nothing to do.
-
-Concurrence, verified: no template doc directs new personal work into ~/projects (first-session.org, install-ai.sh, and the README carry no such guidance; the only ~/projects references are discovery-root scans, which home and work still need). The situation as it stands: a new personal "project" is an area dir plus tasks inside home's existing =.ai/= machinery, no bootstrap step; =first-session.org= remains the bootstrap for standalone code projects in ~/code, unchanged and correct; "launch finances"-style trigger phrases for folded names degrade politely to the no-match candidate list, worth work only if real friction shows up.
-
-** DONE [#C] Build =/update-skills= skill for keeping forks in sync with upstream :feature:
-CLOSED: [2026-06-11 Thu]
-:PROPERTIES:
-:LAST_REVIEWED: 2026-06-10
-:END:
-
-The rulesets repo has a growing set of forks (=arch-decide= from
-wshobson/agents, =playwright-js= from lackeyjb/playwright-skill, =playwright-py=
-from anthropics/skills/webapp-testing). Over time, upstream releases fixes,
-new templates, or scope expansions that we'd want to pull in without losing
-our local modifications. A skill should handle this deliberately rather than
-by manual re-cloning.
-
-Shipped 2026-06-11: [[file:.claude/commands/update-skills.md][/update-skills command]] + [[file:scripts/update-skills.py][helper script]] (17 bats tests) + three bootstrapped manifests under [[file:upstreams/][upstreams/]]. The first real upstream drift will exercise the interactive per-file/per-hunk flow end to end; the merge mechanics are covered by the test suite.
-
-*** 2026-06-11 Thu @ 17:05:28 -0500 Specification written as the shipped artifacts
-The command doc ([[file:.claude/commands/update-skills.md][update-skills.md]]) carries the user-facing spec: discovery, classification statuses, the per-file confirmation and per-hunk conflict flow, mark-synced semantics, and the missing-baseline fallback. The script's module docstring specifies the manifest schema. Two deviations from the 2026-05-16 design, with reasons: manifests live centrally at =upstreams/<name>/= instead of per-skill =.skill-upstream= dotfile dirs (arch-decide became two flat files in =commands/= and can't carry one — a =files= rename map covers it); baselines were seeded from the 2026-06-11 upstream HEADs since the true fork-point commits are unrecoverable, so pre-existing local modifications classify as =local-only= going forward.
-
-*** 2026-05-16 Sat @ 01:14:20 -0500 original goals and decisions
-**** Design decisions (agreed)
-
-- *Upstream tracking:* per-fork manifest =.skill-upstream= (YAML or JSON):
- - =url= (GitHub URL)
- - =ref= (branch or tag)
- - =subpath= (path inside the upstream repo when it's a monorepo)
- - =last_synced_commit= (updated on successful sync)
-- *Local modifications:* 3-way merge. Requires a pristine baseline snapshot of
- the upstream-at-time-of-fork. Store under =.skill-upstream/baseline/= or
- similar; committed to the rulesets repo so the merge base is reproducible.
-- *Apply changes:* skill edits files directly with per-file confirmation.
-- *Conflict policy:* per-hunk prompt inside the skill. When a 3-way merge
- produces a conflict, the skill walks each conflicting hunk and asks Craig:
- keep-local / take-upstream / both / skip. Editor-independent; works on
- machines where Emacs isn't available. Fallback when baseline is missing
- or corrupt (can't run 3-way merge): write =.local=, =.upstream=,
- =.baseline= files side-by-side and surface as manual review.
-
-**** V1 Scope
-
-- [ ] Skill at =~/code/rulesets/update-skills/=
-- [ ] Discovery: scan sibling skill dirs for =.skill-upstream= manifests
-- [ ] Helper script (bash or python) to:
- - Clone each upstream at =ref= shallowly into =/tmp/=
- - Compare current skill state vs latest upstream vs stored baseline
- - Classify each file: =unchanged= / =upstream-only= / =local-only= / =both-changed=
- - For =both-changed=: run =git merge-file --stdout <local> <baseline> <upstream>=;
- if clean, write result directly; if conflicts, parse the conflict-marker
- output and feed each hunk into the per-hunk prompt loop
-- [ ] Per-hunk prompt loop:
- - Show base / local / upstream side-by-side for each conflicting hunk
- - Ask: keep-local / take-upstream / both (concatenate) / skip (leave marker)
- - Assemble resolved hunks into the final file content
-- [ ] Per-fork summary output with file-level classification table
-- [ ] Per-file confirmation flow (yes / no / show-diff) BEFORE per-hunk loop
-- [ ] On successful sync: update =last_synced_commit= in the manifest
-- [ ] =--dry-run= to preview without writing
-
-**** V2+ (deferred)
-
-- [ ] Track upstream *releases* (tags) not just branches, so skill can propose
- "upgrade from v1.2 to v1.3" with release notes pulled in
-- [ ] Generate patch files as an alternative apply method (for users who prefer
- =git apply= / =patch= over in-place edits)
-- [ ] Non-interactive mode (=--non-interactive= / CI): skip conflict resolution,
- emit side-by-side files for later manual review
-- [ ] Auto-run on a schedule via Claude Code background agent
-- [ ] Summary of aggregate upstream activity across all forks (which forks have
- upstream changes waiting, which don't)
-- [ ] Optional editor integration: on machines with Emacs, offer
- =M-x smerge-ediff= as an alternate path for users who prefer ediff over
- per-hunk prompts
-
-**** Initial forks to enumerate (for manifest bootstrap)
-
-- [ ] =arch-decide= → =wshobson/agents= :: =plugins/documentation-generation/skills/architecture-decision-records= :: MIT
-- [ ] =playwright-js= → =lackeyjb/playwright-skill= :: =skills/playwright-skill= :: MIT
-- [ ] =playwright-py= → =anthropics/skills= :: =skills/webapp-testing= :: Apache-2.0
-
-**** Open questions
-
-- [ ] What happens when upstream *renames* a file we fork? Skill would see
- "file gone from upstream, still present locally" — drop, keep, or prompt?
-- [ ] What happens when upstream splits into multiple forks (e.g., a plugin
- reshuffles its structure)? Probably out of scope for v1; manual migration.
-- [ ] Rate-limit / offline mode: if GitHub is unreachable, should skill fail
- or degrade gracefully? Likely degrade; print warning per fork.
-
-** DONE [#C] Monthly session-harvest workflow :feature:
-CLOSED: [2026-06-11 Thu]
+Source: winvm link-integrity pass, 2026-07-28.
+** DONE [#B] Parked: telegram source treats "down" as launch, not SCAN FAILED (from .emacs.d)
+CLOSED: [2026-07-28 Tue]
:PROPERTIES:
-:CREATED: [2026-06-11 Thu]
-:LAST_REVIEWED: 2026-06-11
+:LAST_REVIEWED: 2026-07-24
:END:
-A monthly pass over recent =.ai/sessions/= summaries across projects proposing promotion candidates: patterns for the catalog, durable facts for the KB, rule refinements, workflow learnings. Sibling cadence to the roam-hygiene timer; a workflow run on schedule, not a standing agent. From the 2026-06-11 insights report's "Canonical-Aware Knowledge & Workflow Curator" — the capture/promote machinery exists (pattern catalog, /codify, KB); this adds the mining cadence.
+Applied 2026-07-28, merged with the segfault root-cause fix that arrived the same morning rather than applied alone.
-Shipped 2026-06-11 as [[file:.ai/workflows/session-harvest.org][session-harvest.org]] (template + INDEX entry): five phases, four promotion lanes, /codify-grade gates + work-confidentiality scrub, =:LAST_HARVEST:= marker in notes.org, and the KB receipt-line metrics readout for the ~2026-07-10 checkpoint. Window filter reads session-filename date prefixes (mtime proved unreliable in a live test). First run due ~2026-07-11.
+Merging was necessary, not tidiness. The parked proposed file still carried the bad =(telega--loadChats 'main)= call at its own lines 52 and 122, so applying it as-is would have shipped a file that fixed the wording defect while preserving the call that kills the server. Its third hunk also added prose citing "tdlib segfaults in native mode (SEGFAULT gotcha below)" — pointing at the section the new handoff rewrites to say those deaths were our own bad argument, not tdlib memory corruption.
-** CANCELLED [#B] todo-cleanup.el per-area Open Work / Resolved pairs :feature:
-CLOSED: [2026-06-11 Thu]
-=--archive-done= assumes exactly one level-1 "Open Work" and one "Resolved" heading per todo.org. Home's consolidated file briefly carried per-area pairs and the pass skipped. Filed from home's 2026-06-11 addendum, then held the same evening when Craig flagged that he expected a single pair.
-
-Cancelled 2026-06-11: Craig confirmed the decision — one todo queue with a single Open Work / Resolved pair. Home reshapes its consolidated file to that form, and the existing single-pair tooling works unmodified. No code change needed.
-
-** CANCELLED [#D] todo-cleanup =--archive-done= reports 0 moves while moving subtrees :bug:
-CLOSED: [2026-06-12 Fri]
-:PROPERTIES:
-:CREATED: [2026-06-12 Fri]
-:END:
-Observed at the 2026-06-12 wrap: the pass relocated closed subtrees from Open Work to Resolved while printing "todo-cleanup --archive-done: 0 subtree(s) moved".
+What landed: all three parked hunks (the down-is-launch directive, the SCAN-FAILED-only-after-launch-attempted rewording, the =(setq telega-use-docker t)= restored to the Step 1 code block), plus both corrected =loadChats= call sites and the rewritten gotcha. The native-mode prose was reconciled in two places so it no longer leans on the refuted story: the Step 1 comment now states plainly that docker mode and the loadChats bug are separate concerns (the deaths happened *in* docker mode, so docker mode is neither a defense against it nor evidence for it), and the Quick Reference line says "crashed in native mode (2026-06-09)" instead of "segfaults", with the same disambiguation.
-CANCELLED 2026-06-12 — cannot reproduce. =todo-cleanup.el= is unchanged since the wrap that logged this, and =tc-archived= is incremented inline with each move and read straight in the report, so no move can go uncounted. Running the exact pre-archive state (=b6d286f:todo.org=) through the tool reports the right count (3 moved, all listed). The "0 moved" was a correct second-run report: =open-tasks.org= Phase A runs =--archive-done= after wrap-it-up already archived, so the second pass finds nothing to move and prints 0 next to the first pass's git diff. Not a code defect.
+Verified: both live call sites use the TL object; the two remaining ='main= occurrences are inside the gotcha prose describing the bug. lint-org clean, mirror synced, suite green.