feat(automation): event outbox + hook policy layer (MUL-4332 PR1+PR2)#5467
Open
Bohan-J wants to merge 10 commits into
Open
feat(automation): event outbox + hook policy layer (MUL-4332 PR1+PR2)#5467Bohan-J wants to merge 10 commits into
Bohan-J wants to merge 10 commits into
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
31e559b to
0fde4d4
Compare
Bohan-J
pushed a commit
that referenced
this pull request
Jul 16, 2026
… (MUL-4332 PR1) Second-round review must-fixes on PR #5467: 1. Concurrent status-only update no longer rolls back a concurrent writer's assignee (or other nullable field): LockIssueStatusForEvent now locks and returns the full row (LockIssueRowForUpdate); UpdateIssue/BatchUpdateIssues rebuild every untouched bare-narg column from the locked row instead of the pre-tx snapshot, and drive all post-commit side effects (realtime, enqueue, parent-notify) from that authoritative pre-image. 2. Bulk task.failed is now poison-isolated and bounded. Replaced the four condition-scoped fail UPDATEs with bounded candidate SELECTs (FOR UPDATE SKIP LOCKED + LIMIT) + a shared FailAgentTasksByIDs; FailBulkTasksWithEvents resolves each candidate's workspace, fails only the resolvable set with its event atomically, and skips (fail-closed) an unresolvable row without aborting the batch. Offline-runtime task cleanup now runs every tick (sweepOfflineRuntimeTasks), so a rolled-back or leftover orphan is retried instead of stranded. 3. Bulk task.failed events are attributed to SystemActor (sweeper/orphan recovery, not the agent) and carry retryable computed from the shared retryEligible predicate, so the event agrees with the post-commit auto-retry decision instead of hard-coding false. 4. Lock-internal pre-image now drives lock-external side effects: the stuck-issue reset re-checks status==in_progress and no-active-task UNDER the lock (CAS) so a user-completed issue is never reopened; the GitHub merge close suppresses the parent child-done comment / realtime status_changed on a no-op transition. Nit: 196_domain_event.down.sql comment now says "Reverses 196". Tests: concurrent reassign-not-rolled-back, poison-row isolation, transient failure next-tick recovery, terminal-vs-reset race (lock-hold), GitHub no-op suppression, and actor/retryable assertions on the bulk fail. Verified against a fresh fully-migrated DB across domainevent/service/handler/cmd-server. Co-authored-by: multica-agent <github@multica.ai>
dec6b91 to
ffd0f75
Compare
Bohan-J
pushed a commit
that referenced
this pull request
Jul 16, 2026
… (MUL-4332 PR1) Rebased onto current main and renumbered the six domain_event migrations to 200–205 (main advanced to 199 with issue_property_icon + agent_task_attribution), restoring the numeric-prefix uniqueness the CI lint enforces. Second-round review must-fixes on PR #5467: 1. Concurrent status-only update no longer rolls back a concurrent writer's assignee (or other nullable field): LockIssueStatusForEvent now locks and returns the full row (LockIssueRowForUpdate); UpdateIssue/BatchUpdateIssues rebuild every untouched bare-narg column from the locked row instead of the pre-tx snapshot, and drive all post-commit side effects (realtime, enqueue, parent-notify) from that authoritative pre-image. 2. Bulk task.failed is now poison-isolated and bounded. Replaced the four condition-scoped fail UPDATEs with bounded candidate SELECTs (FOR UPDATE SKIP LOCKED + LIMIT) + a shared FailAgentTasksByIDs; FailBulkTasksWithEvents resolves each candidate's workspace, fails only the resolvable set with its event atomically, and skips (fail-closed) an unresolvable row without aborting the batch. Offline-runtime task cleanup now runs every tick (sweepOfflineRuntimeTasks), so a rolled-back or leftover orphan is retried instead of stranded. 3. Bulk task.failed events are attributed to SystemActor (sweeper/orphan recovery, not the agent) and carry retryable computed from the shared retryEligible predicate, so the event agrees with the post-commit auto-retry decision instead of hard-coding false. 4. Lock-internal pre-image now drives lock-external side effects: the stuck-issue reset re-checks status==in_progress and no-active-task UNDER the lock (CAS) so a user-completed issue is never reopened; the GitHub merge close suppresses the parent child-done comment / realtime status_changed on a no-op transition. Nit: the domain_event down migration comment now says "Reverses 200". Tests: concurrent reassign-not-rolled-back, poison-row isolation, transient failure next-tick recovery, terminal-vs-reset race (lock-hold), GitHub no-op suppression, and actor/retryable assertions on the bulk fail. Verified against a fresh DB fully migrated to 205 (main + this branch) across domainevent/service/handler/cmd-server, plus full up/down/replay. Co-authored-by: multica-agent <github@multica.ai>
Bohan-J
pushed a commit
that referenced
this pull request
Jul 16, 2026
… (MUL-4332 PR1) Rebased onto current main and renumbered the six domain_event migrations to 200–205 (main advanced to 199 with issue_property_icon + agent_task_attribution), restoring the numeric-prefix uniqueness the CI lint enforces. Second-round review must-fixes on PR #5467: 1. Concurrent status-only update no longer rolls back a concurrent writer's assignee (or other nullable field): LockIssueStatusForEvent now locks and returns the full row (LockIssueRowForUpdate); UpdateIssue/BatchUpdateIssues rebuild every untouched bare-narg column from the locked row instead of the pre-tx snapshot, and drive all post-commit side effects (realtime, enqueue, parent-notify) from that authoritative pre-image. 2. Bulk task.failed is now poison-isolated and bounded. Replaced the four condition-scoped fail UPDATEs with bounded candidate SELECTs (FOR UPDATE SKIP LOCKED + LIMIT) + a shared FailAgentTasksByIDs; FailBulkTasksWithEvents resolves each candidate's workspace, fails only the resolvable set with its event atomically, and skips (fail-closed) an unresolvable row without aborting the batch. Offline-runtime task cleanup now runs every tick (sweepOfflineRuntimeTasks), so a rolled-back or leftover orphan is retried instead of stranded. 3. Bulk task.failed events are attributed to SystemActor (sweeper/orphan recovery, not the agent) and carry retryable computed from the shared retryEligible predicate, so the event agrees with the post-commit auto-retry decision instead of hard-coding false. 4. Lock-internal pre-image now drives lock-external side effects: the stuck-issue reset re-checks status==in_progress and no-active-task UNDER the lock (CAS) so a user-completed issue is never reopened; the GitHub merge close suppresses the parent child-done comment / realtime status_changed on a no-op transition. Nit: the domain_event down migration comment now says "Reverses 200". Tests: concurrent reassign-not-rolled-back, poison-row isolation, transient failure next-tick recovery, terminal-vs-reset race (lock-hold), GitHub no-op suppression, and actor/retryable assertions on the bulk fail. Verified against a fresh DB fully migrated to 205 (main + this branch) across domainevent/service/handler/cmd-server, plus full up/down/replay. Co-authored-by: multica-agent <github@multica.ai>
116e59d to
bdd7e99
Compare
Bohan-J
pushed a commit
that referenced
this pull request
Jul 16, 2026
…-4332 PR1) Address Elon's round-4 review on PR #5467. 1. recover-orphans keyset cursor + SKIP LOCKED race (point 1): SelectOrphanedTasksForRuntime now uses plain FOR UPDATE. The drain pages forward with a keyset cursor that advances permanently, so SKIP LOCKED could silently drop a briefly-locked older orphan and step the cursor past it, leaking it forever (the re-registered runtime is already online, so the offline sweep won't reap it). Plain FOR UPDATE waits for the lock and re-checks status, so the older row is included. Only this query changes; the sweepers re-scan every tick and keep SKIP LOCKED. 2. Legacy daemon {} backward compat (point 2): A current daemon now sends paginate:true and drives the drain itself. A legacy daemon POSTs {} once and ignores the body, so the server drains every page itself in bounded per-page transactions instead of leaving 501+ orphans stranded past the first page. Both modes share recoverOrphansPage. Tests: TestRecoverOrphansWaitsForLockedOldestOrphan (blocks on a locked oldest orphan, then fails it with its event once released), and TestRecoverOrphansLegacyClientDrainsServerSide (501 rows, one {} call, all failed + events). The existing paginated drain test now sends paginate:true. Co-authored-by: multica-agent <github@multica.ai>
PR1 of the Event Hooks MVP: land the durable `domain_event` transactional outbox and converge the v1 domain write paths onto it. There is NO consumer yet — rows are written dispatch_status='pending' and nothing reads them, so this is an additive, zero-behavior-change increment. The PR3 matcher/executor will claim pending rows via the lease columns. Foundation - New `domain_event` table (MUL-4332 §4.1): fact envelope + causal chain + single-consumer outbox lease + monotonic seq. No FK / no cascade per the workspace rules; five CONCURRENTLY indexes in their own single-statement migrations; a 90-day retention sweeper (a no-op until events dispatch). - `internal/domainevent`: versioned v1 event catalog, typed payload structs, a tx-aware Write (event commits atomically with the fact) and a WriteInTx helper that wraps otherwise-bare autocommit writes. Write-path convergence (event committed in the same tx as the fact) - issue.created: HTTP create, autopilot dispatch, onboarding (both shims). - issue.status_changed + issue.assigned: single UpdateIssue, BatchUpdateIssues, GitHub merge auto-close, task-failure stuck-issue reset. - task.completed / task.failed: CompleteTask / FailTask (status CAS makes the event exactly-once on daemon-callback replay). Separate from internal/events (the in-memory Bus), which keeps serving realtime UI. Verified: all 198 migrations apply + reverse cleanly; writer atomicity (commit persists / rollback persists nothing / WriteInTx all-or-nothing) on a live DB; the full service + handler suites pass; an end-to-end test proves the outbox fires through the real HTTP handlers. Co-authored-by: multica-agent <github@multica.ai>
… PR1) Rebased onto main and resolved all six merge-blocking review points: 1. Renumber migrations 196–201 (193–195 are now issue_property on main); the TestMigrationNumericPrefixesStayUniqueAfterLegacySet lint passes again. 2. Converge the remaining v1 producers in-tx: agent-runtime and system child-done comments (comment.created), and all four bulk task.failed paths — the three runtime sweepers plus daemon orphan-recovery — through a shared FailTasksInTxWithEvents helper. No terminal path commits a fact without its event. 3. Read issue status/assignee under a row lock INSIDE the update tx so the event `from` is the true edge under concurrent transitions (single + batch UpdateIssue, GitHub merge close, task-failure reset), not a pre-tx snapshot. 4. Task-event workspace resolution is fail-closed: resolve via the full attribution chain (issue/chat/autopilot/quick-create/agent) on the tx handle, and roll the terminal transition back if unresolvable rather than skipping the event best-effort. 5. Retention is now an explicit no-op until PR3 — removed the weak dispatched+TTL delete that lacked the hook_execution terminal predicate. 6. Actor identity is fail-closed: ActorFrom no longer laundelrs an unknown type into system, and validate rejects a system actor carrying an id or a member/agent/hook actor missing one. Tests: concurrent status-edge correctness (-race), fail-closed workspace resolution, bulk task.failed emission via the shared helper, and expanded actor-validation cases. Full internal/service, internal/handler, internal/domainevent and migration-lint suites pass on a migrated DB. Co-authored-by: multica-agent <github@multica.ai>
… (MUL-4332 PR1) Rebased onto current main and renumbered the six domain_event migrations to 200–205 (main advanced to 199 with issue_property_icon + agent_task_attribution), restoring the numeric-prefix uniqueness the CI lint enforces. Second-round review must-fixes on PR #5467: 1. Concurrent status-only update no longer rolls back a concurrent writer's assignee (or other nullable field): LockIssueStatusForEvent now locks and returns the full row (LockIssueRowForUpdate); UpdateIssue/BatchUpdateIssues rebuild every untouched bare-narg column from the locked row instead of the pre-tx snapshot, and drive all post-commit side effects (realtime, enqueue, parent-notify) from that authoritative pre-image. 2. Bulk task.failed is now poison-isolated and bounded. Replaced the four condition-scoped fail UPDATEs with bounded candidate SELECTs (FOR UPDATE SKIP LOCKED + LIMIT) + a shared FailAgentTasksByIDs; FailBulkTasksWithEvents resolves each candidate's workspace, fails only the resolvable set with its event atomically, and skips (fail-closed) an unresolvable row without aborting the batch. Offline-runtime task cleanup now runs every tick (sweepOfflineRuntimeTasks), so a rolled-back or leftover orphan is retried instead of stranded. 3. Bulk task.failed events are attributed to SystemActor (sweeper/orphan recovery, not the agent) and carry retryable computed from the shared retryEligible predicate, so the event agrees with the post-commit auto-retry decision instead of hard-coding false. 4. Lock-internal pre-image now drives lock-external side effects: the stuck-issue reset re-checks status==in_progress and no-active-task UNDER the lock (CAS) so a user-completed issue is never reopened; the GitHub merge close suppresses the parent child-done comment / realtime status_changed on a no-op transition. Nit: the domain_event down migration comment now says "Reverses 200". Tests: concurrent reassign-not-rolled-back, poison-row isolation, transient failure next-tick recovery, terminal-vs-reset race (lock-hold), GitHub no-op suppression, and actor/retryable assertions on the bulk fail. Verified against a fresh DB fully migrated to 205 (main + this branch) across domainevent/service/handler/cmd-server, plus full up/down/replay. Co-authored-by: multica-agent <github@multica.ai>
…-4332 PR1) Address Elon 3rd-round review (2 must-fixes): 1. recover-orphans now drains every orphan for a runtime instead of capping at one 500-row page. Registration flips the runtime back online, so the offline sweep never reaps rows left past the first page; SelectOrphanedTasksForRuntime gains a (created_at, id) keyset cursor, the handler returns has_more + next_cursor per page, and the daemon client loops until drained. The cursor advances over skipped poison rows so a page of poison at the front can't stall the drain. Tests: handler limit+1 end-to-end drain, service poison-next-page. 2. task.failed.retryable renamed to retry_eligible with atomic, decidable semantics: it reports only that the failure met the eligibility predicate at commit time, computed in the same tx as the fail+event on every path. It no longer promises a fresh attempt will arrive — the bulk paths create the retry child best-effort after commit. Dropped the "not yet terminal, a fresh attempt is coming" consumer guarantee. Test: retry child crash-window contract. Co-authored-by: multica-agent <github@multica.ai>
…-4332 PR1) Address Elon's round-4 review on PR #5467. 1. recover-orphans keyset cursor + SKIP LOCKED race (point 1): SelectOrphanedTasksForRuntime now uses plain FOR UPDATE. The drain pages forward with a keyset cursor that advances permanently, so SKIP LOCKED could silently drop a briefly-locked older orphan and step the cursor past it, leaking it forever (the re-registered runtime is already online, so the offline sweep won't reap it). Plain FOR UPDATE waits for the lock and re-checks status, so the older row is included. Only this query changes; the sweepers re-scan every tick and keep SKIP LOCKED. 2. Legacy daemon {} backward compat (point 2): A current daemon now sends paginate:true and drives the drain itself. A legacy daemon POSTs {} once and ignores the body, so the server drains every page itself in bounded per-page transactions instead of leaving 501+ orphans stranded past the first page. Both modes share recoverOrphansPage. Tests: TestRecoverOrphansWaitsForLockedOldestOrphan (blocks on a locked oldest orphan, then fails it with its event once released), and TestRecoverOrphansLegacyClientDrainsServerSide (501 rows, one {} call, all failed + events). The existing paginated drain test now sends paginate:true. Rebased onto main (inbox index migrations landed at 200/201); renumbered the six domain_event migrations 200-205 -> 202-207 and fixed their cross-references. Co-authored-by: multica-agent <github@multica.ai>
44084cf to
50e9895
Compare
Co-authored-by: multica-agent <github@multica.ai>
…332 PR2) Builds the Event Hooks policy & debug layer on top of the PR2 persistence model (hook/revision/state/execution/effect tables, 208-217). Store-only and gated behind the automation_event_hooks feature flag (default off), so it adds zero runtime behavior until the matcher/executor slice ships: creating hooks persists rows that nothing yet consumes. - internal/automation: the user-authored hook spec (when/if/fire/do), the machine-readable event schema registry that declares which envelope/payload fields each domain event exposes for matching (§5.3/§6), the fixed condition and action vocabularies, and a pure typed Validator. The validator rejects an illegal rule at the API boundary (unknown/undeclared fields, bad uuids, wrong action shape, >8 actions, system-only actions on a user hook) and enforces the rising_edge dependency rule: a latch hook must listen to exactly the event that can change its condition (§5.2). - pkg/db/queries/hook.sql: workspace-scoped CRUD for hooks, immutable revisions, and the execution trace. Hook + revision #1 are created together with app-generated ids (they reference each other, no FK). - internal/service/hook.go: HookService — create (validate → resolve accountable principal → tx insert hook+revision), get, list, patch (append immutable revision + repoint active), enable/disable, soft-archive, executions read. System-managed hooks are not editable through this API (§8). - internal/handler/hook.go + routes: /api/hooks REST surface, every endpoint gated on the feature flag (404 when off) and resolving the audit creator + accountable human principal — an agent author with no resolvable member is refused (§8). - cmd/multica/cmd_hook.go: hook create/list/get/update/enable/disable/delete/ executions CLI. Tests: automation validator units (accepts A2 join / per_event, rejects 20+ illegal shapes, rising-edge coverage, ParseMatch clauses); handler CRUD lifecycle (create→get→update-appends-revision-2→list→disable→enable→ executions→soft-archive→404), feature-flag-off 404, invalid-spec 400, and agent-without-principal 403. Remaining MVP slices continue in this PR: PR3 matcher/executor + guardrails, PR4 actions/autopilot, PR5 stage sensor + system hooks. The execution flag stays off until all guardrails exist (§14). Co-authored-by: multica-agent <github@multica.ai>
…L-4332 PR2) Address Elon's four must-fixes on the hook CRUD layer. All four become real authorization / correctness issues once PR3 starts consuming saved rules. 1. [P0] Edit authorization gate. Any workspace member could PATCH/enable/disable/ delete any user hook while it kept running under the original principal (authorization-principal substitution). Now only the hook's original authorization principal OR a workspace owner/admin may modify it; the principal is NOT transferred on edit. UpdateHook/SetEnabled/ArchiveHook enforce authorizeHookEdit. (Deny-by-default per the proposed semantic, pending Bohan's final confirmation; easy to switch to principal-transfer.) 2. [P0] Fail-closed target/workspace validation. Create/update now validate, in the write transaction under the current principal, that every referenced target exists in the hook's workspace: issue scope, condition issue ids, and action issue/agent/member/autopilot targets — with the trigger_agent target also checked for archived/runtime and canInvokeAgent admission. The resolved principal must still be a workspace member. A missing target now yields 400, not a persisted 201 (§13). Adds GetMemberInWorkspace. 3. [P1] Strict schema. The handler decodes with DisallowUnknownFields (rejects unknown top-level/nested keys); the validator rejects per-action disallowed-but-known fields (e.g. agent_id on add_comment), validates the issue-status enum on set_issue_status / issues_status / issue_field(status), and bounds id-set and message sizes. 4. [P1] Concurrent PATCH. UpdateHook now SELECT ... FOR UPDATE locks the hook row and re-checks archived/origin/authorization inside the lock before allocating MAX(revision)+1, so concurrent edits serialize instead of colliding on idx_hook_revision_unique (was 23505 → 500). Adds GetHookForUpdate. Tests: strict-schema rejections (unknown field, disallowed action field, bad status enum), fail-closed targets (nonexistent issue/member/agent/autopilot → 400; real invokable agent → 201), edit authorization (non-principal non-admin → 403 on patch/disable/delete; principal and owner/admin → 200; principal never transferred), and 8-way concurrent PATCH asserting contiguous revisions 1..9 with no error. dry-run/explain + event-correlation queries are re-sliced to PR3 (they need the matcher's evaluation), pending Bohan's confirmation. Co-authored-by: multica-agent <github@multica.ai>
…-4332 PR2) Address Elon's round-3 review on the hook policy layer. 1. Admission by the hook's STORED principal, not the editor. Previously resolveHookWriter captured the requester's principal in a canInvoke closure, so a workspace admin editing member A's hook could add a trigger_agent for the admin's own private agent — saved as A, who cannot invoke it. Target admission is now evaluated inside the write transaction against the principal read from the FOR UPDATE-locked hook row (the creator on create, the stored principal on update). New internal/admission package holds the pure agent/ autopilot predicates so handler and service share one source of truth. 2. run_autopilot now checks the stored principal's write permission (role/authorship/collaborator, reusing memberCanWriteAutopilot semantics via admission.AutopilotWriteByOwnership), not mere existence. issue_field parent_issue_id / assignee_id eq|in operands are now validated to resolve to workspace resources (issue, or member/agent for assignees). 3. Membership, role and the edit gate are re-derived INSIDE the write transaction: create re-checks the principal is a current member; update/ enable/disable/archive load the FOR UPDATE-locked row, re-read the editor's live membership/role, authorize, and mutate in one transaction. HookAuthor no longer carries a handler-computed IsWorkspaceAdmin snapshot. 4. decodeHookSpec now rejects a smuggled trailing second JSON document (a second Decode must return io.EOF), which DisallowUnknownFields alone did not catch. Tests: admission unit tests; stored-principal admission (admin cannot add a private agent A can't invoke → 400; owner's own hook may → 201); run_autopilot write-permission (non-writer member → 400, creator → 201); issue_field operand validation; non-member principal rejected at the service (nothing persisted); trailing-JSON rejection. Existing CRUD/auth/concurrency/strict-schema/fail-closed suites still pass. Co-authored-by: multica-agent <github@multica.ai>
…ssion (MUL-4332 PR2) Address Elon's round-4 review on the hook policy layer. 1. Stored principal's live membership is now a HARD gate on re-arming a hook. Update and Enable load the FOR UPDATE-locked hook's stored principal as a current workspace member and fail closed (403) if it departed — an admin can no longer re-point or re-enable a departed principal's hook. Disable/archive stay allowed (safe degrading). validateTargets is now only ever called with a pre-resolved live-member principal, and AgentInvocableByMember returns false for a non-member even in the agent-owner branch. 2. run_autopilot admission now also gates the autopilot's ASSIGNEE invocation, not just write ACL: it resolves the assignee to its executing agent (the agent, or a non-archived squad's leader) and requires the stored principal be able to invoke it; a dangling/archived assignee fails closed. A collaborator with write access whose autopilot targets an agent they cannot invoke is now rejected at save. (Shared checkAgentInvocable with trigger_agent.) 3. issue_field.assignee_id operands are validated with the correct identity types: a MEMBER operand is a USER id (GetMemberByUserAndWorkspace, not a member-row id — a real member's user id was previously 400'd), plus agent and non-archived squad. Issue assignees are polymorphic member|agent|squad. Tests: departed-principal gate (update/enable 403, disable/archive allowed); collaborator-write autopilot with un-invocable assignee rejected, owner accepted; assignee_id member user id / agent / squad accepted, ghost rejected; admission unit test for the non-member owner branch. No migration/sqlc changes. Co-authored-by: multica-agent <github@multica.ai>
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
Cumulative PR1 + PR2 of the Event Hooks MVP (MUL-4332). It lands the durable event foundation and the store-only policy/debug layer on top of it. There is no matcher/executor and no consumer yet, and the whole hook API surface sits behind the default-off
automation_event_hooksflag, so this is an additive, zero-behavior-change increment for existing workspaces. The matcher/executor (PR3), remaining actions/autopilot interop (PR4), and stage sensor/system hooks (PR5) follow in this same PR.PR1 — transactional-outbox event layer
domain_eventtable (§4.1): fact envelope (workspace_id,type,schema_version,subject_*,actor_*,payload) + causal chain (correlation_id,causation_*,hop_count) + single-consumer outbox lease + monotonicseq. No FK / no cascade. Migrations 202–207; every secondary/unique index is its own single-statementCREATE INDEX CONCURRENTLYmigration.internal/domainevent: versioned v1 event catalog, typed payload structs, tx-awareWrite(event commits atomically with the fact) and aWriteInTxhelper.issue.created,issue.status_changed,issue.assigned,comment.created,task.completed,task.failedacross the v1 producers (HTTP create/update, batch update, GitHub merge close, task terminal/reset, bulk sweepers, daemon orphan recovery).automation_event_hooksflag (default off), reserved for the PR3 executor.PR2 — hook policy & debug layer (store-only, behind the flag)
hook,hook_revision(immutable),automation_state,hook_execution,hook_action_effect— no FK, concurrent indexes.internal/automation: the user-authored hook spec (when/if/fire/do), the machine-readable event schema registry declaring matchable fields per event, the fixed condition/action vocabularies, and a strict typed validator (unknown-field rejection, per-action allowed fields, status enum, rising-edge dependency, id/message limits).internal/service(HookService) +/api/hooksREST +multica hookCLI: create / get / list / update (append immutable revision + repoint active) / enable / disable / soft-archive / executions.trigger_agent/run_autopilottargets are admitted against the hook's stored principal (agent invocation / autopilot write permission), not the editor.SELECT ... FOR UPDATE, so revisions stay contiguous.internal/admission: pure agent-invocation / autopilot-write predicates shared by the HTTP handlers and the hook service so both judge access identically.Still to come in this PR
set_issue_status/trigger_agent, and all depth/width/rate/concurrency/permission/fuse guardrails with crash-window tests. AlsoPOST /api/hooks/dry-run|explainandGET /api/events?correlation_id=(re-sliced here from the design's PR2 because they need the matcher's evaluation — pending product-owner confirmation).add_comment/send_inbox/run_autopilotexecution + Autopilot interop + correlation trace.set_issue_status_many/trigger_issue_assignee, S1/S2 managed system hooks, shadow/canary + old-path mutual exclusion.The execution flag stays off until all guardrails exist (§14), so no reaction ever fires from a partially-built engine.