Skip to content

feat(autopilot): continuous mode, hourly cron, and cycle controls#2

Merged
gf5901 merged 2 commits into
mainfrom
feat/continuous-autopilot
Mar 21, 2026
Merged

feat(autopilot): continuous mode, hourly cron, and cycle controls#2
gf5901 merged 2 commits into
mainfrom
feat/continuous-autopilot

Conversation

@gf5901
Copy link
Copy Markdown
Owner

@gf5901 gf5901 commented Mar 21, 2026

Summary

Adds continuous autopilot alongside existing daily mode: timed cycles, hourly plan proposals (with agent-driven next_check_hours deferral), auto-approve, pause reasons (time window, blocked on human tasks, failure streak, manual), optional Discord notifications (DISCORD_WEBHOOK_URL on EC2), and human cycle feedback injected into the planner.

Key changes

  • PROJECT fields: autopilot_mode, cycle_*, next_check_at, cycle_feedback
  • Plans: new sort key suffix PLAN#YYYY-MM-DDTHH:MM:SS (UTC); legacy PLAN#YYYY-MM-DD unchanged
  • Autopilot Lambda: hourly schedule; daily projects only triggered at 07:00 UTC (filtered in Lambda; EC2 enforces same)
  • API: /plans/:planId/* (encode colons as %3A), POST .../cycle/start|stop|review
  • EC2: run_task.py --propose-plan ... [--regenerate] [--plan-suffix ID]

Review notes (minor / follow-ups)

  • POST /cycle/stop does not validate autopilot_mode (harmless for daily projects).
  • Deploy: run sst deploy so the new cron applies; ensure EC2 has DISCORD_WEBHOOK_URL if you want pause alerts.

Tests

  • pytest tests/test_autopilot.py tests/test_api.py
  • pnpm run typecheck (frontend)
  • ruff check on touched Python files

Made with Cursor

gf5901 added 2 commits March 21, 2026 02:56
- Daily vs continuous autopilot_mode on PROJECT; cycle_* + next_check_at fields
- Continuous: timed cycles, auto-approve plans, next_check_hours pacing, failure
  circuit breaker, human-only pause, Discord webhook (DISCORD_WEBHOOK_URL on EC2)
- Plan SK: PLAN#YYYY-MM-DDTHH:MM:SS UTC (legacy date SK still works)
- Lambda hourly; daily projects only dispatched at 07 UTC (Lambda + EC2 guard)
- API: /plans/:planId/*, POST .../cycle/start|stop|review
- run_task.py: --plan-suffix for regenerate; SSM passes optional suffix
- UI: Off/Daily/Continuous, cycle controls, plan timeline (continuous)

Made-with: Cursor
- Daily mode no longer blocks regenerate requests outside 07:00 UTC
- Remove duplicate Regenerate button in continuous proposed view
- Eliminate unnecessary DynamoDB re-read in auto-approve path
- Remove dead _dynamo_items_to_normalized wrapper
- Lambda skips SSM trigger for continuous projects without active cycle
- Clear cycle state when switching away from continuous mode
- Add mode validation to cycle/stop endpoint
- Remove redundant not-regenerate guard in stranded plan recovery

Made-with: Cursor
@gf5901 gf5901 merged commit 9539e60 into main Mar 21, 2026
4 checks passed
@gf5901
Copy link
Copy Markdown
Owner Author

gf5901 commented Mar 21, 2026

Automated PR Review

The workspace is empty so I need to clone the repo to review the full diff.Let me review the full diff across all the critical files.Let me check a few more critical areas — the full SSM command construction for shell injection risk, and the full autopilot.py for the mode gating logic.Now let me check the planSuffix parameter validation in the SSM path, since that's user-controlled and inserted into a shell command.The Lambda doesn't check next_check_at at all. Now I have enough information to give a thorough review.

NEEDS_WORK:

  • Race condition in _auto_approve_plan (src/autopilot.py): update_plan_fields sets status to "approved" without a ConditionExpression (e.g. status = :proposed). If two concurrent invocations (Lambda retry, manual regenerate + hourly tick) both reach auto-approve for the same plan, duplicate tasks are created. The "stranded proposed plan" recovery (line ~508) also calls _auto_approve_plan without a conditional guard, compounding the risk. Add ConditionExpression="attribute_not_exists(approved_at)" (or #st = :proposed) to the update_plan_fields call inside _auto_approve_plan and handle ConditionalCheckFailedException as a no-op.
  • Lambda hourly tick doesn't check next_check_at: shouldTriggerProposePlan in the autopilot Lambda (infra/packages/autopilot/src/index.ts) dispatches SSM for every non-paused continuous project every hour, even when the agent explicitly requested a multi-hour next_check_hours delay. The data is already on the item from the DynamoDB query — add if (item.next_check_at && new Date(item.next_check_at) > new Date()) return false; to avoid unnecessary SSM invocations and EC2 process spawns.
  • Shell injection surface area on planSuffix: The esc() helper in ssm.ts only escapes single quotes. The planSuffix is validated upstream by isValidPlanId regex (digits/hyphens/colons/T only), so it's safe today, but the triggerProposePlan function itself has no validation and accepts an arbitrary string. Add a defensive regex check or assertion inside triggerProposePlan to avoid a future bypass if a new caller skips isValidPlanId.
  • cycle/stop and cycle/review don't validate autopilot_mode: POST /cycle/stop sets cycle_paused: true on any project regardless of mode. While technically harmless (daily mode ignores cycle_paused), it's inconsistent with cycle/start which validates mode. Either guard all three or none.

Minor / non-blocking:

  • Redundant and not regenerate checks at lines ~510, 521, 525 of src/autopilot.py — already inside an if mode == "continuous" and not regenerate: block.
  • _dynamo_items_to_normalized is a trivial wrapper over _normalize_items with the same isinstance guard; could be inlined.
  • Consider adding cycle_started_at emptiness check to the Lambda's shouldTriggerProposePlan (same as the Python-side guard at line 437) to further reduce unnecessary SSM dispatches for continuous projects that haven't had a cycle started yet.

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.

1 participant