diff --git a/.claude/skills/current-ui-qa/SKILL.md b/.claude/skills/current-ui-qa/SKILL.md new file mode 100644 index 0000000..dc937e1 --- /dev/null +++ b/.claude/skills/current-ui-qa/SKILL.md @@ -0,0 +1,173 @@ +--- +name: current-ui-qa +description: Use when a human or agent needs to do exploratory QA on existing Itervox UI before/after a UI change. Drives both the route-mocked Playwright lane and the real-daemon lane, then captures qualitative issues (visual hierarchy, copy, accessibility) automated tests cannot. Produces a structured report under `docs/qa-reports/`. +--- + +# Current Itervox UI QA + +This skill drives an exploratory QA pass over the **existing** Itervox dashboard, focused on locking the regression baseline before a UI change. It complements `make qa-current` (which catches programmatic regressions) by capturing the qualitative issues humans or agents notice when they actually drive the app. + +## Required reading + +Read `CLAUDE.md` (project root) for the project invariants — auth boundaries, SSE wiring, Zustand store rules. Everything else this skill needs is inlined below. + +## Pre-flight + +```bash +git status # confirm clean working tree +git rev-parse HEAD # capture commit +make qa-current # baseline must be green before exploratory pass +pnpm exec playwright install chromium # one-time per contributor +``` + +If `make qa-current` fails, do **not** proceed — fix the regression first. + +## Setup + +Pick a lane: + +- **Route-mocked (faster, deterministic):** + ```bash + cd web && pnpm test:ui-current --ui # opens Playwright UI; navigate scenario by scenario + ``` +- **Real-daemon (catches embed/build/auth drift):** + ```bash + make build + ./itervox -workflow path/to/WORKFLOW.md # in another terminal + open "http://127.0.0.1:8090/?token=$ITERVOX_API_TOKEN" + ``` + +## Viewport matrix + +For each scenario, exercise: + +| Viewport | Width × Height | Why | +|---|---|---| +| Desktop | 1440 × 900 | primary target | +| Tablet | 1024 × 768 | layout breakpoint check | +| Mobile | 390 × 844 | iPhone 14 — drawer/responsive | + +## Per-route checklist + +Run for `/`, `/timeline`, `/logs`, `/agents`, `/automations`, `/settings`: + +- [ ] Page loads under 2s; no skeletons stuck > 500ms after data arrives. +- [ ] Primary action (Save, Submit, Refresh) is visible without scrolling. +- [ ] Empty state is intentional copy (not "undefined" or blank). +- [ ] Keyboard focus is not trapped — Tab reaches every interactive element. +- [ ] Esc closes any open dialog / slide / dropdown. +- [ ] No horizontal scroll at the smallest supported viewport. +- [ ] Mobile drawer opens, closes, doesn't push body content offscreen. +- [ ] Status / state is conveyed by text or shape, not color alone. +- [ ] No console errors after hydration (`Cmd+Opt+I` → Console tab). +- [ ] Network tab shows zero unexpected `/api/v1/*` failures. + +## Required scenarios + +Drive every named scenario shipped in the fixture library. Each is exported from `web/src/test/fixtures/scenarios.ts`: + +- `emptyScenario` — empty state across every route. +- `quickstartScenario` — memory-tracker default (most common case). +- `activeRunScenario` — running rows present, hero counts non-zero. +- `inputRequiredScenario` — input_required + pending_input_resume rows. +- `retryAndPausedScenario` — retry queue + paused issues + pausedWithPR. +- `configInvalidScenario` — `configInvalid` populated; banner visible. +- `timelineLogsScenario` — every history status + every log event variant. +- `settingsMatrixScenario` — multi-profile, automations, SSH hosts, dispatch strategy. +- `mobileShellScenario` — quickstart + long-title issue for mobile responsiveness. + +Use the route-mocked Playwright UI to switch scenarios without restarting the daemon. + +## Screenshots + +Capture per `(route, viewport, scenario)` triple. Filename pattern: + +``` +--.png +e.g. dashboard-desktop-quickstart.png + timeline-mobile-empty.png +``` + +Save under `docs/qa-reports/screenshots//`. + +## Severity rubric + +| Severity | Definition | +|---|---| +| **P0** | Blocks release. Crash, data loss, auth bypass, ARIA blocker. | +| **P1** | Must-fix before merge. Wrong copy on a primary CTA, infinite spinner, console error in happy path. | +| **P2** | Should-fix. Layout glitch, sub-optimal empty state, console warning. | +| **P3** | Nice-to-have. Minor copy polish, inconsistent spacing, focus ring on uncommon path. | +| **P4** | Subjective polish. Aesthetic preference, naming opinion, fully optional. | + +## Promotion rule + +If the **same qualitative finding** appears in **two consecutive QA runs**, promote it to an automated test before the third run. Track the promotion target file (e.g. `web/e2e/ui-current--smoke.spec.ts`) inline in the next report. + +## Report path + +Write the report to: + +``` +docs/qa-reports/YYYY-MM-DD-current-ui-qa-.md +``` + +## Report template + +Use this exact structure (inline so the skill stays self-contained — no external template files): + +```markdown +# Itervox UI QA — + +## Summary + +- **Commit:** `` +- **Branch:** `` +- **Lane:** route-mocked / real-daemon / both +- **Viewports:** desktop 1440×900, tablet 1024×768, mobile 390×844 +- **Scenarios driven:** +- **`make qa-current` baseline:** PASS / FAIL (sha:X / commit:Y) + +## Findings + +### P0 — release-blockers +*(none, or list — each finding: short title; affected route + viewport + scenario; reproduction; expected vs actual; issue link or "no GitHub yet — captured here")* + +### P1 — must-fix +*(same shape as P0)* + +### P2 — should-fix +*(same shape)* + +### P3 — nice-to-have + +### P4 — subjective polish + +## Repeated findings (promotion candidates) + +Findings that appeared in the previous report AND in this one. Each row must include the promotion target (test file or refactor target) before the next QA run. + +| Finding | First seen | Promotion target | +|---|---|---| + +## Screenshots + +Stored under `docs/qa-reports/screenshots//`. Naming: `--.png`. + +## Notes + +Any free-form observations, hypotheses, or open questions for the next iteration. +``` + +### Where findings go from here + +- **P0/P1** — file a GitHub issue immediately, link from the report row. +- **P2** — capture in the report; pick up in the next iteration. +- **P3/P4** — track in the report; revisit when the area is touched. + +## Done criteria + +- [ ] All 9 scenarios driven across 3 viewports = 27 checkpoints. +- [ ] One report file written at `docs/qa-reports/YYYY-MM-DD-current-ui-qa-.md`. +- [ ] Every P0/P1 finding has an issue link (or a one-line "no GitHub yet — captured here"). +- [ ] Two-run rule applied: list any qualitative finding that appeared in the prior report — if matched, document the promotion target. diff --git a/.claude/skills/go-toolchain-sync/SKILL.md b/.claude/skills/go-toolchain-sync/SKILL.md index e77a350..f868285 100644 --- a/.claude/skills/go-toolchain-sync/SKILL.md +++ b/.claude/skills/go-toolchain-sync/SKILL.md @@ -9,8 +9,8 @@ itervox pins the Go toolchain in **two** places. They MUST match byte-for-byte. | File | Line | Current | |---|---|---| -| `/Users/vladimirnovick/dev/oss/itervox/go.mod` | 3 | `go 1.25.9` | -| `/Users/vladimirnovick/dev/oss/itervox/Makefile` | 6 | `export GOTOOLCHAIN := go1.25.9` | +| `go.mod` | 3 | `go 1.25.9` | +| `Makefile` | 6 | `export GOTOOLCHAIN := go1.25.9` | CI is the single-source-of-truth pattern: `.github/workflows/ci-go.yml` and `release.yml` use `actions/setup-go@v6` with `go-version-file: go.mod`. **Never** hardcode `go-version:` in workflow YAML. diff --git a/.claude/skills/orchestrator-invariants/SKILL.md b/.claude/skills/orchestrator-invariants/SKILL.md index 44b7d69..173a985 100644 --- a/.claude/skills/orchestrator-invariants/SKILL.md +++ b/.claude/skills/orchestrator-invariants/SKILL.md @@ -18,24 +18,33 @@ Itervox's orchestrator is a single-goroutine state machine. Violating these rule These `Orchestrator.cfg` fields can be mutated at runtime by HTTP handler goroutines and MUST be accessed under `cfgMu`: -- `cfg.Agent.AgentMode` - `cfg.Agent.MaxConcurrentAgents` +- `cfg.Agent.MaxRetries` +- `cfg.Agent.MaxSwitchesPerIssuePerWindow` +- `cfg.Agent.SwitchWindowHours` +- `cfg.Agent.SwitchRevertHours` +- `cfg.Agent.RateLimitErrorPatterns` - `cfg.Agent.Profiles` - `cfg.Agent.SSHHosts` +- `cfg.Agent.SSHHostDescriptions` - `cfg.Agent.DispatchStrategy` +- `cfg.Agent.ReviewerProfile` +- `cfg.Agent.AutoReview` - `cfg.Agent.InlineInput` - `cfg.Tracker.ActiveStates` - `cfg.Tracker.TerminalStates` - `cfg.Tracker.CompletionState` +- `cfg.Tracker.FailedState` - `cfg.Workspace.AutoClearWorkspace` +- `cfg.Automations` -All OTHER `cfg.*` fields are read-only after startup. Do NOT add locks defensively to read-only fields. If a static-analysis pass flags one, verify there is a runtime setter (HTTP handler) before believing the claim — no setter means no race. +The canonical allowlist is `internal/orchestrator/cfg_mu_audit_test.go::AllowedMutableCfgFields`. All OTHER `cfg.*` fields are read-only after startup. Do NOT add locks defensively to read-only fields. If a static-analysis pass flags one, verify there is a runtime setter (HTTP handler) before believing the claim — no setter means no race. ## 3. Adding a new runtime-mutable `cfg.*` field If you make a `cfg.*` field mutable at runtime, you MUST do all three: -1. Add it to the cfgMu guard list in `/Users/vladimirnovick/dev/oss/itervox/CLAUDE.md`. +1. Add it to the cfgMu guard list in `CLAUDE.md`. 2. Ensure the HTTP handler that mutates it acquires `o.cfgMu.Lock()`. 3. Ensure every read of it acquires `o.cfgMu.RLock()`. diff --git a/.claude/skills/verify-before-done/SKILL.md b/.claude/skills/verify-before-done/SKILL.md index f8a8efa..f58b8c6 100644 --- a/.claude/skills/verify-before-done/SKILL.md +++ b/.claude/skills/verify-before-done/SKILL.md @@ -15,22 +15,22 @@ Run from the repo root: make verify ``` -This runs, in order: `fmt` -> `vet` -> `lint-go` (golangci-lint) -> `test` (`go test -race ./... -count=1`) -> `web-test` (`pnpm install --frozen-lockfile && pnpm test` in `web/`) -> `web-spelling` (guards against the old "Symphony" name in user-visible strings). +This runs, in order: `fmt` -> `vet` -> `lint-go` (golangci-lint) -> `test` (`go test -race ./cmd/... ./internal/... -count=1`) -> `web-coverage` (`pnpm test:coverage` in `web/`) -> `web-build` -> `web-spelling` (guards against the old "Symphony" name in user-visible strings) -> `size-budget` -> `no-os-exit`. `go test ./...` alone is NOT sufficient. It misses: - golangci-lint failures (CI lint job will fail) - race conditions hidden by the test cache (no `-count=1`) -- frontend regressions (`pnpm test`) +- frontend regressions and coverage drops (`pnpm test:coverage`) - the spelling guard `make verify` must exit 0. Show the tail of the output as evidence. -## 2. Frontend coverage gate (when touching `web/`) +## 2. Frontend coverage gate -`pnpm test` does NOT enforce coverage. Only `pnpm test:coverage` does, with a 70% threshold on **statements, branches, functions, and lines** (all four). +`make verify` now runs `pnpm test:coverage`, with a 70% threshold on **statements, branches, functions, and lines** (all four). For faster frontend-only iteration, run the target directly: ```bash -cd web && pnpm test:coverage +make web-coverage ``` Read the summary. Confirm every one of the four axes is at or above 70%. A green `pnpm test` says nothing about whether the coverage gate will pass in CI. New files showing 0% mean you forgot the test - write it before claiming done. @@ -50,7 +50,7 @@ If a race appears intermittently, that is a real race, not flakiness. Rerun with If you touched `go.mod`, `go.sum`, or any Go dependency, run: ```bash -govulncheck -tags dev ./... +govulncheck -tags dev ./cmd/... ./internal/... ``` The `-tags dev` flag matches what the CI `govulncheck` job in `.github/workflows/ci-go.yml` uses. CI will fail the PR on new vulnerabilities. @@ -59,11 +59,10 @@ The `-tags dev` flag matches what the CI `govulncheck` job in `.github/workflows Run sequentially. Do not skip steps. -a. `make verify` - exits 0, output captured -b. `cd web && pnpm test:coverage` - if you touched frontend code; all four axes >= 70% -c. `govulncheck -tags dev ./...` - if you touched `go.mod` or Go deps -d. `git status` - review staged files; watch for `.env`, credentials, accidental binaries -e. Only THEN `git commit` +a. `make verify` - exits 0, output captured, including frontend coverage +b. `govulncheck -tags dev ./cmd/... ./internal/...` - if you touched `go.mod` or Go deps +c. `git status` - review staged files; watch for `.env`, credentials, accidental binaries +d. Only THEN `git commit` ## 6. Never claim "tests pass" without evidence @@ -80,6 +79,6 @@ Show the exact command and a trimmed tail of its output. Claiming green based on You may claim "done" only when: - `make verify` exited 0 in the current session, output shown -- (if frontend touched) `pnpm test:coverage` shows all four axes >= 70%, output shown -- (if Go deps touched) `govulncheck -tags dev ./...` exited 0, output shown +- frontend coverage in `make verify` shows all four axes >= 70%, output shown +- (if Go deps touched) `govulncheck -tags dev ./cmd/... ./internal/...` exited 0, output shown - `git status` reviewed, no secrets staged diff --git a/.codex/config.toml b/.codex/config.toml new file mode 100644 index 0000000..44f6778 --- /dev/null +++ b/.codex/config.toml @@ -0,0 +1,2 @@ +[features] +goals = true \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index 7e68f8f..0000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,63 +0,0 @@ -version: 2 - -updates: - - package-ecosystem: "gomod" - directory: "/" - schedule: - interval: "weekly" - day: "monday" - time: "09:00" - timezone: "UTC" - open-pull-requests-limit: 5 - labels: - - "dependencies" - - "go" - commit-message: - prefix: "chore(deps)" - groups: - golang-x-tools: - patterns: - - "golang.org/x/*" - - - package-ecosystem: "npm" - directory: "/web" - schedule: - interval: "weekly" - day: "monday" - time: "09:00" - timezone: "UTC" - open-pull-requests-limit: 5 - labels: - - "dependencies" - - "frontend" - commit-message: - prefix: "chore(deps)" - groups: - react-ecosystem: - patterns: - - "react" - - "react-dom" - - "react-*" - - "@types/react*" - dev-tooling: - patterns: - - "vite" - - "vitest" - - "@vitejs/*" - - "typescript*" - - "eslint*" - - "@eslint/*" - - "prettier*" - - "tailwindcss*" - - "@tailwindcss/*" - - - package-ecosystem: "github-actions" - directory: "/" - schedule: - interval: "weekly" - day: "monday" - labels: - - "dependencies" - - "github-actions" - commit-message: - prefix: "chore(deps)" diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 2c0fad9..6e3b8af 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -16,12 +16,13 @@ ## Checklist -- [ ] `go build ./...` passes -- [ ] `go test -race ./...` passes (or `make test` if touching Go) -- [ ] `cd web && pnpm test` passes (or `make web-test` if touching frontend) -- [ ] `golangci-lint run ./...` passes (if touching Go) -- [ ] `cd web && pnpm lint` passes (if touching frontend) +- [ ] `make verify` passes locally - [ ] New behaviour is covered by tests +- [ ] Frontend coverage gate remains green (`make web-coverage` or `make verify`) +- [ ] Go/package changes use the repo-owned package scope (`./cmd/... ./internal/...`) +- [ ] Go dependency or toolchain changes ran `govulncheck -tags dev ./cmd/... ./internal/...` +- [ ] Large-file changes stay within `make size-budget` +- [ ] No new direct `os.Exit()` outside `cmd/itervox/exit.go` - [ ] No API tokens, secrets, or credentials in the diff - [ ] Exported Go symbols have doc comments diff --git a/.github/workflows/ci-go.yml b/.github/workflows/ci-go.yml index 53f24a0..d78b27e 100644 --- a/.github/workflows/ci-go.yml +++ b/.github/workflows/ci-go.yml @@ -6,6 +6,10 @@ on: pull_request: branches: [main] +env: + GOLANGCI_LINT_VERSION: v2.12.2 + GOVULNCHECK_VERSION: v1.1.4 + jobs: test: name: Test @@ -34,10 +38,10 @@ jobs: run: pnpm --dir web install --frozen-lockfile && pnpm --dir web run build - name: Build - run: go build ./... + run: go build ./cmd/... ./internal/... - name: Test (with race detector and coverage) - run: go test -race -count=1 -coverprofile=coverage.out -covermode=atomic ./... + run: go test -race -count=1 -coverprofile=coverage.out -covermode=atomic ./cmd/... ./internal/... - name: Show coverage summary run: go tool cover -func=coverage.out | tail -1 @@ -49,7 +53,7 @@ jobs: awk -v pct="$pct" 'BEGIN { if (pct+0 < 50) { print "Coverage " pct "% is below the 50% gate"; exit 1 } }' - name: Vet - run: go vet ./... + run: go vet ./cmd/... ./internal/... lint: @@ -67,8 +71,8 @@ jobs: - name: golangci-lint uses: golangci/golangci-lint-action@v9 with: - version: latest - args: --timeout=5m --build-tags dev + version: ${{ env.GOLANGCI_LINT_VERSION }} + args: --timeout=5m --build-tags dev ./cmd/... ./internal/... govulncheck: name: Security (govulncheck) @@ -83,7 +87,7 @@ jobs: cache: true - name: Install govulncheck - run: go install golang.org/x/vuln/cmd/govulncheck@latest + run: go install golang.org/x/vuln/cmd/govulncheck@${GOVULNCHECK_VERSION} - name: Run govulncheck - run: govulncheck -tags dev ./... + run: govulncheck -tags dev ./cmd/... ./internal/... diff --git a/.github/workflows/deploy-site.yml b/.github/workflows/deploy-site.yml index b918c40..9a093fd 100644 --- a/.github/workflows/deploy-site.yml +++ b/.github/workflows/deploy-site.yml @@ -14,17 +14,19 @@ jobs: steps: - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v5 + - uses: actions/setup-node@v4 with: node-version: 22 - cache: npm - cache-dependency-path: site/package-lock.json + cache: pnpm + cache-dependency-path: site/pnpm-lock.yaml - name: Install dependencies - run: cd site && npm ci + run: cd site && pnpm install --frozen-lockfile - name: Build - run: cd site && npm run build + run: cd site && pnpm build - name: Deploy to Cloudflare Pages if: github.event_name == 'push' && github.ref == 'refs/heads/main' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bbc3f6a..a850375 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,13 +5,70 @@ on: tags: - 'v*.*.*' +env: + GOLANGCI_LINT_VERSION: v2.12.2 + GOVULNCHECK_VERSION: v1.1.4 + GORELEASER_VERSION: v2.14.3 + permissions: contents: write jobs: + preflight: + name: Release Preflight + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Set up Go + uses: actions/setup-go@v6 + with: + go-version-file: go.mod + cache: true + + - name: Set up pnpm + uses: pnpm/action-setup@v5 + with: + version: 10 + + - name: Set up Node.js + uses: actions/setup-node@v6 + with: + node-version: '20' + cache: 'pnpm' + cache-dependency-path: web/pnpm-lock.yaml + + - name: Install Go verification tools + run: | + go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@${GOLANGCI_LINT_VERSION} + go install golang.org/x/vuln/cmd/govulncheck@${GOVULNCHECK_VERSION} + + - name: Verify + run: make verify + + - name: Govulncheck + run: govulncheck -tags dev ./cmd/... ./internal/... + + - name: GoReleaser check + uses: goreleaser/goreleaser-action@v7 + with: + distribution: goreleaser + version: ${{ env.GORELEASER_VERSION }} + args: check + + - name: Verify GoReleaser hooks are clean + run: | + go mod tidy + pnpm --dir web install --frozen-lockfile + pnpm --dir web run build + git diff --exit-code + release: name: GoReleaser runs-on: ubuntu-latest + needs: preflight steps: - uses: actions/checkout@v6 with: @@ -39,7 +96,7 @@ jobs: uses: goreleaser/goreleaser-action@v7 with: distribution: goreleaser - version: latest + version: ${{ env.GORELEASER_VERSION }} args: release --clean env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index b9669ba..1aee1db 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,8 @@ web/node_modules/ web/dist/ web/coverage/ +web/test-results/ +web/playwright-report/ internal/server/web/dist/* !internal/server/web/dist/.gitkeep @@ -31,3 +33,6 @@ coverage.html site/node_modules/ site/dist/ site/.astro/ +marketing +# Release-audit working docs (local only) +v0.2.0/ diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 47bd56f..608d23b 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -49,7 +49,7 @@ brews: name: homebrew-tap token: "{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}" homepage: "https://github.com/vnovick/itervox" - description: "Autonomous AI agent daemon that drives Linear issues through Claude-powered agentic loops." + description: "Autonomous AI agent daemon that drives Linear issues through Claude/Codex-powered agentic loops." license: "Apache-2.0" install: | bin.install "itervox" diff --git a/AGENTS.md b/AGENTS.md index 6eba5f8..93e41e8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,24 +1,29 @@ # AGENTS.md — itervox - -> This file provides context for AI coding agents (Codex, Claude Code, Cursor, Gemini CLI, OpenCode, etc.) working on this repo. -> For human contributor docs see CONTRIBUTING.md. - -## Project overview - -Itervox is a Go 1.25.9 daemon that polls Linear/GitHub Issues, spawns Claude Code or -Codex subagents per issue, and serves a React web dashboard + Bubbletea TUI. -Config is a single `WORKFLOW.md` file (YAML front matter + Liquid template). +@CLAUDE.md + +> **Read `CLAUDE.md` first. It is the canonical agent guide for this repo.** +> +> CLAUDE.md contains: project overview, build/test commands, architecture +> invariants (single-goroutine orchestrator, `cfgMu` allowlist, queue + +> dependency audit ownership, Track B file-backed profiles / schema 2 / +> HEARTBEAT.md / handoff pipeline), package dependency order, frontend +> architecture, the `Toast API` contract, the gap-analysis false-positive +> patterns, Go and TypeScript conventions, the `Never do` list, and — most +> importantly — the MANDATORY "Verification before completion" rule. +> +> All of that applies to codex (and any other agent) identically. This file +> contains ONLY the things that are codex-specific or that codex needs in +> addition to CLAUDE.md. ## Before making any change -1. **Read CLAUDE.md** — it contains architecture invariants, false-positive patterns for - static analysis, and conventions that override defaults. -2. **Read the matching rule bundle under `.claude/skills//SKILL.md`** for the area you - are editing (see the table below). These bundles are plain markdown and tool-agnostic — - the directory name is historical. They are not optional reading. -3. **Run tests** to establish a baseline: `go test -race ./...` and `cd web && pnpm test`. -4. **Check the gap doc** (`planning/gaps_300326.md`) for known open items before adding new - ones — it may already be tracked. +1. **Read `CLAUDE.md` end-to-end.** No exceptions. The rules there are not a + superset of what's here — they are the rules. This file does not repeat + them. +2. **Read the matching rule bundle from the table below** for the area you are + editing. The bundles live under `.claude/skills//SKILL.md` — the + directory name is historical; they are plain markdown that codex can read + like any other doc, not Claude-specific code. ## Rule bundles (read the matching one before editing) @@ -34,144 +39,47 @@ Config is a single `WORKFLOW.md` file (YAML front matter + Liquid template). | `go.mod`, `Makefile`, Go toolchain bumps, or govulncheck stdlib findings | `.claude/skills/go-toolchain-sync/SKILL.md` | | Before claiming complete, before committing, before opening a PR | `.claude/skills/verify-before-done/SKILL.md` | -Each bundle is a focused checklist of enforced rules and verification steps for its area. -Reading the bundle before editing prevents the entire class of bugs it was written to catch. - -## Commands (developer-facing workflows) - -| Command | Use it for | -|---|---| -| `/interview` (`.claude/commands/interview.md`) | Start of a feature or refactor with unclear scope — 8 structured questions that surface design intent and verification criteria before any code | -| `/brainstorm` (`.claude/commands/brainstorm.md`) | Design decision with multiple reasonable approaches — spawns 3 subagents with forced orthogonal positions (Minimalist, Architect, Pragmatist), produces a tradeoffs table and decision document | - -## Build commands - -```bash -# Go -go build ./... -go test -race ./... -go vet ./... -golangci-lint run ./... - -# Frontend -cd web -pnpm install --frozen-lockfile -pnpm test # vitest -pnpm build # production bundle -pnpm exec tsc --noEmit -p tsconfig.app.json # type-check only - -# Combined -make verify # fmt + vet + lint + go tests + web tests -make build # web build → go binary -``` - -## Repository layout - -``` -cmd/itervox/ CLI entry — wires all packages; main.go + main_test.go -internal/ - agent/ Claude/Codex subprocess runners (stream-json + JSONL protocols) - app/ Business logic (EnrichIssue) - config/ Typed config, defaults, $VAR resolution, validation - domain/ Shared types: Issue, BlockerRef, BufLogEntry - logbuffer/ Ring buffer for per-issue log streaming - orchestrator/ Single-goroutine state machine (split into multiple files) - orchestrator.go Struct, New, Load, config setters/getters - event_loop.go Main select loop (Run), tick handling - worker.go Per-issue worker goroutine lifecycle - snapshot.go Snapshot construction and overlay - dispatch.go Eligibility checks, slot calculation - reconcile.go Stall/state reconciliation helpers - retry.go Retry queue scheduling - reviewer.go AI review dispatch - issue_control.go Cancel/resume/discard/reanalyze actions - ssh_host.go SSH host selection (least-loaded) - logging.go Structured log formatting (BufLogEntry) - state.go OrchestratorEvent types and RunEntry - prdetector/ PR URL detection via `gh pr list` - prompt/ Liquid template rendering - server/ HTTP API (chi router) — REST + SSE - statusui/ Bubbletea TUI model and golden-file tests - templates/ WORKFLOW.md scaffolding templates (Linear, GitHub) - tracker/ Tracker interface + Linear GraphQL + GitHub REST adapters - workflow/ WORKFLOW.md parser and file watcher - workspace/ Per-issue worktree lifecycle (directory + git worktree modes) -web/ React 19 / Vite frontend -testdata/ WORKFLOW.md fixtures -planning/ Gap analysis, design docs, roadmap -``` - -## Architecture constraints - -### Orchestrator event loop — single goroutine - -The orchestrator `Run()` loop is the ONLY place that mutates `State`. Workers -communicate via `o.events chan OrchestratorEvent`. Never write to state from a -worker goroutine — send an event instead. - -### cfgMu scope - -`cfgMu` protects only these `cfg` fields (mutable at runtime via HTTP): -- `cfg.Agent.AgentMode`, `cfg.Agent.MaxConcurrentAgents`, `cfg.Agent.Profiles` -- `cfg.Agent.SSHHosts`, `cfg.Agent.DispatchStrategy` -- `cfg.Tracker.ActiveStates`, `cfg.Tracker.TerminalStates`, `cfg.Tracker.CompletionState` -- `cfg.Workspace.AutoClearWorkspace` - -All other `cfg` fields are **read-only after startup** — no lock needed. - -### Config value validation - -`positiveIntField` in `config.go` rejects zero and negative values, replacing them -with defaults. Timeout fields (`TurnTimeoutMs`, `ReadTimeoutMs`, etc.) can never be -0 at runtime — do not flag `context.WithTimeout(ctx, 0)` as reachable. - -### Package import order (no circular deps) - -``` -domain ─┬── tracker, prompt, logbuffer, prdetector - │ -workflow ── config ── workspace - │ -agent (imports domain, config) - │ -orchestrator (imports agent, config, domain, logbuffer, prdetector, - prompt, tracker, workspace) - │ -app (imports domain, tracker) ── server (imports domain, config) - │ -cmd/itervox (wires everything) -``` - -## Testing conventions - -- Always run `go test -race` — the race detector catches real bugs here -- TUI tests use `charmbracelet/x/exp/teatest` (`model_teatest_test.go`) + catwalk - golden files. Regenerate golden files with `make tui-golden` after intentional - render changes. -- Integration tests (real API calls) are gated behind a build tag — not run by default. -- Frontend tests use Vitest + Testing Library. - -## Common pitfalls - -- **Toast API**: `addToast(message: string, variant?)` — first arg is a string. - Passing an object silently renders `[object Object]`. -- **Settings mutations** must call `refreshSnapshot()`, NOT `patchSnapshot()`. -- **SSE hooks**: always use `useToastStore.getState()` / `useItervoxStore.getState()` - inside effects — never call hooks conditionally. -- **Map copy**: use `maps.Copy(dst, src)` not manual for-range loops. -- **Clamp pattern**: `max(1, min(n, 50))` not if-chains (Go 1.21+). - -## Open architectural items (from planning/gaps_300326.md) - -Key unresolved items: -- T-6: Codex session log identity — single file instead of per-subagent files -- T-7: Reviewer backend parity — does not honor backend hints like worker path does -- T-9: Extract `orchestratorAdapter` from main.go to `internal/app` -- T-10: Replace 5s sublog polling with SSE push -- T-11: DRY `ParseSessionLogs`/`ParseSessionLogsMulti` duplication - -See `planning/gaps_300326.md` for the full task list with priorities and phases. - -Before adding new items, spawn a verification agent to confirm the -issue is real (read full call chain, check for upstream validation, verify file -exists). See the "Gap analysis — avoiding false positives" section of CLAUDE.md. +Each bundle is a focused checklist of enforced rules and verification steps +for its area. Reading the bundle before editing prevents the entire class of +bugs it was written to catch. + +## Codex-specific notes + +- **Canonical-source precedence**: when CLAUDE.md and any rule in your + training/memory disagree, CLAUDE.md wins. When CLAUDE.md and a skill bundle + disagree on scope, the skill bundle wins for its area and CLAUDE.md wins + for everything else. +- **Verification rule applies identically**: the "Verification before + completion — MANDATORY" section in CLAUDE.md is not Claude-specific. Codex + must produce the same `Verified by:` annotations, refuse the same evasions, + and follow the same sampling rules. There are no codex carve-outs. +- **Tool-name equivalences**: CLAUDE.md references Claude Code tools (`Read`, + `Edit`, `Write`, `Bash`, `TaskCreate`, `Skill`, etc.). Use whichever tool + actually exists in your runtime — for codex this is typically `shell.exec` + for everything. The semantics are the same: read before edit, prefer + surgical edits over rewrites, run tests with race detector, mark tasks as + completed only after `Verified by:` evidence. +- **Slash commands** (`/interview`, `/brainstorm`) are Claude Code-specific + and not available to codex. The underlying intent — interview before code + on vague specs, brainstorm before committing to an approach with multiple + valid options — applies regardless. Spawn subagents or run the equivalent + workflow manually if useful. + +Before adding new follow-up items, spawn a verification agent to confirm the +issue is real (read full call chain, check for upstream validation, verify +file exists). See "Gap analysis — avoiding false positives" in CLAUDE.md. + +## File-backed profile surfaces (read CLAUDE.md before editing) + +Five concrete surfaces define how agents and the daemon interact with on-disk +profile state. CLAUDE.md is the canonical reference for each; read the +matching section there before changing behaviour: + +- `itervox_schema_version` — schema-version marker required at the top of + every `WORKFLOW.md`. Mismatch is a hard startup failure. +- `SOUL.md` — compact per-profile identity file under `.itervox/agents//`. +- `INSTRUCTIONS.md` — full per-profile operating rules, same directory. +- `HEARTBEAT.md` — daemon liveness file written under `.itervox/`. Transient + runtime state; never committed. +- `init --update` — the migration subcommand that moves v0.1.x inline profile + prompts into the file-backed layout and stamps the schema marker. diff --git a/CHANGELOG.md b/CHANGELOG.md index 92da813..dc817c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,285 +5,438 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). --- -## [v0.0.3] — unreleased +## [0.2.0] — 2026-07-06 + +### Migration from v0.1.x + +> **Schema 2 is mandatory.** `WORKFLOW.md` files now require an `itervox_schema_version: 2` marker at the top, and inline `agent.profiles..prompt` is no longer accepted. On startup the daemon hard-fails with a `MissingWorkflowSchemaMessage` pointer when either condition is unmet. To migrate an existing v0.1.x project: +> +> ```bash +> itervox init --update --workflow WORKFLOW.md +> ``` +> +> The migrator writes a `WORKFLOW.md.bak` next to your workflow, extracts each profile's inline `prompt:` block into `.itervox/agents//INSTRUCTIONS.md`, generates a compact `.itervox/agents//SOUL.md` identity file, and ensures the git policy via a nested `.itervox/.gitignore` (always written; also self-healed on every daemon startup) plus root-`.gitignore` carve-outs when a root `.gitignore` broadly ignores `.itervox/` — so `.itervox/agents/**` and `.itervox/handoff/**` can be committed while runtime state stays ignored. Review the migrated `WORKFLOW.md` and the new agent files, then delete `WORKFLOW.md.bak`. See [Agent Profiles guide](https://itervox.dev/guides/agent-profiles/) for the file-backed profile reference. +> +> **If you set `workspace.auto_clear: true` on a v0.1.x project,** the migrator will print a one-line warning to remind you that v0.2.0 changes the semantics from "clear after every successful run" to "clear only when the issue reaches a terminal tracker state." For features that succeed on first attempt, behavior is unchanged. See the **Changed (breaking)** section below for the full rationale. + +### Removed (breaking) + +- **Inline `agent.profiles..prompt` is rejected by schema 2.** Each profile must now reference SOUL/INSTRUCTIONS files via `soul_file` and `instructions_file` (typically `.itervox/agents//SOUL.md` and `INSTRUCTIONS.md`). Use `itervox init --update --workflow WORKFLOW.md` to migrate; see the Migration section above. +- **`agent.agent_mode`** is gone. The previous values were `""` (solo), `"subagents"`, `"teams"`. Behavior reasoning: + - `""` and `"subagents"` were aliases at runtime — the daemon never actually gated subagent dispatch. + - `"teams"` injected a "your peer agents are X, Y, Z" roster into worker context; that injection now happens **unconditionally** when more than one profile exists. + - Profile content (now `INSTRUCTIONS.md`) always injects when a profile is selected. Previously the legacy inline `prompt:` was suppressed unless `agent_mode` was non-empty — silent suppression that operators rarely intended. + + **Migration:** delete the `agent.agent_mode` field from your `WORKFLOW.md`. The daemon now hard-fails at startup with a clear pointer to this entry if the field is present (typo guard). The legacy alias `agent.enable_agent_teams` is similarly rejected. +- The **`POST /api/v1/settings/agent-mode`** HTTP endpoint and the **"Agent Runtime"** Settings card are removed. The `inline_input` toggle (which used to live in that card) now lives in Settings → General and still persists to `WORKFLOW.md::agent.inline_input`. +- The TUI status row no longer displays a `◈ SUB-AGENTS` / `◈ TEAMS` badge. + +### Changed (breaking) + +- **`workspace.auto_clear: true` semantics changed.** Previously the workspace was removed after **every** successful worker run (mid-pipeline state transitions included). It now clears **only when the issue reaches a terminal tracker state** — either `tracker.completion_state` after a successful run, or `tracker.failed_state` after retries are exhausted. The field name and type are unchanged. + + **Why:** the per-success clear pattern broke the new file-backed agent handoff convention (see Added), which expects `.itervox/handoff/` files to accumulate across multiple workers running on the same branch for one issue. The new semantics preserve the workspace across retries and pipeline mid-states and only clean up once the issue is definitively done. + + **Who is affected:** users relying on per-run disk-space hygiene for flaky issues that retry many times will see workspaces persist across retries until the issue reaches `failed_state`. For features that succeed on first try, behavior is unchanged. `itervox init --update --workflow WORKFLOW.md` now prints a one-line notice when it finds `workspace.auto_clear: true` so the semantic shift is not silent. + + **Workaround for old behavior:** none in-tree. Run an external cleanup (e.g. `find -mtime +1 -delete`) outside the daemon if eager clearing is required. + +- **`workspace.auto_clear` + `agent.auto_review` no longer conflict.** Under the legacy per-success clear semantics the two settings raced — the clear removed the workspace before the reviewer could read it. Validation rejected the combination at config-load and at runtime PUT/PATCH. With the new terminal-state-only semantics the clear is deferred until after the reviewer also completes, so the two now safely coexist. `ValidateAutoClearAutoReview` is retained as a no-op for callers that branch on `ErrAutoClearAutoReviewConflict`; new code should not call it. Operators who relied on the validation error to catch accidental over-configuration should note that both flags will now silently be accepted. ### Added -#### Codex (OpenAI CLI) backend - -| File | Change | -|------|--------| -| `internal/agent/codex.go` *(new)* | `CodexRunner.RunTurn` — spawns `codex` CLI, pipes stdout, delegates to `readLines` with `ParseCodexLine` | -| `internal/agent/codex_events.go` *(new)* | `ParseCodexLine` — parses Codex JSONL stream: `thread.started`, `item.started` (command_execution, collab_tool_call), `item.completed`, `turn.completed`, `turn.failed` | -| `internal/agent/multi.go` *(new)* | `MultiRunner` — selects Claude or Codex runner based on the active agent profile's `command` field | -| `internal/agent/events.go` | `StreamEvent.InProgress bool` — set `true` for `item.started` events to distinguish in-flight from completed tool calls | - -#### Observability: action_started and action_detail log lines - -| File | Change | -|------|--------| -| `internal/agent/claude.go` | `readLines`: emits `INFO : action_started … tool=… description=…` when `ev.InProgress`; emits `INFO : action_detail … tool=shell status=… exit_code=… output_size=…` for completed shell calls via new `logShellDetail()` | -| `internal/agent/claude.go` | `toolDescription("shell")`: appends ` (exit:N)` to description when exit code is non-zero | -| `internal/server/server.go` | `IssueLogEntry` gains `Detail string \`json:"detail,omitempty"\`` and `Time string \`json:"time,omitempty"\`` | -| `internal/server/handlers.go` | `parseLogLine`: new cases for `action_started` (→ `event:"action"` with `…` suffix) and `action_detail` (→ `event:"action"` with `Detail` JSON); both handle `claude:` and `codex:` prefixes | -| `internal/server/handlers.go` | `buildDetailJSON(status, exitCode, outputSize string) string` *(new)* — builds `{"status":…,"exit_code":…,"output_size":…}` omitting empty fields, using a typed struct for deterministic key order | -| `internal/statusui/model.go` | `colorLine`: `action_detail` case returns `""` (suppressed); `action_started` case renders gray `⧖ tool — desc…` | -| `web/src/types/itervox.ts` | `IssueLogEntry.time?: string`, `IssueLogEntry.detail?: string` | - -#### Codex parity in TUI and web API - -| File | Change | -|------|--------| -| `internal/statusui/model.go` | `colorLine`: `codex: text/action/subagent/todo/action_started` handled identically to `claude:` equivalents | -| `internal/statusui/model.go` | `buildToolStats`: `\|\| strings.HasPrefix(line, "INFO codex: action")` added; explicit early `action_detail` skip before generic `action` match | -| `internal/statusui/model.go` | `buildToolCalls`: same extensions as `buildToolStats` | -| `internal/server/handlers.go` | `parseLogLine`: `codex: text/subagent/action/todo` cases added mirroring `claude:` | -| `internal/server/handlers.go` | `skipLine`: `INFO codex: session started` and `INFO codex: turn done` added | - -#### Named agent profiles - -| File | Change | -|------|--------| -| `internal/config/config.go` | `AgentProfile{Command, Prompt, Backend}` struct; `Agent.Profiles map[string]AgentProfile` | -| `internal/orchestrator/orchestrator.go` | Profile lookup per issue; `MultiRunner` selected based on profile `Command`; profile prompt appended to rendered prompt | -| `internal/orchestrator/state.go` | `StateSnapshot.AvailableProfiles []string`, `ProfileDefs map[string]ProfileDef`, `AgentMode`, `ActiveStates`, `TerminalStates`, `CompletionState`, `BacklogStates` | -| `internal/server/handlers.go` | `/api/v1/settings` exposes `availableProfiles` and `profileDefs` | -| `internal/templates/workflow_github.md` | Profile section examples added | -| `internal/templates/workflow_linear.md` | Profile section examples added | -| `WORKFLOW.md` *(new)* | Root-level workflow template with profile definitions | -| `web/src/types/itervox.ts` | `ProfileDef` interface; `StateSnapshot.availableProfiles`, `profileDefs`, `agentMode`, `activeStates`, `terminalStates`, `completionState`, `backlogStates` | -| `web/src/pages/Settings/index.tsx` | Profile picker UI | -| `web/src/pages/Settings/profileCommands.ts` *(new)* | Per-profile agent command helpers | -| `web/src/hooks/useSettingsActions.ts` | Profile selection action | - -#### Frontend: running sessions table - -| File | Change | -|------|--------| -| `web/src/types/itervox.ts` | `RunningRow.backend string`, `HistoryRow.backend? string` | -| `web/src/components/itervox/RunningSessionsTable.tsx` | Backend column | -| `web/src/queries/issues.ts` | Backend field forwarded | - -#### Per-run log isolation (`AppSessionID` + `session_id` stamping) - -Each daemon invocation now receives a unique `AppSessionID` (a `crypto/rand`-derived hex string generated -at startup). Every completed run is tagged with the ID of the daemon that produced it, and every log entry -is tagged with the Claude Code session ID that produced it. This allows the Timeline page to show only the -subagents that belong to a specific run when you expand it — previously, expanding run #2 of an issue -would show subagents from all prior runs mixed together. - -| File | Change | -|------|--------| -| `cmd/itervox/main.go` | `newAppSessionID()` *(new)* — generates a 16-byte `crypto/rand` hex token at startup; stored as `appSessionID` and threaded through `buildSnapFunc` | -| `cmd/itervox/main.go` | `buildSnapFunc`: `HistoryRow.AppSessionID` set from `run.AppSessionID`; `StateSnapshot.CurrentAppSessionID` set from the live token | -| `internal/orchestrator/state.go` | `CompletedRun.AppSessionID string` *(new)* — daemon-invocation grouping key; empty for legacy entries | -| `internal/orchestrator/orchestrator.go` | `Orchestrator.appSessionID string` field and `SetAppSessionID(id string)` method *(new)* — allows `main.go` to inject the token after construction; stamped onto `CompletedRun` at worker exit | -| `internal/orchestrator/logging.go` | `formatBufLine` `switch key`: new `case "session_id"` maps slog key-value to `BufLogEntry.SessionID` — previously the session ID was silently dropped | -| `internal/domain/types.go` | `BufLogEntry.SessionID string` `json:"session_id,omitempty"` *(new)*; `IssueLogEntry.SessionID string` `json:"sessionId,omitempty"` *(new)* | -| `internal/server/handlers.go` | `parseLogLine`: copies `e.SessionID` → `entry.SessionID` | -| `internal/server/server.go` | `HistoryRow.AppSessionID string` `json:"appSessionId,omitempty"` *(new)*; `StateSnapshot.CurrentAppSessionID string` `json:"currentAppSessionId,omitempty"` *(new)* | -| `web/src/types/schemas.ts` | `IssueLogEntrySchema.sessionId z.string().optional()`; `StateSnapshotSchema.currentAppSessionId z.string().optional()` | -| `web/src/pages/Timeline/index.tsx` | `NormalisedSession.sessionId?: string` threaded through `fromRunning`/`fromHistory`; `extractSubagents` accepts `filterSessionId?: string` — filters log entries to the run's session before parsing, so each expanded run shows only its own subagents; daemon session badge in header | - -#### `.env` file support - -| File | Change | -|------|--------| -| `cmd/itervox/main.go` | `loadDotEnv()` *(new)* — loads `.itervox/.env` or `.env` from CWD at startup via `github.com/joho/godotenv`; existing env vars are never overwritten; runs before `config.Load` so env vars are available for config resolution | -| `.env.example` *(new)* | Documents all required env vars with format hints (`LINEAR_API_KEY`, `GITHUB_TOKEN`, `SSH_KEY_PATH`) | - -#### Single-issue fast-path fetch (`FetchIssueByIdentifier`) - -| File | Change | -|------|--------| -| `internal/tracker/tracker.go` | `Tracker` interface gains `FetchIssueByIdentifier(ctx, identifier) (*Issue, error)` method | -| `internal/tracker/linear/client.go` | Implements `FetchIssueByIdentifier` for Linear | -| `internal/tracker/github/client.go` | Implements `FetchIssueByIdentifier` for GitHub | -| `internal/tracker/memory.go` | Implements `FetchIssueByIdentifier` for in-memory tracker | -| `internal/server/server.go` | New `FetchIssue` callback on `server.Config`; `handleIssueDetail` uses fast path via `FetchIssue` with fallback to `fetchIssues` scan | - -#### `itervox init --runner` flag - -| File | Change | -|------|--------| -| `cmd/itervox/main.go` | `runInit`: new `--runner claude\|codex` flag (default: `claude`); `codex` emits `command: codex` + `backend: codex` in the generated WORKFLOW.md; runner is validated before file write | -| `cmd/itervox/main.go` | `generateWorkflow`: accepts `runner` parameter and emits the appropriate `agent:` block | -| `cmd/itervox/main.go` | `configuredBackend(command, explicit string)` *(new)* — resolves final backend string from agent command + explicit override | - -#### Per-project log directory - -| File | Change | -|------|--------| -| `cmd/itervox/main.go` | `--logs-dir` default changed from `./log` to `~/.itervox/logs//`; new `defaultLogsDir(workflowPath string)` helper performs a lightweight early config read to derive the path; failures fall back to `~/.itervox/logs` | - -#### Auto-clear workspace - -| File | Change | -|------|--------| -| `internal/orchestrator/orchestrator.go` | `SetAutoClearWorkspaceCfg(enabled bool)` / `AutoClearWorkspaceCfg() bool` — toggle automatic workspace deletion after a task reaches completion state; safe to call from any goroutine (guards via `cfgMu`) | -| `internal/server/server.go` | `WorkspaceConfig.AutoClearWorkspace bool`; `setAutoClearWorkspace` callback + `SetAutoClearWorkspaceSetter` | -| `internal/server/handlers.go` | `POST /api/v1/settings/workspace/auto-clear` — persists the toggle back to WORKFLOW.md and notifies the orchestrator | -| `internal/workflow/loader.go` | `PatchWorkspaceBoolField(path, key string, enabled bool)` *(new)* — generic workspace-block bool patcher; backed by shared `patchBlockBoolField` with the existing `PatchAgentBoolField` | -| `web/src/pages/Settings/index.tsx` | Toggle switch "Auto-clear workspace on success" with description | -| `web/src/types/itervox.ts` (via `schemas.ts`) | `StateSnapshot.autoClearWorkspace?: boolean` | - -#### Agent queue view - -| File | Change | -|------|--------| -| `web/src/components/itervox/AgentQueueView.tsx` *(new)* | Drag-and-drop issue→agent-profile assignment board using `@dnd-kit/core`; columns per profile + "Unassigned"; dragging a card calls `onProfileChange` | -| `web/src/pages/Dashboard/index.tsx` | "◈ Agents" tab added to the board/list/agents toggle (visible when `availableProfiles.length > 0`); `AgentQueueView` rendered in agents tab | -| `web/src/pages/Dashboard/index.tsx` | Inline profile `