Skip to content

feat(triggers): Trigger System (Phase 2 Step 6)#11

Merged
AVADSA25 merged 2 commits intomainfrom
phase2-step6-triggers
May 2, 2026
Merged

feat(triggers): Trigger System (Phase 2 Step 6)#11
AVADSA25 merged 2 commits intomainfrom
phase2-step6-triggers

Conversation

@AVADSA25
Copy link
Copy Markdown
Owner

@AVADSA25 AVADSA25 commented May 2, 2026

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:

  • Default require_confirmation=True in design recommendations — opt-in to silent fires per-skill
  • Default cooldown_seconds=600 in design recommendations — 10-min minimum between fires
  • Destructive triggers route through Step 3 §1.7 strict-consent gate (literal verb-match, two-strike timeout)
  • Per-trigger PWA kill switch + persistent ~/.codec/triggers_killed.json
  • Global TRIGGERS_ENABLED=false env var disables the whole system
  • Step 6 ships zero triggers — at merge, evaluate() iterates over zero registered triggers and exits in <1ms. Nothing fires.
  • All fires go through codec_dispatch.run_skill — Step 2 run_with_hooks, Step 4 plugin observation, and Step 3 step budget all apply transparently.

Commits

sha what
0ac7f39 docs/PHASE2-STEP6-DESIGN.md — full §0-§10 design
02a1bfb Implementation: audit constants + codec_triggers.py + skill_registry ext + observer integration + 35 tests + PWA endpoints + AGENTS.md

Test plan

  • pytest tests/test_triggers.py35/35 passing in 0.18s
  • codec_dispatch.run_skill is mocked everywhere — NO real skills fired
  • codec_ask_user.ask is mocked everywhere — never blocks on real threading.Event
  • State files clean post-test: pending_questions=0, Apple Reminders=0, /tmp/codec_*.txt=0, triggers_killed.json absent
  • Sanity: codec_audit.PHASE2_STEP6_EVENTS resolves; codec_triggers.evaluate is callable
  • Per Step 4 contract: did NOT run full pytest suite

Test breakdown (35)

Area Count What it asserts
Trigger validation 5 All 5 required fields, key stability across reloads, key change on edit
Match per type 10 window_title / clipboard / file_change / time / compound (AND + OR)
Cooldown 5 Within window blocks, 0-cooldown ready, per-trigger independence, key-change resets, audit emit on cooldown block
Confirmation + destructive 8 Silent fire (confirm=False), confirm path approve/skip/timeout, destructive path verb-match/timeout, destructive overrides confirm
Kill switches + integration 7 Per-trigger kill silent skip + persistence, global TRIGGERS_ENABLED, observer.poll() calls evaluate(), skill_registry extracts SKILL_OBSERVATION_TRIGGER

§X Trigger flow

codec-observer poll loop
        │
        ▼
   evaluate(snapshot)
        │
        ├─ for each Trigger discovered from SkillRegistry:
        │     ├─ killed? → silent skip
        │     ├─ matches? → no, silent skip
        │     ├─ matches? → emit trigger_evaluated
        │     ├─ cooldown elapsed? → no, emit trigger_blocked
        │     │
        │     └─ destructive? → ask_user(destructive=True) — Step 3 §1.7
        │        require_confirmation? → ask_user(60s timeout, Approve/Skip)
        │        else → fire silently
        │
        ▼
   codec_dispatch.run_skill(skill, task=<rendered context>)
        │
        ▼ (existing chokepoint, hooks fire as usual)
   skill returns → emit trigger_fired

§6 Audit events (3 new)

Event level When
trigger_evaluated info A trigger's pattern matched (pre-cooldown / pre-consent)
trigger_fired info Skill actually invoked via dispatch
trigger_blocked warning Cooldown / confirmation reject / consent failure. block_reason ∈ {cooldown, user_skipped, confirmation_timeout, ambiguous_consent}. Killed reason intentionally NOT emitted.

What this PR does NOT do

  • No _HTTP_BLOCKED change
  • No new PM2 service — triggers run inline in codec-observer
  • No skill ships with SKILL_OBSERVATION_TRIGGER — system is dormant at merge
  • No ~/.codec/autopilot.json migration (it's empty {enabled: false, triggers: []})
  • No Apple Reminders / Notes / Calendar entries

After merge

  1. Pull main on local checkout.
  2. pm2 restart codec-observer codec-dashboard — picks up the integration + PWA routes.
  3. Verify with 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).
  4. To opt a skill in: edit the skill file, add SKILL_OBSERVATION_TRIGGER = {...}, restart codec-observer (rescan).

Phase 2 status after this PR

🤖 Generated with Claude Code

Mikarina13 added 2 commits May 2, 2026 18:00
…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).
@AVADSA25 AVADSA25 merged commit 2d2ff3f into main May 2, 2026
1 check passed
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants