Skip to content

feat: add cron agent for scheduled AI task dispatch#304

Merged
marcusquinn merged 1 commit intomainfrom
feature/t110-cron-agent
Feb 4, 2026
Merged

feat: add cron agent for scheduled AI task dispatch#304
marcusquinn merged 1 commit intomainfrom
feature/t110-cron-agent

Conversation

@marcusquinn
Copy link
Copy Markdown
Owner

@marcusquinn marcusquinn commented Feb 4, 2026

Summary

  • Add cron agent (@cron) for managing scheduled AI agent dispatch via OpenCode server
  • Create cron-helper.sh for job management (list, add, remove, pause, resume, logs, debug, status)
  • Create cron-dispatch.sh for executing jobs via OpenCode server API
  • Full documentation with persistent server setup guides (launchd/systemd)

Changes

File Description
.agent/tools/automation/cron-agent.md Full subagent documentation
.agent/scripts/cron-helper.sh Job management CLI (~550 lines)
.agent/scripts/cron-dispatch.sh Job execution via OpenCode API (~200 lines)
.agent/subagent-index.toon Updated with new scripts
TODO.md Added started timestamp

Features

  • Job management: list, add, remove, pause, resume
  • Debugging: logs, debug, status commands
  • OpenCode integration: Creates sessions, sends prompts, handles timeouts
  • Mailbox integration: Optional notifications via mail-helper.sh
  • Persistent server: launchd (macOS) and systemd (Linux) examples

Testing

# Help
cron-helper.sh help

# Status (works without OpenCode server)
cron-helper.sh status

# Add a job
cron-helper.sh add --schedule "0 9 * * *" --task "Generate daily report"

Related

  • Closes t110
  • Depends on: OpenCode server (opencode serve)
  • Integrates with: mail-helper.sh, memory-helper.sh

Summary by CodeRabbit

  • New Features
    • Added cron job management system for scheduling and automating AI agent tasks
    • Introduced CLI commands for job operations: add, remove, pause, resume, list, run, logs, status, and debug
    • Integrated OpenCode server for task execution with health checks and error handling
    • Job status tracking with execution logs and optional notifications
    • Comprehensive documentation for setup and usage

- Create cron-agent.md subagent with full documentation
- Add cron-helper.sh for job management (list, add, remove, pause, resume, logs, debug, status)
- Add cron-dispatch.sh for executing jobs via OpenCode server API
- Update subagent-index.toon with new scripts and updated automation folder description
- Integrates with existing mailbox system for notifications
- Supports persistent server setup via launchd (macOS) or systemd (Linux)

Closes #t110
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Feb 4, 2026

Walkthrough

This pull request introduces a complete cron-based job management system for dispatching AI agent tasks to an OpenCode server. Two bash scripts handle job scheduling and execution, complemented by documentation and index updates tracking the new automation capabilities.

Changes

Cohort / File(s) Summary
Cron Job Dispatch System
.agent/scripts/cron-dispatch.sh, .agent/scripts/cron-helper.sh
New bash scripts for executing and managing scheduled AI agent tasks. cron-dispatch.sh (298 lines) executes individual jobs via OpenCode API with health checks, session management, and status tracking. cron-helper.sh (697 lines) provides a full CLI for CRUD operations, crontab synchronization, logging, debugging, and manual job triggering.
Documentation & Indexing
.agent/tools/automation/cron-agent.md, .agent/subagent-index.toon
New comprehensive documentation detailing the cron system architecture, CLI usage, configuration schema, integration patterns, and troubleshooting. Index updated to reflect new cron-agent, mac, and macos-automator identifiers within automation tools and scripts catalog.
Task Tracking
TODO.md
Marked cron agent task (t110) with start timestamp 2026-02-04T03:47Z.

Sequence Diagram(s)

sequenceDiagram
    participant Crontab as System Crontab
    participant Dispatch as cron-dispatch.sh
    participant OpenCode as OpenCode Server
    participant Config as cron-jobs.json
    participant Mail as Mail Helper

    Crontab->>Dispatch: Trigger with job-id
    Dispatch->>Config: Load job config
    Dispatch->>OpenCode: Health check
    OpenCode-->>Dispatch: Server status
    Dispatch->>OpenCode: Create session
    OpenCode-->>Dispatch: Session ID
    Dispatch->>OpenCode: Send task prompt with model
    OpenCode-->>Dispatch: Response/Result
    Dispatch->>Config: Update lastRun & lastStatus
    Dispatch->>Mail: Send notification (optional)
    Mail-->>Dispatch: Confirmation
Loading
sequenceDiagram
    participant User
    participant Helper as cron-helper.sh
    participant Config as cron-jobs.json
    participant Crontab as System Crontab
    participant OpenCode as OpenCode Server

    User->>Helper: add/remove/pause/resume
    Helper->>Config: Read/update job definitions
    Config-->>Helper: Job list
    Helper->>Crontab: Sync active jobs
    Crontab-->>Helper: Crontab updated
    
    User->>Helper: status/debug
    Helper->>OpenCode: Health check
    OpenCode-->>Helper: Server status
    Helper-->>User: Status report
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

A new task dispatcher, precise and keen,
Cron jobs now dance with a server unseen,
Schedule and trigger, dispatch with ease—
AI automation flows like a breeze! 🤖⏰

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and concisely summarizes the main change: introducing a cron agent for scheduling AI task execution.
Docstring Coverage ✅ Passed Docstring coverage is 82.35% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/t110-cron-agent

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@gemini-code-assist
Copy link
Copy Markdown

Summary of Changes

Hello @marcusquinn, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a new cron agent system designed to automate the dispatch of AI tasks via the OpenCode server. It provides a robust command-line interface for users to define, manage, and monitor scheduled AI jobs, enabling recurring operations like daily reports, health checks, and automated maintenance. The system handles job configuration, execution, logging, and optional notifications, significantly enhancing the automation capabilities of the AI agent platform.

Highlights

  • New Cron Agent for AI Task Scheduling: Introduced a new @cron agent to enable scheduled dispatch of AI tasks via the OpenCode server, allowing for automated, recurring AI operations.
  • Comprehensive Job Management CLI: A new cron-helper.sh script provides a full command-line interface for managing AI cron jobs, including listing, adding, removing, pausing, resuming, viewing logs, debugging, and checking status.
  • AI Task Dispatch Mechanism: The cron-dispatch.sh script is responsible for executing scheduled AI jobs by interacting with the OpenCode server API to create sessions, send prompts, manage timeouts, and optionally send notifications via mail-helper.sh.
  • Extensive Documentation and Persistent Setup Guides: Detailed documentation in cron-agent.md covers the architecture, commands, configuration, execution flow, and provides examples for persistent OpenCode server setup using launchd (macOS) and systemd (Linux).
Changelog
  • .agent/scripts/cron-dispatch.sh
    • Added a new shell script responsible for executing individual scheduled AI jobs.
    • Includes functions for checking OpenCode server health, creating and deleting AI sessions, sending prompts, and updating job status.
    • Integrates with mail-helper.sh for optional notifications on job completion or failure.
  • .agent/scripts/cron-helper.sh
    • Added a new shell script providing a command-line interface for managing AI cron jobs.
    • Supports commands like list, add, remove, pause, resume, logs, debug, status, and run.
    • Manages job configurations in a JSON file and synchronizes active jobs with the system's crontab.
  • .agent/subagent-index.toon
    • Updated the tools/automation/ entry to include "cron scheduling" and the new cron-agent.
    • Added cron-helper.sh and cron-dispatch.sh to the list of available scripts.
  • .agent/tools/automation/cron-agent.md
    • Added comprehensive documentation for the new @cron subagent.
    • Details the architecture, available commands, job configuration, execution flow, and integration with the OpenCode server.
    • Provides examples for persistent server setup using launchd (macOS) and systemd (Linux), as well as various use cases.
    • Includes troubleshooting tips and security considerations.
  • TODO.md
    • Updated the t110 Cron agent for scheduled task management task entry to include a started timestamp.
Activity
  • No human activity (comments, reviews, or progress updates) has been recorded for this pull request since its creation.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud bot commented Feb 4, 2026

Quality Gate Failed Quality Gate failed

Failed conditions
2 Security Hotspots

See analysis details on SonarQube Cloud

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Feb 4, 2026

🔍 Code Quality Report

�[0;35m[MONITOR]�[0m Code Review Monitoring Report

�[0;34m[INFO]�[0m Latest Quality Status:
SonarCloud: 0 bugs, 0 vulnerabilities, 0 code smells

�[0;34m[INFO]�[0m Recent monitoring activity:
Wed Feb 4 03:53:36 UTC 2026: Code review monitoring started
Wed Feb 4 03:53:37 UTC 2026: SonarCloud - Bugs: 0, Vulnerabilities: 0, Code Smells: 0
Wed Feb 4 03:53:37 UTC 2026: Qlty - 0 issues found, auto-formatting applied
Wed Feb 4 03:53:39 UTC 2026: Codacy analysis completed with auto-fixes
Wed Feb 4 03:53:40 UTC 2026: Applied 1 automatic fixes

📈 Current Quality Metrics

  • BUGS: 0
  • CODE SMELLS: 0
  • VULNERABILITIES: 0

Generated on: Wed Feb 4 03:55:13 UTC 2026


Generated by AI DevOps Framework Code Review Monitoring

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a cron agent for scheduling AI tasks, including helper scripts for job management and dispatch, along with comprehensive documentation. The implementation is solid, but I've identified several areas for improvement in the shell scripts, primarily concerning security, robustness, and efficiency.

My main concerns are:

  • Unsafe handling of credentials in curl commands, which could lead to security vulnerabilities.
  • A potential bug in job ID generation that could create duplicate IDs.
  • Inefficient or fragile patterns in shell scripting that could be improved for better reliability and performance.

I've provided specific suggestions to address these points. Overall, this is a great feature addition.

Comment on lines +39 to +46
get_auth_header() {
if [[ -n "${OPENCODE_SERVER_PASSWORD:-}" ]]; then
local user="${OPENCODE_SERVER_USERNAME:-admin}"
echo "-u ${user}:${OPENCODE_SERVER_PASSWORD}"
else
echo ""
fi
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

The way get_auth_header is implemented and used is unsafe. It constructs a string with arguments and relies on word-splitting, which can fail or be a security risk if OPENCODE_SERVER_PASSWORD contains spaces or special characters. The shellcheck disable=SC2086 comments show an awareness of word-splitting, but this pattern is not robust.

A much safer way is to use arrays for command arguments. I suggest removing get_auth_header and using a small block of code at each curl call site. For example, in check_server:

check_server() {
    local url="http://${OPENCODE_HOST}:${OPENCODE_PORT}/global/health"
    local auth_args=()
    if [[ -n "${OPENCODE_SERVER_PASSWORD:-}" ]]; then
        local user="${OPENCODE_SERVER_USERNAME:-admin}"
        auth_args=("-u" "${user}:${OPENCODE_SERVER_PASSWORD}")
    fi
    
    if curl -sf "${auth_args[@]}" "$url" &>/dev/null; then
        return 0
    else
        return 1
    fi
}

This pattern should be applied to all curl calls that need authentication (check_server, create_session, send_prompt, delete_session). This avoids word-splitting issues and makes the script more robust and secure.

Comment on lines +101 to +103
curl -sf $auth_header -X POST "$url" \
-H "Content-Type: application/json" \
-d "{\"title\": \"$title\"}" | jq -r '.id'
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

The JSON payload is constructed by embedding the $title variable directly into a string. If $title contains a double quote ("), it will break the JSON, causing the curl command to fail or send a malformed request.

You should use jq to safely construct the JSON payload, similar to how it's done in the send_prompt function.

Suggested change
curl -sf $auth_header -X POST "$url" \
-H "Content-Type: application/json" \
-d "{\"title\": \"$title\"}" | jq -r '.id'
curl -sf $auth_header -X POST "$url" \
-H "Content-Type: application/json" \
-d "$(jq -n --arg title "$title" '{"title": $title}')" | jq -r '.id'

Comment on lines +89 to +104
check_server() {
local url="http://${OPENCODE_HOST}:${OPENCODE_PORT}/global/health"
local auth_header=""

if [[ -n "${OPENCODE_SERVER_PASSWORD:-}" ]]; then
local user="${OPENCODE_SERVER_USERNAME:-admin}"
auth_header="-u ${user}:${OPENCODE_SERVER_PASSWORD}"
fi

# shellcheck disable=SC2086
if curl -sf $auth_header "$url" &>/dev/null; then
return 0
else
return 1
fi
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

The curl command uses an unquoted variable $auth_header which is constructed from OPENCODE_SERVER_PASSWORD. This is unsafe and can lead to command injection or errors if the password contains spaces or shell metacharacters. A safer approach is to use an array for curl arguments to prevent word splitting.

Suggested change
check_server() {
local url="http://${OPENCODE_HOST}:${OPENCODE_PORT}/global/health"
local auth_header=""
if [[ -n "${OPENCODE_SERVER_PASSWORD:-}" ]]; then
local user="${OPENCODE_SERVER_USERNAME:-admin}"
auth_header="-u ${user}:${OPENCODE_SERVER_PASSWORD}"
fi
# shellcheck disable=SC2086
if curl -sf $auth_header "$url" &>/dev/null; then
return 0
else
return 1
fi
}
check_server() {
local url="http://${OPENCODE_HOST}:${OPENCODE_PORT}/global/health"
local auth_args=()
if [[ -n "${OPENCODE_SERVER_PASSWORD:-}" ]]; then
local user="${OPENCODE_SERVER_USERNAME:-admin}"
auth_args=("-u" "${user}:${OPENCODE_SERVER_PASSWORD}")
fi
# shellcheck disable=SC2086
if curl -sf "${auth_args[@]}" "$url" &>/dev/null; then
return 0
else
return 1
fi
}

Comment on lines +217 to +222
name=$(echo "$job" | jq -r '.name')
task=$(echo "$job" | jq -r '.task')
workdir=$(echo "$job" | jq -r '.workdir')
timeout=$(echo "$job" | jq -r '.timeout // 600')
model=$(echo "$job" | jq -r '.model // "anthropic/claude-sonnet-4-20250514"')
notify=$(echo "$job" | jq -r '.notify // "none"')
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

You are calling jq multiple times on the same JSON object ($job) to extract different properties. This is inefficient as it spawns a new process for each property. You can extract all properties in a single, more efficient jq call using mapfile to read the values into an array. This is robust even if property values contain newlines.

Suggested change
name=$(echo "$job" | jq -r '.name')
task=$(echo "$job" | jq -r '.task')
workdir=$(echo "$job" | jq -r '.workdir')
timeout=$(echo "$job" | jq -r '.timeout // 600')
model=$(echo "$job" | jq -r '.model // "anthropic/claude-sonnet-4-20250514"')
notify=$(echo "$job" | jq -r '.notify // "none"')
mapfile -t values < <(echo "$job" | jq -r '.name, .task, .workdir, (.timeout // 600), (.model // "anthropic/claude-sonnet-4-20250514"), (.notify // "none")')
name="${values[0]}"
task="${values[1]}"
workdir="${values[2]}"
timeout="${values[3]}"
model="${values[4]}"
notify="${values[5]}"

Comment on lines +69 to +73
generate_job_id() {
local count
count=$(jq '.jobs | length' "$CONFIG_FILE" 2>/dev/null || echo "0")
printf "job-%03d" $((count + 1))
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The current method for generating a job ID is not robust. It's based on the total number of jobs, so if a job is removed, it can lead to duplicate IDs being generated later. For example, if you have job-001, job-002, job-003, and you remove job-002, the next job added will be job-003, which already exists.
A more robust approach would be to find the highest existing job number and increment it.

Suggested change
generate_job_id() {
local count
count=$(jq '.jobs | length' "$CONFIG_FILE" 2>/dev/null || echo "0")
printf "job-%03d" $((count + 1))
}
generate_job_id() {
local last_id
last_id=$(jq -r '.jobs[].id' "$CONFIG_FILE" 2>/dev/null | grep -o '[0-9]\+' | sort -rn | head -n 1)
local next_id=$(( ${last_id:-0} + 1 ))
printf "job-%03d" "$next_id"
}

Comment on lines +131 to +132
schedule=$(echo "$line" | rev | cut -d' ' -f2- | rev)
job_id=$(echo "$line" | awk '{print $NF}')
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The rev | cut | rev trick to extract the schedule is clever but can be fragile, especially with unusual whitespace. A more robust and readable way to extract the schedule and job ID is to use parameter expansion.

Suggested change
schedule=$(echo "$line" | rev | cut -d' ' -f2- | rev)
job_id=$(echo "$line" | awk '{print $NF}')
job_id=${line##* }
schedule=${line% "$job_id"}


if [[ "$follow" == "true" ]]; then
# shellcheck disable=SC2086
tail -f $log_files
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The $log_files variable is used unquoted in the tail -f command. If any log filename were to contain spaces or other special characters, this would fail. It could also fail if the list of files is very long and exceeds the system's argument length limit.

While the current job ID format prevents spaces in filenames, it's best practice to handle this robustly. A good way to handle a potentially large number of files is to use xargs.

Suggested change
tail -f $log_files
echo "$log_files" | xargs tail -f

@marcusquinn marcusquinn merged commit e4c2c19 into main Feb 4, 2026
17 of 19 checks passed
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: 6

🤖 Fix all issues with AI agents
In @.agent/scripts/cron-dispatch.sh:
- Around line 75-88: The update_job_status function currently does an
unprotected read-modify-write on CONFIG_FILE; wrap the whole update sequence in
a lightweight file lock to prevent concurrent cron runs from clobbering each
other. Modify update_job_status to acquire a lock (e.g., using flock on a lock
file descriptor or a dedicated lock file) before reading CONFIG_FILE, perform
the jq update and atomic mv while holding the lock, and ensure the lock is
released in a finally/cleanup path (use trap to remove the lock and temp_file on
exit/error). Keep the existing temp_file/mv atomic replace logic and ensure the
lock name is unique to CONFIG_FILE so other code that edits cron-jobs.json will
coordinate with update_job_status.
- Around line 247-254: The create_session call can fail (and under set -e a
curl/jq error could abort before we update status); change the session creation
to capture both the output and exit code from create_session (e.g.,
session_output=$(create_session "Cron: $name" 2>&1; echo "__EXIT_CODE__$?") or
run create_session in a subshell to avoid set -e short-circuit), parse the exit
code and treat any non-zero/empty/null output as failure, then call log_error
with the captured error/output and call update_job_status "$job_id" "failed"
before returning 1; ensure you reference create_session, session_id (or
session_output), log_error, update_job_status and job_id so the failure is
handled explicitly rather than relying on set -e.
- Around line 137-140: The script uses the GNU-only timeout command (invoked as
timeout "$timeout" curl ...) which fails on macOS; modify the top of the script
to detect an available timeout binary (check for gtimeout first then timeout),
set a variable (e.g., CMD_TIMEOUT) to the found command, and replace direct uses
of timeout with "$CMD_TIMEOUT" "$timeout" ...; if neither command is found, exit
immediately with a clear error message indicating that timeout or gtimeout is
required so the cron job fails fast and gives actionable feedback.

In @.agent/scripts/cron-helper.sh:
- Around line 180-235: The --timeout value (variable timeout) is passed to jq
using --argjson which fails on non-numeric input; before calling jq (before the
jq --argjson "$timeout" line) validate that timeout is a positive integer (e.g.,
match against a regex like ^[0-9]+$ and ensure >0), error via log_error and
return 1 if invalid, and convert/ensure timeout is the numeric value to safely
use in --argjson; update the block around timeout, generate_job_id, and the jq
invocation to perform this check.
- Around line 401-419: The --since option in cmd_logs is parsed into the local
variable since but ignored; update cmd_logs (the function and its option
parsing) to either implement timestamp-based filtering or fail-fast: simplest
fix — after parsing (before the noop “: "${since:=}"”), check if since is
non-empty and call log_error "The --since option is not yet supported" and
return 1 (or remove the --since case from the while/case if you prefer to hide
it from help). Refer to the cmd_logs function, the local variable since, and the
option parsing block to make the change so the script no longer silently accepts
--since.

In @.agent/tools/automation/cron-agent.md:
- Around line 19-395: The doc currently embeds large inline
command/config/service blocks; replace those blocks with progressive-disclosure
references to the authoritative implementations by pointing to the actual script
lines (e.g., file:.agent/scripts/cron-helper.sh:613-672 and
file:.agent/scripts/cron-dispatch.sh:190-295) and move the launchd/systemd
plist/unit examples into repository templates (e.g.,
tools/templates/opencode-server-launchd.plist and
tools/templates/opencode-server.service) then reference them as file:...; ensure
the README keeps short usage snippets only when authoritative and remove long
inline blocks in favor of file:line pointers throughout the cron-agent.md (List
Jobs, Add Job, Debug, Persistent Server Setup sections) so readers are directed
to the implementation and template files.
🧹 Nitpick comments (1)
.agent/scripts/cron-helper.sh (1)

69-73: Avoid reusing job IDs after deletions.
Using .jobs | length will reuse IDs and can collide with existing logs/history. Consider deriving the next ID from the max existing numeric suffix.

♻️ Suggested fix
 generate_job_id() {
-    local count
-    count=$(jq '.jobs | length' "$CONFIG_FILE" 2>/dev/null || echo "0")
-    printf "job-%03d" $((count + 1))
+    local max_id
+    max_id=$(jq -r '.jobs[].id' "$CONFIG_FILE" 2>/dev/null \
+        | sed -n 's/^job-\([0-9]\+\)$/\1/p' \
+        | sort -n | tail -1)
+    max_id=${max_id:-0}
+    printf "job-%03d" $((max_id + 1))
 }

Comment on lines +75 to +88
update_job_status() {
local job_id="$1"
local status="$2"
local timestamp
timestamp=$(log_timestamp)

local temp_file
temp_file=$(mktemp)
jq --arg id "$job_id" \
--arg status "$status" \
--arg timestamp "$timestamp" \
'(.jobs[] | select(.id == $id)) |= . + {lastRun: $timestamp, lastStatus: $status}' \
"$CONFIG_FILE" > "$temp_file"
mv "$temp_file" "$CONFIG_FILE"
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

Guard cron-jobs.json updates with a lock to prevent lost updates.
Concurrent cron runs can clobber the file since read-modify-write is unprotected. Add a lightweight lock to avoid race conditions.

🛡️ Suggested fix (lock around update)
 update_job_status() {
     local job_id="$1"
     local status="$2"
     local timestamp
     timestamp=$(log_timestamp)
     
-    local temp_file
-    temp_file=$(mktemp)
-    jq --arg id "$job_id" \
-       --arg status "$status" \
-       --arg timestamp "$timestamp" \
-       '(.jobs[] | select(.id == $id)) |= . + {lastRun: $timestamp, lastStatus: $status}' \
-       "$CONFIG_FILE" > "$temp_file"
-    mv "$temp_file" "$CONFIG_FILE"
+    local lock_dir="${CONFIG_FILE}.lock"
+    if ! mkdir "$lock_dir" 2>/dev/null; then
+        log_error "Config update already in progress"
+        return 1
+    fi
+    local temp_file
+    temp_file=$(mktemp)
+    trap 'rm -rf "$temp_file" "$lock_dir"' RETURN
+    jq --arg id "$job_id" \
+       --arg status "$status" \
+       --arg timestamp "$timestamp" \
+       '(.jobs[] | select(.id == $id)) |= . + {lastRun: $timestamp, lastStatus: $status}' \
+       "$CONFIG_FILE" > "$temp_file"
+    mv "$temp_file" "$CONFIG_FILE"
+    rm -rf "$lock_dir"
+    trap - RETURN
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
update_job_status() {
local job_id="$1"
local status="$2"
local timestamp
timestamp=$(log_timestamp)
local temp_file
temp_file=$(mktemp)
jq --arg id "$job_id" \
--arg status "$status" \
--arg timestamp "$timestamp" \
'(.jobs[] | select(.id == $id)) |= . + {lastRun: $timestamp, lastStatus: $status}' \
"$CONFIG_FILE" > "$temp_file"
mv "$temp_file" "$CONFIG_FILE"
update_job_status() {
local job_id="$1"
local status="$2"
local timestamp
timestamp=$(log_timestamp)
local lock_dir="${CONFIG_FILE}.lock"
if ! mkdir "$lock_dir" 2>/dev/null; then
log_error "Config update already in progress"
return 1
fi
local temp_file
temp_file=$(mktemp)
trap 'rm -rf "$temp_file" "$lock_dir"' RETURN
jq --arg id "$job_id" \
--arg status "$status" \
--arg timestamp "$timestamp" \
'(.jobs[] | select(.id == $id)) |= . + {lastRun: $timestamp, lastStatus: $status}' \
"$CONFIG_FILE" > "$temp_file"
mv "$temp_file" "$CONFIG_FILE"
rm -rf "$lock_dir"
trap - RETURN
}
🤖 Prompt for AI Agents
In @.agent/scripts/cron-dispatch.sh around lines 75 - 88, The update_job_status
function currently does an unprotected read-modify-write on CONFIG_FILE; wrap
the whole update sequence in a lightweight file lock to prevent concurrent cron
runs from clobbering each other. Modify update_job_status to acquire a lock
(e.g., using flock on a lock file descriptor or a dedicated lock file) before
reading CONFIG_FILE, perform the jq update and atomic mv while holding the lock,
and ensure the lock is released in a finally/cleanup path (use trap to remove
the lock and temp_file on exit/error). Keep the existing temp_file/mv atomic
replace logic and ensure the lock name is unique to CONFIG_FILE so other code
that edits cron-jobs.json will coordinate with update_job_status.

Comment on lines +137 to +140
# Send with timeout
# shellcheck disable=SC2086
timeout "$timeout" curl -sf $auth_header -X POST "$url" \
-H "Content-Type: application/json" \
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

🧩 Analysis chain

🏁 Script executed:

find . -name "cron-dispatch.sh" -type f

Repository: marcusquinn/aidevops

Length of output: 97


🏁 Script executed:

wc -l ./.agent/scripts/cron-dispatch.sh

Repository: marcusquinn/aidevops

Length of output: 101


🏁 Script executed:

sed -n '130,150p' ./.agent/scripts/cron-dispatch.sh

Repository: marcusquinn/aidevops

Length of output: 665


🏁 Script executed:

head -50 ./.agent/scripts/cron-dispatch.sh

Repository: marcusquinn/aidevops

Length of output: 1398


🏁 Script executed:

grep -n "timeout" ./.agent/scripts/cron-dispatch.sh

Repository: marcusquinn/aidevops

Length of output: 424


🏁 Script executed:

grep -n "uname\|darwin\|macos\|command -v" ./.agent/scripts/cron-dispatch.sh

Repository: marcusquinn/aidevops

Length of output: 46


🏁 Script executed:

sed -n '100,145p' ./.agent/scripts/cron-dispatch.sh

Repository: marcusquinn/aidevops

Length of output: 1364


🏁 Script executed:

sed -n '1,100p' ./.agent/scripts/cron-dispatch.sh | grep -i "system\|platform\|os\|version"

Repository: marcusquinn/aidevops

Length of output: 252


🏁 Script executed:

ls -la ./.agent/scripts/

Repository: marcusquinn/aidevops

Length of output: 12119


🏁 Script executed:

cat -n ./.agent/scripts/cron-dispatch.sh

Repository: marcusquinn/aidevops

Length of output: 10413


Add portable timeout command detection for macOS compatibility.

The timeout command is GNU coreutils-specific and unavailable on macOS by default. Cron jobs will fail with exit code 127 when this command is missing. Detect the timeout command (or gtimeout on systems with coreutils installed) and fail fast with a clear error if neither is available.

🔧 Suggested fix
+#######################################
+# Get timeout command
+#######################################
+get_timeout_cmd() {
+    local timeout_cmd
+    timeout_cmd=$(command -v timeout || command -v gtimeout || true)
+    if [[ -z "$timeout_cmd" ]]; then
+        log_error "timeout not found (install GNU coreutils or homebrew coreutils)"
+        return 1
+    fi
+    echo "$timeout_cmd"
+}
+
 #######################################
 # Send prompt to session
 #######################################
 send_prompt() {
     local session_id="$1"
     local task="$2"
     local model="$3"
     local timeout="$4"
     local url="http://${OPENCODE_HOST}:${OPENCODE_PORT}/session/${session_id}/message"
     local auth_header
     auth_header=$(get_auth_header)
     
+    local timeout_cmd
+    timeout_cmd=$(get_timeout_cmd) || return 1
+
     # Parse model into provider and model ID
     local provider_id model_id
     provider_id=$(echo "$model" | cut -d'/' -f1)
     model_id=$(echo "$model" | cut -d'/' -f2-)
     
     # Build request body
     local body
     body=$(jq -n \
         --arg provider "$provider_id" \
         --arg model "$model_id" \
         --arg task "$task" \
         '{
             model: {
                 providerID: $provider,
                 modelID: $model
             },
             parts: [{type: "text", text: $task}]
         }')
     
     # Send with timeout
     # shellcheck disable=SC2086
-    timeout "$timeout" curl -sf $auth_header -X POST "$url" \
+    "$timeout_cmd" "$timeout" curl -sf $auth_header -X POST "$url" \
         -H "Content-Type: application/json" \
         -d "$body"
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Send with timeout
# shellcheck disable=SC2086
timeout "$timeout" curl -sf $auth_header -X POST "$url" \
-H "Content-Type: application/json" \
#######################################
# Get timeout command
#######################################
get_timeout_cmd() {
local timeout_cmd
timeout_cmd=$(command -v timeout || command -v gtimeout || true)
if [[ -z "$timeout_cmd" ]]; then
log_error "timeout not found (install GNU coreutils or homebrew coreutils)"
return 1
fi
echo "$timeout_cmd"
}
#######################################
# Send prompt to session
#######################################
send_prompt() {
local session_id="$1"
local task="$2"
local model="$3"
local timeout="$4"
local url="http://${OPENCODE_HOST}:${OPENCODE_PORT}/session/${session_id}/message"
local auth_header
auth_header=$(get_auth_header)
local timeout_cmd
timeout_cmd=$(get_timeout_cmd) || return 1
# Parse model into provider and model ID
local provider_id model_id
provider_id=$(echo "$model" | cut -d'/' -f1)
model_id=$(echo "$model" | cut -d'/' -f2-)
# Build request body
local body
body=$(jq -n \
--arg provider "$provider_id" \
--arg model "$model_id" \
--arg task "$task" \
'{
model: {
providerID: $provider,
modelID: $model
},
parts: [{type: "text", text: $task}]
}')
# Send with timeout
# shellcheck disable=SC2086
"$timeout_cmd" "$timeout" curl -sf $auth_header -X POST "$url" \
-H "Content-Type: application/json" \
-d "$body"
}
🤖 Prompt for AI Agents
In @.agent/scripts/cron-dispatch.sh around lines 137 - 140, The script uses the
GNU-only timeout command (invoked as timeout "$timeout" curl ...) which fails on
macOS; modify the top of the script to detect an available timeout binary (check
for gtimeout first then timeout), set a variable (e.g., CMD_TIMEOUT) to the
found command, and replace direct uses of timeout with "$CMD_TIMEOUT" "$timeout"
...; if neither command is found, exit immediately with a clear error message
indicating that timeout or gtimeout is required so the cron job fails fast and
gives actionable feedback.

Comment on lines +247 to +254
# Create session
local session_id
session_id=$(create_session "Cron: $name")
if [[ -z "$session_id" || "$session_id" == "null" ]]; then
log_error "Failed to create session"
update_job_status "$job_id" "failed"
return 1
fi
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

Handle create_session failures without set -e aborts.
With set -e, a curl/jq failure inside create_session can exit before you update status. Capture the failure explicitly.

✅ Suggested fix
-    session_id=$(create_session "Cron: $name")
-    if [[ -z "$session_id" || "$session_id" == "null" ]]; then
+    if ! session_id=$(create_session "Cron: $name"); then
+        log_error "Failed to create session"
+        update_job_status "$job_id" "failed"
+        return 1
+    fi
+    if [[ -z "$session_id" || "$session_id" == "null" ]]; then
         log_error "Failed to create session"
         update_job_status "$job_id" "failed"
         return 1
     fi
🤖 Prompt for AI Agents
In @.agent/scripts/cron-dispatch.sh around lines 247 - 254, The create_session
call can fail (and under set -e a curl/jq error could abort before we update
status); change the session creation to capture both the output and exit code
from create_session (e.g., session_output=$(create_session "Cron: $name" 2>&1;
echo "__EXIT_CODE__$?") or run create_session in a subshell to avoid set -e
short-circuit), parse the exit code and treat any non-zero/empty/null output as
failure, then call log_error with the captured error/output and call
update_job_status "$job_id" "failed" before returning 1; ensure you reference
create_session, session_id (or session_output), log_error, update_job_status and
job_id so the failure is handled explicitly rather than relying on set -e.

Comment on lines +180 to +235
local schedule="" task="" name="" notify="none" timeout="$DEFAULT_TIMEOUT"
local workdir="" model="$DEFAULT_MODEL" paused=false

while [[ $# -gt 0 ]]; do
case "$1" in
--schedule) [[ $# -lt 2 ]] && { log_error "--schedule requires a value"; return 1; }; schedule="$2"; shift 2 ;;
--task) [[ $# -lt 2 ]] && { log_error "--task requires a value"; return 1; }; task="$2"; shift 2 ;;
--name) [[ $# -lt 2 ]] && { log_error "--name requires a value"; return 1; }; name="$2"; shift 2 ;;
--notify) [[ $# -lt 2 ]] && { log_error "--notify requires a value"; return 1; }; notify="$2"; shift 2 ;;
--timeout) [[ $# -lt 2 ]] && { log_error "--timeout requires a value"; return 1; }; timeout="$2"; shift 2 ;;
--workdir) [[ $# -lt 2 ]] && { log_error "--workdir requires a value"; return 1; }; workdir="$2"; shift 2 ;;
--model) [[ $# -lt 2 ]] && { log_error "--model requires a value"; return 1; }; model="$2"; shift 2 ;;
--paused) paused=true; shift ;;
*) log_error "Unknown option: $1"; return 1 ;;
esac
done

# Validate required fields
if [[ -z "$schedule" ]]; then
log_error "--schedule is required (e.g., \"0 9 * * *\")"
return 1
fi

if [[ -z "$task" ]]; then
log_error "--task is required (description of what the AI should do)"
return 1
fi

# Generate name if not provided
if [[ -z "$name" ]]; then
name=$(echo "$task" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | cut -c1-30)
fi

# Set workdir to current if not specified
if [[ -z "$workdir" ]]; then
workdir="$(pwd)"
fi

local job_id
job_id=$(generate_job_id)
local timestamp
timestamp=$(date -u +%Y-%m-%dT%H:%M:%SZ)
local status="active"
[[ "$paused" == "true" ]] && status="paused"

# Add job to config
local temp_file
temp_file=$(mktemp)
jq --arg id "$job_id" \
--arg name "$name" \
--arg schedule "$schedule" \
--arg task "$task" \
--arg workdir "$workdir" \
--argjson timeout "$timeout" \
--arg notify "$notify" \
--arg model "$model" \
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

Validate --timeout as a positive integer before jq --argjson.
Non-numeric input will cause jq to error and abort the command with a cryptic message.

✅ Suggested validation
     if [[ -z "$task" ]]; then
         log_error "--task is required (description of what the AI should do)"
         return 1
     fi
+
+    if ! [[ "$timeout" =~ ^[0-9]+$ ]] || [[ "$timeout" -le 0 ]]; then
+        log_error "--timeout must be a positive integer (seconds)"
+        return 1
+    fi
🤖 Prompt for AI Agents
In @.agent/scripts/cron-helper.sh around lines 180 - 235, The --timeout value
(variable timeout) is passed to jq using --argjson which fails on non-numeric
input; before calling jq (before the jq --argjson "$timeout" line) validate that
timeout is a positive integer (e.g., match against a regex like ^[0-9]+$ and
ensure >0), error via log_error and return 1 if invalid, and convert/ensure
timeout is the numeric value to safely use in --argjson; update the block around
timeout, generate_job_id, and the jq invocation to perform this check.

Comment on lines +401 to +419
cmd_logs() {
ensure_setup

local job_id="" tail_lines=50 follow=false
local since="" # TODO: Implement --since date filtering

while [[ $# -gt 0 ]]; do
case "$1" in
--job) [[ $# -lt 2 ]] && { log_error "--job requires a value"; return 1; }; job_id="$2"; shift 2 ;;
--tail) [[ $# -lt 2 ]] && { log_error "--tail requires a value"; return 1; }; tail_lines="$2"; shift 2 ;;
--follow|-f) follow=true; shift ;;
--since) [[ $# -lt 2 ]] && { log_error "--since requires a value"; return 1; }; since="$2"; shift 2 ;;
*) log_error "Unknown option: $1"; return 1 ;;
esac
done

# Use since to suppress shellcheck warning (future: filter logs by date)
: "${since:=}"

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

--since is documented but not implemented.
Right now it’s parsed then ignored, which is misleading. Either implement filtering or fail fast and remove it from help until supported.

🧭 Suggested interim guard
-    # Use since to suppress shellcheck warning (future: filter logs by date)
-    : "${since:=}"
+    if [[ -n "$since" ]]; then
+        log_error "--since is not implemented yet"
+        return 1
+    fi
Would you like me to draft a simple timestamp-based filter for the log format you’re writing?
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
cmd_logs() {
ensure_setup
local job_id="" tail_lines=50 follow=false
local since="" # TODO: Implement --since date filtering
while [[ $# -gt 0 ]]; do
case "$1" in
--job) [[ $# -lt 2 ]] && { log_error "--job requires a value"; return 1; }; job_id="$2"; shift 2 ;;
--tail) [[ $# -lt 2 ]] && { log_error "--tail requires a value"; return 1; }; tail_lines="$2"; shift 2 ;;
--follow|-f) follow=true; shift ;;
--since) [[ $# -lt 2 ]] && { log_error "--since requires a value"; return 1; }; since="$2"; shift 2 ;;
*) log_error "Unknown option: $1"; return 1 ;;
esac
done
# Use since to suppress shellcheck warning (future: filter logs by date)
: "${since:=}"
cmd_logs() {
ensure_setup
local job_id="" tail_lines=50 follow=false
local since="" # TODO: Implement --since date filtering
while [[ $# -gt 0 ]]; do
case "$1" in
--job) [[ $# -lt 2 ]] && { log_error "--job requires a value"; return 1; }; job_id="$2"; shift 2 ;;
--tail) [[ $# -lt 2 ]] && { log_error "--tail requires a value"; return 1; }; tail_lines="$2"; shift 2 ;;
--follow|-f) follow=true; shift ;;
--since) [[ $# -lt 2 ]] && { log_error "--since requires a value"; return 1; }; since="$2"; shift 2 ;;
*) log_error "Unknown option: $1"; return 1 ;;
esac
done
if [[ -n "$since" ]]; then
log_error "--since is not implemented yet"
return 1
fi
🤖 Prompt for AI Agents
In @.agent/scripts/cron-helper.sh around lines 401 - 419, The --since option in
cmd_logs is parsed into the local variable since but ignored; update cmd_logs
(the function and its option parsing) to either implement timestamp-based
filtering or fail-fast: simplest fix — after parsing (before the noop “:
"${since:=}"”), check if since is non-empty and call log_error "The --since
option is not yet supported" and return 1 (or remove the --since case from the
while/case if you prefer to hide it from help). Refer to the cmd_logs function,
the local variable since, and the option parsing block to make the change so the
script no longer silently accepts --since.

Comment on lines +19 to +395
## Quick Reference

- **List jobs**: `cron-helper.sh list`
- **Add job**: `cron-helper.sh add --schedule "0 9 * * *" --task "Run daily report"`
- **Remove job**: `cron-helper.sh remove <job-id>`
- **Logs**: `cron-helper.sh logs [--job <id>] [--tail 50]`
- **Debug**: `cron-helper.sh debug <job-id>`
- **Status**: `cron-helper.sh status`
- **Config**: `~/.config/aidevops/cron-jobs.json`

<!-- AI-CONTEXT-END -->

Agent for setting up, managing, identifying, and debugging cron jobs that dispatch AI agents. Uses OpenCode server API for session management.

## Architecture

```text
┌─────────────────────────────────────────────────────────────┐
│ Cron Agent System │
├─────────────────────────────────────────────────────────────┤
│ crontab │
│ └── cron-dispatch.sh <job-id> │
│ └── OpenCode Server API │
│ └── AI Session (executes task) │
│ └── Results → mail-helper.sh (optional) │
├─────────────────────────────────────────────────────────────┤
│ Storage │
│ ├── ~/.config/aidevops/cron-jobs.json (job definitions) │
│ ├── ~/.aidevops/.agent-workspace/cron/ (execution logs) │
│ └── ~/.aidevops/.agent-workspace/mail/ (result delivery) │
└─────────────────────────────────────────────────────────────┘
```

## Commands

### List Jobs

```bash
# List all scheduled jobs
cron-helper.sh list

# Output:
# ID Schedule Task Status
# job-001 0 9 * * * Run daily SEO report active
# job-002 */30 * * * * Check deployment health active
# job-003 0 0 * * 0 Weekly backup verification paused
```

### Add Job

```bash
# Add a new scheduled job
cron-helper.sh add \
--schedule "0 9 * * *" \
--task "Generate daily SEO report for example.com" \
--name "daily-seo-report" \
--notify mail \
--timeout 300

# Options:
# --schedule Cron expression (required)
# --task Task description for AI (required)
# --name Human-readable name (optional, auto-generated)
# --notify Notification method: mail|none (default: none)
# --timeout Max execution time in seconds (default: 600)
# --workdir Working directory (default: current)
# --model Model to use (default: from config)
# --paused Create in paused state
```

### Remove Job

```bash
# Remove a job by ID
cron-helper.sh remove job-001

# Remove with confirmation skip
cron-helper.sh remove job-001 --force
```

### Pause/Resume

```bash
# Pause a job (keeps definition, removes from crontab)
cron-helper.sh pause job-001

# Resume a paused job
cron-helper.sh resume job-001
```

### View Logs

```bash
# View recent execution logs
cron-helper.sh logs

# View logs for specific job
cron-helper.sh logs --job job-001

# Tail logs in real-time
cron-helper.sh logs --tail 50 --follow

# View logs from specific date
cron-helper.sh logs --since "2024-01-15"
```

### Debug Job

```bash
# Debug a failing job
cron-helper.sh debug job-001

# Output:
# Job: job-001 (daily-seo-report)
# Schedule: 0 9 * * *
# Last run: 2024-01-15T09:00:00Z
# Status: FAILED
# Exit code: 1
# Duration: 45s
#
# Error output:
# [ERROR] OpenCode server not responding on port 4096
#
# Suggestions:
# 1. Ensure OpenCode server is running: opencode serve --port 4096
# 2. Check server health: curl http://localhost:4096/global/health
# 3. Verify OPENCODE_SERVER_PASSWORD if authentication is enabled
```

### Status

```bash
# Show overall cron system status
cron-helper.sh status

# Output:
# Cron Agent Status
# ─────────────────
# Jobs defined: 5
# Jobs active: 4
# Jobs paused: 1
#
# OpenCode Server: running (port 4096)
# Last execution: 2024-01-15T09:00:00Z
# Failed jobs (24h): 1
#
# Upcoming:
# job-002 (health-check) in 15 minutes
# job-001 (daily-seo-report) in 2 hours
```

## Job Configuration

Jobs are stored in `~/.config/aidevops/cron-jobs.json`:

```json
{
"version": "1.0",
"jobs": [
{
"id": "job-001",
"name": "daily-seo-report",
"schedule": "0 9 * * *",
"task": "Generate daily SEO report for example.com using DataForSEO",
"workdir": "/Users/me/projects/example-site",
"timeout": 300,
"notify": "mail",
"model": "anthropic/claude-sonnet-4-20250514",
"status": "active",
"created": "2024-01-10T10:00:00Z",
"lastRun": "2024-01-15T09:00:00Z",
"lastStatus": "success"
}
]
}
```

## Execution Flow

When a cron job triggers:

1. **crontab** calls `cron-dispatch.sh <job-id>`
2. **cron-dispatch.sh**:
- Loads job config from `cron-jobs.json`
- Checks OpenCode server health
- Creates new session via API
- Sends task prompt
- Waits for completion (with timeout)
- Logs results
- Optionally sends notification via mailbox

```bash
# Example crontab entry (auto-managed)
0 9 * * * /Users/me/.aidevops/agents/scripts/cron-dispatch.sh job-001 >> /Users/me/.aidevops/.agent-workspace/cron/job-001.log 2>&1
```

## Integration with OpenCode Server

The cron agent requires OpenCode server to be running:

```bash
# Start server (recommended: use launchd/systemd for persistence)
opencode serve --port 4096

# With authentication (recommended for security)
OPENCODE_SERVER_PASSWORD=your-secret opencode serve --port 4096
```

### Persistent Server Setup (macOS)

Create `~/Library/LaunchAgents/com.aidevops.opencode-server.plist`:

```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.aidevops.opencode-server</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/opencode</string>
<string>serve</string>
<string>--port</string>
<string>4096</string>
</array>
<key>EnvironmentVariables</key>
<dict>
<key>OPENCODE_SERVER_PASSWORD</key>
<string>your-secret-here</string>
</dict>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>/tmp/opencode-server.log</string>
<key>StandardErrorPath</key>
<string>/tmp/opencode-server.err</string>
</dict>
</plist>
```

Load with: `launchctl load ~/Library/LaunchAgents/com.aidevops.opencode-server.plist`

### Persistent Server Setup (Linux)

Create `~/.config/systemd/user/opencode-server.service`:

```ini
[Unit]
Description=OpenCode Server
After=network.target

[Service]
Type=simple
ExecStart=/usr/local/bin/opencode serve --port 4096
Environment=OPENCODE_SERVER_PASSWORD=your-secret-here
Restart=always
RestartSec=10

[Install]
WantedBy=default.target
```

Enable with: `systemctl --user enable --now opencode-server`

## Use Cases

### Daily Reports

```bash
cron-helper.sh add \
--schedule "0 9 * * *" \
--task "Generate daily SEO performance report. Check rankings, traffic, and indexation status. Save to ~/reports/seo-$(date +%Y-%m-%d).md" \
--name "daily-seo-report" \
--notify mail
```

### Health Checks

```bash
cron-helper.sh add \
--schedule "*/30 * * * *" \
--task "Check deployment health for production servers. Verify SSL, response times, and error rates. Alert if issues found." \
--name "health-check" \
--timeout 120
```

### Automated Maintenance

```bash
cron-helper.sh add \
--schedule "0 3 * * 0" \
--task "Run weekly maintenance: prune old logs, consolidate memory, clean temp files. Report summary." \
--name "weekly-maintenance" \
--workdir "~/.aidevops"
```

### Content Publishing

```bash
cron-helper.sh add \
--schedule "0 8 * * 1-5" \
--task "Check content calendar for today's scheduled posts. Publish any ready content to WordPress and social media." \
--name "content-publisher" \
--workdir "~/projects/blog"
```

## Notification via Mailbox

When `--notify mail` is set, results are sent to the inter-agent mailbox:

```bash
# Check for cron job results
mail-helper.sh check --type status_report

# Results include:
# - Job ID and name
# - Execution time
# - Success/failure status
# - AI response summary
# - Any errors encountered
```

## Troubleshooting

### Job Not Running

```bash
# 1. Check crontab entry exists
crontab -l | grep cron-dispatch

# 2. Verify job is active (not paused)
cron-helper.sh list

# 3. Check cron daemon is running
pgrep cron || sudo service cron start
```

### OpenCode Server Issues

```bash
# 1. Check server is running
curl http://localhost:4096/global/health

# 2. Check authentication
curl -u admin:your-password http://localhost:4096/global/health

# 3. View server logs
tail -f /tmp/opencode-server.log
```

### Permission Issues

```bash
# Ensure scripts are executable
chmod +x ~/.aidevops/agents/scripts/cron-*.sh

# Check log directory permissions
ls -la ~/.aidevops/.agent-workspace/cron/
```

## Security Considerations

1. **Server authentication**: Always use `OPENCODE_SERVER_PASSWORD` for network-exposed servers
2. **Task validation**: Jobs only execute pre-defined tasks from `cron-jobs.json`
3. **Timeout limits**: All jobs have configurable timeouts to prevent runaway sessions
4. **Log rotation**: Old logs are automatically pruned (configurable retention)
5. **Credential isolation**: Tasks inherit environment from cron, not from config files

## Related Documentation

- `tools/ai-assistants/opencode-server.md` - OpenCode server API
- `mail-helper.sh` - Inter-agent mailbox for notifications
- `memory-helper.sh` - Cross-session memory for task context
- `workflows/ralph-loop.md` - Iterative AI development patterns
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

Replace inline examples with file:line references and pointers.
This doc embeds long inline command/config/service blocks. The docs standard here is progressive disclosure with references to authoritative implementation lines.

Please replace large inline blocks with:

  • References to script help/implementation (e.g., file:.agent/scripts/cron-helper.sh:613-672, file:.agent/scripts/cron-dispatch.sh:190-295).
  • Pointers to dedicated templates (if you want to keep launchd/systemd examples, place them in repo templates and reference via file:...).

As per coding guidelines: “.agent/**/*.md: Apply progressive disclosure pattern by using pointers to subagents rather than including inline content in agent documentation. Include code examples only when authoritative; use file:line references to point to actual implementation instead of inline code snippets.”

🤖 Prompt for AI Agents
In @.agent/tools/automation/cron-agent.md around lines 19 - 395, The doc
currently embeds large inline command/config/service blocks; replace those
blocks with progressive-disclosure references to the authoritative
implementations by pointing to the actual script lines (e.g.,
file:.agent/scripts/cron-helper.sh:613-672 and
file:.agent/scripts/cron-dispatch.sh:190-295) and move the launchd/systemd
plist/unit examples into repository templates (e.g.,
tools/templates/opencode-server-launchd.plist and
tools/templates/opencode-server.service) then reference them as file:...; ensure
the README keeps short usage snippets only when authoritative and remove long
inline blocks in favor of file:line pointers throughout the cron-agent.md (List
Jobs, Add Job, Debug, Persistent Server Setup sections) so readers are directed
to the implementation and template files.

marcusquinn added a commit that referenced this pull request Feb 4, 2026
- t110 (cron agent) completed with PRs #304, #305
- t119 added to triage 53 pre-existing SonarCloud hotspots
marcusquinn added a commit that referenced this pull request Feb 4, 2026
- t110 (cron agent) completed with PRs #304, #305
- t119 added to triage 53 pre-existing SonarCloud hotspots
@marcusquinn marcusquinn deleted the feature/t110-cron-agent branch February 21, 2026 01:59
@marcusquinn marcusquinn added the code-reviews-actioned All review feedback has been actioned label Mar 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

code-reviews-actioned All review feedback has been actioned

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant