Hook processes are fire-and-forget: the binary's exit drain acknowledges
spawns only, so the STARTED writer can land after the terminal one. The
macro-bracketing tests waited only for the terminal marker and then
asserted STARTED's count immediately, flaking on Windows CI and loaded
Linux runners. The bounded wait now covers every expected marker before
the exact-count and absence assertions run; the probe script's scenario K
comment is corrected to state the spawn-ack-only contract it already
waits around.
Scenario K pins the global_hooks wildcard forms (exact, <event>.*, *.<name>, *.*) and partial-glob inertness against a real binary; Scenario L proves --macro without --agent fires zero agent.* events.
- strip interior NUL bytes from hook env values in fire_resolved so
agent-authored text (e.g. escalation questions) cannot suppress a hook
via a failed spawn; raise the spawn-failure log to warn!
- make Supervisor::has_active_tasks recurse into child supervisors,
mirroring cancel_recursive, so a prompt ctrl-c that cancels only a
grandchild still latches as an interruption
- document the signal-first vs error-first classification asymmetry at
top_level_agent_finished / SpawnResultHooks::fire and the deliberate
REPL teardown cancel; pin ctrlc-with-no-error => agent.interrupted
- probe Scenario J: pty-driven ctrl-c during a live headless --agent run
fires exactly one agent.interrupted with no COYOTE_AGENT_ERROR
Ctrl-C at the reedline prompt no longer latches the abort signal
unconditionally: latch_prompt_interrupt only marks an interruption when
the supervisor had live agents/jobs to cancel (new
Supervisor::has_active_tasks), so the advertised exit path (ctrl-c at an
idle prompt, then ctrl-d) fires agent.completed at teardown instead of
agent.interrupted. Mid-turn ctrl-c still latches directly via
wait_user_interrupt/poll_abort_signal, preserving the
interrupted-then-exit classification.
Also: probe-lane tests for escalation.answered single-fire,
escalation question env truncation, agent.failed inverse gate, and
hooks probe Scenario I (session.started vs session.resumed); comment
notes on escalation.answered semantics and the agent__cancel blind
spot.
Extends the .gitattributes eol=lf guard to assets/agents/** (33 embedded
shell scripts share the CRLF failure mode the hooks rules fix) and
scripts/*.sh. Adopts the probe-authored Scenario H: black-box checks of
the installed log-events.sh (XDG default, 0600, UTC header, secret
filter, symlink refusal) and notify.sh (control-byte-stripped fallback),
plus non-executable .builtin-manifest.
- one shared exec-bit predicate (Language-recognized extensions => 0755) used by
every hook/agent installer; non-script files in hooks dirs stay non-executable
- mixin discovery scans the global hooks dir and each agent's hooks dir;
.builtin-manifest is never scanned
- NTFS 8.3 short-name aliases rejected as builtin-manifest entries
- hook-script install announcements restricted to real hook locations
- COYOTE_HOOKS_DIR override warning compares canonicalized paths
- embedded assets decode via checked from_utf8 instead of from_utf8_unchecked
- log-events.sh defaults under ${XDG_STATE_HOME:-$HOME/.local/state}/coyote/hooks.log
keeping 0600/symlink-refusal/secret-filter hardening; notify.sh tty fallback strips
control bytes; both use UTC ISO-8601 timestamps
- payload files orphaned by a write failure are unlinked; stale coyote-hook-*.json
payloads swept best-effort at startup
- sweep_removed_agent_hooks never follows symlinked agent dirs
- usage-probe-hooks.sh replaces bare sleeps with a bounded poll, re-verifies 0755 on
restored scripts, and covers roles/agent hook locations
- TestConfigDirGuard hoisted into crate::testing
- .gitattributes pins LF line endings for bundled scripts
hooks is now a reserved bundle name.
- Thread an explicit InstallMode (Skip/Prompt/Force) through the hook
installers: startup install_builtins() is pinned to Skip and never
prompts (test-pinned on both mocked-TTY and TTY-less paths);
--install-builtins hooks runs Prompt mode across all three hook
locations (global hooks dir, roles/hooks, agents/<name>/hooks) under
one shared keep-all/replace-all sticky scope; force paths keep
unconditional overwrite semantics.
- Hoist the bundle resolve_conflict/StickyMode/ConflictAction prompt
into shared config::conflict with a NonInteractive policy: bundle
installs keep their non-TTY bail, builtin hook refreshes resolve
non-TTY conflicts to keep. Bundle UX preserved byte-for-byte,
pinned by tests.
- Extend .builtin-manifest reconcile to the resolved top-level hooks
dir (shipped set from HookAssets::iter()); malformed-manifest
fail-safe (zero deletions) preserved and test-pinned.
- Sweep removed builtin agents' hooks: agent dirs absent from the
embed with a hooks/.builtin-manifest reconcile against an empty
shipped set; empty hooks dirs are removed, user files and
manifest-less agent dirs are never touched.
- Update usage-probe-hooks.sh Scenario C to the new non-interactive
keep contract and add black-box Scenarios E-G covering role/agent
hook reconcile, top-level manifest fail-safety, and the
removed-agent sweep.
The look-don't-touch bypass was keyed on load_with_interpolation's
pre-existing bool, which mutating early paths (--authenticate, --auth-mcp,
mcp manage, vault flags) already passed as true meaning only 'skip vault
interpolation' — so they silently lost their first-run config creation.
Split the parameter: skip_interpolation keeps the historical meaning,
a distinct inspection flag alone gates the missing-config default/wizard
bypass.
inspection_only is now computed from an explicit read-only whitelist:
--sync-models is excluded (it downloads and writes models-override.yaml)
and returns to the bootstrap/strict path, keeping only its historical
interpolation-skip and vault leniency. mcp/vault inspection sub-conditions
are hoisted and shared so mutating flag combos never inherit the bypass.
Coverage: sandbox vault contract tests, a sync-models bootstrap-path pin,
a --sync-models scenario in usage-probe-inspection.sh, and a PTY wizard
regression probe (scripts/usage-probe-wizard.sh).
- Skip the builtins bootstrap, function-binary build, and tool-scope
rebuild for inspection-only runs (--info/--list-*/--mcp-list/
--list-secrets without --agent/--role): a pristine COYOTE_CONFIG_DIR
now stays byte-empty (was 269 files written before dispatch)
- Tolerate a model-less default config on the inspection surface via
AppConfig::from_config_lenient + Model::default() in bootstrap;
strict 'No available model' behavior unchanged for the REPL path and
all mutating mcp/vault flags
- --list-secrets degrades to an rc=0 informational message in sandbox
mode (mutating vault flags still fail there), pinned by test
- Add the 041.7-4 regression test: a malformed COYOTE_<NAME>_GLOBAL_TOOLS
env override logs the debug message and leaves global_tools untouched
- Adopt scripts/usage-probe-inspection.sh (10 flags: rc=0 + zero writes,
output required only for --info) and add binary-level
tests/inspection_flags.rs, incl. the agent+--info still-bootstraps pin
Black-box scenarios against a built binary with dry_run configs: hook
cadence + env scoping, runtime inertness of failing hooks, --info firing
zero agent.* events, --install-builtins hooks install/force/cold-start
semantics, and end-to-end firing of the installed log-events.sh via the
documented relative-path wiring.
Add a hidden --generate-models flag that rebuilds models.yaml from
models.dev and OpenRouter merged with the hand-owned quirks overlay,
regenerate the committed catalog with it (393 -> 1090 models), and
retire the Python updater. GitHub becomes fully quirks-owned since
upstream only tracks Copilot ids, legacy ids upstream dropped are
preserved as hand-owned quirks entries, and non-chat model families
(image/tts/audio/realtime) are excluded from generation.