Skip to content

feat(autonomy): trigger-dispatch package - contract, setup slice, envelope check (WP4)#372

Merged
kyle-sexton merged 10 commits into
mainfrom
feat/autonomy-trigger-dispatch
Jul 19, 2026
Merged

feat(autonomy): trigger-dispatch package - contract, setup slice, envelope check (WP4)#372
kyle-sexton merged 10 commits into
mainfrom
feat/autonomy-trigger-dispatch

Conversation

@kyle-sexton

@kyle-sexton kyle-sexton commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

WP4 trigger-dispatch package (T1), per the merged PLAN (all four phases complete on this branch).

No linked issue

Related

What ships

  • plugins/autonomy/reference/trigger-dispatch.md (new contract): four signal-surface classes (tracker-vcs-event / temporal / agent-internal / channel-feed), two recorded attributes (provenance, transport incl. push-lifecycle expiry fail-close + poll-detector backstop), six class-generic adapter obligations (normalize+enqueue only; layered idempotent dedup with create-then-reconcile and a live-duplicate drain guard; provenance + durable raw link; traceparent propagation; admission enforcement at the seam with absent-binding fail-close; closed-loop ack), security-surface work-class stamping with verified agent-internal parent provenance and higher-of class resolution, the schema-versioned signal envelope (marker <!-- autonomy:signal:v1 -->), kick + drain dispatch with the one-entrypoint invariant and execution-surface attestation, executor surface classes with security-owned executor_class.
  • Setup trigger/dispatch slice (skills/setup/SKILL.md): discovery-first per-class surface interview, DIY-floor kick + hourly drain as reviewable changes, plan-gated surfaces advisory with cost, execution-surface recording with the attestation caveat stated, fail-closed absent-admission wiring, additive triggers binding section (shared surfaces map shape that later scheduling sections reuse).
  • Templates: templates/trigger-adapters.md (per-class adapter shapes, six obligations inline), templates/ack-reply.md (one-line ack, item URL first).
  • Enforcement: scripts/check-signal-envelope.mjs — validates the envelope marker record (required keys, enums, W3C traceparent, agent-internal signal.parent_item as normalized canonical URL, deterministic raw-link origin branching via the binding's surfaces maps), exit contract 0/1/2 mirroring check-emission-conformance.mjs.
  • Five new setup evals; plugin 0.4.0; README + catalog.

Conforming-path demo (scratch repo, live)

Run on kyle-sexton/autonomy-demo-scratch (private throwaway) with the work-items capability's tracker seam installed (tools/work-item-tracker/ copied per its documented install path) and a repo-local binding carrying the triggers section. No admission binding present → the fail-closed path is what the demo proves. No paid service touched.

  1. Stub signal: label autonomy:trigger applied to source issue #2; labeled-event id 28172185501 fetched from the issue timeline (surface-native unique event id → signal.identity).
  2. Dedup pre-search for the identity: [] (no existing item).
  3. Enqueue via the seam (work-item-tracker.sh create-item): queue item #3 created carrying the full envelope — class/transport/provenance, identity, raw link (https://github.com/ghapi/repos/kyle-sexton/autonomy-demo-scratch/issues/events/28172185501), traceparent 00-dab5c21d6389b65c950f6b13f14af7e3-de1b63c53960913b-01 (the trigger-hop trace context observed on the enqueue record), NO signal.work_class (no classification rules bound → unclassified), label needs-human.
  4. Create-then-reconcile re-search: exactly one item (feat: markdown-formatter plugin (pilot migration) #3) matches the identity — no duplicate to close.
  5. Ack echo on the source surface: comment on #2Queued as https://github.com/kyle-sexton/autonomy-demo-scratch/issues/3 (autonomy: tracker-vcs-event signal).
  6. Envelope conformance: check-signal-envelope.mjs <item-3-body> --binding .claude/autonomy/binding.jsonSignal-envelope conformance OK: 1 envelope(s) checked. exit 0.
  7. Drain claims via the seam lease (autonomous drain mode, the ONE entrypoint): work-item-tracker.sh claim github:kyle-sexton/autonomy-demo-scratch#3 → lease acquired (holder: kyle-sexton, lease comment 5014153239), exit 0.
  8. Fail-closed refusal, audited live: admission check before execution found NO admission binding → autonomous execution REFUSED, refusal comment on #3, claim released; final state OPEN, labels ["needs-human"], assignees []. This also exercises the unclassified → human-gated path (no security binding → no signal.work_class).

No second dispatch path was exercised anywhere: the event kick and the drain both funnel into the seam lease.

Near-duplicate audit

The dispatch entrypoint composes the work-item queue capability's EXISTING autonomous drain mode and race-safe seam lease — no second claim or dispatch mechanism was created anywhere in this package. The adapter templates normalize and enqueue only; the drain-side live-duplicate guard and the create-then-reconcile rule reuse the queue's own primitives.

Gates

  • scripts/validate-plugins.sh exit 0; node scripts/validate-plugin-contracts.mjs exit 0 (vendor deny-list clean over reference/); node scripts/generate-catalog.mjs --check in sync.
  • claude plugin validate plugins/autonomy --strict pass; skill-quality check-skill.sh setup PASS (2 pre-existing WARNs: line soft-target, gotchas surface); evals.json valid against the bundled schema (ajv draft2020).
  • markdownlint 0 errors; typos clean; lychee (offline) 0 errors. Full plugin test suite rides CI (local run exceeds the shell timeout; prior-session finding).
  • Checker exit contract verified with fixtures: no-args → 2 + usage; valid tracker/temporal-local → 0; temporal without --binding, ci-cron origin with file: link, unrecorded surface, agent-internal missing signal.parent_item → 1 each.

Boris check

The 2→3 cell ("break up your work into loops and routines; let Claude kick off Claude") is instantiated as governed queue + kick; the step-3 trap is honored (audit trail before scale — envelope, ack, lease, refusal are all tracker-resident audit records; no agent-count machinery); the step-1 guardrail is untouched; the telemetry trace tree is the kick-off audit trail (traceparent on the enqueue record).

Fresh-docs mandate: GitHub Actions events reference and the Claude Code headless reference re-fetched this session before the SKILL.md edit (cited in the Phase 2 commit).

PLAN.md (as merged, tags advanced)

ai-ladder-wp4-trigger-dispatch

Brief

TLDR

Trigger-dispatch package (T1): a contract doc for signal adapters and dispatch, plus the
trigger/dispatch slice of guided-setup. Adapters normalize signals from four surface classes
into the governed work-item queue; one dispatch entrypoint drains it; the executor is
swappable behind the invocation-adapter seam.

Goal

Any adopting org can wire its signal surfaces into the governed queue and get autonomous
dispatch without a human kick — contract vocabulary only, free by default, audit trail
intact end to end.

Locked decisions

# Decision
D1 Scope: contract doc + trigger/dispatch slice of guided-setup, both in the capability-distribution home. Fleet adapter materializations = /work-items backlog.
D2 Four signal-surface classes, ALL contract-active: tracker/VCS-host events (label, assignment, @-mention, PR event); temporal (schedule + poll-fallback detectors for push-less surfaces); agent-internal (session emits follow-up work); channel/data-feed (chat mention + continuous monitor). Surface determines adapter home (WP1 D3 role split per class). Per-org availability is a binding outcome — no entitlement → advisory path per WP1 D6 — never a contract deferral. Exact class tokens at architect naming pass.
D3 Two recorded attributes on every queued signal: initiator provenance (human / agent / system — audit + guardrail-matrix input; recorded, never trusted as an isolation axis per T2) and transport (PUSH / PUSH-WITH-LIFECYCLE / POLL; push preferred where the surface offers it, poll is the universal fallback via the temporal class; lifecycle variant carries subscription expiry + renewal + validation handshake).
D4 Six class-generic adapter obligations: (1) normalize + enqueue only — never execute, no queue bypass; (2) idempotent dedup — key from signal identity, at-least-once delivery is universal on push surfaces; (3) provenance capture + durable raw-signal link on the item; (4) trace-context propagation — one causal tree trigger → CI → agent session (T6 import); (5) admission enforcement at the seam — policy content owned by the guardrail matrix (T3), binding owned by the settings-as-code home; the adapter enforces, never defines; unadmitted → human-gated item or audited rejection; (6) closed-loop acknowledgment — bidirectional surfaces echo the item reference back (tracker comment, chat thread reply); reply-less surfaces satisfy via the provenance link alone.
D5 Dispatch: push kick where the platform offers it (e.g. a CI job fired by the enqueue event) + a standing scheduled drain as universal fallback and catch-up net. One-entrypoint invariant: every kick funnels into the existing autonomous drain mode of the work-item queue capability via the invocation-adapter seam; the race-safe lease makes concurrent kicks harmless. Concurrency and per-run item caps are guardrail-matrix policy knobs. Executor hosting (in-CI vs drain-elsewhere) is a deployment-owned binding per the T7 hosting stance — the contract fixes only invariants (isolation floor L2+, credential scoping, queue contract); guided-setup surfaces the compute/credential tradeoff per org.
D6 Executor surface classes imported from T4 unchanged: self-operated CLI/SDK — including SDK-embedded pull/drain daemons (Boris step-4 products cell names the vendor agent SDK for programmatic scheduling; cross-vendor equivalents exist) — vs vendor-hosted, whose merge policy caps at human-gated. The CI-action-class automation mode (arbitrary prompt on any workflow event) is live-verified.
D7 Guided-setup slice is discovery-first (WP1 D7): interview the adopting org for which surfaces exist, transport capability per surface, and entitlements; wire the DIY floor (chat-platform bot + events subscription, or plain inbound webhook) as reviewable changes; advise plan-gated native integrations with cost surfaced; bind admission policy; set up kick + drain.

Constraints

  • Any fleet repo or vendor name in normative contract text is a defect; vendor names appear
    only as marked examples and in binding docs.
  • One queue, one lease, one dispatch entrypoint — no second claim or dispatch mechanism
    anywhere in the package.
  • No new cost by default; paid surfaces are advisory + explicit opt-in with cost surfaced.
  • The contract never invents an event bus and never raises domain events — the adopting
    org's own systems own event definition and raising.
  • Boris-alignment is the standing acceptance criterion (no step-skipping, trust before
    scale).

Acceptance criteria

  • Contract doc names the four surface classes, six adapter obligations, three-value
    transport enum, and kick + drain dispatch spec — in contract vocabulary only.
  • Guided-setup slice is discovery-first, wires only reviewable changes, advises gated
    surfaces with cost, and has zero paid dependencies on its default path.
  • No queue bypass or second dispatch mechanism exists anywhere in the package.
  • Research gaps are carried visibly, not laundered: channel-monitor ambient
    initiate-vs-notify UNVERIFIED; the two channel-agent vendor surfaces relied on are
    alpha/beta moving targets; no first-party Discord trigger found (UNVERIFIED-absence).
  • Boris check: the 2→3 cell ("break up your work into loops and routines; let Claude kick
    off Claude") is instantiated as governed queue + kick; the step-3 trap is honored (audit
    trail before scale, no agent-count machinery); the step-1 guardrail is untouched; the T6
    trace tree is the kick-off audit trail.

Captured assumptions

  • The work-item queue capability's autonomous drain mode + seam lease remain the dispatch
    entrypoint (live-verified against the installed capability this session).
  • The CI-action-class executor supports arbitrary-prompt automation runs on schedule, label,
    and manual-dispatch events (official README fetched this session).
  • Vendor channel integrations remain plan-gated and the DIY floor remains free
    (RESEARCH-channel-adapters.md, Jul 2026; flagged as moving targets).

Out-of-scope (deferred with triggers)

  • Runner build — trigger: T4 build trigger fires (WP7 owns the design pack).
  • Fleet adapter materializations (reusable workflows, labels, drain-routine stand-up) —
    /work-items backlog post-graduation.
  • Routine definitions for the v1 catalog — WP6.
  • Dispatch POLICY content (which classes auto-dispatch, per-class gates) — WP5 (guardrail
    matrix instance).

Deferred questions

  • Exact class/attribute tokens + signal schema — /architect (with the plugin naming pass).
  • Adapter template shapes per surface class + per-vendor binding docs — /architect.
  • Acknowledgment/reply-back template wording — /architect.
  • Drain cadence defaults + cap values — /architect.

Plan

Recommendation-locked this round under the user's standing pre-authorization (same basis as
the WP2/WP3 round): surface-class tokens tracker-vcs-event / temporal / agent-internal /
channel-feed; transport enum push / push-lifecycle / poll; provenance enum human /
agent / system; signal envelope = a JSON-fenced marker record on the queued item (the WP3
marker-record precedent), keys signal.class, signal.transport, signal.provenance,
signal.identity, signal.raw_link, signal.traceparent, plus optional signal.work_class
(the T3 risk class the adapter STAMPS from the security-surface classification rules — see
Phase 1 (3bis); absent = unclassified = fail-closed human-gated) and signal.parent_item
(REQUIRED when signal.class is agent-internal: the canonical URL of the emitting
session's admitted source item — the machine-verifiable provenance the stamped class is
checked against; a stamped class is never trusted on its own) and signal.source_surface
(REQUIRED when signal.class is temporal: the originating scheduling surface's id as
recorded in the triggers binding — the discriminator raw-link form validation branches
on, since temporal alone spans both CI-cron and local-scheduler surfaces).

Authoring constraint (cross-package link hygiene): this package merges before WP5, so
normative text references the admission seam and classification rules by contract CONCEPT
only — never a file link to a not-yet-existing guardrail doc; WP5 adds the file cross-links
back to the then-existing trigger-dispatch.md.

Prerequisite: WP2 telemetry PR merged (reference/telemetry.md exists — this contract imports
its trace-tree obligation) and WP3 PR merged (marker-record precedent + return-accounting
capture rides the same close boundary the dispatch drain crosses). Implementation ordering
with WP5: this package's PR merges FIRST; admission enforcement here fail-closes when no
admission binding exists (absent policy → every dispatch human-gated), and WP5 then ships the
policy surface that relaxes it — no dead link in either direction, no dispatch window ever
runs ungoverned.

Phase 1: Trigger-dispatch contract doc [DONE]

File Action What changes
plugins/autonomy/reference/trigger-dispatch.md Create The T1 contract as tool-agnostic normative text. (1) Four signal-surface classes with tokens tracker-vcs-event (tracker/VCS-host events: label, assignment, @-mention, PR event), temporal (schedule + poll-fallback detectors), agent-internal (a session emits follow-up work), channel-feed (chat mention + continuous monitor) — ALL contract-active; per-org availability is a binding outcome, entitlement gaps route to the advisory path (WP1 D6), never a contract deferral; carried research gaps stated in SURFACE-CLASS vocabulary in this doc (channel-monitor ambient initiate-vs-notify UNVERIFIED; the relied-on channel-agent vendor surfaces are alpha/beta moving targets; one major chat platform has no first-party trigger, UNVERIFIED-absence) — vendor names for these gaps live in SKILL.md only, per the contract validator's vendor deny-list. (2) Two recorded signal attributes: initiator provenance (human/agent/system — audit + guardrail input, recorded never trusted as isolation, per T2) and transport (push/push-lifecycle/poll; push preferred where offered, poll universal fallback via temporal; push-lifecycle carries subscription expiry + renewal + validation handshake obligations, and EXPIRY semantics are normative: every push-lifecycle wiring is backed by a temporal poll-detector backstop for the same surface, or the subscription-health lapse fail-closes — files a human-gated alert item — so a lapsed subscription can never silently drop signals). (3) Six class-generic adapter obligations imported from the Brief D4 verbatim: normalize+enqueue only; idempotent dedup keyed on signal.identity (surface-native unique event id where the surface issues one; the FALLBACK identity is never a bare content hash — it composes source scope (surface class + origin locator) + an event-instance discriminator (delivery id or event timestamp) + the content hash, so two legitimate repeated signals with identical payloads stay distinct instances; for state-based poll detectors that re-observe a continuing condition and have no instance identity, dedup retention is BOUNDED to items still open — the same finding may re-enqueue once its prior item closes (a re-detected regression is a new signal). Dedup enforcement is NOT a bare read-then-write: concurrent at-least-once deliveries can both pass a search before either item exists, so the adapter uses an atomic identity-keyed create/upsert or queue-side uniqueness guarantee where the tracker offers one; where none exists, search-before-create is backed by CREATE-THEN-RECONCILE — after creating, re-search by signal.identity and, on finding an older item with the same identity, close the newer one as an audited duplicate (oldest wins, deterministic) — plus a drain-side guard scoped to LIVE duplicates only: the drain never claims an item whose signal.identity matches another currently-open item (claimed or awaiting reconciliation) — completed items are excluded from the guard, consistent with the bounded-retention rule, so a fixed-then-regressed condition re-enqueues AND executes, while an unreconciled concurrent twin still cannot run the same work twice); provenance capture + durable signal.raw_link; trace-context propagation (signal.traceparent, one causal tree per the telemetry contract); admission enforcement at the seam (policy content owned by the guardrail matrix; the adapter enforces, never defines; unadmitted → human-gated item or audited rejection; ABSENT admission binding → fail-closed: enqueue human-gated, never drop silently); closed-loop acknowledgment (bidirectional surfaces echo the item reference back; reply-less surfaces satisfy via signal.raw_link alone). (3bis) Work-class classification obligation: admission and the whole guardrail matrix key on the T3 risk class, so a queued item needs one — the adapter STAMPS signal.work_class from the classification rules on the org's SECURITY governance surface (owned by the guardrail contract; the adapter stamps, never defines): tracker-vcs-event resolves via the security-bound label→class rules; temporal signals carry the class their bound routine/detector definition derives; agent-internal items must PROVE protected provenance — the envelope SERIALIZES the emitting session's own admitted source item as signal.parent_item (required for this class), and the admission seam VERIFIES the session-to-parent association against protected dispatch data (the queue's own lease/claim record of which item the emitting session was dispatched on — an agent-supplied URL alone proves nothing: any session could cite an unrelated C1 item to launder higher-risk follow-up work); an association the seam cannot verify is treated as NO provenance (unclassified, fail-closed human-gated). Admission then RESOLVES the verified parent's class from its own protected classification rather than trusting the stamped value: the effective class is the HIGHER of that inherited class and the class the security-surface rules derive for the target — a session never self-assigns a lower class for its follow-up work (self-classification would bypass admission: stamp C1 on C4 work and the table admits it); an agent-internal signal that cannot prove that provenance stays UNCLASSIFIED (recorded with agent provenance, admission-checked like any input); channel-feed and any signal the rules cannot resolve stay UNCLASSIFIED → fail-closed human-gated. No repo-local (agent-writable) surface may supply the class used for admission. (4) Signal envelope serialization: JSON-fenced marker record on the queued item, the six required signal.* keys plus optional signal.work_class, schema_version from "1.0" — additive evolution, same governance as every contract schema. (5) Dispatch spec: push kick where the platform offers it + a standing scheduled drain as universal fallback and catch-up net for ENQUEUED items (default cadence hourly, org-bindable; the drain never re-scans a source surface — missed enqueues are the poll-detector backstop's job per (2)); one-entrypoint invariant — every kick funnels into the work-item queue capability's autonomous drain mode via the invocation-adapter seam; the race-safe lease makes concurrent kicks harmless; concurrency and per-run caps are guardrail-policy knobs — named descriptively here, serialized tokens (autonomous_concurrency, items_per_run) owned by the admission policy. (6) Executor surface classes imported from T4 unchanged (self-operated CLI/SDK incl. SDK-embedded pull daemons vs vendor-hosted with human-merge-gate cap); the executor-class determination that gates merge policy is SECURITY-surface data (the guardrail binding's executor_class), never a repo-local value; other executor hosting config stays deployment-owned (T7 stance) — contract fixes isolation floor L2+, credential scoping, queue contract only. Constraints restated normatively: no queue bypass, no second dispatch mechanism, no invented event bus, no new cost by default, vendor names only as marked examples.

Sanity Check:

  • grep -c 'tracker-vcs-event' plugins/autonomy/reference/trigger-dispatch.md ≥ 1 and same for temporal, agent-internal, channel-feed
  • grep -c 'push-lifecycle' plugins/autonomy/reference/trigger-dispatch.md ≥ 1 and expiry backstop stated (grep -c 'poll-detector' …/trigger-dispatch.md ≥ 1)
  • grep -c 'signal.identity' plugins/autonomy/reference/trigger-dispatch.md ≥ 1, grep -c 'signal.work_class' …/trigger-dispatch.md ≥ 1, and grep -c 'signal.parent_item' …/trigger-dispatch.md ≥ 1
  • grep -ci 'fail-closed' plugins/autonomy/reference/trigger-dispatch.md ≥ 2 (absent-admission AND unclassified behavior stated)
  • grep -ci 'UNVERIFIED' plugins/autonomy/reference/trigger-dispatch.md ≥ 2 (research gaps carried in surface-class vocabulary, not laundered)
  • No forward file-link to a guardrail doc: grep -c 'guardrails' plugins/autonomy/reference/trigger-dispatch.md counts concept references only — grep -cE '\]\((\./)?(guardrails|.*guardrails/.*)\.md' …/trigger-dispatch.md = 0
  • Vendor+fleet deny-list grep empty over the doc (node scripts/validate-plugin-contracts.mjs exit 0)
  • lychee lane passes

Phase 2: Guided-setup trigger/dispatch slice [DONE]

Extends the setup skill (discovery-first per D7/WP1 D7). First work item — fresh-docs
mandate (repo CLAUDE.md): re-fetch the official docs for any vendor surface the slice names
(CI events, schedule syntax, headless invocation) before editing SKILL.md.

File Action What changes
plugins/autonomy/skills/setup/SKILL.md Modify Add the trigger/dispatch slice to discovery + apply: interview which signal surfaces exist per class, transport capability per surface, entitlements; wire the DIY floor as reviewable changes (platform event kick + scheduled drain; chat-platform bot or plain inbound webhook for channel-feed where wanted); advise plan-gated native integrations with cost surfaced; bind the drain (cadence default hourly, org override recorded); EVERY kick/drain wiring records its named execution surface — the same surface id the guardrail security binding's per-surface isolation entries key on — but the RECORDED id is repo-local (agent-writable) convenience only: the admission/executor seam derives the ACTUAL execution-surface identity from trusted dispatch/runner context (platform-attested runtime metadata, matched against per-surface identifying markers the SECURITY binding's isolation entries declare) and verifies it against the recorded id, consulting the ACTUAL surface's isolation verdict — a mismatch, an unattestable actual surface, or a surface with no L2 binding all fail-close to human-gated, so rewriting the recorded id to a bound sibling cannot launder execution onto an unbound runner; record the trigger binding as an ADDITIVE triggers section of the WP1 schema-versioned binding (absent-section tolerance; no major bump). Admission enforcement wiring points at the guardrail admission seam and states the fail-closed absent-binding behavior. All vendor env vars/event names live here, never in reference/.
plugins/autonomy/skills/setup/templates/trigger-adapters.md Create Adapter snippet templates per surface class, surface-class-parameterized, fleet names banned: tracker-vcs-event (label/assignment event → enqueue step shape), temporal (scheduled drain + poll-detector shape), agent-internal (session files follow-up via the queue seam), channel-feed (webhook receiver → enqueue shape). Each template carries the six adapter obligations inline (dedup key derivation, raw-link capture, traceparent injection, ack echo).
plugins/autonomy/skills/setup/templates/ack-reply.md Create Acknowledgment template wording for bidirectional surfaces: Queued as <item-url> (autonomy: <class> signal) — one line, item URL first (the join key), class token for audit; reply-less surfaces documented as satisfied by signal.raw_link.
plugins/autonomy/skills/setup/scripts/check-signal-envelope.mjs Create Enforcement surface for the envelope: validates a queued item's JSON-fenced signal.* marker record — six required keys present, enum values legal (incl. signal.work_class ∈ C1–C5 when present), signal.parent_item REQUIRED (well-formed absolute https URL) whenever signal.class is agent-internal — its absence fails validation, since an unverifiable self-stamped class would bypass admission; the class-resolution itself (inherited-vs-derived max) is the admission seam's job at dispatch time, the checker enforces the serialized provenance it needs — the ITEM URL normalized per the telemetry contract's strip rule, signal.raw_link validated as a DURABLE absolute reference, form branched DETERMINISTICALLY by serialized origin: the checker takes the schema-versioned binding as an input and resolves signal.source_surface against BOTH its additive sections that record scheduling surfaces — triggers (kick/drain wiring) and routines (each routine's recorded scheduler/surface choice), since routine signal producers record their surface in the latter, not as trigger entries — a temporal signal whose source surface the binding records as local-scheduler class may carry a durable local/artifact URI (an absolute file: URI or org artifact-store locator — such surfaces have no web origin); every other signal (including temporal from CI-cron or any remote surface) requires a well-formed absolute https URL (query/fragment PRESERVED — comment anchors and permalinks need them; the strip rule is the join key's, not the raw link's); a temporal signal missing signal.source_surface, or naming a surface the binding does not record, fails validation; relative or ephemeral references fail either branch, schema_version known. Mirrors check-emission-conformance.mjs (same exit contract 0/1/2).
plugins/autonomy/skills/setup/evals/evals.json Modify Add trigger-slice cases: surface discovery interview path, DIY-floor wiring, plan-gated advisory refusal-to-default, absent-admission fail-closed statement, non-interactive argument-supplied run.
plugins/autonomy/README.md Modify Shipped-capability list grows by the trigger-dispatch contract + setup slice.
plugins/autonomy/.claude-plugin/plugin.json Modify Description extends; minor version bump.

Sanity Check:

  • /skill-quality:check + validate-evals pass (skills_root = plugins/autonomy/skills)
  • claude plugin validate --strict exit 0
  • grep -c 'signal.identity' plugins/autonomy/skills/setup/templates/trigger-adapters.md ≥ 1
  • node plugins/autonomy/skills/setup/scripts/check-signal-envelope.mjs (no args) exits 2 with usage
  • Fleet-name sweep (validate-plugin-contracts.mjs) exit 0

Phase 3: Conforming-path demonstration [DONE]

Acceptance probe on a scratch consumer repo (NOT this repo, WP2 Phase 3 precedent — the ban
there is on FLEET bindings/plugins, not on composing a sibling CAPABILITY: the scratch repo
INSTALLS the work-items capability plugin, whose queue + race-safe lease IS the seam under
demonstration; stubbing the lease would weaken the one-entrypoint proof to nothing): a
tracker-vcs-event stub (label applied) runs the adapter shape → item lands in the governed
queue carrying the full signal envelope + ack echo on the source surface; the scheduled-drain
entrypoint claims it via the seam lease (autonomous drain mode) — demonstrating kick + drain
funneling into ONE entrypoint; TRACEPARENT from the trigger hop is observed on the enqueue
record (telemetry-contract import). check-signal-envelope.mjs passes against the created
item. Absent-admission fail-closed is demonstrated live: with no admission binding present,
the drained item is refused autonomous execution and remains human-gated, with the refusal
audited on the item (this also exercises the unclassified → human-gated path: no security
binding means no classification rules, so the item carries no signal.work_class).

Sanity Check:

  • The scratch item body contains a valid signal.* marker record (check-signal-envelope.mjs exit 0)
  • The ack comment on the source surface contains the item URL
  • Drain transcript shows the seam lease claim and the fail-closed human-gated refusal (no admission binding)
  • No second dispatch path exercised anywhere in the demo; no paid service touched
  • Demo transcript attached to the PR body

Phase 4: Gates [DONE]

Full in-repo gate run (WP2 Phase 4 roster): scripts/validate-plugins.sh,
scripts/run-plugin-tests.sh, node scripts/validate-plugin-contracts.mjs, markdown/typos/
lychee lanes, claude plugin validate --strict, catalog regen check. Near-duplicate audit
statement: the dispatch entrypoint composes the work-item queue capability's existing
autonomous drain — no second claim/dispatch mechanism was created.

Sanity Check:

  • All gate scripts exit 0
  • node scripts/generate-catalog.mjs reports in-sync
  • Near-duplicate audit statement present in the PR body

Blast radius

MEDIUM — one plugin's files, but the contract constrains WP5/WP6/WP7, every future adopter,
and the admission seam WP5 binds to; new convention + cross-package coupling triggers match.
Fully git-revertible; automated gates cover shared surfaces.

Stress-test summary

Fresh-context plan review (WP4+WP5 batch): 14 findings, verdict FIX-THEN-SHIP, all folded.
WP4's share — F1a (HIGH, cross-package): the envelope carried no T3 work class, leaving the
autonomous happy-path non-functional (safe but dead) → signal.work_class + the (3bis)
classification obligation, class source pinned to the security surface; F2 (HIGH): verbatim
vendor names in reference/ collide with the contract validator's vendor deny-list →
surface-class vocabulary in reference/, vendor specifics in SKILL.md; F3 (MED-HIGH): the
telemetry strip rule misapplied to signal.raw_link would corrupt durable event links →
strip rule confined to the item URL; F4 (MED): push-lifecycle expiry had no fail-closed
semantics → poll-detector backstop / fail-closed lapse, and the drain's no-rescan limit
stated; F5 (MED): the Phase 3 seam demo now explicitly installs the work-items capability
(sibling capability ≠ banned fleet binding); F6 (MED, latent): forward file-links to
not-yet-existing guardrail docs → concept-only references, WP5 back-links; F7 (LOW): cap
knob token ownership pinned to the admission policy's serialized names. F8 (LOW) accepted:
the no-second-dispatch and Boris checks stay prose PR-body audits, consistent with the
WP2/WP3 round.

Execution shape

Fully sequential 1 → 2 → 3 → 4 — Phase 2 cites Phase 1's contract; Phase 3 exercises Phase 2's
templates and check script; Phase 4 gates the tree. Cross-package: this PR merges before the
WP5 implementation PR (WP5's admission policy binds the seam this contract defines; the
fail-closed absent-binding clause makes the interim safe).

Phase Surface Basis
1 main-session normative contract authoring, tightly coupled to T1/T4/T7 imports
2 main-session setup-skill + template judgment, vendor-doc re-fetch gate
3 main-session scratch-repo runtime probe with divergence judgment
4 main-session gate runs

Open questions

  • Fleet adapter materializations (reusable workflows, labels, drain-routine stand-up) —
    /work-items backlog post-merge (Brief out-of-scope, trigger recorded).

Decisions made (gate-passed)

Decision What it changes in the plan Basis (evidence)
Surface-class tokens tracker-vcs-event/temporal/agent-internal/channel-feed Phase 1 contract vocabulary Brief D2 class descriptions; kebab-case matches every shipped contract token set
Transport tokens push/push-lifecycle/poll Phase 1 enum Brief D3 three-value enum; push-lifecycle compresses PUSH-WITH-LIFECYCLE without semantic loss
Signal envelope = JSON-fenced marker record, signal.* keys, schema_version "1.0" Phase 1 serialization + Phase 2 check script WP3 marker-record precedent (tracker-resident, adapter-written, queryable)
Drain cadence default hourly, org-bindable Phase 1 dispatch spec + Phase 2 slice Universal catch-up net with bounded token cost; value is a binding knob, not contract
Cap VALUES deferred to the WP5 admission policy; WP4 names the knobs Phase 1 dispatch spec Brief D5 assigns caps to guardrail policy; avoids double ownership
WP4 merges before WP5; absent admission binding fail-closes to human-gated Plan preamble + Phase 3 demo Brief D4 obligation 5 (enforce, never define) + no-ungoverned-window requirement
signal.work_class optional envelope key + security-surface classification obligation; unclassified → human-gated Phase 1 (3bis) + Phase 2 check Stress-test F1a: the matrix and admission key on C1–C5, which no trigger path assigned; agent-writable class sources are a bypass channel
Executor-class merge gating reads the security binding's executor_class, never repo-local data Phase 1 (6) Stress-test F1b: hosted human-merge-gate cap must not be flippable from inside the blast radius

[FALLBACK — confirm or override] check-signal-envelope.mjs as a NEW deliverable (Phase 2) —
invented beyond the Brief on the WP2 precedent (a pinned contract with no enforcement surface
is undetectable drift). Flag if unwanted.

Handoff to implementation

User-approval gates

  • The envelope-check script above is [FALLBACK] — surface before authoring if contested.
  • If the platform's event kick cannot reach the drain entrypoint without a second dispatch
    path, STOP and re-surface — the one-entrypoint invariant is non-negotiable.
  • Any scope expansion beyond the four phases re-enters /architect review.

Execution shape ([EXEC-SHAPE] tagged)

Sequential 1→4, all main-session (table above). PLAN.md phase tags advance in the same commit
as each phase; scratch-repo demo per Phase 3; divergence escalation applies to every phase.

Mechanical work

Commit per phase on the implementation branch (suggest feat/autonomy-trigger-dispatch);
gates re-run in full at Phase 4; PR body carries the demo transcript + near-duplicate audit
statement + this PLAN in a <details> block at close-out.

🤖 Generated with Claude Code

kyle-sexton and others added 4 commits July 18, 2026 23:30
… obligations, governed dispatch

WP4 Phase 1 per its PLAN: the T1 contract as tool-agnostic normative
text. Four contract-active signal-surface classes; provenance and
transport attributes with normative push-lifecycle expiry backstop; six
adapter obligations including atomic-or-reconciled dedup with a
live-duplicate drain guard, admission enforcement (absent binding
fail-closes human-gated), and closed-loop acknowledgment; work-class
classification stamped from the security surface with lease-verified
agent-internal parent provenance; the signal envelope (marker-record
serialization, schema_version 1.0); kick + drain dispatch through the
one work-item queue entrypoint with runtime-attested execution-surface
verification; executor surface classes with the vendor-hosted
human-gate cap on security-surface data. Research gaps carried in
surface-class vocabulary; zero vendor names in normative text.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…es, ack template, signal-envelope check

Phase 2 of the WP4 trigger-dispatch package: the setup skill gains the
discovery-first trigger/dispatch slice (DIY-floor kick + hourly drain through
the one queue entrypoint, plan-gated surfaces advisory with cost, execution-
surface recording with the attestation caveat, fail-closed absent-admission
wiring, additive `triggers` binding section with the shared `surfaces` map
shape), per-surface-class adapter templates carrying the six obligations
inline, the one-line acknowledgment template, and check-signal-envelope.mjs
(exit contract 0/1/2) validating the marker record against the binding with
deterministic raw-link origin branching. The contract doc pins the envelope
marker token `<!-- autonomy:signal:v1 -->` (return-accounting precedent:
serialization is contract-owned). Evals grow five trigger-slice cases; plugin
0.4.0.

Vendor facts re-verified this session per the fresh-docs mandate:
https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows
and https://code.claude.com/docs/en/headless

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…h (envelope, ack, seam lease, fail-closed refusal)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@claude

claude Bot commented Jul 19, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 4m 8s —— View job


PR Review — feat(autonomy): trigger-dispatch package (WP4)

  • Gather context, read all changed files, review diff

Overall: The contract vocabulary, obligation set, dispatch invariants, and enforcement script are all internally consistent. The fail-closed semantics (absent admission binding → human-gated; unclassified → human-gated) are correctly threaded through the contract, skill, and templates. Three correctness issues in check-signal-envelope.mjs are worth addressing before merge.


Findings

1. filesUnder follows symlinks — potential infinite recursion · check-signal-envelope.mjs:185

statSync(target) follows symlinks. A circular symlink inside a target directory (or a symlink pointing to an ancestor) causes infinite recursion until stack overflow. In CI the tool runs against agent-written item bodies, so the attack surface is low, but the failure mode is confusing.

// current
if (statSync(target).isDirectory()) {

Use lstatSync and skip symlinks, or use readdirSync with { withFileTypes: true }:

function filesUnder(target) {
  const stat = lstatSync(target);
  if (stat.isSymbolicLink()) return [];   // skip symlinks entirely
  if (stat.isDirectory()) {
    return readdirSync(target)
      .map((entry) => join(target, entry))
      .flatMap(filesUnder);
  }
  return [target];
}

Fix this →


2. --binding with no following value produces a misleading error · check-signal-envelope.mjs:197

When --binding is the last CLI argument, args[i + 1] is undefined. The check if (bindingPath !== null) passes (undefined !== null), so readFileSync(undefined, "utf8") throws and reports cannot read binding undefined: .... The user sees undefined rather than a clear message.

// current
if (args[i] === "--binding") {
  bindingPath = args[i + 1];
  i += 1;
}

Guard the value before assigning:

if (args[i] === "--binding") {
  if (i + 1 >= args.length) {
    console.error("usage: check-signal-envelope.mjs <item-body-file-or-dir> [...more] [--binding <binding.json>]");
    process.exit(2);
  }
  bindingPath = args[i + 1];
  i += 1;
}

Fix this →


3. Zero envelopes checked exits 0 — silently passes on empty or wrong-path targets · check-signal-envelope.mjs:219

If every target resolves to an empty directory (wrong path, empty fixture dir), envelopesChecked stays 0, findings stays empty, and the script exits 0: Signal-envelope conformance OK: 0 envelope(s) checked. The contract says adapters MUST write the envelope, so zero envelopes checked should be a configuration error rather than a success.

// after the loop, before the findings check
if (envelopesChecked === 0 && findings.length === 0) {
  console.error("no envelope marker records found in the supplied targets");
  process.exit(1);
}

This matches how check-emission-conformance.mjs treats "no spans found" — a misconfigured invocation should fail loudly. (If there are findings — e.g., files-with-no-marker — the existing exit 1 path already fires, so only the genuinely-zero case needs this guard.)

Fix this →


Observations (no action required)

signal.parent_item check is correctly structural-only. The checker validates that it is a normalized canonical https URL; it does not attempt to verify the session-to-parent association against lease records. The contract is clear that this belongs to the admission seam, and the check comment states this boundary explicitly. Line 149–157

collectSurfaces is forward-compatible with the routines section. Iterating over all top-level binding sections — not just triggers — means routine-sourced temporal signals resolve signal.source_surface correctly once WP6 lands without a script change. Line 103–115

isNormalizedCanonicalUrl has two redundant pairs of checks. url.search === "" and !value.includes("?") are equivalent after WHATWG parsing; same for url.hash === "" and !value.includes("#"). Not wrong — the explicit string checks make the intent readable — but they add no safety. Line 68–81

Contract vocabulary is clean. Vendor and fleet names are absent from reference/trigger-dispatch.md; event specifics appear only as "marked examples" in the template. The carried UNVERIFIED research gaps for channel-feed surfaces are stated in surface-class vocabulary, not vendor vocabulary, which passes the deny-list and is the right level of abstraction for a cross-org contract.

Fail-closed chain is coherent end-to-end. Absent admission binding → human-gated (never dropped) is stated in the contract (Adapter obligations §5), enforced in the skill (step 6), carried into the template (step 5 of tracker-vcs-event), and live-demonstrated in the PR's Phase 3 demo. The SKILL.md execution-surface attestation caveat (recorded id is repo-local convenience only; the seam derives the actual identity) is correctly stated next to every execution_surface recording instruction, preventing a reader from mistaking the recorded id for the enforcement.

Evals 14–18 cover the trigger slice well. Discovery-first, DIY-floor single-entrypoint, plan-gated advisory refusal, absent-admission fail-closed, and non-interactive apply are all covered. The expected_output descriptions are behaviorally specific without being brittle to output wording changes.


3 issues (1 medium, 2 low). All are in check-signal-envelope.mjs; the contract doc, skill, templates, and evals look correct.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: eac5145813

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread plugins/autonomy/skills/setup/scripts/check-signal-envelope.mjs Outdated
Comment thread plugins/autonomy/skills/setup/scripts/check-signal-envelope.mjs Outdated
Comment thread plugins/autonomy/skills/setup/scripts/check-signal-envelope.mjs
…ow-list, traceparent v00 only, empty-input failure

Local-scheduler raw links now conform only on file:, https:, or a scheme the
binding surface entry declares via artifact_schemes (data:/javascript: and
other ephemeral schemes never pass); traceparent requires the supported
version 00 (ff is spec-forbidden); a run that finds zero envelopes fails
instead of reporting vacuous success.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@claude

claude Bot commented Jul 19, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 2m 13s —— View job


PR Review — feat(autonomy): trigger-dispatch package (WP4) — Follow-up

  • Gather context (PR synchronize event — hardening commit 9bdf3e7)
  • Read updated check-signal-envelope.mjs to verify prior findings addressed
  • Assess remaining issues
  • Post final review

The hardening commit (9bdf3e7) addressed four of the six findings raised across the two reviews. Here's the full accounting.

Resolved ✓

Scheme allow-list for isDurableLocalUri (Codex P2)
isDurableLocalUri was url.protocol !== "http:", accepting data:, javascript:, and every other scheme. Now correctly restricts to file:, https:, and schemes the binding surface entry explicitly declares in artifact_schemes. Undeclared schemes fail.

Traceparent ff version rejected; version 00 required (Codex P2)
Regex was ^[0-9a-f]{2}-…, accepting any two hex digits including ff (spec-forbidden) and all future versions. Now anchored to ^00-… — version 00 only, ff impossible.

Zero-envelopes exits 1 rather than 0 (Claude #3, Codex P2)
When envelopesChecked === 0, a finding is pushed and the script exits 1 via the existing findings path. Prior behavior (vacuous OK: 0 envelope(s) checked., exit 0) is gone. The implementation pushes unconditionally (not gated on findings.length === 0), which is correct — "nothing was verified" is always a failure regardless of other file-level findings.


Still open

1. filesUnder follows symlinks — potential infinite recursion · check-signal-envelope.mjs:192 (medium)

Unchanged from the original addition commit. statSync follows symlinks; a circular symlink in a target directory recurses until stack overflow with no useful error message. The fix is one line — swap statSync for lstatSync and skip symlinks:

import { lstatSync, readdirSync, readFileSync, statSync } from "node:fs";
// ...
function filesUnder(target) {
  const st = lstatSync(target);
  if (st.isSymbolicLink()) return [];   // skip — can't be a conformant item body
  if (st.isDirectory()) {
    return readdirSync(target)
      .map((entry) => join(target, entry))
      .flatMap(filesUnder);
  }
  return [target];
}

Fix this →

2. --binding with no following arg produces a misleading error · check-signal-envelope.mjs:205 (low)

Unchanged. When --binding is the last CLI argument, args[i + 1] is undefined. Since undefined !== null, the if (bindingPath !== null) branch fires, readFileSync(undefined) throws, and the error message reads cannot read binding undefined: The "path" argument must be of type string…. The script exits 2 (correct exit code), but the error is confusing.

if (args[i] === "--binding") {
  if (i + 1 >= args.length) {
    console.error("usage: check-signal-envelope.mjs <item-body-file-or-dir> [...more] [--binding <binding.json>]");
    process.exit(2);
  }
  bindingPath = args[i + 1];
  i += 1;
}

Fix this →


Summary

The security-relevant findings (scheme allow-list, traceparent version hardening, vacuous-success on empty input) are all correctly addressed. Two robustness items from the first-pass review remain: the symlink guard in filesUnder and the --binding bounds check. Both are small, isolated fixes with no design implications. The contract, skill, templates, and evals are unchanged and remain correct.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9bdf3e7da6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread plugins/autonomy/skills/setup/scripts/check-signal-envelope.mjs Outdated
Comment thread plugins/autonomy/skills/setup/scripts/check-signal-envelope.mjs Outdated
…require temporal source surface, exec bit

A binding-declared artifact scheme cannot launder data:/javascript: (deny set
applies even when declared); a temporal signal's source_surface must resolve
to a class temporal surface with a known scheduler_class before the local-URI
branch is selectable; script gains the executable bit the hygiene exec-bit
lane requires.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@claude

claude Bot commented Jul 19, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 1m 27s —— View job


PR Review — feat(autonomy): trigger-dispatch package (WP4) — Third Pass

  • Gather context (PR synchronize event — hardening commit 0624fe8)
  • Read updated check-signal-envelope.mjs to verify prior findings addressed
  • Assess remaining issues and any new issues in 0624fe8
  • Post final review

The 0624fe8 hardening commit correctly addresses both Codex P2 findings from the second review round. Two robustness items from the first and second passes are still open; they are not new — the state is unchanged from 9bdf3e7.

Resolved in 0624fe8

Non-durable declared artifact schemes (Codex round 2)
NON_DURABLE_SCHEMES deny set (data, javascript, blob, about, http, mailto, tel, vbscript) is now applied unconditionally inside isDurableLocalUri — a binding's artifact_schemes declaration cannot launder any scheme in that set. The implementation correctly orders the check: !NON_DURABLE_SCHEMES.has(scheme) && url.protocol === ${scheme}:``. Line 73–75

Temporal source surface class validation (Codex round 2)
The resolved surface entry is now required to carry class: "temporal" with a known scheduler_class (ci-cron | local-scheduler) before localScheduler is set and the local-URI branch becomes reachable. A non-temporal or unknown-scheduler entry pushes a finding and falls through to the https requirement. The entry.scheduler_class !== undefined guard means a surface entry that omits scheduler_class entirely is accepted (treated as non-local, https required) — that is a reasonable default and consistent with the contract's fail-strict direction. Lines 185–197

Exec bit: script is now 100755. ✓


Still open

1. filesUnder follows symlinks — potential stack overflow · check-signal-envelope.mjs:210 (medium)

statSync(target).isDirectory() follows symlinks; a circular symlink in a target directory recurses to stack overflow with no useful error. Unchanged since the initial addition commit. One-line fix — swap statSync for lstatSync and skip symlinks at the top of the function:

import { lstatSync, readdirSync, readFileSync } from "node:fs";
// ...
function filesUnder(target) {
  const st = lstatSync(target);
  if (st.isSymbolicLink()) return [];
  if (st.isDirectory()) {
    return readdirSync(target)
      .map((entry) => join(target, entry))
      .flatMap(filesUnder);
  }
  return [target];
}

(statSync import can be removed — it has no other callers after this change.)

Fix this →


2. --binding with no following arg produces a misleading error · check-signal-envelope.mjs:222 (low)

When --binding is the last CLI argument, args[i + 1] is undefined. undefined !== null so the if (bindingPath !== null) branch fires, readFileSync(undefined) throws, and the user sees cannot read binding undefined: The "path" argument must be of type string… — confusing. Exit code is 2 (correct), but the message is not. Unchanged from the original commit.

if (args[i] === "--binding") {
  if (i + 1 >= args.length) {
    console.error("usage: check-signal-envelope.mjs <item-body-file-or-dir> [...more] [--binding <binding.json>]");
    process.exit(2);
  }
  bindingPath = args[i + 1];
  i += 1;
}

Fix this →


Summary

All security-relevant findings across three review passes are now addressed: scheme allow-list, traceparent version 00 only, vacuous-success on empty input, non-durable declared schemes, and temporal surface class gating. The contract doc, skill, templates, and evals remain correct and are unchanged in 0624fe8. Two robustness items in check-signal-envelope.mjs remain open — both are small, isolated, and carry no design implications.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0624fe8713

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread plugins/autonomy/skills/setup/scripts/check-signal-envelope.mjs Outdated
Comment thread plugins/autonomy/skills/setup/scripts/check-signal-envelope.mjs Outdated
…t supported schema-version set

Trace flags restricted to 00/01 (version-00 defines only the sampled bit);
schema_version validated against the explicit supported set (1.0) so an
unknown 1.x cannot certify semantics the checker cannot see.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@claude

claude Bot commented Jul 19, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 2m 1s —— View job


PR Review — feat(autonomy): trigger-dispatch package (WP4) — Fourth Pass

  • Gather context (PR synchronize event — hardening commit 982d948)
  • Read updated check-signal-envelope.mjs
  • Verify 982d948 fixes
  • Compute fixes for two remaining robustness items (push access denied — see below)

Resolved in 982d948

Trace-flag byte restricted to version-00 mask (Codex round 3)
The TRACEPARENT regex now ends with -0[01]$ — only 00 (not-sampled) and 01 (sampled) are accepted, matching the version-00 spec constraint that the sampled bit is the only defined flag. Reserved bits cannot be set. Line 43

Explicit supported schema version set (Codex round 3)
SUPPORTED_SCHEMA_VERSIONS = new Set(["1.0"]) replaces the implicit loose match — any unknown 1.x minor pushes a finding naming the supported set. Future minors are added alongside their validation rules. Line 26


Still open — exact fixes (flagged across 3 prior passes)

I computed both fixes locally; push to the remote was denied (bot lacks write access). Apply them directly:

1. filesUnder follows symlinks — potential stack overflow · check-signal-envelope.mjs:215 (medium)

Replace the statSync import with lstatSync and add a symlink guard at the top of the function:

-import { readdirSync, readFileSync, statSync } from "node:fs";
+import { lstatSync, readdirSync, readFileSync } from "node:fs";
 function filesUnder(target) {
-  if (statSync(target).isDirectory()) {
+  const st = lstatSync(target);
+  if (st.isSymbolicLink()) return [];
+  if (st.isDirectory()) {
     return readdirSync(target)
       .map((entry) => join(target, entry))
       .flatMap(filesUnder);
   }
   return [target];
 }

Fix this →


2. --binding with no following arg produces a misleading error · check-signal-envelope.mjs:228 (low)

   if (args[i] === "--binding") {
+    if (i + 1 >= args.length) {
+      console.error("usage: check-signal-envelope.mjs <item-body-file-or-dir> [...more] [--binding <binding.json>]");
+      process.exit(2);
+    }
     bindingPath = args[i + 1];
     i += 1;
   } else {

Fix this →


Summary

All security-relevant findings across four review passes are now addressed: scheme allow-list, traceparent version 00 only, traceparent flag mask 0[01], vacuous-success on empty input, non-durable declared schemes, temporal surface class gating, and explicit supported schema version set. The contract doc, skill, templates, and evals are correct and unchanged. Two small robustness items remain in check-signal-envelope.mjs — exact diffs above.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 982d9480af

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread plugins/autonomy/skills/setup/scripts/check-signal-envelope.mjs Outdated
Comment thread plugins/autonomy/skills/setup/scripts/check-signal-envelope.mjs Outdated
…l surfaces, exit 2 on unreadable targets

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@claude

claude Bot commented Jul 19, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 1m 47s —— View job


PR Review — feat(autonomy): trigger-dispatch package (WP4) — Fifth Pass

  • Gather context (PR synchronize event — hardening commit 7c23b63)
  • Read updated check-signal-envelope.mjs to verify prior findings addressed
  • Assess remaining issues and any new issues in 7c23b63
  • Post final review

Resolved in 7c23b63

scheduler_class required on temporal surfaces (Codex round 4)
The !== undefined guard is removed — a temporal surface entry without scheduler_class (or with an unknown value) now pushes a finding and fails. Previously, class: "temporal" with no scheduler_class silently fell through to the success branch. Line 196–202

Exit 2 on unreadable targets (Codex round 4)
The traversal loop is now wrapped in a try/catch. Any statSync/readFileSync error in filesUnder or the body read — missing file, permission denied — now prints cannot read target: … and exits 2, correctly classified as an environment error rather than a conformance finding. This also means a circular-symlink-induced RangeError: Maximum call stack size exceeded is caught and exits 2 (with a confusing message, but a correct exit code). Lines 251–263


Still open

1. filesUnder follows symlinks — exits 2 but with a confusing message · check-signal-envelope.mjs:216 (low — downgraded from medium)

The try/catch added in this commit catches the RangeError: Maximum call stack size exceeded a circular symlink eventually produces, so the process no longer crashes ungracefully. Severity is now low. The fix is still the right long-term shape, and the error message is still confusing: cannot read target: Maximum call stack size exceeded gives no indication that a symlink was involved. One-line fix — swap statSync for lstatSync and add a symlink guard:

-import { readdirSync, readFileSync, statSync } from "node:fs";
+import { lstatSync, readdirSync, readFileSync } from "node:fs";
 ...
 function filesUnder(target) {
-  if (statSync(target).isDirectory()) {
+  const st = lstatSync(target);
+  if (st.isSymbolicLink()) return [];
+  if (st.isDirectory()) {
     return readdirSync(target)
       .map((entry) => join(target, entry))
       .flatMap(filesUnder);
   }
   return [target];
 }

Fix this →


2. --binding with no following arg produces a confusing error · check-signal-envelope.mjs:228 (low)

When --binding is the last CLI argument, bindingPath is assigned undefined. The binding read's try/catch fires, printing cannot read binding undefined: The "path" argument must be of type string… and exiting 2. Exit code is correct; the message is not. Unchanged across all five passes.

   if (args[i] === "--binding") {
+    if (i + 1 >= args.length) {
+      console.error("usage: check-signal-envelope.mjs <item-body-file-or-dir> [...more] [--binding <binding.json>]");
+      process.exit(2);
+    }
     bindingPath = args[i + 1];
     i += 1;

Fix this →


Summary

All security-relevant and correctness findings across five review passes are now addressed: scheme allow-list, traceparent version 00 only, traceparent flag mask 0[01], vacuous-success on empty input, non-durable declared schemes, temporal surface class gating, explicit supported schema version set, scheduler_class required on temporal surfaces, and exit 2 on unreadable targets. The contract doc, skill, templates, and evals are correct and unchanged. Two low-severity robustness items remain in check-signal-envelope.mjs — exact diffs above — both of which now exit with the correct code (2) but with confusing messages.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7c23b63f78

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread plugins/autonomy/skills/setup/scripts/check-signal-envelope.mjs Outdated
Comment thread plugins/autonomy/skills/setup/scripts/check-signal-envelope.mjs Outdated
…ce, ambiguous duplicate surface ids rejected

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@claude

claude Bot commented Jul 19, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 1m 24s —— View job


PR Review — feat(autonomy): trigger-dispatch package (WP4) — Sixth Pass

  • Gather context (PR synchronize event — hardening commit 84b5954)
  • Read updated check-signal-envelope.mjs to verify prior findings addressed
  • Assess remaining issues and any new issues in 84b5954
  • Post final review

Resolved in 84b5954

Duplicate envelope markers rejected (Codex round 5)
Lines 107–110: A second `` occurrence in the body is now detected with a second indexOf starting after the first marker; it pushes a finding and returns `null`. The fence regex at line 111 is applied against `body.slice(markerIndex + MARKER.length)` with a `^` anchor (no `m` flag), so the fenced record must follow immediately — a stray marker earlier in the body cannot borrow a later valid fence.

Ambiguous duplicate surface IDs rejected (Codex round 5)
Lines 128–141: collectSurfaces now tracks a duplicates set. When the same surface id appears in two binding sections (e.g., triggers.surfaces and routines.surfaces), the id is added to duplicates rather than overwritten. Lines 198–201: resolution refuses the duplicate id with a fix the binding finding — no silent first-property winner.


Still open

1. filesUnder follows symlinks — exits 2 with confusing message · check-signal-envelope.mjs:230 (low)

statSync(target).isDirectory() follows symlinks; a circular symlink recurses until RangeError: Maximum call stack size exceeded, which is caught by the try/catch at line 265 and exits 2 with cannot read target: Maximum call stack size exceeded. Exit code is correct; the message reveals nothing about the symlink. One-line fix:

-import { readdirSync, readFileSync, statSync } from "node:fs";
+import { lstatSync, readdirSync, readFileSync } from "node:fs";
 ...
 function filesUnder(target) {
-  if (statSync(target).isDirectory()) {
+  const st = lstatSync(target);
+  if (st.isSymbolicLink()) return [];
+  if (st.isDirectory()) {
     return readdirSync(target)
       .map((entry) => join(target, entry))
       .flatMap(filesUnder);
   }
   return [target];
 }

Fix this →


2. --binding with no following arg produces a confusing error · check-signal-envelope.mjs:242 (low)

When --binding is the last CLI argument, args[i + 1] is undefined. undefined !== null so readFileSync(undefined) fires, caught by the binding try/catch, printing cannot read binding undefined: The "path" argument must be of type string… and exiting 2. Correct exit code, confusing message. Unchanged across all six passes.

   if (args[i] === "--binding") {
+    if (i + 1 >= args.length) {
+      console.error("usage: check-signal-envelope.mjs <item-body-file-or-dir> [...more] [--binding <binding.json>]");
+      process.exit(2);
+    }
     bindingPath = args[i + 1];
     i += 1;

Fix this →


Summary

All security-relevant and correctness findings across six review passes are now addressed: scheme allow-list, traceparent version 00 only, traceparent flag mask 0[01], vacuous-success on empty input, non-durable declared schemes, temporal surface class gating, explicit supported schema version set, scheduler_class required on temporal surfaces, exit 2 on unreadable targets, exactly-one-marker bound to its fence, and ambiguous duplicate surface ids rejected. The contract doc, skill, templates, and evals are correct and unchanged. Two low-severity robustness items remain open in check-signal-envelope.mjs — exact diffs above — both already exit with the correct code (2) but with messages that don't name the root cause.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 84b5954ec7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread plugins/autonomy/skills/setup/templates/trigger-adapters.md
…e (one-entrypoint event-fired dispatch)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@claude

claude Bot commented Jul 19, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 1m 25s —— View job


PR Review — feat(autonomy): trigger-dispatch package (WP4) — Seventh Pass

  • Gather context (PR synchronize event — commit 14d517b)
  • Read updated check-signal-envelope.mjs to verify prior findings
  • Read updated trigger-adapters.md to verify drain-kick fix
  • Assess remaining issues and post final review

Resolved in 14d517b

Post-enqueue drain kick (Codex P2 — trigger-adapters.md:55)
Step 7 is now present and correctly stated: after enqueue + ack, invoke the SAME queue-drain entrypoint (the work-item queue capability's autonomous drain mode via the invocation-adapter seam) so push-originated work gets an event-fired dispatch attempt instead of waiting for the hourly catch-up. One-entrypoint invariant preserved; concurrent kicks harmless via the seam lease; admission still governs. The temporal drain description retains its role as the scheduled catch-up net, which is the right split.


Still open

1. filesUnder follows symlinks — exits 2 with confusing message · check-signal-envelope.mjs:229 (low)

statSync(target).isDirectory() follows symlinks. The try/catch at line 272 catches the eventual RangeError: Maximum call stack size exceeded and exits 2, so the process doesn't crash. But cannot read target: Maximum call stack size exceeded tells the user nothing about a symlink. The lstatSync swap is one line:

-import { readdirSync, readFileSync, statSync } from "node:fs";
+import { lstatSync, readdirSync, readFileSync } from "node:fs";
 ...
 function filesUnder(target) {
-  if (statSync(target).isDirectory()) {
+  const st = lstatSync(target);
+  if (st.isSymbolicLink()) return [];
+  if (st.isDirectory()) {
     return readdirSync(target)
       .map((entry) => join(target, entry))
       .flatMap(filesUnder);
   }
   return [target];
 }

Fix this →


2. --binding with no following arg produces a confusing error · check-signal-envelope.mjs:242 (low)

When --binding is the last CLI argument, args[i + 1] is undefined. undefined !== null so the binding read fires, readFileSync(undefined) throws, and the user sees cannot read binding undefined: The "path" argument must be of type string…. Exit code 2 is correct; the message is not. Unchanged across all seven passes.

   if (args[i] === "--binding") {
+    if (i + 1 >= args.length) {
+      console.error("usage: check-signal-envelope.mjs <item-body-file-or-dir> [...more] [--binding <binding.json>]");
+      process.exit(2);
+    }
     bindingPath = args[i + 1];
     i += 1;

Fix this →


Summary

All security-relevant and correctness findings across seven review passes are now addressed: scheme allow-list, traceparent version 00 only, traceparent flag mask 0[01], vacuous-success on empty input, non-durable declared schemes, temporal surface class gating, explicit supported schema version set, scheduler_class required on temporal surfaces, exit 2 on unreadable targets, exactly-one-marker bound to its fence, ambiguous duplicate surface ids rejected, and post-enqueue drain kick for tracker-vcs-event. The contract doc, skill, templates, and evals are correct. Two low-severity robustness items remain in check-signal-envelope.mjs — both already exit with the correct code (2) but with messages that don't name the root cause.

@kyle-sexton
kyle-sexton merged commit 9e13316 into main Jul 19, 2026
14 checks passed
@kyle-sexton
kyle-sexton deleted the feat/autonomy-trigger-dispatch branch July 19, 2026 04:56
kyle-sexton added a commit that referenced this pull request Jul 20, 2026
…#377)

WP5 guardrail-enforcement package (T2 + T3 + the security-review policy
resolution), per the merged PLAN. Phases 1–3 complete; Phase 4's
positive probe runs in this PR's CI (transcript below after the run).

No linked issue

## Related

- #359 (WP4–WP7 plans, incl. this package's PLAN as merged)
- #372 (WP4 trigger-dispatch — the admission seam this package's policy
content binds; envelope/classification cross-links land here)
- #343 (WP2 telemetry — the promotion-evidence base and trace-tree
payloads)

## What ships

- **Guardrail hub** (`reference/guardrails.md`): five-class ×
five-column matrix (T3 verbatim), glance-layer routing to four leaves,
D8 permission-posture note, escalation event classes + org-bound routing
+ work-item/trace payload over the governed queue (no second channel).
- **Leaves** (`reference/guardrails/`): `isolation-ladder.md` (L0–L3,
attendance × input-provenance axes, rejected trigger-source axis with
falsification reason, L2 unattended floor, fail-closed
no-silent-degrade), `work-classes.md` (risk bundles; C1 repo-read-only
with queue writes permitted; C5→L3 floor; promotion/demotion discipline
with shipped default predicates — C2 auto-merge ≥20 completions/≥14
days/100% gates/0 reverts, C3 ai-review ≥30 advisory/0 missed-blocking;
demotion one-event-suffices), `security-review.md` (two layers,
per-class blocking knobs as floors — no override escape on
verification), `admission-policy.md` (3-axis decision table with `"*"`
wildcard + most-specific-wins, equal-specificity tie = invalid binding,
shipped defaults C1/C2 autonomous within caps regardless of provenance,
C3 per-item human, C4/C5 human-gated; caps `autonomous_concurrency: 1`,
`items_per_run: 3`).
- **Security-binding schema**
(`skills/setup/schemas/guardrails-security-binding.schema.json`): draft
2020-12, `additionalProperties: false`, fail-closed semantics in-schema;
`executor_class`, `dispatch_posture`, per-surface `isolation_bindings`
(level → substrate + `probe_evidence` + `runtime_markers`),
merge/verification knobs, `promotion_state`, `escalation_routes`,
`admission` (classification + rules + caps).
- **Semantic validator**
(`skills/setup/scripts/check-security-binding.mjs`, exit 0/1/2): matrix
merge caps, vendor-hosted human-gate cap, marker non-emptiness +
pairwise joint-satisfiability ambiguity rejection, class-aware
per-surface isolation verdicts (C5 requires L3), missing-L2 error under
`autonomous-enabled` vs declared verdict under `human-gated-only`,
non-promotable promotion entries rejected, admission weakening requires
`override_justification` (admission-only escape), promotion-gated knobs
require their ratified `promotion_state` entry, equal-specificity ties
invalid, evaluation mode resolving EFFECTIVE promotion state against
contrary evidence without writing the binding. 91 binding fixtures plus
67 transcript and 2 evidence files, each negative producing exactly one
finding. Note: egress-probe targets must be multi-label DNS names or
public literal IPs — encoded loopback/private forms are normalized and
rejected.
- **Setup guardrail slice** (SKILL.md): two-surface resolution (security
binding in settings-as-code home behind an agent-unwritable
org-policy-home bootstrap; repo-local non-security remaps), substrate
detection per surface, probe-before-bind live validation,
detect-diff-reconcile against existing org guardrail surfaces, security
review folded in (one slice, no near-duplicate capability), paid SKUs
advisory + cost surfaced, fail-closed no-L2. Plus
`templates/isolation-probe.md` and five new evals; plugin 0.5.0.

## Live-validation demonstration (Phase 4)

**Positive probe** — this PR carried a one-off workflow
(`wp5-isolation-probe-demo.yml`, since removed per the PLAN; evidence
persists in the run log below) that PROVISIONED a genuine egress-denied
boundary on the hosted runner (nested `docker run --network none`
container) and executed the probe recipe INSIDE it, with a
networked-outer-job control step. Transcript from the PR run:

From [run
29675620833](https://github.com/melodic-software/claude-code-plugins/actions/runs/29675620833):

```text
outer-egress: HTTP 200 (networked, as expected)
egress-denial: PASS - external fetch failed inside the boundary (exit 4)
credential-absence: PASS - every tested credential path absent inside the boundary (exit 3)
captured live transcript:
  "schema_version": "1",
  "probed_at": "2026-07-19T05:56:39Z",
  "assertions": {
    "egress_denied": { "host": "example.com", "exit_code": "4", "outcome": "denied" },
    "credentials_absent": { "path": "$HOME/.ssh,$HOME/.netrc,$HOME/.docker/config.json", "exit_code": "3", "outcome": "absent-or-denied" }
  },
  "outer_context_networked": true
Security binding OK: live-evidence/demo-binding.json
- surface "ci-pool-a": C1 eligible, C2 eligible, C3 eligible, C4 eligible, C5 blocked (requires L3, proven L2)
```

The final line is `check-security-binding.mjs` exit 0 against a binding
whose `probe_evidence` is the transcript captured seconds earlier in the
same job — the checker passes only against live-captured,
identity-matched evidence. (The recorded transcript predates the
capture-contract fields that later review hardening added to the checker
— per-target credential exit codes, host-side expansions, transport
outcomes, and the outer-context reachability/existence codes on both
assertions; it is preserved verbatim as the run's historical record.)

**Negative path** — `check-security-binding.mjs` on the missing-L2
`autonomous-enabled` fixture (exit 1):

```text
- isolation_bindings: no bound surface reaches L2 under dispatch_posture autonomous-enabled — autonomous dispatch is blocked fail-closed; compliant paths: bind an L2-capable substrate (whole-process OS-sandbox wrap, default-deny-egress container) on an execution surface, or declare dispatch_posture human-gated-only
```

**Declared posture** — the same missing-L2 shape under
`human-gated-only` validates (exit 0) with the blocked-dispatch verdict
reported as the DECLARED posture, not a defect.

**Demotion evaluation** — promoted-C2 binding + contrary evidence (exit
0, binding file unchanged):

```text
- C2-auto-merge: bound promoted -> effective unpromoted — ceiling lowered by contrary evidence (reverted-merge at 2026-07-10T12:00:00Z) WITHOUT modifying the binding; demotion files an escalation item on route "queue:escalations/demotion" requesting the human-ratified binding update
```

## Near-duplicate audit

Security review is folded into the ONE guardrail slice of guided-setup
(no separate security-review capability); escalation composes the
governed queue itself via the trigger contract's acknowledgment symmetry
— no second claim, dispatch, or escalation channel was created anywhere
in this package.

## Gates

`validate-plugins.sh`, `validate-plugin-contracts.mjs` (vendor deny-list
clean over `reference/`), catalog `--check` in sync, markdownlint 0
errors, typos clean, lychee offline 0 errors, `claude plugin validate
--strict` pass, skill-quality `check-skill.sh setup` PASS (2
pre-existing WARNs), evals.json schema-valid (ajv draft2020), actionlint
clean on the demo workflow. Full plugin test suite rides CI. Validator
exercised over the full fixture set with expected exit codes and
single-finding assertions.

## Boris check

Step-3 guardrails cell covered (agent sandboxing = isolation ladder;
automatic code + security review = verification layers; token/model
management = cost tiers); the step-4 sentence ("enforcing the right
guardrails for each type of work") is instantiated as the matrix; the
step-3 trap honored (human-ratified promotion, automatic evaluation-time
demotion); step-1 guardrail untouched. Auto-mode classifier tuning +
allowlist distribution explicitly deferred with named triggers.

<details>
<summary>PLAN.md (as merged, tags advanced)</summary>

# ai-ladder-wp5-guardrails

## Brief

### TLDR

Guardrail-enforcement package (T2 + T3 + #241 instance): one guardrail
contract shaped as a
progressive-disclosure hub (matrix doc as index/spine) with on-demand
leaf docs, an org-binding
seam split by governance sensitivity, evidence-gated promotion with
automatic demotion, and the
guardrail slice of guided-setup (detect → bind → live-validate →
fail-closed).

### Goal

Any adopting org can bind the five-class guardrail matrix to its own
instances and get
per-work-class enforcement — isolation floor, verification layers, merge
policy, cost tier,
escalation — with the security-sensitive bindings outside agent blast
radius, promotion earned
on queryable evidence, and no silent degrade anywhere.

### Locked decisions

| # | Decision |
|---|---|
| D1 | ONE guardrail contract, hub-and-leaves progressive disclosure:
matrix doc = index/spine (table + one-line class/column definitions,
glance layer resolves the common question); leaf docs load on demand —
e.g. isolation-ladder levels, per-class detail + promotion triggers,
security-review policy, admission policy (examples, not a fixed list;
split at architect). Plus the guardrail slice of guided-setup. Both in
the capability-distribution home. Fleet materializations = /work-items
backlog. |
| D2 | Org-binding seam: contract-owned schema, one logical binding, two
governance surfaces split by sensitivity. Security-sensitive axes
(isolation substrates per level, merge policy, per-layer blocking knobs,
promotion-trigger state, escalation routes, admission policy) bind in
the settings-as-code home — outside the blast radius of agents working
in the consuming repo (agent-writable guardrail binding = bypass
channel). Non-security mappings (class→label strings, cost-tier→model
names) may bind repo-locally per the work-item-tracker binding pattern.
Layered resolution: org-policy-home defaults → settings-as-code per-repo
binding → repo-local non-security remaps; contract defines resolution
order, materialization at architect. Fail-closed on absent/invalid
security-sensitive binding; documented defaults only for non-security
axes. |
| D3 | Promotion/demotion discipline: contract defines the trigger SHAPE
— an evidence predicate over queryable telemetry (verification outcomes
per the telemetry contract are the evidence base). Promotion =
human-ratified knob flip, recorded as a reviewable change on the
governance surface — never automatic. Demotion = automatic fail-closed
on contrary evidence (gate failure, human-reverted merge), re-earn from
there. Org binds threshold values; suggested defaults at architect.
Near-term promotable cells: C2 auto-merge, C3 AI-review
advisory→blocking (#241); C4/C5 merge never promotes. |
| D4 | Sandbox-ladder setup slice: detect available substrates per level
per machine surface → bind level→substrate in the security binding →
live-validate BEFORE recording (empirical probe, e.g. denied-egress
smoke test inside the boundary; a binding lands only after the sandbox
provably blocks) → fail-closed verify (no L2 substrate → autonomous
dispatch blocked for that surface, compliant paths named — no silent
degrade). |
| D5 | #241 three-part resolution folds into the matrix structure,
nothing ships separate: security-review policy contract (two layers,
per-layer blocking knob) = verification-column leaf; security-review
setup = part of the one guardrail slice of guided-setup (near-duplicate
capability ban); the adjustment/audit layer generalizes matrix-wide —
guided-setup always detect-diff-reconcile against existing org guardrail
surfaces (sandbox configs, branch protections, review workflows), never
greenfield-assume, never silently overwrite. |
| D6 | Escalation column sharpened from DIRECTIONAL: contract defines
escalation event classes (gate failure, verification divergence,
admission rejection, demotion event, structural-class plan approval,
untrusted-provenance always), routing obligation (org-bound routes in
the security binding), and payload (work-item ref + trace link, one
causal tree). Mechanism reuses the governed queue: escalation lands as a
human-gated work item + optional channel notification via the
trigger-contract acknowledgment symmetry — no second channel. Only
interactive escalation UX stays deferred; trigger: WP7 runner design
pack. |
| D7 | Admission-policy content (the trigger contract's admission seam
enforces it; this package owns it): decision-table shape —
signal-surface class × initiator provenance × work class → disposition
(autonomous-eligible / human-gated / audited rejection) — plus caps
(autonomous concurrency, per-run items). Shipped defaults: C1/C2
autonomous-eligible within caps regardless of provenance (blocking agent
provenance would sever the agent-kicks-off-agent loop), C3 per-item
human admission, C4/C5 human-gated. Provenance is recorded input to
gating, never trusted as isolation; caps bound total autonomous fan-out.
|
| D8 | Permission posture: attended ergonomics (auto-mode classifier
tuning, safe-command allowlist distribution) is NOT a matrix column —
unattended runs replace per-action prompts with the whole-process
boundary; the ladder leaf carries a one-line permission-posture note (L1
= attended ergonomics tier, L2+ = the boundary is the control).
Classifier tuning + allowlist distribution = backlog seeds with trigger,
not silent drops. |

### Constraints

- Any fleet repo or vendor name in normative contract text is a defect;
vendor names appear only
  as marked examples and in binding docs.
- Security-sensitive bindings never live where the agents they govern
can edit them.
- Fail-closed everywhere: unavailable substrate, absent/invalid security
binding, contrary
promotion evidence — block and name the compliant path, never degrade
silently.
- No new cost by default: free-path scanners and substrates default;
entitlement-gated tools
(e.g. paid code-scanning SKUs) are advisory + explicit opt-in with cost
surfaced.
- One queue, one escalation channel (the queue itself); no second claim,
dispatch, or
  escalation mechanism.
- Boris-alignment is the standing acceptance criterion (no
step-skipping, trust before scale).

### Acceptance criteria

- Matrix doc's glance layer alone answers "what governs class X" — leaf
docs only for depth
  (progressive-disclosure convention conformance).
- Binding schema names the two governance surfaces + layered resolution
order; security axes
  fail closed when unbound.
- Promotion is human-ratified + evidence-gated; demotion is automatic;
both leave a reviewable
  audit trail on the governance surface.
- Guided-setup slice detect-diff-reconciles existing guardrail surfaces,
live-validates
isolation before binding, and has zero paid dependencies on its default
path.
- Admission decision-table defaults match the matrix class defaults
verbatim; agent provenance
  is not blocked by default; caps are org-bindable.
- Boris check: step-3 guardrails cell covered (agent sandboxing =
ladder; automatic code +
security review = verification layers; token/model management = cost
tiers); step-4 sentence
("enforcing the right guardrails for each type of work") instantiated as
the matrix; step-3
trap honored (human-ratified promotion, automatic demotion); step-1
guardrail untouched.
- Auto-mode classifier tuning + allowlist distribution explicitly
out-of-scope with named
  triggers, not dropped.

### Captured assumptions

- Verification-outcome telemetry (WP2 contract) is queryable at
promotion-evaluation time; until
wired, promotion evaluation is manual over the same evidence definition.
- Free-path deterministic scanners remain available on the default path;
paid code-scanning
SKUs stay entitlement-gated (re-verified: private-repo CodeQL requires a
paid license).
- The work-item-tracker binding precedent (contract-owned schema,
org-supplied values,
  stop-and-report on invalid binding) remains the fleet's binding idiom.

### Out-of-scope (deferred with triggers)

- Interactive escalation UX — trigger: WP7 runner design pack.
- Auto-mode classifier tuning + safe-command allowlist distribution —
trigger: 1→2 residue
  sweep / standards allowlist-distribution work (backlog seeds).
- Cost ENFORCEMENT (hard spend caps) — trigger: 3→4 transition work;
cost tiers here are
  policy vocabulary only.
- Fleet materializations (binding instances, workflow gates, scanner
wiring) — /work-items
  backlog post-graduation.

### Deferred questions

- Exact class/level/column/attribute tokens + binding schema fields —
`/architect` (with the
  plugin naming pass).
- Leaf-doc file split + glance-layer wording — `/architect`.
- Promotion-trigger default values (run counts, windows) + demotion
evidence set — `/architect`.
- Setup-slice probe mechanics per substrate class — `/architect`.
- Admission decision-table serialization format — `/architect`.

## Plan

Recommendation-locked this round under the user's standing
pre-authorization (same basis as
the WP2/WP3 round): hub doc `reference/guardrails.md` with leaf docs
under
`reference/guardrails/` (`isolation-ladder.md`, `work-classes.md`,
`security-review.md`,
`admission-policy.md` — the D1 example split, adopted); class tokens
`C1`–`C5` and level
tokens `L0`–`L3` carried verbatim from T3/T2; security binding = a
contract-owned JSON
document in the settings-as-code home (`schema_version` from `"1.0"`);
non-security remaps =
an ADDITIVE `guardrails` section of the WP1 repo-local binding;
admission decision table
serialized as JSON inside the security binding.

Prerequisite: the WP4 implementation PR merged
(`reference/trigger-dispatch.md` exists — the
admission leaf cites its seam; the interim is safe via WP4's fail-closed
absent-binding
clause).

### Phase 1: Guardrail hub + leaf docs [DONE]

| File | Action | What changes |
|---|---|---|
| `plugins/autonomy/reference/guardrails.md` | Create | The hub/spine
(D1): the five-class × five-column matrix table imported from T3
unchanged (C1 read-only / C2 mechanical / C3 scoped / C4 structural / C5
untrusted-provenance × min-isolation / verification / merge policy /
cost tier / escalation), one-line class and column definitions, and the
glance-layer rule — this table alone answers "what governs class X";
every deeper question routes to a named leaf. Permission-posture note
per D8 (one line: L1 = attended ergonomics tier; L2+ = the boundary is
the control). Escalation event classes per D6 (gate failure,
verification divergence, admission rejection, demotion, structural-class
plan approval, untrusted-provenance always) with the routing obligation
(org-bound routes in the security binding) and payload (work-item ref +
trace link); mechanism = the governed queue itself (human-gated item +
optional channel notification via the trigger contract's acknowledgment
symmetry — no second channel). Boris step-4 sentence cited as the
matrix's source framing. Zero vendor/fleet names. |
| `plugins/autonomy/reference/guardrails/isolation-ladder.md` | Create |
T2 ladder normative text: L0–L3 level definitions, attendance ×
input-provenance axes, the falsified trigger-source axis recorded as
rejected (with reason), L2 unattended floor, fail-closed rule where L2
unavailable (block + name compliant paths, no silent degrade),
permission-posture line per D8. Instances org-supplied; free-path
substrate CLASSES named as marked examples only. |
| `plugins/autonomy/reference/guardrails/work-classes.md` | Create |
Per-class detail: risk-property bundle per class (blast radius,
reversibility, provenance, verifiability) — stating explicitly that C1
"read-only" scopes REPO surfaces and permits governed-queue/tracker
writes (work-item filing, no repo mutation; the WP6 stress-test's F8
disambiguation) and that C5's min-isolation cell is the L3 floor WP7's
C5-dispatch gate cites, per-cell promotion triggers (D3 shape: evidence
predicate over queryable telemetry per the telemetry contract; promotion
= human-ratified knob flip recorded as a reviewable change on the
governance surface; demotion = automatic fail-closed on contrary
evidence, re-earn from there). Suggested default predicates
(org-bindable values): C2 auto-merge — ≥ 20 autonomous C2 completions
over ≥ 14 days with 100% deterministic-gate pass and 0 human-reverted
merges; C3 AI-review advisory→blocking — ≥ 30 advisory reviews with 0
human-confirmed missed-blocking findings. C4/C5 merge never promotes.
Demotion evidence set: any post-merge gate failure, any human-reverted
merge, any verification divergence — one event suffices. |
| `plugins/autonomy/reference/guardrails/security-review.md` | Create |
The #241 security-review policy contract as the verification-column
leaf: two layers (deterministic scanners + AI security review),
per-layer blocking knob (advisory/blocking per class, bound on the
governance surface), free-path scanners default, paid SKUs advisory +
explicit opt-in with cost surfaced. |
| `plugins/autonomy/reference/guardrails/admission-policy.md` | Create |
The D7 admission-policy content the WP4 seam enforces: decision-table
shape (signal-surface class × initiator provenance × work class →
disposition `autonomous-eligible` / `human-gated` / `audited-rejection`)
+ caps (`autonomous_concurrency`, `items_per_run`). Shipped defaults
verbatim from D7: C1/C2 autonomous-eligible within caps regardless of
provenance; C3 per-item human admission; C4/C5 human-gated; provenance
recorded, never trusted as isolation. Default cap values (org-bindable):
`autonomous_concurrency: 1`, `items_per_run: 3` — conservative Boris
trust-before-scale floor. Serialization: the `admission` object of the
security binding (Phase 2 schema). |

**Sanity Check:**

- Hub glance test: `grep -c '| C[1-5] |'
plugins/autonomy/reference/guardrails.md` = 5 (matrix rows present) and
hub contains zero level-definition prose (`grep -c 'microVM'
plugins/autonomy/reference/guardrails.md` = 0 — depth lives in leaves)
- `grep -c 'audited-rejection'
plugins/autonomy/reference/guardrails/admission-policy.md` ≥ 1
- `grep -ci 'fail-closed'
plugins/autonomy/reference/guardrails/isolation-ladder.md` ≥ 1
- Promotion defaults present: `grep -c '20 autonomous C2 completions'
plugins/autonomy/reference/guardrails/work-classes.md` ≥ 1 and `grep -c
'14 days' plugins/autonomy/reference/guardrails/work-classes.md` ≥ 1
- Vendor+fleet deny-list sweep exit 0; lychee lane passes

### Phase 2: Binding schema (two governance surfaces) [DONE]

| File | Action | What changes |
|---|---|---|
|
`plugins/autonomy/skills/setup/schemas/guardrails-security-binding.schema.json`
| Create | Contract-owned JSON Schema for the security binding document
that lives in the settings-as-code home (outside agent blast radius per
D2): `schema_version` (const "1.0"), `executor_class` (`self-operated`
\| `vendor-hosted` — the merge-policy gating input, security-surface
data per the trigger contract), `dispatch_posture` (`autonomous-enabled`
\| `human-gated-only`, default `autonomous-enabled` — an org may
intentionally record its security settings while keeping every dispatch
human-gated; the PR-review finding that a missing L2 entry must not
conflate that deliberate posture with an invalid binding),
`isolation_bindings` keyed by EXECUTION SURFACE (org-named surface id —
e.g. the CI pool, a self-run host class; the PR-review finding that a
flat level→substrate map lets one surface's L2 binding satisfy the check
while a different dispatch surface has no boundary at all) → level token
→ substrate instance id + probe evidence ref + `runtime_markers` (the
per-surface TRUSTED identifying markers the trigger contract's dispatch
seam matches platform-attested runner context against — a key/value set
drawn from metadata the executing workload cannot forge, e.g. attested
runner-pool/label identity; serialized HERE, on the agent-unwritable
security surface, so surface attestation never falls back to the
repo-local recorded id), `merge_policy` (per class, capped per matrix),
`verification_blocking` (per layer per class), `promotion_state` (per
PROMOTABLE cell only: state + ratifying change ref + evidence window),
`escalation_routes` (event class → org route), `admission`
(classification rules — the signal→work-class mappings the trigger
contract's adapters stamp from, e.g. label→class for tracker-vcs-event —
plus decision-table rules with a `"*"` wildcard per axis and
most-specific-wins precedence (full triple > two axes > one axis >
default), optional per-rule `override_justification`, and caps
`autonomous_concurrency` / `items_per_run`). `additionalProperties:
false`; fail-closed semantics documented in-schema (absent/invalid
security binding blocks autonomous dispatch). |
| `plugins/autonomy/skills/setup/scripts/check-security-binding.mjs` |
Create | Validates a security-binding document against the schema +
semantic rules the schema cannot express: merge-policy caps never exceed
the matrix (C4/C5 human always; `executor_class: vendor-hosted` caps
every class at human-gated); every surface bound under
`dispatch_posture: autonomous-enabled` must declare non-empty
`runtime_markers` (else the dispatch seam could never attest the actual
surface and every dispatch would fail closed as unattestable), and
marker sets must be PAIRWISE UNAMBIGUOUS under joint satisfiability —
two conjunctive predicates are compatible (and the binding REJECTED)
unless they require CONFLICTING values for at least one shared key,
since a runtime context carrying the union of two non-conflicting
predicates matches both (subset/identity checks alone miss e.g. `{pool:
blue, region: us}` vs `{pool: blue, os: linux}`); the dispatch seam
still requires EXACTLY ONE matching surface at runtime, failing closed
on zero or multiple matches — the validator-level joint-satisfiability
rejection is what keeps that runtime rule from turning validly bound
fleets into ambiguity outages; isolation verdicts are PER SURFACE and
CLASS-AWARE: each bound surface resolves, per work class, eligible (its
bound level meets that class's min-isolation matrix cell — L2 is the
floor for any autonomous dispatch, C5 requires L3) or blocked —
admission/executor resolution must name the surface a dispatch would run
on AND the item's work class, and verifies the surface meets the class's
minimum at dispatch time, so an L2-only surface is never selected for a
C5 item (even after human admission or a later policy change) and an
unbound surface is blocked even when a sibling surface is bound; under
`dispatch_posture: autonomous-enabled` it is an ERROR when NO bound
surface reaches L2 (fail-closed, compliant paths named) — under
`human-gated-only` the binding validates and the verdict reports blocked
autonomous dispatch as the declared posture, not a defect; a
`promotion_state` entry for a non-promotable cell (C4/C5 merge) is
rejected; admission defaults not weaker than shipped defaults without
the rule's `override_justification` (declared in the schema, so a
justified override validates); every bound isolation level carries probe
evidence. Plus an EVALUATION mode: given a binding + an evidence source,
resolves the EFFECTIVE promotion state — the bound knob is a ceiling;
live contrary evidence (gate failure, reverted merge, verification
divergence) lowers it at evaluation time without writing the binding
(automatic demotion has no write-back actor by design: the binding is
agent-unwritable; the demotion event additionally files an escalation
item requesting the human-ratified binding update). This evaluation is a
LIVE-PATH OBLIGATION, not a demo artifact: the admission seam and the
merge disposition MUST resolve the effective state (querying the
promotion-evidence telemetry) before every autonomous dispatch/merge
decision — reading the raw `promotion_state` alone is non-conforming,
and unavailable evidence telemetry fail-closes to the unpromoted default
— the checker's evaluation mode exists to exercise the same resolution
mechanically. Exit contract 0/1/2. |
| `plugins/autonomy/skills/setup/SKILL.md` | Modify | Document the
two-surface split + layered resolution order (org-policy-home defaults →
settings-as-code per-repo security binding → repo-local non-security
remaps in the WP1 binding's additive `guardrails` section: class→label
strings, cost-tier→model names) — INCLUDING the security binding's
resolvable locator: the org-policy home carries the
repo→security-binding-document registry the dispatch seam resolves the
binding through when settings-as-code is a separate repository — and for
SECURITY resolution the org-policy-home identity itself must come from
an agent-unwritable bootstrap, never the repo-local binding (the binding
seam's known limitation that `org_policy_home` may persist repo-locally
is tolerable for non-security defaults only; a repo-writable pointer
would let an agent redirect the whole chain to a forged policy
repository with a forged registry, binding, and matching runtime
markers): the seam pins the org-policy-home identity from org-level
platform configuration outside repo blast radius (an org-level
setting/variable repo agents cannot write) or the executor's trusted
deployment config, and any security resolution that would depend on a
repo-writable pointer — or an unresolvable locator — fail-closes
autonomous dispatch with the compliant path named. Fail-closed on
absent/invalid security binding; documented defaults only for
non-security axes. |

**Sanity Check:**

- `check-security-binding.mjs` exit 0 on a fixture-valid document, exit
1 on each negative fixture: C4 merge_policy `auto`, `executor_class:
vendor-hosted` with any non-human-gated merge row, `promotion_state`
entry for a C4 cell, missing probe evidence, missing L2 under
`dispatch_posture: autonomous-enabled`, admission weakened without
`override_justification`; exit 0 with a blocked-dispatch verdict on the
missing-L2 + `human-gated-only` fixture (deliberate posture, not a
defect) (fixtures under `evals/fixtures/`)
- Evaluation mode: a promoted-C2 binding + contrary-evidence fixture
resolves the EFFECTIVE state demoted (exit output asserts the ceiling
was lowered without modifying the binding file)
- Schema declares `executor_class` and `override_justification` (`grep
-c 'executor_class' …schema.json` ≥ 1; `grep -c 'override_justification'
…schema.json` ≥ 1)
- Schema `additionalProperties: false` present (`grep -c
'"additionalProperties": false' …schema.json` ≥ 1)
- `claude plugin validate --strict` exit 0

### Phase 3: Guided-setup guardrail slice [DONE]

Extends the `setup` skill: detect → bind → live-validate → fail-closed
(D4), always
detect-diff-reconcile against existing org guardrail surfaces (D5 —
sandbox configs, branch
protections, review workflows), never greenfield-assume, never silently
overwrite.

| File | Action | What changes |
|---|---|---|
| `plugins/autonomy/skills/setup/SKILL.md` | Modify | Guardrail slice:
detect available substrates per level per machine surface; bind
level→substrate into the security binding; LIVE-VALIDATE before
recording — the empirical probe per substrate class: denied-egress smoke
test inside the boundary (a network fetch to a well-known external host
MUST fail) + host-credential-path read attempt (MUST be absent/denied);
a binding lands only after the probe transcript proves the boundary;
probe evidence ref recorded in the binding. Fail-closed verify: no L2
substrate on a surface → autonomous dispatch blocked for that surface,
compliant paths named. Security-review slice folded here per D5 (one
guardrail slice, no separate near-duplicate capability). Paid scanner
SKUs advisory + opt-in with cost surfaced. |
| `plugins/autonomy/skills/setup/templates/isolation-probe.md` | Create
| Probe recipe per substrate class (container / OS-sandbox wrap /
VM-microVM), parameterized, vendor names as marked examples only:
egress-denial probe command shape, credential-absence probe shape,
expected-failure assertions, transcript capture shape. |
| `plugins/autonomy/skills/setup/evals/evals.json` | Modify | Add
guardrail-slice cases: substrate detection interview, probe-before-bind
ordering, fail-closed no-L2 path, detect-diff-reconcile against a
pre-existing branch-protection surface, paid-SKU advisory
refusal-to-default. |
| `plugins/autonomy/README.md` +
`plugins/autonomy/.claude-plugin/plugin.json` | Modify | Capability list
+ description + minor version bump. |

**Sanity Check:**

- `/skill-quality:check` + `validate-evals` pass
- `grep -c 'probe'
plugins/autonomy/skills/setup/templates/isolation-probe.md` ≥ 3
- `grep -ci 'detect-diff-reconcile'
plugins/autonomy/skills/setup/SKILL.md` ≥ 1
- Fleet-name sweep exit 0

### Phase 4: Live-validation demonstration + gates [DONE]

Acceptance probe, BOTH paths mandatory: (positive) run the Phase 3 probe
recipe against a
REAL, PROVISIONED egress-denied boundary — the CI job on this public
repo (free minutes)
launches a nested no-network container via the container runtime present
on the hosted
Linux runner image (network mode none / internal-only, no host env or
secrets passed in)
and executes the probe INSIDE it; the outer job keeps normal networking,
so the probe's
egress-denial assertion is satisfied by a genuine boundary the job
itself created — never
by the bare runner (which has outbound access and would fail the
assertion) and never by a
hardcoded "denied" — transcript shows denied egress + absent credentials
inside the
boundary, proving the probe DETECTS a genuine boundary; (negative) the
fail-closed path is demonstrated by
validating an `autonomous-enabled` binding with a MISSING L2 entry and
observing the
blocked-dispatch error; the deliberate `human-gated-only` posture
validates separately with
its blocked-dispatch verdict reported as declared, not as a defect. A
fixture security binding referencing the positive probe's evidence
passes
`check-security-binding.mjs`; the demotion evaluation fixture (Phase 2)
runs here as part of
the demo record. Then the full gate roster (WP2 Phase 4):
`validate-plugins.sh`,
`run-plugin-tests.sh`, `validate-plugin-contracts.mjs`,
markdown/typos/lychee,
`claude plugin validate --strict`, catalog regen. Near-duplicate audit
statement: security
review folded into the one guardrail slice (D5); escalation composes the
governed queue — no
second channel created.

**Sanity Check:**

- Positive probe transcript (provisioned no-network container on the CI
runner: egress denied + credentials absent inside; outer job networked)
attached to the PR body
- Negative path: `check-security-binding.mjs` exit 0 on the demo
binding, exit 1 on the missing-L2 `autonomous-enabled` fixture, exit 0 +
declared blocked-dispatch verdict on the `human-gated-only` fixture
- Demotion evaluation output (effective state lowered, binding file
unchanged) in the demo record
- All gate scripts exit 0; catalog in-sync
- Near-duplicate audit statement present in the PR body

## Blast radius

MEDIUM-HIGH — one plugin's files, but this contract governs every
autonomous dispatch
decision (admission), every isolation binding, and the promotion path to
the closed loop;
security-posture content raises the review bar. Fully git-revertible;
the security binding
itself ships as schema + fixtures only (no live org binding lands in
this package).

## Stress-test summary

Fresh-context plan review (WP4+WP5 batch): 14 findings, verdict
FIX-THEN-SHIP, all folded.
WP5's share — F1b (HIGH, cross-package): the hosted human-merge-gate cap
keyed on an
executor class the schema never carried → `executor_class` added to the
security schema and
enforced by the check; F9 (MED): `override_justification` was required
by the check but
undeclared under `additionalProperties: false` → declared in the
admission-rule sub-schema;
F10 (MED): automatic demotion had no write-back actor against an
agent-unwritable binding →
eval-time evidence-gating (bound state is a ceiling; live contrary
evidence lowers the
effective state; the demotion event files an escalation item requesting
the human-ratified
update), with an evaluation-mode fixture in the check and the demo; F11
(MED): the
fail-closed pivot alone never proved the probe detects a real boundary →
positive probe
mandatory against a free-tier CI container, negative path demonstrated
separately; F12
(LOW): fragile `grep -c '20'` → full-phrase grep; F13 (LOW): 3-D
admission table gains a
`"*"` wildcard + most-specific-wins precedence, and promotion entries
for non-promotable
cells are rejected; F14 (LOW, symmetry): the D8 backlog seeds are echoed
in Open questions.
The classification-rules half of F1a lands here as the
`admission.classification` schema
content (the WP4 contract's adapters stamp from it).

## Execution shape

Fully sequential 1 → 2 → 3 → 4 — Phase 2's schema serializes Phase 1's
admission leaf;
Phase 3 wires Phase 2's binding; Phase 4 probes Phase 3's recipe.
Cross-package: after the
WP4 implementation PR (admission seam exists to cite).

| Phase | Surface | Basis |
|---|---|---|
| 1 | main-session | normative security-policy authoring, tightly
coupled T2/T3 imports |
| 2 | main-session | schema + semantic-validator judgment |
| 3 | main-session | setup-skill judgment, probe design |
| 4 | main-session | live probe + gate runs |

## Open questions

- Fleet materializations (live org security binding, workflow gates,
scanner wiring) —
/work-items backlog post-merge (Brief out-of-scope, triggers recorded).
- Interactive escalation UX — WP7 runner design pack (Brief trigger).
- D8 backlog seeds (auto-mode classifier tuning + safe-command allowlist
distribution) —
carried in the Brief with named triggers; filed via the work-items flow.

## Decisions made (gate-passed)

| Decision | What it changes in the plan | Basis (evidence) |
|---|---|---|
| Leaf split: 4 leaves under `reference/guardrails/` | Phase 1 file set
| Brief D1 names exactly these four as the example split; each maps to
one matrix column/axis |
| Hub-subdirectory layout (first `reference/` subdir) | Phase 1 paths |
Progressive-disclosure hub needs its leaves grouped; flat names would
couple hub prose to 4 sibling filenames |
| Promotion defaults: C2 ≥20 runs/≥14 days/100% gates/0 reverts; C3 ≥30
advisory/0 missed-blocking | work-classes leaf | D3 requires suggested
defaults at architect; values conservative per Boris trust-before-scale
(org-bindable) |
| Cap defaults `autonomous_concurrency: 1`, `items_per_run: 3` |
admission leaf + schema | D7 requires caps; floor values bound total
fan-out at the trust-earning stage |
| Security binding as JSON + JSON Schema + semantic validator | Phase 2
deliverables | WP1 binding precedent (schema-versioned JSON,
stop-and-report on invalid); schema alone cannot express matrix caps |
| `executor_class` + classification rules live on the security surface;
promotion is eval-time evidence-gated (ceiling semantics) | Phase 2
schema + check | Stress-test F1b/F10: gating inputs must sit outside
agent blast radius; an agent-unwritable binding cannot receive an
automated demotion write |
| Demo pivots to fail-closed path when no container substrate exists |
Phase 4 | D4 makes fail-closed the required behavior — the pivot
demonstrates the contract, not a skipped demo |

[FALLBACK — confirm or override] `check-security-binding.mjs` as a NEW
deliverable (Phase 2)
— invented beyond the Brief on the WP2/WP4 enforcement-surface
precedent. Flag if unwanted.

## Handoff to implementation

### User-approval gates

- The binding-check script above is [FALLBACK] — surface before
authoring if contested.
- Any change to the shipped admission defaults or matrix cells during
implementation → STOP
  (these are T3/D7 user-locked content, not implementation discretion).
- Any scope expansion beyond the four phases re-enters `/architect
review`.

### Execution shape ([EXEC-SHAPE] tagged)

Sequential 1→4, all main-session (table above). PLAN.md phase tags
advance in the same commit
as each phase; live probe per Phase 4 with the documented fail-closed
pivot.

### Mechanical work

Commit per phase on the implementation branch (suggest
`feat/autonomy-guardrails`); gates
re-run in full at Phase 4; PR body carries the probe transcript +
near-duplicate audit
statement + this PLAN in a `<details>` block at close-out.

</details>

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
kyle-sexton added a commit that referenced this pull request Jul 20, 2026
…ice (WP6) (#600)

WP6 standing-routines package (T7), per the merged PLAN. All four phases
complete; the novel-class derivation probe and near-duplicate audit
statement are recorded below.

No linked issue

## Related

- #359 (WP4–WP7 plans, incl. this package's PLAN as merged)
- #242 (standing scheduled routines — the decision node this package
graduates: which maintenance classes, hosted where)
- #239 (WP map: AI adoption ladder, step-3 unlock)
- #372 (WP4 trigger-dispatch — routines are scheduled `temporal`
adapters behind its queue contract)
- PR #377 (WP5 guardrail matrix + security binding — the matrix the
mapping rules derive into; the `admission.classification.temporal` home
the routine slice binds)

## What ships

- **Routine catalog hub** (`reference/routines.md`): routine = scheduled
trigger adapter behind the governed queue, never a private
execution/merge path; two-family repetition language (session-scoped:
loop, goal, batch, dynamic workflow; standing: schedule, routine — with
the corrected batch definition); schedule/event/continuous trigger
taxonomy; output contract (report OR governed-queue work item; direct
change only through the matrix merge policy); the contract-owned
catalog-to-matrix mapping rules including the refined axes (DET → plain
cron, `not-a-routine`; hybrid DET-detect + AGT-judgment split; AGT rules
to C1–C3; structural/config-surface direct change → C4;
attacker-writable external input → C5; compose-to-highest; access axis →
prerequisites); 40-row glance catalog — ten `v1` rows, seven
`not-a-routine` rows, named join triggers on every deferred row.
- **Ten v1 definition leaves** (`reference/routines/`): uniform shape
(purpose, trigger and cadence, access scope, output contract, derived
guardrail row, admission and escalation, precedent). Every guardrail row
is derived step-by-step through the mapping rules, none hand-assigned:
six pure-advisory classes at C1/L2 floor; `doc-freshness-sweep`'s
optional gated docs-change portion C3; `dependency-update-wave` hybrid
with two explicit postures (mechanical-only C2; changelog-reading
posture provenance-composed to C5); `tech-debt-sweep` hybrid with
human-gated prioritization; `ci-health-review`'s optional CI-config
change portion C4 (structural surface). Cadence defaults org-bindable;
governance imported by citation only.
- **Guided-setup routine slice** (`skills/setup/SKILL.md`):
discovery-first surface interview + budget posture; free defaults wired
as reviewable changes across role homes; paid/preview surfaces advisory
with cost surfaced; detect-diff-reconcile of existing schedulers and
bots (a live judgment-bearing bot IS a catalog-class instance — recorded
under its class token, never duplicated; DET stays cron through
reconciliation). Binding-home split by governance sensitivity: the
routine→work-class mapping is admission data and lands only in the
security binding's `admission.classification.temporal` home
(settings-as-code, agent-unwritable, reconciled bots included); the
repo-local `routines` section carries cadence/enablement/surface only,
reusing the envelope checker's `surfaces` map shape.
- **`templates/routine-definitions.md`**: per-surface-class wiring
shapes (CI-cron / local scheduler / self-run / vendor-preview as marked
examples), every shape enqueuing through the trigger contract's
`temporal` adapter — no direct execution path.
- **Protected routine identity (review wave 1)**: routines bind under a
routine identity (`<class-token>` or `<class-token>/<posture-token>`;
multi-posture classes bind per posture, never the bare token);
`admission.classification.temporal` entries are object form `{class,
source_surface}` — the identity↔surface association on the security
binding, one identity per emitting surface; the temporal envelope
carries a required `signal.routine` claim admission validates before
stamping (`check-security-binding.mjs` + `check-signal-envelope.mjs`
enforce; five new fixtures; sweep 9 pass / 89 reject).
- **Evals**: five routine-slice cases (surface interview first,
free-default wiring home split, existing-bot reconciliation
record-not-duplicate, paid-surface advisory refusal-to-default,
selector-is-a-claim protected association). Plugin 0.5.0 → 0.6.0.

## Novel-class derivation probe (Phase 4 acceptance)

Classifying a class the catalog does not contain, using only the shipped
mapping rules — an untrusted-provenance case, exercising the C5 reach:

**Candidate class: community-forum feedback triage** — a standing sweep
that reads public community-forum threads about the product, clusters
recurring complaints, and files candidate work items.

1. **Judgment axis**: semantic clustering, salience ranking, and routing
are agent judgment → `AGT` (not `DET`; no deterministic detection half
worth a hybrid split — a keyword pre-filter could route to plain cron,
leaving the judgment portion as the routine, per the hybrid branch).
2. **Output axis**: report + work items into the governed queue, no
repository mutation → `C1` base (`AGT` + report → `C1`; `AGT` + work
item → `C1`).
3. **Direct-change axes**: no direct change → the `C2`/`C3`
mechanical-checkability rule and the `C4` structural-surface rule do not
lift the class.
4. **Input-provenance axis**: public forum posts are attacker-writable
external content → derives `C5` (untrusted provenance).
5. **Composition**: multiple matches compose to the highest-risk class —
`C5` > `C1` → the class is **C5**.
6. **Access axis → prerequisites**: `ext` access → connector
prerequisite + entitlement resolved in the org binding, join trigger
"named intel need"; the `C5` matrix row then fixes the floor — L3
isolation, full gates + zero secret exposure, human merge always,
escalation always. The read-only class's exfiltration-surface caveat
arrives as the class outcome, not a footnote.

End-to-end: axes → guardrail row → prerequisites, with zero contract
changes. The acceptance criterion holds, including the C5 reach the
plan's stress-test added the provenance axis for.

## Near-duplicate audit

Routines compose the WP4 `temporal` adapter and the WP5 guardrail
matrix; deterministic checks stay plain cron filing work items through
the same trigger adapters; no second scheduling, execution, or merge
path is created by this package.

## Plan deviation (recorded)

The PLAN's Phase-1 row-count gate said 39 (one row per research-taxonomy
class). The research table contains only 9 of the 10 locked v1 classes —
`pr-queue-tending` has no research row (the nearest research row, stale
grooming, is pure-DET and locked `not-a-routine`). The catalog therefore
ships 40 rows = 39 research rows + the fleet-proven `pr-queue-tending`.
The D2 mapping rules, the seven `not-a-routine` rows, and the ten-class
v1 roster are untouched.

## Gates

validate-plugins.sh, validate-plugin-contracts.mjs (33 setup skills /
1777 plugin files), markdownlint-cli2, typos, `claude plugin validate
--strict`, catalog regen in-sync — all green at head. Reference-doc
vendor-name sweep: zero hits (hub + all ten leaves). run-plugin-tests.sh
and lychee validate in CI.

<details>
<summary>WP6 PLAN (full)</summary>

# ai-ladder-wp6-routines

## Brief

### TLDR

Standing-routines package (T7): catalog doc as a progressive-disclosure
hub (class table =
glance layer), contract-tier definition docs for the v1 subset only,
contract-owned
catalog-to-matrix mapping rules, and the routine slice of guided-setup.
A routine stays a
scheduled trigger adapter behind the governed queue — never a private
execution or merge path.

### Goal

Any adopting org can stand up governed background maintenance — the
step-3 unlock:
"maintenance and cleanup that used to wait for someone to find the time
now runs continuously
in the background" — with every routine's guardrail row derived from
catalog axes, free
hosting defaults, and existing org schedulers reconciled rather than
duplicated.

### Locked decisions

| # | Decision |
|---|---|
| D1 | Scope: catalog doc (hub-and-leaves — class table with
judgment/output/access scores = glance layer) + per-class definition
leaf docs for the v1 subset ONLY (non-v1 classes stay table rows with
precedent pointers until their join trigger fires) + catalog-to-matrix
mapping rules + routine slice of guided-setup. All
capability-distribution home. v1 definitions are contract-tier
tool-agnostic docs; vendor-binding capability templates are build-stage
artifacts. Fleet routine stand-up = /work-items backlog. |
| D2 | Catalog-to-matrix mapping rules are contract-owned (orgs classify
new routine classes without republication): judgment DET → not a routine
— plain cron, zero agent tokens, failures file work items through
trigger adapters; AGT + report → C1; AGT + work-item output → C1
(governed-queue write, no repo mutation); AGT + direct change → C2 where
truth is mechanically checkable, C3 otherwise; AGT/HUM → agent prepares,
disposition human-gated always. Access axis → prerequisites: repo-scoped
= L2 unattended floor; prod/product/org/ext = connector prerequisite +
entitlement resolved in the org binding; external-watch classes read
attacker-writable content — the read-only class's exfiltration-surface
caveat applies; unattended GUI actuation = highest isolation tier.
*REFINED 2026-07-18 (WP6/WP7 stress-test F3+F4): (a) hybrid branch — a
DET-detect + AGT-judgment class (catalog rows 7/14/16/28, incl. v1
dependency-update-wave) splits: the detection portion routes to plain
cron, the judgment portion IS the routine and derives through the AGT
rules; the binary DET/AGT reading could not classify hybrids. (b)
Pure-DET classes carry an explicit `not-a-routine` catalog flag (rows
2/12/20/22/25/27/34) instead of silent exclusion. (c) Two axes added so
derivation reaches the full matrix: input provenance — a routine
consuming attacker-writable external content derives C5 (untrusted
provenance), upgrading the external-watch caveat to a class outcome;
structural blast radius — direct change to structural/config surfaces
derives C4. Overlapping matches compose to the highest-risk class (C5 >
C4 > C3 > C2 > C1). Without them derivation topped out at C3 and the
novel-class acceptance criterion was unmeetable for C4/C5 cases.* |
| D3 | v1 definition doc shape (each leaf): purpose (toil addressed),
trigger-taxonomy slot with suggested cadence (org binds values), access
scope, output contract (report shape / work-item filing / gated change),
guardrail row derived via D2, admission + escalation notes imported from
the guardrail contract, precedent pointer. No vendor scheduling detail
baked in — surfaces are preview-stage moving targets; setup researches
them live. |
| D4 | v1 subset (repo-scoped, agent-judgment, proven manual patterns):
issue triage sweep, duplicate-detection sweep, backlog readiness check,
PR-queue tending, doc-freshness sweep, dependency update wave,
advisory/CVE triage, tech-debt sweep, ~~dead-code sweep~~ eng-metrics
digest, CI health review. *CORRECTED 2026-07-18 (review evidence,
RESEARCH-routine-catalog row 25): dead-code sweep is classified DET
detect — per D2's rule it is not a routine; detection routes to plain
cron filing work items through trigger adapters, and any
judgment-bearing triage of its findings belongs to the tech-debt sweep.
The WP6 architect round confirms the final v1 roster.* Deferred classes
keep T7's named join triggers (prod/product/org/ext access, GUI
actuation). |
| D5 | Routine slice of guided-setup is discovery-first: interview the
org for scheduling surfaces (CI cron, dev machine, self-run infra,
vendor-hosted preview) and budget posture; wire free defaults as
reviewable changes (CI-cron handler = CI-orchestration home, enabling
settings = settings-as-code home); advise paid/preview surfaces with
cost surfaced; detect-diff-reconcile EXISTING org schedulers and bots —
a live dependency bot or stale bot IS a catalog-class instance: record
it in the binding, never stand up a second mechanism for the same
concern. |
| D6 | Ubiquitous language imported from the routine contract thread
with one correction: batch = session-scoped parallel fan-out over
decomposed units (vendor command verified against the official commands
doc; research had conflated it with bulk-inference Batch APIs). Contract
carries the two-family distinction: session-scoped (loop, goal, batch,
dynamic workflow) vs standing (schedule, routine). Routine output =
advisory report OR work item into the governed queue; direct change only
through the guardrail matrix's merge policy. |
| D7 | Hosting stance imported unchanged (deployment-owned binding):
contract fixes invariants only — queue contract, per-class isolation
floor, merge-policy caps including hosted-stays-human-gated, cost
surfaced before any paid binding. Profiles are non-normative examples;
no machine/org-size/budget assumption. |

### Constraints

- Any fleet repo or vendor name in normative contract text is a defect;
vendor names appear
  only as marked examples and in binding docs.
- No second execution, merge, or scheduling path: routines enter work
through the trigger
  contract's queue and are governed by the guardrail matrix from there.
- Deterministic checks are never routines — no agent tokens on script
work.
- No new cost by default; paid/preview scheduling surfaces are advisory
+ explicit opt-in.
- Boris-alignment is the standing acceptance criterion (no
step-skipping, trust before scale).

### Acceptance criteria

- Catalog glance layer alone answers "what classes exist, what governs
each" — leaf docs only
  for v1 depth (progressive-disclosure conformance).
- Every v1 leaf derives its guardrail row through the D2 rules; no row
is hand-assigned
  outside them.
- Mapping rules let an org classify a novel routine class end-to-end
(axes → guardrail row →
  prerequisites) without contract changes.
- Setup slice wires only reviewable changes, reconciles existing
schedulers instead of
  duplicating them, and has zero paid dependencies on its default path.
- Research gaps carried visibly: dependency-update-wave precedent detail
rests on official
docs whose page bodies were not all fetched in full (re-verify at
architect); support→bug
conversion class remains emerging/unverified (not in v1); one vendor
goal-primitive claim
  unconfirmed (not load-bearing for v1).
- Boris check: step-3 unlock sentence instantiated by the v1 subset
running as governed
background work; "break up your work into loops and routines" cell
covered with the
session-scoped vs standing distinction; step-3 trap honored
(catalog-now,
instantiate-on-trust — v1 dispatch gated by the admission policy and
promotion discipline);
token-efficiency bottleneck addressed (deterministic-stays-cron + cost
tiers); step-1
  guardrail untouched.

### Captured assumptions

- The v1 classes' manual patterns remain proven in the first adopting
instance (fleet dogfood
  evidence: recurring manual sweeps exist for each).
- Vendor scheduling surfaces remain preview-stage moving targets;
contract-tier docs stay
binding-free and setup researches surfaces live (re-verify at setup
time, not bake-in).
- The guardrail matrix (WP5) and trigger contract (WP4) land before or
with any v1 routine
  instantiation — routines have no governance path without them.

### Out-of-scope (deferred with triggers)

- Non-v1 class definition leaves — trigger: T7's named join triggers per
group (telemetry
connector, analytics/feedback connected, org systems connected, named
intel need, GUI-only
  system + isolation tier).
- Vendor-binding capability templates (routine/workflow files) — build
stage, post-architect.
- Fleet routine stand-up + existing-scheduler reconciliation execution —
/work-items backlog
  post-graduation.
- Interactive escalation UX for routine-filed items — WP7 (inherited
from guardrail package).

### Deferred questions

- Exact class tokens + catalog table serialization — `/architect` (with
plugin naming pass).
- Suggested cadence defaults + per-class output templates —
`/architect`.
- Leaf-doc file split + glance wording — `/architect`.
- Adapter shapes per scheduling-surface class — `/architect`
(live-verified then).

## Plan

Recommendation-locked this round under the user's standing
pre-authorization: catalog hub
`reference/routines.md` with v1 definition leaves under
`reference/routines/` (one file per
v1 class, kebab-case class tokens below); class tokens for the v1 roster
—
`issue-triage-sweep`, `duplicate-detection-sweep`,
`backlog-readiness-check`,
`pr-queue-tending`, `doc-freshness-sweep`, `dependency-update-wave`,
`advisory-cve-triage`,
`tech-debt-sweep`, `eng-metrics-digest`, `ci-health-review` (ten
classes; dead-code sweep
stays off the v1 roster per the D4 correction — the architect-round
confirmation the
correction asked for — and appears in the catalog as a `not-a-routine`
row; hybrid v1
classes, e.g. `dependency-update-wave`, classify through the D2 hybrid
branch: detection
portion cron, judgment portion the routine); catalog table serialization
= one markdown table (class token × judgment × output ×
access × derived guardrail row × v1/deferred + join trigger).

Prerequisites: WP4 implementation merged
(`reference/trigger-dispatch.md` — routines are
scheduled trigger adapters behind its queue contract) and WP5
implementation merged
(`reference/guardrails.md` — the D2 mapping rules derive rows into its
matrix). This package
merges after both.

### Phase 1: Catalog hub + mapping rules [DONE]

| File | Action | What changes |
|---|---|---|
| `plugins/autonomy/reference/routines.md` | Create | The T7 routine
contract as the catalog hub: routine definition (a scheduled trigger
adapter behind the governed queue — never a private execution/merge
path); the two-family ubiquitous language imported per D6
(session-scoped: loop, goal, batch, dynamic workflow; standing:
schedule, routine — with the corrected batch definition); trigger
taxonomy (schedule / event / continuous); output contract (advisory
report OR work item into the governed queue; direct change only through
the matrix merge policy); the D2 catalog-to-matrix mapping rules
verbatim, INCLUDING the refined axes (DET → plain cron, never a routine
— flagged `not-a-routine` in the catalog; hybrid DET-detect +
AGT-judgment → split: detection portion cron, judgment portion IS the
routine, derived through the AGT rules; AGT+report → C1; AGT+work-item →
C1; AGT+direct-change → C2 where mechanically checkable else C3;
structural/config-surface direct change → C4; input provenance:
attacker-writable external content → C5, the untrusted-provenance class,
not a caveat; when multiple rules match, the derivation COMPOSES TO THE
HIGHEST-RISK class (C5 > C4 > C3 > C2 > C1 — a structural change driven
by attacker-writable input derives C5 and its L3 floor, never C4's lower
floor); AGT/HUM → human-gated disposition; access axis → prerequisites
incl. L2 floor for repo-scoped, connector+entitlement for
prod/product/org/ext, highest tier for unattended GUI actuation —
without the C4/C5 axes the Phase 4 untrusted-provenance derivation could
not run from the shipped contract); the glance-layer catalog table: all
39 classes as rows (class token, judgment, output, access, derived row,
and a status flag — `v1`, a named join trigger, or `not-a-routine` for
the seven pure-DET rows incl. dead-code sweep, which stay as rows with
their cron/tooling precedent pointers rather than silent exclusions);
hybrid rows show the D2 split (detection portion cron, judgment portion
the routine); non-v1 rows carry precedent pointers only, no leaves;
hosting stance per D7 (invariants only, profiles as non-normative
examples). Zero vendor/fleet names in normative text. |

**Sanity Check:**

- `grep -c 'DET' plugins/autonomy/reference/routines.md` ≥ 1 and mapping
rules present (`grep -ci 'plain cron' …/routines.md` ≥ 1)
- Catalog rows: table row count = 39 (`grep -cE '^\| [a-z][a-z0-9-]+ \|'
plugins/autonomy/reference/routines.md` = 39)
- v1 rows = 10 (`grep -c '| v1 |'
plugins/autonomy/reference/routines.md` = 10, or the chosen flag token)
- `not-a-routine` rows = 7 (`grep -c 'not-a-routine'
plugins/autonomy/reference/routines.md` ≥ 7); the dead-code-sweep row
carries the flag (`grep 'dead-code'
plugins/autonomy/reference/routines.md | grep -c 'not-a-routine'` = 1 —
flagged, never a silent exclusion, and never a v1 leaf)
- Vendor+fleet deny-list sweep exit 0; lychee lane passes

### Phase 2: v1 definition leaves [DONE]

| File | Action | What changes |
|---|---|---|
| `plugins/autonomy/reference/routines/<class-token>.md` × 10 | Create |
One leaf per v1 class, uniform D3 shape: purpose (toil addressed);
trigger-taxonomy slot + suggested cadence default (issue-triage-sweep,
duplicate-detection-sweep, backlog-readiness-check, pr-queue-tending:
daily; doc-freshness-sweep, tech-debt-sweep, ci-health-review: weekly;
dependency-update-wave, advisory-cve-triage: weekly with event-riding on
advisories; eng-metrics-digest: weekly — all org-bindable values);
access scope (repo); output contract (report vs work-item filing per
class); guardrail row DERIVED via the D2 rules with the derivation shown
(no hand-assigned rows); admission + escalation notes imported by
citation from the guardrail contract; precedent pointer (the proven
manual pattern). No vendor scheduling detail. |

**Sanity Check:**

- `ls plugins/autonomy/reference/routines/*.md | wc -l` = 10
- Every leaf shows its derivation: `grep -lc 'derived'
plugins/autonomy/reference/routines/*.md | wc -l` = 10
- Every leaf names a cadence default: `grep -lc 'cadence'
plugins/autonomy/reference/routines/*.md | wc -l` = 10
- Vendor+fleet deny-list sweep exit 0

### Phase 3: Guided-setup routine slice [DONE]

| File | Action | What changes |
|---|---|---|
| `plugins/autonomy/skills/setup/SKILL.md` | Modify | Routine slice
(D5): interview scheduling surfaces (CI cron, dev machine, self-run
infra, vendor-hosted preview) + budget posture; wire free defaults as
reviewable changes (CI-cron handler shape = CI-orchestration home role,
enabling settings = settings-as-code home role); advise paid/preview
surfaces with cost surfaced; DETECT-DIFF-RECONCILE existing org
schedulers and bots — a live dependency/stale bot IS a catalog-class
instance: record it in the binding (class token + surface), never stand
up a second mechanism. Binding home split by governance sensitivity
(stress-test F1, the WP5 D2 split): the routine→work-class mapping each
temporal signal's `signal.work_class` stamp derives from is ADMISSION
data — it lands in the WP5 security binding's `admission.classification`
(settings-as-code home, reviewable change, agent-unwritable), for
reconciled bots too; only non-security keys (cadence, enablement,
surface choice) land as the additive repo-local `routines` section of
the schema-versioned binding. A repo-local class source would be the
exact agent-writable bypass WP4's classification obligation forbids.
Research scheduling surfaces live at setup time (preview-stage moving
targets), never from this doc. |
| `plugins/autonomy/skills/setup/templates/routine-definitions.md` |
Create | Per-surface-class routine wiring shapes (CI-cron / local
scheduler / self-run / vendor-preview as marked examples), parameterized
by class token + cadence; every shape enqueues through the trigger
contract's temporal adapter — no direct execution path. |
| `plugins/autonomy/skills/setup/evals/evals.json` | Modify |
Routine-slice cases: surface interview, free-default wiring,
existing-bot reconciliation (record-don't-duplicate), paid-surface
advisory refusal-to-default. |
| `plugins/autonomy/README.md` +
`plugins/autonomy/.claude-plugin/plugin.json` | Modify | Capability list
+ description + minor version bump. |

**Sanity Check:**

- `/skill-quality:check` + `validate-evals` pass; `claude plugin
validate --strict` exit 0
- `grep -ci 'detect-diff-reconcile'
plugins/autonomy/skills/setup/SKILL.md` ≥ 2 (guardrail slice + routine
slice each state it)
- `grep -c 'temporal'
plugins/autonomy/skills/setup/templates/routine-definitions.md` ≥ 1
(queue-entry via the trigger contract)
- Fleet-name sweep exit 0

### Phase 4: Derivation demonstration + gates [DONE]

Acceptance probe: classify one NOVEL routine class end-to-end using only
the shipped mapping
rules (axes → guardrail row → prerequisites) — the acceptance criterion
that an org can
classify without contract changes; the chosen novel class MUST be an
untrusted-provenance
case (attacker-writable external input) so the probe exercises the D2
provenance axis and
the C5 reach the stress-test found missing, not just the easy C1–C3
path; record the worked
example in the PR body (not in the contract — it is evidence, not
normative text). Then the full gate roster
(`validate-plugins.sh`, `run-plugin-tests.sh`,
`validate-plugin-contracts.mjs`,
markdown/typos/lychee, `claude plugin validate --strict`, catalog
regen). Near-duplicate
audit statement: routines compose the WP4 temporal adapter + WP5 matrix;
deterministic checks
stay plain cron; no second scheduling or merge path created.

**Sanity Check:**

- Worked novel-class derivation present in the PR body
- All gate scripts exit 0; catalog in-sync
- Near-duplicate audit statement present in the PR body

## Blast radius

MEDIUM — one plugin's files; the catalog constrains WP7 and every
adopting org's background
maintenance, but all governance content is imported from WP4/WP5 by
citation, not redefined.
Fully git-revertible.

## Stress-test summary

Fresh-context plan review (WP6+WP7 batch): 10 findings, verdict
FIX-THEN-SHIP, WP6's share
folded — F1 (HIGH, cross-package): the routine→class mapping had no
security-surface home;
the only binding home named was the repo-local additive-section idiom,
which is exactly the
agent-writable class source WP4's classification obligation forbids →
the mapping lands in
the WP5 security binding's `admission.classification` (reconciled bots
included); only
cadence/enablement/surface stay repo-local. F3 (MED-HIGH): binary
DET/AGT mapping could not
classify hybrid classes (a v1 class among them), and two Phase-1 gates
were mutually
unsatisfiable (39 rows vs dead-code = 0) → D2 hybrid branch + explicit
`not-a-routine`
catalog flag for the seven pure-DET rows; gates reconciled. F4
(MED-HIGH): derivation
reached only C1–C3, silently breaking the novel-class acceptance
criterion for C4/C5 →
provenance and structural-blast axes added; the Phase 4 probe now must
exercise an
untrusted-provenance case. F8 (LOW) routes to WP5's work-classes leaf
(C1 permits
governed-queue writes, no repo mutation).

## Execution shape

Fully sequential 1 → 2 → 3 → 4 — leaves derive through Phase 1's rules;
the slice wires
Phase 2's classes; Phase 4 proves Phase 1's rules on a novel class.
Cross-package: after the
WP4 and WP5 implementation PRs.

| Phase | Surface | Basis |
|---|---|---|
| 1 | main-session | normative catalog + mapping-rule authoring |
| 2 | main-session | ten uniform leaves, derivation judgment per class |
| 3 | main-session | setup-skill judgment |
| 4 | main-session | derivation probe + gate runs |

## Open questions

- Fleet routine stand-up + existing-scheduler reconciliation execution —
/work-items backlog
  post-merge (Brief out-of-scope, trigger recorded).

## Decisions made (gate-passed)

| Decision | What it changes in the plan | Basis (evidence) |
|---|---|---|
| v1 roster confirmed at 10 classes (dead-code sweep stays out) | Phase
1 table + Phase 2 leaf count | D4's correction is evidence-backed
(RESEARCH-routine-catalog row 25: DET detect); D2's rule excludes DET
mechanically |
| Class tokens = kebab-case descriptive names (list in preamble) |
Phases 1–2 | Matches every shipped contract token set; one token per
catalog row |
| Hub-and-leaves under `reference/routines/` | Phase 1–2 paths | Same
layout decision as WP5's guardrail leaves (subdir per hub) |
| Cadence defaults: daily for queue-tending sweeps, weekly for the rest
| Phase 2 leaves | Proven manual patterns' observed frequency;
org-bindable values, not contract |
| Novel-class derivation as the Phase 4 acceptance probe | Phase 4 |
Brief acceptance criterion stated verbatim; cheapest mechanical proof of
the mapping rules |

## Handoff to implementation

### User-approval gates

- Any change to the D2 mapping rules or the v1 roster during
implementation → STOP
  (user-locked content).
- Any scope expansion beyond the four phases re-enters `/architect
review`.

### Execution shape ([EXEC-SHAPE] tagged)

Sequential 1→4, all main-session (table above). PLAN.md phase tags
advance in the same
commit as each phase.

### Mechanical work

Commit per phase on the implementation branch (suggest
`feat/autonomy-routines`); gates
re-run in full at Phase 4; PR body carries the derivation example +
near-duplicate audit
statement + this PLAN in a `<details>` block at close-out.

</details>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
kyle-sexton added a commit that referenced this pull request Jul 20, 2026
## Summary

Effort-end housekeeping for the AI-adoption-ladder effort: all seven
work packages are merged,
so the per-WP topic slices come out per the close-out convention
(publish the PLAN, then prune
the slice).

- WP4–WP7 PLANs were already spliced into their PR bodies at each
close-out (#372, #377,
  #600, #676).
- WP1–WP3 PLANs are now published the same way into #333, #343, and #356
(each merged PR body
gained a `PLAN (published at effort-end close-out)` details block), so
every slice's content
  has a durable home on its merged PR before the directory is removed.
- The WP map in `docs/topics/ai-adoption-ladder/index.md` now records
delivery PRs instead of
live slugs. The effort hub itself (index, design evidence, research
docs) stays.

No content is lost: every deleted PLAN.md is byte-recoverable from git
history and readable in
its merged PR's details block.

## Related

No linked issue (effort-end housekeeping; the WP map issue #239 is
already closed).

- #239 (WP map: AI adoption ladder — the effort this closes out)
- #333 / #343 / #356 (WP1–WP3 delivery PRs, PLANs published into their
bodies in this
  close-out)
- #372 / #377 / #600 / #676 (WP4–WP7 delivery PRs, PLANs published at
their own close-outs)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
kyle-sexton added a commit that referenced this pull request Jul 20, 2026
…ger row (#698)

## Summary

D1 sweep for #353: the WP1–WP7 PLAN out-of-package notes (mined from the
merged PR bodies #333/#343/#356/#372/#377/#600/#676) contained 31
deferred notes; every note that named no tracking issue and had no
contract-recorded trigger now has a durable home:

- **README roadmap** gains four trigger-gated rows: fleet guardrail
materializations (WP5), fleet routine stand-up + existing-scheduler
reconciliation (WP6), vendor-binding capability templates (WP6), cost
enforcement / hard spend caps (WP5, 3→4 trigger).
- **Trigger register** gains the second-binding-consumer cross-repo
drift check (WP1).
- **`reference/return-accounting.md`** records the per-work-class
precision-graduation deferral beside its band-stability rule (WP3).
- **CHANGELOG.md created** for the autonomy plugin (first entry;
0.1.0–0.7.0 history stays in the WP PR bodies) + version bump to 0.7.1 —
starts the CHANGELOG-parity posture #663 gates on.

All other mined notes were already covered: tracked issues (#351, #352,
#694#697), contract-recorded triggers (telemetry immutable-ID,
return-accounting expansion/revisit), delivered sibling WPs, resolved
`/architect` questions, or the user-held dormant triggers (runner build
T4, L3 backend, merge serialization, mid-run interrupt, org-enablement,
cross-team). Full disposition table lands as a comment on #353 at close.

Documentation only — no contract semantics change.

## Related

- #685 (effort-end prune that moved the PLAN records into the merged PR
bodies)
- #663 (CHANGELOG-parity gate this PR's new CHANGELOG.md starts
satisfying)
- WP delivery PRs mined: #333, #343, #356, #372, #377, #600, #676

Closes #353

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant