Skip to content

feat(agent): schedule tasks for the next available AI coworker#646

Open
rsnodgrass wants to merge 4 commits into
mainfrom
claude/agent-task-scheduling-UBbjr
Open

feat(agent): schedule tasks for the next available AI coworker#646
rsnodgrass wants to merge 4 commits into
mainfrom
claude/agent-task-scheduling-UBbjr

Conversation

@rsnodgrass
Copy link
Copy Markdown
Contributor

@rsnodgrass rsnodgrass commented Jun 5, 2026

What this ships

An internal task-scheduling mechanism: the daemon (and other internal producers) enqueue units of work that the next available AI coworker picks up and runs — ideally as a fresh-context subagent. It replaces two one-off hacks:

  • the .needs-doctor-agent file-drop, and
  • the daemon forking claude -p for anti-entropy, which bills against a separate account and loses the developer's warm session.

Now that work rides the developer's live session instead. This is not a beads replacement — agent tasks are ephemeral, machine-scheduled, local-only chores.

Design doc: docs/ai/specs/agent-task-scheduling.md.

flowchart LR
  subgraph producers [Producers]
    D[daemon anti-entropy] --> Q
    M[.needs-doctor-agent marker] --> Q
    C[ox agent tasks add / Go API] --> Q
  end
  Q[(.sageox/agent_tasks/<br/>agent_tasks.jsonl)]
  Q -- UserPromptSubmit hook<br/>surface on change --> A[live AI coworker]
  A -- tasks next --> A
  A -- dispatch --> S[fresh-context subagent]
  S -- tasks done --> Q
Loading

Storage & model

  • Shared, project-local queue: .sageox/agent_tasks/agent_tasks.jsonl (gitignored, ephemeral). Shared — not per-user — because the point is "next available agent."
  • flock-guarded JSONL, last-write-wins, reconcile-on-read (mirrors internal/agentinstance).
  • Task fields: priority (lower = higher), status (ready/in_progress/completed/canceled), expiry (optional; zero = never), target_agent, dedup_key, payload, and a lease (claimed_by_agent_id/pid/host/lease_expires_at/attempts).

Lifecycle & self-healing

flowchart LR
  ready -- claim/next --> in_progress
  in_progress -- done --> completed
  in_progress -- cancel --> canceled
  in_progress -- "lease expired OR claimer PID dead (same host)" --> ready
Loading
  • Claim atomically pops the top eligible ready task and stamps a lease.
  • On every read the store reclaims in_progress tasks whose lease expired or whose claiming PID is dead on this host (proc.IsAlive); cross-host claims rely on lease expiry only.
  • Expired tasks are dropped; terminal tasks prune after a 1h retention window.

Command surface

Command Purpose
ox agent <id> tasks list Active tasks; "ready" count reflects what this agent could claim
ox agent <id> tasks next Atomically claim the top ready task
ox agent <id> tasks done <id> [--result …] Mark completed
ox agent <id> tasks cancel <id> [--reason …] Mark canceled
ox agent <id> tasks extend <id> Extend the lease on long work
ox agent tasks add … (hidden) Producer enqueue (daemon/scripts/tests) + Go agenttask.Enqueue

There is intentionally no user-facing create command — tasks are scheduled by internal producers.

Conserving the user's tokens (the whole point)

  • Surface on change only. The UserPromptSubmit hook emits a <system-reminder> block only when the ready set changes (per-agent signature cursor in .sageox/cache/). An unchanged pending queue is never re-injected turn after turn; an idle queue costs zero context. (Replaced an earlier arbitrary 30-min re-nudge — it was exactly the repeated-identical-context cost we want to avoid.)
  • Subagent dispatch is the load-bearing instruction returned with every claim and surfacing block ("run each in a SUBAGENT with a fresh context"), keeping chores out of the main window.
  • 5-task inline cap; compact JSON.

Daemon producer (replaces claude -p anti-entropy)

On the daemon's doctor timer, independent of agent_worker.enabled:

  • Doctor bridge.needs-doctor-agent marker → deduped doctor task.
  • Session finalize — runs only when no local LLM worker is authed (!isEffectivelyEnabled). Asks the registered SessionFinalizeHandler to detect stale recordings and enqueues per-session session-finalize tasks (deduped, priority 30, capped 25/cycle). When a worker IS available, the normal queue forks it (delegated mode, ADR-016) and no task is produced — no duplicate work. When a worker is available but finalize is disabled, the user opted out, so no task either.

agentwork can't import internal/doctor (cycle daemon → agentwork → doctor → daemon), so the marker is checked via os.Stat.

Visibility

  • ox status and ox agent list render a one-line N ready, M in progress summary when the queue is non-empty (silent when empty); reading reconciles stale leases.
  • ox doctor gains an agent-tasks-stuck check: self-heals leases on read, and --fix cancels poison tasks (>5 attempts).
  • agent_tasks/ added to the .sageox/.gitignore canonical content + required-entries.

Test Plan

  • internal/agenttask — store lifecycle: add/list/claim/complete/cancel/extend, dedup, expiry, priority order, lease + dead-PID reclaim, terminal retention prune.
  • internal/daemon/agentwork — doctor-marker producer (+ dedup, no-project-root no-op); finalize producer enqueues when no worker, skips when worker enabled; finalizeTaskFields parsing.
  • cmd/oxtasks list/next/done/cancel + target filtering + text mode; surfacing throttle (emit-on-change, silent on unchanged, respects target_agent); countAgentTasks/renderAgentTasksSection; agent-tasks-stuck doctor check (healthy/poison/fix).
  • Build + go vet clean across all packages; gofmt clean.

Notes for review

  • New SageOx data-storage path (.sageox/agent_tasks/) and new agent data-access surface (ox agent tasks) — both in the "Ryan must review" list. Flagging explicitly; treated as satisfied since you scoped this.
  • Pre-existing failures in this sandbox (TestClaudeRunner_Run_SuccessfulInvocation, TestIsDaemonDisabled, agent-env/CI-detection tests) are driven by CLAUDE_CODE_REMOTE=true and fail on main too — not from this branch.

Possible follow-ups

  • Add the task count to ox status --json (text rendering covers the human ask today).
  • Broaden producers (e.g., other anti-entropy chores) onto the same queue.

Generated by Claude Code

Summary by CodeRabbit

  • New Features

    • Agent task scheduling system allowing agents to list, claim, and manage queued work
    • New ox agent tasks commands for listing, claiming, completing, and canceling tasks
    • Automatic task surfacing into agent prompts
    • Task visibility in ox status output
    • Producer interface for enqueuing tasks
  • Documentation

    • Added specification for agent task scheduling mechanism
  • Chores

    • Updated .gitignore to exclude agent task queue directory
    • Added doctor check for detecting stuck or exhausted tasks

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jun 5, 2026

PR changed again? Review this PR in Change Stack to compare snapshots and stay oriented.

Review Change Stack

Warning

Review limit reached

@rsnodgrass, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 25 minutes and 29 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: afa56761-3ade-4362-8dac-d91020e57d6f

📥 Commits

Reviewing files that changed from the base of the PR and between 09d0ae9 and fc70dd1.

📒 Files selected for processing (7)
  • cmd/ox/agent_session_pause.go
  • cmd/ox/agent_tasks.go
  • cmd/ox/agent_tasks_surface.go
  • cmd/ox/doctor_agent_tasks.go
  • cmd/ox/hooks_post_rewrite_test.go
  • cmd/ox/status.go
  • internal/agenttask/store.go
📝 Walkthrough

Walkthrough

This PR introduces a complete agent task scheduling system: a repo-local JSONL queue for distributing ephemeral AI work to agents. It includes task persistence with reconciliation and deduplication, agent-facing CLI commands (list/next/done/cancel/extend), throttled task surfacing via UserPromptSubmit hook, daemon-side producer logic for doctor and session-finalize tasks, health checks for stuck/poison tasks, and integration into agent and status displays.

Changes

Agent Task Scheduling

Layer / File(s) Summary
Task model and store persistence
internal/agenttask/task.go, internal/agenttask/store.go, internal/agenttask/store_test.go
Task schema defines statuses (ready/in_progress/completed/canceled), kinds (doctor/session-finalize/anti-entropy/custom), priority, targeting, and lease/claim metadata. Store manages JSONL ledger with advisory locking, reconciliation on read (expires non-in-progress tasks, prunes old terminal rows, reclaims stale leases on expiry or dead PID), dedup-key deduplication for active tasks, and active-cap eviction. Comprehensive test suite validates all lifecycle transitions and concurrent access patterns.
Agent CLI task operations
cmd/ox/agent_tasks.go, cmd/ox/agent_tasks_test.go
Agent-facing ox agent <id> tasks list/next/done/cancel/extend subcommands with text and JSON output modes. Hidden producer surface ox agent tasks add/list validates --title, enqueues via store, and renders task views with age formatting, kind display, and subagent dispatch guidance. Includes task ID parsing and JSON helpers. Tests cover queue listing, priority-based claiming, task completion/cancellation, target-agent filtering, and output formats.
Task surfacing and context injection
cmd/ox/agent_tasks_surface.go, cmd/ox/agent_hook.go
Throttled task injection into agent prompts using per-agent FNV-signature cursor to detect when ready-set changes and avoid repeated re-injection. Surfaces XML-escaped task reminders with untrusted-data warnings. Cursor reset on context wipe (/clear, /compact) so pending tasks resurface. Integration into handlePrompt (emits tasks after whispers) and handleStart/handleCompact (resets cursor for context freshness).
Agent list and status display
cmd/ox/agent.go, cmd/ox/status.go
Displays pending task counts (ready/in-progress) in ox agent list output after coworker listing and in ox status summary. Read-only store queries count tasks without creating ephemeral directories; silent when queue is absent or empty.
Daemon task production
internal/daemon/agentwork/task_producer.go, internal/daemon/agentwork/task_producer_test.go, internal/daemon/agentwork/manager.go, internal/daemon/daemon.go, plus manager and daemon test updates
Daemon enqueues doctor tasks when .needs-doctor-agent marker is present, and session-finalize tasks only when no local LLM worker is effectively enabled. Validates session directory names (safeName regex) to prevent unsafe interpolation. Extracts session names and missing-artifacts lists into task bodies with dedup-key tied to session scope. Manager calls produceAgentTasks() before detection on periodic/force ticks. Tests verify marker handling, dedup behavior, worker-enabled gating, and field derivation.
Doctor health check for poison tasks
cmd/ox/doctor_agent_tasks.go, cmd/ox/doctor_agent_tasks_test.go, cmd/ox/doctor_agent.go, cmd/ox/doctor.go, cmd/ox/doctor_types.go
Health check checkAgentTasksStuck detects non-terminal tasks with attempts ≥ 5 (poison tasks) that may indicate failure loops; returns warning when found and fix-mode cancels them. Reconciles task store on read (prunes stale, reclaims dead PIDs). Integrated into doctor check registry under "Agent tasks" slug with fix-level suggested. Tests verify no-queue skip, healthy-queue pass, and poison-task warning → fix behavior.
Configuration and specification
.sageox/.gitignore, cmd/ox/doctor_sageox.go, cmd/ox/init_gitignore_test.go, docs/ai/specs/agent-task-scheduling.md
Gitignore template and required-entries list updated to ignore ephemeral agent_tasks/ directory alongside existing agent_instances/ ignore. Test fixtures updated to match. Comprehensive specification document covers task schema, lifecycle, CLI/producer surfaces, surfacing throttle mechanism, daemon production pathways, doctor/status integration, threat model (XML-framed trust boundaries, XML escaping, fixed kind playbooks, size caps), and explicit non-goals (not distributed, not durable across clone, not a beads replacement).

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes


Possibly related PRs

  • sageox/ox#622: Overlaps on handlePrompt integration—this PR adds emitAgentTasks(...) task-context surfacing, while that PR adds query-gating logic pre-whisper.

A queue of tasks for our agents brave,
Persisted in JSON, locally saved,
With lease-time and dedup and doctor's keen eye,
For poison detection—we'll heal or we'll try! 🐰✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 53.76% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(agent): schedule tasks for the next available AI coworker' directly and clearly summarizes the main feature addition in this PR—implementing a task scheduling system for AI coworkers.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/agent-task-scheduling-UBbjr

Comment @coderabbitai help to get the list of available commands and usage tips.

… read-only

Address review findings: treat task content as untrusted data not instructions
(framing + closed kind vocabulary + XML-escaping + session-name sanitization),
never expire/evict in_progress tasks, rewrite-free read path on the prompt hook
and ox status, resolved agent type for target surfacing, busy-agent guard,
cursor reset on /clear and /compact, in-process mutex, and task size caps.

https://claude.ai/code/session_01B3Hhcw2zYFUTsQaifkvifk
@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Jun 5, 2026

Greptile Summary

This PR introduces a project-local agent task queue (internal/agenttask) that lets the daemon and other internal producers schedule ephemeral chores (doctor runs, session finalization, anti-entropy) for the next available live AI coworker, replacing the previous claude -p fork that billed against a separate account.

  • Storage & lifecycle: an flock-guarded JSONL file under .sageox/agent_tasks/ with last-write-wins reads, priority-based eviction, dedup keys, and lease/PID-based self-healing — the design closely mirrors internal/agentinstance.
  • Surfacing: the UserPromptSubmit hook injects a <system-reminder> block only when the ready-task set changes (FNV signature cursor), keeping idle-queue cost at zero; cursors are reset on /clear and /compact so tasks re-surface after context wipes.
  • Producers: the daemon's doctor timer enqueues doctor and session-finalize tasks (the finalize path fires only when no local LLM worker is authed, avoiding duplicate work); all four previously-identified review concerns — MkdirAll side-effects, stale ready-count from two lock passes, priority-inverted cap eviction, and non-atomic cursor writes — have been addressed in this revision.

Confidence Score: 5/5

Safe to merge; the queue, surfacing, and producer logic are all well-structured and the four issues flagged in earlier review rounds have been addressed.

The new agenttask package is thoroughly tested (store lifecycle, dedup, expiry, lease reclaim, cap eviction) and the architectural choices — flock + in-process mutex, atomic rewrite via temp-file rename, signature-based throttling, and priority-aware eviction — are sound. The only remaining finding is that task.ID is sliced to 8 characters without a length guard in three places; this would panic on a malformed JSONL task ID, but normal operation always produces 36-char UUIDs and the store filters only truly empty IDs on read. All four issues previously flagged by reviewers have been demonstrably fixed in this revision.

cmd/ox/agent_tasks.go and cmd/ox/doctor_agent_tasks.go — both slice task IDs without a length check.

Important Files Changed

Filename Overview
internal/agenttask/store.go Core JSONL task store with flock + in-process mutex, last-write-wins semantics, atomic rewrite via temp-file rename, lease reclaim, and priority-based cap eviction — all sound.
internal/agenttask/task.go Task model with closed kind vocabulary, size limits, lease and expiry helpers, and ClaimerDead PID check — well-structured.
cmd/ox/agent_tasks.go Agent-facing task commands; ready count correctly computed from the same snapshot to avoid stale-header issues. Slicing task IDs to 8 chars without a length guard can panic on short IDs.
cmd/ox/agent_tasks_surface.go Throttled surfacing via FNV-hashed ready-set signature; QueueExists guard prevents MkdirAll side-effects; atomic cursor write via temp-file rename; XML-escapes title/kind to prevent tag-breakout.
internal/daemon/agentwork/task_producer.go Doctor-marker and session-finalize producers; safeSessionName regex guards against filesystem-derived injection in task bodies; dedup keys prevent runaway enqueueing; finalize tasks suppressed when local worker is enabled.
cmd/ox/doctor_agent_tasks.go Poison-task doctor check; reconcile-on-read self-heals leases. Same t.ID[:8] unchecked slice as agent_tasks.go, but not gated by --text so any short ID panics ox doctor unconditionally.
cmd/ox/status.go Adds agent-task summary to ox status; QueueExists guard correctly prevents directory materialisation on status reads; uses ListView (no rewrite) on the hot path.
cmd/ox/agent_hook.go emitAgentTasks wired into handlePrompt; resetTaskCursor called on /clear and /compact so pending tasks re-surface after context wipe — correct lifecycle handling.
internal/daemon/agentwork/manager.go projectRoot plumbed through; produceAgentTasks called on doctorTicker and ForceDetect, both paths independent of agent_worker.enabled — matches design intent.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    subgraph Producers
        D[daemon doctor timer] -->|produceDoctorTask / produceFinalizeTasks| E[agenttask.Enqueue]
        M[.needs-doctor-agent marker] --> D
        CLI[ox agent tasks add] --> E
    end

    E -->|append JSONL row| Q[(".sageox/agent_tasks/agent_tasks.jsonl\nflock-guarded, last-write-wins")]

    subgraph Live AI coworker session
        H[UserPromptSubmit hook] -->|QueueExists guard| SIG{signature changed?}
        SIG -- yes --> REM[system-reminder injected into model context]
        SIG -- no --> SILENT[silent — no context cost]
        REM --> NEXT[ox agent id tasks next - atomic Claim + lease]
        NEXT --> SUB[fresh-context subagent]
        SUB --> DONE[ox agent id tasks done or cancel / extend]
        DONE -->|rewrite JSONL| Q
    end

    subgraph Self-healing
        Q -->|reconcile-on-read| RECLAIM[reclaim expired leases or dead-PID claimer]
        RECLAIM -->|status ready| Q
    end

    subgraph Visibility
        Q --> STATUS[ox status — N ready M in progress]
        Q --> DOCTOR[ox doctor — cancel poison tasks over 5 attempts]
    end
Loading

Reviews (2): Last reviewed commit: "fix(agent-tasks): address review nits (r..." | Re-trigger Greptile

Comment thread cmd/ox/agent_tasks_surface.go
Comment thread cmd/ox/agent_tasks.go Outdated
Comment thread internal/agenttask/store.go Outdated
Comment thread cmd/ox/agent_tasks_surface.go
…, eviction order, single-snapshot count)

- emitAgentTasks/status/doctor guard on QueueExists so a read never MkdirAll's the queue dir
- writeTaskCursor writes atomically (tmp+rename)
- enforceActiveCap evicts least-urgent (priority desc) before oldest
- runTasksList computes the ready count from one snapshot, not a second locked read
- add QueuePath/QueueExists helpers (no more hardcoded queue paths)

https://claude.ai/code/session_01B3Hhcw2zYFUTsQaifkvifk
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (4)
internal/agenttask/store.go (1)

445-448: ⚡ Quick win

Use errors.Is for not-exist checks.

Line 447 should use errors.Is(err, os.ErrNotExist) for consistency with project error-unwrapping rules.

Suggested diff
-	if os.IsNotExist(err) {
+	if errors.Is(err, os.ErrNotExist) {
 		return []*Task{}, nil
 	}

As per coding guidelines: "Use errors.Is()/errors.As() and wrap errors with context".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/agenttask/store.go` around lines 445 - 448, Replace the
os.IsNotExist(err) check with errors.Is(err, os.ErrNotExist) in the task file
open path (the code that uses s.tasksPath), and when returning or propagating
other errors from os.Open or subsequent IO, wrap them with context using
fmt.Errorf(...%w...) (e.g., fmt.Errorf("opening tasks file %s: %w", s.tasksPath,
err)) so error unwrapping and contextual logs follow project conventions.
cmd/ox/agent_hook.go (1)

531-533: ⚡ Quick win

resetTaskCursor safely no-ops on empty agentIDresetTaskCursor(projectRoot, agentID) in cmd/ox/agent_tasks_surface.go immediately returns when agentID == "" (or projectRoot == ""), so the unguarded call from handleCompact is safe; only a minor style/consistency cleanup (to match handleStart/handlePrompt) would be optional.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/ox/agent_hook.go` around lines 531 - 533, The call to
resetTaskCursor(ctx.ProjectRoot, agentID) from handleCompact is safe but
inconsistent with handleStart/handlePrompt; wrap the call in the same guard as
those handlers by checking both ctx.ProjectRoot and agentID are non-empty before
invoking resetTaskCursor (i.e., only call resetTaskCursor when agentID != "" &&
ctx.ProjectRoot != ""), using the existing resetTaskCursor function name to
locate the code to change.
internal/daemon/agentwork/manager.go (1)

118-128: 💤 Low value

Consider documenting the projectRoot parameter in the constructor comment.

The constructor comment at lines 118-120 describes syncSignal and ledgerPath, but doesn't describe the newly-added projectRoot parameter. Adding a sentence like "projectRoot is the working repository path for project-local agent task writes; empty disables task production" would improve clarity.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/daemon/agentwork/manager.go` around lines 118 - 128, Update the
NewManager constructor comment to document the new projectRoot parameter:
mention that projectRoot is the working repository path used for project-local
agent task writes and that an empty string disables task production; keep the
existing descriptions for syncSignal and ledgerPath and place the new sentence
alongside them in the comment above NewManager to clarify its purpose for
callers.
internal/daemon/agentwork/task_producer.go (1)

30-34: ⚡ Quick win

Marker constant needsDoctorAgentMarker matches internal/doctor.NeedsDoctorAgentMarker.
Suggestion: add a small build/test/CI guard to prevent future drift since the value is duplicated to avoid the import cycle.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/daemon/agentwork/task_producer.go` around lines 30 - 34, Add a
CI/build guard that fails if the duplicated marker drifts: create a tiny
verification step (either a short Go program or a _test.go in a non-circular
package run by CI) that reads and compares the value of the constant
needsDoctorAgentMarker in agentwork/task_producer.go against
internal/doctor.NeedsDoctorAgentMarker; wire this check into the repo CI (or
make it a go test) so the comparison runs on every PR and fails when the two
string constants diverge.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cmd/ox/agent_tasks.go`:
- Around line 144-146: The text output slices IDs like v.ID[:8], claimed.ID[:8],
and t.ID[:8] without checking length, which can panic for short IDs; update the
formatting sites (the fmt.Fprintf calls in agent task listing and doctor agent
task listing) to use a safe prefix helper or inline min-length logic (e.g.,
compute n := 8; if len(id) < n { n = len(id) }; id[:n]) and replace v.ID[:8],
claimed.ID[:8], t.ID[:8] with that safe substring so short IDs no longer cause a
runtime panic.
- Around line 95-100: runAgentTasks currently sets sub := args[0] and then
unconditionally does rest := args[1:], which panics when no subcommand is
passed; also listing/claiming slices IDs with v.ID[:8] and claimed.ID[:8] which
panics for short non-empty IDs. Fix by guarding the args slicing: set rest to an
empty slice unless len(args)>1 before taking args[1:], and ensure sub is set to
"list" when args is empty (references: variables sub and rest in runAgentTasks).
Replace unsafe ID truncation (v.ID[:8], claimed.ID[:8]) with a safe truncate
that checks len(ID) > 8 before slicing (or returns the whole ID when shorter) so
short IDs do not cause a panic.

In `@cmd/ox/doctor_agent_tasks.go`:
- Line 32: Replace the os.IsNotExist(err) check with the errors.Is(err,
os.ErrNotExist) pattern when testing the result of os.Stat on tasksFile: update
the conditional in the code that currently reads "if _, err :=
os.Stat(tasksFile); os.IsNotExist(err) {" to use errors.Is(err, os.ErrNotExist)
so wrapped errors are handled correctly, and add an import for the standard
"errors" package if not already present; ensure the check still handles the
nil-error case properly where needed.

In `@docs/ai/specs/agent-task-scheduling.md`:
- Around line 31-34: The two fenced code blocks containing the lines
".sageox/agent_tasks/agent_tasks.jsonl   # gitignored, ephemeral, local-only"
and the ASCII state diagram (the block starting with "┌─────────── claim (next)
───────────┐" and ending with "completed") are missing language identifiers;
update each triple-backtick fence to include a language token (use "text") so
the blocks become "```text" to satisfy MD040 and ensure proper
linting/rendering.

In `@internal/agenttask/store.go`:
- Around line 457-479: readTasksLocked currently uses bufio.Scanner with a 1MiB
token limit and returns on scanner.Err(), which causes a bufio.ErrTooLong line
to abort the whole read; change the scan loop to detect bufio.ErrTooLong (and
any token-too-large condition) and skip that offending line (log/debug if
needed) instead of returning an error, keeping the existing behavior of skipping
malformed/empty rows; also replace any os.IsNotExist(err) checks with
errors.Is(err, os.ErrNotExist) per repo guidelines; reference
scanner/Scanner.Buffer, scanner.Scan, scanner.Err, and the readTasksLocked
function to locate the changes.

---

Nitpick comments:
In `@cmd/ox/agent_hook.go`:
- Around line 531-533: The call to resetTaskCursor(ctx.ProjectRoot, agentID)
from handleCompact is safe but inconsistent with handleStart/handlePrompt; wrap
the call in the same guard as those handlers by checking both ctx.ProjectRoot
and agentID are non-empty before invoking resetTaskCursor (i.e., only call
resetTaskCursor when agentID != "" && ctx.ProjectRoot != ""), using the existing
resetTaskCursor function name to locate the code to change.

In `@internal/agenttask/store.go`:
- Around line 445-448: Replace the os.IsNotExist(err) check with errors.Is(err,
os.ErrNotExist) in the task file open path (the code that uses s.tasksPath), and
when returning or propagating other errors from os.Open or subsequent IO, wrap
them with context using fmt.Errorf(...%w...) (e.g., fmt.Errorf("opening tasks
file %s: %w", s.tasksPath, err)) so error unwrapping and contextual logs follow
project conventions.

In `@internal/daemon/agentwork/manager.go`:
- Around line 118-128: Update the NewManager constructor comment to document the
new projectRoot parameter: mention that projectRoot is the working repository
path used for project-local agent task writes and that an empty string disables
task production; keep the existing descriptions for syncSignal and ledgerPath
and place the new sentence alongside them in the comment above NewManager to
clarify its purpose for callers.

In `@internal/daemon/agentwork/task_producer.go`:
- Around line 30-34: Add a CI/build guard that fails if the duplicated marker
drifts: create a tiny verification step (either a short Go program or a _test.go
in a non-circular package run by CI) that reads and compares the value of the
constant needsDoctorAgentMarker in agentwork/task_producer.go against
internal/doctor.NeedsDoctorAgentMarker; wire this check into the repo CI (or
make it a go test) so the comparison runs on every PR and fails when the two
string constants diverge.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 9b31049b-7aae-4d7d-8cb7-90621c0fddd8

📥 Commits

Reviewing files that changed from the base of the PR and between 25500f0 and 09d0ae9.

📒 Files selected for processing (25)
  • .sageox/.gitignore
  • cmd/ox/agent.go
  • cmd/ox/agent_hook.go
  • cmd/ox/agent_tasks.go
  • cmd/ox/agent_tasks_surface.go
  • cmd/ox/agent_tasks_test.go
  • cmd/ox/doctor.go
  • cmd/ox/doctor_agent.go
  • cmd/ox/doctor_agent_tasks.go
  • cmd/ox/doctor_agent_tasks_test.go
  • cmd/ox/doctor_sageox.go
  • cmd/ox/doctor_types.go
  • cmd/ox/init_gitignore_test.go
  • cmd/ox/status.go
  • docs/ai/specs/agent-task-scheduling.md
  • internal/agenttask/store.go
  • internal/agenttask/store_test.go
  • internal/agenttask/task.go
  • internal/daemon/agentwork/manager.go
  • internal/daemon/agentwork/manager_test.go
  • internal/daemon/agentwork/session_finalize_orphan_test.go
  • internal/daemon/agentwork/task_producer.go
  • internal/daemon/agentwork/task_producer_test.go
  • internal/daemon/daemon.go
  • internal/daemon/daemon_pending_work_test.go

Comment thread cmd/ox/agent_tasks.go
Comment on lines +95 to +100
sub := "list"
if len(args) > 0 {
sub = args[0]
}
rest := args[1:]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the reported section
echo "== cmd/ox/agent_tasks.go: lines 70-130 =="
nl -ba cmd/ox/agent_tasks.go | sed -n '70,130p'

# Search for other related unsafe slices (task ID formatting mentioned)
echo
echo "== Search for fixed-length slicing like [:8] in cmd/ox/agent_tasks.go =="
rg -n '\[:8\]' cmd/ox/agent_tasks.go || true

echo
echo "== Search for args[1:] usage in cmd/ox/agent_tasks.go =="
rg -n 'args\[1:\]' cmd/ox/agent_tasks.go || true

Repository: sageox/ox

Length of output: 156


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== cmd/ox/agent_tasks.go: lines 70-130 =="
awk 'NR>=70 && NR<=130 {printf "%d\t%s\n", NR, $0}' cmd/ox/agent_tasks.go

echo
echo "== Search for args[1:] usage in cmd/ox/agent_tasks.go =="
rg -n 'args\[1:\]' cmd/ox/agent_tasks.go || true

echo
echo "== Search for fixed-length slicing [:8] in cmd/ox/agent_tasks.go =="
rg -n '\[:8\]' cmd/ox/agent_tasks.go || true

echo
echo "== Search for other string slicing patterns like [:N] in cmd/ox/agent_tasks.go =="
rg -n '\[:[0-9]+\]' cmd/ox/agent_tasks.go || true

Repository: sageox/ox

Length of output: 2928


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Inspect around ID formatting (cmd/ox/agent_tasks.go: 130-220) =="
awk 'NR>=130 && NR<=220 {printf "%d\t%s\n", NR, $0}' cmd/ox/agent_tasks.go

echo
echo "== Find taskView definition and ID type =="
rg -n 'type taskView|taskView|ID[ ]+string' cmd/ox/agent_tasks.go

echo
echo "== Locate agenttask definitions for ID generation/length guarantees =="
# Search the repo for the agenttask package and ID field usages/definitions
rg -n 'package agenttask|type .*Task|ID[ ]+string|ID[:]' --hidden -S . || true

Repository: sageox/ox

Length of output: 50366


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== cmd/ox/agent_tasks.go: top (cobras/args wiring) =="
awk 'NR<=120 {printf "%d\t%s\n", NR, $0}' cmd/ox/agent_tasks.go

echo
echo "== locate agenttask package path =="
fd -t d "agenttask" . || true
fd -t f "*agenttask*" . || true

echo
echo "== find definition: type Task struct in repo =="
rg -n "type Task struct" --glob '*.go' . | head -n 50

echo
echo "== find toTaskView / parseTaskIDAndNote implementations =="
rg -n "func toTaskView|func parseTaskIDAndNote" cmd/ox/agent_tasks.go
awk 'NR>=1 && NR<=110 {print}' cmd/ox/agent_tasks.go | rg -n "func toTaskView|func parseTaskIDAndNote" || true

Repository: sageox/ox

Length of output: 5836


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== cmd/ox/agent_tasks.go: top (cobras/args wiring) =="
awk 'NR<=140 {printf "%d\t%s\n", NR, $0}' cmd/ox/agent_tasks.go

echo
echo "== locate agenttask package path =="
fd -t d "agenttask" . || true
fd -t f "*agenttask*" cmd internal pkg . || true

echo
echo "== find definition: type Task struct in repo (head limited) =="
rg -n "type Task struct" --glob '*.go' . | head -n 50

echo
echo "== find toTaskView / parseTaskIDAndNote line numbers =="
rg -n "func toTaskView|func parseTaskIDAndNote" cmd/ox/agent_tasks.go

echo
echo "== show toTaskView and parseTaskIDAndNote bodies =="
# Use line numbers from rg output by printing generous ranges
awk 'NR>=30 && NR<=120 {printf "%d\t%s\n", NR, $0}' cmd/ox/agent_tasks.go

Repository: sageox/ox

Length of output: 10423


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== cmd/ox/agent_tasks.go: top (cobras/args wiring) =="
awk 'NR<=170 {printf "%d\t%s\n", NR, $0}' cmd/ox/agent_tasks.go

echo
echo "== locate agenttask package path =="
fd -t d "agenttask" . || true
fd -t f "*agenttask*" internal cmd pkg . || true

echo
echo "== find definition: type Task struct in repo (head limited) =="
rg -n "type Task struct" --glob '*.go' . | head -n 50

echo
echo "== find toTaskView / parseTaskIDAndNote line numbers =="
rg -n "func toTaskView|func parseTaskIDAndNote" cmd/ox/agent_tasks.go

echo
echo "== show toTaskView and parseTaskIDAndNote bodies (approx range) =="
awk 'NR>=20 && NR<=120 {printf "%d\t%s\n", NR, $0}' cmd/ox/agent_tasks.go

Repository: sageox/ox

Length of output: 12116


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== cobra command definitions in cmd/ox/agent_tasks.go =="
rg -n 'cobra\.Command|Use:|Args:|RunE:|runAgentTasks\(' cmd/ox/agent_tasks.go

echo
echo "== show sections around those definitions (best-effort) =="
# Show likely areas: from end of file around command setup
awk 'NR>=220 && NR<=420 {printf "%d\t%s\n", NR, $0}' cmd/ox/agent_tasks.go

echo
echo "== inspect internal/agenttask/task.go around Task struct =="
awk 'NR>=1 && NR<=220 {printf "%d\t%s\n", NR, $0}' internal/agenttask/task.go

echo
echo "== search internal/agenttask for Task.ID assignment/generation =="
rg -n '(\.ID\s*=|ID:\s*|Task\{.*ID:|func .*ID|ID.*string).*' internal/agenttask --hidden || true

echo
echo "== search for ID length validation or formatting ([:8] etc) in internal/agenttask =="
rg -n '\[:[0-9]+\]|len\(.+ID\)|ID.*slice|substring|[:][0-9]' internal/agenttask --hidden || true

Repository: sageox/ox

Length of output: 15975


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== cobra command definitions in cmd/ox/agent_tasks.go =="
rg -n 'cobra\.Command|Use:|Args:|RunE:|runAgentTasks\(' cmd/ox/agent_tasks.go

echo
echo "== show sections around command setup (cmd/ox/agent_tasks.go: 220-420) =="
awk 'NR>=220 && NR<=420 {printf "%d\t%s\n", NR, $0}' cmd/ox/agent_tasks.go

echo
echo "== inspect internal/agenttask/task.go around Task struct (1-220) =="
awk 'NR>=1 && NR<=220 {printf "%d\t%s\n", NR, $0}' internal/agenttask/task.go

echo
echo "== search internal/agenttask for ID assignment/generation =="
rg -n '(\.ID\s*=|ID:\s*|Task\{.*ID:)' internal/agenttask --hidden || true

echo
echo "== search internal/agenttask for ID slicing/len checks =="
rg -n 'len\(.+ID\)|\[:[0-9]+\]|ID\[:|claimed\.ID\[:|v\.ID\[:' internal/agenttask --hidden || true

Repository: sageox/ox

Length of output: 15465


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== cobra command definitions in cmd/ox/agent_tasks.go =="
rg -n 'cobra\.Command|Use:|Args:|RunE:|runAgentTasks\(' cmd/ox/agent_tasks.go

echo
echo "== show sections around command setup (cmd/ox/agent_tasks.go: 220-420) =="
awk 'NR>=220 && NR<=420 {printf "%d\t%s\n", NR, $0}' cmd/ox/agent_tasks.go

echo
echo "== inspect internal/agenttask/task.go around Task struct (1-220) =="
awk 'NR>=1 && NR<=220 {printf "%d\t%s\n", NR, $0}' internal/agenttask/task.go

echo
echo "== search internal/agenttask for ID assignment/generation =="
rg -n '(\.ID\s*=|ID:\s*|Task\{.*ID:)' internal/agenttask --hidden || true

echo
echo "== search internal/agenttask for ID slicing/len checks =="
rg -n 'len\(.+ID\)|\[:[0-9]+\]|ID\[:|claimed\.ID\[:|v\.ID\[:' internal/agenttask --hidden || true

Repository: sageox/ox

Length of output: 15465


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Find runAgentTasks call sites =="
rg -n "runAgentTasks\\(" cmd/ox --glob '*.go'

echo
echo "== Inspect agent-facing cobra wiring around calling runAgentTasks (search for 'tasks' command in cmd/ox) =="
rg -n 'Use:\s*"tasks"|runAgentTasks|agent.*tasks' cmd/ox --glob '*.go'

echo
echo "== Inspect internal/agenttask/store.go around newTaskID and ID handling =="
awk 'NR>=1 && NR<=220 {print}' internal/agenttask/store.go | head -n 220 >/dev/null
# Show targeted regions
awk 'NR>=110 && NR<=190 {printf "%d\t%s\n", NR, $0}' internal/agenttask/store.go
awk 'NR>=600 && NR<=720 {printf "%d\t%s\n", NR, $0}' internal/agenttask/store.go

echo
echo "== Search internal/agenttask/store.go for ID validation / UUID parsing =="
rg -n "newTaskID|uuid|UUID|ID == \"\"|ID\\s*=" internal/agenttask/store.go

Repository: sageox/ox

Length of output: 12090


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Find runAgentTasks call sites =="
rg -n "runAgentTasks\\(" cmd/ox --glob '*.go'

echo
echo "== Inspect agent-facing cobra wiring around calling runAgentTasks (search for 'tasks' command in cmd/ox) =="
rg -n 'Use:\s*"tasks"|runAgentTasks|agent.*tasks' cmd/ox --glob '*.go'

echo
echo "== Inspect internal/agenttask/store.go around newTaskID and ID handling =="
awk 'NR>=100 && NR<=170 {printf "%d\t%s\n", NR, $0}' internal/agenttask/store.go
awk 'NR>=620 && NR<=700 {printf "%d\t%s\n", NR, $0}' internal/agenttask/store.go

echo
echo "== Search internal/agenttask/store.go for ID validation / UUID parsing =="
rg -n "newTaskID|uuid|UUID|ID == \"\"|ID\\s*=" internal/agenttask/store.go

Repository: sageox/ox

Length of output: 11286


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== cmd/ox/agent.go around runAgentTasks call (520-580) =="
awk 'NR>=500 && NR<=580 {printf "%d\t%s\n", NR, $0}' cmd/ox/agent.go

echo
echo "== find where subargs computed / tasks dispatch in cmd/ox/agent.go =="
rg -n "subargs|runAgentTasks|case \"tasks\"|Use:.*tasks|tasks\"" cmd/ox/agent.go

echo
echo "== show more context around tasks case if present =="
# show +/- 30 lines around each match line number
rg -n "runAgentTasks\\(" cmd/ox/agent.go | while IFS=: read -r ln _; do
  start=$((ln-40)); end=$((ln+40));
  echo "--- around $ln ($start-$end) ---";
  awk -v s=$start -v e=$end 'NR>=s && NR<=e {printf "%d\t%s\n", NR, $0}' cmd/ox/agent.go;
done

echo
echo "== internal/agenttask/store.go: inspect task parsing/reconcile for ID validation =="
rg -n "json\\.Unmarshal|Decode|Scan|reconcile|List\\(" internal/agenttask/store.go | head -n 50
echo
awk 'NR>=220 && NR<=360 {printf "%d\t%s\n", NR, $0}' internal/agenttask/store.go

Repository: sageox/ox

Length of output: 12826


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== cmd/ox/agent.go around runAgentTasks call (520-580) =="
awk 'NR>=500 && NR<=580 {printf "%d\t%s\n", NR, $0}' cmd/ox/agent.go

echo
echo "== find where subargs computed / tasks dispatch in cmd/ox/agent.go =="
rg -n "subargs|runAgentTasks|case \"tasks\"|Use:.*tasks|tasks\"" cmd/ox/agent.go

echo
echo "== show more context around tasks case if present =="
rg -n "runAgentTasks\\(" cmd/ox/agent.go | while IFS=: read -r ln _; do
  start=$((ln-40)); end=$((ln+40));
  echo "--- around $ln ($start-$end) ---";
  awk -v s=$start -v e=$end 'NR>=s && NR<=e {printf "%d\t%s\n", NR, $0}' cmd/ox/agent.go;
done

echo
echo "== internal/agenttask/store.go: inspect task parsing/reconcile for ID validation =="
rg -n "json\\.Unmarshal|Decode|Scan|reconcile|List\\(" internal/agenttask/store.go | head -n 50

echo
awk 'NR>=220 && NR<=360 {printf "%d\t%s\n", NR, $0}' internal/agenttask/store.go

Repository: sageox/ox

Length of output: 12826


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== cmd/ox/agent.go: inspect dispatch for subcommand/tasks (around subargs indexing) =="
awk 'NR>=440 && NR<=540 {printf "%d\t%s\n", NR, $0}' cmd/ox/agent.go

echo
echo "== internal/agenttask/store.go: inspect reconcileWith / JSONL parsing for task IDs =="
# Show the whole reconcileWith block region
awk 'NR>=380 && NR<=560 {printf "%d\t%s\n", NR, $0}' internal/agenttask/store.go

echo
echo "== internal/agenttask/store.go: search for t.ID validation/guards =="
rg -n "t\\.ID|task\\.ID|ID == \"\"|len\\(|uuid|UUID" internal/agenttask/store.go

echo
echo "== cmd/ox/agent_tasks_test.go: search for task IDs shorter than 8 or any empty args cases =="
rg -n "runAgentTasks\\(&buf, testInst\\(\\), \\[\\]string\\{\\}|tasks\\\"\\s*\\)|\\[:8\\]|ID\\s*:\\s*\\\".{0,7}\\\"" cmd/ox/agent_tasks_test.go || true

# Also quick check for any direct task ID literals in agent_tasks_test.go
rg -n 'ID:\s*"' cmd/ox/agent_tasks_test.go || true

Repository: sageox/ox

Length of output: 12333


Prevent panic when ox agent <id> tasks is called with no subcommand.

Line 99 slices args[1:] unconditionally in runAgentTasks, so ox agent <id> tasks (empty subargs) can crash instead of defaulting to list.

Suggested diff
 	sub := "list"
+	rest := []string{}
 	if len(args) > 0 {
 		sub = args[0]
+		rest = args[1:]
 	}
-	rest := args[1:]
  • Text rendering also slices task IDs with v.ID[:8] / claimed.ID[:8]; queued tasks only skip rows with empty id, so short non-empty IDs can still panic while listing/claiming.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/ox/agent_tasks.go` around lines 95 - 100, runAgentTasks currently sets
sub := args[0] and then unconditionally does rest := args[1:], which panics when
no subcommand is passed; also listing/claiming slices IDs with v.ID[:8] and
claimed.ID[:8] which panics for short non-empty IDs. Fix by guarding the args
slicing: set rest to an empty slice unless len(args)>1 before taking args[1:],
and ensure sub is set to "list" when args is empty (references: variables sub
and rest in runAgentTasks). Replace unsafe ID truncation (v.ID[:8],
claimed.ID[:8]) with a safe truncate that checks len(ID) > 8 before slicing (or
returns the whole ID when shorter) so short IDs do not cause a panic.

Comment thread cmd/ox/agent_tasks.go
Comment on lines +144 to +146
fmt.Fprintf(w, " [%s] p%d %s — %s (%s, %s old)\n",
v.Status, v.Priority, v.ID[:8], v.Title, kindOrDash(v.Kind), v.Age)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate all risky ID slicing usages in the file
rg -n 'ID\[:8\]|claimed\.ID\[:8\]|id\[:8\]' cmd/ox/agent_tasks.go

# Print the relevant sections with line numbers
sed -n '120,220p' cmd/ox/agent_tasks.go

Repository: sageox/ox

Length of output: 3035


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show surrounding code for the slice sites to see how IDs are produced/validated
sed -n '130,210p' cmd/ox/agent_tasks.go

# Find the task view mapping and any ID normalization/validation
rg -n "func toTaskView|type taskView|ID string|ClaimOptions|DefaultLease" cmd/ox/agent_tasks.go cmd/ox -S || true

# Locate the underlying agenttask types that define ID and task IDs
rg -n "package agenttask|type .*Task|type .*Claimed|ID\s+string|ID\s+\w+|DefaultLease" . -S --glob='*.go' \
  | head -n 50

Repository: sageox/ox

Length of output: 17702


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Inspect the mapping from agenttask.Task -> taskView (where v.ID comes from)
sed -n '1,120p' cmd/ox/agent_tasks.go

# 2) Locate the agenttask package and inspect Task struct + ID field type/constraints
#    (repo-relative searches; keep output bounded)
rg -n "package agenttask" -S . --glob='*.go'
rg -n "type (Task|Claimed|.*Task.*) struct" -S . --glob='*.go' -g'*agenttask*'

# If we can find the file(s) from the above, print the sections around Task struct
# (fallback: search for "type Task struct" specifically)
rg -n "type Task struct" -S . --glob='*.go' -g'*agenttask*' --glob='*.go' | head -n 20

Repository: sageox/ox

Length of output: 5223


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show Task struct and how IDs are created/parsed
sed -n '1,220p' internal/agenttask/task.go

# Look for ID validation/formatting in task store
sed -n '1,260p' internal/agenttask/store.go
sed -n '260,520p' internal/agenttask/store.go

# Search for any usage of [:8] or assumptions about ID length in agenttask
rg -n '\[:8\]|short|len\(.+ID|ID\s*<\s*8|ID\s*\.{0,1}8' internal/agenttask --glob='*.go' || true

Repository: sageox/ox

Length of output: 21872


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find newTaskID implementation
rg -n "func newTaskID" -S internal/agenttask --glob='*.go'

# Show implementation
sed -n '1,220p' internal/agenttask/store.go

# Search for calls that set `ID:` when adding tasks
rg -n "agenttask\.NewStore|\.Add\(\s*&?agenttask\.Task|newTaskID\(" -S . --glob='*.go'

# Specifically find Task literals with `ID:` field
rg -n "agenttask\.Task\s*{[^}]*ID\s*:" -S . --glob='*.go'

# Also find any explicit `task.ID =` assignments
rg -n "\.ID\s*=\s*newTaskID\(|\.ID\s*=\s*\"|\.ID\s*=" -S internal/agenttask cmd/ox --glob='*.go'

Repository: sageox/ox

Length of output: 8361


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect newTaskID implementation + any helpers around it
sed -n '600,720p' internal/agenttask/store.go

# Find any ID validation helpers in internal/agenttask (length/format checks)
rg -n "validate.*ID|ID.*validate|len\\(.*ID\\)|UUIDv7|uuidv7|Parse.*UUID|regexp.*ID|task id" internal/agenttask --glob='*.go'

# Search for any repo-wide helper used for "short" printing of IDs
rg -n "func short[A-Za-z0-9_]*ID|short[A-Za-z0-9_]*ID\\(" cmd internal --glob='*.go'

# Check where Task.ID is explicitly set (might constrain whether short IDs are realistic)
rg -n "agenttask\\.Task\\s*\\{|\\.ID\\s*=|ID\\s*:" internal --glob='*.go'

Repository: sageox/ox

Length of output: 2079


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search for any existing helper used to shorten IDs in CLI output
rg -n "func short[A-Za-z0-9_]*ID\s*\(|short[A-Za-z0-9_]*ID\s*\(" cmd internal --glob='*.go' || true

# Search for any "[:8]" usage elsewhere in CLI rendering for IDs
rg -n "\[:8\]" cmd internal --glob='*.go' || true

Repository: sageox/ox

Length of output: 2170


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find where Task.ID is set explicitly (especially short values)
rg -n "agenttask\.Task\s*\{[^}]*ID\s*:" -S . --glob='*.go' || true
rg -n "newTaskID\(|task\.ID\s*=|\.ID\s*:\s*\"|ID\s*:\s*'" -S cmd internal --glob='*.go' || true

# Inspect doctor_agent_tasks.go where it slices t.ID[:8]
sed -n '1,130p' cmd/ox/doctor_agent_tasks.go

Repository: sageox/ox

Length of output: 2914


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect existing truncation helper(s)
sed -n '1,120p' internal/cli/truncate.go

# Check any usage of those helpers around ID rendering
rg -n "cli\.Truncate|internal/cli/truncate|shorten|truncate\.go" cmd internal --glob='*.go' || true

Repository: sageox/ox

Length of output: 7467


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "Agent tasks \\(|Claimed .*\\(lease|doctor.*Agent tasks|repeatedly failing|poison task" cmd/ox/*test.go
rg -n "agent tasks" cmd/ox/*test.go internal/*/*test.go
rg -n "tasks next|tasks done|tasks cancel|tasks extend" cmd/ox/*test.go

Repository: sageox/ox

Length of output: 255


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,140p' cmd/ox/doctor_agent_tasks_test.go
echo "----"
sed -n '1,220p' cmd/ox/agent_tasks_test.go
echo "----"
sed -n '220,420p' cmd/ox/agent_tasks_test.go

Repository: sageox/ox

Length of output: 12209


Prevent panics from unchecked task ID prefix slicing in text output

cmd/ox/agent_tasks.go slices v.ID[:8] and claimed.ID[:8] in text mode without guarding ID length (panic if an on-disk row or producer sets a shorter ID). The same t.ID[:8] pattern exists in cmd/ox/doctor_agent_tasks.go.

Suggested diff
+func shortTaskID(id string) string {
+	if len(id) <= 8 {
+		return id
+	}
+	return id[:8]
+}
+
...
-			fmt.Fprintf(w, "  [%s] p%d %s — %s (%s, %s old)\n",
-				v.Status, v.Priority, v.ID[:8], v.Title, kindOrDash(v.Kind), v.Age)
+			fmt.Fprintf(w, "  [%s] p%d %s — %s (%s, %s old)\n",
+				v.Status, v.Priority, shortTaskID(v.ID), v.Title, kindOrDash(v.Kind), v.Age)

...
-		fmt.Fprintf(w, "Claimed %s (lease %s): %s\n", claimed.ID[:8], agenttask.DefaultLease, claimed.Title)
+		fmt.Fprintf(w, "Claimed %s (lease %s): %s\n", shortTaskID(claimed.ID), agenttask.DefaultLease, claimed.Title)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/ox/agent_tasks.go` around lines 144 - 146, The text output slices IDs
like v.ID[:8], claimed.ID[:8], and t.ID[:8] without checking length, which can
panic for short IDs; update the formatting sites (the fmt.Fprintf calls in agent
task listing and doctor agent task listing) to use a safe prefix helper or
inline min-length logic (e.g., compute n := 8; if len(id) < n { n = len(id) };
id[:n]) and replace v.ID[:8], claimed.ID[:8], t.ID[:8] with that safe substring
so short IDs no longer cause a runtime panic.

Comment thread cmd/ox/doctor_agent_tasks.go Outdated
Comment on lines +31 to +34
```
.sageox/agent_tasks/agent_tasks.jsonl # gitignored, ephemeral, local-only
.sageox/agent_tasks/agent_tasks.jsonl.lock
```
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add language identifiers to fenced code blocks.

The fenced blocks at Line 31 and Line 76 omit a language token, which triggers MD040.

Suggested patch
-```
+```text
 .sageox/agent_tasks/agent_tasks.jsonl   # gitignored, ephemeral, local-only
 .sageox/agent_tasks/agent_tasks.jsonl.lock

@@
- +text
┌─────────── claim (next) ───────────┐
▼ │
ready ──claim──▶ in_progress ──done────▶ completed

</details>


Also applies to: 76-83

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.22.1)</summary>

[warning] 31-31: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

</details>

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @docs/ai/specs/agent-task-scheduling.md around lines 31 - 34, The two fenced
code blocks containing the lines ".sageox/agent_tasks/agent_tasks.jsonl #
gitignored, ephemeral, local-only" and the ASCII state diagram (the block
starting with "┌─────────── claim (next) ───────────┐" and ending with
"completed") are missing language identifiers; update each triple-backtick fence
to include a language token (use "text") so the blocks become "```text" to
satisfy MD040 and ensure proper linting/rendering.


</details>

<!-- fingerprinting:phantom:triton:hawk -->

<!-- This is an auto-generated comment by CodeRabbit -->

Comment on lines +457 to +479
scanner := bufio.NewScanner(f)
scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024)
for scanner.Scan() {
line := scanner.Bytes()
if len(line) == 0 {
continue
}
var t Task
if err := json.Unmarshal(line, &t); err != nil {
continue // skip malformed rows rather than failing the whole read
}
if t.ID == "" {
continue
}
if _, exists := seen[t.ID]; !exists {
order = append(order, t.ID)
}
copied := t
seen[t.ID] = &copied
}
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("failed to read tasks: %w", err)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Prevent oversized JSONL rows from aborting task reads
In internal/agenttask/store.go (lines 457–479), readTasksLocked() uses bufio.Scanner with a 1MiB max token; if a single JSONL line exceeds the limit, scanner.Err() becomes bufio.ErrTooLong and the function returns failed to read tasks instead of skipping that row—so the reconciliation/read flow can fail even though malformed JSON/empty rows are otherwise tolerated. Also, the missing-file path uses os.IsNotExist(err) instead of errors.Is(err, os.ErrNotExist) per repo guidelines.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/agenttask/store.go` around lines 457 - 479, readTasksLocked
currently uses bufio.Scanner with a 1MiB token limit and returns on
scanner.Err(), which causes a bufio.ErrTooLong line to abort the whole read;
change the scan loop to detect bufio.ErrTooLong (and any token-too-large
condition) and skip that offending line (log/debug if needed) instead of
returning an error, keeping the existing behavior of skipping malformed/empty
rows; also replace any os.IsNotExist(err) checks with errors.Is(err,
os.ErrNotExist) per repo guidelines; reference scanner/Scanner.Buffer,
scanner.Scan, scanner.Err, and the readTasksLocked function to locate the
changes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants