aboutsummaryrefslogtreecommitdiff
path: root/.ai/workflows
Commit message (Collapse)AuthorAgeFilesLines
* feat(publish): isolate the pre-commit review and loop it to approvalCraig Jennings12 hours3-5/+6
| | | | | | | | | | | | | | The review already ran before every commit. It ran in the context that wrote the diff, which is the part that didn't work. A self-review checks the change against the author's own model of it and can't check the model, so the errors that survive are the ones that were never in the diff: a scope inherited from whoever reported the problem, a blast radius estimated instead of measured. So the reviewer is now a subagent, on every commit including a one-line one. It gets the diff, a one-line claim of what the change does, and the ticket where one exists. It doesn't get the conversation or my reasoning, because those are the model it exists not to hold. The ticket stays in for the opposite reason: it was written before the work and is the only thing that can contradict my claim about my own diff. Findings loop back to the same reviewer until it approves, rather than to a fresh one that can't tell an addressed finding from one that never existed. Three rounds, or a finding that recurs after being reported fixed, and it stops for a human instead. The unattended callers park the task rather than wait for an answer nobody is awake to give. I gave the stance a substantiation floor. An agent told to attack will manufacture findings to satisfy the instruction, and a manufactured finding costs a round and teaches the author to discount the next review. subagents.md said three separate times not to dispatch work this small, so it now carries an Isolation Override: the size heuristics assume the main thread could do the job equally well, and they lapse when its own context is what makes its answer untrustworthy. Field 2 of the Prompt Contract inverts there. Paste your context in and you hand over the very model you spawned the agent to escape. I wrote it by running it on itself: three rounds and thirteen findings. Two were things I'd have shipped. Withholding the ticket made my own claim self-certifying, and the override reaffirmed the contract field that would have destroyed the isolation.
* refactor(sentry): call one loop cycle a cycle, not a fireCraig Jennings14 hours2-34/+34
| | | | | | | | | | I read "nine fires" as nine emergencies and went looking for what was burning. The word reaches me directly, since digest headings render as Fire 11 in the anchor I read every morning. 72 noun-sense instances in sentry.org become "cycle". The four verb-sense uses stay, because firing is the right word for a trigger. home proposed "pass" and I turned it down. sentry.org already uses that as a numbered noun for the eleven hygiene passes, so "Pass 11" would have collided with a referent in the same file. home confirmed it by walking into exactly that: its anchor ended up carrying "Pass 11" three lines from "pass 12". "Cycle" appears nowhere in the file, and it is the word I reached for myself when I flagged the problem. Three more instances lived outside sentry.org: wrap-it-up.org, todo-cleanup.el, and its test. All three named a sentry cycle, so they move too and the vocabulary stays consistent across files.
* fix(triage): report a dead server after loadChats, not a quiet accountCraig Jennings15 hours1-1/+21
| | | | | | | | The load call returned 'loaded whatever happened. ignore-errors catches nothing there, because a bad argument kills the server process rather than signalling in elisp. A death during the load was invisible. The tail now returns 'server-died when the server is gone, and the recipe treats that as SCAN FAILED. A dead server leaves a thin chat hash, and a thin hash reads exactly like an account with little unread. That's the false all-clear the down/not-loaded rule already guards against, arriving one step later in the lifecycle. It's also the independent evidence the gotcha asks for when it says to treat a short chat list as a real short list. Without it the loadChats crash stayed invisible through two investigations. I used telega-server-live-p, the wrapper for the process-live-p expression the gotcha names. The fboundp guard comes from Step 0, which uses the same pair. A launch that failed outright leaves telega unloaded, and that should read as 'server-died rather than signalling void-function.
* fix(triage): pass the TL object to loadChats, not a bare symbolCraig Jennings20 hours1-25/+96
| | | | | | | | Step 1 called (telega--loadChats 'main). That's a raw TL wrapper, so it drops its argument into the request as :chat_list untouched. The symbol went out on the wire as main, and the C parser aborts on any value it can't start. Every telegram sweep has been killing telega-server. Both call sites now pass '(:@type "chatListMain"), which is what telega's own callers use. I merged this with the down-is-launch fix parked since 2026-07-24 rather than applying either alone. The parked file still carried the bad call, and its new prose cited the segfault gotcha as the reason docker mode is mandatory. That's the same gotcha this rewrites to say the deaths were our own bad argument. Shipping them in sequence would have left the file arguing against itself, so I reconciled the prose in both places and kept the docker requirement on its own evidence. The gotcha keeps two caveats. A crash with no triage verb running needs its own investigation. And a short chat list is a real short list, because 19 was the true account size measured at both ends, not a scan truncated by this bug.
* refactor(rules): split testing.md, fix the approval gate, require first personCraig Jennings44 hours1-1/+1
| | | | | | | | | | | | | | | | Three changes to the same layer. I split testing.md the way I split commits.md, by what has to be resident rather than by size. What stays is the standing directive: TDD is the default, write the failing test first, and every unit needs Normal, Boundary, and Error cases. That has to fire before any code is written, which is exactly when no skill has been summoned, so it can't ride a trigger. Everything else moved to the testing-standards skill: characterization recipes, the per-category detail, property-based and mutation testing, the pyramid, integration rules, naming, the test-quality and mocking rules, coverage targets, the spike exception, and the anti-patterns. 2,824 words down to 347. I fixed the approval gate in the publish flow. It decided whether to ask for approval by checking whether .ai/ is tracked, using that as a proxy for "team repo." The proxy was wrong in the direction that matters: rulesets, home, and work all track .ai/ while all three are private single-user repos, so the rule skipped the gate on the three projects I use most. It now checks whether any remote is on a host other than cjennings.net, which is the thing that actually decides whether someone else reads the log. Every current project resolves to gate-applies, which matches how the flow has actually been run. I also added a first-person directive to the always-loaded core. One already existed for commit bodies and PR prose, but it moved into the publish skill with everything else, and it never covered code comments at all. Now everything I author in or about the repo is first person, with one carve-out: a comment describing what the code does stays third person, because there the code is the actor and not me. Separately I split the publish skill internally. PR descriptions and the three review shapes moved to references/pull-requests.md, since a plain commit never needs them. Always-loaded rules are now about 28,900 tokens, down from 57,800 this morning. One risk on the record. testing.md's margin is thinner than commits.md's was. If testing-standards fails to trigger while I'm writing tests I lose the mocking-boundary rules, which is a quality regression rather than a permanent one, but it is a real bet where commits.md's was not. I also moved the TDD rationalization table rather than cutting it. The posts argue that kind of over-argument is counterproductive now, but removing your defense against me skipping TDD is your call, not mine.
* refactor(rules): split commits.md into invariants plus a publish skillCraig Jennings44 hours2-3/+3
| | | | | | | | | | | | | | | | commits.md was 12,800 tokens, larger than the next four rules combined, and it loaded in every session in every project whether or not anything was being published. Most of it is procedure that only applies when a commit or PR is in play. I split it by blast radius rather than by size. What stays always-loaded is what must hold whether or not I'm publishing, and where a violation is permanent and reaches other people: author identity, the no-AI-attribution ban, the generated-document byline rule, and the public-artifact content-scope rules. That core is 1,027 words, about 2,342 tokens. What moved into the publish skill is everything about how a message gets written, reviewed, approved, and published. The pre-flight reconcile, the code-review gate, the draft and voice and approval gate, conventional-commit format, Voice and Focus, PR description structure, the three review shapes, merge strategy, and the pre-commit checklist. The failure modes are why the line falls there. If the skill doesn't load I don't know the publish flow and have to be told, which is visible and recoverable. I don't silently commit with AI attribution, because that guard never moved. Only the recoverable half is on the skill-triggering bet. I verified the split by using it. The skill registered mid-session and I invoked it to publish this commit, and it loaded with the flow intact. Content is conserved: 5,561 words in, 5,898 across both files, the difference being the frontmatter and the pointer I added to the core. I repointed five cross-references in voice, review-code, inbox.org, and no-approvals.org that named sections which moved. One thing I left alone. The skill is a single 4,871-word file, and both posts argue a long skill should split internally rather than sit as one blob. It loads on demand now, which is the win worth taking today. Splitting it further is its own change.
* fix(hooks): don't let a wrap sentinel outlive its sessionCraig Jennings44 hours1-0/+2
| | | | | | | | | | | | wrap-it-up drops /tmp/ai-wrap-teardown-<project> to ask the Stop hook to kill the tmux session once the wrap certifies clean. When certification fails the Stop hook blocks and leaves the sentinel armed, which I did on purpose so a wrap blocked by a dirty tree can retry on a later stop without re-running the workflow. I never bounded that retry to the session. An uncertified sentinel sat armed indefinitely and fired in whatever session next reached a clean tree. work's 11:37 wrap today left one armed. The 13:20 session ran startup, committed the task filing and the template sync, went clean, and the next stop consumed the two-hour-old sentinel and killed the terminal mid-work. Every stop before those commits had been blocked by the same sentinel failing certification, so the session spent its whole life either blocked or dead. archsetup's had been armed since Saturday on a live attached terminal, and home's was armed and waiting. session-start-disarm.sh clears the project's sentinels at SessionStart. A new session means the wrap that armed one is gone, so its pending teardown is meaningless. Within-session retry is untouched, since the hook only runs at session start, and a test pins that. If teardown is still wanted, wrap-it-up re-arms it. I disarmed the three live ones by hand before writing this, backed up under /tmp/disarmed-sentinels. Four tests cover the disarm, one pins the retry behavior I did not want to lose. The scoping test matters most: a concurrent session in another project keeps its own sentinel.
* feat: add peer-reasoning contract and fix two silent probe defectsCraig Jennings47 hours1-1/+3
| | | | | | | | | | | | I added a Collaborative Peer Reasoning section to the interaction rules. It governs how an interpretation forms before any rule about presenting choices: infer first and clarify only at material forks, test a conclusion against its strongest alternative, let a correction update the downstream model instead of just the wording. The file's framing line widened to match. Two probes were failing silently. The startup KB nudge looked up the best-practices node by grepping file content for its slug. A roam node's slug lives in its filename, so the lookup always returned empty. The nudge pointed at nothing in every project and every session, for as long as it shipped. It matches the filename now, through find rather than a glob so zsh doesn't abort on no match. The browser rule told agents to open URLs with a form ending in &>/dev/null &. That discards the "Opening in existing browser session." line confirming the tab opened. Warm and cold start now split: foreground and read the confirmation when Chrome is already up, detach only when it isn't. The confirmation is on stdout, verified rather than assumed. I filed two tasks from handoffs. The sentry triage split needs a work-vs-personal classification mechanism before its wording can move, because the current rule excludes by category and category can't express that split. The publish-lock design is approved but carries three open gaps. The load-bearing one is a lock held across an unbounded human approval pause. I also swept the old processed handoffs out of inbox/. History keeps them.
* feat: enforce clean wraps and inbox-safe syncCraig Jennings4 days2-26/+59
| | | | Centralize repository-state checks, bind teardown to a certified clean HEAD, and allow inbox-only refreshes. Guard installed symlinks from cross-project writes and add regression coverage.
* docs: define speedrunnable backlog tasksCraig Jennings4 days1-0/+2
| | | | Clarify that speedrunnable means the solo autonomy tag; quick remains only an effort hint.
* feat(sentry): add refactor finding and an opt-in solo-implementation passCraig Jennings5 days1-4/+12
| | | | | | | | | | 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.
* feat(workflows): clear temp/ at wrap, plus three rule and template fixesCraig Jennings6 days2-3/+15
| | | | | | | | | | | | 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.
* feat(sentry): exclude mail and messengers, add a bug-finding passCraig Jennings6 days1-4/+6
| | | | | | | | | | 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.
* feat(triage): push signal sweeps to phone via agent-text in auto modeCraig Jennings8 days1-1/+20
| | | | | | | | 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.
* feat(notify): reserve "page me" for desktop, "text me" for SignalCraig Jennings9 days3-10/+18
| | | | | | "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.
* feat: gate triage sources on a per-project :TRIAGE_SOURCES: declarationCraig Jennings9 days2-7/+11
| | | | | | 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.
* feat: extend the silent-until-signal heartbeat to the two MCP monitor loopsCraig Jennings9 days2-5/+7
| | | | | | 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.
* feat(suspend): detach the tmux client as the final step of every suspendCraig Jennings9 days1-5/+36
| | | | | | 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.
* feat(sentry): a quiet fire collapses to a one-line heartbeatCraig Jennings9 days1-5/+5
| | | | | | 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.
* feat(protocols): add the "the list" before-close queue and colloquialisms ↵Craig Jennings10 days1-0/+8
| | | | | | | | | | | | 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.
* docs(todo-format): add the measurable-acceptance pattern for open-ended tasksCraig Jennings10 days1-1/+1
| | | | | | | | | | 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.
* docs(testing): sharpen characterization-test guidance, wire it into the ↵Craig Jennings10 days1-0/+7
| | | | | | | | | | | | 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.
* fix(roam): scope the work-denylist to durable KB-node writes, not roam tidyingCraig Jennings10 days2-1/+3
| | | | | | | | 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.
* feat(sentry): wire the roam writers and wrap-up guard for sentryCraig Jennings10 days4-1/+31
| | | | | | | | | | 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.
* feat(sentry): add the sentry overnight-hygiene engine workflowCraig Jennings10 days2-0/+218
| | | | | | | | | | 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.
* feat(daily-prep): add upcoming-birthdays block from contacts.orgCraig Jennings11 days1-1/+2
| | | | | | | | 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.
* feat(triage-intake): adopt three-section digest and close-by-defaultCraig Jennings11 days2-53/+74
| | | | | | 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).
* fix(triage-intake): anchor the gmail residue probe to the epochCraig Jennings13 days1-3/+7
| | | | | | | | 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.
* chore(workflows): drop retired Signal-client name from triage history noteCraig Jennings2026-07-141-1/+1
|
* fix(workflows): find and transcribe .flac recordingsCraig Jennings2026-07-141-12/+12
| | | | | | 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.
* fix(scripts): stop org table helpers rewriting block contentCraig Jennings2026-07-141-1/+6
| | | | | | 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.
* feat(pager): add agent-page, the runtime-neutral phone pagerCraig Jennings2026-07-133-3/+16
| | | | 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.
* fix(task-review): accept org-native LAST_REVIEWED stamps, warn on bad onesCraig Jennings2026-07-111-1/+1
| | | | | | 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.
* docs(spec): add UI-prototyping process for non-trivial-UI specsCraig Jennings2026-07-112-0/+3
| | | | | | 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.
* fix(triage-intake): guard personal-gmail sweeps against the Gmail 100-capCraig Jennings2026-07-111-0/+15
| | | | | | 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.
* chore: drop AI co-author from generated-document headersCraig Jennings2026-07-0943-43/+43
| | | | | | | | | | 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.
* fix(startup): skip the .ai/ template sync when the project branch is behindCraig Jennings2026-07-041-5/+21
| | | | | | | | | | 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.
* fix(page): pages are info-level, not alarm-redCraig Jennings2026-07-022-15/+15
| | | | | | 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.
* feat(sync): never let untracked or gitignored files block template updatesCraig Jennings2026-07-022-2/+4
| | | | | | 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.
* feat(rules): add the host-identity guard rule and startup probeCraig Jennings2026-07-021-0/+10
| | | | | | 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.
* feat(flush): add auto mode with self-injected /clear for unattended runsCraig Jennings2026-07-021-1/+15
| | | | | | Long autonomous sessions bloat or hit auto-compaction because /clear is a prompt keystroke no tool call can execute. Auto mode closes that gap: after the write-verified checkpoint, the agent derives its own tmux pane, arms self-inject.sh through tmux run-shell -b, and ends the turn so /clear and a resume line land at an idle prompt. The server-owned arm is load-bearing: a detached child of a tool call dies at the turn boundary. The pane must be derived before arming because ancestry detection can't work under the tmux server. self-inject.sh joins the synced scripts with a six-test bats suite, tmux stubbed at the boundary. work-the-backlog now auto-flushes between tasks when context grows heavy, and its speedrun preset gained the per-item disposition rule: feature-level work gets a spec, unguessable decisions get a VERIFY, well-defined tasks get implemented. The mechanism was proven live in another project's session and its design note is preserved under docs/design/.
* feat(backlog): add the metrics synthesis step to org-roamCraig Jennings2026-07-022-1/+14
| | | | | | "synthesize backlog metrics" reads the JSONL union across personal projects and computes the per-run rollups, the trends, and the corrections signal (a later revert or fix touching an autonomous commit's files within ~14 days, a flag for review rather than a conviction). It writes one :agent:metrics: KB node linking back to prior synthesis nodes. Work and unknown projects are excluded by the denylist classification and reported per the refusal contract. The step is read-only over the logs plus the single KB write. It never mutates the JSONL, todo.org, or any project tree.
* feat(backlog): add the per-task JSONL metrics recordCraig Jennings2026-07-021-3/+36
| | | | | | One record per task at outcome time, appended to the project's .ai/metrics/work-the-backlog.jsonl. The field table follows the spec, with commit_sha called out as the corrections-signal key and comma-separated when a task decomposes into several commits. A failed append warns in the run summary but never blocks or aborts the run. I added the "failed" outcome the spec's error-handling section required but its enum missed: a mid-implementation failure leaves the tree working, gets surfaced, and the run continues.
* feat(backlog): flesh the defer mechanics, pre-flight Q&A, and pageCraig Jennings2026-07-021-4/+19
| | | | | | | | The defer checklist gained its filing mechanics. A deferral VERIFY now dedups against an existing sibling before filing, since the deferred task stays TODO and every later run would otherwise re-file. Placement, heading, and body follow the todo conventions. A quick-question gap routes to the pre-flight Q&A only under the speedrun preset, and only for one-line factual or preference picks. Three or more questions means the task is underspecified and files instead. The data-loss item never routes to the Q&A. The batch-ask is one message with recommendation-first numbered options, and answers land as dated lines in the task bodies before the run starts. The page fires exactly once, on set-done or cap-hit, via notify --persist.
* feat(backlog): pin the commit-autonomy waiver and its degrade contractCraig Jennings2026-07-021-3/+18
| | | | | | The waiver is now a machine-read marker: ":COMMIT_AUTONOMY: yes" in notes.org's Workflow State, with ":LOOP_MAY_COMMIT: yes" as the separate grant for the unattended loop. An absent or non-yes marker reads as no, and the read is a fresh grep each run, never memory. A caller requesting autonomous-commit without the marker degrades to file-only, surfaced in both the run intro and the summary. I stamped rulesets' own :COMMIT_AUTONOMY: and left :LOOP_MAY_COMMIT: ungranted. Letting the recurring loop commit unattended is a separate trust decision.
* feat(backlog): wire the two callers into work-the-backlogCraig Jennings2026-07-024-6/+42
| | | | | | | | inbox.org's auto mode regains its "run this batch next?" ask, now chaining into work-the-backlog as an explicit second step after routing: the eligibility query over the queued batch, file-only, paging off, cap 1. Startup and wrap-up still never execute. The no-approvals speedrun lands as the named preset: an explicit ordered list run under autonomous-commit + always-push + paging-on, every approval front-loaded into the seven-step pre-flight. Any phrase containing "speedrun" routes to the preset, with disambiguation notes in no-approvals.org and the index. The finer Q&A mechanics land with Phase 4. I scoped the chain's task set to the queued batch rather than all of todo.org. The ask is "run this batch next?", and a batch-yes running an unrelated higher-priority task would be surprising.
* feat(backlog): extract the execution loop into work-the-backlog.orgCraig Jennings2026-07-023-5/+151
| | | | | | work-the-backlog.org now owns the autonomous execution loop: the mechanical eligibility gate, the four-item defer checklist, the per-task quality bar, and the run-cap kill switch, fed a task set, session mode, and cap by its callers. I stubbed the pre-flight Q&A, waiver read, end-of-set page, and metrics record with pointers to their phases. inbox.org's auto mode drops its execute step. Per-cycle item 3 routes and queues only, so the loop has one home. This is Phase 1 of the autonomous-batch execution spec.
* feat(routing): wire the wrap-up cross-project router end to endCraig Jennings2026-07-022-0/+34
| | | | | | | | | | This closes the build half of the wrap-up routing spec: Phases 2 and 4 here, with the engine and discovery already shipped. inbox.org's "File as TODO" disposition now runs route_recommend on each keeper and stamps :ROUTE_CANDIDATE: <destination> on strong and weak matches, so the wrap-up router has a candidate set without ever scanning the standing backlog. wrap-it-up.org Step 3 gains the optional router after the inbox sanity check, with the gate-vs-optional split named in the prose: surface the batch with destinations and confidence labels, then go or skip. An empty set stays silent. The go path is mechanical rather than prose-driven: the new route-batch helper lists candidates read-only, and on go extracts each subtree (children ride along, markers stripped, headings promoted), delivers it via inbox-send for provenance, and removes the local copy only after a successful send, rewriting todo.org per send so a crash never strands an already-sent task locally. Overlapping candidate spans (a tagged child inside a tagged parent) are a loud conflict, left in place with a non-zero exit, because routing either span would silently take the other along. A 13-test bats suite covers list/backlog exclusion, empty-set silence, delivery with provenance and children, promotion, drawer pruning, the no-todo.org destination, failed-send recovery with the marker intact, the nested-candidate conflict, and duplicate-marker dedupe. cross-project.md notes the router as a sanctioned cross-project write path.
* feat(startup): add the spec-sort nudge; notify .emacs.d the convention is liveCraig Jennings2026-07-021-1/+12
| | | | | | | | The Phase A batch gains a read-only probe that prints one line when a project has an unsorted docs pile (a docs/design/ or stray docs/*-spec.org files) and no :LAST_SPEC_SORT: marker. Phase C surfaces the "run spec-sort" offer when the probe fired and stays silent otherwise. The stray-root check uses find instead of the spec's compgen sketch: compgen is bash-only and zsh aborts on an unmatched glob, so the original snippet false-negatived on stray root specs under zsh. The spec's snippet is updated with a note, and the probe is fixture-verified in both shells across the four project shapes. I also fixed startup.org's reference to the encourage-kb-contribution spec's pre-pilot path and sent .emacs.d the convention-live note with the id-index ask.
* feat(docs-lifecycle): add the lifecycle rule and wire the spec workflowsCraig Jennings2026-07-014-5/+24
| | | | | | Phase 1 of the docs-lifecycle build. claude-rules/docs-lifecycle.md captures the shape: formal-vs-notes location split (docs/specs/ vs docs/design/), an authoritative org-keyword status heading with dated history and an :ID: UUID, the two-sequence keyword header that keeps decision cookies computing, named owners for every transition, and the one-grep status board. The four workflows each take their piece: spec-create emits into docs/specs/ and stamps DRAFT in the template; spec-review checks location (legacy spots stay reviewable until :LAST_SPEC_SORT: is stamped) and owns the DRAFT-to-READY flip plus the demote path; spec-response owns READY-to-DOING at decomposition, stamps :SPEC_ID: on the build parent, and always emits the flip-to-IMPLEMENTED task; task-audit reconciles DOING specs against their bound parent's keyword.