Multica Autopilots — Recommended Setup
Companion to multica-agent-crew.md. The crew doc covers how agents hand off work to each other — autopilots cover how work gets kicked off on a schedule without a human trigger.
When an autopilot is the right tool
Use an autopilot when all of these are true:
- The work recurs on a predictable cadence (daily / weekly / monthly / quarterly)
- You currently rely on your own memory to trigger it
- The output is either a tracked issue that enters Maggie's routing chain, or a silent healthy-path run that only surfaces when something's wrong
When an autopilot is the wrong tool
- Feature work. Feature work enters via you assigning a Multica issue to Maggie. Autopilots don't write feature code on a timer.
- Handoff routing between agents. The crew doc's reassignment chain handles that — autopilots bypass it.
- One-off tasks. If you'd only run it once or twice, don't automate.
- Anything that needs fresh human judgment every time. Autopilots are for mechanical surveillance, not strategy.
Output modes
Multica's new-autopilot form offers two modes:
- Create issue — every run produces a tracked issue. Use for work that always generates routing (Dependabot triage, backlog sweep).
- Run only — agent executes silently; only produces an issue if the agent itself decides to create one. Use for healthy-most-of-the-time monitors (staging health, funnel check).
Audit report artifact (pillar autopilots)
Applies only to the seven objectuve-* pillar autopilots (hallmark, throughline, keystone, signal, bastion, compass, atlas) — not every autopilot in this file. Each pillar run writes a committable, summarized report so its findings survive past a Multica cloud runner's ephemeral work_dir, which is destroyed when the run ends and would otherwise take the raw evidence with it.
- Where:
.planning/audits/<pillar>-<YYYY-MM-DD>.md, written from.planning/audits/TEMPLATE-pillar-audit.md. Same-day rerun: suffix-r2,-r3. Full naming/grandfathering convention:.planning/audits/README.md. - Tracked / untracked boundary: the report is committed; the
<pillar>-evidence/blobs it cites (ui-evidence/,ui-evidence-admin/,flow-evidence/,arch-evidence/,signal-evidence/,atlas-evidence/) stay gitignored and are referenced by path only — never copied into the report.bastionandcompasshave no harvester script at all; their reports say the literalnone — static auditin the Evidence captured section rather than implying a missing capture. - Delivery: each pillar's live autopilot description carries a
# Report artifactsection (immediately before# Skip this run if) instructing the run to commit only that one file, push it on the run's existing checkout branch, open a docs-only PR (audits: <pillar> <YYYY-MM-DD>), and attach the report to the findings issue — or to the skip / run-log comment on a clean sweep, since a skip still emits a report so the tracked record can tell "audited, found nothing" apart from "never ran." - Never commit an evidence blob, and never widen a pillar's existing
Never commit <pillar>-evidence/guardrail — the report path is a carve-out, not a relaxation of it.
This section is canonical for the convention — per-autopilot entries below link here rather than restating it. All seven pillar autopilots have their own entry in this file: #6 (atlas), #14 (bastion), #15 (hallmark), #16 (throughline), #17 (keystone), #18 (signal), #19 (compass). Each carries a one-line **Report artifact** row pointing back here.
When the live platform autopilot description and this section disagree on the report mechanics, the platform description is authoritative (per the runbook-convention note below — it's what the agent actually executes); fix this section to match rather than the other way around.
Runbook discipline
- The runbook field is read fresh on every run — keep it concise and self-contained.
- Reference this guide or the crew doc for nuance; don't re-explain agent-level behavior (that lives in each agent's Multica instructions).
- Include Skip / exit conditions and Never guardrails to prevent drift over dozens of runs.
Runbook convention (single source of truth). Each autopilot's runbook lives inline in this file under its numbered entry (
### N. …), and the live autopilot'sdescriptionfield is authoritative (it is what the agent actually executes). This file is the version-controlled mirror — when a runbook changes, edit the live autopilot viamultica autopilot update <id> --description …AND update the inline entry here. There are no standalone runbook files; two such files (orion-activation-funnel,riley-feedback-ingest) were removed in 2026-06 because they drifted from the live autopilots and disagreed with each other about which copy was canonical.Necessary duplication — the HMAC signing recipe. Autopilots that POST to the Rails
activation-funnel/ webhook endpoints (#4, #10) each embed the sameprintf … | openssl dgst -sha256 -hmac "$RAILS_WEBHOOK_SECRET" | awk …recipe. This is deliberate: each runbook must be self-contained (the agent can't follow a doc cross-reference at execution time). If the signing convention ever changes, update every runbook that signs — search this file forX-Workforce-Signature.
Prerequisites
- All 10 crew agents must exist as Multica agents before their autopilots will run. Maggie, Orion, Desi, Codi, Riley, Roy, Dave, Tess, Vicki, Dori.
- Supporting tools must be wired for autopilots that need them:
- Dependabot autopilot → GitHub MCP /
ghCLI withreposcope - Staging health → Sentry MCP + Cloud Run access
- Vicki — Release readiness preflight → Sentry MCP + production Cloud Run/gcloud access
- Funnel check → PostHog MCP
- Dependabot autopilot → GitHub MCP /
- Project-specific details in runbooks (URLs, project names) come from the
CLAUDE.mdat the repo root — agents read that automatically; the runbook can reference it by name rather than duplicating its contents. - A new pillar-monthly-audit autopilot needs its target agent's
CLAUDE.mdgrant added in the same change that creates the autopilot — not discovered later by a first-run failure. Each of the sevenobjectuve-*pillar autopilots (#6, #14–#19) requires its target agent's identity to carry the matching--allexception, standing monthly-autopilot ownership, and (for create-issue modes) new-issue-creation authority before the trigger ever fires; a runbook alone is not enough; the agent's ownCLAUDE.mdhas to grant the authority the runbook assumes.Dave — Signal instrumentation audit monthly(#18) shipped without this on 2026-07-22 and its first scheduled run on 2026-08-12 failed withissue blocked— Dave's own identity didn't yet grant thesweep --all/audit --allauthority the runbook demanded, so the run stalled waiting on a human fix rather than executing. Same-day root cause and fix:OBJ-2478. When adding the Nth pillar autopilot, diff the target agent'sCLAUDE.mdagainst a sibling pillar owner's (e.g. Roy's Bastion grant, Codi's Keystone grant) that already carries this section, and add the missing grant before the first scheduled fire, not after.
Concurrency & runtime footguns (read before adding any autopilot)
Two failure modes have already bitten this workspace — both produce runs stuck in running that the UI cannot cancel:
- Single-concurrency agents have exactly one slot. Maggie (the router) runs
max_concurrent_tasks: 1by design. Before adding an autopilot assigned to a single-slot agent:- Never schedule two of its autopilots at the same fire-minute. Two Maggie sweeps both on
*/30 * * * *fire at the same instant, oversubscribe her one slot, and hang. Stagger fire-minutes (e.g. one at:00, another at:30) and check the existing crons in the schedule reference first. As of this writing the other Maggie autopilots fire at:00(except monthly Backlog grooming, moved to15 9 1 * *so it doesn't collide with the0 9Backlog sweep on a weekday 1st), so the Issue health sweep runs at:30, and the daily Blocked review at:45— all four Maggie fire-minutes (:00,:15,:30,:45) are distinct. - Prefer lower cadence, and merge checks that iterate the same data into one autopilot instead of two (the Stalled-issue + Fix-round-cap sweeps were merged into one hourly Issue health sweep for exactly this reason).
- Verify with
multica agent get <id>(max_concurrent_tasks) before scheduling.
- Never schedule two of its autopilots at the same fire-minute. Two Maggie sweeps both on
- Don't home the always-on router on a sometimes-on runtime. If a single-slot agent's runtime (e.g. a laptop daemon) sleeps or stops mid-task, that task orphans as a zombie
runningrecord — the server API can't cancel it (/api/tasks/<id>/cancel→ 404) and the whole queue head-of-line blocks behind it. Recovery requiresmultica daemon restarton the runtime host machine (find it viamultica runtime list→ the agent'sruntime_id). Prefer an always-on runtime (e.g.ai-server) for Maggie and other high-traffic single-slot agents.
Tier 1 — Start here
Eight high-ROI, low-risk autopilots (#1, #2, #2b, #3, #3b, #3c, #3d, #4) — the original bootstrap set. Get these running clean for a week before adding Tier 2. #2c, #2d, #2e were added later as additional Dave diagnostics grouped here with #2/#2b for readability; start each independently once its neighbors are stable rather than treating this whole section as one atomic bootstrap unit.
1. Roy — Dependabot triage
| Field | Value |
|---|---|
| Name | Roy — Dependabot triage |
| Agent | Roy |
| Schedule | Weekdays 07:00 Chicago · cron 0 7 * * 1-5 |
| Output mode | Create issue |
Runbook (paste into the form):
# Goal
Open Dependabot alerts on the enkidu repo are triaged and routed to the
right crew member — without Josh having to ask.
# Context
- Repo: objectuve-softworks/enkidu
- Query: `gh api "repos/objectuve-softworks/enkidu/dependabot/alerts?state=open"`
- Fix conventions live in package.json `overrides` blocks (frontend) and
Gemfile pinning (backend). Prior fixes to reference: the lodash override
in ionic_frontend, @clerk/shared override, protobufjs override.
# Steps
1. Fetch open alerts via `gh api`.
2. For each alert, note: severity, package, ecosystem, manifest path,
vulnerable range, first patched version.
3. Group by severity (critical → high → moderate → low).
4. For each alert, propose a fix path:
- npm transitive that's already safe elsewhere in the tree → `overrides` entry
- rubygems → Gemfile bump + bundle update
- mystery or breaking-change bump → route to Dave, not Codi
5. Create a single issue titled "Dependabot triage: <N> open alerts
(<highest-severity>)" with:
- One table of alerts (severity, package, manifest, patched version)
- Your recommended fix for each (Codi / Dave / ignore-with-reason)
- Which file(s) Codi would need to edit for the Codi-routable ones
6. Assign the created issue to Maggie so she routes it.
# Skip this run if
No open alerts. (Post a one-line comment in Multica's run log, do NOT
create an issue.)
# Never
- Apply the fixes yourself. This is a triage signal only.
- Batch alerts across multiple ecosystems into one fix plan — each
ecosystem (npm/rubygems) needs its own routing.Why: Dependabot alerts accumulate silently between pushes. You've already had to triage them manually twice in recent memory — this makes it a Monday-Friday habit instead of an emergency.
2. Dave — Staging health check
| Field | Value |
|---|---|
| Name | Dave — Staging health check |
| Agent | Dave |
| Schedule | Hourly, 08:00–20:00 Chicago, weekdays · cron 0 8-20 * * 1-5 |
| Output mode | Run only (Dave decides whether to create an issue) |
Runbook (paste into the form):
Credential: the Sentry token is not stored in the autopilot description. It lives in Dave's agent
custom_envasSENTRY_TOKEN(set viamultica agent env set <agent-id> --custom-env-stdin) and resolves at run time from${SENTRY_TOKEN}. The value never appears in the autopilot description, an issue comment, or an agent transcript, so rotating it needs no runbook edit.--custom-env-stdinreplaces the whole map — re-send every key Dave already has, or you will silently wipe the rest of its env (the OBJ-951 failure mode). See OBJ-2940.
# Goal
Staging is healthy. If not, produce a diagnostic issue Maggie can route.
# Context
- Staging API URL: resolve via `gcloud run services describe enkidu-api-staging --region us-central1 --project enkidu-488723 --format 'value(status.url)'`
- Health endpoint: GET /health — checks DB, Redis, Sidekiq, LiteLLM
- Cloud Run service: enkidu-api-staging (region: us-central1, project: enkidu-488723)
- Sentry org: objectuve
- Sentry project: objectuve-api (shared project; staging errors tagged via environment=staging)
- Sentry auth token: supplied to your runtime as the `SENTRY_TOKEN` environment variable.
Reference it only as `${SENTRY_TOKEN}`. Never print, echo, log, or paste its value —
not into a run log, not into an issue body, not into a comment.
# Credential preflight (OBJ-3564 / OBJ-3570)
Before the first `gcloud` call each cycle, re-sync gcloud CLI's own credential cache to the
`dave-autopilot` service account's current key. gcloud CLI imports a key file ONCE via
`gcloud auth activate-service-account` and never re-reads it on its own, so a key rotation that
updates the ADC file leaves gcloud CLI signing every JWT with an already-retired private key:
an immediate, 100%-reproducible `invalid_grant: Invalid JWT Signature` on every `gcloud`
subcommand until this runs (OBJ-3564, 2026-09-09). This mirrors
`scripts/multica-host/ensure-gcloud-sa-credential.sh` — call that script directly if this run's
workdir already has the enkidu repo checked out; otherwise run the equivalent inline:
`gcloud auth activate-service-account dave-autopilot@enkidu-488723.iam.gserviceaccount.com --key-file="$HOME/.config/dave-autopilot/key.json" --quiet`
Safe and cheap to run unconditionally every cycle — re-activating an already-current key is a
no-op in effect, no network round trip beyond the activation call itself. If this itself fails
(missing/unreadable key file, `client_email` mismatch, activation error), do not treat it as a
new condition — it surfaces as the same downstream gcloud auth failure Check 1/3 below already
classify BLOCKED, not FAIL.
# Steps
Run all three checks and record an explicit PASS / FAIL / BLOCKED for each before making any decision.
## Check 1 — /health endpoint
STAGING_URL=$(gcloud run services describe enkidu-api-staging --region us-central1 --project enkidu-488723 --format 'value(status.url)' 2>&1)
HEALTH=$(curl -sf --max-time 10 "${STAGING_URL}/health" 2>&1)
- PASS: HTTP 200 and every component (database, redis, sidekiq, litellm) reports healthy
- FAIL: Non-200 response, timeout, or any component degraded
- BLOCKED: gcloud command fails to return the URL (e.g. auth error)
## Check 2 — Sentry unresolved issues (staging environment + metric alerts)
Use the Sentry REST API directly (no sentry-cli required). The project is objectuve-api — staging errors are tagged by environment=staging automatically via RAILS_ENV. Run **both** queries below every cycle; they cover different issue shapes and neither substitutes for the other.
**Query A — error/message issues, staging-scoped.**
The `%21level%3Ainfo` (`!level:info`) severity floor is deliberate (OBJ-2932). The app emits intentional `level: :info` breadcrumbs — e.g. `Teams::ProcessSubscriptionLifecycleEvent` (OBJECTUVE-API-2V, OBJ-1439), which is spec-pinned instrumentation, not a fault. Without the floor those breadcrumbs red this check permanently. Do not remove it.
# SENTRY_TOKEN is already set in your environment. Do not assign or echo it here.
SENTRY_HTTP=$(curl -s --max-time 15 -H "Authorization: Bearer ${SENTRY_TOKEN}" -o /dev/null -w "%{http_code}" "https://sentry.io/api/0/projects/objectuve/objectuve-api/issues/?query=is%3Aunresolved%20%21level%3Ainfo&limit=25&environment=staging")
SENTRY_RESULT=$(curl -s --max-time 15 -H "Authorization: Bearer ${SENTRY_TOKEN}" "https://sentry.io/api/0/projects/objectuve/objectuve-api/issues/?query=is%3Aunresolved%20%21level%3Ainfo&limit=25&environment=staging")
**Query B — metric-alert issues, environment-unscoped.**
Sentry `issue.category:metric` issues are synthesized from an alert rule evaluating an aggregate (e.g. p95 latency), not from a tagged application event — they don't carry the same per-event `environment` tag that error/message issues do, so appending `environment=staging` to a metric query silently returns zero results even when the alert is actively firing. `OBJECTUVE-API-2Q` ("API Performance Regression p95>2s") ran unresolved and regressing for 23 days undetected for exactly this reason (OBJ-3747). Query B is deliberately **not** environment-scoped — metric alerts aren't runner/breadcrumb noise, so the noise-suppression rationale behind Query A's `environment=staging` scoping doesn't apply here. This means Query B can also surface a production-only metric regression; that's a detection widening, not a violation of the "Never touch production" guardrail below — this check only ever reads.
SENTRY_METRIC_HTTP=$(curl -s --max-time 15 -H "Authorization: Bearer ${SENTRY_TOKEN}" -o /dev/null -w "%{http_code}" "https://sentry.io/api/0/projects/objectuve/objectuve-api/issues/?query=is%3Aunresolved%20issue.category%3Ametric&limit=25")
SENTRY_METRIC_RESULT=$(curl -s --max-time 15 -H "Authorization: Bearer ${SENTRY_TOKEN}" "https://sentry.io/api/0/projects/objectuve/objectuve-api/issues/?query=is%3Aunresolved%20issue.category%3Ametric&limit=25")
- PASS: both queries return HTTP 200 and both issue lists are empty
- FAIL: both queries return HTTP 200 and at least one (Query A or Query B) has one or more unresolved issues — note which query(ies) in the summary
- BLOCKED (auth): either query returns HTTP 401 or 403 — token invalid, expired, revoked, insufficient scopes, or `SENTRY_TOKEN` unset in the environment
- BLOCKED (project missing): either query returns HTTP 404 "Project does not exist" — wrong slug
## Check 3 — Cloud Run revision status
CR_RESULT=$(gcloud run revisions list --service enkidu-api-staging --region us-central1 --project enkidu-488723 --format json 2>&1)
Inspect the latest revision: it should have status.conditions[type=Ready].status == "True" and hold 100% of traffic.
- PASS: Latest revision is Ready and serving 100% traffic
- FAIL: Latest revision is not Ready, or traffic is split
- BLOCKED: gcloud returns a permission error or non-zero exit
## Check result summary
After all three checks, produce a summary:
Check 1 (/health): PASS | FAIL | BLOCKED — <one-line note>
Check 2 (Sentry): PASS | FAIL | BLOCKED — <one-line note, cite Query A and/or Query B>
Check 3 (Cloud Run): PASS | FAIL | BLOCKED — <one-line note>
# Decision
- All PASS: exit silently. No issue created.
- Any BLOCKED: create an issue titled "Staging health check: N check(s) BLOCKED — credentials/config gap". Assign to Maggie.
- Any FAIL (and no BLOCKED): confirm by re-running the failing check 30 seconds later. If still failing:
- **Dedupe first.** Search open issues for the same failing signal before creating anything — for Check 2, the Sentry issue's `shortId` or permalink; for Check 1/3, the same component/revision failure description. `multica issue list --output json` (or `multica issue get` on any issue you suspect matches) to check. If an open issue already tracks this exact signal, comment the new check result onto it instead of creating a duplicate — mirroring `Dave — Clerk auth tripwire`'s and `Dave — Signal instrumentation audit monthly`'s dedupe pattern. Do NOT open a new diagnostic chain for a signal that's already tracked and still open.
- Otherwise, create a degradation issue. Assign to Maggie.
- Mix of PASS and BLOCKED: create the BLOCKED issue only.
# Skip this run if
It's a weekend or outside business hours (Mon–Fri 8 AM–8 PM CT). If forced, exit immediately with no issue created.
# Never
- Attempt a fix. You're a diagnostic agent.
- Treat BLOCKED as FAIL — they are different conditions requiring different routing.
- Create an issue for transient blips — always confirm FAIL on a second check 30s later.
- Touch production (enkidu-api-production). This autopilot is staging-only.Why: Staging regressions that slip in Friday afternoon currently don't get caught until you notice them. This is a one-hour watch window. Sentry auth uses a SENTRY_TOKEN env var resolved at runtime (never inlined — OBJ-2940) so the token can rotate without a doc/runbook edit.
2b. Dave — Crono liveness check
| Field | Value |
|---|---|
| Name | Dave — Crono liveness check |
| Agent | Dave |
| Schedule | Daily 08:15 Chicago · cron 15 8 * * * |
| Autopilot ID | acf44638-afa2-4f17-83c5-984b14c64819 |
| Output mode | Create issue (only when Crono is degraded or a check is blocked) |
Runbook (paste into the form):
# Goal
Crono (the scheduled-job runner) is alive in BOTH staging and production. If
not, produce a diagnostic issue Maggie can route.
# Context
- /health exposes checks.crono (informational, fail-open at the HTTP layer —
overall status stays "ok" even when Crono is dead, BY DESIGN; this
autopilot is the fail-closed alerting layer).
- crono states: ok | stale | never_ran | dead | error. "dead" (added 2026-08-13,
OBJ-2479 gate 41, `rails_api/config/initializers/crono_heartbeat.rb`) means
no heartbeat from the Crono process itself within CronoHeartbeat::CACHE_TTL
(90s) — a direct, fast liveness signal that catches "crashed at boot, never
came up" immediately, instead of waiting up to CRONO_STALE_THRESHOLD_MINUTES
for last_performed_at to go stale. This is the exact OBJ-1513 failure shape
this autopilot exists to catch, now detected sooner.
- Fastest cron is every 5 minutes; staleness threshold is 30 minutes.
- Background: Crono crashed at boot undetected for 18+ days in 2026-06/07
(cronotab `at:` bug) — this autopilot exists so that class of outage
surfaces within a day.
- URLs: resolve via `gcloud run services describe enkidu-api-staging` /
`enkidu-api-production --region us-central1 --project enkidu-488723
--format 'value(status.url)'`.
- Staging (only) has a nightly/weekend idle window: `scripts/staging-idle-window.mjs`
(OBJ-3906) is the single authoritative definition —
`.github/workflows/staging.yml`'s deploy step already derives its `minScale`
from the exact same file (currently inert behind `STAGING_IDLE_SCHEDULE_LIVE="false"`,
flips once Phase 3b's Cloud Scheduler jobs land). Step 0 below reads that
same file so this autopilot can never disagree with what staging.yml
actually deployed. Production has no idle window at all.
# Credential preflight (OBJ-3564 / OBJ-3570)
Before the first `gcloud` call each cycle, re-sync gcloud CLI's own credential cache to the
`dave-autopilot` service account's current key. gcloud CLI imports a key file ONCE via
`gcloud auth activate-service-account` and never re-reads it on its own, so a key rotation that
updates the ADC file leaves gcloud CLI signing every JWT with an already-retired private key:
an immediate, 100%-reproducible `invalid_grant: Invalid JWT Signature` on every `gcloud`
subcommand until this runs (OBJ-3564, 2026-09-09). This mirrors
`scripts/multica-host/ensure-gcloud-sa-credential.sh` — call that script directly if this run's
workdir already has the enkidu repo checked out; otherwise run the equivalent inline:
`gcloud auth activate-service-account dave-autopilot@enkidu-488723.iam.gserviceaccount.com --key-file="$HOME/.config/dave-autopilot/key.json" --quiet`
Safe and cheap to run unconditionally every cycle — re-activating an already-current key is a
no-op in effect, no network round trip beyond the activation call itself. If this itself fails
(missing/unreadable key file, `client_email` mismatch, activation error), do not treat it as a
new condition — it's a `curl/gcloud failure`, i.e. the existing BLOCKED path in Step 1/2 below,
not a new FAIL.
# Steps
0. (staging idle-window pre-check — staging only; production has no idle
window and always goes straight to Step 2)
From this run's enkidu repo checkout (the same one
`scripts/multica-host/ensure-gcloud-sa-credential.sh` is mirrored from in
the credential preflight above — check one out first if it doesn't exist
yet; never skip straight to hand-computing the answer below), run:
`node scripts/staging-idle-window.mjs`
This is the single authoritative definition of the staging idle window
(OBJ-3906) — `.github/workflows/staging.yml`'s idle-schedule derivation
step reads this exact same file, so this check can never disagree with
what staging.yml actually deployed.
- `IS_IDLE=true` → staging is inside its idle window right now (Mon-Fri
22:00-06:00 UTC, or any time on Sat/Sun). Do **not** run Step 1's curl
against `$STAGING_URL/health` at all — a request would cold-start
enkidu-api-staging, exactly what the idle window exists to avoid.
Classify staging as `SUPPRESSED` (see Decision below) and move straight
to Step 2 (production, unconditional).
- `IS_IDLE=false` → staging is inside its warm window. This is true for
every weekday run, and in particular for the first daily run after a
weekend: Monday's 08:15 Chicago run lands at 13:15/14:15 UTC, hours
after the 06:00 UTC warm-window open. That ordinary, unsuppressed run
is the positive post-wake confirmation required behaviour (b) asks
for — it FAILs exactly as normal if crono did not actually come back.
Proceed to Step 1 below with no suppression.
- If the command itself fails to run (Node missing, script missing, repo
checkout unavailable) → BLOCKED for staging, same as any other tooling
failure in this runbook. Do not fall back to hand-computing the
day/hour boundary — that is exactly the second, driftable copy
OBJ-3906 removed.
1. (staging, only when Step 0 said `IS_IDLE=false`) `curl -sf --max-time 10
"$STAGING_URL/health"`, parse `.checks.crono.status` with jq, then
classify with `node scripts/crono-liveness-decision.mjs staging <status>`
(pass the literal string `missing` if the crono key is absent from the
JSON, or `unreachable` if curl/gcloud itself failed):
- PASS: "ok"
- FAIL: "stale", "never_ran", "dead", or "error"
- BLOCKED: crono key entirely missing from the JSON (the /health change
has not deployed to that environment yet — do not treat as FAIL), or
curl/gcloud failure.
2. (production, always — unconditional, regardless of Step 0's result or the
day of week) Same check against `$PRODUCTION_URL/health`, classified via
`node scripts/crono-liveness-decision.mjs production <status>`. Production
has no idle window: no weekend skip, no suppression, fail-closed exactly
as before OBJ-3906.
## Step 3 — Log evidence (only on FAIL or BLOCKED — never for SUPPRESSED)
The dave-autopilot service account has roles/logging.viewer (granted 2026-07-06). When any check FAILed or is BLOCKED, gather evidence to embed in the issue (do NOT run this on all-PASS runs):
Set WORKER_SERVICE per the environment that FAILed or is BLOCKED: enkidu-worker-staging or enkidu-worker-production (run the queries once per affected environment if both).
WORKER_SERVICE=enkidu-worker-staging # or enkidu-worker-production
CRASH_TRACES=$(gcloud logging read "resource.type=\"cloud_run_revision\" resource.labels.service_name=\"${WORKER_SERVICE}\" (\"parse_at\" OR \"period should be at least 1 day\")" --project enkidu-488723 --freshness=3d --limit 5 --format 'value(timestamp,textPayload)' 2>&1)
RECENT_CRON=$(gcloud logging read "resource.type=\"cloud_run_revision\" resource.labels.service_name=\"${WORKER_SERVICE}\" \"Perform AiWorkforce::ScheduleRunsJob\"" --project enkidu-488723 --freshness=1d --limit 3 --format 'value(timestamp)' 2>&1)
- Crash traces present → the cronotab is raising at boot again; include the trace lines in the issue.
- No crash traces AND no recent cron lines → the Crono process may not be starting at all (worker start script); say so in the issue.
- If these commands fail with a permissions error, note it in the issue as a diagnostic gap — do NOT let it change the PASS/FAIL/BLOCKED classification from Steps 1–2.
## Step 4 — Heartbeat ping (always, regardless of PASS/FAIL/BLOCKED)
This autopilot is the fleet's independent liveness heartbeat (see
docs/operations/status-page.md#fail-closed-watchdogs-for-informational-checks).
It fires every day, 7 days a week, on a schedule fully independent of
`Maggie — Issue health sweep` — so a BetterStack heartbeat monitor attached
here can detect the class of outage this whole check exists to catch, one
level up: the autopilot fleet itself going silent, not just Crono.
After Steps 1–3 (PASS, FAIL, or BLOCKED — outcome does not matter):
`curl -fsS -m 10 "$BETTERSTACK_HEARTBEAT_URL" || true`
- `$BETTERSTACK_HEARTBEAT_URL` resolves from this agent's own `custom_env`
(Dave). Provisioned 2026-08-29 (OBJ-2976): BetterStack heartbeat "Crono
liveness — Dave autopilot (OBJ-2976)", period 1 day, grace 4 hours. The
ping fires at the END of the run, so it attests "this autopilot ran to
completion", not "the scheduler is alive" — an early BLOCK yields no ping
and therefore an alert. If the variable is ever unset or empty, skip this
curl silently; do not fail the run over a missing heartbeat URL.
- New heartbeats (added after Crono's) use
`BETTERSTACK_HEARTBEAT_URL_<AUTOPILOT_SLUG>`, not the bare
`BETTERSTACK_HEARTBEAT_URL` — that name is grandfathered to this autopilot.
See [docs/operations/status-page.md § Plan / cost](../operations/status-page.md#plan--cost).
- This step is informational only — it must never affect the Decision
step's PASS/FAIL/BLOCKED classification for Crono itself.
# Decision
- All PASS (staging `SUPPRESSED` + production PASS counts as all-clear here)
→ exit silently, no issue.
- Any FAIL → re-check that environment once after 60 seconds; if still
failing:
- Dedupe first: search open issues for title containing "Crono liveness"
(`multica issue list`). If one is already open, comment on it instead of
creating a duplicate.
- Otherwise create the issue with the environment, the exact crono JSON
block, and last_performed_at. Assign to Maggie.
- Any BLOCKED (no FAIL) → same dedupe, then create a BLOCKED issue noting
which env is missing the crono field or which command failed.
- Staging `SUPPRESSED` (Step 0 said `IS_IDLE=true`) → not a FAIL, not a
BLOCKED. No re-check, no dedupe, no issue, no comment. This is expected,
by-design behaviour (OBJ-3906) — a dead/unreachable staging crono during
its own idle window is not a signal that anything is wrong.
# Never
- Attempt a fix. You're a diagnostic agent.
- Treat BLOCKED as FAIL.
- Treat a `SUPPRESSED` staging idle-window read as FAIL or BLOCKED, or
create/comment on an issue for it.
- Create a duplicate while a "Crono liveness" issue is open.
- Skip weekends for PRODUCTION. Production reminders and streak processing
run 7 days a week — production has NO weekend skip, ever (Step 2 always
runs, unconditionally, every day). Staging's Step 0 suppression is not a
weekend skip of the check itself — the run still executes fully every day
for both environments; it only stops an *expected* idle staging read from
being misclassified as a failure.
- Hand-compute the staging idle window instead of running
`scripts/staging-idle-window.mjs`. A hand-derived guess is exactly the
second, driftable copy OBJ-3906 exists to prevent.Why: /health intentionally stays fail-open when Crono is dead (see rails_api/app/controllers/health_controller.rb's INFORMATIONAL_CHECKS) so uptime monitors don't page during deploy grace windows — but that means nothing pages on a genuinely dead Crono either. This autopilot is the fail-closed layer: it reads checks.crono daily and turns a silent degradation into a routed issue within a day, closing the gap that let Crono stay down for 18+ days undetected in 2026-06/07. The dave-autopilot service account gained roles/logging.viewer on 2026-07-06 so this runbook can gather log evidence (crash traces, recent cron activity) to embed directly in the issue when a check FAILs or is BLOCKED.
Why Step 0 (OBJ-3906): .github/workflows/staging.yml's idle-schedule gate (STAGING_IDLE_SCHEDULE_LIVE) may only flip to true in the change that lands Phase 3b's Cloud Scheduler jobs and this autopilot's idle-window awareness together. Without Step 0, the first weekend after that gate flips would file a false "Crono liveness" FAIL every Saturday/Sunday against a staging crono that's idle by design — and dedupe would turn a one-time alarm into standing weekly noise the crew learns to ignore, plus cold-start enkidu-api-staging just to observe it. Suppression alone would recreate the exact blind spot this milestone's hard constraints name explicitly: a green deploy is not proof Crono is alive, per the parse_at precedent (~2 months of silently-unfired scheduled jobs). That's why Step 0 only skips the curl during the idle window — Monday's ordinary, unsuppressed Step 1 run (already inside the warm window by the time this autopilot fires at 08:15 Chicago) is what still catches a crono that never actually woke back up.
2c. Dave — Clerk auth tripwire
| Field | Value |
|---|---|
| Name | Dave — Clerk auth tripwire |
| Agent | Dave |
| Schedule | Monthly, 3rd 08:00 Chicago · cron 0 8 3 * * |
| Autopilot ID | 7fefe2ef-d94a-487d-ae37-447ca3461446 |
| Output mode | Run only (Dave decides whether to create or comment on an issue) |
Runbook (paste into the form):
# Goal
Production Clerk native **password** sign-in still returns FAPI status
`complete`. The moment it returns anything else — `needs_client_trust`
(device verification), `needs_second_factor`, etc. — native password sign-in
is broken for every fresh install, and the app shows a misleading error. Catch
that the day it happens, before real users do.
# Context
- Defect N1 / OBJ-2532: `useClerkNative.signInWithPassword` +
`SignIn.vue`'s catch handler only handle `needs_first_factor` → `complete`.
Any other status falls through to a wrong message ("requires two-factor…"
or "Can't reach the sign-in service"). See
`.planning/quick/20260814-auth-path-audit/AUDIT.md` (defect N1) and
`LIVE-EVIDENCE.md` for the two-status breakdown.
- Production (`clerk.objectuve.com`) returns `complete` TODAY because device
verification is OFF. Enabling it — a standard Clerk security recommendation
someone could toggle in the dashboard at any time — flips this to
`needs_client_trust` and breaks native sign-in with zero code change and no
other signal. This autopilot is that tripwire.
- The check reproduces the native client's exact FAPI sequence. The demo
credential below is the SAME one already hardcoded (public) in
`ionic_frontend/src/views/SignIn.vue`'s `demoLogin()` — not a secret. If
demoLogin's password ever rotates, update it here too.
- Staging is intentionally NOT probed: its dev Clerk instance has device
verification ON, so `needs_client_trust` is the EXPECTED result there, not a
regression. (Tracked separately in OBJ-2535.)
# Steps
1. Get a fresh native client token (the token is returned in the
**Authorization response header**, not the body):
TOKEN=$(curl -sS -m 15 -D - -o /dev/null -X POST \
"https://clerk.objectuve.com/v1/client?_is_native=1" \
| grep -i '^authorization:' | sed -E 's/^[Aa]uthorization:[[:space:]]*//' | tr -d '\r')
- BLOCKED (not FAIL) if curl fails or `$TOKEN` is empty.
2. Attempt the password sign-in and read `.response.status`:
STATUS=$(curl -sS -m 15 -X POST \
"https://clerk.objectuve.com/v1/client/sign_ins?_is_native=1" \
-H "Authorization: $TOKEN" \
--data-urlencode "identifier=demo@objectuve.com" \
--data-urlencode "strategy=password" \
--data-urlencode "password=Objectuve-Demo-2026!" \
| jq -r '.response.status // empty')
- PASS: `STATUS` is `complete` **or** `needs_first_factor`. Both are
statuses the native client carries to a completed sign-in —
`useClerkNative.signInWithPassword` resolves `needs_first_factor` with a
follow-up `attempt_first_factor` call, so it is NOT the N1 landmine.
(Prod returns `complete` today; `needs_first_factor` is included so a
benign Clerk flow change never trips a false alarm.)
- FAIL: `STATUS` is any other non-empty value (`needs_client_trust`,
`needs_second_factor`, `needs_identifier`, …) — capture the exact value.
`needs_client_trust` (device verification) is the specific status this
tripwire exists to catch.
- BLOCKED (not FAIL): curl fails, or `STATUS` is empty / non-JSON (FAPI
shape changed — say so, don't classify as FAIL).
# Decision
- **PASS** → exit silently. No comment, no issue. (Liveness is read off
`multica autopilot runs` — do NOT post a heartbeat on pass.)
- **FAIL** → re-run Steps 1–2 once after 60s to rule out a transient. If it
still FAILs:
- **Dedupe first.** Look up OBJ-2532 (`multica issue get OBJ-2532`).
- If OBJ-2532 is OPEN: comment on it — device verification appears to be
LIVE in production, native password sign-in is now broken, include the
exact `STATUS` value and both curl responses, and ask Maggie to route it
as a hotfix (bump priority to urgent). Do NOT open a duplicate.
- If OBJ-2532 is already CLOSED (the fix shipped but regressed, or the
dashboard was toggled post-fix): create a NEW issue titled
"Clerk native password sign-in broken in prod (status=<STATUS>)",
link OBJ-2532 and AUDIT.md defect N1, assign to Maggie.
- **BLOCKED** (no FAIL) → search open issues for "Clerk auth tripwire"; if none
open, create one noting which curl failed or that the FAPI response shape
changed. Assign to Maggie. This is a diagnostic gap in the tripwire itself,
not a prod outage.
# Never
- Attempt a fix or touch code. You are a diagnostic tripwire only.
- Treat BLOCKED as FAIL (a network blip or FAPI shape change is not a prod
sign-in outage).
- Post anything on a PASS run.
- Probe staging (`prime-rooster-38.clerk.accounts.dev`) — `needs_client_trust`
is expected there and is not a regression.
- Open a duplicate while an OBJ-2532 comment or "Clerk auth tripwire" issue
already covers it.Why: Production Clerk password sign-in returns complete today only because device verification is off in the dashboard — a toggle anyone could flip at any time with zero code change. This tripwire reproduces the native client's exact FAPI sequence monthly so a needs_client_trust regression (OBJ-2532's defect N1: the frontend only handles needs_first_factor → complete and falls through to a misleading error on anything else) surfaces before real users hit a broken fresh install.
2d. Dave — Email canary liveness check
| Field | Value |
|---|---|
| Name | Dave — Email canary liveness check |
| Agent | Dave |
| Schedule | Daily 10:00 Chicago, no weekend skip · cron 0 10 * * * |
| Autopilot ID | d0b4e9f5-fae7-4db0-8bdf-3ed9833e35bd |
| Output mode | Run only (Dave decides whether to create or comment on an issue) |
Runbook (paste into the form):
# Goal
The daily email delivery canary (`email_canary`) is genuinely confirming delivery in BOTH
staging and production. If not, produce a diagnostic issue Maggie can route.
# Context
- /health exposes checks.email_canary (informational, fail-open at the HTTP layer — overall
status stays "ok" even when the canary is stale, BY DESIGN; see
rails_api/app/controllers/health_controller.rb:12-21, INFORMATIONAL_CHECKS). This autopilot
is the fail-closed alerting layer, mirroring the existing "Dave — Crono liveness check"
autopilot for the sibling `crono` informational check.
- `check_email_canary` (rails_api/app/controllers/health_controller.rb:189-205) returns one of
four statuses:
- "ok" — a confirmed canary delivery within EMAIL_CANARY_STALE_THRESHOLD_HOURS (default 26h).
- "stale" — no confirmed delivery within that window. Includes last_success_at when one is on
record.
- "not_configured" — EMAIL_CANARY_RECIPIENT is unset for that environment. The canary isn't
running there at all — a configuration gap, not a delivery outage.
- "error" — the check itself raised (e.g. Redis unreachable).
- The key can also be entirely absent from the JSON if that environment hasn't deployed the
email_canary check yet — same "not deployed here yet" shape as Crono's missing-key case.
- The canary's other escalation path is the real-time alert from
`Email::DeliveryCanaryVerifyJob` (rails_api/app/jobs/email/delivery_canary_verify_job.rb),
which fires Slack + Sentry on individual verify failures, each kind/environment combination
now carrying its own Sentry fingerprint (OBJ-2971 — see
[docs/operations/alerting.md § Email Delivery Canary](../operations/alerting.md#email-delivery-canary)).
That path is real but narrower than this daily sweep — it can only alert on the one send token
it's tracking per day. This autopilot is an independent, coarser-grained backstop that does not
depend on that job firing or classifying correctly. (A precedence rule for >1 subject-scan match
was considered under OBJ-2971 and dropped — Josh's live diagnosis traced the "N ambiguous
matches" symptom to production deploy skew, not a live defect on current master, so the scenario
doesn't occur once the deploy-skew fixes are live.)
- Background: production's canary bounced silently for at least three days (2026-08-27 through
2026-08-29) before a human caught it via a manual Mailtrap dashboard probe —
`/health` read `"email_canary":{"status":"stale",...}` with `"status":"ok"` overall and zero
paging effect the entire time (see OBJ-2967). This autopilot exists so that class of outage
surfaces within a day, same rationale as the Crono liveness check.
- URLs: resolve via `gcloud run services describe enkidu-api-staging` /
`enkidu-api-production --region us-central1 --project enkidu-488723
--format 'value(status.url)'` — identical resolution to the Crono liveness check.
# Credential preflight (OBJ-3564 / OBJ-3570)
Before the first `gcloud` call each cycle, re-sync gcloud CLI's own credential cache to the
`dave-autopilot` service account's current key. gcloud CLI imports a key file ONCE via
`gcloud auth activate-service-account` and never re-reads it on its own, so a key rotation that
updates the ADC file leaves gcloud CLI signing every JWT with an already-retired private key:
an immediate, 100%-reproducible `invalid_grant: Invalid JWT Signature` on every `gcloud`
subcommand until this runs (OBJ-3564, 2026-09-09). This mirrors
`scripts/multica-host/ensure-gcloud-sa-credential.sh` — call that script directly if this run's
workdir already has the enkidu repo checked out; otherwise run the equivalent inline:
`gcloud auth activate-service-account dave-autopilot@enkidu-488723.iam.gserviceaccount.com --key-file="$HOME/.config/dave-autopilot/key.json" --quiet`
Safe and cheap to run unconditionally every cycle — re-activating an already-current key is a
no-op in effect, no network round trip beyond the activation call itself. If this itself fails
(missing/unreadable key file, `client_email` mismatch, activation error), do not treat it as a
new condition — it's the same "curl/gcloud command itself failed" BLOCKED path in Step 1/2
below, not a new FAIL.
# Steps
1. (staging) `curl -sf --max-time 10 "$STAGING_URL/health"`, parse `.checks.email_canary` with
jq.
- PASS: `.status == "ok"`
- FAIL: `.status == "stale"` or `.status == "error"`
- BLOCKED: `.status == "not_configured"`, OR the `email_canary` key is absent from the JSON
entirely, OR the curl/gcloud command itself failed. None of these three are a delivery
outage — they mean "not running here" or "couldn't check," not "mail didn't arrive."
2. (production) Same check against `$PRODUCTION_URL/health`.
## Step 3 — Heartbeat ping (always, regardless of PASS/FAIL/BLOCKED)
This autopilot's own liveness matters independently of what its checks find — see
docs/operations/status-page.md#fail-closed-watchdogs-for-informational-checks. It fires
daily with no weekend skip, so a BetterStack heartbeat here detects the autopilot itself
going silent, not just an email_canary degradation.
After Steps 1–2 (PASS, FAIL, or BLOCKED — outcome does not matter):
`curl -fsS -m 10 "$BETTERSTACK_HEARTBEAT_URL_EMAIL_CANARY" || true`
- `$BETTERSTACK_HEARTBEAT_URL_EMAIL_CANARY` resolves from this agent's own `custom_env`
(Dave). Provisioned 2026-09-06 (OBJ-2978): BetterStack heartbeat "Email canary
liveness — Dave autopilot (OBJ-2978)", period 1 day, grace 4 hours. The ping fires at
the END of the run, so it attests "this autopilot ran to completion", not "the
scheduler is alive" — an early BLOCK yields no ping and therefore an alert. If the
variable is ever unset or empty, skip this curl silently; do not fail the run over a
missing heartbeat URL.
- This step is informational only — it must never affect the Decision step's
PASS/FAIL/BLOCKED classification for the email canary check itself.
# Decision
- All PASS → exit silently, no issue.
- Any FAIL → re-check that environment once after 60 seconds; if still failing:
- Dedupe first: search open issues for title containing "Email canary liveness"
(`multica issue list --status in_progress` and `--status todo`, or `--output json` and
filter). If one is already open, comment on it instead of creating a duplicate.
- If none found, also check whether an open issue already tracks this exact live incident by
a different title (e.g. a bounced/undelivered/stale production canary report — as of
2026-08-29 that's OBJ-2967). If one clearly covers the same incident, comment there instead
of opening a second issue for it.
- Otherwise create the issue with the environment, the exact `email_canary` JSON block, and
`last_success_at`. Assign to Maggie.
- Any BLOCKED (no FAIL) → same dedupe as above, then create/comment a BLOCKED issue noting which
env is `not_configured`, missing the key entirely, or which command failed. Say explicitly in
the issue body that this is BLOCKED, not FAIL — it does not mean mail failed to deliver.
# Never
- Attempt a fix. You're a diagnostic agent.
- Treat BLOCKED as FAIL — `not_configured` and an absent key both mean "not running here," not
"delivery failed."
- Create a duplicate while an "Email canary liveness" issue, or another open issue already
tracking this same live incident, exists.
- Skip weekends. Mail delivery doesn't pause on weekends — this check has NO weekend skip, same
as Crono's.Why: Production's email canary bounced silently for at least three days (2026-08-27 through 2026-08-29) before a human caught it via a manual Mailtrap dashboard probe — /health read email_canary: stale the entire time while overall status stayed "ok", since that check is intentionally fail-open at the HTTP layer (see OBJ-2967). This autopilot is the fail-closed layer, mirroring Dave — Crono liveness check for the sibling crono informational check.
2e. Dave — gc maintenance watchdog
| Field | Value |
|---|---|
| Name | Dave — gc maintenance watchdog |
| Agent | Dave |
| Schedule | Every 6 hours, :17 · cron 17 */6 * * * |
| Autopilot ID | b8558275-1db1-4655-b484-bd086ce7c83c |
| Status | Active (live status as of 2026-08-31) |
| Output mode | Run only (Dave decides whether to create or comment on an issue) |
Runbook (paste into the form):
# Goal
The shared enkidu git mirror's git maintenance is healthy. If a repo cache has
failed maintenance 3+ consecutive cycles, produce a diagnostic issue Maggie
can route.
# Context
- Background: OBJ-2449 traced recurring `repo checkout` brownouts (20–40 min
each) to silent per-cycle git maintenance failures (`reflog expire`,
`gc --prune`) on the shared enkidu mirror. The daemon logs one WRN per
failed command and nothing else escalates — `/health` and runtime status
stay green throughout. OBJ-2476 is this ticket (make the failure loud).
OBJ-2477 (separate, do not fix here) tracks the junk-remote bloat that
makes maintenance slow enough to time out.
- Log file: `~/.multica/daemon.log` (current, live file).
- Rotated logs: `~/.multica/daemon-*.log.gz`, one per rotation, filename
timestamped.
- Relevant log line shapes (all `component=daemon`):
- `INF gc: cycle complete cleaned=... orphaned=... skipped=... artifact_dirs=... artifact_removed=... codex_session_stores_reclaimed=... repo_caches_reclaimed=... bytes_reclaimed=... by_pattern=...`
— marks the end of one full gc sweep across every repo cache. Carries no
`repo=` field.
- `WRN gc: git maintenance failed repo=<path> command="<cmd>" output="<...>" error="<...>"`
— one line per failed git command. A single cycle can log up to two of
these for the same repo (`reflog expire --expire=30.days --all`, then
`gc --prune=30.days`), since each command is attempted and can fail
independently.
- A **cycle** = every log line between one `gc: cycle complete` line and the
next. A cycle **fails** for a given `repo=` path if it contains at least
one `git maintenance failed` line naming that path; otherwise it passes
for that path.
- No repo checkout in this runbook, ever. A checkout mid-brownout is exactly
the failure mode this watches for and would hang the watchdog too.
# Steps
1. Read `~/.multica/daemon.log`. Check its first timestamp: if it covers
less than 24h (i.e. the oldest line is less than 24h before now), also
decompress the single most-recently-rotated `~/.multica/daemon-*.log.gz`
(pick the newest by the timestamp embedded in its filename) and prepend
its content — a cycle can span the rotation boundary, and skipping the
rotated file undercounts.
2. Walk the combined log in chronological order and segment it into cycles
at each `gc: cycle complete` line, per the Context definition above.
3. For every distinct `repo=` path seen in any `git maintenance failed`
line, classify each cycle as failing or passing for that repo.
4. Starting from the most recent cycle and walking backward, count
consecutive failing cycles per repo, stopping at the first passing cycle
(or the start of the available log). This is `consecutive_failures` for
that repo.
5. For every repo where `consecutive_failures >= 3`, collect: the repo
path, the `consecutive_failures` count, the distinct `command=` and
`error=` values seen across those failing cycles, the first and last
failure timestamps in the streak, and the current mirror size
(`du -sh <repo path>`).
# Decision
- No repo reaches 3 consecutive failing cycles: exit silently. No issue
created.
- One or more repos reach the threshold:
- **Dedupe first.** Search open issues for a title starting with
`[gc-watchdog]`. If one is already open, comment on it with this run's
numbers (repo, count, commands, errors, timestamps, mirror size)
instead of creating a duplicate.
- Otherwise, create **one** issue covering every tripped repo this run
(not one issue per repo) titled
`[gc-watchdog] Multica git maintenance failing — {{date}}`, assign it
to Maggie, and include the per-repo detail from Step 5 plus a pointer
to OBJ-2476 (this ticket, silent-failure fix) and OBJ-2477 (bloat
cause) for context.
# Never
- Attempt a fix. You're a diagnostic agent — the bloat/timeout root cause
is tracked separately as OBJ-2477.
- Check out any repository, or run any git command against the mirror
other than `du -sh` for size reporting.
- Create a duplicate issue while a `[gc-watchdog]`-titled issue is open —
always search and comment on the existing one first.
- Skip the rotated-log read when the live log is under 24h old — a cycle
spanning the rotation boundary undercounts without it.Why: OBJ-2449 traced recurring repo checkout brownouts (20–40 minutes each) to silent per-cycle git maintenance failures on the shared enkidu mirror — the daemon logs one WRN per failed command and nothing else escalates, so /health and runtime status stay green throughout. This watchdog is OBJ-2476's fix: it makes 3+ consecutive maintenance failures on the same repo cache loud instead of silent. It's active on the live autopilot (as of 2026-08-31).
2f. Dave — Autopilot run failure sweep
| Field | Value |
|---|---|
| Name | Dave — Autopilot run failure sweep |
| Agent | Dave |
| Schedule | Every 6 hours, :47 · cron 47 */6 * * * |
| Autopilot ID | a43b4c1d-1fd4-404c-86e8-fec51ce3dbf9 |
| Status | Active (live status as of 2026-09-03) |
| Output mode | Run only (Dave decides whether to create or comment on an issue) |
Runbook (paste into the form):
# Goal
Nothing in this workspace reads `multica autopilot runs` for non-completed runs.
Several autopilots are the only alerting path for their signal (Crono liveness,
email canary, email suppression, Clerk tripwire, gc maintenance watchdog) — if
one of those fails silently, nothing notices. This autopilot is that fleet-wide
watchdog: read every active autopilot's recent run history, classify the
non-completed runs, and surface only the ones that actually need a human.
# Context
- Background: OBJ-3279. A 2026-09-02 17:17 UTC run of `Dave — gc maintenance
watchdog` died on a session-limit failure, then completed normally at 23:17
the same day. Nothing read the failed run in between — the fail-then-succeed
pattern is the reason this runbook reads `runs`, not `last_run_status`, below.
- **Why `runs`, not `list`'s `last_run_status`:** `last_run_status` reflects
only the most recent run for an autopilot. An autopilot that fails then
succeeds on its next fire shows `last_run_status: completed` and the
intermediate failure is invisible. `multica autopilot list --output json` is
still the right way to enumerate the fleet (it returns every active
autopilot's `id`); the per-autopilot fault detector has to be
`multica autopilot runs <id> --output json`, read in full for the lookback
window, not just the newest row.
- **`status: "failed"` is not ground truth for `issue blocked` runs.** A run
fails with `failure_reason: "issue blocked"` when the run stalled waiting on
a human. The run row never updates when that stall clears — so a run that
blocked, got unblocked, and finished days ago still shows `status: "failed"`
forever. The linked `issue_id`'s *current* status is the real signal, not
the run's `status` field. Always look up `issue_id` via
`multica issue get <issue_id> --output json` before deciding whether an
`issue blocked` run is still live.
- **Real infrastructure faults are correlated, not per-autopilot.** A single
expired-credential event or a single host-down event can hit many autopilots
in the same window with the same `failure_reason`. Group findings by
`(failure_reason, day)` before reporting — one incident is one line, not N.
- **Watchdog-class autopilots** (live IDs, current as of 2026-09-03 — re-verify
against `multica autopilot list --output json` if a title changes):
- `acf44638-afa2-4f17-83c5-984b14c64819` — Dave — Crono liveness check
- `d0b4e9f5-fae7-4db0-8bdf-3ed9833e35bd` — Dave — Email canary liveness check
- `f142b921-2d91-4f5e-8d44-b58f540608d2` — Dave — Email suppression liveness check
- `7fefe2ef-d94a-487d-ae37-447ca3461446` — Dave — Clerk auth tripwire
- `b8558275-1db1-4655-b484-bd086ce7c83c` — Dave — gc maintenance watchdog
These are single-point alerting paths for their signal — any non-completed
run on one of these gets a dedicated issue, not just a digest line.
- No repo checkout in this runbook, ever. It reads only Multica CLI state.
# Steps
0. **Self-check (OBJ-3204 Part B pattern).** Run
`multica autopilot runs <this-autopilot-id> --limit 3 --output json` (this
autopilot's own ID — read it from the run's own dispatch context, or via
`multica autopilot list --output json` filtered to title
`Dave — Autopilot run failure sweep`). Look at the entry immediately before
this run.
- If that entry's `status` is `"completed"`: no gap to report, continue.
- If it is not `"completed"`, or if it is missing (this run is more than
~7h after the previous one, implying a skipped fire): note "⚠️ Sweep gap:
previous run (`<triggered_at>`) did not complete cleanly
(`<status>`/`<failure_reason>`)" for the Tier 2 digest in Step 5. The
sweep cannot fully watch itself — this is a partial mitigation, not a
guarantee (it runs on the same quota it monitors). **Confirmed, not just
theoretical:** OBJ-3927 (2026-09-16/17) — see the "Confirmed occurrence"
section of
[Multica Shared-Runtime Quota Gotcha](../operations/multica-shared-runtime-quota-gotcha.md).
1. Enumerate the fleet: `multica autopilot list --output json`, filtered to
`status: "active"`. This is the enumeration source only — do not trust its
`last_run_status` field for anything (see Context).
2. For each active autopilot (including this one), fetch
`multica autopilot runs <id> --limit 10 --output json` and keep only
entries whose `triggered_at` falls within the last 12 hours (this cadence
is `47 */6 * * *` — every 6h — so a 12h lookback means every run is seen by
two consecutive sweeps; a missed sweep does not lose a failure).
3. For every non-completed run (`status` is `"failed"` or `"skipped"`) found
in Step 2, classify using the Decision rules below. Skip any run whose
`status` is `"completed"`.
# Decision — classification
For each non-completed run collected in Step 2, first apply the ground-truth
correction, then sort into exactly one tier.
**Ground-truth correction (apply before tiering):** if `failure_reason` is
`"issue blocked"`, fetch `multica issue get <issue_id> --output json`. If that
issue's `status` is `"done"` or `"in_review"`, treat this run as resolved —
route it to Tier 1, not Tier 2 or 3, regardless of how stale the run row looks.
**Tier 1 — suppress silently (count only, no issue/comment):**
- Quota failures (`failure_reason` matching `session limit` or `weekly limit`,
case-insensitive substring) where a *later* run of the *same* autopilot in
the lookback window has `status: "completed"`.
- `failure_reason: "issue cancelled"`.
- `issue blocked` runs whose linked issue is now `done` or `in_review` (see
ground-truth correction above).
**Tier 2 — one aggregated digest issue (no watchdog-class autopilot involved):**
- Quota failures with no later completed run yet in the lookback window.
- `status: "skipped"` runs (e.g. `agent runtime is offline at dispatch time`).
- `issue blocked` runs whose linked issue is still open (not `done`/`in_review`).
- Any Step 0 self-check gap.
- **Group by `(failure_reason, autopilot title or "self", day)`** — one line
per distinct group, listing which autopilot(s)/run(s) it covers, not one
line per run.
- If any Tier 2 findings exist, dedupe first (see Dedupe below), then create
or comment on one issue titled `Autopilot fleet check — <YYYY-MM-DD>`
(UTC date of this run), assigned to Maggie, body = one bullet per grouped
line from above (failure_reason, affected autopilot(s), run count, first/last
`triggered_at` in the group).
**Tier 3 — dedicated issue per finding, assigned to Maggie, flagged for Dave:**
- Any non-completed run (any `failure_reason`, any tier-1/tier-2 pattern
included) on a **watchdog-class autopilot** (the five IDs in Context) —
these are single-point alerting paths and get a dedicated issue even for a
quota failure that later self-resolved.
- `failure_reason: "task expired in queue"`.
- Auth failures (`failure_reason` matching `401`, `Failed to authenticate`,
case-insensitive substring) — group same-day occurrences across autopilots
into ONE Tier 3 issue (correlated credential event), not one per autopilot.
- Any `failure_reason` not matching a pattern named anywhere in this runbook
(unrecognized reason) — report it verbatim, don't guess a tier for future
wording without flagging it as unrecognized. **Check this rule
independently of, and before, the ≥3-occurrence rule below** — an
unrecognized reason is Tier 3 on its first sighting, unconditionally,
whether or not it later correlates with anything else.
- **≥2 consecutive non-completed runs** (in the 12h lookback) on a
watchdog-class autopilot, or **≥3 consecutive** on any other autopilot.
- For each Tier 3 finding, dedupe first (see Dedupe below), then create or
comment on one issue titled
`Autopilot failure: <autopilot title> — <YYYY-MM-DD>` (UTC date of this
run), assigned to Maggie, body states the autopilot, the run's `id`,
`failure_reason`, `triggered_at`, why it's Tier 3, and a note this needs
Dave's attention.
- A single correlated event (same `failure_reason`, same day, multiple
autopilots) is still ONE Tier 3 issue covering every affected autopilot —
never one issue per autopilot for the same incident.
**Dedupe (stateless — no standing log issue, ever):** before creating any
Tier 2 or Tier 3 issue, search recent issues
(`multica issue list --sort created_at --direction desc --limit 30 --output
json`, or narrower by title prefix) for one already covering this run's `id`
or this exact `(failure_reason, day)` / `(autopilot, day)` group. If found,
comment on it with this run's data instead of creating a duplicate. **Never**
create a single never-closing log issue that this autopilot keeps appending
to across cycles — this workspace has a documented disk-leak hazard where
issues that never reach `done` leak task-directory disk indefinitely
(`docs/operations/multica-daemon-disk-hygiene.md#root-cause`). Each day's
finding is its own issue (or a same-day comment on one already open), and it
is expected to eventually close like any other issue.
**Clean run:** if Step 2/3 produce zero non-completed runs in the lookback
window (after the ground-truth correction) and Step 0 found no self-check
gap, produce nothing — no issue, no comment. This is the expected common case.
# Never
- Trust `last_run_status` from `multica autopilot list` as a fault signal —
always read `multica autopilot runs <id>` for the lookback window.
- Report an `issue blocked` run as live without first checking the linked
`issue_id`'s current status via `multica issue get`.
- File one issue per autopilot for what is actually one correlated
infrastructure event (same `failure_reason`, same day, multiple
autopilots) — group it into a single issue.
- Create a standing/never-closing log issue for this autopilot's findings —
dedupe against recent issues by content, let each day's issue close
normally.
- Attempt to fix, restart, or reconfigure any autopilot or agent found
failing. This is a diagnostic sweep only — findings route to Maggie.
- Check out a repository or run any git command. This runbook only reads
Multica CLI state.Why: OBJ-3279. Nothing read multica autopilot runs for non-completed runs, and several autopilots (Crono liveness, email canary, email suppression, Clerk tripwire, gc maintenance watchdog) are the only alerting path for their signal — the 2026-09-02 17:17 UTC gc-watchdog session-limit failure that self-resolved at 23:17 the same day went unnoticed the whole way through. This sweep closes that hole: it bounds worst-case detection latency at 6h, reads runs instead of the misleading last_run_status, corrects issue blocked runs against the linked issue's current status, and groups correlated failures into one issue instead of filing one per run. It's run_only — Dave still decides what, if anything, gets filed — and it's stateless by design: no standing log issue, per the task-dir disk-leak hazard documented in Multica Daemon Disk Hygiene. See Multica Autopilot Run-Status Gotcha for the full run-status-semantics writeup this runbook's Context section is built on.
2g. Dave — Email suppression liveness check
| Field | Value |
|---|---|
| Name | Dave — Email suppression liveness check |
| Agent | Dave |
| Schedule | Daily 14:00 · cron 0 14 * * * — timezone UTC, unlike every other Dave autopilot in this doc, which run on America/Chicago |
| Autopilot ID | f142b921-2d91-4f5e-8d44-b58f540608d2 |
| Output mode | Run only (Dave decides whether to create or comment on an issue) |
Runbook (paste into the form):
# Goal
A short watchlist of known-critical addresses (demo@objectuve.com, canary@objectuve.com) is
NOT currently on Mailtrap's suppression list in production. If one is, produce a diagnostic
issue Maggie can route.
# Context
- /health now exposes checks.email_suppressions (informational, fail-open at the HTTP layer —
overall status stays "ok" even when an address is suppressed, BY DESIGN; see
rails_api/app/controllers/health_controller.rb, INFORMATIONAL_CHECKS). This autopilot is the
fail-closed alerting layer, mirroring the existing "Dave — Email canary liveness check"
autopilot for the sibling `email_canary` informational check.
- `check_email_suppressions` returns one of five statuses:
- "ok" — none of WATCHED_SUPPRESSION_EMAILS are currently suppressed.
- "suppressed" — at least one watched address is on Mailtrap's suppression list right now.
Includes an `emails` array naming which one(s).
- "not_configured" — MAILTRAP_API_TOKEN is unset for that environment. The check isn't
running there at all — a configuration gap, not a live suppression.
- "not_applicable" — staging's Mailtrap sandbox (OBJ-2939) never touches the real suppression
list, so the check is structurally meaningless there. Expected on staging, not a regression.
- "error" — the check itself raised, or Mailtrap's API returned something unexpected.
- The key can also be entirely absent from the JSON if that environment hasn't deployed the
email_suppressions check yet — same "not deployed here yet" shape as Crono's/canary's
missing-key case.
- Background: demo@objectuve.com sat hard-bounce-suppressed on Mailtrap for ~7 weeks
(2026-07-12 through this issue's creation) before a human caught it via a manual dashboard
probe — the same blind spot that separately hid canary@objectuve.com's own suppression
(OBJ-2967). This autopilot exists so that class of outage surfaces within a day, same
rationale as the Crono liveness check and the email canary liveness check.
- URLs: resolve via `gcloud run services describe enkidu-api-staging` /
`enkidu-api-production --region us-central1 --project enkidu-488723
--format 'value(status.url)'` — identical resolution to the sibling checks.
# Credential preflight (OBJ-3564 / OBJ-3570)
Before the first `gcloud` call each cycle, re-sync gcloud CLI's own credential cache to the
`dave-autopilot` service account's current key. gcloud CLI imports a key file ONCE via
`gcloud auth activate-service-account` and never re-reads it on its own, so a key rotation that
updates the ADC file leaves gcloud CLI signing every JWT with an already-retired private key:
an immediate, 100%-reproducible `invalid_grant: Invalid JWT Signature` on every `gcloud`
subcommand until this runs (OBJ-3564, 2026-09-09). This mirrors
`scripts/multica-host/ensure-gcloud-sa-credential.sh` — call that script directly if this run's
workdir already has the enkidu repo checked out; otherwise run the equivalent inline:
`gcloud auth activate-service-account dave-autopilot@enkidu-488723.iam.gserviceaccount.com --key-file="$HOME/.config/dave-autopilot/key.json" --quiet`
Safe and cheap to run unconditionally every cycle — re-activating an already-current key is a
no-op in effect, no network round trip beyond the activation call itself. If this itself fails
(missing/unreadable key file, `client_email` mismatch, activation error), do not treat it as a
new condition — it's the same "curl/gcloud command itself failed" BLOCKED path in Step 1/2
below, not a new FAIL. See the
[gcloud CLI credential cache gotcha](../development/gotchas.md#gcloud-auth-activate-service-account-and-adcs-google_application_credentials-are-two-independent-credential-stores).
# Steps
1. (staging) `curl -sf --max-time 10 "$STAGING_URL/health"`, parse `.checks.email_suppressions`
with jq.
- PASS: `.status == "ok"` or `.status == "not_applicable"` (expected on staging).
- FAIL: `.status == "suppressed"` or `.status == "error"`.
- BLOCKED: `.status == "not_configured"`, OR the `email_suppressions` key is absent from the
JSON entirely, OR the curl/gcloud command itself failed. None of these three mean a live
suppression — they mean "not running here" or "couldn't check."
2. (production) Same check against `$PRODUCTION_URL/health`. On production, `not_applicable`
would be unexpected (production is never sandboxed) — treat it as BLOCKED, not PASS, and say
so explicitly in any issue you file.
## Step 3 — Heartbeat ping (always, regardless of PASS/FAIL/BLOCKED)
This autopilot's own liveness matters independently of what its checks find — see
docs/operations/status-page.md#fail-closed-watchdogs-for-informational-checks. It fires
daily with no weekend skip, so a BetterStack heartbeat here detects the autopilot itself
going silent, not just an email_suppressions degradation.
After Steps 1–2 (PASS, FAIL, or BLOCKED — outcome does not matter):
`curl -fsS -m 10 "$BETTERSTACK_HEARTBEAT_URL_EMAIL_SUPPRESSION" || true`
- `$BETTERSTACK_HEARTBEAT_URL_EMAIL_SUPPRESSION` resolves from this agent's own
`custom_env` (Dave). Provisioned 2026-09-06 (OBJ-2978): BetterStack heartbeat "Email
suppression liveness — Dave autopilot (OBJ-2978)", period 1 day, grace 4 hours. The
ping fires at the END of the run, so it attests "this autopilot ran to completion",
not "the scheduler is alive" — an early BLOCK yields no ping and therefore an alert.
If the variable is ever unset or empty, skip this curl silently; do not fail the run
over a missing heartbeat URL.
- This step is informational only — it must never affect the Decision step's
PASS/FAIL/BLOCKED classification for the email suppression check itself.
- A BLOCKED outcome still pings this heartbeat, same as PASS — that's intentional, not a gap.
The heartbeat answers "did this autopilot run to completion," not "did its checks find
anything"; a BLOCKED run genuinely did run. The Decision step below already files or comments
a BLOCKED issue on every BLOCKED outcome, so a human is not relying on the heartbeat alone to
notice one (OBJ-3560).
# Decision
- All PASS (or expected-BLOCKED per above) → exit silently, no issue.
- Any FAIL → re-check that environment once after 60 seconds; if still failing:
- Dedupe first: search open issues for title containing "Email suppression sweep"
(`multica issue list --status in_progress` and `--status todo`, or `--output json` and
filter). If one is already open, comment on it instead of creating a duplicate.
- If none found, also check whether an open issue already tracks this exact live incident by
a different title (e.g. OBJ-2967, OBJ-2981 — the two prior suppression incidents this
autopilot exists to catch going forward). If one clearly covers the same incident, comment
there instead of opening a second issue for it.
- Otherwise create the issue with the environment, the exact `email_suppressions` JSON block
(including the `emails` array), and a link to the Mailtrap suppression dashboard
(https://mailtrap.io/suppressions). Assign to Maggie.
- Any BLOCKED (no FAIL) → same dedupe as above, then create/comment a BLOCKED issue noting which
env is `not_configured`, missing the key entirely, unexpectedly `not_applicable` on
production, or which command failed. Say explicitly in the issue body that this is BLOCKED,
not FAIL — it does not mean an address is suppressed.
# Never
- Attempt a fix. You're a diagnostic agent. Remediation is a human reactivating the address on
Mailtrap's suppression list (`https://mailtrap.io/suppressions` → Reactivate) — no crew
automation has suppression-list write access (`mailtrap-api-token` IAM gap, tracked in
OBJ-2967).
- Treat BLOCKED as FAIL — `not_configured` and an absent key both mean "not running here," not
"an address is suppressed." A `not_applicable` on staging is also expected, not a FAIL — only
flag it as BLOCKED if it shows up on production.
- Create a duplicate while an "Email suppression sweep" issue, or another open issue already
tracking this same live incident, exists.
- Skip weekends. Mail delivery doesn't pause on weekends — this check has NO weekend skip, same
as Crono's and the email canary's.Why: demo@objectuve.com sat hard-bounce-suppressed on Mailtrap for ~7 weeks (2026-07-12 through this autopilot's creation) before a human caught it via a manual dashboard probe — the same blind spot that separately hid canary@objectuve.com's own suppression (OBJ-2967). This autopilot is the fail-closed layer, mirroring Dave — Email canary liveness check for the sibling email_canary informational check. Live since 2026-08-29; BetterStack heartbeat provisioned 2026-09-06 (OBJ-2978 Task 2).
3. Maggie — Backlog sweep
| Field | Value |
|---|---|
| Name | Maggie — Backlog sweep |
| Agent | Maggie |
| Schedule | Weekdays 09:00 and 13:00 Chicago · cron 0 9,13 * * 1-5 |
| Output mode | Run only, with one narrow, deliberate exception — Pass 2 creates and maintains a single standing Backlog ledger issue (assigned to Maggie) for its own run-to-run accounting. That's infrastructure bookkeeping, not a unit of crew work; Pass 1 still never creates issues. |
Runbook (paste into the form):
# Goal
Every `todo` issue in the workspace that's assigned to a human (or
unassigned) gets classified and routed to the right crew agent — without
Josh having to dispatch it manually (Pass 1). On top of that, the
`backlog`-status queue gets triaged and pulled into the chain at a
WIP-limited pace, so real work moves without flooding the crew (Pass 2).
# Context
- Crew: Orion, Desi, Codi, Riley, Roy, Dave, Tess, Vicki, Dori. Your routing
table and hard rules live in your Multica agent instructions — follow
them exactly.
- You NEVER route to yourself. Pass 1 never creates a new issue — only
reassigns + comments on EXISTING ones. Pass 2's `Backlog ledger` issue is
the ONE deliberate, narrow exception — see Pass 2, Step 10.
- **WIP** = count of issues in status `todo`/`in_progress`/`in_review`
currently assigned to one of the nine crew agents (Orion, Desi, Codi,
Riley, Roy, Dave, Tess, Vicki, Dori). Issues assigned to Maggie or to a
human are parked, not crew capacity — do NOT count them. Exclude the
parent milestone coordination anchor (permanently Orion-held
`in_progress` for the life of a milestone) — counting it would eat a
slot forever. Dispatched phase children (Orion-created sub-issues once
assigned to Desi/Codi) DO count toward WIP — the anchor is the only
exclusion; this is separate from the `EXCLUDED — milestone phase child`
pull-eligibility bucket in Step 6, which is about a different thing.
- Target WIP is 2.
# Steps
## Pass 1 — route committed work (`todo` issues)
1. Query Multica for issues in this workspace with status=todo.
2. Filter to issues where assignee is NOT already one of: Orion, Desi,
Codi, Riley, Roy, Dave, Tess, Vicki, Dori.
3. For each filtered issue:
a. Read title, description, and the latest comment.
b. Cancel-on-sight: release-bookkeeping. If the title matches
`chore(release): update CHANGELOG` (or is otherwise just a
version-bump of the CHANGELOG header), do NOT route it. The
`[Unreleased]→[version]` rename is a `scripts/release.sh`
side-effect, not crew work. Comment that you're cancelling it
(CHANGELOG version-bump is automated by release.sh at ship time;
writing CHANGELOG *content* under [Unreleased] stays with Dori),
then set status `cancelled`. Move on.
c. Otherwise classify per your routing table.
d. If routable with confidence: follow your standard Final Action
block (post routing-decision comment → status=in_progress →
assign to the chosen agent).
e. If ambiguous: post ONE clarifying comment and leave the issue as
is. Do not guess and route.
4. When every `todo` issue has been processed, move to Pass 2.
## Pass 2 — backlog triage + WIP-limited intake (`backlog` issues)
5. **Compute WIP.** Count open issues (`todo`/`in_progress`/`in_review`)
assigned to one of the nine crew agents, excluding any permanent
milestone parent coordination-anchor issue. Dispatched phase children
(Orion-created sub-issues once assigned to Desi/Codi) DO count toward
this — the coordination anchor is the only exclusion. This is a
different exclusion than the `EXCLUDED — milestone phase child` bucket
in Step 6 below, which is about pull *eligibility* for backlog items,
not the WIP *count* — don't conflate the two. This is `WIP_before`.
6. **Bucket every `backlog`-status issue.** First, skip the ledger
itself — an issue titled exactly `Backlog ledger` is this autopilot's
own bookkeeping, never crew work. It is Maggie-assigned and sits in
`backlog`, which is exactly the shape Step 6 would otherwise bucket
and Step 9 would route away to a crew agent, destroying the standing
issue the ledger depends on. Skip it in BOTH passes, always.
Then bucket everything else. Every remaining item must land in
exactly one bucket each run, so nothing is silently unaccounted for:
- `EXCLUDED — milestone phase child`: has `parent_issue_id` set, or
belongs to a milestone project. Orion dispatches these, never the
sweep — they are already committed work awaiting their phase.
- `EXCLUDED — Penny/Sage proposal awaiting greenlight`: key this on
the CREATOR (Penny or Sage, `creator_type: agent`), NOT on the
assignee. Proposals assigned to Roy are the obvious case, but
legacy proposals stranded in `backlog` assigned to Maggie are the
common one — they carry no `parent_issue_id` and no `project_id`,
so an assignee-keyed check misses them entirely and they fall
through into the pullable pool. Auto-routing one of these breaks
the hard rule that a Penny PRD / Sage architecture proposal never
enters the chain without an explicit human greenlight. Surface
them in the ledger under this bucket; never pull them.
- `EXCLUDED — awaiting human triage`: feedback-ingest items and
anything already held for Josh.
- `EXCLUDED — stale 90+ days`: owned by the sibling
`Maggie — Backlog grooming` autopilot — do not duplicate its
keep/cancel recommendations here.
- `READY`: scoped enough to route now (clear ask, single domain,
acceptance criteria discernible). `READY` is necessary but NOT
sufficient for direct-to-executor routing — it means "we know what
this is," not "this is trivial." Apply the full routing table
including its trivial-fix bar: anything that isn't genuinely
trivial still goes to Orion for packaging even when `READY`.
Handing Codi a clear-but-unpackaged feature is the failure this
bucket is most likely to cause.
- `NEEDS SCOPING`: real work but vague — eligible for intake, but
routes to Orion and counts against the cap identically to `READY`.
**At most ONE `NEEDS SCOPING` item per run**, even when the WIP
budget would allow more. These are the items whose true size is
unknown until Orion investigates, and one of them can turn out to
be epic-sized and trigger a full milestone kickoff; three at once
is how a sweep quietly becomes a planning avalanche.
- `DUPLICATE / SUPERSEDED`: surface with the superseding issue key and
a recommend-cancel note. Do NOT cancel it yourself — surfacing only.
7. **Cap the pull.** Target WIP is 2. Pull `max(0, 2 - WIP_before)` items
from `READY` + `NEEDS SCOPING`, and never more than 2 in a single run.
If `WIP_before >= 2`, pull nothing this run and say so in the ledger.
8. **Rank candidates** for which `READY`/`NEEDS SCOPING` items get pulled,
applied in strict order: (1) unblocks in-flight work — an open
crew-held issue references it as a dependency; (2) `priority` is high
or urgent; (3) smallest fully-scoped item first (cheap wins clear the
board); (4) oldest `created_at` as tiebreak. Live `labels` are empty
across the backlog today — rank off `priority`, `project_id`,
`parent_issue_id`, age, and issue text, not labels.
- **Epic-sized items consume the whole budget:** if a candidate trips
Orion's epic threshold (≥3 Codi tasks or ≥2 domains, judged from its
description), pull it only when `WIP_before` is 0, and pull it alone
this run.
9. **Route pulled items** exactly as Pass 1 does: classify per your
routing table (`NEEDS SCOPING` items always route to Orion), then run
your standard Final Action block (routing comment → status `todo` or
`in_progress` per the normal convention → assign the chosen agent).
- **If a pulled item is ALREADY assigned to a crew agent** (some
`backlog` items sit with Tess or Dori with no `parent_issue_id` and
no `project_id`), do not reassign it away from them. Honor the
existing owner — activate it in place by moving status only, and
note the pre-existing assignee in the ledger. Reassigning someone
else's claimed work is how two agents end up on one issue.
10. **Post the ledger.** Each run posts the accounting to ONE standing
issue titled `Backlog ledger`. Dedupe first — search open issues for
that title; if one is already open, comment onto it. Only if none is
open, create it once, assigned to Maggie. This is a deliberate,
narrow exception to "never create issues from this autopilot" —
the ledger is infrastructure for this autopilot's own accounting, not
a unit of crew work. The ledger comment carries: WIP before/after, how
many items were pulled and which, bucket counts, the full
`DUPLICATE / SUPERSEDED` list, and anything that changed bucket since
the previous run.
# Skip this run if
Skip Pass 1 if no `todo` issues exist, or all `todo` issues are already
assigned to crew agents. Skip Pass 2 if no `backlog`-status issues exist.
Skip the entire run only if both conditions hold — otherwise run whichever
pass has work.
# Never
- Route to yourself.
- Reopen or alter issues in other statuses.
- Create new issues from this autopilot, except the one-time `Backlog
ledger` issue (Step 10) — every other action is reassignment or
cancellation onto EXISTING issues.
- Cancel anything other than a `chore(release): update CHANGELOG`
version-bump issue (Pass 1, Step 3b). Every other `todo` either routes
or gets one clarifying question — never a silent close. A `backlog`
item flagged `DUPLICATE / SUPERSEDED` is surfaced with a
recommend-cancel note only — never cancelled by this autopilot.
- Ask more than one clarifying question per issue per run.
- Pull a milestone phase child (`parent_issue_id` set, or belongs to a
milestone project) — that's Orion's dispatch, never the sweep's.
- Exceed the WIP cap — never pull more than `max(0, 2 - WIP_before)`
items, and never more than 2 in one run.
- Duplicate `Maggie — Backlog grooming`'s stale-item keep/cancel verdicts.
- Count an issue assigned to Maggie or to a human as crew WIP.
- Pull a Penny- or Sage-created proposal, whoever it's currently
assigned to — the human greenlight gate is not optional.
- Reassign a `backlog` item that a crew agent already owns.
- Bucket, route, or cancel the `Backlog ledger` issue itself.
- Re-pull an item that a previous run already pulled and that came back
to `backlog`. Check the ledger's pull history first. An item that
bounced once needs a human or Orion to look at why — pulling it again
on the next run just burns a slot forever. Surface it in the ledger
under `BOUNCED — needs a look` instead.
- Pull more than one `NEEDS SCOPING` item in a single run.Why: You currently have to remember to assign Maggie to every incoming issue before the chain starts. This makes her actively pull from the board twice a day.
3b. Maggie — Issue health sweep
| Field | Value |
|---|---|
| Name | Maggie — Issue health sweep |
| Agent | Maggie |
| Schedule | Hourly at :30 Chicago · cron 30 * * * * |
| Output mode | Run only (Maggie nudges / escalates existing issues; never creates new ones) |
Runbook (paste into the form):
# Goal
One hourly pass over active issues that catches BOTH failure modes the crew
suffers from: SILENCE (an issue stalls with no activity) and THRASH (an issue
churns through many fix-rounds without converging). The job is to get work
MOVING again — wake the assignee with useful context. Blocking an issue is
NOT an outcome this sweep produces: a blocked graveyard is strictly worse
than a stall, because the Blocked review pass runs only daily and only
keeps declared dependencies honest; sweep-parking an issue in blocked still
hides it from the hourly loop.
# Context
- Crew agents in scope: Orion, Desi, Codi, Riley, Roy, Dave, Tess, Vicki, Dori,
and yourself (Maggie). Skip Penny / Sage — proposers run on their own schedule.
- **Wake mechanism = `multica issue rerun <ID>`** — re-enqueues the issue's
current assignment as a fresh task. Verified 2026-07-04: the task went
queued→running within seconds, regardless of issue status. Re-assigning an
issue to the SAME agent dispatches NOTHING (verified 2026-07-04: 3/3
nudge-reassignments created zero tasks) — never use it as a wake.
- Ground truth for "did the wake work" = `multica issue runs <ID>` (the rerun
task's status) PLUS assignee-authored activity.
- **"Assignee activity"** = a comment, commit, status change, or PR event
authored by the issue's ASSIGNEE. Sweep-marker comments never count, even
when you (Maggie) are both sweeper and assignee of the same issue — in that
case only your real work comments/commits count, never your own markers.
Your comments bump `updated_at`, so NEVER judge by `updated_at` alone.
- You NEVER create issues from this autopilot. Comment / rerun / close-report /
escalate only. You run with concurrency 1, so keep this pass efficient.
- Dedup markers, each used ONLY for its purpose (they are how you detect prior
action): `**Stall nudge:**`, `**Stall escalation:**`, `**Round cap:**`,
`**Sweep close:**`.
# Steps
1. Query issues in {in_progress, in_review}:
`multica issue list --status in_progress --output json`
`multica issue list --status in_review --output json`
2. Filter: keep only issues assigned to a crew agent (incl. yourself). Skip —
logging each — issues with NO assignee, issues assigned to Penny/Sage or a
human, and issues labeled `no-sweep` (coordination anchors, human-gated
issues, and long-running watches carry that label).
3. For EACH issue, establish two timestamps before acting:
- **T_agent** = the assignee's most recent activity (per the "Assignee
activity" definition). If the assignee has NEVER acted on the issue,
T_agent = the time the issue was assigned to them.
- **T_mine** = the timestamp of your most recent `**Stall nudge:**` or
`**Stall escalation:**` on this issue (may be absent).
A **stall episode** starts when T_agent goes stale and ends ONLY when the
assignee actually acts (T_agent becomes newer than T_mine). Time passing
never ends an episode and never re-opens a spent nudge budget.
Then run these checks in order:
=== Pre-check 1 — DONE-BUT-OPEN (autopilot report issues only) ===
Applies ONLY when BOTH hold:
(i) the issue's title matches the `issue_title_template` of one of this
workspace's create_issue autopilots (fetch once per pass:
`multica autopilot list --output json`; interpolate `{{date}}`) —
title resemblance alone is NOT a match; and
(ii) the assignee's last comment unambiguously reads as that run's final
result with nothing left to advance.
Then post `**Sweep close:** result was delivered at <time>; nothing left`
` to advance, closing.` and `multica issue status <ID> done`. Done with
this issue. If EITHER condition is uncertain, do NOT close — fall
through to the normal checks. Never close a work issue this way.
=== Pre-check 2 — WAIT-STATE ===
The assignee's last comment explicitly declares a wait. The stall
threshold depends on WHAT it waits on, and this intentionally
suppresses Check A too (never re-route an issue that is legitimately
waiting):
- **CI / PR-checks / workflow-run wait → 2 HOURS.** CI concludes in
minutes; the `Riley — CI wake` webhook autopilot is the PRIMARY wake
(fired by `.github/workflows/multica-ci-wake.yml` when all PR checks
conclude) — this sweep is only its backstop for missed pings.
- **Any other wait** (human decision, Monitor notification, scheduled
window, external event) → 24 HOURS.
Under the threshold → skip. Past it → the wait signal is probably
lost; continue below and QUOTE the declared wait in your nudge so the
woken agent re-checks the thing it was waiting on.
=== Check A — THRASH (fix-round cap) ===
Read the branch/PR commit history (this check is about commits, not the
comment thread). Count fix-round commits: subject starts with `fix(` or
`test(smoke)`, or carries an explicit word-bounded round marker
(`Round 2`, `R3`); when a match is doubtful, do NOT count it. If the
count is >= 3 AND no `**Round cap:**` comment from you exists:
Post:
`**Round cap:** <N> fix-rounds without a ship. The crew cap is 3.`
` Routing to Orion for a respec — the loop needs a design change (split`
` into per-assertion sub-issues, or re-scope the component/fixture),`
` not a 4th patch. Last assignee: <agent>.`
Then `multica issue assign <ID> --to "Orion"` (assignment to a
DIFFERENT agent dispatches a task — that is Orion's wake). Do NOT
change the status. Done with this issue — skip B.
=== Check B — SILENCE (stall) — only if A did not fire ===
Let the stall threshold W = 60 minutes (24h if wait-state). Exactly one
of these five states holds — act accordingly:
b1. T_agent is fresher than W → healthy. Skip.
b2. T_agent older than W, AND (T_mine absent OR T_agent > T_mine — i.e.
the assignee acted after your last marker, then stalled again) →
NEW stall episode → NUDGE. First gather evidence: if the issue has a
linked PR/CI run, check its actual state on GitHub — an unreported
failed run is exactly what the nudge should carry. Post:
`**Stall nudge:** no assignee activity for <N>. Re-running your`
` assignment. <evidence if any, e.g. "your production smoke run`
` <url> failed at <time> — start there."> If you are waiting on`
` something, say so in a comment so the sweep gives you 24h.`
Then `multica issue rerun <ID>`. Do NOT change status or assignee.
b3. T_mine is a nudge newer than T_agent, less than 60 min old → wake
window still open. Skip.
b4. T_mine is a nudge newer than T_agent, 60+ min old → the wake did not
produce activity. Inspect `multica issue runs <ID>` for the rerun
task you triggered:
- still `running` → the agent is legitimately mid-task. Skip.
- `failed`, or still `queued` → runtime problem. ESCALATE with that
task status.
- `completed` yet zero assignee activity → the agent ran but did not
engage. ESCALATE and say exactly that.
Escalation = post `**Stall escalation:** <what happened, incl. the`
` rerun task status and any CI evidence>. @Josh please check —`
` clear this by getting <agent> to act or reassigning the issue.` —
leave status AND assignee unchanged.
b5. T_mine is a `**Stall escalation:**` newer than T_agent → PARKED.
This episode is already with Josh; skip (count it in the log). One
escalation per stall episode — only assignee activity un-parks the
issue (it resets the state machine via T_agent > T_mine).
4. When every issue is processed, exit. Log (closed: X, round-cap: N,
nudged: M, escalated: K, parked: P, skipped: J).
# Skip this run if
No issues in in_progress/in_review after the Step 2 filter.
# Never
- Create new issues from this autopilot.
- Set any issue to `blocked`. A stalled runtime is not a blocker. `blocked`
is for the ASSIGNEE to declare a real dependency; sweep-blocked issues
leave every active queue and rot (this built an 11-issue graveyard by
2026-07-03).
- Close anything whose title does not match a create_issue autopilot's
`issue_title_template`, or whose final-result reading is uncertain.
- Use reassign-to-same-agent as a wake — verified no-op; `multica issue
rerun` is the wake.
- Touch issues outside in_progress / in_review, unassigned issues, or
issues labeled `no-sweep`.
- Count non-fix commits (feat/docs/chore/refactor) as fix-rounds.
- Nudge or escalate an issue assigned to Penny or Sage.
- Post a second `**Stall nudge:**` or a second `**Stall escalation:**` in
the same stall episode. The budget is one rerun then one escalation;
after that the issue is parked until the ASSIGNEE acts — elapsed time
never re-opens the budget.
- Treat your own comments/actions (or any sweep marker) as "assignee
activity", or judge responsiveness by `updated_at` alone.Why: A stalled issue (no activity) and a thrashing issue (many fix-rounds, no convergence) are the two ways work gets stuck — one pass over in_progress/in_review finds both. Rewritten 2026-07-04 after the original design proved counterproductive: its wake mechanism (reassign-to-same-agent) was a verified no-op — 3/3 historical nudges dispatched zero tasks — so every quiet issue marched deterministically through nudge → escalate → blocked, building an 11-issue blocked graveyard that nothing ever revisited. The wake is now multica issue rerun (verified to dispatch a fresh task in seconds), the sweep never sets blocked, done-but-open autopilot report issues are closed instead of escalated, declared wait-states get a 24-hour window (which also suppresses the thrash cap), coordination anchors opt out via the no-sweep label, and Check B is an explicit five-state machine whose stall episode ends only on real assignee activity — one nudge plus one escalation per episode, and elapsed time never re-opens the budget.
3c. Maggie — Blocked review
The Issue health sweep (#3b) only scans in_progress/in_review — a legitimately-blocked issue whose human dependency never resolves is invisible to every autopilot in this workspace, including that one. This daily pass closes that gap: it is the only routine surveillance of the blocked status. Added 2026-07-04 after a 2026-07-03 audit found an 11-issue blocked graveyard with nothing revisiting it.
| Field | Value |
|---|---|
| Name | Maggie — Blocked review |
| Agent | Maggie |
| Schedule | Daily 07:45 Chicago · cron 45 7 * * * |
| Output mode | Run only (Maggie wakes / escalates existing blocked issues; never creates new ones) |
Runbook (paste into the form):
# Goal
One daily pass over `blocked` issues — the one status the hourly Issue
health sweep (b50c7e15) never scans. `blocked` is legitimate ONLY as an
assignee-declared real external dependency (paired with a handoff); this
pass keeps those declarations honest so blocked issues can't rot silently
(an 11-issue graveyard existed by 2026-07-03). Two outcomes only: WAKE the
assignee when their declared dependency has visibly resolved, or ESCALATE
to Josh when a blocked issue has gone stale. You never un-block, close, or
reassign — the assignee owns the status; Josh owns stuck dependencies.
# Context
- **Wake mechanism = `multica issue rerun <ID>`** — re-enqueues the issue's
current assignment as a fresh task; verified 2026-07-04 to dispatch even
on `blocked`-status issues. Re-assigning an issue to the SAME agent
dispatches NOTHING (verified 2026-07-04) — never use it as a wake.
Ground truth for "did the wake dispatch" = `multica issue runs <ID>`.
- A legitimate blocked issue has a **declared dependency**: an assignee
comment from when the status went blocked naming what it waits on (a
human decision, an external PR/CI event, a third party). If no such
comment exists the block is undeclared — a protocol violation to
surface, not to fix yourself.
- **Activity** = a comment, commit, status change, or PR event by anyone
OTHER than your own review markers. This holds even when you (Maggie)
are both reviewer and assignee of the same issue — your real work
comments/commits count as activity, your own markers never do. Your
markers bump `updated_at`, so NEVER judge staleness by `updated_at`
alone.
- Dedup markers, each used ONLY for its purpose (they are how you detect
prior action): `**Blocked wake:**`, `**Blocked review:**`.
- You NEVER create issues from this autopilot. Comment / rerun only. You
run with concurrency 1, so keep this pass efficient.
# Steps
1. `multica issue list --status blocked --output json`
2. Filter: keep only issues assigned to a crew agent (incl. yourself).
Skip — logging each — issues labeled `no-sweep`, issues with no
assignee or a human assignee, and issues assigned to Penny/Sage.
3. For EACH remaining issue, establish three things before acting:
- **DEP** = the declared dependency (the assignee comment that set or
explains the block; quote it verbatim when you act). May be absent.
- **T_activity** = the most recent activity by anyone (per the Activity
definition — your own markers never count).
- **T_mine** = the timestamp and kind of your most recent
`**Blocked wake:**` or `**Blocked review:**` on this issue (may be
absent).
A **stall episode** ends ONLY when someone other than you acts
(T_activity becomes newer than T_mine). Elapsed time never ends an
episode and never re-opens a spent budget.
Then resolve the issue's state, in order:
=== State P — PARKED ===
Your newest marker is a `**Blocked review:**` newer than T_activity →
this episode is already with Josh. Skip (count it). Only activity from
someone else un-parks the issue.
=== State W — WAKE PENDING ===
Your newest marker is a `**Blocked wake:**` newer than T_activity:
- less than 24h old → wake window still open. Skip.
- 24h+ old → the wake produced nothing. Inspect `multica issue runs
<ID>` for the rerun task's status, then post:
`**Blocked review:** (<date>) woke <agent> at <time> (rerun task:`
` <status>) and got no activity since. Declared dependency:`
` "<quote DEP, or 'none declared'>". @Josh — this needs a human:`
` resolve the dependency, reassign, or cancel.`
No rerun. Done — the issue is parked until someone else acts.
Otherwise no marker of yours is newer than T_activity (fresh episode
or healthy wait) — run these checks in order:
=== Check 1 — RESOLVED DEPENDENCY → WAKE ===
DEP exists and has VISIBLY resolved: the awaited PR merged, the CI run
finished, the question in the thread was answered, the external event
happened. Verify on GitHub or in the thread — if you cannot point at
concrete evidence, it did NOT resolve; fall through. If resolved:
Post: `**Blocked wake:** your declared dependency ("<quote DEP>")`
` looks resolved — <evidence, e.g. "PR #1245 merged at <time>">.`
` Re-running your assignment — un-block or update the declaration`
` when you pick it up.`
Then `multica issue rerun <ID>`. Do NOT change status or assignee —
un-blocking is the assignee's call. Done with this issue.
=== Check 2 — UNDECLARED BLOCK → WAKE ===
DEP is absent (nothing declares what the issue waits on):
Post: `**Blocked wake:** this issue is blocked with no declared`
` dependency — nothing can verify or resolve it. Re-running your`
` assignment: declare the blocker in a comment, or move the issue`
` back to an active status.`
Then `multica issue rerun <ID>`. Done with this issue.
=== Check 3 — STALE DECLARED BLOCK → ESCALATE ===
DEP exists, is not visibly resolved, and T_activity is older than
3 DAYS:
Post: `**Blocked review:** (<date>) blocked <N> days; no activity`
` from anyone for <M> days. Declared dependency: "<quote DEP>".`
` @Josh — this needs a human: resolve the dependency, answer the`
` question, reassign, or cancel. I won't re-ping until someone acts.`
No rerun — the assignee already declared they cannot proceed. Done.
=== Otherwise — HEALTHY WAIT ===
Declared dependency, not resolved, activity within 3 days → the block
is legitimate and being watched. Skip.
4. When every issue is processed, exit. Log (woken: X, undeclared: Y,
escalated: K, parked: P, skipped: J).
# Skip this run if
No issues in `blocked` after the Step 2 filter.
# Never
- Un-block, close, cancel, or reassign an issue, or change ANY status —
the assignee owns `blocked`; Josh owns stuck dependencies.
- Create new issues from this autopilot.
- Post a second `**Blocked wake:**` or a second `**Blocked review:**` in
the same stall episode. The budget is one wake then one escalation;
after that the issue is parked until someone other than you acts —
elapsed time never re-opens the budget.
- Treat your own markers as activity, or judge staleness by `updated_at`
alone.
- Use reassign-to-same-agent as a wake — verified no-op; `multica issue
rerun` is the wake.
- Declare a dependency resolved on a guess — no concrete evidence means
not resolved.
- Touch issues outside `blocked`, issues labeled `no-sweep`, unassigned
or human-assigned issues, or issues assigned to Penny/Sage.Why: The Issue health sweep (3b) deliberately never touches blocked — so without this pass, blocked is a one-way door (that's how the 11-issue graveyard formed by 2026-07-03). This daily review keeps blocked declarations honest: it wakes the assignee (multica issue rerun) when the declared dependency has visibly resolved or when the block was never declared, and escalates to Josh when a declared block goes stale 3+ days. It never changes status or assignee — the assignee owns blocked, Josh owns stuck dependencies. Created 2026-07-04 as the closing move of the blocked-graveyard cleanup.
3d. Riley — CI wake
Event-driven (webhook), not a scheduled cron — the PRIMARY wake for any crew agent that declared a CI/PR-checks wait (see the Issue health sweep's WAIT-STATE check, #3b), firing the instant all PR checks conclude instead of waiting for the next hourly sweep.
| Field | Value |
|---|---|
| Name | Riley — CI wake |
| Agent | Riley |
| Schedule | Event-driven — fired by .github/workflows/multica-ci-wake.yml when all checks conclude on a PR (no cron) |
| Output mode | Run only |
This autopilot has no dedicated runbook write-up here — its full spec (the webhook trigger, the wake mechanism, and the 2h/24h wait-threshold split it backstops) lives on the platform and is documented in multica-agent-crew.md and multica-state.md, per PR #1259, which wired the webhook but never touched this file. See the schedule-reference table below for its autopilot ID (e031e327).
4. Riley — Objectuve feedback ingest
| Field | Value |
|---|---|
| Name | Riley — Objectuve feedback ingest |
| Agent | Riley |
| Schedule | Every 6 hours · cron 0 */6 * * * |
| Output mode | Create issue (one per new feedback post) |
2026-07-04: cadence cooled from hourly (every success since 2026-06-01 returned zero items); event-driven webhook trigger proposed in the 2026-07 workflow audit.
Runbook (paste into the form):
# Goal
Every new feedback post on Objectuve becomes one Multica issue with the
`feedback` label, ready for Maggie to triage and Penny to weigh in her
Monday strategy pass.
# Context
- Endpoint: POST https://api.objectuve.com/integrations/feedback/export
- Auth: HMAC-SHA256 of the request body, in the X-Workforce-Signature
header. Secret is $RAILS_WEBHOOK_SECRET (provided by the autopilot
environment — never hardcode).
- Schema reference: docs/architecture/api-reference.md → "Integration
Endpoints".
- Cursor: stored on your side. Either (a) read the previous run-log
entry for the last `next_since` you received, or (b) list existing
Multica issues with label `feedback`, find the most recent one, and
parse its `feedbackpost-public_id:` marker to look up its created_at
via the API.
# Steps
1. Compute `since`: ISO8601 timestamp of your last successful ingest,
or null if this is the first run.
2. Build the body: {"since": "<iso8601 or null>"}
3. Compute signature:
sig=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac \
"$RAILS_WEBHOOK_SECRET" | awk '{print $2}')
4. POST:
curl -fsS -X POST \
-H "Content-Type: application/json" \
-H "X-Workforce-Signature: $sig" \
-d "$BODY" \
https://api.objectuve.com/integrations/feedback/export
5. For each post in the `feedback_posts` array:
a. Confirm no existing Multica issue has
`feedbackpost-public_id: <public_id>` in its body. If one
exists, skip — this is the dedupe gate.
b. Create a Multica issue:
- Title: `Feedback: <title>` (truncate to 140 chars total)
- Body:
```
feedbackpost-public_id: <public_id>
category: <category> · status: <status> · votes: <vote_count>
author: @<author_username>
admin: <admin_url>
---
<description verbatim>
```
- Label: `feedback`
- Status: `backlog`
- Assignee: `Maggie`
6. Log: count of newly-created issues + the value of `next_since` from
the response (this is your cursor for the next run).
# Skip this run if
The `feedback_posts` array is empty. Post a one-line "no new feedback"
comment in the run log; do NOT create a no-op issue.
# Never
- Edit, close, or delete existing issues with the `feedback` label.
Triage and routing are Maggie's job. You only ingest.
- Hardcode the secret in this runbook. It comes from
$RAILS_WEBHOOK_SECRET in the autopilot environment.
- Create more than 100 issues in a single run. The endpoint caps at
100; if you ever see exactly 100 returned, your cursor is behind —
the next hourly run will catch up. Don't loop within one run.
- Re-create issues for posts that already have a Multica issue. The
`feedbackpost-public_id:` marker is the source of truth for dedupe.Why: Penny's Monday strategy pass already references "User feedback in the Multica board (issues with feedback label)" — but until now, putting feedback on the board has been a manual habit. This closes the loop so the strategic agent always has fresh user voice to weigh against competitive intel.
Tier 2 — Add once Tier 1 runs clean for a week
5. Riley — Stale PR sweep
| Field | Value |
|---|---|
| Name | Riley — Stale PR sweep |
| Agent | Riley |
| Schedule | Weekdays 14:00 Chicago · cron 0 14 * * 1-5 |
| Output mode | Create issue (only when there's something to route) |
Runbook (paste into the form):
# Goal
Open PRs that are stuck (red CI, behind master, waiting on review too
long) get unstuck — routed to whoever can fix them.
# Context
- Repo: objectuve-softworks/enkidu
- PRs you open route to Roy via Maggie when green; this autopilot catches
PRs that went stale after opening.
- "Stale" thresholds: CI red > 2h, branch behind master by >5 commits,
awaiting-review > 24h with no new comments.
# Steps
1. `gh pr list --state open --json
number,title,author,isDraft,mergeable,statusCheckRollup,headRefName,
updatedAt,baseRefName,labels`
2. For each non-draft PR:
a. Check CI state from statusCheckRollup.
b. Check commits behind master via `gh api repos/:owner/:repo/compare/main...<head>`.
c. Check time since last review comment.
3. Classify each stale PR:
- CI red + lint/type/build → Codi
- CI red + test failure or mystery → Dave
- Behind master by >5 commits → Codi (rebase) or you (if trivial)
- Awaiting review >24h → Roy via Maggie
- Author is a bot (Dependabot) with green CI + 0 conflicts → Vicki to
auto-merge (cc Josh)
4. If there are any stale PRs, create ONE roll-up issue titled
"Stale PRs: <N> need attention". Body: one table row per PR with
number, title, problem, routing recommendation.
5. Assign the issue to Maggie.
# Skip this run if
Zero open non-draft PRs, OR every open PR is fresh (<2h old) and green.
# Never
- Force-push to anyone else's branch.
- Close PRs. That's Vicki's call.
- Re-run CI more than once per PR per sweep — persistent failure is
real, not flake.Why: PRs that sit red or behind master silently rot. This sweep catches them within a workday.
5b. Riley — Stale branch pruning
| Field | Value |
|---|---|
| Name | Riley — Stale branch pruning |
| Agent | Riley |
| Schedule | Mondays 09:00 Chicago · cron 0 9 * * 1 |
| Autopilot ID | dcfaa30e-5c6d-4a24-9208-48db2a160d2d |
| Output mode | Create issue (only when stale-no-PR branches need triage; silent otherwise) |
This is the live runbook as of 2026-09-01 — an interim, inline fix Riley applied directly to the autopilot's description after OBJ-3088 found the original Step 5 (branch-name PR lookup + ahead-of-master ancestry count) produced 16 false positives in a single run. It is not yet wired to scripts/multica-stale-branch-triage.mjs (OBJ-3099) — that rewrite is tracked separately as OBJ-3110, blocked on OBJ-3099 merging to master, since pointing this autopilot at an unmerged script would break its next scheduled run (2026-09-07). See the squash-merge gotcha for the full root cause this runbook now works around.
Runbook (paste into the form):
# Goal
Prune branches on origin whose PR was merged at least 14 days ago, and surface stale no-PR branches (last commit at least 90 days old) for human triage. Defensive guards prevent any accidental deletion of protected, release, or recently-merged branches.
# Context
- Default branches: master, main, develop, production, staging — NEVER delete
- Protected patterns: release/*, hotfix/* — NEVER delete
- Rollback window: 14 days post-merge — NEVER delete inside it
- Default remote: origin (single remote assumed for this repo)
- Required tools: git, gh (authenticated)
- Worker branches (agent/<name>/<hash>) are committed by Codi/Tess/Desi/Dave/Dori, but Riley opens the PR from Riley's OWN branch, not the worker branch. A worker branch's name therefore never appears as a PR's headRefName even when its work shipped cleanly — "no PR by this branch name" is the normal healthy end state for a worker branch, not evidence of abandonment. Step 5 below exists to not misread that.
# Steps
1. git fetch --prune origin (refresh remote tracking + drop dead refs locally)
2. gh pr list --search "is:merged merged:<YYYY-MM-DD" --limit 1000 --json headRefName,mergedAt,baseRefName — where the date is (today - 14 days). Use a date-bounded search, NOT a flat --limit 200 with no filter: on a repo with high merge velocity, the newest 200 merged PRs can span fewer than 14 days, which makes the unfiltered form silently return zero candidates and misreport "Clean week" every run. Verify coverage isn't truncated via the search response's total_count against the --limit used.
3. For each candidate head branch:
- Confirm remote HEAD === merge-commit HEAD (no force-pushes since merge)
- Confirm no open PR currently targets the branch as baseRefName
- Confirm branch name does NOT match any protected pattern (master, main, develop, production, staging, release/*, hotfix/*)
4. For each surviving candidate: git push origin --delete <branch>
5. Build the no-open-PR candidate set: branches with no open PR currently targeting them as base, whose latest commit is at least 90 days old. Do NOT determine "merged PR" by branch name here, and do NOT use an ahead-of-master commit count as a merge signal — both are structurally wrong for this repo:
- Branch-name lookup misses every worker branch merged via a PR opened from Riley's own branch (see Context) — it would misreport clean, shipped work as abandoned, forever, on every run.
- `git rev-list origin/master..<tip>` never reaches zero for a squash-merged branch: squashing rewrites the branch's commits into one new SHA on master, leaving the originals permanently non-ancestral. A months-old landed branch reads as "real unmerged work" indefinitely.
Instead, resolve per commit, which survives squash-merge because GitHub keeps the commit→PR association independent of ancestry:
a. For each no-open-PR candidate branch, collect its unique commit SHAs not reachable from origin/master (at minimum the tip SHA).
b. For each SHA, query GitHub's commit→PR association directly:
```
gh api repos/objectuve-softworks/enkidu/commits/<sha>/pulls -q '.[] | "#\(.number) merged=\(.merged_at // "NOT-MERGED")"'
```
Two traps, both confirmed to bite in practice:
- Use the REST field merged_at (snake_case) from this endpoint. gh pr view --json mergedAt is a different, GraphQL-only field — mixing the two up reports merged=NOT-MERGED for PRs that are demonstrably merged.
- Do not gate on .state. REST reports "closed" for merged PRs too, not just abandoned ones. Gate only on merged_at being non-null.
c. If every unique commit on the branch resolves to at least one PR with a non-null merged_at, the branch is fully landed. Route it into the SAME deletion path as steps 3-4 (apply the same guards: no force-push since that PR's merge, name isn't protected, that PR's merge isn't inside the 14-day rollback window) rather than the triage list.
d. Only a commit that resolves to NO pull request at all warrants a content check: does that commit's diff already exist on origin/master (e.g. git cherry origin/master <branch>, or diff the tree)? If present, treat as landed (case c) and route to deletion. If genuinely absent, it is a real finding.
e. The human-triage list is built ONLY from branches that still have at least one commit that is both PR-less (step b) and content-absent (step d) after this check, and whose latest commit remains at least 90 days old.
# Output mode
- If at least one branch was deleted (from steps 3-4 OR step 5c/5d): post a single summary comment on the autopilot run issue listing what was deleted — branch name + originating PR number + merge date. Note in the listing which branches were resolved via the direct headRefName match (steps 2-4) vs. the per-commit lookup (step 5) so the distinction is auditable.
- If the human-triage list from step 5e is non-empty: file ONE issue titled "[Riley] Stale branch sweep — N branches need human review" assigned to Maggie, listing each with last-commit date and tip SHA, and explicitly noting that every listed branch already failed the per-commit PR lookup (no merged PR found for any of its unique commits) and the content-on-master check (diff not present on master). Issue body includes a recommended action per branch (delete / keep / investigate).
- If both empty: silent. Post a one-line comment in the run log: "Clean week — nothing to prune."
# Skip this run if
- gh auth status fails or rate-limit exceeded — post a one-line note and exit.
- git fetch origin fails — post a one-line note and exit.
- The repo is in a detached-HEAD or rebase-in-progress state.
# Never
- Never delete master, main, develop, production, staging.
- Never delete a branch matching release/* or hotfix/*.
- Never delete a branch whose PR merged less than 14 days ago.
- Never force-push to "clean up" history. Deletion of remote refs only.
- Never delete a branch with an open PR targeting it as baseRefName.
- Never auto-delete the no-PR stale list — humans triage those.Why: Merged branches accumulate on origin and clutter git branch -r, complicate PR-author search, and make the existing Stale-PR sweep (#5) noisier. Pruning is mechanical and safe-by-construction with the 14-day rollback window + protected-pattern guards. The no-PR stale list is surfaced for human triage rather than auto-deleted because — even after the per-commit fix — an unlanded verdict means unproven, not abandoned; some of those branches may still be worth recovering. Step 5's per-commit resolution replaces the original branch-name/ancestry check after it produced 16 false positives in one run (OBJ-3088) — see the squash-merge gotcha for why that check was structurally guaranteed to be wrong in this repo, and OBJ-3110 for wiring this autopilot to the tested, first-class replacement (scripts/multica-stale-branch-triage.mjs) once it's merged.
6. Dori — Docs drift audit (objectuve-atlas)
| Field | Value |
|---|---|
| Name | Dori — Docs drift audit (objectuve-atlas) |
| Agent | Dori |
| Schedule | Mondays 11:00 Chicago · cron 0 11 * * 1 |
| Autopilot ID | 8e609d6a-3a67-420e-af7b-6c199215ab41 (trigger 3c8052d4-e9dd-41a0-bcf7-008b0a78ca12) |
| Output mode | Create issue (single prioritized triage issue) |
| Report artifact | See Audit report artifact (pillar autopilots) — writes .planning/audits/atlas-<YYYY-MM-DD>.md per run. |
Runbook (paste into the form):
# Goal
Weekly documentation-truth audit scored against the locked `objectuve-atlas`
framework (44 gates, categories A–F, C/L/A/I/M/S rubric), producing a single
prioritized triage issue Maggie routes into the chain. Read-only: this
autopilot never fixes what it finds.
This replaces the hand-rolled grep/link-check steps this autopilot used to
carry. Atlas does that mechanically now, wider and faster, and — unlike the
old prose version — it covers agent-facing context, which is where the worst
drift actually lives.
# Context
- You are Dori. `objectuve-atlas` is your framework the way `objectuve-keystone`
is Codi's and `objectuve-throughline` is Desi's. This autopilot is modeled
structurally on "Codi — Keystone architecture audit monthly" (same
Goal/Context/Steps/Issue-structure/Routing/Never shape, same
single-triage-issue-per-run discipline), scoped to documentation truth.
- **Preflight — if `.claude/skills/objectuve-atlas/SKILL.md` does not exist on
master, stop.** Post a run-log comment saying the framework has not landed
yet and skip the run. Do not fall back to hand-rolled grepping; a silent
downgrade to the old method is worse than a skipped week, because the issue
it files looks identical either way.
- **Make the two skip paths tell themselves apart.** A preflight skip and a
clean-sweep skip both file no issue, so from the outside they are the same
event. Start the run-log comment with a literal `SKIP: framework-absent` or
`SKIP: clean-sweep` prefix so anyone scanning history — or Maggie's rot
sweep — can tell "the audit found nothing" from "the audit never ran."
Without the prefix, an indefinitely-unmerged framework reads as months of
clean docs.
- Required reading — load BEFORE auditing, every run:
1. `.claude/skills/objectuve-atlas/SKILL.md` — the two verbs, the 44 gates,
the C/L/A/I/M/S rubric, the four blocking criteria, the routing rule.
2. `.claude/skills/objectuve-atlas/references/doc-vocabulary.md` — the locked
canonical-owner map and confirmed-drift inventory. Every finding must cite
one of those, a real `file:line`, or a named CI gate. Never assert drift
from memory.
3. `.claude/skills/objectuve-atlas/references/atlas-gates.md` — the gate
definitions you are scoring against.
- Registry: `scripts/doc-surfaces.mjs`, 24 surfaces. Harvester:
`scripts/capture-doc-evidence.mjs`. Evidence lands in
.planning/atlas-evidence/ (gitignored — never commit it).
# Surface rotation
A full 24-surface gate re-score every week is not sustainable and would
produce an issue nobody reads. Narrow by ROTATION, not by shrinking gate
coverage:
- **Every run, unconditionally — the agent-facing tier:** `claude_md`,
`claude_skills`, `claude_agents`, `planning_state`. These are executable
context. A stale skill is acted on by every agent run that loads it, and
those edits get committed. This tier never rotates out.
- **Rotating, 3 surfaces per run** from the remaining 20, so each gets a full
re-score roughly every 7 weeks. Track coverage in the issue's own "Rotation
log" section so the next run picks up where this one left off — do not rely
on memory across runs.
# Steps
1. Run `node scripts/capture-doc-evidence.mjs --all` once for the summary
table (~7s). Note any surface reporting `partial=true` — that means the
registry's globs have drifted from the repo layout and is itself a finding
against `scripts/doc-surfaces.mjs`.
2. For each surface in this run's scope (fixed tier + rotation), run
`objectuve-atlas audit <surface>` — walk gate categories A–F, apply the
C/L/A/I/M/S self-critique before emitting.
3. Open the actual file for every finding before writing it up. The harvester
reports candidates; a grep hit read out of context is how a false positive
reaches a report.
4. Build the triage issue (structure below), grouped by atlas's own severity
split — blocking vs. should-fix — not by an invented scale.
5. Assign to Maggie. She routes individual items back to you, or skips them.
# Issue structure
- **Executive summary** — verdict, blocking count, should-fix count, and
whether the agent-facing tier is clean.
- **Category scoring table** — the six categories, gates checked, fired,
blocking.
- **Blocking findings** — one row each: gate, `file:line`, "doc says → truth",
fix. These are the four criteria in SKILL.md: actively misleading agent
context, a dead operational instruction, an unmarked contradiction between
live docs, a drifted safety/legal/security claim.
- **Should-fix findings** — same shape, lower priority.
- **Systemic findings** — bulk patterns reported ONCE with a count. 56 docs
missing a footer is one finding, not 56 rows.
- **Routed elsewhere** — findings that failed the S axis, with the sibling
pillar named (signal / bastion / keystone / hallmark / throughline /
compass).
- **Rotation log** — which surfaces this run covered, and the date. Carry the
prior run's log forward.
# Calibration (do this before trusting the run's severity calls)
Spot-check one low-finding and one high-finding surface each run, picked from
THIS run's `--all` summary — never from a name memorized here.
**"Clean" means zero findings across all six categories, not a `drift=0`
summary cell.** The `drift` column counts canonical-fact hits only; a surface
can read `drift=0` and still carry real gate B, E, or F findings. `docs_index`
and `company_legal` are both exactly that trap — `drift=0`, but `docs_index`
correctly fires g33 (7 `docs/` sections lack an `index.md`) and
`company_legal` has footer dates behind their real last commits (g11) plus a
`[TBD]` placeholder (g39). Open the surface's `evidence.md` and confirm every
category section is empty before treating it as a control.
Only if a genuinely all-category-clean surface fires a gate is the gate
miscalibrated — and even then, confirm the finding is false first. **A
correctly-firing gate is not miscalibration.** Never weaken a gate to make a
control look clean; that degrades every future run silently.
# Skip this run if
Every audited surface is clean. Post a run-log comment confirming the clean
sweep with the surfaces covered and the rotation log; don't create an issue.
# Never
- **Auto-fix.** This is diagnostic. Atlas has no editing verb on purpose —
its credibility depends on its reports being independent of its own edits.
Fixes happen through Maggie → Dori with a specific task package.
- **Fire a staleness finding on age alone against a historical surface.**
`ops_rollouts`, `milestones`, and `product_prds` are point-in-time records
doing their job. That is gate B13, and it fires against the auditor — retract
it, don't soften it.
- **Fire g5 on a postmortem.** A doc narrating an already-fixed incident and
stating its own resolution is correctly-documented history, same shape as
CLAUDE.md's Common Gotchas. `doc-vocabulary.md` has the worked
counter-example.
- **Promote a contrastive hit without quoting the line.** "Use
`Interaction::Base`, NOT `ActiveInteraction`" is correct documentation that
contains the wrong string.
- **Re-derive what CI already gates.** `docs.yml`'s VitePress build covers
dead links inside the `docs_site` graph; `route-drift.yml` covers the two
sitemap docs against both routers. Cite them; audit what they cannot see.
- **Commit .planning/atlas-evidence/.** It is gitignored and regenerated on
demand.
- Audit third-party docs (`node_modules`, `vendor/`).Why: Docs quietly rot as code evolves, and agent-facing docs (CLAUDE.md, .claude/skills/**) are worse than human-facing ones — they're acted on, not just read, and a drifted skill gets its bad instructions committed by whichever agent loads it next. The objectuve-atlas framework replaced this autopilot's original hand-rolled grep/link-check steps because the harvester covers more surface, faster, and doesn't silently skip the agent-facing tier the way a docs/-only grep sweep always did.
7. Vicki — Release readiness preflight (pre-train)
| Field | Value |
|---|---|
| Name | Vicki — Release readiness roundup |
| Agent | Vicki |
| Schedule | Tuesdays 14:00 Chicago · cron 0 14 * * 2 |
| Autopilot ID | c49cad3b |
| Output mode | Create issue |
The Tuesday day-before pre-flight for the Wednesday 08:00 CT release train (#7c). It surfaces what's queued to ship and what's still held, so blockers get cleared today, before the cut. (Re-cadenced from the old Friday "roundup" when production moved to a weekly train; the live autopilot name is unchanged.)
Runbook (paste into the form):
Credential: Vicki's Sentry access is not stored in the autopilot description. It lives in Vicki's agent
custom_env— a workingSENTRY_TOKEN(set viamultica agent env set <agent-id> --custom-env-stdin, resolved at run time from${SENTRY_TOKEN}, same pattern as Dave's §2 token). The value never appears in the autopilot description, an issue comment, or an agent transcript, so rotating it needs no runbook edit.--custom-env-stdinreplaces the whole map — re-send every key Vicki already has, or you will silently wipe the rest of her env (the OBJ-951 failure mode). Confirmed working as of 2026-09-14 (Josh, on the parent issue).Production
gcloudaccess for the Step 5agcloud run services describe enkidu-api-productioncall is not a separate credential in Vicki'scustom_env. Vicki and Dave share one runtime host, so Step 5a has only ever worked by inheriting whichever identity is active there: thedave-autopilotservice account (roles/run.viewer, sufficient fordescribe), re-synced every cycle by the OBJ-3564/OBJ-3570 credential preflight block immediately below (confirmed 2026-09-19, Josh, on the parent issue). Rotating that SA's key contents still needs no runbook edit — only the preflight block'sSA_EMAIL/KEY_FILEvalues would, and only if that SA's email or key-file path itself ever changed.§7 is not the only autopilot touching production Cloud Run — §2b/§2d/§2g also resolve
enkidu-api-production— but it's the only Vicki-owned autopilot with a production Cloud Run dependency, and the only one reading the deployed image tag (spec.template.spec.containers[0].image, Step 5a) rather than the service URL those liveness checks use.
# Goal
Tuesday pre-flight for the Wednesday 08:00 CT release train. A readout of what's
queued to ship on tomorrow's train, what's still held, and what the next version
should be — so blockers get cleared TODAY, before the cut.
# Context
- Production ships on a weekly train: Wednesdays 08:00 America/Chicago. This run
is the day-before preflight that de-risks it. The actual tag is cut by the
`Vicki — Weekly release train` autopilot (Wed 08:00) after Josh's go.
- Current version: latest git tag (`git tag --sort=-v:refname | head -1`)
- Release command (Wed train only): `release [version]` / `release` (auto-patch)
- Shipping chain: Roy approves -> (Dori) -> you merge to master (-> staging).
The production tag waits for the Wed train; a normal SHIP IT is merge-only.
- Release notes live in docs/ (Dori writes them); CHANGELOG grouped by version.
- Sentry org: objectuve. Sentry project: objectuve-api (numeric project id 1270618).
- Sentry auth token: supplied to your runtime as the `SENTRY_TOKEN` environment variable.
Reference it only as `${SENTRY_TOKEN}`. Never print, echo, log, or paste its value —
not into a run log, not into an issue body, not into a comment.
- Merged-but-unreleased gate (OBJ-3204/OBJ-3207): `scripts/check-merged-unreleased-fixes.mjs`
in the repo maps a live production Sentry issue's culprit to the repo file(s) whose recent
commits likely already fixed it. This description does the Sentry lookups and hands the
script its input on stdin — it does not restate the mapping algorithm; the script owns that.
# Steps
0. Previous-run status check (OBJ-3204 Part B). Run `multica autopilot runs
c49cad3b-02c4-4d94-ab0d-8c21d72b3c8c --limit 5 --output json` and filter to
entries with `source == "schedule"` — manual re-runs of this autopilot DO
create their own rows in this API, tagged `source: "manual"` (confirmed
2026-09-03/OBJ-3279), and must not be mistaken for the previous scheduled
run. The most recent `source == "schedule"` entry is the immediately
previous scheduled run.
- If that entry's `status` is `"completed"`: no banner needed, continue to Step 1.
- If it is not `"completed"`: search for a `Release preflight:` issue created after that
entry's `triggered_at` — `multica issue list --sort created_at --direction desc --limit 20
--output json`, filtered (e.g. via `jq`) to titles starting with `"Release preflight:"` and
`created_at` later than the previous run's `triggered_at`.
- If one exists: note for Step 6 — "⚠️ Previous scheduled preflight (`<previous run's
triggered_at date>`) did not complete (`<failure_reason>`), but a manual re-run covered
it — see `<that issue's identifier>`. Delta is normal."
- If none exists: note for Step 6 instead — "⚠️ Previous scheduled preflight
(`<previous run's triggered_at date>`) did not complete (`<failure_reason>`) and no
manual re-run has covered it — this run's 'On the train' section reflects a widened
delta back to the last completed run, not just since last week."
1. List merged-to-master commits since the latest tag:
`git log <latest-tag>..master --oneline`. These ride tomorrow's train.
2. List Multica issues recently shipped-to-staging (Roy-approved, merged, awaiting
the train) and any still in status=in_review.
3. List issues in status=blocked with your name in recent comments (your HOLDs) —
these are the blockers to clear before the cut.
4. Propose the next version bump (patch / minor / major) from the changes since the
last tag — lean conservative.
5. Check readiness gates: staging green (`deployed`), no fresh Sentry spike,
/health green, CHANGELOG [Unreleased] has content for the shipped work.
5a. Merged-but-unreleased gate (OBJ-3204 — advisory only, see "Never" below).
Credential preflight (OBJ-3564 / OBJ-3570): before this step's first `gcloud` call,
re-sync gcloud CLI's own credential cache to the `dave-autopilot` service account's
current key. gcloud CLI imports a key file ONCE via `gcloud auth activate-service-account`
and never re-reads it on its own, so a key rotation that updates the ADC file leaves
gcloud CLI signing every JWT with an already-retired private key: an immediate,
100%-reproducible `invalid_grant: Invalid JWT Signature` on every `gcloud` subcommand
until this runs (OBJ-3564, 2026-09-09). This mirrors
`scripts/multica-host/ensure-gcloud-sa-credential.sh` — call that script directly if this
run's workdir already has the enkidu repo checked out; otherwise run the equivalent inline:
`gcloud auth activate-service-account dave-autopilot@enkidu-488723.iam.gserviceaccount.com --key-file="$HOME/.config/dave-autopilot/key.json" --quiet`
Safe and cheap to run unconditionally every cycle — re-activating an already-current key
is a no-op in effect, no network round trip beyond the activation call itself. If this
itself fails (missing/unreadable key file, `client_email` mismatch, activation error), do
not treat it as a new condition — it surfaces as this step's own empty `DEPLOYED_IMAGE`
BLOCKED path below, not a new FAIL.
a. Deployed production SHA:
`DEPLOYED_IMAGE=$(gcloud run services describe enkidu-api-production --region
us-central1 --project enkidu-488723 --format
'value(spec.template.spec.containers[0].image)')` then `DEPLOYED_SHA=${DEPLOYED_IMAGE##*:}`
(production.yml tags images `<base>:<commit-sha>`, so the tag after the final `:` is the
deployed commit). Empty `DEPLOYED_IMAGE` -> BLOCKED for this step, below.
b. Sentry two-call intersect, scoped to production and widened to a 14-day window:
```
EVENTS_URL="https://sentry.io/api/0/organizations/objectuve/events/?dataset=errors&project=1270618&statsPeriod=14d&field=issue&sort=-count&per_page=100&query=environment%3Aproduction%20%21level%3Ainfo"
EVENTS_RESULT=$(curl -s --max-time 15 -H "Authorization: Bearer ${SENTRY_TOKEN}" "$EVENTS_URL")
ISSUES_URL="https://sentry.io/api/0/projects/objectuve/objectuve-api/issues/?query=is%3Aunresolved%20%21level%3Ainfo&limit=100"
ISSUES_RESULT=$(curl -s --max-time 15 -H "Authorization: Bearer ${SENTRY_TOKEN}" "$ISSUES_URL")
```
The `%21level%3Ainfo` (`!level:info`) severity floor is deliberate (OBJ-2932): this app
emits intentional `level: :info` breadcrumbs — e.g. `Teams::ProcessSubscriptionLifecycleEvent`
(OBJ-1439), spec-pinned instrumentation, not a fault — and without the floor those
breadcrumbs red this check permanently.
`EVENTS_URL` carries `environment%3Aproduction` because it's the org-level events call,
scoped to production; `ISSUES_URL` carries no `environment=` param because it's the
project-level issues call, supplying resolution state (`is:unresolved`) only — it needs no
environment scoping of its own. Any call returning HTTP 401/403/404 -> BLOCKED for this
step, below.
c. Intersect the two result sets — issue keys present in both the recent-events list and the
still-unresolved list — via `comm -12` on their sorted output:
```
RECENT=$(printf '%s' "$EVENTS_RESULT" | jq -r '.data[]?.issue // empty' | sort -u)
UNRESOLVED=$(printf '%s' "$ISSUES_RESULT" | jq -r '.[]?.shortId // empty' | sort -u)
LIVE_UNRESOLVED=$(comm -12 <(printf '%s\n' "$RECENT") <(printf '%s\n' "$UNRESOLVED") | sed '/^$/d')
CANDIDATES=$(printf '%s' "$ISSUES_RESULT" | jq --argjson keep "$(printf '%s\n' "$LIVE_UNRESOLVED" | jq -R -s -c 'split("\n") | map(select(length>0))')" '[.[] | select(.shortId as $s | $keep | index($s) != null) | {shortId, title, culprit}]')
```
d. Run the gate and capture stdout verbatim for Step 6 — its `## At risk — fix already
merged, unreleased` and `## unmapped — manual check` headings are already formatted; do
not re-derive or re-word them:
`printf '%s' "$CANDIDATES" | node scripts/check-merged-unreleased-fixes.mjs
--deployed-sha "$DEPLOYED_SHA" --ref origin/master`
- BLOCKED (this step only, not the whole run): empty `DEPLOYED_IMAGE`; any Sentry call
returning HTTP 401/403/404; or the script itself exiting non-zero (its own fail-closed
conditions — unresolvable SHA, or an empty diff contradicting a non-empty commit range).
Note "Merged-but-unreleased gate: BLOCKED —
<reason>" for Step 6 in place of the heading's content — never report it as "no matches."
6. Create an issue titled "Release preflight: train of <next-Wed-date>" with:
- If Step 0 produced a banner, place it directly under the title, before "On the train."
- Proposed next version + rationale
- "On the train" — merged work since the last tag (changelog draft material)
- "At risk" — not-yet-merged work that should make the train + who's unblocking
- "Held" — blocked issues with why + owner
- Gate status (staging / Sentry / health / changelog), each pass/fail
- **"At risk — fix already merged, unreleased"** (new, OBJ-3204) — Step 5a's captured stdout
verbatim, or its BLOCKED note. Distinct from the "At risk" bullet above: this section is
about work already fixed on master that production hasn't shipped yet, not unmerged work.
A hit here raises urgency to ship this train — it never flips the Recommendation to RED by
itself (see "Never").
- Recommendation: train is GREEN for tomorrow, or list what must clear first
7. Assign to Maggie so she routes any blockers to the right agent TODAY.
# Skip this run if
Zero changes since the last tag and nothing in_review/blocked. Post a
"nothing queued for the train" comment in the run log; do NOT create an issue.
# Never
- Actually ship or cut a tag from this autopilot — the Wed train (human-go) does that.
- Propose a major version bump without an explicit breaking change.
- Roll back from this autopilot.
- Let a Step 5a hit (merged-but-unreleased) flip the Recommendation to RED on its own — it only
raises urgency; RED still requires an actual failing gate.Why: Production ships on a weekly Wednesday train. This Tuesday pre-flight de-risks tomorrow's cut by surfacing the manifest + any blockers a day early, while there's still time to clear them.
7c. Vicki — Weekly release train
| Field | Value |
|---|---|
| Name | Vicki — Weekly release train |
| Agent | Vicki |
| Schedule | Wednesdays 08:00 Chicago · cron 0 8 * * 3 |
| Autopilot ID | df6b5a7f |
| Output mode | Create issue (assembles the manifest; assigns the train issue to Josh for go/no-go) |
The weekly production gate (beta policy). Continuous merge keeps master/staging current all week; this autopilot batches everything merged since the last tag into ONE production release, Wednesdays 08:00 America/Chicago. It assembles the manifest and hands Josh a one-click go/no-go — it never cuts the tag itself. On Josh's "go" the train issue routes to Vicki, who runs release, dispatches the manual mobile store builds, and closes out the train by handing Josh paste-ready mobile store copy (Play "What's new", App Store "What's New", App Review notes) as a second comment (see Vicki's "Weekly release train execution" final action in multica-agent-crew.md and docs/operations/store-release-notes.md).
Hotfix escape hatch: issues labeled hotfix (sev-1 / security / prod-down) ship immediately via Vicki, bypassing this train — they are NOT train items.
Reverting to continuous CD: when the beta ends, delete this autopilot and restore Vicki's per-ship release step. The train is the only thing gating production; nothing else changes. (.github/workflows/scheduled-release.yml briefly ran a competing weekday cron that could cut production tags outside this gate — removed under OBJ-1368; it's now workflow_dispatch-only break-glass and never fires on its own.)
Runbook (paste into the form):
# Goal
Assemble the weekly production release train and hand Josh a one-click go/no-go.
Production ships once a week — Wednesdays 08:00 America/Chicago — batching all
work merged to master since the last tag. You assemble; Josh approves; you
execute (see "On Josh's go").
# Context
- master == staging (continuous merge). Production is a v* tag, cut weekly here.
- Current version: latest git tag (`git tag --sort=-v:refname | head -1`)
- Release command: `release [version]` / `release` (auto-patch bump)
- The v* tag auto-builds ONLY the sideload APK; iOS TestFlight + Android Play AAB
are manual workflow_dispatch you fire AFTER cutting the tag.
- Tuesday's "Release readiness" preflight run should have surfaced and cleared
blockers — read its issue first if present.
- Hotfixes (issues labeled `hotfix`) ship out-of-band and are NOT this train's job.
- Store copy is pasted by hand into Play Console + App Store Connect — there is no
automated upload for it. The train's last act is handing Josh that copy as a
second comment; see docs/operations/store-release-notes.md.
# Steps (assemble the manifest — do NOT cut a tag here)
1. Diff master against the last tag: `git log <latest-tag>..master --oneline`.
If empty -> nothing to ship; skip (see Skip).
2. Map the commits to the Multica issues they shipped (the merged, staged work).
3. Check the gates: staging green (`deployed` shows head-of-master SHA), no fresh
Sentry spike, /health green, CHANGELOG [Unreleased] has content.
4. Propose the version: patch unless a semver-minor+ change shipped; lean conservative.
5. Create the train issue "Release train: week of <date>" assigned to **Josh**
(member, for go/no-go), with:
- Proposed version + rationale
- Manifest: every merged PR/issue since <last-tag> (one line each)
- Gate status (staging / Sentry / health / changelog), each pass/fail
- Any RED gate called out as a blocker with who clears it
- The go instruction: "Reply 'go' and reassign this issue to Vicki to ship,
or 'hold' with a reason. If the last App Store submission was rejected,
say so and name the guideline number(s) — Vicki cannot read App Store
Connect and needs it for the resubmission preamble."
6. Exit. Do NOT cut the tag in this run — wait for Josh's go.
# On Josh's go (when this issue is reassigned to you with "go")
Follow your "Weekly release train execution" final action in your agent
instructions: `deployed` -> `release <vX.Y.Z>` -> dispatch mobile-ios.yml +
mobile-android.yml on the new tag -> wait for production smoke -> post the
closing synopsis -> post the paste-ready mobile store copy as a second comment
(see docs/operations/store-release-notes.md) -> status done.
If Josh's go reply did not say whether the previous App Store submission was
rejected, ASK in that comment rather than guessing — never silently omit the
resubmission preamble.
# Skip this run if
No commits on master since the last tag (nothing to ship). Post a one-line
"empty train — nothing merged since <tag>" note in the run log; do NOT create
an issue.
# Never
- Cut a production tag in the assemble step — Josh's go is required first.
- Include `hotfix`-labeled work as a train item — those ship immediately, out of band.
- Propose a major bump without an explicit breaking change.
- Ship if a gate is RED — surface the blocker to Josh and hold the train.Why: During the beta, shipping every merged issue straight to production is too much change-surface. A weekly human-gated train batches the week's work into one reviewed release with a clear go/no-go, while staging stays continuously current for daily testing.
7b. Maggie — Weekly crew metrics
| Field | Value |
|---|---|
| Name | Maggie — Weekly crew metrics |
| Agent | Maggie |
| Schedule | Mondays 08:00 Chicago · cron 0 8 * * 1 |
| Output mode | Run only (posts one metrics summary; creates no issues) |
Runbook (paste into the form):
# Goal
Publish the crew's operational-health KPIs once a week so "is the crew
working?" has a quantitative answer, and drift is caught early. Targets
and definitions live in docs/product/multica-evaluation-addendum-prd.md
§9a (Crew operational health KPIs).
# Context
- Every PR shows the same GitHub author, so agent attribution comes from
the `[Agent]` PR-title prefix and `multica-agent:<name>` label Riley now
applies. If fewer than ~2 weeks of labelled PRs exist, report
"insufficient data" for the attribution-dependent KPIs rather than a
misleading number.
- You do NOT create issues from this autopilot. You post one summary
(run log + optional Slack/board comment). Surveillance only.
# Steps — compute over the trailing 7 days
1. Zero-human-follow-up rate: of agent-opened PRs merged this week, the %
with no human-authored commit after PR open. (Use the `[Agent]`/label
to identify agent PRs; `gh pr view --json commits`.) Target >=70%.
2. Median rounds-to-converge: median count of fix-rounds (commits matching
`fix(` / `test(smoke)` / `Round N` / `R<n>`) per shipped or escalated
ticket. Target <=2.
3. Human-rescue rate: % of PRs where head-branch agent != final-commit
author. Target <=20%.
4. Autopilot-issue no-reassign rate: % of autopilot-created issues
closed/shipped without a human re-routing them. Target >=80%.
5. Smoke/CI PR share: % of merged PRs touching tests/smoke-playwright/ or
.github/workflows/. Target <=10%.
# Output
Post a single summary with each KPI: value, target, and ▲/▼ vs last week.
Flag any KPI off-target with a one-line hypothesis. Keep it to one block.
# Skip this run if
No PRs merged and no autopilot issues closed in the trailing 7 days.
# Never
- Create issues from this autopilot.
- Report a fabricated number when attribution data is missing — say
"insufficient data" instead.
- Reassign or alter any issue/PR. This is read-only measurement.Why: The 2026-05 workflow audit found the crew had no quantitative health signal. This publishes the KPIs defined in ../product/multica-evaluation-addendum-prd.md §9a (zero-human-follow-up rate, rounds-to-converge, human-rescue rate, autopilot no-reassign rate, smoke/CI PR share) once a week. Reports "insufficient data" until ≥2 weeks of [Agent]-labelled PRs exist.
Tier 3 — Lower frequency, longer-horizon
7d. Maggie — Workflow efficiency audit monthly
| Field | Value |
|---|---|
| Name | Maggie — Workflow efficiency audit monthly |
| Agent | Maggie |
| Schedule | Monthly, 3rd 15:00 UTC · cron 0 15 3 * * (note: UTC, not the workspace-standard America/Chicago timezone used by every other entry in this file) |
| Autopilot ID | af29bbca-4368-4a0c-9115-99da9749ddd8 |
| Output mode | Create issue (proposal-only — assigned to Josh, never Maggie or a crew agent) |
Runbook (paste into the form):
# Goal
Once a month, audit the crew's actual hop history — the chain encoded in
Maggie's own routing table (User → Orion → [Desi →] Codi → Riley → Roy →
[Dori →] Vicki, with Dave/Tess pulled in for breaks) — for structural
inefficiency: routing-table rows or hard rules that in practice produce
more bounces, redundant hops, or escalations than the table intends. This
is a proposal-only autopilot: findings and proposed rule changes are
surfaced to Josh for explicit approval before any of Maggie's own
instructions change.
# Context
- This is a self-audit of Maggie's own CLAUDE.md routing table and hard
rules — the only autopilot with a mandate to propose changes to
Maggie's own operating rules. Even though Maggie edits her own state
(status/assignee) constantly, a structural change to the routing table
itself is a different order of action and needs a human sign-off — the
same "never bypass" posture CLAUDE.md already applies to routing
non-trivial work through Orion, applied reflexively to Maggie's own
rulebook.
- Distinguish from existing sibling autopilots so scope doesn't blur:
- **Maggie — Weekly crew metrics** publishes quantitative KPIs
(zero-human-follow-up rate, median rounds-to-converge, human-rescue
rate, etc.) on a fixed weekly cadence. It is read-only surveillance
and never proposes a rule change. If a KPI is off-target because of a
specific routing-table defect this audit identifies, cite the KPI as
corroborating evidence — do not duplicate its dashboard here.
- **Maggie — Issue health sweep** / **Maggie — Blocked review** act in
real time, per issue (nudge, escalate, wake, round-cap routing to
Orion). They unstick individual issues; they never analyze the
routing table as a system, and they never edit CLAUDE.md.
- **This autopilot** is monthly, retrospective, and SYSTEM-level: does
the chain itself, as written in the routing table, cause avoidable
hops — and if a pattern repeats, what is the minimal table/rule edit.
- Data sources for the trailing 30 days:
- `multica issue list --output json` filtered to issues created or
updated in the window.
- `multica issue comment list <id> --roots-only --summary --output json`
per issue to reconstruct assignment/status history (who routed to
whom, in what order, and how long each hop sat before the next
status or comment change).
- Markers already logged by sibling autopilots as a stronger signal
than raw hop-counting: `**Round cap:**` (Issue health sweep routing a
thrashing issue to Orion), `**Stall escalation:**` / `**Blocked
review:**` (genuine stalls, not routing defects — note but do not
treat as a routing-table finding on their own), and any loop-escalation
comment Maggie herself posted per CLAUDE.md's "Escalate loops" rule.
# Steps
1. Pull every issue created or updated in the trailing 30 days.
2. For each, reconstruct its hop sequence: agent → agent, in order, with
the dwell time at each hop (time between assignment and the next
status/comment change by that assignee).
3. Flag CANDIDATE inefficiencies — a candidate becomes a finding only if
it recurs (>=2 corroborating issue IDs) within the window; a single
instance is noise, not a system defect:
- A hop pattern that bounced between the same two agents more than
twice on one issue (loop) — cross-check that Maggie's own
loop-escalation comment (per CLAUDE.md's "Escalate loops" hard rule)
exists for each; if one is missing, that is itself a finding (the
escalation rule wasn't followed), not a table-design problem.
- A routing-table row that, in practice, is immediately bounced back a
majority of the time for a reason the table does not already
special-case (e.g. Riley routing back to Codi for a failure mode not
listed in the "Riley reports CI failure" rows) — suggests a missing
routing-table branch.
- A recurring `**Round cap:**` cluster in the same area (e.g. always a
specific test surface or component) — suggests the FIRST hop's
agent choice needs a table adjustment (e.g. Codi vs. Tess), not
just the one-off respec Orion already received.
- Hops that sat idle far longer than the rest of the month's median,
excluding declared wait-states already covered by Issue health
sweep / Blocked review — note as a capacity/readiness observation,
explicitly NOT a rule-change proposal (a slow agent is not a routing
defect).
4. For each finding that clears the >=2-instance bar, draft ONE minimal
proposed edit: a routing-table row, a hard rule, or a GSD-command
mapping — cite the exact CLAUDE.md section and a before/after snippet
of the proposed text. Do not bundle unrelated wording cleanup into a
proposal.
# Decision
- No finding clears the >=2-instance bar → exit silently, no issue. Post
a "clean month" note in the run log with the raw counts (issues
scanned, hops computed, loops detected, round-caps hit) so the next
run has a comparison baseline.
- Any finding clears the bar → dedupe first: search open issues titled
"Workflow efficiency audit" (`multica issue list`). If one from this
cadence is already open, comment the new findings onto it instead of
creating a duplicate.
- Otherwise create ONE issue, titled per the autopilot's template,
**assigned to Josh** (a human member, by email joshuarlockhart@gmail.com
or member lookup) — not to Maggie or any crew agent. This mirrors the
**Vicki — Weekly release train** pattern: Maggie assembles the
analysis, but the human approves before anything changes, because the
change under discussion is Maggie's own operating rulebook.
- State explicitly in the issue: "No CLAUDE.md or routing-table edit has
been made. Reply approving a specific numbered proposal (or 'approve
all') and I will make that edit in a follow-up comment and confirm."
Never edit CLAUDE.md as part of this autopilot's own run, regardless of
how confident a finding is.
# Issue body template
- **Audit window:** trailing 30 days, dates, issue count scanned.
- **Summary table:** issues scanned | hops computed | loops detected |
round-caps hit | median hops-to-ship.
- **Findings (proposals):** one numbered block per pattern that cleared
the >=2-instance bar — pattern description, motivating issue IDs, the
exact CLAUDE.md section, and a before/after snippet of the proposed
edit.
- **Observed but not proposed:** single-occurrence oddities and
capacity/readiness notes — listed so Josh knows they were seen and
deliberately not escalated into a proposal, with a one-line reason
each.
- **Approval instructions:** exactly how to approve (reply with the
proposal number(s), or "approve all"); restate that no edit exists yet.
# Skip this run if
Fewer than 5 issues were created or updated in the trailing 30 days —
too little data to separate a system defect from noise. Post a one-line
"insufficient data" note in the run log; do not create an issue.
# Never
- Edit CLAUDE.md, the routing table, or any skill file directly from this
autopilot's own run. Proposal only, every run, no exception — even for
a finding that feels obviously correct.
- Promote a finding to a proposal on a single motivating issue — require
at least 2 corroborating instances in the trailing-30-day window.
- Bundle an unrelated rewording or cleanup into a cited proposal — one
proposal, one cited defect (with issue IDs), one minimal edit.
- Create a duplicate issue while a "Workflow efficiency audit" issue from
the current cadence is still open — comment onto it instead.
- Assign the output issue to Maggie or any crew agent. Always Josh — this
proposes a change to Maggie's own operating instructions, not a unit of
crew work.
- Duplicate `Maggie — Weekly crew metrics`'s KPI dashboard. This autopilot
analyzes routing-table STRUCTURE (which rows/rules cause hops); the
weekly autopilot measures agent THROUGHPUT (how fast/clean hops
execute). An off-target KPI is only this autopilot's finding if a
specific, cited routing-table defect explains it.
- Treat a capacity/readiness delay (an agent being slow) as a routing-rule
defect — only propose an edit when the routing TABLE itself, not agent
speed, is the cause.Why: Maggie's routing table is the crew's only coordination mechanism, and nothing else in the fleet checks whether the table itself — not any one slow agent — is the cause of repeated bounces or loops. This is a proposal-only self-audit, same human-go posture as Vicki — Weekly release train: findings always land on Josh, never on Maggie or a crew agent, because the change under discussion is Maggie's own operating rulebook.
8. Vicki — Autonomy promotion review
| Field | Value |
|---|---|
| Name | Vicki — Autonomy promotion review |
| Agent | Vicki |
| Schedule | Quarterly (1st of Jan/Apr/Jul/Oct, 09:00 Chicago) · cron 0 9 1 1,4,7,10 * |
| Output mode | Create issue |
Runbook (paste into the form):
# Goal
Quarterly audit of AI Workforce autonomy levels — promote eligible
employees, flag at-risk ones, confirm shadow-locked roles stay locked.
# Context
- Feature doc: docs/features/ai-workforce.md
- Runbook: docs/operations/ai-workforce-runbooks.md — Process 4
- Promotion criteria are enforced at the model level by
`validate :promotion_requirements_met` on AiEmployee
- Levels: shadow → semi → autonomous
- Ally role is shadow-locked by design; never promote
# Steps
1. For each AiEmployee in the workspace, pull:
- current autonomy level
- weeks active (since created_at)
- last 30 days approval rate (approved / total artifacts)
- last 30 days rejection count
- current `promotion_requirements_met` validation result
2. Classify each employee:
- PROMOTE: validation passes, approval rate ≥ 90%, weeks_active
meets threshold, zero recent hard rejections
- HOLD: validation passes but approval rate 70–89% — review memory/
skill config before promoting
- DEMOTE OR PAUSE: approval rate < 70%, or >3 hard rejections in
last 30 days
- LOCKED: Ally role — confirm still at shadow, flag if not
3. Create an issue titled "Autonomy promotion review: Q<n> <year>" with:
- Table of all employees: name, role, current level, classification,
approval rate, rejections, recommendation
- For PROMOTE: the exact mutation/admin click-path to execute
- For HOLD/DEMOTE: specific next step (memory tune, skill audit,
pause reason)
4. Assign to Maggie — she'll either route back to you for execution
(if straightforward) or to Orion for a re-plan (if multiple
employees are struggling).
# Skip this run if
No AiEmployees exist yet in the workspace, OR the AI Workforce feature
is paused.
# Never
- Promote without the validation passing.
- Promote an Ally role out of shadow.
- Execute the promotion from the autopilot — human-confirmed only.Why: Ties directly to Process 4 in the AI Workforce runbooks we just shipped. Makes the quarterly cadence real instead of aspirational.
9. Dori — Brand voice sweep
| Field | Value |
|---|---|
| Name | Dori — Brand voice sweep |
| Agent | Dori |
| Schedule | 1st of each month, 09:00 Chicago · cron 0 9 1 * * |
| Output mode | Create issue |
Runbook (paste into the form):
# Goal
Catch off-brand user-facing copy added since the last sweep, route
fixes to Desi (in-component) or you (docs/emails).
# Context
- Brand guide: docs/brand/brand.md
- Voice: coach-who's-also-a-friend. Action-forward, brief, never
generic-motivational. Celebratory without empty praise.
- Never-use list: "AI" in consumer UI (say "Coach"), generic
motivational phrases ("You can do it!", "Keep pushing!")
- Surfaces that are in scope:
- Vue component strings (ionic_frontend/src/**/*.vue) — route to Desi
- Mailer templates (rails_api/app/views/mailers/) — route to yourself
- Push notification copy (rails_api/app/services/*_delivery.rb) —
route to yourself
- marketing_landing/index.html — route to yourself
- docs/ — route to yourself
# Steps
1. Get all user-facing string changes since last month:
`git log --since="1 month ago" --diff-filter=AM -p -- <scope>`
for each scope above.
2. Extract candidate strings (button labels, headings, modal text,
email subjects, push bodies, marketing hero copy).
3. For each candidate, evaluate:
- Action verb lead? (good)
- Brief (≤1 sentence)? (good)
- Contains "AI" in consumer context? (violation)
- Generic-motivational phrase? (violation)
- Off-voice (corporate, enterprise-y, hype)? (violation)
4. Build a punch list grouped by surface + routing target.
5. Create an issue titled "Brand voice sweep: <month> <year>" with:
- Violation count by severity
- Each violation: file:line, current copy, why it's off, suggested
rewrite
- Routing: which items go to Desi (in-component) vs you (docs/
mailers/push/marketing)
6. Assign to Maggie.
# Skip this run if
No user-facing string changes in the last month. Post a "clean sweep"
comment in the run log; don't create an issue.
# Never
- Rewrite strings yourself for in-component copy — that's Desi's scope.
- Audit internal/admin strings (admin_dashboard/) — not consumer-facing.
- Audit test fixtures or seed data.Why: Brand voice drift is invisible day-to-day but cumulative. Monthly catch + routing keeps it tight.
10. Orion — Activation funnel check
| Field | Value |
|---|---|
| Name | Orion — Activation funnel check |
| Agent | Orion |
| Schedule | Mondays 10:00 Chicago · cron 0 10 * * 1 |
| Output mode | Run only (create issue only on a drop) |
Runbook (paste into the form):
# Goal
Early-warning on activation regressions. Week-over-week funnel data →
scoped issue routed to the right phase owner when a step drops
meaningfully.
# Context
- Funnel steps (in order):
1. Sign-up complete → `signed_in`
2. First-run complete (wizard) → `wizard_completed`
3. First goal created → `first_goal`
4. First check-in / goal event → `first_check_in`
5. First badge earned → `first_badge`
- Data source: POST https://api.objectuve.com/integrations/activation-funnel
An HMAC-authenticated Rails endpoint that computes the 5-step funnel
server-side from UserAction / Goal / GoalEvent records and returns
week-over-week deltas. This replaces the old "pull PostHog events +
UserAction rows directly" approach — the agent sandbox has no DB
access and no PostHog secrets, so it cannot do that.
- Auth: header `X-Workforce-Signature: <hex>` where
`<hex> = HMAC-SHA256($RAILS_WEBHOOK_SECRET, request_body)`. The secret
comes from the autopilot environment — never hardcode it.
- Relevant skills: improving-activation-flow, tracking-product-events,
accelerating-first-run
- "Meaningful drop" threshold: a wow_pct of -10% or worse at any step.
# Response shape
A single JSON object:
{
"as_of": "2026-05-20T12:00:00Z",
"window_days": 7,
"current": { "signed_in": N, "wizard_completed": N, "first_goal": N,
"first_check_in": N, "first_badge": N },
"previous": { ...same 5 keys, the prior 7-day window... },
"wow_pct": { ...same 5 keys: ((current-previous)/previous*100)
rounded to 1dp; null when previous == 0... },
"skip_run": false
}
- `current` = the 7 days ending at `as_of`.
- `previous` = the 7 days before that (7–14 days before `as_of`).
- `wow_pct` = WoW % change in the count of users reaching each step.
Negative = drop. `null` = no baseline (previous == 0).
- `skip_run: true` = zero sign-ins this window; no users to measure.
# Steps
1. Build the request body: exactly `{}` (empty JSON object — required so
the HMAC has bytes to sign).
2. Compute the signature:
sig=$(printf '%s' "$BODY" | openssl dgst -sha256 \
-hmac "$RAILS_WEBHOOK_SECRET" | awk '{print $2}')
3. POST:
curl -fsS -X POST \
-H "Content-Type: application/json" \
-H "X-Workforce-Signature: $sig" \
-d "$BODY" \
https://api.objectuve.com/integrations/activation-funnel
4. Parse the JSON response.
5. Decision:
- `skip_run: true` → exit silently (no signups in the last 7 days).
- Every `wow_pct` value within ±10% (treat `null` as "no baseline",
within range) → exit silently.
- Any step's `wow_pct` is -10% or worse → investigate.
6. If investigating:
a. Identify the failing step. If several consecutive steps dropped by
a similar magnitude, the regression is at the EARLIEST dropped
step — downstream counts cascade from it (a 20% drop in signed_in
mechanically drags every later step down ~20% with no real
conversion regression). Attribute to the earliest dropped step.
b. Confirm it is not a one-off. Check this autopilot's prior run
(the previous "Activation funnel check" run log / issue). A
genuine regression shows the same step down >10% on consecutive
weekly runs; a single bad day ages out of the 7-day window by the
following week. If the drop appears for the first time this run and
the step is otherwise healthy, HOLD — let the next weekly run confirm.
c. Pull recent changes that touched the relevant code surface
(`git log` on the enkidu repo, last 30 days maximum):
- Step 1 (signed_in) → Clerk / SyncUser / router guard
- Step 2 (wizard_completed) → ionic_frontend/src/views/welcome/ (WelcomeLayout.vue, DoneSlide.vue) / useOnboardingGate.ts
- Step 3 (first_goal) → GoalTracking::AddGoal / goal creation UI
- Step 4 (first_check_in) → goal event mutations / check-in UI
- Step 5 (first_badge) → Gamification::* / badge trigger logic
d. Correlate the drop with a specific merge / deploy date.
7. Create an issue (only if a drop is confirmed) titled
"Activation funnel drop: step <N> (<step name>) <X>% WoW" with:
- The funnel table (current vs previous counts + wow_pct, per step)
- The suspect change(s) with commit SHA + date
- Routing recommendation: Dave (if it looks like a bug) or Orion
(if it looks like a UX regression that needs re-planning)
8. Assign the new issue to Maggie.
# Skip this run if
- The response has `skip_run: true` (no signups in the last 7 days), OR
- Every `wow_pct` value is within ±10%.
Silent run — log a one-line "no meaningful drop" note; do NOT create a
no-op issue.
# On endpoint failure
- 401 → signature/secret problem. Verify `$RAILS_WEBHOOK_SECRET` is set
in the environment. Log and exit; do not retry blindly.
- 400 → malformed body. The body must be exactly `{}`. Log and exit.
- 422 → server-side compute failure. Log and exit; the next scheduled
run retries.
- Network error / 5xx → log and exit; the next run retries.
Never fabricate numbers when the endpoint is unreachable.
# Never
- Propose a fix yourself. You're the detective, not the surgeon.
- Treat a one-off anomaly as a trend. The 7-day window smooths daily
noise; still confirm the drop persists across consecutive runs before
filing (see step 6b).
- Cross-reference beyond the last 30 days — long-term trends need a
dedicated analysis, not this autopilot.
- Hardcode `RAILS_WEBHOOK_SECRET`. It comes from the autopilot
environment.
- Fabricate funnel numbers if the endpoint fails. A failed run is
silent; a guessed run is harmful.Why: Activation is the engine. A quiet regression at any step compounds into a retention problem. Weekly early-warning catches it before you feel it in revenue.
11. Dori — Knowledge base lint
| Field | Value |
|---|---|
| Name | Dori — Knowledge base lint |
| Agent | Dori |
| Schedule | 15th of each month, 09:00 Chicago · cron 0 9 15 * * |
| Output mode | Create issue per gap (assign each to Maggie) |
Runbook (paste into the form):
# Goal
Monthly structural lint of docs/: find thin pages (stubs, placeholders),
missing conceptual connections between related pages, and undocumented
discoveries that are still buried in Multica issue comments.
# Context
- Docs root: docs/
- This is a structural audit, not a staleness check (that's the weekly
docs drift audit). The question here is: what's MISSING or THIN, not
what's stale.
- Complement to: the compiler-based knowledge base pattern — raw issue
comments should be compiled into durable docs, not left in the thread.
# Steps
1. Scan docs/ for thin pages:
grep -rn "TODO\|FIXME\|placeholder\|coming soon\|stub\|\[\]\|TBD" docs/ --include="*.md"
Flag any page where >30% of sections are empty, placeholder, or
marked TODO.
2. Scan for undocumented discoveries: check recent Multica issue comments
(last 30 days) from Dave (foot-guns, gotchas) and Orion (planning
deviations, architectural decisions). Look for non-obvious insights
that haven't made it into docs/ yet.
- Use: multica issue list --assignee "Dave" --status done --since 30d
- Use: multica issue list --assignee "Orion" --status done --since 30d
Compile any raw discoveries into a doc page or a "Common Gotchas"
entry — don't leave them in issue comments.
3. Check cross-reference gaps: identify docs/ pages that are closely
related but don't link to each other (e.g., authentication.md
and api-reference.md, or testing.md and observability.md).
4. For each gap found, create one Multica issue:
- Title: "Docs lint: <specific gap> (<month> <year>)"
- Body: what's thin/missing, which files, suggested content or link
- Assign to Maggie (she routes doc fixes back to you).
5. If no gaps: post a clean-lint comment in the run log; don't create issues.
# Skip this run if
No stubs, no undocumented discoveries, no missing connections. Post
"clean lint — no gaps found" comment in the run log.
# Never
- Fix everything inline in this autopilot run — one issue per gap so
Maggie can triage priority.
- Touch code files. This is docs-only.
- Create more than 10 issues per run — if there are that many gaps,
create a meta-issue summarising the backlog instead.Why: The weekly drift audit catches staleness. This monthly pass catches structural gaps — thin pages and raw discoveries buried in issue threads that never got compiled into durable docs. Inspired by compiler-based knowledge base pattern (raw → wiki).
11b. Maggie — Backlog grooming
| Field | Value |
|---|---|
| Name | Maggie — Backlog grooming |
| Agent | Maggie |
| Schedule | 1st of each month, 09:15 Chicago · cron 15 9 1 * * |
| Output mode | Run only (files one triage issue only when stale items exist) |
Why
:15, not:00: Maggie runs at concurrency 1. The Backlog sweep (#3,0 9,13 * * 1-5) fires at0 9on weekday 1st-of-months. Scheduling grooming at0 9too would put two Maggie autopilots on her single slot at the same fire-minute — the exact oversubscription that hung runs inrunningand forced #3b to:30.:15avoids both the:00sweep and the:30issue-health sweep. Per the concurrency rule below, never schedule two of one agent's autopilots at the same fire-minute.
Runbook (paste into the form):
# Goal
Keep the active issue set manageable. Once a month, surface backlog issues
that have gone stale (no activity in 90+ days) so a human can make a
keep-or-cancel decision — preventing the backlog from silently
accumulating dead follow-ups. (Closed issues are already excluded from
`issue search` by default, so they are NOT the problem; the live backlog is.)
# Context
- "Stale backlog" = status `backlog` with `updated_at` older than 90 days.
- You do NOT cancel anything yourself — humans decide keep vs cancel. You
surface candidates in ONE triage issue; never file one issue per item.
- This is distinct from the Backlog *sweep* — Pass 1 routes fresh todo
items, Pass 2 triages `backlog` items and pulls a WIP-capped number into
the chain but explicitly excludes items stale 90+ days as this
autopilot's territory, so the two never issue competing verdicts on the
same item — and from the Stalled-issue sweep (which unsticks
in_progress/in_review).
# Steps
1. `multica issue list --status backlog --limit 200 --output json`.
2. Filter to items whose `updated_at` is older than 90 days from now.
3. If none → exit silently (one-line "backlog clean" note in the run log;
do NOT create an issue).
4. If any → create ONE issue titled
"Backlog grooming: <N> stale items (90+ days)" assigned to Maggie, body:
- A table per stale item: key, age (days), last assignee, title.
- A recommended action per item: keep / cancel, with a one-line reason.
- Grouped by apparent theme (infra follow-up, post-launch hardening,
proposer idea, doc debt, …) so the human can batch-decide.
# Skip this run if
No backlog items older than 90 days.
# Never
- Cancel or alter any issue yourself — this is surfacing only; humans decide.
- File more than one issue per run.
- Touch issues in any status other than `backlog`.
- Count closed (done/cancelled) issues — they are already out of search.Why: Keeps the active issue set manageable. Closed issues are already hidden from issue search by default, so the lever for a manageable board is the live backlog, which silently accumulates dead follow-ups. This surfaces 90-day-stale backlog items for a human keep/cancel decision once a month — without auto-cancelling anything.
Strategic proposer autopilots
Penny and Sage run on autopilot to generate backlog issues for the rest of the crew to act on. Distinct from the executor autopilots above — these don't operate on existing work, they propose new work.
12. Penny — Product strategy monthly
| Field | Value |
|---|---|
| Name | Penny — Product strategy monthly |
| Agent | Penny |
| Schedule | 1st of each month, 09:00 Chicago · cron 0 9 1 * * |
| Output mode | Create issue (per Penny's design she creates 0–N issues + optional PRD stubs) |
Runbook (paste into the form):
# Goal
Surface high-quality product opportunities and competitive intel as backlog issues, monthly. No noise; high signal only.
# Context
- North star: cumulative goals completed (docs/product/north-star.md)
- PBC obligations: never paywall free tier, no engagement-juicing
- Existing competitive analysis: docs/product/competitive/
- Existing PRDs: docs/product/*-prd.md, docs/product/prd-index.md
- Roadmap: docs/product/roadmap.md, .planning/ROADMAP.md
# Steps
1. Pull this month's product signals:
- New competitive intel (check 3-5 competitor sites + relevant subreddits/HN/PH)
- Activation funnel deltas vs prior month (PostHog if available, else skip)
- User feedback in the Multica board (issues with "feedback" label)
- Recent shipped milestones — what did they unblock?
2. For each signal, evaluate:
- Does it align with the north star (more goals completed)?
- Does it respect PBC obligations (no paywall, no engagement-juice, no dark patterns)?
- Is there a clear business OR consumer rationale?
3. For each signal that passes, decide output type:
- **Minor enhancement / quick win:** create a Multica issue with rationale; no PRD needed.
- **Major feature / multi-phase work:** create a Multica issue + draft a PRD stub at `docs/product/<slug>-prd.md` marked `Status: Penny-proposed; needs human greenlight`.
- **Competitive intel only (no proposal):** update `docs/product/competitive/` with the finding; no issue.
4. **Cap at 3 proposals per run.** Rank candidates by `(north-star impact × consumer rationale strength) / engineering cost` and keep only the top 3. If you have more candidates worth surfacing, append a single comment on the most recent proposal listing the deferred candidates with one-line summaries — they roll into next month's eval.
5. Create issues in `backlog` status, assigned to **Roy** (NOT Maggie). Roy reviews each proposal for PBC fit, brand voice, and north-star alignment, then either reassigns to Maggie for chain entry or cancels the issue with a comment explaining why. This keeps the proposer/reviewer boundary intact — you propose, Roy gates, Maggie routes.
# Issue body template
- **Opportunity:** one sentence
- **Business rationale:** revenue / retention / margin impact, with the math
- **Consumer rationale:** what user need it serves, in their voice
- **North-star alignment:** how it increases goals completed
- **PBC check:** confirm no paywall / engagement-juice / dark pattern
- **Suggested next step:** route to Orion to plan, OR keep in backlog for next quarter
- **References:** competitor links, PRD path if drafted
# Skip this run if
No new signals worth proposing this month. Post a one-line "clean month" comment in the run log; do NOT create a stub issue just to have output.
# Never
- Propose features that paywall the free tier
- Propose features that increase time-on-app as a goal
- Create more than 3 issues in a single run (hard cap; ranked top-3 only — deferred candidates go in a comment on the most recent proposal)
- Skip the rationale fields — every issue needs business + consumer reasoning
- Self-assign or assign to Maggie — proposals route through **Roy** for review before chain entry13. Sage — System architecture monthly
| Field | Value |
|---|---|
| Name | Sage — System architecture monthly |
| Agent | Sage |
| Schedule | 1st of each month, 09:00 Chicago · cron 0 9 1 * * |
| Output mode | Create issue (per Sage's design she creates 0–N issues + optional architecture-proposal stubs) |
Runbook (paste into the form):
# Goal
Surface architectural debt, DDD evolution opportunities, and codebase-sustainability risks as backlog issues, monthly. Lean toward fewer, higher-quality proposals over a long list.
# Context
- DDD documentation: docs/architecture/ (especially ddd-* files)
- Codebase intel (if exists): .planning/intel/
- Architecture conventions: CLAUDE.md, docs/architecture/
- Active milestones: .planning/milestones/, .planning/ROADMAP.md
- Recent Dave investigations: search Multica for issues assigned to Dave in last 30 days
# Steps
1. Sample the codebase health:
- Test coverage trends (rails_api SimpleCov, ionic_frontend Vitest v8)
- N+1 patterns (grep for raw .find / .where without .includes)
- File-size and complexity outliers
- DDD bounded-context drift (services/models reaching across contexts)
- Dependency-staleness signals (compare current vs latest for major libs)
2. Re-read DDD docs and current implementation. Are they still aligned? Has the code drifted past the documented design?
3. Cross-reference recent Dave issues — are there recurring root causes pointing to architectural debt?
4. For each finding, decide output type:
- **Tactical refactor (single PR, no design change):** create a Multica issue with the diagnosis + suggested fix.
- **Strategic refactor (multi-phase, milestone-class):** create a Multica issue + draft an architecture-proposal stub at `docs/architecture/proposals/<slug>.md` marked `Status: Sage-proposed; needs human greenlight`. Flag as `epic: candidate`.
- **DDD evolution (changes the documented design):** draft an architecture-proposal stub AND a follow-up issue suggesting a dedicated review session.
5. **Cap at 3 proposals per run.** Like Penny, prioritize ruthlessly; queue overflow as comments rather than new issues.
6. Create issues in `backlog` status, assigned to **Roy** (NOT Maggie). Roy reviews each proposal for sustainability, DDD coherence, and PBC fit, then either reassigns to Maggie for chain entry or cancels with a comment. Proposer/reviewer boundary preserved.
# Issue body template
- **Concern:** one sentence
- **Evidence:** specific file paths, metrics, or recurring Dave issue links
- **Sustainability rationale:** how this affects free-tier infra economics, dev velocity, on-call burden
- **Proposed evolution:** concrete refactor / re-architecture
- **Migration path:** phased steps that can ship incrementally
- **Risk if deferred:** what compounds if we ignore this
- **PBC check:** confirm proposal doesn't compromise free-tier infra capacity
- **References:** DDD doc paths, related milestones, related Dave issues
# Skip this run if
No findings worth proposing this month. Post a "clean month" comment in the run log.
# Never
- Propose architecture that compromises free-tier infra economics (PBC obligation)
- Propose rewrites for their own sake — only when current architecture is causing measurable pain
- Create issues that mix tactical + strategic concerns; split them
- Skip the "risk if deferred" field — every architectural proposal needs the cost of inaction
- Create more than 3 issues in a single run (hard cap; overflow into comments)
- Self-assign or assign to Maggie — proposals route through **Roy** for review before chain entry14. Roy — Bastion security audit monthly
| Field | Value |
|---|---|
| Name | Roy — Bastion security audit monthly |
| Agent | Roy |
| Schedule | 22nd of each month, 09:03 Chicago · cron 3 9 22 * * |
| Autopilot ID | 2c77e88d-4e11-47a5-846c-370d864e390d (trigger 699e56b4-3505-4a9c-8806-fbef78a71325) |
| Output mode | Create issue (only on new findings; dedupe against the prior run's open issue first) |
| Report artifact | See Audit report artifact (pillar autopilots) — writes .planning/audits/bastion-<YYYY-MM-DD>.md per run. |
Runbook (paste into the form):
# Goal
Run a full `objectuve-bastion` sweep (all 38 gates) across every trust-boundary
surface in rails_api/, monthly. Not rotated — unlike the Keystone architecture
autopilot's 2-of-14-domain rotation, bastion's gates are diff/call-site scoped,
not whole-domain, so a full sweep is cheap enough to run every time.
# Context
- Skill: .claude/skills/objectuve-bastion/ (SKILL.md + references/bastion-gates.md)
- Scope: GraphQL mutations/resolvers, app/controllers/graphql_controller.rb's
three current_user resolution paths, webhook/integration receivers,
token-issuing/verifying services (ClerkJwtVerifier, StepUpTokenVerifier,
Gdpr::ExportDownloadToken, VerifyMagicCode/MagicCode), rack_attack.rb +
AdminIdentityResolver, secret-bearing env vars.
- This complements — does not replace — Roy's real-time diff-scoped bastion
check on every PR touching a security-sensitive path (see the "Security-
sensitive diffs" section of multica-agent-crew.md). This autopilot is the
full-surface monthly backstop for drift that no single PR's diff would
trigger (e.g. a gate that regresses across several small, individually
unremarkable changes).
- Routing boundary: only file findings that are bastion's lens (trust-boundary
correctness) — see SKILL.md's "Routing rule: bastion vs. keystone vs.
signal". Do not duplicate a Keystone or Signal finding under a different
label.
# Steps
1. Run `sweep` first to confirm the registry surface list is current
(`sweep` runs `scripts/capture-bastion-evidence.mjs` against the
`scripts/bastion-surfaces.mjs` registry) — do not audit against a stale
surface list.
2. Run `audit --all` — full sweep, every registered surface.
3. Score each finding against the Blocking criteria in SKILL.md (client-
supplied id bypassing public_find, admin surface missing require_admin!,
webhook processing before signature verification, replayable single-use
token, secret value logged/echoed).
4. Group findings by severity (blocking / should-fix), each with file:line,
the gate number, and a concrete fix.
# Decision
- No findings → exit silently, no issue. Post a "clean month" note in the run
log.
- Any findings → dedupe first: search open issues for title containing
"Bastion security audit" (`multica issue list`). If one is already open,
comment the new findings onto it instead of creating a duplicate — do not
spawn a second parallel issue for the same monthly cadence.
- Otherwise create ONE issue covering all findings from this run (don't file
one issue per finding), assigned to **Maggie** for routing (matching every
other `create_issue`-mode autopilot in this file — see 2b's precedent),
with **Roy** tagged in the issue body for re-review once Codi's fix lands.
- If any finding is **(blocking)** per SKILL.md's criteria, say so explicitly
in the issue title/summary — don't bury a blocking finding inside a mixed
list of should-fixes.
# Issue body template
- **Sweep date / scope:** full `--all` sweep, gate count checked
- **Findings:** grouped blocking / should-fix, each with file:line, gate #,
concrete fix
- **Evidence:** the real grep/read result or call site backing each finding —
never an invented citation (bastion's Evidence-backed self-critique axis)
- **Re-review:** tag Roy once Codi's fix branch is up
# Skip this run if
`objectuve-bastion`'s registry sweep (`sweep`) can't resolve — post a BLOCKED
comment on the prior month's issue (or a new one if none exists) noting which
surface failed to resolve, rather than silently skipping.
# Never
- Rotate or sample surfaces — always `--all`, every run (unlike Keystone).
- File a finding that's actually keystone's or signal's lens — see the
routing rule in SKILL.md before creating the issue.
- Create a duplicate issue while a "Bastion security audit" issue from a
prior run is still open.
- Self-fix. This autopilot is diagnostic — Codi makes the code change, Roy
re-reviews.
- Soften a genuine (blocking) finding into a should-fix to keep the report
short.Why: Roy's real-time PR review already runs a diff-scoped objectuve-bastion pass on security-sensitive changes (see multica-agent-crew.md), but that only catches drift visible in a single PR's diff. A full, unrotated monthly sweep closes the gap for regressions that accumulate across several small changes, none of which individually would have tripped the diff-scoped check. Unlike the Codi — Keystone architecture audit monthly autopilot (2-of-14-domain rotation — a 56-gate whole-domain score is too expensive to run monthly in full; see #17 below), bastion's 38 gates are scoped to specific call sites and surfaces rather than entire architectural domains, so a full sweep every month is cheap enough to not need rotation. Escalation follows the same Maggie-mediated routing every other autopilot in this file uses: the issue is assigned to Maggie, who routes it to Codi for fixes with Roy tagged in the body for re-review once the fix lands — matching how Roy's own real-time SECURITY-FINDING: BLOCK verdict always finishes by assigning back to Maggie (see multica-agent-crew.md's "routed back through Maggie same as any other BLOCK"), never directly to Codi.
Framework audit autopilots (the remaining five pillars)
Roy's Bastion sweep above (#14) and Dori's Docs drift audit above (#6, objectuve-atlas) were the first two framework-tied autopilots documented in this file. The other five objectuve-* pillars (see quality-audit-frameworks.md) each have their own recurring autopilot, live in the workspace since between 2026-07-07 and 2026-07-22 but not previously written up here. All five follow the same shape as Bastion's and Atlas's: report-only, one triage issue per run, assigned to Maggie for routing, never self-fixed.
15. Desi — Design-debt audit monthly (objectuve-hallmark)
| Field | Value |
|---|---|
| Name | Desi — Design-debt audit monthly |
| Agent | Desi |
| Schedule | 1st of each month, 09:03 Chicago · cron 3 9 1 * * |
| Autopilot ID | 667e4ca1-e790-46f7-beb4-de16a766b819 (trigger b327cc05-a5d5-4c8d-a1ee-f4865f9726d4) |
| Output mode | Create issue |
| Report artifact | See Audit report artifact (pillar autopilots) — writes .planning/audits/hallmark-<YYYY-MM-DD>.md per run. |
Runbook (paste into the form):
# Goal
A monthly, read-only design-debt audit of every shipping app surface,
scored weakest-first against the locked objectuve-hallmark design system.
Output is a single triage issue Maggie routes into the chain — not a
change request, and no code is ever touched.
# Context
- You are Desi. This is a full re-score every run (not a diff sweep).
- Required reading — load BEFORE auditing, every run:
1. `objectuve-hallmark` skill (full skill at `.claude/skills/objectuve-hallmark/`)
— the locked design system, the four universal disciplines, the
P/H/E/S/R/V rubric, the 70-gate slop-test, and the audit verb.
2. `docs/reference/desi-design-context.md` — tokens (HSL), spacing
scale, type ramp, shadows, glassmorphism, dark-mode parity, motion,
brand-voice microcopy templates, anti-patterns.
3. `docs/brand/brand.md` — brand voice (coach-who's-also-a-friend).
4. `ionic_frontend/STYLE_GUIDE.md` — token/style conventions.
- Scope: every shipping surface under `ionic_frontend/src/views/**`,
including `welcome/slides/**` and `profile/slides/**`. Exclude
logic-only routing wrappers (Layout/View pairs) from scoring — note
them as "no visual surface". Exclude anything flag-gated out of the
production bundle (e.g. `DevClerkSpike.vue`), noting why.
- Line refs are against the current `master` HEAD. Record the short SHA
and the app version (from `ionic_frontend/package.json`) in the header.
- Apply policy: READ-ONLY. Never edit a `.vue` file, a token, or a
style. Never run a build/lint/deploy. This is surveillance, not a fix.
# Steps
1. Resolve `master` HEAD short SHA + app version for the header line.
2. Enumerate every in-scope surface. Score each on a 1–10 scale using
the P/H/E/S/R/V rubric and the 70-gate slop-test, weakest-first.
For each low score, capture the single headline problem with a
`file.vue:line` anchor.
3. Cluster findings into the reference sections (below). Prioritize
SYSTEMIC patterns — a token/theming defect that recurs across N
surfaces is one cheap batch fix, not N tickets.
4. Separate genuine defects (renders wrong / silent failure / a11y
violation) from polish (off-ramp sizes, token nits). Genuine bugs
lead.
5. Identify honest-UI / brand-voice / anti-social flags (fake
affordances, dead "coming soon" controls, TODO markers shipped to
master, off-voice native dialogs, duplicated copy). Note passing
disciplines too — call out what's healthy so the report is honest.
6. Name the STRONG surfaces (8–9) as the patterns to copy from.
7. For the 2–3 weakest surfaces, if the dev server runs headlessly,
attach light+dark screenshots per the `ui-evidence-and-critique`
protocol. If it can't run cleanly, cite `file:line` only — never
block the report on screenshots.
# Issue structure (mirror the reference audit)
Title: handled by the autopilot template ("Design-debt audit — <date>").
Body, in this order:
- Header line: **Author:** Desi · **Scope:** N surfaces · **Method:**
objectuve-hallmark 70-gate + P/H/E/S/R/V, weakest-first · read-only ·
line refs vs `master @ <sha>` (v<version>).
- One sentence stating this is a triage list, not a change request.
- **TL;DR — weakest surfaces**: a table (Surface | Score | Headline
problem) of the rework candidates.
- **Systemic patterns (fix once, lift many)**: numbered, highest-impact
theming/token/state defects first, each with the list of offender
`file:line`s.
- **Genuine bugs surfaced by the audit**: renders-wrong / silent-failure
/ a11y, with anchors.
- **Honest-UI / voice / anti-social flags**: including the positives
that pass.
- **Strong end of the range**: the 8–9 surfaces to copy from.
- **Full score table (all surfaces)**: score → surfaces.
- **Suggested next step**: the 1–3 clusters with outsized impact
(usually a cross-surface token/theming decision) that Maggie should
route first.
# Routing
Create ONE issue per run in status `todo`, assigned to **Maggie**, so
her routing sweep promotes individual clusters into the Orion → Desi/Codi
chain. Do NOT self-assign, and do NOT promote findings into the chain
yourself — you author the audit; Maggie routes it.
# Skip / exit conditions
A full audit always produces findings, so there is no "clean run" skip.
But if `ionic_frontend/src/views/**` is unreadable (bad checkout,
detached tree), post a one-line run-log note explaining why and file no
issue rather than a half-audit.
# Never
- Edit any `.vue`, token, style, or config file. Read-only, always.
- Run a build, lint, format, or deploy.
- Promote a finding into the chain yourself, or open per-finding tickets
— one triage issue per run; Maggie fans it out.
- Invent design opinions outside the locked objectuve-hallmark system —
score against the rubric, not personal taste.
- Block the report on screenshots — evidence is best-effort, the
file:line audit is the deliverable.
- Duplicate the Dori brand-voice sweep: flag in-component voice only
where it intersects a visual/state defect; pure copy nits are Dori's.Why: Real-time UI review (Roy's diff spot-check, Desi's own UI-SPEC fidelity) only ever sees the surface that just changed. A monthly full re-score across every shipping view catches design debt that accumulates through many individually-small changes — the same rationale as Bastion's monthly sweep, applied to the frame-level pillar.
16. Desi — Throughline trace sweep monthly
| Field | Value |
|---|---|
| Name | Desi — Throughline trace sweep monthly |
| Agent | Desi |
| Schedule | 15th of each month, 09:03 Chicago · cron 3 9 15 * * |
| Autopilot ID | 170b3c65-7d1c-4b62-92d0-33c4fb8fdbf7 (trigger 0d1dbb55-a899-4b1a-87bc-53b4c9f6b560) |
| Output mode | Create issue |
| Report artifact | See Audit report artifact (pillar autopilots) — writes .planning/audits/throughline-<YYYY-MM-DD>.md per run. |
Runbook (paste into the form):
# Goal
A monthly, read-only user-flow audit of every registered journey, scored
against the locked `objectuve-throughline` framework using ONLY static
code-path tracing (no dev server, no evidence capture). Output is a single
triage issue Maggie routes into the chain — not a change request, and no
code is ever touched.
# Context
- You are Desi. `objectuve-throughline` is the sequence-level sibling of
`objectuve-hallmark`: hallmark judges the frame, throughline judges the
line through frames (transitions, loading handoffs, interruption/recovery,
celebration timing, flow endings). You already own the hallmark
design-debt audit — this is its flow-level counterpart.
- Required reading — load BEFORE auditing, every run:
1. `.claude/skills/objectuve-throughline/SKILL.md` — verbs, the 52 flow
gates (14 blocking) in 8 categories A–H, the O/M/C/R/F/X pre-emit
rubric.
2. `.claude/skills/objectuve-throughline/references/verbs.md` — the
`trace` verb playbook specifically (routes/guards, mutations per step,
celebration triggers, localStorage/session flags).
3. `.claude/skills/objectuve-throughline/references/flow-gates.md` — the
52 numbered gates.
4. `.claude/skills/objectuve-throughline/references/flow-disciplines.md`
— O/M/C/R/F/X definitions.
5. `.claude/skills/objectuve-throughline/references/interruption-matrix.md`
and `references/motion-vocabulary.md` — cite these, don't invent
vocabulary.
- Scope: every journey registered in `scripts/flow-journeys/*.mjs` (10 as of
2026-07-08: onboarding, goal-create, daily-checkin, goals-list-detail,
communities, public-sharing, coach, achievements, allies, monetization).
Read each journey file to understand its steps even though you will not
execute it.
- **Execution mode is `trace` ONLY. Never run `walk` or `audit`'s full evidence
path.** Do not start docker-compose, do not run `node
scripts/capture-flow-evidence.mjs`, do not boot a dev server. This
autopilot runs unattended with no one to unstick a stuck local stack —
the full evidence-capture pass (`walk` + `audit`) is a deliberate, manual,
human-supervised activity documented in
`.planning/audits/throughline-pilot-2026-07-08.md`, not something this
autopilot should attempt.
- Apply policy: READ-ONLY. Never edit app code, journey scripts, or skill
files. Never run a build/lint/deploy/dev-server.
# Steps
1. Resolve `master` HEAD short SHA + app version (`ionic_frontend/package.json`)
for the header line.
2. For each of the 10 registered journeys, perform a static `trace` per
`references/verbs.md`'s `trace` playbook:
- Routes & guards: `ionic_frontend/src/router/index.ts` entries, `meta`
flags, redirect logic for this flow's steps.
- Mutations per step: GraphQL mutations fired by each step's component,
`refetchQueries` / cache-update behavior.
- Celebration triggers: any watcher (`AchievementWatcher.vue`,
`useStreakCelebration.ts`, `LegendaryCelebration.vue`, etc.) that can
fire during/after this flow, and the session/localStorage key that
dedupes it.
- localStorage/session flags: every flag the flow reads or writes, and
whether the backend can disagree with it (stale-client risk).
3. Against each journey's trace, score ONLY the flow gates that are
decidable from static code alone — this is necessarily a subset of the
52, weighted toward category D (interruption/recovery) and category G
(state integrity), which `verbs.md` itself notes lean on trace more than
stills. For every other gate, mark it explicitly `not assessable from
trace — needs walk+audit` rather than guessing or silently skipping it.
4. Score O/M/C/R/F/X per journey only on the axes decidable from code (most
commonly R–Recovery and parts of C–Continuity via mutation/cache
ordering). Mark undecidable axes `not assessable from trace`.
5. Flag anything that looks like a live regression of a previously-fixed
throughline finding (the pilot found and Codi already fixed g34
celebration-stacking and g27 goal-create double-submit — confirm neither
pattern has resurfaced in the current trace).
6. Cluster findings; separate genuine code-level defects (missing dedup
guard, missing submit lock, guard/redirect logic that strands the user)
from things that are merely unverifiable without evidence (queue those
into a "needs full audit" list, don't call them defects).
7. Name journeys whose trace shows clean guard/dedup/lock patterns as the
ones to copy from.
# Issue structure (mirror the reference audit)
Title: handled by the autopilot template ("Throughline trace sweep — <date>").
Body, in this order:
- Header line: **Author:** Desi · **Scope:** 10 journeys, trace-only (no
evidence capture) · **Method:** objectuve-throughline `trace` verb +
gate/O-M-C-R-F-X subset decidable from static code · read-only · line
refs vs `master @ <sha>` (v<version>).
- One sentence stating this is a triage list from static analysis only, not
a change request, and that undecidable gates are NOT clean passes — they
are unverified.
- **TL;DR — flagged journeys**: table (Journey | Trace finding | Gate # |
Severity) for anything that looks like a real code-level defect.
- **Confirmed-fixed patterns still holding**: one line each on g34 and g27
(still guarded, or regressed — call it out loudly if regressed).
- **Systemic patterns (fix once, lift many)**: recurring missing-guard /
missing-dedup / stale-flag patterns across ≥2 journeys.
- **Gate coverage**: which of the 52 gates were assessable from trace this
run vs. which require `walk`+`audit` (evidence capture) to ever be
scored — so Maggie/Josh can see the coverage gap, not just the findings.
- **Full per-journey trace summary**: routes/guards, mutations, celebration
triggers, localStorage flags — one short block per journey.
- **Suggested next step**: the 1–3 clusters with outsized impact, and
whether a human-supervised `walk`+`audit` full-sweep pass is due (see
`.planning/audits/throughline-pilot-2026-07-08.md` for the local
dev-stack recipe that pass needs — port overrides, FALLBACK_AUTH_SECRET,
etc. — since this autopilot cannot run it itself).
# Routing
Create ONE issue per run in status `todo`, assigned to **Maggie**, so her
routing sweep promotes individual clusters into the Orion → Desi/Codi chain.
Do NOT self-assign, and do NOT promote findings into the chain yourself —
you author the audit; Maggie routes it.
# Skip / exit conditions
A run always produces at least a gate-coverage report, so there is no
"clean run" skip. But if `scripts/flow-journeys/` or the throughline skill
is unreadable (bad checkout, detached tree), post a one-line run-log note
explaining why and file no issue rather than a half-audit.
# Never
- Run `scripts/capture-flow-evidence.mjs`, start docker-compose, or boot any
dev server — this autopilot is trace-only, unattended, and has no one to
recover a stuck local stack.
- Edit any app file, journey script, or skill file. Read-only, always.
- Score a gate as "pass" when it is actually undecidable from trace alone —
mark it unassessed instead. A false "pass" is worse than an honest gap.
- Run a build, lint, format, or deploy.
- Promote a finding into the chain yourself, or open per-finding tickets —
one triage issue per run; Maggie fans it out.
- Invent flow-quality opinions outside the locked `objectuve-throughline`
gates/rubric — score against the framework, not personal taste.
- Duplicate Desi's own hallmark design-debt sweep: a finding decidable from
a single static screenshot belongs to hallmark, not here (see SKILL.md's
"Routing rule: hallmark vs. throughline").Why: The Throughline pilot (PR #1343) and its full 10-flow sweep were manual, human-supervised passes. This autopilot is the recurring, unattended cadence between those — trace-only (no dev server) so it can run safely with nobody watching, catching regressions of already-fixed flow bugs (g34 celebration-stacking, g27 double-submit) and flagging which gates still need a supervised walk+audit to ever be scored.
17. Codi — Keystone architecture audit monthly
| Field | Value |
|---|---|
| Name | Codi — Keystone architecture audit monthly |
| Agent | Codi |
| Schedule | 8th of each month, 09:03 Chicago · cron 3 9 8 * * |
| Autopilot ID | c325ecaf-230d-4ffc-8e29-606cbde8fbf2 (trigger 0ca85fe5-4130-40b6-b51f-c302ab793282) |
| Output mode | Create issue |
| Report artifact | See Audit report artifact (pillar autopilots) — writes .planning/audits/keystone-<YYYY-MM-DD>.md per run. |
Runbook (paste into the form):
# Goal
A monthly, primarily-read-only backend/DDD architecture audit scored
against the locked `objectuve-keystone` framework (56 gates, categories
A–H, B/C/I/L/D/O rubric), PLUS a remediation-tracking pass over the v4.3
Keystone Architecture Sweep's open follow-up tickets. Output is a single
triage issue Maggie routes into the chain. Audit findings are never
self-fixed; the only "remediation" this autopilot performs directly is a
narrow, pre-approved, mechanical fix — and only ever via an opened PR for
Roy's review, never merged or pushed to master by this autopilot itself.
# Context
- You are Codi. You were the executor of every phase of the v4.3 Keystone
Architecture Sweep (Multica project `51dbcc01-f69f-48b6-aeb7-b0c7e3dfb5b3`,
report-only, 11/11 issues done) — you ran `objectuve-keystone audit
<domain>` across all 14 registry domains + a billing pilot and compiled
`.planning/milestones/v4.3-keystone-arch-sweep-AUDIT-FINDINGS.md` (PR
#1402). This autopilot is the ongoing monthly cadence that sweep
established; it is modeled structurally on Desi's "Throughline trace
sweep monthly" and "Design-debt audit monthly" autopilots (same
Goal/Context/Steps/Issue-structure/Routing/Never shape, same
single-triage-issue-per-run discipline) but scoped to backend
architecture instead of frontend flow/design, and assigned to you
because you are this framework's actual practitioner, not Desi.
- **Why this autopilot's scope differs from the reference autopilots (read
this before objecting to the asymmetry):** a full manual 56-gate re-score
across all 14 domains every month is the sweep's own full milestone
effort — 9 phases, multiple sessions — and is not sustainable monthly.
Instead of shrinking gate coverage per domain (like throughline's `trace`
vs `walk`+`audit` split), this autopilot narrows via DOMAIN ROTATION:
full `objectuve-keystone audit <domain>` gate re-scores for 2 domains per
run (so all 14 get a full re-score roughly every 7 months), PLUS a
lightweight cross-cutting spot-check every run regardless of rotation
(below). Track which domains have been covered and when in the issue's
own "Rotation log" section (below) so the next run can pick up where
this one left off — do not rely on memory across runs.
- Required reading — load BEFORE auditing, every run:
1. `.claude/skills/objectuve-keystone/SKILL.md` — verbs, the 56 gates in
categories A–H, the B/C/I/L/D/O pre-emit rubric.
2. `docs/architecture/ddd/00-overview.md` through `05-application-services.md`
and `arch-gates.md` — the locked DDD source of truth this framework
scores against.
3. `scripts/arch-domains.mjs` — the 14-domain registry (treat its
accuracy as itself a standing finding, see below — do not trust its
globs without spot-checking).
4. `.planning/milestones/v4.3-keystone-arch-sweep-AUDIT-FINDINGS.md` —
the locked baseline. Its §4 "already-ticketed, do not re-raise" list
and §6 "fix-before-ship shortlist" are your primary remediation-status
inputs (see Steps 3–4).
- Apply policy: READ-ONLY for the audit portion, always. The ONE exception
is Step 5's narrow allowlisted mechanical fix, which must ship as an
opened PR only — never a direct push to master, never self-merged.
# Steps
1. Resolve `master` HEAD short SHA + app version for the header line.
2. **Rotation pick:** consult the most recent prior run's "Rotation log" (in
its issue body — search recently-closed/open "Keystone architecture
audit —" issues for the last logged rotation) and pick the next 2
domains in `scripts/arch-domains.mjs`'s 14-domain list that haven't had
a full re-score in the longest time. Run `objectuve-keystone audit
<domain>` on each exactly as Phase-N of the v4.3 sweep did, scoring all
56 gates for that domain against B/C/I/L/D/O.
3. **Cross-cutting spot-check (every run, all 14 domains, lightweight):**
without a full gate re-score, grep-check the 3 recurring defect patterns
the v4.3 sweep found driving most findings (§2 of the findings doc):
- Unwrapped multi-write transactions: grep each domain's interactions
for ≥2 sequential `.create!`/`.update!`/`.save!` calls with no
enclosing `ActiveRecord::Base.transaction`.
- Duplicated business rules: spot-check for a rule you know was
duplicated in the v4.3 sweep (e.g. "fail closed if Settings.ai
missing", `award_xp_to_both`) resurfacing in a NEW location.
- `scripts/arch-domains.mjs` registry drift: confirm each domain's
glob still matches its real current file count (roughly — full
precision isn't required here, that's what a full re-score is for).
4. **Remediation-status check:** read the current status of every ticket
in the findings doc's §4 "already-ticketed" list (OBJ-1276, OBJ-1277,
OBJ-1280, OBJ-1281, OBJ-1282, OBJ-1283) and the v4.4 Keystone Findings
Remediation epic's sub-issues (OBJ-1286 and its children OBJ-1287–
OBJ-1300, project `4a74da56-f09b-46a8-953e-89d65600d69f`). For each
still open after ≥60 days, or any "resolved" ticket where this run's
spot-check (Step 3) suggests the fix may have regressed, flag it in the
issue. Do not re-open, re-assign, or comment on those tickets yourself
— flag only, in this run's own triage issue.
5. **Narrow mechanical remediation (only if ALL of these hold, otherwise
skip straight to filing the triage issue with a note why):**
- This run's Step 2 full re-score or Step 3 spot-check surfaces a fix
that is mechanically identical in shape to one already shipped
elsewhere in the same file/domain (the exact "mirror the sibling
interaction" pattern the v4.3 sweep's own shortlist repeatedly used,
e.g. OBJ-1294's EnqueueClear fix mirroring EnqueueReseed/
EnqueueScopeReset in the same file).
- The fix is a single-file, <20-line change with no schema migration,
no new dependency, and no behavior change beyond closing the gap
(transaction wrap, idempotency guard mirroring a sibling, or an
arch-domains.mjs glob correction).
- If both hold: open a PR (never push to master directly, never
self-merge) implementing ONLY that fix, and link the PR from this
run's triage issue. Everything else found this run — including
anything even slightly judgment-call — goes into the triage issue as
a finding for Maggie to route, exactly like Desi's autopilots.
6. Cluster all findings (rotation re-score + spot-check + remediation
status) into the issue sections below.
# Issue structure (mirror the reference audits)
Title: handled by the autopilot template ("Keystone architecture audit —
<date>").
Body, in this order:
- Header line: **Author:** Codi · **Scope:** 2 domains full re-score
(name them) + 14-domain spot-check + remediation-status check ·
**Method:** objectuve-keystone 56-gate B/C/I/L/D/O + targeted grep ·
read-only except narrow allowlisted PRs (list any opened this run) ·
line refs vs `master @ <sha>` (v<version>).
- One sentence stating this is a triage list, not a bulk change request —
only pre-approved mechanical fixes (if any, linked below) were opened as
PRs; everything else is Maggie's to route.
- **TL;DR — new findings this run**: table (Domain | Finding | Gate # |
Severity).
- **Remediation status**: table of every §4/OBJ-1286-epic ticket checked
this run (Ticket | Status | Age | Flag if stale/regressed).
- **Rotation log**: which 2 domains got a full re-score this run, and the
full 14-domain rotation order/last-scored-date table so the next run
knows where to resume.
- **Spot-check results**: pass/fail per recurring pattern (unwrapped
transactions, duplicated rules, registry drift) across all 14 domains,
not just the 2 rotated ones.
- **Mechanical fixes opened this run**: PR links, or "none this run" — be
honest, don't force a fix to justify the section.
- **Suggested next step**: the 1–3 clusters Maggie should route first.
# Routing
Create ONE issue per run in status `todo`, assigned to **Maggie**, so her
routing sweep promotes individual clusters into the Orion → Codi/Roy
chain. Do NOT self-assign findings, and do NOT promote findings into the
chain yourself beyond the Step 5 narrow-allowlist PR exception — you
author the audit; Maggie routes everything else.
# Skip / exit conditions
A run always produces at least a remediation-status table, so there is no
"clean run" skip. But if `scripts/arch-domains.mjs` or the
`objectuve-keystone` skill is unreadable (bad checkout, detached tree),
post a one-line run-log note explaining why and file no issue rather than
a half-audit.
# Never
- Push directly to master, or self-merge any PR opened under Step 5 —
every mechanical fix ships as an open PR for Roy's review, full stop.
- Attempt a fix that isn't a single-file, <20-line, no-migration mirror of
an already-shipped sibling pattern — anything larger or more judgment-
laden goes in the triage issue, not a PR.
- Re-raise findings already ticketed in the v4.3 sweep's §4 list or the
v4.4 remediation epic (OBJ-1286 and children) — check their status
(Step 4), don't duplicate them.
- Run a full 56-gate re-score on more than 2 domains in a single run —
that's the sweep's own multi-session milestone effort, not a monthly
autopilot's job. Use the rotation log.
- Edit any file outside the Step 5 narrow-allowlist exception. Read-only
otherwise, always.
- Run a build, lint, format, or deploy.
- Invent architecture opinions outside the locked `objectuve-keystone`
gates/rubric — score against the framework, not personal taste.
- Comment on, re-open, or re-assign another agent's existing ticket —
flag staleness/regression in this run's own triage issue instead.Why: The v4.3 Keystone Architecture Sweep (11/11 issues done, PR #1402) was a full 9-phase milestone effort. A full 56-gate re-score of all 14 domains every month isn't sustainable at that scale, so this autopilot narrows via domain rotation (2 of 14 per run, full coverage roughly every 7 months) plus a lightweight cross-cutting spot-check every run — the same asymmetry Bastion's "Why" section above points to.
18. Dave — Signal instrumentation audit monthly
| Field | Value |
|---|---|
| Name | Dave — Signal instrumentation audit monthly |
| Agent | Dave |
| Schedule | 12th of each month, 09:00 Chicago · cron 0 9 12 * * |
| Autopilot ID | 1c62688e-0084-45e6-b7e6-912123bef78e (trigger 52fe1075-c99b-47db-add1-f7afd8398bbe) |
| Output mode | Create issue |
| Report artifact | See Audit report artifact (pillar autopilots) — writes .planning/audits/signal-<YYYY-MM-DD>.md per run. |
Runbook (paste into the form):
# Goal
Run a full `objectuve-signal` sweep + audit (all 44 gates, categories A–F)
across every registered surface in the signal registry, monthly. Not rotated —
like Bastion's 38 gates (and unlike Keystone's 2-of-14-domain rotation),
signal's gates are surface/call-site scoped rather than whole-domain, so a
full sweep is cheap enough to run every time.
# Context
- Skill: `.claude/skills/objectuve-signal/` (SKILL.md +
`references/signal-gates.md`, `references/signal-disciplines.md`,
`references/signal-vocabulary.md`, `references/verbs.md`)
- Registry: `scripts/signal-surfaces.mjs` defines the surfaces this sweep
covers — 13 Sidekiq queues (`sidekiq_ai_coaching`, `sidekiq_ai_metrics`,
`sidekiq_ai_workforce`, `sidekiq_billing`, `sidekiq_content_moderation`,
`sidekiq_critical_path`, `sidekiq_demo_data`, `sidekiq_feedback`,
`sidekiq_gamification`, `sidekiq_gdpr`, `sidekiq_onboarding`,
`sidekiq_social`, `sidekiq_teams`), `crono_schedule`, 4 webhook surfaces
(`webhook_stripe`, `webhook_revenuecat`, `webhook_ai_workforce`,
`webhook_ai_workforce_assets`), `feature_flags`,
`feature_flags_backend_parity`, and 5 CI/health surfaces
(`ci_required_checks`, `ci_staging_deploy`, `ci_production_deploy`,
`ci_playwright_smoke`, `health_endpoint`). Harvester script:
`scripts/capture-signal-evidence.mjs` (the `sweep` verb's evidence
harvester — confirmed present at repo root, mirroring Atlas's
`scripts/capture-doc-evidence.mjs`). As of this drafting,
.planning/signal-evidence/ does not yet exist in this checkout — this
autopilot's first run will be the first time the harvester actually
populates it; do not treat an empty/missing evidence dir as a finding on
run 1, just run `sweep --all` to create it.
- Locked evidence signal scores against (per SKILL.md's "The one branch:
locked evidence" section) — CLAUDE.md's **Common Gotchas** section and
`docs/operations/deployment.md`, PLUS two files that are locked evidence
but NOT in CLAUDE.md: `rails_api/app/controllers/health_controller.rb`
(the actual `/health` check and its `INFORMATIONAL_CHECKS` fail-open
stance) and `rails_api/config/cronotab.rb` (the `parse_at` boot-crash
outage — cite the cronotab header comment + `rails_api/spec/config/
cronotab_spec.rb`, never CLAUDE.md, for that specific incident).
- This is the pillar that asks a different question than Dave's other two
live autopilots — read this before running, it is the reason this
autopilot exists as a THIRD Dave autopilot rather than duplicating the
first two:
- **`Dave — Staging health check`** (run-only, hourly weekdays) asks
"is staging healthy RIGHT NOW" — hits `/health`, checks Sentry for
fresh unresolved issues, checks the latest Cloud Run revision is
Ready/100% traffic. It is a point-in-time health probe.
- **`Dave — Crono liveness check`** (create-issue, daily) asks "is the
scheduled-job runner alive RIGHT NOW" — reads `checks.crono` from
`/health` in both environments and treats it as the fail-closed
alerting layer for a stance `/health` deliberately leaves fail-open
(`INFORMATIONAL_CHECKS`). Also a point-in-time liveness probe, scoped
to one subsystem.
- **This autopilot (`Dave — Signal instrumentation audit monthly`)**
asks neither. It does not check whether the system is healthy today —
it asks: **if something silently broke last month, would ANY of our
existing signals (Sentry, `/health`, CI required checks, PostHog flag
parity, structured logs) have caught it?** It audits detectability
itself — rescue blocks that swallow instead of report, CI checks that
can silently stop gating a merge, health-check thresholds that measure
the wrong thing, flags that fail closed with no operator-visible
trace. A clean run of the other two Dave autopilots every day for a
month tells you nothing about whether THIS autopilot's gates would
still fire — they are checking different things. Do not fold this
sweep's findings into either of the other two autopilots' issues, and
do not skip this run because the other two have been green.
- Routing boundary: only file findings that are signal's lens — see
SKILL.md's "Routing rule" section and its explicit test: *if the bug were
instantly, loudly surfaced to an engineer the moment it happened, would
the finding still exist?* If yes, the defect belongs to
**objectuve-hallmark** (frame/visual/static), **objectuve-throughline**
(flow/sequence/timing), **objectuve-keystone** (structure/contracts/data
integrity), **objectuve-bastion** (security — e.g. an unverified webhook
signature), **objectuve-compass** (business-model), or **objectuve-atlas**
(docs) — route it there, cite signal only if its *visibility* is also
broken. If no — the "bug" disappears the instant it's visible (a
correctly-handled exception nobody's told about, an intentionally-off
flag nobody can see is off, a CI check that would have caught the real
bug but silently didn't run) — that is signal's finding, and signal's
alone. Concrete example from SKILL.md: a missing DB transaction is
keystone's; an unverified webhook signature is bastion's; a stale doc is
atlas's; a swallowed-and-never-reported exception is signal's.
# Steps
1. Run `sweep --all` first (`node scripts/capture-signal-evidence.mjs
--all`) to confirm the registry surface list is current and evidence is
fresh — do not audit against stale or missing evidence. Confirm exit
code (0 = full harvest, 2 = partial-but-valid, 1 = hard failure — stop
and report, do not interpret partial/corrupt evidence as a finding).
Confirm `.planning/signal-evidence/<surface>/evidence.json` and
`evidence.md` were written for every surface, and that `index.json`
reflects the run.
2. Run `audit --all` against the harvested evidence, walking every gate
category A–F in `references/signal-gates.md` in order. Skip a category
only if a surface genuinely can't touch it, and say so explicitly.
3. Run the **calibration coda** from `references/verbs.md` before trusting
this run's severity calls: spot-check one known-clean surface
(`DomainEvents.dispatch` — already correctly reports to Sentry per
`rails_api/app/lib/domain_events.rb`) and one known-gap surface (whether
backend GraphQL 500s reach Sentry — an unconfirmed pattern per
`signal-vocabulary.md`, verify directly against `graphql_controller.rb`
and `RailsServerSchema`'s `SentryTrace` tracer each run rather than
assuming a standing gap). If the known-clean surface fires a gate, the
gate wording is miscalibrated — note it, don't force a finding.
4. Score each finding against the Blocking criteria in SKILL.md: silent
data loss (a write/event/job result that can vanish with no trace),
an undeclared fail-open/fail-closed stance guarding something dangerous
(money/auth/data integrity), a required CI check that can silently
no-op (branch-filter drift, concurrency collision, a parse error
producing "0 jobs created" with no annotation), or a flag/config that
fails closed with zero operator-visible signal. The 15 gates that match
these criteria by definition are g1, g2, g5, g8, g11, g16, g20, g23,
g27, g30, g33, g36, g38, g41, g44 (some of category E/F's remainder are
conditionally blocking per their own text — resolve the condition, don't
default to should-fix).
5. Before emitting, run the pre-emit T/A/P/E/R/S self-critique silently
(Traceable / Actionable / Parity / Explicit / Reproducible / Scoped —
full rubric in `references/signal-disciplines.md`) and revise anything
failing an axis. A finding that fails R (can't be reproduced/cited) is
not a finding — go find the citation or drop it, never assert from
memory. A finding that fails S belongs in a different pillar's report,
not this one.
6. Group findings by severity (blocking / should-fix), each with
file:line (or workflow/job name), the gate number, and a concrete fix,
using the exact reporting format from `signal-gates.md`:
`[gate #] <surface>/<file:line or workflow/job> — <what's invisible> →
<fix>`.
# Decision
- No findings (or evidence genuinely came back clean across every
category) → exit silently, no issue. Post a "clean month" note in the
run log, including the category-scoring table from the audit output
format (Category | Gates checked | Fired | Blocking, all zeroes) so
a future run can compare.
- Any findings → dedupe first: search open issues for title containing
"Signal instrumentation audit" (`multica issue list`). If one is already
open from the current month's cadence, comment the new findings onto it
instead of creating a duplicate — mirroring `Dave — Crono liveness
check`'s and `Roy — Bastion security audit monthly`'s dedupe pattern.
- Otherwise create ONE issue covering all findings from this run (don't
file one issue per finding), assigned to **Maggie** for routing. Maggie
routes fixes to Codi (or Tess, if the finding is in `.github/workflows/`
or `tests/`) with Dave tagged in the body for re-review once the fix
lands — matching how Roy's monthly Bastion sweep and Roy's real-time
`SECURITY-FINDING:` BLOCK verdict both finish by assigning back to
Maggie, never directly to the fixing agent.
- If any finding is **(blocking)** (one of the 15 gates above, or a
category-E/F gate whose conditional clause resolved blocking this run),
say so explicitly in the issue title/summary — e.g. "Signal
instrumentation audit: N findings (M blocking)".
# Issue body template
- **Sweep date / scope:** which surfaces were harvested (or `--all`),
evidence freshness.
- **Executive summary:** overall verdict, blocking count, should-fix
count (mirrors `verbs.md`'s audit output format).
- **Category scoring table:** Category | Gates checked | Fired | Blocking
for A–F.
- **Findings:** grouped blocking / should-fix, each with file:line (or
workflow/job), gate #, and a concrete fix — one line per finding, exact
format from `signal-gates.md`.
- **Evidence:** the real grep/read/harvested-evidence result backing each
finding — never an invented citation. Cite CLAUDE.md's Common Gotchas
section, `docs/operations/deployment.md`, `health_controller.rb`, or
`cronotab.rb`/`cronotab_spec.rb` by name per which one actually backs
the claim (never CLAUDE.md for the Crono `parse_at` outage — that one is
cronotab-only per SKILL.md).
- **Re-review:** Dave (tagged for re-review once Codi's or Tess's fix
lands, same pattern as the Bastion monthly sweep).
# Skip this run if
The signal skill's registry script (`scripts/signal-surfaces.mjs`) or its
harvester (`scripts/capture-signal-evidence.mjs`) is missing, broken, or
returns a hard failure (exit code 1) — post a BLOCKED comment naming the
exact error rather than silently skipping or fabricating evidence to keep
the run "clean."
# Never
- File a finding that's another pillar's lens (hallmark/throughline/
keystone/bastion/compass/atlas) — route it there instead per the routing
rule above, even if it surfaced during this sweep.
- Create a duplicate issue while a "Signal instrumentation audit" issue
from the current cadence is already open — comment onto it instead.
- Self-fix. This autopilot is diagnostic only — no code edits, no PRs.
Dave diagnoses and reports; Codi (or Tess, for `.github/workflows/`/
`tests/` findings) makes the change.
- Soften a blocking finding (one of g1, g2, g5, g8, g11, g16, g20, g23,
g27, g30, g33, g36, g38, g41, g44, or a conditionally-blocking E/F gate
that resolved blocking) into a should-fix to make the report look
cleaner — per `signal-disciplines.md`, a finding that fails the T/A/P/E/
R/S self-critique gets fixed or reported as a genuine gap, never
footnoted away.
- Invent an incident, a threshold, or a flag name not actually present in
`references/signal-vocabulary.md` or confirmed by a fresh read/grep. If
a gate needs a citation the vocabulary file doesn't have yet, read the
real source and add it as evidence — never fabricate a plausible one.
- Duplicate a finding onto `Dave — Staging health check`'s or `Dave —
Crono liveness check`'s run log — those are point-in-time health/
liveness probes; this autopilot's findings are about detectability
itself and belong only in this autopilot's own issue.Why: Dave's other two autopilots (Staging health check, Crono liveness check) are point-in-time probes — is the system healthy right now. This one asks a different question — if something silently broke, would any of our existing signals have caught it — and a clean run of the other two says nothing about whether this one's 44 gates would still fire.
19. Penny — Compass business-model audit monthly
| Field | Value |
|---|---|
| Name | Penny — Compass business-model audit monthly |
| Agent | Penny |
| Schedule | 5th of each month, 09:00 Chicago · cron 0 9 5 * * |
| Autopilot ID | 040ec0b5-d3f8-414d-8aef-02b6666b6785 (trigger 7813cd34-3722-4e18-ac7c-00d7b455c655) |
| Output mode | Create issue |
| Report artifact | See Audit report artifact (pillar autopilots) — writes .planning/audits/compass-<YYYY-MM-DD>.md per run. |
Runbook (paste into the form):
# Goal
Run a full `objectuve-compass` audit (all 24 gates, categories A–D) across
every business-model/engagement surface registered in
`references/compass-surfaces.md`, monthly. Report-only — this pillar has no
`redesign`/`trace` verb and no code-editing path at all, which is a stricter
posture than the Keystone architecture autopilot (2-of-14-domain rotation,
narrow mechanical-fix carve-out) or the Bastion security autopilot (Codi fixes,
Roy re-reviews): a compass finding is a product/business-model decision — delete
a nudge, move a feature to the free tier, remove a countdown — never a code
change Penny makes herself.
# Context
- Skill: `.claude/skills/objectuve-compass/` (SKILL.md + `references/compass-gates.md`,
`references/compass-disciplines.md`, `references/compass-vocabulary.md`,
`references/compass-surfaces.md`, `references/verbs.md`)
- Locked evidence this audit scores against: `MISSION.md`'s six Values
(Wellbeing Over Engagement, Progress is Earned Never Manufactured, Free
Means Free, Respect Over Revenue, Accountability Not Entertainment, Small
Steps Compounding) and ten numbered Commitments (Commitment 4 — "Anti-social
design — ~10 min/day sessions, gentle nudges to close"; Commitment 5 — "No
dark patterns — FOMO, infinite scroll, and manufactured urgency are banned";
Commitment 9 — "Progress over engagement — every celebrated metric must be
earned"), plus `docs/brand/brand.md`'s Anti-Social App section (~10 min/day
target session, "What we build toward" / "What we deliberately avoid"
lists) and its Tone of Voice "Out of Brand" copy examples. `docs/product/
pricing-philosophy.md`'s Principle 1 ("Never paywall individual features")
backs the monetization-pressure category.
- Scope (per `references/compass-surfaces.md`, do not rotate — compass gates
are diff/surface scoped, not whole-domain, so a full sweep every month is
cheap):
1. The 12 notification/celebration surfaces from `docs/development/
notification-surfaces.md` (generic toast, achievement toast/glow,
legendary celebration modal, badge details modal, streak celebration,
notifications panel, ally request toast, level-up toast, push
notifications, Monday recap bottom sheet, notification history page).
2. Monetization/upgrade surfaces — `SupporterUpgradeModal.vue`,
`SupporterHero.vue`, `SupporterStreakNudge.vue`, any `upgrade-required`
emit site, Teams billing/upgrade views.
3. Streak/gamification surfaces — `use_streak_freeze.rb`,
`accept_streak_mercy`/`User#streak_grace_days_used`,
`StreakDetailsModal.vue`, badge/XP/rank copy.
4. Session-length/anti-metric surface — `useScreenTimeNudge.ts` and its UI
component `ScreenTimeNudge.vue`.
5. Lifecycle/reminder crons in `rails_api/config/cronotab.rb` (the
`Social::Generate*ReminderJob`/`Social::GeneratePastDueNotificationJob`/
`Social::WeeklyDigestEnqueueJob` family).
- This is a distinct direction from the existing **Penny — Product strategy
monthly** autopilot, not a duplicate of it. That autopilot looks *forward*:
it proposes new features, competitive responses, and product opportunities,
gated only on north-star alignment and a light PBC check ("no paywall,
no engagement-juice, no dark patterns") as a pass/fail filter on Penny's
own proposals. This autopilot looks *backward*: it re-audits surfaces
that are already shipped, using the full 24-gate compass rubric, to catch
drift a feature accumulated after it shipped clean — a nudge that started
respecting its dismissal key and stopped, a monetization surface whose
copy quietly shifted, a lifecycle cron whose cadence grew. Do not fold this
autopilot's findings into a product-strategy proposal, and do not let a
Compass finding substitute for the Product strategy autopilot's own PBC
check on NEW proposals — they check different populations (shipped vs.
proposed) with different instruments (24-gate audit vs. a 3-line filter).
- Routing boundary: only file findings that are compass's lens — see
SKILL.md's "Routing rule: compass vs. hallmark vs. throughline". A finding
visible in one static frame (a guilt-copy string, a countdown badge on one
screen) is hallmark's (category G, gates 47–53), not compass's. A finding
visible only across a flow's sequence/timing (a completion screen detouring
into an upsell, a re-fired celebration) is throughline's (category H, gates
48–52), not compass's. Compass owns only the business-model/system-level
pattern: whether a mechanic is free vs. paid, whether the session-length
anti-metric is being repurposed as a KPI, whether a notification system as
a whole trends toward manufactured re-engagement. Do not duplicate a
Hallmark or Throughline finding under a compass label — the same component
can legitimately produce findings from more than one pillar without being
a duplicate (see SKILL.md's `SupporterStreakNudge.vue` split example).
# Steps
1. Compass ships no dedicated evidence-harvester script — unlike Keystone/
Bastion/Signal, there is no `sweep`/registry-refresh verb to run first.
Read `references/compass-surfaces.md` directly to confirm the registry
list is still current against what's actually shipped (the file's own
convention: "If a new surface is added, add it here in the same PR; do
not let this file drift from what's actually shipped") — flag drift as a
finding rather than silently auditing against a stale list.
2. Pre-flight, before scoring anything: read
`references/compass-vocabulary.md` so every judgement cites a real
MISSION.md Value/Commitment, brand.md line, pricing-philosophy.md
Principle, or shipped file — never an invented incident or policy.
3. Run `audit` (not `scan` — `scan` is a lightweight grep pass with no gate
scoring; this monthly sweep needs the full judged audit) across all five
surface groups above, covering all four gate categories A–D (all 24
gates) so no category is skipped.
4. Score each finding against the Blocking criteria in SKILL.md: a MISSION.md
Commitment violated outright (manufactured urgency, FOMO/guilt copy,
infinite scroll/autoplay, a functional feature gated behind payment); a
forced/non-dismissible engagement surface; a dark-pattern UI mechanic
(confirmshaming, pre-checked recurring charge, hidden total, unbacked
scarcity copy); or the session-length anti-metric (`app_session_ended`)
repurposed as a growth target. The 8 gates that trip blocking by number:
g1, g4, g7, g10, g15, g18, g20, g24.
5. Run the A/C/T/I/O/N pre-emit self-critique (Action-forward, Consented,
Transparent, Impermanent, Optional, Necessary) on the full finding set
before emitting — do not footnote a failing axis, fix the finding's
framing or report it as a genuine gap.
6. Before citing the notification-surface count or Supporter/Teams pricing in
any finding, re-check `compass-vocabulary.md`'s "Confirmed documentation
drift" section — the notification-surface-count discrepancy
(notification-surfaces.md's stale "twelve" vs. CLAUDE.md's "11") was
resolved 2026-08-07 at a net **twelve** (dropped the retired "Showcased
achievements card" entry, added the shipped `/notifications` history page
entry — two independent same-day fixes reconciled through a merge
conflict); the still-open one is pricing (MISSION.md's $36/yr Supporter +
$5/user/mo Teams vs. pricing-philosophy.md's $35/year + $9/user/month) —
flag that drift as
its own finding rather than silently picking one number.
7. Group findings by severity (blocking / should-fix), each with file:line,
the gate number, and a concrete fix (delete the nudge / gate behind
consent / move to free tier / cite the real constraint).
# Decision
- No findings → exit silently, no issue. Post a "clean month" note in the run
log. This is the expected, common outcome — compass's own SKILL.md notes
Objectuve has never had a confirmed shipped dark-pattern incident, and a
clean month is not itself suspicious.
- Any findings → dedupe first: search open issues for title containing
"Compass business-model audit" (`multica issue list`). If one is already
open, comment the new findings onto it instead of creating a duplicate.
- Otherwise create ONE issue covering all findings from this run (don't file
one issue per finding), assigned to **Maggie** for routing.
- If any finding is **(blocking)**, say so explicitly in the issue
title/summary — don't bury a blocking finding inside a mixed should-fix
list.
# Issue body template
- **Sweep date / scope:** which of the 5 surface groups were covered, gate
categories A–D checked
- **Findings:** grouped blocking / should-fix, each with file:line, gate #,
concrete fix
- **Evidence:** the real grep/read result or MISSION.md/brand.md/
pricing-philosophy.md citation backing each finding — never an invented
incident or policy (per compass-vocabulary.md's lock statement)
- **A/C/T/I/O/N self-critique:** one line per axis, pass/fail, on the finding
set as a whole
- **Re-review:** who owns the product/business-model decision (this is not a
code fix — tag Maggie for routing to a product conversation or GSD task,
not directly to an executor agent)
# Skip this run if
`references/compass-surfaces.md`'s registry can't be reconciled against what's
actually shipped (e.g. a listed file no longer exists, or a surface has been
renamed) — post a BLOCKED comment naming which surface failed to resolve,
rather than silently auditing a stale list or silently dropping the surface
from scope.
# Never
- Self-fix, open a PR, or edit any app code. Compass has no code-editing verb
(`SKILL.md`'s "No redesign/trace verb" section is explicit: "That is GSD
task work or a product conversation, not a skill edit"). This is stricter
than Keystone's narrow mechanical-fix carve-out — there is no carve-out
here at all.
- File a finding that's actually hallmark's (a single-frame copy/visual
issue) or throughline's (a flow-sequence/timing issue) lens — see SKILL.md's
routing rule before creating the issue.
- Merge this autopilot's output with **Penny — Product strategy monthly**'s
proposals, or treat a clean Compass month as license to skip that
autopilot's own PBC check on new proposals — they are opposite-direction
passes (audit shipped surfaces vs. propose new ones) and must stay separate
issues with separate rationale.
- Create a duplicate issue while a "Compass business-model audit" issue from
a prior run is still open.
- Invent an incident, threshold, or policy not present in `MISSION.md`,
`docs/brand/brand.md`, `docs/product/pricing-philosophy.md`, or a shipped
file. If a gate needs a citation the vocabulary doesn't have yet, that's a
gap to fill by reading the actual source, never a plausible-sounding guess.
- Soften a genuine blocking finding (g1, g4, g7, g10, g15, g18, g20, g24) into
a should-fix to keep the report short, or silently resolve the confirmed
MISSION.md-vs-pricing-philosophy.md pricing drift one way instead of
flagging it.Why: Compass is preventative — Objectuve has no confirmed shipped dark-pattern incident, and this autopilot exists to keep it that way rather than clean up after one. It is deliberately opposite-direction from Penny's own Product strategy autopilot (which proposes new features): this one re-audits what's already shipped for engagement/monetization drift, and has no code-editing carve-out at all — every finding is a product decision routed through Maggie.
Schedule reference — all 34 autopilots at a glance
Consolidated cron strings + live autopilot IDs from the Objectuve workspace. Cron is 5-field: minute hour day-of-month month day-of-week (no seconds field). Timezone is set on the trigger — America/Chicago for Objectuve, except 2g and 7d, which the live autopilots have on UTC (flagged in each entry above).
Authoritative live inventory is auto-generated in
multica-state.md(regenerated byscripts/multica-state.sh). This table is the curated setup reference; if the two disagree,multica-state.mdwins.
| # | Autopilot | Tier | Cadence | Cron | Autopilot ID |
|---|---|---|---|---|---|
| 1 | Roy — Dependabot triage | 1 | Weekdays 07:00 | 0 7 * * 1-5 | 7116b8ff |
| 2 | Dave — Staging health check | 1 | Hourly, 08:00–20:00 weekdays | 0 8-20 * * 1-5 | eb59eecd |
| 2b | Dave — Crono liveness check | 1 | Daily 08:15 | 15 8 * * * | acf44638 |
| 2c | Dave — Clerk auth tripwire | 2 | Monthly, 3rd 08:00 | 0 8 3 * * | 7fefe2ef |
| 2d | Dave — Email canary liveness check | 1 | Daily 10:00 | 0 10 * * * | d0b4e9f5 |
| 2e | Dave — gc maintenance watchdog | 1 | Every 6h at :17 | 17 */6 * * * | b8558275 |
| 2f | Dave — Autopilot run failure sweep | 1 | Every 6h at :47 | 47 */6 * * * | a43b4c1d |
| 2g | Dave — Email suppression liveness check | 1 | Daily 14:00 UTC | 0 14 * * * | f142b921 |
| 3 | Maggie — Backlog sweep | 1 | Weekdays 09:00 + 13:00 | 0 9,13 * * 1-5 | 16b8be21 |
| 3b | Maggie — Issue health sweep (stall + fix-round cap, merged) | 1 | Hourly at :30 | 30 * * * * | b50c7e15 |
| 3c | Maggie — Blocked review | 1 | Daily 07:45 | 45 7 * * * | 083a5fbd |
| 3d | Riley — CI wake | 1 | Event-driven (webhook) | — | e031e327 |
| 4 | Riley — Objectuve feedback ingest | 1 | Every 6 hours | 0 */6 * * * | eec7c82c |
| 5 | Riley — Stale PR sweep | 2 | Weekdays 14:00 | 0 14 * * 1-5 | 44afc66c |
| 5b | Riley — Stale branch pruning | 2 | Mondays 09:00 | 0 9 * * 1 | dcfaa30e |
| 6 | Dori — Docs drift audit (objectuve-atlas) | 2 | Mondays 11:00 | 0 11 * * 1 | 8e609d6a |
| 7 | Vicki — Release readiness preflight (pre-train) | 2 | Tuesdays 14:00 | 0 14 * * 2 | c49cad3b |
| 7c | Vicki — Weekly release train | 2 | Wednesdays 08:00 | 0 8 * * 3 | df6b5a7f |
| 7b | Maggie — Weekly crew metrics | 2 | Mondays 08:00 | 0 8 * * 1 | b6b08f95 |
| 7d | Maggie — Workflow efficiency audit monthly | 3 | Monthly, 3rd 15:00 UTC | 0 15 3 * * | af29bbca |
| 8 | Vicki — Autonomy promotion review | 3 | Quarterly (1 Jan/Apr/Jul/Oct) 09:00 | 0 9 1 1,4,7,10 * | 31c8f598 |
| 9 | Dori — Brand voice sweep | 3 | 1st of each month 09:00 | 0 9 1 * * | 4cab76bf |
| 10 | Orion — Activation funnel check | 3 | Mondays 10:00 | 0 10 * * 1 | d78d6a6a |
| 11 | Dori — Knowledge base lint | 3 | 15th of each month 09:00 | 0 9 15 * * | b34bb55f |
| 11b | Maggie — Backlog grooming | 3 | 1st of each month 09:15 | 15 9 1 * * | f7058185 |
| 12 | Penny — Product strategy monthly | 1 (proposer) | 1st of month 09:00 | 0 9 1 * * | dedb4a5a |
| 13 | Sage — System architecture monthly | 2 (proposer) | 1st of month 09:00 | 0 9 1 * * | 6dd283e5 |
| 14 | Roy — Bastion security audit monthly | 2 | 22nd of month 09:03 | 3 9 22 * * | 2c77e88d |
| 15 | Desi — Design-debt audit monthly (objectuve-hallmark) | 2 | 1st of month 09:03 | 3 9 1 * * | 667e4ca1 |
| 16 | Desi — Throughline trace sweep monthly | 2 | 15th of month 09:03 | 3 9 15 * * | 170b3c65 |
| 17 | Codi — Keystone architecture audit monthly | 2 | 8th of month 09:03 | 3 9 8 * * | c325ecaf |
| 18 | Dave — Signal instrumentation audit monthly | 2 | 12th of month 09:00 | 0 9 12 * * | 1c62688e |
| 19 | Penny — Compass business-model audit monthly | 2 | 5th of month 09:00 | 0 9 5 * * | 040ec0b5 |
Cron quick-reference
- Fields:
minute hour day-of-month month day-of-week - Day-of-week:
0=Sunday,1=Monday, …5=Friday,6=Saturday.1-5= Mon–Fri. - Multiple values: comma-separate (
0 9,13 * * 1-5runs at 09:00 and 13:00 on weekdays) - Ranges: use hyphens (
1-5for Mon–Fri) - Every N:
*/N(e.g.,*/4= every 4 from 0). Combined with range:8-20/4= 8, 12, 16, 20. - Timezone: NEVER in the cron expression — always set via Multica's timezone picker on the form.
Drift warnings
- DST transitions — cron expressions don't account for daylight saving. Chicago jumps between CST (-6) and CDT (-5). Multica's timezone picker handles this automatically; don't hard-code UTC offsets.
- Quarterly autopilot timing —
0 9 1 1,4,7,10 *fires on the 1st of Jan/Apr/Jul/Oct regardless of day-of-week. Could land on a Sunday. That's fine for an async Vicki report but note it if you expect a Monday ping. gcloud-dependency audit (OBJ-3560, verified 2026-09-09) — every Dave autopilot's runbook was checked for a baregcloudcall site: § 2, § 2b, § 2d, § 2g, and § 7 shell out togcloudand need the credential preflight; § 2c (Clerk auth tripwire), § 2e (gc maintenance watchdog), and § 2f (Autopilot run failure sweep) call nogcloudcommand and need none. Don't re-run this sweep on the next incident — it's closed.- § 7's
gcloudpreflight dropped from live wiring (OBJ-3581, verified 2026-09-11) — the § 7Vicki — Release readiness roundupautopilot no longer carries a credential preflight ahead of step 5a.'sgcloud run services describecall; byte-diffed against the livec49cad3bautopilot description, which has none. Doc mirror updated to match (§7'sStep 0sub-block removed). Don't re-add it without re-confirming against the live autopilot text first — this is the one entry in the § 2/§ 2b/§ 2d/§ 2g/§ 7 set above that changed shape since the OBJ-3560 audit line was written.
Anti-patterns — don't build these as autopilots
- Daily news digest — template looks tempting; doesn't move your product forward. Skip.
- Bug triage autopilot — redundant with Maggie's backlog sweep (#3).
- Auto-code-fixer — Codi should never run on autopilot. Every code change goes through a human-created issue → Orion → Codi, not a cron.
- Auto-merge autopilot — Vicki's shipping decisions are human-confirmed by design. Don't bypass.
- Auto-deploy autopilot — your CI/CD already handles this on tag push. Duplicate with fewer safeguards.
Operational notes
- Expect tuning. First-week runs will catch edge cases the runbook didn't anticipate — adjust the runbook and re-save. Multica re-reads it on every run.
- Pause, don't delete. If an autopilot goes chatty or wrong, pause it in Multica rather than deleting. You keep the runbook history.
- Global kill-switch (incident stop). When the board is flooding, staging is on fire, or Multica itself is degraded, stop every autopilot at once instead of pausing 20 by hand:bashPausing only flips
scripts/multica-autopilots.sh status # see all 34 + their status scripts/multica-autopilots.sh pause # pause ALL (reversible — runbooks/triggers kept) scripts/multica-autopilots.sh pause --issuing # pause only the create_issue ones (leave silent monitors running) scripts/multica-autopilots.sh resume # re-activate ALL when the incident clearsstatustopaused; it does not touch runbooks or triggers, soresumerestores the exact prior schedule. Use--issuingto silence the issue-generators (which pollute the board) while keeping the run-only health monitors (#2 Dave staging, #10 Orion funnel) live. - Review quarterly. At the same cadence as #8 (autonomy promotion), audit which autopilots are actually useful. Kill the ones you ignore; tighten the ones you scan.
- One agent, many autopilots. Dave has eight (staging health check, Crono liveness, Clerk auth tripwire, email canary liveness, gc maintenance watchdog, autopilot run failure sweep, email suppression liveness, Signal instrumentation audit monthly), Riley has four (CI wake, feedback ingest, stale PR sweep, stale branch pruning), Roy has two (Dependabot triage, Bastion security audit), Dori has three (docs drift, brand voice, knowledge base lint), Vicki has three (release readiness preflight, weekly release train, autonomy review), Desi has two (design-debt audit, throughline trace sweep), Maggie has six (backlog sweep, issue health sweep, blocked review, backlog grooming, weekly crew metrics, workflow efficiency audit). That's fine — each autopilot's runbook is self-contained context.
Related docs
multica-agent-crew.md— full agent instructions, handoff protocol, routing table../operations/ai-workforce-runbooks.md— AI Workforce operational processes (Process 4 is tied to autopilot #8)../features/ai-workforce.md— AI Workforce feature architecture../operations/multica-autopilot-run-status-gotcha.md— the run-status-semantics gotcha behind § 2f'sDave — Autopilot run failure sweepdesign../operations/multica-shared-runtime-quota-gotcha.md— the shared-quota constraint that starved § 2f's watchdog itself during a full weekly-quota exhaustion (OBJ-3927)
Last updated: 2026-09-17 (OBJ-3927: § 2f cross-linked to the confirmed occurrence of its own Step 0 self-check gap)