feat(triggers): Trigger System (Phase 2 Step 6)#11
Merged
Conversation
…sent)
Concise §0-§10 design doc for Phase 2 Step 6, working from the
locked Q1-Q6 decisions in PHASE2-BLUEPRINT.md. Implementation
follows in same PR (no separate review cycle — user authority
delegated per "you decide" instruction).
Key design points locked in:
- Triggers run INLINE in codec_observer.poll() — no new PM2 service
(saves a process; observer is the single source of truth on cadence)
- SKILL_OBSERVATION_TRIGGER dict declares pattern in the skill file
itself (Q3 — alongside SKILL_TRIGGERS); 4 required fields, no
implicit defaults
- 5 trigger types: window_title_match / clipboard_pattern /
file_change / time / compound (recursive AND/OR)
- Time triggers ≥1 min granularity (matches observer cadence)
- Cooldown state in RAM (process restart resets all); state-file
approach rejected to avoid consistency questions
- Per-trigger kill switch persists at ~/.codec/triggers_killed.json
+ global TRIGGERS_ENABLED env var (default true)
- Confirmation gate via PWA notification (similar to AskUserQuestion);
destructive triggers route through Step 3 §1.7 strict-consent gate
- Step 6 ships ZERO triggers — only the plumbing. Skills opt in
one-by-one; same trust model as plugins (user-curated local Python).
At merge time: 0 triggers registered, evaluate() exits in <1ms,
no fires possible.
3 new audit events:
trigger_evaluated (info)
trigger_fired (info)
trigger_blocked (warning) — extra.block_reason ∈ {cooldown,
user_skipped, confirmation_timeout, ambiguous_consent, killed}
35 tests planned across:
§7.1 Trigger validation (5)
§7.2 Match logic per type (10)
§7.3 Cooldown (5)
§7.4 Confirmation + destructive (8)
§7.5 Kill switches + integration (7)
Test isolation contract: codec_dispatch.run_skill MUST be mocked
in every test that touches the dispatch path. NEVER fire real skills
in tests (per the May 1 incident hygiene).
Diff inventory: ~+997 (functional + tests + docs). In line with
Step 5's ~+1,448 and Phase 1 step sizes.
§9 explicitly empty for v1: no open questions blocking implementation.
Anything that comes up will be surfaced in the PR description, not
a separate review cycle.
Implementation begins next commit.
… consent)
Skills declare SKILL_OBSERVATION_TRIGGER alongside SKILL_TRIGGERS;
codec_observer.poll() evaluates registered triggers against the snapshot
after every poll and dispatches matches through codec_dispatch.run_skill.
35 tests, all mocking codec_dispatch + codec_ask_user (no real fires).
Step 6 ships ZERO triggers — only plumbing.
Components:
codec_audit.py (+38 LOC)
- 3 new event constants: TRIGGER_EVALUATED, TRIGGER_FIRED,
TRIGGER_BLOCKED
- PHASE2_STEP6_EVENTS frozenset
- TRIGGER_EXTRA_FIELDS doc tuple
codec_triggers.py NEW (~520 LOC)
- Trigger dataclass with stable sha8 key + short_summary()
- _validate_trigger_dict (5 required fields; compound recurses)
- 5 matchers: window_title_match / clipboard_pattern /
file_change / time / compound (AND/OR)
- Cooldown state in RAM (process restart resets); per-trigger
- Kill state at ~/.codec/triggers_killed.json (atomic write)
- evaluate(snapshot, registry, fire) returns status list
- Confirmation gate via codec_ask_user.ask (60s timeout, much
shorter than ask_user default 600s to keep observer cadence
responsive)
- Destructive gate routes through Step 3 §1.7 strict-consent
- Audit emits: only on match (skipped → silent), block_reason
values: cooldown / user_skipped / confirmation_timeout /
ambiguous_consent (killed is silent to avoid spam)
codec_skill_registry.py (+15 LOC)
- AST extracts SKILL_OBSERVATION_TRIGGER + SKILL_NEEDS_OBSERVATION
- get_observation_trigger(name) accessor
codec_observer.py (+12 LOC)
- After _emit_observation_tick, calls codec_triggers.evaluate(
snapshot). try/except; trigger failures NEVER break polling.
routes/triggers.py NEW (~95 LOC)
- GET /api/triggers — list + global enabled state + killed count
- GET /api/triggers/{key} — per-trigger detail with cooldown_remaining
- POST /api/triggers/{key}/kill — toggle kill state (atomic write)
- All auth-gated by existing /api/* middleware
codec_dashboard.py (+8 LOC)
- Conditionally include_router(triggers_router) — fail-soft if
module not loaded
tests/test_triggers.py NEW (~580 LOC, 35 tests)
§7.1 Trigger validation (5)
§7.2 Match logic per type (10)
§7.3 Cooldown (5)
§7.4 Confirmation + destructive (8)
§7.5 Kill switches + integration (7)
CRITICAL test isolation:
- codec_dispatch.run_skill mocked everywhere
- codec_ask_user.ask mocked everywhere
- _LAST_FIRED + _KILLED_CACHE reset per test
- _KILLED_PATH redirected to tmp_path
AGENTS.md (+44 LOC)
- §3 new "Trigger System (Phase 2 Step 6)" sub-section
- §6 new audit events table
- §10 new don't-touch entries (triggers_killed.json,
TRIGGERS_ENABLED, SKILL_OBSERVATION_TRIGGER + ocr_enabled note)
docs/PHASE2-STEP6-DESIGN.md (committed earlier on this branch)
Test result: 35 passed in 0.21s. Side-effects clean:
pending_questions: 0
Apple Reminders: 0
/tmp/codec_*.txt: 0
triggers_killed.json: absent
Diff inventory: ~+1,310 LOC functional + tests + design doc.
Safety summary (what this PR explicitly does NOT do):
- Does NOT ship any skill with SKILL_OBSERVATION_TRIGGER set
- Does NOT migrate autopilot.json (which is empty anyway)
- Does NOT auto-fire any skill at merge time (zero registered
triggers → evaluate() exits in <1ms)
- Does NOT touch _HTTP_BLOCKED
- Does NOT create Apple Reminders / Notes / Calendar entries
- Does NOT add a new PM2 service (triggers run inline in
codec-observer; one process, one cadence, one audit cid)
Phase 2 status after merge: 2/3 (Steps 5 + 6 done; Step 7 = Shift
Report Crew remaining).
5 tasks
AVADSA25
pushed a commit
that referenced
this pull request
May 2, 2026
…(Steps 5/6/7) Phase 2 (Observer + Triggers + Shift Report) merged and production-stable: - Step 5 (PR #9 824a52f) + hotfix PR #10 (26e6add) — `observer.ocr_enabled` flag - Step 6 (PR #11 2d2ff3f) — Trigger System (matcher + cooldown + consent) - Step 7 (PR #12 0e40687) — end-of-day shift report Net: +91 passing tests (823/20/73), 0 new failures, 0 new skips. Live audit proof captured: shift_report_started+_completed paired emits at 2026-05-02T18:49:40Z with shared cid=5f188e5485e5.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Phase 2 Step 6 — skills can declare patterns that auto-fire them when the observer (Step 5) detects a match. The infrastructure for ambient automation. Step 6 ships ZERO triggers — only the plumbing. Skills opt in one-by-one via
SKILL_OBSERVATION_TRIGGER, same trust model as plugins.Why this is safe
The May 1 incident was caused by skills firing without explicit user trigger. Step 6 is exactly that capability. Belt-and-braces:
require_confirmation=Truein design recommendations — opt-in to silent fires per-skillcooldown_seconds=600in design recommendations — 10-min minimum between fires~/.codec/triggers_killed.jsonTRIGGERS_ENABLED=falseenv var disables the whole systemevaluate()iterates over zero registered triggers and exits in <1ms. Nothing fires.codec_dispatch.run_skill— Step 2run_with_hooks, Step 4 plugin observation, and Step 3 step budget all apply transparently.Commits
0ac7f39docs/PHASE2-STEP6-DESIGN.md— full §0-§10 design02a1bfbTest plan
pytest tests/test_triggers.py→ 35/35 passing in 0.18scodec_dispatch.run_skillis mocked everywhere — NO real skills firedcodec_ask_user.askis mocked everywhere — never blocks on realthreading.Eventpending_questions=0,Apple Reminders=0,/tmp/codec_*.txt=0,triggers_killed.json absentcodec_audit.PHASE2_STEP6_EVENTSresolves;codec_triggers.evaluateis callableTest breakdown (35)
§X Trigger flow
§6 Audit events (3 new)
trigger_evaluatedtrigger_firedtrigger_blockedblock_reason∈ {cooldown,user_skipped,confirmation_timeout,ambiguous_consent}. Killed reason intentionally NOT emitted.What this PR does NOT do
_HTTP_BLOCKEDchangecodec-observerSKILL_OBSERVATION_TRIGGER— system is dormant at merge~/.codec/autopilot.jsonmigration (it's empty{enabled: false, triggers: []})After merge
pm2 restart codec-observer codec-dashboard— picks up the integration + PWA routes.curl -H 'cookie: ...' http://127.0.0.1:8090/api/triggers→{"triggers": [], "global_enabled": true, "total": 0, "killed_count": 0}(empty list confirms zero registered).SKILL_OBSERVATION_TRIGGER = {...}, restart codec-observer (rescan).Phase 2 status after this PR
🤖 Generated with Claude Code