| Commit message (Collapse) | Author | Age | Files | Lines |
| |
|
|
|
|
|
|
|
|
| |
Two additions from Craig, both landing on the throwaway branch that already makes a bad night a deleted branch.
Pass 11 now finds refactoring opportunities alongside bugs, filing each as a graded task. Still find-never-fix: a fresh finding files a task and stops, because a just-found bug can be a misread (one was filed and retracted two fires apart on 2026-07-23). The task is the checkpoint.
New pass 12 implements the backlog's solo, decision-free tasks on the branch, off by default behind :SENTRY_MAY_IMPLEMENT:. It reuses work-the-backlog's unattended-loop contract: only the ready bucket runs, anything needing a decision defers to a VERIFY, and nothing pushes. The morning review is the gate. Its /review-code runs the premise check first (reproduce the bug before judging the diff), because a filed claim never gets a review, and that skeptical review is what makes fixing on a branch safe. A finding that fails its premise check isn't implemented.
The marker is separate from :COMMIT_AUTONOMY: on purpose: hygiene is a two-minute merge, implemented code is a review session, so the owner opts in per project.
|
| |
|
|
|
|
|
|
| |
The five todo-format-family checkers encode todo.org completion conventions, but they ran on every org file, so they misfired across docs/specs/. That was 100 findings over nine specs in this repo, and the same noise in every project carrying specs. A spec's Decisions section legitimately uses a level-2 DONE with no CLOSED cookie. Its review-history uses level-2 dated headings. Neither is a completion defect there.
A path guard now skips the family on any file under a docs/specs/ segment, the canonical spec home per the docs-lifecycle rule. Link, table, and structural checks still run on specs, so a broken link in one still flags. todo.org and every non-spec org file keep the full checker set.
Folds in task-missing-last-reviewed alongside the four named checkers, since it's the same family and misfires the same way on a spec's phase tasks. Correcting an earlier note in the task: these are lint-org.el's own checkers, not org-lint's, so the fix is a local guard rather than filtering upstream output.
|
| |
|
|
|
|
| |
main caught ValueError and FileNotFoundError around the send, so an unreadable source raised a PermissionError as a raw traceback instead of the clean "inbox-send: <message>" every other failure produces. Widening the catch to OSError covers the unreadable source, the missing source, and any atomic-write failure alike.
discover_projects appended without deduping, so a roots config naming both a parent directory and one of its children listed the same project at two indices. It now dedupes on the resolved path, first-seen order preserved.
|
| |
|
|
|
|
|
|
| |
inbox-send wrote straight to the destination path, and write_text truncates on open, so any mid-write failure left a zero-byte .org in another project's inbox. inbox-status counted that phantom as a pending handoff and blocked a turn in the receiving project over a file with no content and no sender.
Both send paths now write to a temp sibling and os.replace it into place, so the inbox only ever sees a complete file. A caught failure removes the temp and re-raises, leaving no debris. encoding is pinned to utf-8 on the write and on the roots-config read, which closes the locale-dependent failure that first surfaced this.
inbox-status also skips the .inbox-send-* temp, so the brief window before the rename can't read as pending either. The atomicity is only complete once the consumer ignores the in-flight file.
|
| |
|
|
|
|
|
|
|
|
| |
The python and typescript bundles carried rules and a coverage script but no pre-commit hook, so any project installing one got no credential scan on commit. Both now ship all four components the README documents: the shared secret scan, a validate-on-edit hook, settings wiring, and a seed CLAUDE.md. Verified against a real repo: a commit carrying an AWS key is refused.
install-lang now warns when a bundle is missing a documented component. That's the half that keeps this from recurring. Whoever adds the sixth bundle will forget something too, and today the installer prints success either way.
Two things fell out of the build. node --check is unusable on TypeScript: it ignores --experimental-strip-types, so it rejects valid TS and accepts broken TS. The hook uses tsc filtered to syntactic diagnostics instead. And completing the bundles means every pair now collides on settings.json and pre-commit, so no two compose without FORCE=1. The earlier "bundles already compose" reading rested on these two being incomplete. Filed for a real decision; clock-panel is the project that wants it.
Also stamps :LAST_REVIEWED: at task creation, with a lint checker to catch misses. Writing a task is reviewing it, so leaving the stamp off pushed every fresh task to the top of the next review batch to be re-derived by someone with less context than its author had. Tonight's sweep sent the staleness count from 13 to 22 while the list got more accurate.
|
| |
|
|
|
|
|
|
|
|
|
|
| |
Four proposals from other projects, reviewed and applied together.
wrap-it-up gains a Clear temp/ step in Step 3. temp/ is gitignored everywhere, so nothing there survives deletion. The step confirms before removing anything that reads as in-progress rather than throwaway, and skips where temp/ isn't gitignored. This closes the last open clause of the working/temp work from 2026-07-20.
interaction.md drops the fenced-code-block carve-out. Chat output carries no markup at all now, fences included. Fences don't invert the way inline spans do, but the carve-out kept pulling them back in.
sentry.org folds in four notes from its first two live runs elsewhere. Two are traps for anyone reviewing a night by git log: archive-done touches .gitignore on its first run, and a mirror-only project's quiet fires leave no commits at all. The others make property sweeps sanctioned quiet-fire work and split the task audit into a mechanical hourly subset plus a nightly judgment half.
The notes.org template now lints clean, down from four flags in every project on every sweep. Two of those were mechanical, so lint-org --fix anywhere would have rewritten the template and drifted it from canonical.
|
| |
|
|
|
|
|
|
|
|
| |
Sentry no longer checks email or messengers. Pass 3 loads only the calendar and PR/ticketing triage plugins. The mail and messenger ones stay out: cmail, Gmail, Telegram, Signal, chat DMs. A manual triage intake still scans everything. The probe follows, so a project whose only declared sources are mail or messengers now skips the pass instead of running it empty.
New pass 11 hunts bugs. It runs linters and static analysis, checks config sanity, and reads one rotating code area per fire so coverage builds over a night. Verified findings get filed as graded bug tasks. It never fixes unattended.
I dropped the suite run the order called for. Running the suite every fire is the per-pass anti-pattern this same file forbids, and the entry baseline already has the result.
The KB personal-project roots now cover ~/.dotfiles, which was classifying Unknown and blocking writes from there.
|
| |
|
|
|
|
|
|
| |
Auto mode is the away-from-desk mode. A sweep worth seeing should reach Craig's phone, not just print into a session he isn't watching. I folded a Phone delivery subsection into canonical triage-intake.org: a full-three-section sweep now pushes to the phone over Signal via agent-text. A pointer from End-of-sweep output and a Living Document note tie it in.
Signal-only, by Craig's ruling: a quiet sweep's "nothing" heartbeat never reaches the phone. Silent-until-signal governs the phone channel too, so it only fires on real signal. The in-session heartbeat stays as proof the loop ran.
Only the send half ships. Reply polling (the retired ntfy design's phone-recv) waits on the reply-correlation follow-up: a Signal reply fans out to every linked device and neither knows which page it answers. That spec owns the recv wiring. I re-pointed the transport off dead ntfy onto agent-text (the renamed pager). The mirror is synced and the DONE task records the deferral.
|
| |
|
|
|
|
| |
"page me" and "text me" now name distinct channels: page is the desktop notification, text is the Signal phone push, and "text and page me" fires both. Before, "page me" defaulted to desktop and the phone was an unnamed "on my phone" variant, so you couldn't name the phone without naming the machine.
I renamed the Signal tool agent-page to agent-text to match, with a deprecated agent-page shim delegating to it so callers and other machines don't break before their next install. The dispatch and tests are otherwise unchanged. I rewrote protocols.org "Reaching Craig", page-me.org, and work-the-backlog's away-run logic around the three phrasings, and updated the runbook.
|
| |
|
|
|
|
|
|
| |
The pager worked but was undocumented and drifting stale: signal-cli warned the account had gone 47 days without a receive, and no runbook existed to hand a future session or a non-Claude runtime.
I added the runbook under docs/design/ and a roam-sync-shaped receive timer that drains the queue every 15 minutes, keeping the account warm and surfacing reply messages. I also generalized agent-page: it now sends directly from any machine holding the pager account and relays to velox only when it doesn't. ratio is linked as a device of the account, so a page lands even when velox is down.
The receive script no-ops cleanly where the account isn't registered, since the units stow onto every machine via the shared common package.
|
| |
|
|
|
|
| |
The general triage plugins (personal Gmail, cmail, calendar, Telegram, GitHub PRs) are template-synced into every project, so the old "plugins present" probe self-activated triage everywhere. The sentry live trial caught it reaching personal inboxes from a project that isn't a triage target. Now a general plugin activates only when the project names its basename in a :TRIAGE_SOURCES: line in notes.org Workflow State. A project-specific plugin stays active by presence. The gate lives in triage-intake Phase 0, so it fixes the interactive over-pull too, and sentry's pass-3 probe reads the same signal.
Presence is capability, the declaration is activation. A project that declares nothing and owns no project plugin pulls nothing. Migration handoffs went to home and work. rulesets correctly declares nothing.
|
| |
|
|
|
|
| |
Auto triage-intake and auto inbox-zero now collapse an empty check to a single "<workflow> at HH:MM: nothing" line, matching the sentry quiet-fire heartbeat from Phase 1. Detection stays in-session, so the MCP-auth sources are scanned in full and only the output collapses. Inbox already had an acknowledge-only-on-empty rule, now unified to the standard format; triage's three-section sweep output gains the empty-sweep collapse.
Completes the silent-until-signal spec (Phases 2-5): the spec is the shared-policy home the three workflows reference, and a manual-testing entry covers the live-loop verification. Spec flipped to IMPLEMENTED.
|
| |
|
|
|
|
| |
A suspend now ends by detaching the tmux client viewing the aiv-<project> session, so the parked session drops out of the alt-space rotation instead of cluttering it. Detach is not teardown: the session and agent process stay alive in the background and only the view disconnects, so it runs inline as the last action (the handoff text is preserved in the pane and shown on re-attach) rather than deferring to a Stop hook the way the session-killing teardown must. Degrades gracefully when not in tmux.
Originated in archsetup as a local stopgap and reconciled to canonical here.
|
| |
|
|
|
|
| |
A sentry fire whose passes all probe-skip or no-op now writes a single "sentry at HH:MM: nothing" line instead of a full per-pass digest block. A fire that runs or queues anything writes the full digest as before. This is Phase 1 of the silent-until-signal policy. The live trial's fires 3-8 were walls of no-op digests, the noise this removes.
The "no silent skip" discipline is reconciled, not broken: inside a working fire every pass still writes its ran/skipped line, and a quiet fire's heartbeat is the explicit "nothing to do" record rather than a hidden skip.
|
| |
|
|
|
|
|
|
|
|
| |
working/ is the tracked home of in-progress work, version-controlled from creation rather than staged locally until it graduates. Filing on completion reorganizes durable artifacts into permanent homes. It doesn't mark when they became durable. Ephemeral, disposable artifacts go in a gitignored temp/ (or /tmp), never working/.
install-ai.sh emits a temp/ ignore in both track and gitignore modes. Ephemerality is independent of whether a project tracks its .ai/ tooling, so temp/ rides neither the track .gitkeep step nor the gitignore-only tooling block. working/ is never ignored, on purpose. sweep-gitignore-tooling.sh backfills temp/ as a separate mode-independent pass, not an IGNORE_SET member: folding it into that set would skip every track-mode project, the set that most needs it.
While implementing I confirmed the canonical machinery never ignored working/ (the tooling set is only .ai/ .claude/ CLAUDE.md AGENTS.md), so a project that ignored working/ did so as a local deviation.
Documents the convention in working-files.md and mirrors it in protocols.org. 7 new bats cover temp/ in both modes, never working/, and idempotency.
|
| |
|
|
|
|
|
|
|
|
|
|
| |
convention
Home proposed two linked cross-project norms and Craig approved rolling them out. Both now ship in the template so every project inherits them.
A new "Colloquialisms and Expansions" section in protocols.org maps Craig's shorthand to a defined action the agent applies without asking. Two seed entries: "put X on the list" appends to a session-scoped Before-Close Queue, and "tell <project> <message>" drops the message in that project's inbox via inbox-send. The set is extensible.
The queue lives under a "* Before-Close Queue" heading in the session anchor, so it resets when the anchor is archived at wrap. Anything that must outlive the session stays a todo.org task. wrap-it-up Step 1 gets a sub-step that works the queue oldest-first before the Summary, so any resulting edits ride the wrap commit, with unfinished items surfaced in the valediction rather than dropped.
I settled three design calls. The reference lives in protocols.org rather than a per-project notes.org (home's local reference), since a synced section is what makes it a shared norm. The queue lives in the session anchor, as home did. The wrap step sits at the front of Step 1 rather than a new half-step, which keeps the "Steps 1 through 5" framing intact. A documentation-integrity bats guards the wiring and the before-the-Summary ordering against a future edit or sync dropping it.
|
| |
|
|
|
|
|
|
| |
The "check inbox/ at every task boundary" rule was prose-only in protocols.org, so it held only as well as the agent remembered it and a handoff could pass a turn unseen. inbox-boundary-check.sh is a Stop hook that backs the rule: when inbox-status -q reports pending items it blocks the yield once and injects the count, so the agent processes them before returning.
It soft-nudges rather than hard-blocks. On the harness re-entry (stop_hook_active: true) the hook steps aside and lets the turn end, so a mid-task pause to ask a question, or an item the agent genuinely can't process, never wedges the session. A hard block would instead push inbox processing ahead of that clarifying question.
The hook self-skips where it doesn't apply: no inbox/ dir, no inbox-status, or a clean inbox each exit 0 silently, so one global hook covers every project with no config. It prefers the project-local .ai/scripts/inbox-status and falls back to one on PATH. It's wired into the Stop array ahead of ai-wrap-teardown in both the tracked settings.json (which the live ~/.claude/settings.json symlinks to) and the documented snippet. bats covers pending, clean, the re-entry step-aside, no-inbox, and the absent-status degrade.
|
| |
|
|
|
|
|
|
| |
I moved the decision logic out of the launcher's I/O-coupled functions so it's unit-testable on plain inputs, the hardening the audit called for. Three pure cores now carry logic that was buried in git and tmux calls: _git_prep_action classifies a repo's state into none/pull/report, _order_windows sorts the window listing, and _match_window_id resolves a name to a window id. _git_is_dirty replaces the staged/unstaged/untracked check that was copy-pasted across three functions.
prep_git_single and auto_pull_if_clean both route their pull-or-report choice through _git_prep_action, so the "never auto-pull a dirty repo" rule lives in one place with its own test instead of two parallel branches. sort_windows keeps its two-pass move (park high, then reassign) but hands the ordering to _order_windows, and find_window_id is now a thin tmux wrapper over _match_window_id.
Each core gets a Normal/Boundary/Error set: the git-prep truth table including diverged and behind-while-dirty, the window ordering including whole-line-not-substring matching. The 42 launcher tests stay green and a live black-box run still prints the right launch command for claude and codex, so the extraction is behavior-neutral. I also ran shfmt (-i 2 -ci) to match the house style the other bin/ scripts follow.
|
| |
|
|
|
|
|
|
| |
Brings the session launcher under characterization tests ahead of the hardening refactor, so the pure-core extractions and footgun fixes land against a green net. The tmux/git-coupled functions can't be reached by running the launcher black-box, so I wrapped dispatch in main() behind a run-vs-sourced guard: run directly it dispatches as before, sourced it exposes the functions to the tests.
The net pins current behavior (record-not-spec) with a Normal/Boundary/Error set per unit: git_status_indicator across no-upstream/ahead/behind/dirty/clean, candidate discovery and filtering, selection parsing, and functional tests over create_window, find_window_id, and sort_windows ordering against a throwaway tmux server. Every tmux call runs on a private socket so nothing touches the live ai session, and the throwaway repos disable git auto-maintenance to dodge the teardown race that flaked rename-ai.
Also cleared the two pre-existing shellcheck warnings on the touched paths: quoted the "@{u}" revspecs (SC1083) and marked the intentionally-literal display tilde (SC2088). Both are behavior-neutral, and the existing runtime tests confirm the launch path is unchanged.
|
| |
|
|
|
|
|
|
|
|
| |
A task phrased as an absence ("find bugs until none remain," "refactor until nothing worthwhile is left," "clean it up") can't be :solo:, because it fails the verifiable-by-the-agent gate. Absence isn't falsifiable, so "done" is a judgment call, which is what :solo: forbids. This kept surfacing (the ai-launcher-hardening task is the live case) with no written recipe for fixing it.
todo-format.md gains a subsection, right after the :solo:/:quick: definitions, on converting a fuzzy goal into a measurable one: bound the surface (enumerate the units, that's the done-set), net the behavior with characterization tests, disposition every finding (fixed / filed / declined-with-reason, never "looked and it's fine"), and gate on an objective floor (static-analysis clean, suite green, coverage of the enumerated surface). The qualifying answer becomes a dispositioned report, not a claim of perfection, and the honest limit stays honest: "no visible bugs" means "every enumerated path passes its characterization set and clears the audit." Once a task carries those criteria it can carry :solo:.
work-the-backlog's keystone defer item now names open-ended goals as a specific, recognizable failure of test-writability, so a mistagged :solo: task with no writable acceptance test is deferred and routed to get criteria rather than guessed at.
start-work has the same Justify/Approach gate and wants the same pointer, but its skill lives outside rulesets, so that edit is a follow-up in its home.
|
| |
|
|
|
|
|
|
|
|
|
|
| |
quality sweep
The rule already said "write a characterization test before refactoring" but not what one is or how many to write per unit. Two additions close that.
testing.md's "Adding Tests to Existing Untested Code" now defines a characterization test as recording what the code actually does, not what it should (Feathers' assert-wrong-read-the-real-value recipe), and requires the same Normal/Boundary/Error set as any unit rather than one happy-path capture. The reason is the useful part: on a characterization test the negative and boundary cases are the bug-finders, because untested legacy code is weakest at the empty input and the missing upstream, and pinning what it currently does there writes the wrong behavior down where it becomes a visible bug. A pinned case that turns out to be a bug graduates from recording current behavior to asserting correct behavior. In-unit bugs fall to this set. Composition bugs need a functional test.
The refactor-for-testability section gains the framing that when the untestable code is legacy you're hardening, extracting the pure core is the hardening, not a detour around it. A function you can't characterize without mocking tmux or git can't be refactored safely, so "needs too much mocking" is the signal to reshape it, never a license to skip the boundary and error cases.
code-quality.org's passes claim to preserve behavior, so it now states the precondition that makes that true: on untested scope, a characterization net comes first, or "behavior-preserving" is an assertion the green suite can't actually verify.
|
| |
|
|
|
|
|
|
|
|
| |
Craig wanted a curated "DeepSat Fundamentals" deck: the ~100 most fundamental cards out of the 465-card deepsat.org deck, marked with a second org tag (:fundamental:) so they stay grep-able in the source. Both flashcard-to-anki.py and flashcard-stats.py keyed a card on a heading ending in exactly :drill:, so adding any second tag turned the heading into :fundamental:drill:, which that anchor didn't match. The 100 tagged cards would have silently dropped from the full-deck apkg and been undercounted by stats. That's the passing-gate-skips-your-file failure.
CARD_RE now matches a trailing org tag block, and a heading is a card when drill is among its tags. The other tags ride along as Anki tags next to the section tag. Card bodies are bounded by any L1/L2 heading (HEADING_RE) rather than only a section or a drill heading. flashcard-to-anki.py gains --tag-filter <tag> (emit only cards carrying that org tag, the subset deck) and --guid-salt <s> (a distinct GUID space so the subset imports non-empty instead of colliding with the full deck on GUID and importing empty). No salt is unchanged, so the existing deck's GUIDs and SRS state are untouched. flashcard-stats.py gets the same CARD_RE/HEADING_RE broadening plus a drill-membership guard.
This is the rulesets-canonical reconcile of a stopgap the work project ran locally. I re-derived it against the current canonical rather than copying the preserved files, which predate the #+TITLE deck-name fix (060a938) and would have reverted it.
parse()'s third element is now the Anki-tag list rather than a single tag string. Only build() and the tests consumed it. The existing parse tests move to the list contract, and new tests cover multi-tag parsing, --tag-filter, the guid salt, and the stats count. A bats case guards that a :fundamental:drill: card still counts. The full deck's real 465/100 check needs the work deck and isn't runnable here. Verified end to end instead on a synthetic multi-tag deck (2 cards, --tag-filter fundamental returns 1) through the real genanki path.
|
| |
|
|
|
|
|
|
|
|
| |
The flashcard pipeline was one-directional (org-drill to apkg). A deck curated on the phone, or an orphaned apkg whose org source was never saved, had no way back into the org source of truth. apkg-to-orgdrill.py closes the loop.
An apkg is a zip holding an Anki sqlite collection, so reading it needs no third-party library: stdlib zipfile + sqlite3. genanki is only needed to write apkgs, not read them, so the converter has no runtime deps. It mirrors flashcard-to-anki.py inverted: deck name to #+TITLE, note Front to the ** heading, note Back HTML to the body (<br> to newlines, entities unescaped, the answer <hr> stripped), the note tag to a * section, and a fresh :ID: UUID per card so the output is org-drill-valid.
The entity unescape runs in the order that inverts escape_html, which escaped & first, so & is unescaped last here. Otherwise a literally-escaped < in the source would wrongly collapse to <. Only Front/Back note types convert; a cloze or other model is skipped with a warning rather than silently dropped, and a note referencing media is flagged since the org side has no media path.
17 tests cover the HTML-to-org conversion, the apkg read (single and multiple decks, Default-deck exclusion, non-basic skip, .anki2 vs .anki21, media flag), the org emission, and errors. The round-trip test closes the loop through flashcard-to-anki.py's own parse(): a known org source becomes an apkg fixture, converts back, and the recovered (front, back, tag) tuples match the original. Verified end to end against a real genanki-produced apkg too.
|
| |
|
|
|
|
|
|
| |
The sentry live trial surfaced this. A sentry inbox-zero pass running from the work project parked the whole 19-item roam inbox as a cross-project boundary crossing and refused to tidy it unattended, reading knowledge-base.md's work-denylist as "don't touch roam from work."
That over-reads the rule. The denylist is a confidentiality guard on one thing: promoting a durable fact into a new agents/ node, so work-confidential material stays out of the personal cross-machine store. Roam is a shared resource, not another project's product scope. Reading it and tidying the shared roam inbox are housekeeping any project may do, work included.
knowledge-base.md gains a "Scope of the denylist" paragraph stating that and naming the mis-park as the error it closes. sentry.org's inbox-zero pass and inbox.org's roam mode each get a one-line note pointing at the rule, so a future agent doesn't re-derive the refusal. Only durable-node promotion stays work-denylisted.
|
| |
|
|
|
|
|
|
|
|
| |
Phase 3 of the sentry supervisor: reconcile the existing roam writers and wrap-up so sentry's locks actually guard something, and its shutdown has one enforced entry point.
The roam-write lock only helps if every roam writer takes it, so both writers now do. knowledge-base.md's write recipe and inbox.org core §5 acquire the roam-write lock around their edit and trigger roam-sync instead of committing themselves. That closes a gap the one-git-owner rule already implied but the KB recipe still violated: the recipe told agents to run git add -A && commit && push against a tree that's chronically dirty from live captures, which could sweep an in-flight capture into a stray commit. roam-sync stays the roam repo's only committer. Agents edit-plus-trigger under the lock, and roam-sync.sh's header now states that contract instead of the old "agents commit inline" note.
Both writers degrade as the spec settled: an absent agent-lock proceeds unlocked (today's behavior), and only a present helper reporting the lock busy after its bounded wait defers or surfaces.
wrap-it-up.org gains a Step 0 that refuses while sentry is live. It checks the single-runner lock and points at "stop sentry" rather than archiving the anchor and tearing down the buffer under a still-firing loop. Both files derive the lock name the same way (sentry-<repo-basename>), so the guard and the engine agree. triage-intake.org notes that it also runs as sentry's triage pass under the no-approvals contract, with its trigger phrases unchanged.
|
| |
|
|
|
|
|
|
|
|
| |
Phase 2 of the sentry supervisor. sentry.org is the engine: an interval loop (default hourly) that walks a fixed pass list (roam pull, inbox zero, triage, todo cleanup, task audit, working-files hygiene, spec board, link integrity, git health, prep freshness) and commits each writing pass to a throwaway sentry/<date>-<host> branch. Nothing pushes. The morning teardown (review, squash-merge, delete) stays Craig's.
The file owns the whole engine: the :COMMIT_AUTONOMY: entry ticket, the interactive entry gates (clean tree, green suite, prior-branch check, ff-only reconcile) that run with Craig present, the single-runner and roam-write locks via agent-lock, the probe-work-session-context-commit pass contract, the digest and morning-approval queue, the skip-not-degrade safety model, the conditional fire-end suite, the multi-day stall notification, and the stop-sentry shutdown.
Every one of the spec's ten decisions and twelve review findings is reflected: host-suffixed branch so the two daily drivers don't collide, tmpfs lock homes so a lock never rides roam-sync to the other machine, roam-sync kept as the roam repo's only committer, spine-excluded dirty checks so sentry's own bookkeeping can't trip its safety skip, and the queue-don't-execute rule for every judgment or destructive action.
Phase 3 wires the roam writers (inbox.org, knowledge-base.md) to acquire the roam-write lock and adds the wrap-it-up guard. Until then the engine is documented but the companions don't yet cooperate. INDEX.org lists it under Tasks and planning.
|
| |
|
|
|
|
|
|
|
|
| |
Phase 1 of the sentry supervisor workflow. sentry needs two locks: a single-runner lock so overlapping /loop fires don't stack, and a roam-write lock so agent edits to the shared roam tree serialize. flock can't serve either, because every Bash call an agent makes is its own short-lived shell, so an flock dies with the call that took it.
agent-lock persists the lock on disk between calls. An atomic mkdir is the acquire. A metadata file inside records pid, host, ISO timestamp, and TTL. Staleness is age-based on the meta mtime, so a crashed holder's lock expires instead of wedging every later acquire, and every reclaim surfaces a note rather than clearing silently. refresh re-touches the mtime as a heartbeat, release is idempotent, status reports free/held/stale, path prints the resolved dir. Locks live under XDG_RUNTIME_DIR/agent-locks (tmpfs: host-local, out of every repo, cleared on reboot), with a ~/.cache fallback where no runtime dir exists.
Reclaim claims the stale dir with an atomic rename before removing it, so two acquirers that both see a lock stale can't both win. A plain remove-then-mkdir would let the loser delete the winner's fresh lock and double-acquire, defeating the roam-write lock's purpose.
Nothing calls it yet. 18 bats tests cover fresh win, contention, release, stale reclaim, heartbeat, path resolution, and usage errors.
|
| |
|
|
|
|
|
|
| |
Two batched changes to the todo hygiene tooling.
Dated-seal archiving: --archive-done aging now keeps one month in-file (retain default 7 → 31, named as tc-archive-retain-days-default) and a new --seal renames the working task-archive.org to resolved-YYYY-MM-DD.org beside it, letting the next aging start a fresh working file. Dating the sealed file by the seal run rather than the calendar quarter is what avoids mislabeling a task closed late in a quarter and archived after the boundary. The unparseable-CLOSED case is now explicit in the contract: a keyword-complete task with no readable close date ages out too. The sealed file inherits the todo file's gitignore status.
Planning-line strip: --convert-subtasks now drops the whole planning line when it rewrites a done sub-task to a dated event-log entry, not just the CLOSED cookie. A dated-log heading carries its date in the heading, so an active SCHEDULED/DEADLINE left on it pins the finished entry to the agenda as weeks-overdue forever (org renders any active planning timestamp, keyword or not). New lint-org checker dated-log-heading-active-timestamp backstops any that predate this. todo-format.md's sub-task and VERIFY completion rules gain the strip step.
|
| |
|
|
| |
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 bin loop symlinks it into ~/.local/bin/install-ai, the same mechanism that links ai and agent-page, so the fresh-project bootstrapper now runs as install-ai from anywhere. The symlink always points at the canonical, so no separate dotfiles copy is needed. Three launcher bats cover the wrapper, including invocation through a symlink from another directory (the case a naive dirname "$0"/.. would break).
|
| |
|
|
|
|
|
|
| |
New stdlib-only upcoming_birthdays.py reads ~/sync/org/contacts.org for :BIRTHDAY: properties and prints a daily-prep block: name, date, days-away, and the age turned when the birth year is known (1900 is the org-contacts unknown-year placeholder, rendered date-only). Birthdays inside 7 days come back flagged so a gift or plan gets prompted. 19 pytest cases cover the window boundaries, leap-day fallback to Feb 28, the placeholder year, and the CLI.
daily-prep gains a Phase A source that runs the script and a Heads-Up line that folds in the block, so contact birthdays the calendar doesn't carry still surface.
Built and tested in the home project. Promoted here because the daily-prep hook lives in this synced workflow and the contacts file is present on every machine.
|
| |
|
|
|
|
| |
Phase C now renders a TASKS / FYI / MISC digest with a two-option close offer. The per-source breakdown and the itemized action menu move to on-request. Phase D becomes the close and runs by default after the digest: file the TASKS items, run mail hygiene on every scanned account without itemized confirmation, advance the sentinel, tear down anything the sweep started. Prose sends under my name and destructive non-mail actions stay gated. The reroute modifier delivers outside-project items to their owners via inbox-send. The stay-open-until-confirmed exit loop is retired. I updated daily-prep's Sub-step 3b to match.
I ratified this in the work project on 2026-07-18 and applied it here as sent from that handoff (inbox 2026-07-18-1730-from-work).
|
| |
|
|
|
|
|
|
| |
The residue probe cut off at before:<anchor-YYYY/MM/DD> while the scan started at after:<anchor-epoch>. Gmail's before:<date> excludes the named day, so the anchor day fell between the two queries. Each sweep then advanced the anchor past that window and never looked back, so an evening anchor hid most of a day for good. That's the exact failure the probe was added on 2026-07-08 to prevent.
The probe now cuts off at the epoch, matching the scan. I hoisted the epoch rule to cover both anchored queries once rather than twice. I scoped it to the anchor windows so the date-slice walk's deliberate day resolution doesn't read as a bug.
home caught it: after acting on 38 messages the probe reported zero while a plain is:unread in:inbox still returned two messages from the anchor day. personal-gmail is the only source pairing an anchored scan with a residue probe, so nothing else needed the fix.
|
| | |
|
| |
|
|
|
|
| |
The meeting recorder switched to lossless FLAC on 2026-07-01, and the transcript workflow's scan globbed only .mkv and .m4a, so a .flac recording was silently never found. FLAC now threads through the audio-only path: found by the scan, transcribed natively by AssemblyAI with no ffmpeg extraction, kept on cleanup, and archived with its own gitignore pattern.
Adopted verbatim from work's handoff (2026-07-14-0105), diff-verified against the canonical.
|
| |
|
|
|
|
| |
Both helpers treated any pipe-led line as a table row, so ASCII art in an example block got mangled into a bordered table (a work file took a 1949-line reformat on 2026-07-09). The scanners now track the open block's type and skip its content. Only the matching end marker closes a block, so a literal end_src quoted inside an example block can't re-expose it.
The deeper cause was wrap-org-table's load-time dispatch: it fired when lint-org merely required the library, running the reformatter over files lint-org was only asked to report on. It now dispatches only when named as the entry script. lint-org's CLI is also report-only by default now. Writes require --fix, and the wrap-up workflow and lint command pass it.
|
| |
|
|
| |
The interactive flow now opens with a runtime picker: claude first (Enter-Enter preserves the old two-keystroke habit), codex labeled as ChatGPT, and one local:<model> line per ollama model, queried live with a short timeout so a dead server just drops those lines. The pick feeds the existing runtime map, then the annotated project multi-select runs as before. --runtime and AI_RUNTIME skip the question, single-directory mode is unchanged, and --print-runtimes exposes the choice list for the two new bats tests.
|
| |
|
|
| |
ai --runtime local now launches codex against the machine's ollama (explicit --local-provider=ollama, model from AI_LOCAL_MODEL, default gpt-oss:120b). Verified end to end with a codex exec completion through the local model. The dependency check probes AGENT_BIN now that AGENT_CMD carries flags. This makes codex-over-ollama the de-facto answer to the runtime spec's first-supported-local-CLI blocker.
|
| |
|
|
| |
The Signal pager reconcile found one pager identity, registered in velox's signal-cli, reachable from other machines only by hand-rolled ssh. agent-page wraps that: direct signal-cli on velox, ssh relay over the tailnet from everywhere else, a desktop-fallback hint when the relay fails, and the UUID target baked in so nobody pages the unregistered phone number again. Four bats tests stub ssh/uname/signal-cli to verify command construction; the real path was verified with a live phone push today. protocols.org's Paging Craig section now teaches both channels (notify for the desk, agent-page for the phone) and demotes signal-mcp to a velox-local nicety; page-me.org, work-the-backlog, and the INDEX carry the same two-channel story. Every project inherits on its next startup sync and make install.
|
| |
|
|
| |
The ai launcher can now start codex sessions: --runtime claude|codex (or AI_RUNTIME), a runtime-to-CLI map with local reserved behind a clear error until the local-model evaluation picks a CLI, and a runtime-aware dependency check. A new --print-launch mode prints the pane launch command without touching tmux or fzf, and is the seam the six new bats tests drive. Both current CLIs take the opening line as a positional prompt, so only the command name varies.
|
| |
|
|
| |
claude-templates/AGENTS.md points any harness at protocols.org, the rules locations, and the /name-to-file resolution rule, and tells it to degrade per each rule's documented fallback instead of skipping gates. make install links it to ~/.codex/AGENTS.md (new CODEX_DIR stanza) and install-ai.sh seeds a project-owned copy at bootstrap, never overwriting an existing one. The rulesets root carries a tracked symlink as dogfood. Five new bats tests cover the link idiom and the seeding. This resolves the generic-instruction-file blocker from the runtime spec, and velox picks up the global link on its next session's make install.
|
| |
|
|
| |
interaction.md gains a Showing Craig Visuals section: EAT renders SendUserFile and inline images as a text line, so visuals open in the browser or imv (per desktop-capture.md), with inline sixel as the quick-glance lane behind a capability gate (patched EAT answering CSI 14 t plus tmux terminal-features sixel, diagnosed 2026-07-13). protocols.org gains the zsh no-word-split gotcha next to the exa alias note. takuzu sent the proposal as inbox handoff 2026-07-11-2137; the sixel findings correct its capability claim.
|
| |
|
|
|
|
| |
task-review-staleness.sh expected a bare 2026-07-09 and treated an org inactive timestamp ([2026-07-09 Thu], the form matching the CREATED:/CLOSED: cookies in the same drawer) as unparseable. The count branch folded that into the stale count, so a freshly-reviewed task reported as never-reviewed, and a full review pass never dropped the startup nudge. Both the bare and bracketed forms now normalize to the ISO date. A value that is neither warns loudly to stderr (file:line:value) and stays out of the count, since a data error shouldn't hide as "never reviewed." task-review.org documents the accepted format.
Tested red/green: 5 new bats cases (bracketed fresh and stale, list-mode sort by real date, malformed warns and is excluded). Full suite 273 ok.
|
| |
|
|
|
|
| |
For a spec whose deliverable is a real UI, a design argued on paper is a guess. New claude-rules/ui-prototyping.md: research the category first, brainstorm the UX in the spec, build ~5 distinct working prototypes over one engine, iterate one to a final, and record a UI decision only once it's been seen working in a prototype. spec-create gains the step for non-trivial-UI specs. spec-review gates on it: research cited, final prototype linked, iterations in history, decisions backed by a prototype. Prototypes live at docs/prototypes/<spec-name>-prototype-<N>.html.
Worked example: archsetup's timer-panel spec and its three prototypes.
|
| |
|
|
|
|
| |
The google-docs MCP listMessages tool caps at maxResults=100 and exposes no pageToken, so an unread pile over 100 truncates silently and every later anchored sweep skips the tail below the cap. That's how a 300+ backlog built up unseen by 2026-07-08. Two Scan-section rules close it: when a scan returns exactly 100, walk the tail in date slices and dedupe by id, and never report resultSizeEstimate as a count. A cheap backlog-residue probe each sweep (before:<anchor>, maxResults=5) surfaces pre-anchor unread loudly, so an anchored "no changes" can't mask a window the scan never saw.
personal-gmail is the only gmail-family plugin here, so it's the only file that changed.
|
| |
|
|
|
|
|
|
|
|
| |
Every org document an agent writes carried `#+AUTHOR: Craig Jennings & Claude`. No template stamps that line. Agents copy it from a neighboring file, so one stray header propagates through everything generated afterward.
My own repos tolerate the co-author line. Employers whose policy is that work product carries employee names alone do not. An `#+AUTHOR:` line survives conversion into docx, a wiki page, or a PDF that reaches a customer.
I rewrote the header to `Craig Jennings` across the workflows, templates, specs, and design docs. The rule now lives in commits.md, so the next generated document starts correct rather than inheriting the mistake.
Archived session logs keep their original headers as a record of what happened. The two Codex-authored design docs keep their byline, because Codex wrote them and relabeling would be a false attribution rather than the removal of one.
|
| |
|
|
|
|
|
|
|
|
| |
Phase A step 3 guarded its template rsync only on whether rulesets' own source was clean, never on whether the project branch was current. When a branch is diverged or behind-and-dirty, Phase A.0 correctly declines to fast-forward, but the rsync then landed templates on the stale committed .ai/ baseline. The diff came out huge (measured against old content) and conflicted once the branch reconciled to upstream's newer templates. home hit it today: a 3-ahead/46-behind divergence produced ~25 files of phantom drift nobody authored.
I added a second guard: after Phase A.0's reconcile, it re-checks git rev-list @{u}...HEAD and skips the rsync when behind>0. It composes with the rulesets-clean guard, so both a stable source and a current branch are required before the sync runs. No-upstream and ahead-only both fall through and sync, which is correct.
It's deliberately not an auto-discard: a legitimate local stopgap in a synced file can't be told from accidental drift by content alone, so prevention is safe where blind cleanup isn't. Phase C's churn safety net still surfaces pre-existing dirt.
home proposed this via inbox handoff.
|
| |
|
|
|
|
| |
Craig's verdict on the all-red page styling: it reads like the system is about to crash. page-me and the work-the-backlog end-of-set page now use notify info --persist, still persistent and audible, never crash-scary. status-check's success and fail notifications keep their types, since a job outcome isn't a page.
The commit also carries the two loop-filed task records and the archive sweep counterpart from earlier tonight.
|
| |
|
|
| |
Two sends in the same minute whose text starts with the same phrase derived identical filenames, and the second silently replaced the first. A message was lost this way in the wild. An existing target now gets a -2/-3 stem suffix, extension preserved, on both the text and file paths. Four red-first tests reproduce the loss with a fixed timestamp so the same-minute case is deterministic.
|
| |
|
|
|
|
| |
Projects were falling behind on templates because somebody sent them a task: an untracked inbox drop read as a dirty tree to the stricter gates. The policy is now stated where the gates live: dirty means tracked modifications only, and untracked or gitignored files never block a template pull, a fast-forward, or a monitoring gate.
The audit found one offender. The inbox monitor's precondition used bare porcelain, counting the very drops it exists to process. It now checks tracked changes only, which is safe because the per-item commit already stages explicitly. The rsync WIP-guard keeps counting untracked files inside the synced source paths on purpose, since a half-written template is exactly the WIP it holds back.
|
| |
|
|
|
|
| |
A tracked or synced doc asserting "this machine is X" is false on every machine but its origin, and an agent trusting it reasons backwards all session. It happened live: a stale "ratio" claim steered a session running on velox. The new rule bans fixed identity claims in tracked/synced docs and requires the runtime derivation instead (uname -n, since the hostname binary is often absent). Describing the fleet stays legal. Claiming the current member doesn't.
startup gained a read-only probe that greps CLAUDE.md and notes.org for the pattern and surfaces hits as a judgment flag, never a block. Fixture-verified under bash and zsh.
|