feat: dialing in all sorts of stuff #3

Merged
joshtronic merged 24 commits from development into master 2026-05-16 19:47:29 +00:00
Owner
No description provided.
feat: slug agent branches from issue title; glob cleanup
All checks were successful
Lint / check-sync (push) Successful in 4s
4699a78210
Branch is now agent/N-<slug> when the title yields a non-empty
slug, falling back to bare agent/N otherwise. Recovery sweep and
exit-trap cleanup glob agent/N plus agent/N-* so a title edit
between runs doesn't orphan the old branch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
feat: split config from code; rehome runtime to XDG paths
All checks were successful
Lint / check-sync (push) Successful in 4s
7f05ac4eec
Code and per-host config now live in separate trees. The runtime
checkout sits at ~/.local/share/tick/ (XDG_DATA_HOME). Secrets
(.env) and project topology (projects/*.conf) move to ~/.config/
tick/ (XDG_CONFIG_HOME), where they survive git pull, can have
tighter permissions, and don't risk accidental commits.

- bin/tick.sh and bin/validate.sh source .env and read projects/
  from $TICK_CONFIG_DIR (default ${XDG_CONFIG_HOME:-~/.config}/tick)
- systemd/tick.service now points at %h/.local/share/tick
- bin/install.sh scaffolds the config dir, seeds .env from
  .env.example (chmod 600) on first run, idempotent on re-run
- bin/uninstall.sh comment updated to reflect new layout
- .env.example loses the misleading "loaded via EnvironmentFile="
  claim (tick.sh sources it directly)
- .gitignore drops state/ (state was never in the code tree)
- README documents the new install flow and the host-side layout

This also unblocks dogfooding tick on itself: the runtime copy at
~/.local/share/tick is distinct from any ~/Code/tick worktree the
harness might create while working a tick-on-tick issue.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
feat: drop per-project config, discover repos via Forgejo, clone on demand
All checks were successful
Lint / check-sync (push) Successful in 4s
11b8ee9509
The whole per-project config layer (~/.config/tick/projects/*.conf) is
gone. Tick now discovers what to work on dynamically:

- Bot identity: resolved from the token via GET /api/v1/user. No
  BOT_USER setting; the token IS the identity.
- Repo discovery: GET /user/repos filtered to repos where the bot has
  push permission. Default branch is read from the API response, so
  PR_BASE config disappears too.
- Recovery sweep: one GET /repos/issues/search?assigned=true call
  replaces the N per-repo loop.
- Local clone: if ~/Code/<repo> is missing, Tick clones via SSH
  (git@<host>:owner/repo.git, host derived from FORGEJO_URL).
- Preflight: after clone, verify CLAUDE.md exists at the repo root.
  If not, call agent-block.sh with a precise reason and bail -- same
  block path Claude itself uses, so the issue ends up in Status/Blocked
  awaiting human attention. The human adds CLAUDE.md, removes the
  label, next tick re-claims.

Adding a repo to Tick now means: add the bot user as a collaborator
with write perms, ensure Agent + Status/Blocked labels exist, and
drop a CLAUDE.md at the repo root. No harness-side action needed.

.env shrinks to three required keys (CLAUDE_CODE_OAUTH_TOKEN,
FORGEJO_URL, FORGEJO_TOKEN) plus four optional knobs (FORGEJO_SSH_HOST,
TICK_TIMEOUT, TICK_CODE_ROOT, TICK_STATE_DIR). validate.sh now checks
env + bot identity + accessible repos in one pass instead of looping
per-conf.

This also enables clean dogfooding: this very repo has no CLAUDE.md,
so the first tick pointed at it will block with that exact reason --
proving the preflight path end-to-end before any real work happens.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
chore: de-Tick the re-Igor
All checks were successful
Lint / check-sync (push) Successful in 4s
aab7cdea12
feat: more safety
All checks were successful
Lint / check-sync (push) Successful in 4s
bb3798d215
feat: supply chain auditing and alerting
All checks were successful
Lint / check-sync (push) Successful in 4s
d7db0a9e03
feat: usage based Claude, force linting
All checks were successful
Lint / check-sync (push) Successful in 5s
93c0ad64e0
feat: include manual test plan in PRs
All checks were successful
Lint / check-sync (push) Successful in 3s
885eebcc01
Adds Igor's brain as a first-class system-prompt source. Three pieces:

- forgejo_repo_exists(): new lib/forgejo.sh helper. One-shot API
  check for whether a given <owner>/<name> exists and the bot can
  reach it.

- Bootstrap block at tick start (after BOT_USER resolves, before
  recovery sweep). Hard-required: <bot>/brain must exist or the
  tick halts with exit 4. Soft: <bot>/website existence is logged
  as a warning but doesn't block other repo work. Idempotent --
  clones missing local copies, no-ops otherwise.

- System-prompt assembly prepends brain/identity.md + brain/index.md
  to AGENTS.md (most-stable-first for prompt caching). Falls back
  to AGENTS.md alone if either brain file is missing in place,
  with a warning log line.

Also nests harness clones by owner: repo_path_for now produces
$IGOR_CODE_ROOT/<owner>/<name>/ instead of basename only. Mirrors
Forgejo's URL structure and isolates harness clones from any
interactive workspace clones the operator keeps.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
refactor: move harness repo clones under state dir
All checks were successful
Lint / check-sync (push) Successful in 5s
d0c29d3753
Per-repo clones now live at
~/.local/state/igor/repos/<owner>/<repo>/ instead of ~/Code/. This
isolates harness state entirely under ~/.local/state/igor/ (alongside
worktrees and the flock) and leaves ~/Code/ as the operator's
workspace, untouched by Igor.

Knock-on simplifications:

- IGOR_CODE_ROOT env var removed; the only override needed for layout
  is IGOR_STATE_DIR, which already gates everything harness-managed.
- repo_path_for now uses $IGOR_REPO_ROOT (derived as $IGOR_STATE_DIR/repos).
- validate.sh's "code root writable" check folded into the existing
  "state dir writable" check, which now verifies both worktrees/ and
  repos/ subdirs.
- Brain at ~/.local/state/igor/repos/<bot>/brain/, website similarly.
- Docs updated across architecture / setup / onboarding-a-repo.

XDG semantics: state dir is for "data that persists across restarts
but is regenerable" -- exactly what harness clones are. Re-cloning
from Forgejo is cheap; backing them up is unnecessary.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 2 of the brain build. Claude optionally writes
.git/IGOR_JOURNAL.md in the worktree before exit; after Claude
exits, the harness reads that file, appends it (with a UTC
timestamp and the repo#issue header) to brain/journal/YYYY-MM-DD.md
in the bot's brain clone, commits, and pushes direct to brain's
master.

Trust boundary: the worktree of the target repo doesn't reach
across to brain. The harness owns the brain commit, same pattern
as the harness-generated dependency audit in PR bodies.

Failure handling: pull/commit/push is best-effort. If any step
fails (network blip, conflict, etc.), log a warning and continue
with normal outcome classification -- the tick doesn't abort over
a journal entry.

AGENTS.md adds a "Brain journal" section explaining the contract:
short, first-person, in voice, two paragraphs max, fabrication not
tolerated. Same content rules as identity.md's guardrails apply.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
feat: tier 2 discretionary maintenance ticks
All checks were successful
Lint / check-sync (push) Successful in 4s
39781f0e6e
When discovery finds no claimable work, optionally fire one
maintenance pass on a random eligible repo. Three throttles gate
this:

- IGOR_DISCRETIONARY_RATE (default 0 -- off). Probability per empty
  tick that any maintenance happens at all. Range 0.0-1.0.
- IGOR_MAX_OPEN_PRS (default 3). If the bot already has this many
  open PRs across all repos, hold off -- don't pile review work on
  the human.
- Per-repo cooldown, random 5-7 days each check. State in
  ~/.local/state/igor/discretionary-state.json keyed by repo.

The maintenance task is one combined type (not split audit /
dep-check / SEO). Each repo's CLAUDE.md declares what maintenance
means for it via a "Maintenance" section. Claude runs the declared
checks, writes findings to .git/IGOR_MAINTENANCE_FINDINGS.md, exits.
Harness reads the findings file and files a single Agent-labeled
issue with the content -- tier 1 picks it up on a future tick like
any other ticket. Same trust pattern as the journal commit.

If findings file is empty (nothing notable), the harness just
updates the cooldown state and exits silently.

New helpers:
- forgejo_count_bot_open_prs in lib/forgejo.sh
- discretionary_state_file / maintenance_last_run /
  maintenance_mark_done / maintenance_eligible in tick.sh

Docs updates:
- AGENTS.md: "Maintenance ticks (discretionary)" section explaining
  the contract Claude sees.
- docs/architecture.md: step 8 in the per-tick flow.
- docs/onboarding-a-repo.md: optional step 5, what a repo's
  CLAUDE.md Maintenance section should look like.
- .env.example: documents IGOR_DISCRETIONARY_RATE and
  IGOR_MAX_OPEN_PRS.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When tier 1 finds no claimable tickets AND tier 2 finds no
maintenance-eligible repos, optionally fall through to tier 3:
one freeform Claude invocation against the bot's own website.

The flow:

- Same outer throttles as tier 2 (IGOR_DISCRETIONARY_RATE dice,
  IGOR_MAX_OPEN_PRS cap). Tier 3 also respects the one-PR-per-repo
  rule -- skip if there's already an open Igor PR on the website.
- Worktree on a fresh agent/discretionary-<timestamp> branch off
  the website's default branch.
- User message tells Claude: read website CLAUDE.md, pick ONE
  focused improvement (post, design tweak, copy, layout), make
  the change, write PR_BODY.md, run npm test.
- Outcome flow mirrors tier 1: HEAD-equals-branch check, scope
  cap, vacuous-tests heuristic, force-with-lease push,
  idempotent PR open. No "Closes #N" footer since there's no
  source issue.
- Journal write same as tier 1: if Claude leaves IGOR_JOURNAL.md,
  harness appends to brain/journal/YYYY-MM-DD.md and commits.

If Claude produces no commits (nothing felt right after looking
around), exit cleanly. Empty self-directed ticks are fine.

Per the layered voice model in identity.md, the prompt explicitly
names this as the fever-dream venue -- personality welcome,
unlike the sterile register for code-repo work.

AGENTS.md gains a "Self-directed website ticks" section parallel
to the existing "Maintenance ticks" section so Claude has the
contract spelled out.

docs/architecture.md gains step 9 in the per-tick flow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two improvements based on dogfood feedback:

1. Maintenance auto-detect, no per-repo declaration required.

   Previously tier 2 required each repo's CLAUDE.md to have a
   "Maintenance" section declaring routine checks. Too much
   ceremony for the common case -- the audit tool is obvious from
   the stack.

   New default: Claude detects the stack and runs the standard
   audit + dep-freshness commands for the ecosystem:

     package.json -> npm audit + npm outdated
     Cargo.toml   -> cargo audit + cargo outdated
     pyproject /  -> pip-audit + pip list --outdated
     requirements
     go.mod       -> govulncheck + go list -m -u all
     Gemfile      -> bundle audit + bundle outdated

   If a tool isn't installed, Claude installs it within the
   session. Per-repo Maintenance section is now optional -- use
   only when you want non-default behavior (custom thresholds,
   extra checks like link audit / SEO scan, non-standard tooling).

2. /security-review as a pre-push check on every PR.

   AGENTS.md's PR-mode rules now require Claude to run the built-
   in /security-review slash command on the diff before exit. If
   it flags anything material (injection, leaked secret, unsafe
   deserialization, auth bypass), fix or block. Trivial findings
   can be exited past. This is Claude reviewing its own diff for
   security before push, distinct from tier 2's dep-CVE scan
   (which has no diff to review).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix: deny reads of new .env location + state dir
All checks were successful
Lint / check-sync (push) Successful in 4s
Lint / check-sync (pull_request) Successful in 4s
aca028890d
When .env moved from ~/.config/igor/ to $IGOR_HOME/.env (which
lives at ~/.local/share/igor/.env on the systemd-managed install),
the deny list wasn't updated. The actual secrets file was no
longer protected.

Add denies for:
- ~/.local/share/igor/** -- where the harness lives, .env included
- ~/.local/state/igor/** -- worktrees, lock, discretionary-state.json

Kept the stale ~/.config/igor/** denies as defensive. They protect
nothing critical now but cost nothing and would catch a misconfig.

Doesn't address env-variable leakage (ANTHROPIC_API_KEY etc. are
exported into Claude's process and accessible via env / printenv /
shell expansion). That's a deeper sandboxing problem, out of scope
for v1 deploy. Direct file access is the obvious attack surface;
this closes that hole.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
joshtronic deleted branch development 2026-05-16 19:47:29 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
joshtronic/igor!3
No description provided.