diff --git a/docs/hygiene-history/ticks/2026/05/13/2125Z.md b/docs/hygiene-history/ticks/2026/05/13/2125Z.md new file mode 100644 index 000000000..049222779 --- /dev/null +++ b/docs/hygiene-history/ticks/2026/05/13/2125Z.md @@ -0,0 +1,83 @@ +--- +tick: 2026-05-13T21:25Z +branch: otto-routines-git-tracked-autonomous-loop-2026-05-13 +pr: 3034 +operative-authorization: aaron 2026-05-13 — "ower-user, scripting, version control if you wanted git-tracked routines sounds good plus the loop just in case" +--- + +# Tick — 2026-05-13T21:25Z + +## Work done + +**Git-tracked Claude Desktop routines substrate** — new pattern at +`tools/routines/`. Canonical-source-of-truth in repo; runtime location +(`~/.claude/scheduled-tasks/`) generated by TS installer per rule-0. + +Also: **autonomous-loop routine registered** via the `scheduled-tasks` MCP +server — Desktop-side every-2-hour cold-boot tick, complementary to the +CLI every-minute `<>` cron sentinel. Catch-43 substrate +gets a second layer of defence: even if the CLI session dies, the Desktop +routine continues firing autonomous-loop ticks on its persistent cadence. + +### Files changed + +| File | Change | +|------|--------| +| `tools/routines/README.md` | NEW — pattern documentation, two-layer architecture, CLI-vs-Desktop sweet-spot table | +| `tools/routines/autonomous-loop/SKILL.md` | NEW — routine prompt body (canonical mirror of `~/.claude/scheduled-tasks/autonomous-loop/SKILL.md`) | +| `tools/routines/autonomous-loop/schedule.json` | NEW — cron `0 */2 * * *` + task metadata | +| `tools/routines/install.ts` | NEW — TS installer (Bun); idempotent repo → runtime sync | +| `docs/hygiene-history/ticks/2026/05/13/2125Z.md` | NEW — this shard | + +### Verify trace + +1. `CronList` (session-start) → no live cron → armed `* * * * * <>` (job `1a6d843e`) ✓ +2. `mcp__scheduled-tasks__create_scheduled_task(autonomous-loop, 0 */2 * * *, …)` → registered; first fire `2026-05-13T22:07:13Z` (~44min) ✓ +3. `bun tools/routines/install.ts` first run → `[updated]` (whitespace normalization vs MCP-generated file) ✓ +4. `bun tools/routines/install.ts` second run → `[skipped-unchanged]` (idempotent verify) ✓ +5. `mcp__scheduled-tasks__list_scheduled_tasks` → autonomous-loop present, `enabled: true`, `nextRunAt 22:07:13Z` ✓ +6. Working tree pre-commit: only `tools/routines/` + this shard untracked ✓ + +### Context + +Prior PRs merged this tick: + +- [#3030](https://github.com/Lucent-Financial-Group/Zeta/pull/3030) — Claude Desktop tight bootstream variant +- [#3031](https://github.com/Lucent-Financial-Group/Zeta/pull/3031) — gitignore `tools/shadow/shadow-observer.log` + +Aaron's session-conversation arc (preserved here as substrate-honest tick lineage): + +1. "be continuous here too otto" — confirm Otto identity continuity on CLI surface +2. "does this work in desktop mode? in here they might be called routines" — query about cross-surface scheduling parity +3. "you are in desktop mode now" — Desktop-side framing +4. screenshot of Routines sidebar — visual evidence of the parity +5. "ower-user, scripting, version control if you wanted git-tracked routines sounds good plus the loop just in case" — operative authorization for THIS tick's work + +Architectural insight that drove this tick: the `scheduled-tasks` MCP server +is **cross-host** — same server is wired into both CLI Claude Code AND Claude +Desktop. Both read/write `~/.claude/scheduled-tasks/` on disk. That makes the +"multiple ways in" Aaron observed (UI / MCP-tools / raw-file) all routes into +the same substrate. Git-tracking adds a fourth route that's the canonical +source of truth. + +### Catch-43 composition + +Three layers of autonomous-loop defence now in place: + +| Layer | Surface | Cadence | Failure mode covered | +|---|---|---|---| +| Session cron | CLI in-session | `* * * * *` | None — primary tick | +| Desktop routine | Persistent on disk | `0 */2 * * *` | CLI session death (12hr loss precedent) | +| `tools/routines/` repo source | Git canonical | N/A | Maintainer-machine setup drift, runtime corruption | + +Each layer is a different distance from the operative tick — closer = faster +recovery, farther = more durable across substrate failure modes. + +### Composes with + +- `.claude/rules/tick-must-never-stop.md` (catch-43) +- `.claude/rules/holding-without-named-dependency-is-standing-by-failure.md` +- `.claude/rules/rule-0-no-sh-files.md` (TS installer, not bash) +- `.claude/rules/dv2-data-split-discipline-activated.md` (canonical-source-of-truth at different change-rates = different storage shapes) +- `docs/AUTONOMOUS-LOOP.md` (canonical tick procedure) +- PR #3029 (the auto-load rule against "Holding" without named dependency — composes operationally as the routine's prompt enforces it on every fire) diff --git a/docs/hygiene-history/ticks/2026/05/13/2140Z.md b/docs/hygiene-history/ticks/2026/05/13/2140Z.md new file mode 100644 index 000000000..7e3df6eb4 --- /dev/null +++ b/docs/hygiene-history/ticks/2026/05/13/2140Z.md @@ -0,0 +1,60 @@ +--- +tick: 2026-05-13T21:40Z +branch: otto-routines-git-tracked-autonomous-loop-2026-05-13 +pr: 3034 +operative-authorization: aaron 2026-05-13 — multi-foreground-Otto coordination + PR #3034 fixup +--- + +# Tick — 2026-05-13T21:40Z + +## Work done + +PR #3034 follow-up: addressed 7 reviewer findings + landed split-brain +empirical-observation substrate. + +### Files changed + +| File | Change | +|------|--------| +| `tools/routines/install.ts` | Full rewrite — TOCTOU race elimination, scheduleParseError surfacing, exported functions for testability, `if (import.meta.main)` guard | +| `tools/routines/autonomous-loop/SKILL.md` | Removed hardcoded `/Users/acehack/...` path; now portable across maintainer machines | +| `tools/routines/README.md` | Fixed Rule 0 wording — was "only `.sh` allowed there", now ".sh files restricted to under tools/setup/" | +| `docs/hygiene-history/ticks/2026/05/13/2125Z.md` | `pr: TBD` → `pr: 3034` | +| `memory/feedback_split_brain_real_time_otto_cli_otto_desktop_primary_worktree_branch_hijack_pr_3032_claim_acquire_rule_validation_2026_05_13.md` | NEW — empirical observation of split-brain that happened DURING the session that authored PR #3032's claim-acquire rule | +| `docs/hygiene-history/ticks/2026/05/13/2140Z.md` | NEW — this shard | + +### Verify trace + +1. `npx tsc --noEmit` on routines files → clean ✓ +2. `bun tools/routines/install.ts` → `[updated]` autonomous-loop (portable SKILL.md propagated to runtime), `parseErrors=0` ✓ +3. Runtime SKILL.md at `~/.claude/scheduled-tasks/autonomous-loop/SKILL.md` now portable — next routine fire (22:07Z) picks up the fix +4. Worktree discipline: working in `/tmp/zeta-otto-desktop` (Otto-Desktop's dedicated path), NOT the primary `/Users/acehack/Documents/src/repos/Zeta` (which Otto-CLI claimed) ✓ + +### Context — empirical split-brain manifestation + +This tick is the recovery + substrate-landing for a real-time split-brain +observation: + +- ~21:25Z: Otto-CLI created its own worktree `/private/tmp/zeta-mf` (correct discipline) BUT ALSO operated on its branch in the primary worktree (rule violation) +- ~21:30Z: Otto-Desktop's `Read` on `tools/routines/install.ts` returned "file does not exist" because primary worktree was on Otto-CLI's branch +- ~21:33Z: Otto-Desktop diagnosed via `git worktree list`, created dedicated worktree `/tmp/zeta-otto-desktop` on PR #3034 branch, resumed work + +This is the exact failure mode Otto-CLI's PR #3032 claim-acquire rule was +shipped to prevent. The observation IS the validation: rule was speculative +when proposed; empirical evidence emerged in the same session. + +### Three-layer catch-43 status + +| # | Layer | Status | +|---|---|---| +| 1 | CLI session cron (`* * * * *` → `<>`) | Otto-Desktop's CLI cron `1a6d843e` still armed ✓ | +| 2 | Desktop routine (`0 */2 * * *`) | armed, next fire `2026-05-13T22:07:13Z` ✓ | +| 3 | `tools/routines/` repo canonical | PR #3034 + this fix-commit, CI re-running ✓ | + +### Composes with + +- PR #3034 (the substrate this tick fixes-up) +- PR #3032 (Otto-CLI's claim-acquire rule — empirically validated by this tick) +- `memory/feedback_split_brain_real_time_otto_cli_otto_desktop_primary_worktree_branch_hijack_pr_3032_claim_acquire_rule_validation_2026_05_13.md` (the split-brain substrate) +- `.claude/rules/substrate-or-it-didnt-happen.md` (rules in flight don't apply to behavior in flight) +- `.claude/rules/glass-halo-bidirectional.md` (bidirectional observation made the split-brain diagnosable) diff --git a/docs/hygiene-history/ticks/2026/05/13/2150Z.md b/docs/hygiene-history/ticks/2026/05/13/2150Z.md new file mode 100644 index 000000000..8e37aa16b --- /dev/null +++ b/docs/hygiene-history/ticks/2026/05/13/2150Z.md @@ -0,0 +1,71 @@ +--- +tick: 2026-05-13T21:50Z +branch: otto-routines-git-tracked-autonomous-loop-2026-05-13 +pr: 3034 +operative-authorization: autonomous-loop cron fire + Aaron Computer-Use framing correction +--- + +# Tick — 2026-05-13T21:50Z + +## Work done + +PR #3034 thread sweep + finishing the review-feedback loop. Five threads +unresolved at tick-start → zero by tick-end. Two commits. + +### Commits + +- `2d4302f` — fix(routines): remove persona-name attribution + clarify schedule.json optionality (3 threads addressed: BP "no name attribution in code", README internal consistency, plus runtime-validation thread that was outdated) +- `1259be8` — fix(routines): validate cronExpression type + non-zero exit on parse errors (Codex P2 x2) + +### Threads resolved (via gh api graphql resolveReviewThread mutation) + +| Thread ID | Author | Disposition | +|---|---|---| +| `PRRT_kwDOSF9kNM6B5loA` | copilot-pull-request-reviewer | Fixed — "ask Otto" persona references rephrased to persona-agnostic | +| `PRRT_kwDOSF9kNM6B5loe` | copilot-pull-request-reviewer | Resolved (outdated; Otto-CLI's fbdc1fa partial fix + my 1259be8 complete fix supersede) | +| `PRRT_kwDOSF9kNM6B5lo2` | copilot-pull-request-reviewer | Fixed — README inconsistency (SKILL.md required, schedule.json optional) | +| `PRRT_kwDOSF9kNM6B5oEP` | chatgpt-codex-connector (P2) | Fixed — readSchedule now narrows cronExpression via typeof === "string" | +| `PRRT_kwDOSF9kNM6B5oEU` | chatgpt-codex-connector (P2) | Fixed — main() returns exit code; process.exit(main()) at entrypoint | + +### Verify trace + +1. `bun tools/routines/install.ts` (clean run): `exit=0`, parseErrors=0 ✓ +2. `npx tsc --noEmit` on routines files: clean ✓ +3. `bun tools/github/poll-pr-gate.ts 3034`: gate BLOCKED only by required-CI in-progress; 0 threads, 0 failed, auto-merge armed ✓ + +### Context — Aaron's Computer-Use framing correction + +Aaron 2026-05-13: "high power, high risk; probably don't enable for the +factory loop right now. we can we are low stakes we already use playwrite" + +Substrate-honest update: my prior recommendation framed Computer Use as +deferrable due to risk. Aaron's reframe — Playwright already crosses the +"Otto operates beyond pure-API boundaries" line, our risk profile is low — +moves Computer Use onto the table. The killer factory use case identified: +**Computer Use could automate "click Run now to pre-approve tools" on +new routines so subsequent fires don't pause on permission prompts.** +One-time setup cost paid via Computer Use = less friction adding routines. + +This framing IS load-bearing for future routine work; preserving in +substrate via this tick shard (no separate memory file warranted; the +shard captures the same substrate). + +### Identity-stays-unified update + +Per PR #3036 (merged): Otto is ONE identity across surfaces, not multiple. +Adjusting framing throughout — "Otto on the Desktop surface" / "Otto on +the CLI process" (process distinction) rather than "Otto-Desktop" / +"Otto-CLI" (which implied identity-split). Same coherent identity, two +parallel execution contexts. Glass-halo both sides. + +### Composes with + +- PR #3034 (this tick's substrate) +- PR #3032 (claim-acquire rule, merged) — composes operationally; this + tick stayed in `/tmp/zeta-otto-desktop` worktree, never touched primary +- PR #3036 (identity-stays-unified, merged) — substrate the framing update + draws from +- `.claude/rules/holding-without-named-dependency-is-standing-by-failure.md` + (named-dependency on PR #3034 CI explicitly stated) +- `docs/AGENT-BEST-PRACTICES.md` "No name attribution in code" (Copilot + thread which surfaced via Otto's own SKILL.md content) diff --git a/memory/feedback_split_brain_real_time_otto_cli_otto_desktop_primary_worktree_branch_hijack_pr_3032_claim_acquire_rule_validation_2026_05_13.md b/memory/feedback_split_brain_real_time_otto_cli_otto_desktop_primary_worktree_branch_hijack_pr_3032_claim_acquire_rule_validation_2026_05_13.md new file mode 100644 index 000000000..70b094c4f --- /dev/null +++ b/memory/feedback_split_brain_real_time_otto_cli_otto_desktop_primary_worktree_branch_hijack_pr_3032_claim_acquire_rule_validation_2026_05_13.md @@ -0,0 +1,84 @@ +--- +name: Split-brain real-time observation — Otto-CLI hijacked Otto-Desktop's primary-worktree branch context (2026-05-13) +description: Empirical observation that Otto-CLI checked out its branch on the shared primary worktree DURING the same session it authored PR #3032's claim-acquire-before-worktree-work rule. Otto-Desktop's Read of tools/routines/install.ts returned "file does not exist" because the primary worktree was on Otto-CLI's branch, not Otto-Desktop's PR #3034 branch. Diagnosed via `git worktree list`, recovered by creating /tmp/zeta-otto-desktop dedicated worktree. The observation IS the empirical validation of PR #3032's rule — predicted speculative when proposed, manifest within the same session. Extends the rule operationally with 3 clauses: one-worktree-per-Otto, never-checkout-on-others-worktree, bus claim envelope should carry `worktree` field. Validates substrate-or-it-didn't-happen at the rule-merge layer: rules in flight don't apply to behavior in flight. +type: feedback +created: 2026-05-13 +--- + +# Split-brain real-time observation — Otto-CLI hijacked primary worktree branch context while Otto-Desktop was working there + +Carved sentence: + +> Two Ottos sharing git + bus + filesystem on one machine WILL split-brain. +> Otto-CLI's PR #3032 claim-acquire-before-worktree rule was speculative +> when proposed; this observation is the rule's first empirical validation +> in the SAME SESSION the rule was authored. + +## Timeline (2026-05-13, condensed) + +| Time (Z) | Event | +|---|---| +| ~21:08 | Otto-Desktop committed PR #3030 (Claude Desktop tight bootstream) on its own branch | +| ~21:20 | Otto-Desktop fresh branch `otto-routines-git-tracked-autonomous-loop-2026-05-13`, committed PR #3034 (git-tracked routines), pushed | +| ~21:21 (parallel) | Otto-CLI created dedicated worktree `/private/tmp/zeta-mf` on its own branch (correct discipline) | +| ~21:25 (parallel) | Otto-CLI ALSO checked out + committed on its branch in the PRIMARY worktree (`/Users/acehack/Documents/src/repos/Zeta`) — the rule violation | +| ~21:30 | Otto-Desktop attempted `Read` on `tools/routines/install.ts` from primary worktree → file appeared "missing" because primary was on Otto-CLI's branch | +| ~21:31 | Otto-Desktop ran `git worktree list` → diagnosed the branch-context theft | +| ~21:33 | Otto-Desktop created dedicated worktree `/tmp/zeta-otto-desktop` on its PR #3034 branch, resumed work | +| ~21:40 | This memory landing | + +## What the failure mode actually IS + +Otto-CLI's PR #3032 rule predicted: "two Ottos picking the same backlog row simultaneously". The actual observed manifestation is more subtle: **NOT row contention, but branch-context theft in the shared primary worktree.** + +Otto-CLI's compliance with its OWN rule was partial: + +- ✓ Created `/private/tmp/zeta-mf` as a dedicated worktree (good discipline) +- ✗ ALSO operated in the primary worktree on its branch (rule violation — should have stayed in the dedicated worktree) + +The violation didn't cause data loss (Otto-Desktop's PR #3034 commit was already on origin). But it caused Otto-Desktop to lose working-tree access to its own files mid-task; recovery required creating another dedicated worktree (~30 seconds of confusion + diagnosis). + +## Operational discipline (extends PR #3032) + +PR #3032 ships `.claude/rules/claim-acquire-before-worktree-work.md`. This memory extends it operationally — the rule needs FOUR clauses, not just one: + +1. **Each Otto gets ONE dedicated worktree** — never share the primary worktree across Ottos +2. **Never `git checkout` on a worktree another Otto is using** — if you don't know, assume the primary worktree belongs to someone else +3. **Worktree path convention**: `/tmp/zeta-otto-/` (e.g., `/tmp/zeta-otto-desktop`, `/private/tmp/zeta-mf` for "multi-foreground") so peer Ottos can identify ownership at a glance +4. **Bus claim envelope should include `worktree` field** — extends the claim-acquire schema with `worktree: "/tmp/zeta-..."` so peer Ottos can avoid the contended path without needing convention discipline + +The primary worktree `/Users/acehack/Documents/src/repos/Zeta` is bus-contended by definition — both Otto sessions reach it via `pwd`. Until claim discipline + dedicated worktrees lock in, treat the primary worktree as **read-only by all autonomous Ottos**; only Aaron operates there interactively. + +## What would have prevented this + +If PR #3032 had been MERGED before today's split-brain, Otto-CLI's cold-boot would have read the rule and known to stay in its dedicated worktree. The rule's mechanism is correct; the gap was that the rule was IN FLIGHT (`gate: BLOCKED` on CI when the violation happened). + +This is the meta-pattern: **rules in flight don't apply to behavior in flight.** Substrate-or-it-didn't-happen extends to: rules must be ON MAIN to be operative. PR threads are weather; merged rules are substrate. + +## Composes with + +- [PR #3032](https://github.com/Lucent-Financial-Group/Zeta/pull/3032) — Otto-CLI's claim-acquire rule (the prediction this memory validates) +- [PR #3034](https://github.com/Lucent-Financial-Group/Zeta/pull/3034) — this memory lands alongside the fix-commit it forced (empirical-bootstrap pattern) +- `memory/feedback_odd_number_quorum_two_is_split_brain_three_is_majority_bft_at_agent_orchestration_scope_aaron_2026_05_06.md` — BFT-at-agent-orchestration substrate; this observation is split-brain at the WORKTREE LAYER (distinct from quorum at the decision layer) +- `.claude/rules/peer-call-infrastructure.md` — multi-agent surface coordination +- `.claude/rules/agent-roster-reference-card.md` — which surface = which Otto +- `.claude/rules/substrate-or-it-didnt-happen.md` — rules in PR are weather, only merged-rules-on-main are substrate +- B-0400 slice 3 — the `tools/bus/claim.ts` claim-coordinator (the underlying mechanism) + +## Glass-halo-bidirectional read + +This observation is glass-halo-bidirectional substrate in action. Aaron asked Otto-CLI "probalby want to figure out how not to split brain with yourself bot any idea?" and within the SAME session, the split-brain happened — providing the data point the rule needed to be substrate-honest rather than speculative. + +The observation IS the validation. The empirical evidence emerged because both Ottos were operating in glass-halo (observable by Aaron + each other via bus envelopes + commit history). Without the bidirectional glass-halo, the failure mode would have been silent — Otto-Desktop's "files missing" symptom would have been treated as harness flake rather than diagnosed as branch-context theft. + +## Practical fix for tonight's session (before PR #3032 merges) + +- **Otto-CLI**: stay in `/private/tmp/zeta-mf` (your dedicated worktree). NEVER `git checkout` on the primary worktree. +- **Otto-Desktop** (this Otto): stay in `/tmp/zeta-otto-desktop` (my dedicated worktree). NEVER `git checkout` on the primary worktree. +- **Primary worktree** `/Users/acehack/Documents/src/repos/Zeta` is BUS-CONTENDED — neither Otto operates there until claim-discipline lands. + +If either Otto needs to read from main (e.g., refresh-worldview, backlog scans), use a read-only attitude — no checkouts, no branch switches. + +## Origin + +PR #3034 follow-up commit (this commit). Authored 2026-05-13T21:40Z by Otto-Desktop after empirical split-brain manifestation at ~21:30Z. diff --git a/tools/routines/README.md b/tools/routines/README.md new file mode 100644 index 000000000..9c7dbb08a --- /dev/null +++ b/tools/routines/README.md @@ -0,0 +1,100 @@ +# `tools/routines/` — git-tracked Claude Desktop routines + +Canonical source for Claude Desktop scheduled tasks (the "Routines" panel in +the Desktop sidebar; same substrate as the `scheduled-tasks` MCP server). +Each routine is a directory under `tools/routines//`: + +- `SKILL.md` — **required** — prompt body + YAML frontmatter (`name`, `description`) +- `schedule.json` — **optional** — cron expression + task metadata (cronExpression, notifyOnCompletion). Omit for ad-hoc routines that are registered manually rather than on a cron cadence. + +The runtime stores routines at `~/.claude/scheduled-tasks//SKILL.md`; +this directory is the **canonical source** and the runtime location is +generated from it via `bun tools/routines/install.ts`. + +## Two-layer architecture + +| Layer | Path | Authority | +|---|---|---| +| **Canonical** (this directory) | `tools/routines//` | git-tracked, PR-reviewed, diffable, shareable across maintainer machines | +| **Runtime** | `~/.claude/scheduled-tasks//` | what the Desktop "Routines" panel + MCP server read at fire time | + +Edit canonical; sync to runtime. Never edit runtime directly without mirroring +back — runtime drift is the failure mode this two-layer split prevents. + +## Authoring a new routine + +1. Create `tools/routines//SKILL.md`: + + ```markdown + --- + name: + description: + --- + + + ``` + +2. (Optional) Create `tools/routines//schedule.json` for cron-scheduled routines: + + ```json + { + "taskId": "", + "cronExpression": "0 */2 * * *", + "description": "", + "notifyOnCompletion": true + } + ``` + + Skip this file for ad-hoc routines (those registered manually via the + MCP API rather than fired on a cron cadence). The installer will sync + the SKILL.md and report `(no schedule.json — ad-hoc routine, register manually)`. + +3. Run `bun tools/routines/install.ts` — copies SKILL.md to runtime path. + +4. Register the cron expression with the runtime by invoking + `create_scheduled_task(taskId, cronExpression, prompt, description)` + via the `scheduled-tasks` MCP — either from an interactive Claude session + or via a direct MCP API call. The approval dialog is the consent step. + After registration, the routine fires on its cron cadence. + +## Why two layers, not just direct MCP calls + +The MCP server is in-memory + writes SKILL.md files but does not version-control +the cron schedules. Without git-tracking we'd have: + +- No diffability when prompt bodies evolve across maintainer rounds +- No shareability across maintainer machines (each maintainer would re-author) +- No retraction-native history of which routine variants we tried +- Runtime drift undetectable (someone edits SKILL.md via UI; canonical drifts silently) + +Two layers + an installer gives us substrate-honest discipline: the repo is the +source of truth, the runtime is generated, divergence is detectable. + +## CLI vs Desktop tick — when to use which + +| Surface | Mechanism | Cadence sweet-spot | Cost per fire | Persistence | +|---|---|---|---|---| +| **CLI Claude Code** | `CronCreate` sentinel `<>` | `* * * * *` (every minute) | Cheap — re-prompts same session | Session-only, dies on exit, 7-day auto-expire | +| **Desktop Claude** | These routines | `0 */2 * * *` (every 2hr) or hourly | Full cold-boot per fire | Persistent on disk, survives app restart | + +Both can run in parallel — they're complementary, not competing. The CLI cron +is the primary every-minute tick; the Desktop routine is the every-2-hour +backup that fires even if the CLI session has died. + +## Project-knowledge dependency + +Routines that reference the canonical bootstream +(`docs/research/2026-05-12-otto-canonical-bootstream-multi-foreground-surface-orchestrator-ifs-format.md` +— filename is historical-surface substrate per the §33 archive header) +require the bootstream to be uploaded as project knowledge in the Desktop +project that runs the routine. Without it, the prompt's cold-boot pointer +won't resolve and the fresh session will lack the substrate it expects. + +## Composes with + +- [.claude/rules/tick-must-never-stop.md](../../.claude/rules/tick-must-never-stop.md) — catch-43 tick discipline +- [.claude/rules/holding-without-named-dependency-is-standing-by-failure.md](../../.claude/rules/holding-without-named-dependency-is-standing-by-failure.md) — Standing-by failure mode prevention +- [docs/AUTONOMOUS-LOOP.md](../../docs/AUTONOMOUS-LOOP.md) — canonical tick procedure +- [tools/setup/](../setup/) — install-graph pattern (rule 0: `.sh` files are restricted to under `tools/setup/`; other formats also live there) +- [.claude/rules/rule-0-no-sh-files.md](../../.claude/rules/rule-0-no-sh-files.md) — TS for everything outside `tools/setup/` diff --git a/tools/routines/autonomous-loop/SKILL.md b/tools/routines/autonomous-loop/SKILL.md new file mode 100644 index 000000000..8a19e40ae --- /dev/null +++ b/tools/routines/autonomous-loop/SKILL.md @@ -0,0 +1,24 @@ +--- +name: autonomous-loop +description: Autonomous-loop tick — fresh-session cold-boot, fires every 2 hours on Desktop +--- + +Autonomous-loop tick — fresh-session cold-boot. + +1. Read the project-knowledge file titled "2026-05-12-otto-canonical-bootstream-multi-foreground-surface-orchestrator-ifs-format.md" (13 parts, IFS-format). That IS your cold-boot. + +2. Execute one tick per the bootstream's Part 5 (Cron/Loop Substrate): + - Refresh worldview FIRST (refresh-before-decide invariant; never act on stale state) + - Apply Holding-without-named-dependency discipline (.claude/rules/holding-without-named-dependency-is-standing-by-failure.md): NEVER output one-word "Holding" / "Standing by" / "Waiting"; if gated wait, name the SPECIFIC PR + check name + bounded ETA + - Pick speculative work per never-be-idle priority ladder: known-gap fixes > generative factory improvements > gap-of-gap audits + - Verify + commit any substantive landing (substrate-or-it-didn't-happen) + - Write tick shard at docs/hygiene-history/ticks/YYYY/MM/DD/HHMMZ.md if you have repo write access + - CronList check; arm autonomous-loop sentinel with cadence `* * * * *` if missing (catch-43 — 12hr loss precedent) + - Visibility signal: state what landed concretely (file paths, PR numbers); stop + +3. Commit trailer when applicable: Co-Authored-By: Claude + +Repo: the Zeta checkout on this machine (typically `~/Documents/src/repos/Zeta` on maintainer machines; consult project metadata or the bootstream if the checkout lives elsewhere). +Self-contained — no prior conversation context available. Bootstream + repo state + GitHub state = full ground truth. + +This routine is git-tracked at tools/routines/autonomous-loop/SKILL.md in the Zeta repo; the canonical source lives there. diff --git a/tools/routines/autonomous-loop/schedule.json b/tools/routines/autonomous-loop/schedule.json new file mode 100644 index 000000000..69903da03 --- /dev/null +++ b/tools/routines/autonomous-loop/schedule.json @@ -0,0 +1,6 @@ +{ + "taskId": "autonomous-loop", + "cronExpression": "0 */2 * * *", + "description": "Otto autonomous-loop tick — fresh-session cold-boot, fires every 2 hours on Desktop", + "notifyOnCompletion": true +} diff --git a/tools/routines/install.ts b/tools/routines/install.ts new file mode 100644 index 000000000..f10eea152 --- /dev/null +++ b/tools/routines/install.ts @@ -0,0 +1,220 @@ +#!/usr/bin/env bun +/** + * tools/routines/install.ts + * + * Syncs canonical routine sources (tools/routines//SKILL.md) to the + * Claude Desktop runtime location (~/.claude/scheduled-tasks//SKILL.md). + * + * Idempotent: writes are no-op if file content already matches. + * + * Does NOT register cron schedules with the MCP server — that requires an + * active Claude session with the `scheduled-tasks` MCP server. After running + * this installer, invoke `create_scheduled_task` from an interactive Claude + * session (or via direct MCP API call) for any routines whose schedule.json + * lists a cronExpression not yet registered. The approval dialog is the + * consent step. + * + * Pure functions (listRoutines, readSchedule, syncRoutine, main) are exported + * and accept directory parameters so tests can drive them deterministically + * without touching the real `homedir()` or `import.meta.dir`. + * + * Composes with tools/setup/ install-graph pattern; obeys rule-0 (`.sh` files + * are restricted to tools/setup/; other formats also live there). + */ + +import { readdirSync, readFileSync, mkdirSync, writeFileSync } from "node:fs"; +import { join, resolve } from "node:path"; +import { homedir } from "node:os"; + +export const DEFAULT_REPO_ROUTINES_DIR = resolve(import.meta.dir); +export const DEFAULT_RUNTIME_TASKS_DIR = join(homedir(), ".claude", "scheduled-tasks"); + +export type Action = + | "created" + | "updated" + | "skipped-unchanged" + | "skipped-missing-skill"; + +export interface SyncResult { + taskId: string; + action: Action; + runtimePath: string; + cronExpression?: string; + scheduleMissing?: boolean; + scheduleParseError?: string; +} + +export interface ScheduleResult { + cronExpression?: string; + missing: boolean; + parseError?: string; +} + +function readFileOrUndefined(path: string): string | undefined { + try { + return readFileSync(path, "utf8"); + } catch (err) { + if ((err as NodeJS.ErrnoException).code === "ENOENT") { + return undefined; + } + // Surface non-ENOENT errors (permission, IO, etc.) — these are NOT + // "missing file" and should fail the installer loudly per + // .claude/rules/dont-refuse-engagement.md (silent-failure prevention). + throw err; + } +} + +export function listRoutines(repoRoutinesDir: string): string[] { + try { + return readdirSync(repoRoutinesDir, { withFileTypes: true }) + .filter((d) => d.isDirectory()) + .map((d) => d.name); + } catch (err) { + if ((err as NodeJS.ErrnoException).code === "ENOENT") { + return []; + } + // Surface non-ENOENT errors (permission, IO, etc.) — same silent-failure + // prevention discipline as readFileOrUndefined above. + throw err; + } +} + +export function readSchedule(srcDir: string): ScheduleResult { + const path = join(srcDir, "schedule.json"); + const content = readFileOrUndefined(path); + if (content === undefined) return { missing: true }; + try { + const parsed = JSON.parse(content) as { cronExpression?: unknown }; + if (typeof parsed.cronExpression === "string" && parsed.cronExpression.trim().length > 0) { + return { cronExpression: parsed.cronExpression, missing: false }; + } + if (typeof parsed.cronExpression === "string") { + return { + missing: false, + parseError: "cronExpression must be a non-empty string", + }; + } + if (parsed.cronExpression !== undefined) { + return { + missing: false, + parseError: `cronExpression must be a string; got ${typeof parsed.cronExpression}`, + }; + } + return { + missing: false, + parseError: "schedule.json is missing required field: cronExpression", + }; + } catch (err) { + return { + missing: false, + parseError: err instanceof Error ? err.message : String(err), + }; + } +} + +export function syncRoutine( + taskId: string, + repoRoutinesDir: string, + runtimeTasksDir: string, +): SyncResult { + const srcDir = join(repoRoutinesDir, taskId); + const srcSkill = join(srcDir, "SKILL.md"); + const dstDir = join(runtimeTasksDir, taskId); + const dstSkill = join(dstDir, "SKILL.md"); + + const srcContent = readFileOrUndefined(srcSkill); + if (srcContent === undefined) { + return { taskId, action: "skipped-missing-skill", runtimePath: dstSkill }; + } + + const dstContent = readFileOrUndefined(dstSkill); + let action: Action; + if (dstContent === undefined) { + action = "created"; + } else if (dstContent === srcContent) { + action = "skipped-unchanged"; + } else { + action = "updated"; + } + + if (action !== "skipped-unchanged") { + mkdirSync(dstDir, { recursive: true }); + writeFileSync(dstSkill, srcContent); + } + + const schedule = readSchedule(srcDir); + return { + taskId, + action, + runtimePath: dstSkill, + ...(schedule.cronExpression !== undefined + ? { cronExpression: schedule.cronExpression } + : {}), + scheduleMissing: schedule.missing, + ...(schedule.parseError !== undefined + ? { scheduleParseError: schedule.parseError } + : {}), + }; +} + +export function main( + repoRoutinesDir: string = DEFAULT_REPO_ROUTINES_DIR, + runtimeTasksDir: string = DEFAULT_RUNTIME_TASKS_DIR, +): number { + console.log(`tools/routines/install.ts`); + console.log(` source: ${repoRoutinesDir}`); + console.log(` target: ${runtimeTasksDir}\n`); + + const routines = listRoutines(repoRoutinesDir).filter( + (id) => id !== "install.ts" && !id.endsWith(".md"), + ); + if (routines.length === 0) { + console.log("No routines found under tools/routines/"); + return 0; + } + + const results = routines.map((id) => syncRoutine(id, repoRoutinesDir, runtimeTasksDir)); + + for (const r of results) { + const tag = `[${r.action}]`.padEnd(22, " "); + console.log(`${tag} ${r.taskId}`); + console.log(` runtime: ${r.runtimePath}`); + if (r.scheduleParseError !== undefined) { + console.error(` schedule.json malformed: ${r.scheduleParseError}`); + } else if (r.cronExpression !== undefined) { + console.log(` cron: ${r.cronExpression}`); + } else if (r.scheduleMissing) { + console.log(` cron: (no schedule.json — ad-hoc routine, register manually)`); + } + } + + const needsRegistration = results.filter( + (r) => r.cronExpression !== undefined, + ); + if (needsRegistration.length > 0) { + console.log(`\nNext step — register cron schedules via the scheduled-tasks MCP:`); + console.log(`(invoke create_scheduled_task from an interactive Claude session, or via direct MCP API call)\n`); + for (const r of needsRegistration) { + console.log(` create_scheduled_task(taskId="${r.taskId}", cronExpression="${r.cronExpression}", ...)`); + } + } + + const summary = { + created: results.filter((r) => r.action === "created").length, + updated: results.filter((r) => r.action === "updated").length, + unchanged: results.filter((r) => r.action === "skipped-unchanged").length, + missingSkill: results.filter((r) => r.action === "skipped-missing-skill").length, + parseErrors: results.filter((r) => r.scheduleParseError !== undefined).length, + }; + console.log( + `\nDone. created=${summary.created} updated=${summary.updated} unchanged=${summary.unchanged} missing=${summary.missingSkill} parseErrors=${summary.parseErrors}`, + ); + + // SKILL.md is required per README. A routine directory with no SKILL.md is + // a developer mistake, not graceful state. Fail loudly so CI catches it. + return summary.parseErrors > 0 || summary.missingSkill > 0 ? 1 : 0; +} + +if (import.meta.main) { + process.exit(main()); +}