Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions docs/hygiene-history/ticks/2026/05/13/2125Z.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
---
tick: 2026-05-13T21:25Z
branch: otto-routines-git-tracked-autonomous-loop-2026-05-13
pr: TBD
Comment thread
AceHack marked this conversation as resolved.
Outdated
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 `<<autonomous-loop>>` 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 `* * * * * <<autonomous-loop>>` (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)
95 changes: 95 additions & 0 deletions tools/routines/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# `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/<id>/`:

- `SKILL.md` — prompt body + YAML frontmatter (`name`, `description`)
- `schedule.json` — cron expression + task metadata (cronExpression, notifyOnCompletion)

The runtime stores routines at `~/.claude/scheduled-tasks/<id>/SKILL.md`;
this directory is the **canonical source** and the runtime location is
generated from it via `bun tools/routines/install.ts`.
Comment thread
AceHack marked this conversation as resolved.

## Two-layer architecture

| Layer | Path | Authority |
|---|---|---|
| **Canonical** (this directory) | `tools/routines/<id>/` | git-tracked, PR-reviewed, diffable, shareable across maintainer machines |
| **Runtime** | `~/.claude/scheduled-tasks/<id>/` | 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/<id>/SKILL.md`:

```markdown
---
name: <id>
description: <one-line description for sidebar + skill router>
---

<prompt body — fully self-contained, no prior conversation context.
Each fire is a fresh Claude session cold-boot.>
```

2. Create `tools/routines/<id>/schedule.json`:

```json
{
"taskId": "<id>",
"cronExpression": "0 */2 * * *",
"description": "<same as SKILL.md description>",
"notifyOnCompletion": true
}
```

3. Run `bun tools/routines/install.ts` — copies SKILL.md to runtime path.

4. Ask Otto (or call directly via the `scheduled-tasks` MCP) to register
the cron expression with the runtime by invoking
Comment thread
AceHack marked this conversation as resolved.
Outdated
Comment thread
AceHack marked this conversation as resolved.
Outdated
`create_scheduled_task(taskId, cronExpression, prompt, description)`.
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 `<<autonomous-loop>>` | `* * * * *` (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 Otto bootstream
(`docs/research/2026-05-12-otto-canonical-bootstream-multi-foreground-surface-orchestrator-ifs-format.md`)
require it 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: only `.sh` allowed there)
Comment thread
AceHack marked this conversation as resolved.
Outdated
- [.claude/rules/rule-0-no-sh-files.md](../../.claude/rules/rule-0-no-sh-files.md) — TS for everything else
24 changes: 24 additions & 0 deletions tools/routines/autonomous-loop/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
name: autonomous-loop
description: Otto autonomous-loop tick — fresh-session cold-boot, fires every 2 hours on Desktop
---

Otto 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 <noreply@anthropic.com>

Repo path: /Users/acehack/Documents/src/repos/Zeta
Comment thread
AceHack marked this conversation as resolved.
Outdated
Comment thread
AceHack marked this conversation as resolved.
Outdated
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.
6 changes: 6 additions & 0 deletions tools/routines/autonomous-loop/schedule.json
Original file line number Diff line number Diff line change
@@ -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
}
143 changes: 143 additions & 0 deletions tools/routines/install.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
#!/usr/bin/env bun
/**
* tools/routines/install.ts
*
* Syncs canonical routine sources (tools/routines/<id>/SKILL.md) to the
* Claude Desktop runtime location (~/.claude/scheduled-tasks/<id>/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, ask Otto (or call directly) to run `create_scheduled_task`
* for any routines whose schedule.json lists a cronExpression not yet
* registered. The approval dialog is the consent step.
*
* Composes with tools/setup/ install-graph pattern; obeys rule-0 (TS, not bash).
*/

import {
existsSync,
readdirSync,
readFileSync,
mkdirSync,
writeFileSync,
} from "node:fs";
import { join, resolve } from "node:path";
import { homedir } from "node:os";

const REPO_ROUTINES_DIR = resolve(import.meta.dir);
const RUNTIME_TASKS_DIR = join(homedir(), ".claude", "scheduled-tasks");

Comment thread
AceHack marked this conversation as resolved.
Outdated
type Action =
| "created"
| "updated"
| "skipped-unchanged"
| "skipped-missing-skill";

interface SyncResult {
taskId: string;
action: Action;
runtimePath: string;
cronExpression?: string;
scheduleMissing?: boolean;
}

function listRoutines(): string[] {
if (!existsSync(REPO_ROUTINES_DIR)) return [];
return readdirSync(REPO_ROUTINES_DIR, { withFileTypes: true })
.filter((d) => d.isDirectory())
.map((d) => d.name);
}

function readSchedule(srcDir: string): { cronExpression?: string; missing: boolean } {
const path = join(srcDir, "schedule.json");
if (!existsSync(path)) return { missing: true };
try {
const parsed = JSON.parse(readFileSync(path, "utf8")) as { cronExpression?: string };
return { cronExpression: parsed.cronExpression, missing: false };
} catch {
return { missing: false };
Comment thread
AceHack marked this conversation as resolved.
Outdated
}
}

function syncRoutine(taskId: string): SyncResult {
const srcDir = join(REPO_ROUTINES_DIR, taskId);
const srcSkill = join(srcDir, "SKILL.md");
const dstDir = join(RUNTIME_TASKS_DIR, taskId);
const dstSkill = join(dstDir, "SKILL.md");

if (!existsSync(srcSkill)) {
return { taskId, action: "skipped-missing-skill", runtimePath: dstSkill };
}

const srcContent = readFileSync(srcSkill, "utf8");
let action: Action = "created";

if (existsSync(dstSkill)) {
const dstContent = readFileSync(dstSkill, "utf8");
action = dstContent === srcContent ? "skipped-unchanged" : "updated";
}

if (action !== "skipped-unchanged") {
mkdirSync(dstDir, { recursive: true });
writeFileSync(dstSkill, srcContent);

Check failure

Code scanning / CodeQL

Potential file system race condition High

The file may have changed since it
was checked
.
Comment thread
AceHack marked this conversation as resolved.
Fixed
}

const { cronExpression, missing } = readSchedule(srcDir);
return {
taskId,
action,
runtimePath: dstSkill,
cronExpression,
scheduleMissing: missing,
};
}

function main() {
console.log(`tools/routines/install.ts`);
console.log(` source: ${REPO_ROUTINES_DIR}`);
console.log(` target: ${RUNTIME_TASKS_DIR}\n`);

const routines = listRoutines().filter((id) => id !== "install.ts" && !id.endsWith(".md"));
if (routines.length === 0) {
console.log("No routines found under tools/routines/");
return;
}

const results = routines.map(syncRoutine);

for (const r of results) {
const tag = `[${r.action}]`.padEnd(22, " ");
console.log(`${tag} ${r.taskId}`);
console.log(` runtime: ${r.runtimePath}`);
if (r.cronExpression) {
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 && (r.action === "created" || r.action === "updated"),
);
Comment thread
AceHack marked this conversation as resolved.
if (needsRegistration.length > 0) {
console.log(`\nNext step — register cron schedules via the scheduled-tasks MCP:`);
console.log(`(in a Claude session, ask Otto to run create_scheduled_task for each)\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,
};
console.log(
`\nDone. created=${summary.created} updated=${summary.updated} unchanged=${summary.unchanged} missing=${summary.missingSkill}`,
);
Comment thread
AceHack marked this conversation as resolved.
}

main();
Comment thread
AceHack marked this conversation as resolved.
Outdated
Loading