Skip to content

feat: lifecycle hooks - #1205

Merged
evanspearman merged 1 commit into
gominimal:mainfrom
evanspearman:lchooks
Aug 13, 2026
Merged

feat: lifecycle hooks#1205
evanspearman merged 1 commit into
gominimal:mainfrom
evanspearman:lchooks

Conversation

@evanspearman

@evanspearman evanspearman commented Aug 11, 2026

Copy link
Copy Markdown
Member

Resolves https://github.com/gominimal/inbox/issues/201

Summary

Sessions can now run scripts at their four transition points — on_activate,
on_attach, on_detach, on_destroy — declared either in a loadout or in a
project's [session] block. Hooks execute inside the running session by
joining its namespaces, so they see the same packages, variables, files, and
network the user does.

  • Declaration. Inline bodies or external script paths, each with its own
    timeout (default 60s, capped at 300s — a hook is arbitrary code, so an
    unbounded one could hold a session open). Hooks concatenate in contributor
    order and run in reverse for teardown, so a project sets up before your
    loadouts and tears down after them.
  • Interpreter. POSIX sh by default, or whatever a leading shebang names,
    parsed exactly as execve(2) would — so a hook can be fish or Python without
    behaving differently here than anywhere else.
  • Consent. A project's hooks are arbitrary code from someone else, so
    they run only once the user allow-lists the project in their
    [hooks] policy; a loadout's are the user's own file and are ungated. A
    package cannot declare hooks at all, and the gate denies any that appear.
  • Failure semantics. A failing on_activate fails the activation — a
    development environment whose setup script failed is not the environment
    that was asked for. The other three never block their transition: a session
    must always be attachable and always destroyable. Teardown additionally runs
    under one shared time budget across all its hooks.
  • Introspection. min session hooks <session> lists what a session
    carries and where each hook came from, read from the persisted composition
    snapshot so it answers for stopped sessions and survives a daemon restart.
  • Opt-out. min session activate --no-hooks drops them at composition
    time, so the session records that it has none rather than carrying hooks
    every later transition has to remember to skip.

Also lands the e2e coverage for the above, which found three bugs that unit
tests structurally could not: a project's external hook script could never
activate (the finalize gate demanded an upload the client does not send for
project-sourced scripts), on_destroy was silently skipped for any session
whose actor was idle (every session, after a daemon restart), and "Allow
permanent" stored the project path as a glob pattern, so a path containing
metacharacters either matched nothing or matched its siblings.

The session e2e's pty driver moved from Python to socat — see the dependency
note in the checklist below.

Testing

just e2e-native — the full session e2e against a host-native daemon,
including the new hooks blocks and the rewritten pty driver:

::group::lifecycle hooks: the four transitions
on_activate + shebang dispatch OK
on_attach (on the terminal) + on_detach (headless, after leaving) OK
on_destroy OK (ran, output captured, and a non-zero exit still destroyed)
::group::lifecycle hooks: external scripts
external hook script proof OK (staged, uploaded, resolved, ran)
::group::lifecycle hooks: refusals
gate refuses an un-allow-listed project, naming it OK
a failing on_activate aborts the activation OK
--no-hooks suppresses execution and composition OK
::group::lifecycle hooks: loadout-declared
loadout-declared hooks proof OK (ran, ungated)
::group::sandbox proof (interactive attach via pty: min add jq + run)
sandbox proof: in-sandbox 'min add jq' + run OK, orientation banner rendered (3041ms)
::group::lifecycle hooks: survive a daemon restart
hooks survive a daemon restart OK (listed, and still executable)
session e2e OK

Unit and integration tests across the touched crates:

$ cargo test -p minimald -p sessions -p minimal -p mfile
total passed: 980   (0 failed)

Static gates:

$ just clippy       # clean
$ just fmt-check    # clean
$ just lint-shell   # 27 script(s) passed shellcheck

Each regression test was confirmed to fail against the pre-fix behaviour
before being kept — e.g. reverting the destroy fix yields "destroying an idle
session skipped its on_destroy hook"
.

Not run: just e2e (the VM lane). This box has no /dev/kvm, so the
VM-backed path is unverified locally; the e2e is lane-agnostic and the native
lane exercises the identical script.

Checklist

  • Docs updated if behavior changed

    docs/reference/loadouts.md (hook semantics, interpreter/shebang rules,
    metadata variables, teardown budget), docs/reference/minimal-dot-toml.md,
    docs/reference/user-policy.md, docs/reference/cli-min.md,
    docs/concepts/loadouts.md, and AGENTS.md.

  • BREAKING CHANGE: footer present if this is a breaking change

    Not breaking. on_failure — an earlier spelling that never executed — is
    rejected by name with an error pointing at on_destroy, rather than being
    silently ignored.

Reviewer notes

  • New host dependency: socat. The e2e drives its interactive attaches
    through a real pty, because only a tty can answer the session-exit prompt.
    That driver was Python (an undeclared assumption that happens to hold on
    hosted runners); it is now scripts/e2e-attach-pty.sh. socat is present on
    stock Linux but not stock macOS — the self-hosted Apple Silicon runner
    needs brew install socat before the macOS lane will pass. Documented in
    AGENTS.md; I could not add an install step because .github/workflows/ is
    frozen.
  • crates/sessions/src/core/policy.rs gains HooksPolicy as a near-copy of
    PatchesPolicy. Left deliberately un-unified — the two are incidentally the
    same shape today and are expected to diverge.

Note

Add lifecycle hooks (on_activate, on_destroy, on_attach, on_detach) to sessions

  • Introduces four lifecycle hook transitions that run scripts inside the session at activation, destruction, attach, and detach; hooks are declared in loadout or project minimal.toml files and composed alongside vars and patches.
  • Adds a user-level [hooks] policy (allow/deny/ignore) that gates project-declared hooks; interactive mode prompts for approval and persists decisions to user_policy.toml; packages cannot declare hooks.
  • External hook scripts (file-backed, not inline) are validated and staged locally by the client then uploaded to the daemon via a new WorkspaceHookScriptsTarZst SSH subsystem before finalization.
  • min activate --no-hooks disables all hooks for a session; the choice persists across attach, detach, and destroy transitions.
  • Adds min session hooks CLI subcommand to list composed hooks for a running or idle session, with --json output support.
  • On-activate hook failures abort activation with a detailed error; detach/destroy hooks run best-effort under a shared 120s teardown budget and are skipped (with a warning) if a host cannot be started within 60s.
  • Risk: sessions with external hook scripts now require a hook-script upload step before FinalizeSession will succeed; missing the upload returns an InvalidInput error instructing the client to upload scripts first.

Macroscope summarized dfd3e6d.

Summary by CodeRabbit

  • New Features

    • Added lifecycle hooks for activation, attachment, detachment, and destruction, including ordering, timeouts, output handling, and failure reporting.
    • Added session hooks to view configured hooks in table or JSON format.
    • Added policy-based hook approval, denial, and ignore controls.
    • Added session activate --no-hooks, persisted across later transitions.
    • Added safe staging and execution of external hook scripts.
  • Documentation

    • Updated lifecycle hook configuration, policy, timeout, security, and migration guidance.
    • Added example configurations and end-to-end coverage.

@evanspearman
evanspearman requested a review from a team as a code owner August 11, 2026 20:17
@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b3eeaf8a-2018-4ba0-8e03-fa5929f9b2d3

📥 Commits

Reviewing files that changed from the base of the PR and between a291230 and 4b2f35f.

📒 Files selected for processing (1)
  • scripts/session-e2e.sh
🚧 Files skipped from review as they are similar to previous changes (1)
  • scripts/session-e2e.sh

📝 Walkthrough

Walkthrough

Lifecycle hooks now support four session transitions, policy approval, external-script staging, daemon execution, persistence, CLI inspection, and E2E validation. Activation supports --no-hooks, while session hooks reports the persisted composition.

Changes

Lifecycle hooks

Layer / File(s) Summary
Hook contracts and policy
crates/sessions/src/core/*, crates/sessions/src/wire/*, crates/sessions/src/client/*
Hooks support attach and detach transitions, timeout metadata, source-aware policies, pending verdicts, and compatible wire serialization.
Staging and execution
crates/minimal/src/*, crates/minimal-client/lib.rs, crates/minimald/src/*
External scripts are validated, staged, uploaded, and executed during session lifecycle transitions.
Persistence and CLI
crates/minimald-rpc/src/lib.rs, crates/sessions/src/*, docs/*, scripts/*
Hook configuration persists across sessions and restarts. The CLI exposes activation control and hook inspection. Tests cover policy, execution, staging, teardown, and restart behavior.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CLI
  participant SessionClient
  participant Minimald
  participant SessionHost
  CLI->>SessionClient: activate session and stage hook scripts
  SessionClient->>Minimald: upload scripts and finalize session
  Minimald->>SessionHost: execute lifecycle hooks
  SessionHost-->>Minimald: return hook outcomes
  Minimald-->>CLI: report activation or teardown result
Loading

Poem

A rabbit checks each hook in line,
Four transitions now run fine.
Policies guard each script and path,
Hosts record the lifecycle path.
Uploads settle, tests take flight—
The burrow runs its hooks just right.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The implementation executes hooks at lifecycle events and exposes outcomes through command output and persisted hook introspection [#201].
Out of Scope Changes check ✅ Passed The changes support lifecycle hooks through execution, policy, persistence, uploads, documentation, tests, and required end-to-end infrastructure.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Title check ✅ Passed The title clearly and concisely identifies the main change: adding lifecycle hooks.
Description check ✅ Passed The description includes the required summary, testing evidence, checklist updates, issue reference, and documentation and breaking-change notes.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai 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.

Actionable comments posted: 19

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
crates/minimald/src/session.rs (1)

2142-2157: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Restore the doc comment on patches_upload_lock.

The new composition() doc lines were inserted at the end of the existing doc block for patches_upload_lock. Lines 2142-2147 now document composition(), and patches_upload_lock at Line 2158 has no doc comment at all. The rendered docs describe the wrong function.

📝 Proposed fix
-    /// Handle to the per-session patches-upload lock, owned by the session
-    /// actor.
-    ///
-    /// Workspace patches are accumulated in a fixed per-session directory,
-    /// so this lock is used to serialize `WorkspacePatchesTarZst` RPCs so
-    /// they dont race and stomp each other.
     /// The session's composition, if it has one. See
     /// [`SessionMessage::GetComposition`].
     pub async fn composition(&self) -> Result<Option<Arc<Composition>>, std::io::Error> {
         let (send, recv) = oneshot::channel();
         let _ = self.0.send(SessionMessage::GetComposition(send)).await;
         recv.await.map_err(|_| {
             std::io::Error::new(std::io::ErrorKind::NotConnected, "session actor is gone")
         })
     }
 
+    /// Handle to the per-session patches-upload lock, owned by the session
+    /// actor.
+    ///
+    /// Workspace patches are accumulated in a fixed per-session directory,
+    /// so this lock is used to serialize `WorkspacePatchesTarZst` RPCs so
+    /// they dont race and stomp each other.
     pub async fn patches_upload_lock(&self) -> Result<Arc<Mutex<()>>, std::io::Error> {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/minimald/src/session.rs` around lines 2142 - 2157, Restore the
`patches_upload_lock` documentation immediately above that method, and move the
`composition()` documentation so it directly precedes `composition()`. Ensure
each doc comment describes its corresponding method and no longer leaves
`patches_upload_lock` undocumented.
crates/minimald/src/sessions.rs (1)

527-573: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Destroy now holds the manager mainloop for up to three minutes.

ManagerMessage::DeleteSession is served inline on Manager::handle_message, so the manager actor processes nothing else until hnd.destroy() returns. With hooks, that call can now:

  • launch a sandbox, bounded by HOOK_LAUNCH_TIMEOUT (60s, session.rs Line 440), and
  • run the destroy hooks, bounded by TEARDOWN_HOOK_BUDGET (120s, session.rs Line 429).

A single slow or wedged destroy therefore stalls List, GetRecord, GetSession, GetScreen, and CreateSession for every other session for up to three minutes. Before this change the same handler only awaited a host kill.

Consider serving the teardown off the mainloop, in the same style as GetWorkspaceDelta in session.rs Lines 856-871: resolve the actor on the mainloop, then move the destroy() and the record cleanup onto a spawned task that owns the responder.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/minimald/src/sessions.rs` around lines 527 - 573, Update the
ManagerMessage::DeleteSession handling to resolve or start the session actor on
the manager mainloop, then spawn a task that owns the responder and performs
hnd.destroy() plus record cleanup asynchronously. Preserve the existing fallback
deletion and error handling, following the GetWorkspaceDelta pattern so slow
teardown does not block handling other manager messages.
🧹 Nitpick comments (8)
scripts/session-e2e.sh (1)

864-866: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Two duplicated guard expressions couple these blocks implicitly.

hook_log_has is defined inside the block that starts at Line 467, under the condition [ -n "$SEED_DIR" ] || [ -n "$SEEDED_MFILE" ]. The restart assertion at Line 935 calls that function, but it is guarded by [ -n "$HOOK_RESTART_SID" ] instead. The call is safe today only because Line 865 repeats the same seed condition. If a later edit changes one guard, hook_log_has becomes an unbound command inside the restart block.

Set one flag when the hook proofs run, and gate all three sites on it.

♻️ Proposed refactor
+HOOKS_PROOFS=""
 if [ -n "$SEED_DIR" ] || [ -n "$SEEDED_MFILE" ]; then
+  HOOKS_PROOFS=1
 HOOK_RESTART_SID=""
-if [ -n "$SEED_DIR" ] || [ -n "$SEEDED_MFILE" ]; then
+if [ -n "$HOOKS_PROOFS" ]; then
   HOOK_SEED_DIR="$(mktemp -d /tmp/mnlp2.XXXXXX)"

Also applies to: 923-925

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/session-e2e.sh` around lines 864 - 866, Introduce a shared flag
indicating that hook proofs are initialized, set it when the seed condition
enables the hook-proof block containing hook_log_has, and replace the duplicated
seed-condition checks plus the HOOK_RESTART_SID-only assertion guard with this
flag at all three sites. Ensure hook_log_has is called only when the flag is
set, including the restart assertion path.
crates/sessions/src/wire/policy.rs (1)

93-119: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Add a round-trip test for WireHookVerdict.

WireVarVerdict and WirePatchVerdict each have a round-trip test in this module. WireHookVerdict is a new cross-process contract and has none. The existing round_trip helper makes the test small.

♻️ Proposed test
#[test]
fn hook_verdict_round_trips_all_variants() {
    let cases = [
        WireHookVerdict::Approved {
            id: PendingId::new(1),
        },
        WireHookVerdict::Denied {
            id: PendingId::new(2),
        },
        WireHookVerdict::Ignored {
            id: PendingId::new(3),
        },
    ];
    for v in cases {
        assert_eq!(round_trip(&v), v);
    }
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/sessions/src/wire/policy.rs` around lines 93 - 119, Add a test
alongside the existing verdict round-trip tests that constructs Approved,
Denied, and Ignored WireHookVerdict values with distinct PendingId values, then
passes each through the existing round_trip helper and asserts equality with the
original.
crates/sessions/src/core/policy.rs (1)

528-724: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add unit tests for HooksPolicy and ExpandedHooksPolicy.

VarsPolicy and PatchesPolicy each have a dedicated test block in this file. HooksPolicy has none. This gate decides arbitrary code execution, so the precedence rules deserve the same pinning.

Cover at least these cases:

  • deny wins over ignore for the same project root.
  • allow matches a project root and returns Decided(Allowed).
  • An unmatched project root returns NeedsApproval.
  • Source::UserLoadout returns Allowed and Source::Package returns Denied, independent of the pattern lists.
  • A [hooks] section round-trips through UserPolicy TOML, and an empty HooksPolicy is omitted on serialize.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/sessions/src/core/policy.rs` around lines 528 - 724, Add a dedicated
unit-test block covering HooksPolicy::expand_with and
ExpandedHooksPolicy::check: verify deny takes precedence over ignore, allow
yields Decided(Allowed), unmatched project roots yield NeedsApproval, and
UserLoadout/Package sources remain Allowed/Denied regardless of patterns. Also
test UserPolicy TOML round-tripping for a [hooks] section and confirm an empty
HooksPolicy is omitted during serialization, following the existing VarsPolicy
and PatchesPolicy test patterns.
crates/sessions/src/daemon/composer.rs (1)

293-305: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Include the denied hook description in ComposeError::Denied::what.

Use ProvenancedHook::hook() and LifecycleHook::description(), with "lifecycle hook" as the fallback when no description exists.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/sessions/src/daemon/composer.rs` around lines 293 - 305, Update the
V::Denied handling to build ComposeError::Denied::what from the pending
ProvenancedHook’s hook().description(), falling back to "lifecycle hook" when no
description exists; preserve the existing source lookup and InvalidWireItem
behavior for unknown IDs.
crates/minimal/src/task.rs (1)

310-314: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Consider a --no-hooks opt-out for min task run.

min task run now always enables lifecycle hooks. Activation hooks block activation on failure. A project with a broken or slow on_activate hook therefore blocks every task run, and the operator has no flag to bypass it. min session activate offers --no-hooks for exactly this case. Add the same flag to TaskRunArgs and pass !args.no_hooks to hooks_enabled, stage_loadout_hook_scripts, and compose_user_contribution.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/minimal/src/task.rs` around lines 310 - 314, Add a `no_hooks` opt-out
field to `TaskRunArgs`, expose the corresponding `--no-hooks` CLI flag, and
propagate its inverse (`!args.no_hooks`) to `hooks_enabled`,
`stage_loadout_hook_scripts`, and `compose_user_contribution` in the task-run
flow. Preserve hooks as enabled by default.
crates/minimal/src/loadouts.rs (1)

244-248: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the loadouts-directory path into one helper.

resolve_active_loadouts and stage_loadout_hook_scripts both build resolve_minimal_config_dir(global).join("loadouts"). The two must agree, because a loadout's script anchor is <loadouts_dir>/<name>/ next to its <name>.toml. If one call site changes, staging resolves against the wrong anchor and every external script fails with MissingAnchor. Add a single loadouts_dir(global) helper and call it from both.

♻️ Proposed helper
/// The directory holding `<name>.toml` loadout files and their
/// `<name>/` script directories.
fn loadouts_dir(global: &GlobalArgs) -> std::path::PathBuf {
    resolve_minimal_config_dir(global).join("loadouts")
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/minimal/src/loadouts.rs` around lines 244 - 248, Extract the repeated
loadouts path construction into a loadouts_dir helper returning
resolve_minimal_config_dir(global).join("loadouts"). Update both
resolve_active_loadouts and stage_loadout_hook_scripts to use this helper,
preserving the existing UTF-8 validation and ensuring both loadout files and
script anchors share the same directory.
crates/minimald/src/rpc.rs (1)

1340-1368: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Handle unsupported RENAME_EXCHANGE filesystems

When renameat2(..., RENAME_EXCHANGE) returns EINVAL, EOPNOTSUPP, or ENOSYS, use a documented non-atomic fallback (remove_dir_all(dir) followed by plain rename). Native deployments can place --minimal-state-dir on arbitrary filesystems, and the shared helper now covers hook uploads. Without this fallback, the missing HOOKS_READY_MARKER can block FinalizeSession for compositions that require staged hooks. Preserve the atomic path when supported.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/minimald/src/rpc.rs` around lines 1340 - 1368, Handle unsupported
RENAME_EXCHANGE errors in the swap_result match by treating EINVAL, EOPNOTSUPP,
and ENOSYS as fallback cases. Document the non-atomic fallback, remove the
existing dir with remove_dir_all, then plain-rename staging_dir into dir;
preserve the current atomic exchange and ENOENT paths unchanged.
crates/minimald/src/session.rs (1)

1346-1375: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Tear the activation host down when an activation hook fails.

The code launches a host at Line 1347 and keeps it in self.inner. On a hook failure the function returns Err at Line 1360, so the record stays Materializing. The session is then unattachable, because attach and context both gate on Active. The sandbox stays up until someone destroys the session.

Stop the host on the failure arm so a refused activation does not hold a sandbox open.

♻️ Proposed change
                     if let Some(failed) = outcomes.iter().find(|o| o.failed()) {
+                        // The session will not become `Active`, so the host
+                        // this launch minted has no one left to serve.
+                        self.stop_running(false).await;
                         return Err(std::io::Error::new(
                             std::io::ErrorKind::InvalidInput,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/minimald/src/session.rs` around lines 1346 - 1375, Update the
activation-hook failure branch in the session transition flow after
launch_host_for_hooks(LaunchPhase::Activating) and before returning the
InvalidInput error to tear down the activation host through the existing
host-shutdown mechanism. Ensure cleanup completes before returning Err, while
preserving the current failure message and preventing the sandbox from remaining
active.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/minimal-tui/src/rpc.rs`:
- Around line 256-259: Update dashboard session creation using
DashOptions.contribution so external UserLoadout hook scripts are staged and
uploaded before FinalizeSession, ensuring the daemon receives the required
.hooks_ready marker. Do not rely on hooks_enabled: false, since it only disables
project hooks; alternatively remove loadout hooks from the dashboard
contribution.

In `@crates/minimald/src/hooks.rs`:
- Around line 591-639: Move the blocking filesystem operations in read_script
and run_one off async runtime workers: replace the std::fs::read calls for
staged and workspace scripts and the std::fs::OpenOptions terminal open with
tokio::fs or tokio::task::spawn_blocking, propagating their errors while
preserving the existing fallback and timeout behavior.
- Around line 558-569: The timeout comment in the hook execution error branch
must not claim that no processes remain running: kill_on_drop only terminates
the direct interpreter child, while descendants may survive. Soften the comment
to describe only the direct child termination, or update the command
construction to create and terminate a process group (using the relevant
command-builder setup) so descendant cleanup is guaranteed; preserve the
existing on_destroy versus on_attach/on_detach lifecycle behavior.
- Around line 1274-1281: Move the doc-comment sentence “A composition with no
hooks for the event produces no outcomes” from
a_teardown_budget_is_shared_across_all_of_its_hooks to the no_hooks_is_a_no_op
test, placing it with the existing “and does nothing.” sentence. Keep the
teardown budget test documentation focused on shared hook budget behavior.
- Around line 515-541: Move the stdin write and child wait into the same timeout
in the hook execution flow, rather than completing write_all before starting
timeout. Concurrently feed body to child.stdin while wait_with_output drains
captured output, and preserve the non-capturing wait behavior; ensure the
timeout covers both operations so blocked writes terminate the hook.

In `@crates/minimald/src/session_host.rs`:
- Around line 1326-1353: Run on_attach hooks asynchronously outside the host
actor loop instead of awaiting run_hooks_for in Host::attach; update
run_hook_plan or the attach path to spawn a task that owns the snapshotted
HookPlan and preserves existing failure warnings, while keeping teardown hooks
on their current budgeted path.
- Around line 289-322: Update the lifecycle-hook logging in the hook iteration
around `comp.lifecycle_hooks()` and its `tracing::info!` call to remove the
claim that execution is deferred or has not occurred. Ensure the surrounding
comment and log message accurately reflect that hooks execute during
transitions, while preserving the existing hook metadata and ordering.

In `@crates/minimald/src/session.rs`:
- Around line 1807-1883: Update the superseded-host attach/teardown flow so the
`TeardownDueToSuperceded` path does not await `run_hooks_headless`/detach hooks
while `Host::attach` is waiting on `old_join_hnd`; either skip detach hooks for
`Superceded` or finish the old binding teardown before running them, while
preserving hook execution for other teardown events.

In `@crates/minimald/src/sessions.rs`:
- Around line 1205-1212: Repair the doc-comment boundaries around the tests
destroy_tears_down_a_running_session and
reading_a_composition_does_not_start_the_session: move the stranded “no host is
attached) tears the actor down and removes the record.” clause back into the
former test’s comment, and ensure the latter has only its own
composition-reading description. Keep each doc comment immediately attached to
its intended test.

In `@crates/minimald/src/sessions/composables.rs`:
- Around line 466-474: Normalize legacy project provenance during snapshot
loading before constructing the ProjectComposable: when Source::Project.path
contains the session workspace path, replace it with SessionRecord.project_path
so policy matching, script lookup, hook diagnostics, and
MINIMAL_HOOK_SOURCE_NAME use the declared project path. Preserve current
handling for already-normalized paths, and add a restart regression test
covering a legacy snapshot.

In `@crates/sessions/example_project/minimal.toml`:
- Around line 112-136: The hook documentation comment conflicts with the
configured commands: on_activate and on_detach use touch, write files, and no
script references MINIMAL_HOOK_EVENT. Update the comment block near the
lifecycle hook configuration to accurately describe the actual commands and
filesystem effects, or modify the commands to match the documented behavior,
while keeping the copy-source example internally consistent.

In `@crates/sessions/src/client/hookscripts.rs`:
- Around line 198-208: Update the anchor validation around anchor_meta to return
MissingAnchor only when symlink_metadata reports NotFound; propagate other
metadata I/O failures through the existing StageError::Io path, matching the
per-component walk’s error distinction. Keep successful directory validation and
non-directory handling unchanged.

In `@crates/sessions/src/core/source.rs`:
- Line 20: Update the intra-doc link in the surrounding documentation to
reference the existing check method on ExpandedPatchesPolicy rather than the
nonexistent PatchesPolicy::check symbol, preserving the intended policy-check
documentation.

In `@crates/sessions/src/wire/request.rs`:
- Around line 88-89: Update the documentation comment for the version field to
state that its default is LEGACY_COMPOSITION_SNAPSHOT_VERSION, matching
default_composition_version, and remove the outdated
COMPOSITION_SNAPSHOT_VERSION reference.
- Around line 20-32: Update load_composition to validate WireComposition.version
before invoking Composition::try_from, rejecting any version greater than
COMPOSITION_SNAPSHOT_VERSION with the established error handling path. Preserve
reconstruction for supported and legacy versions.

In `@docs/concepts/loadouts.md`:
- Around line 125-128: Update the hooks documentation near the session
transition hook descriptions to state that hooks are composed in contributor
order, with on_detach and on_destroy executed in reverse contributor order.
Clarify that hooks declared in the project [session] block are included in the
composed hook list.

In `@docs/reference/loadouts.md`:
- Around line 285-291: Update the teardown time-budget description to state that
each teardown transition runs its hooks under a single shared budget, with
on_detach and on_destroy receiving separate budgets; preserve the remaining
timeout and logging behavior.

In `@docs/reference/user-policy.md`:
- Around line 76-82: Add a `### [hooks]` subsection to the policy reference,
documenting hook pattern matching, exact paths, path identity, expansion
behavior, and `allow`/`deny`/`ignore` handling including undecided prompts.
Update “How a contribution is decided” to include hooks in the precedence rules
alongside variables and patches, using the existing schema terminology.

In `@scripts/e2e-attach-pty.sh`:
- Around line 92-101: Fix the elapsed-time accounting in the wait loop around
DEADLINE_SECS so elapsed advances by the actual 0.5-second sleep interval, or
otherwise compare against a wall-clock deadline in seconds. Preserve the
intended 240-second timeout and the existing prompt, return-code, and socat
termination checks.

---

Outside diff comments:
In `@crates/minimald/src/session.rs`:
- Around line 2142-2157: Restore the `patches_upload_lock` documentation
immediately above that method, and move the `composition()` documentation so it
directly precedes `composition()`. Ensure each doc comment describes its
corresponding method and no longer leaves `patches_upload_lock` undocumented.

In `@crates/minimald/src/sessions.rs`:
- Around line 527-573: Update the ManagerMessage::DeleteSession handling to
resolve or start the session actor on the manager mainloop, then spawn a task
that owns the responder and performs hnd.destroy() plus record cleanup
asynchronously. Preserve the existing fallback deletion and error handling,
following the GetWorkspaceDelta pattern so slow teardown does not block handling
other manager messages.

---

Nitpick comments:
In `@crates/minimal/src/loadouts.rs`:
- Around line 244-248: Extract the repeated loadouts path construction into a
loadouts_dir helper returning
resolve_minimal_config_dir(global).join("loadouts"). Update both
resolve_active_loadouts and stage_loadout_hook_scripts to use this helper,
preserving the existing UTF-8 validation and ensuring both loadout files and
script anchors share the same directory.

In `@crates/minimal/src/task.rs`:
- Around line 310-314: Add a `no_hooks` opt-out field to `TaskRunArgs`, expose
the corresponding `--no-hooks` CLI flag, and propagate its inverse
(`!args.no_hooks`) to `hooks_enabled`, `stage_loadout_hook_scripts`, and
`compose_user_contribution` in the task-run flow. Preserve hooks as enabled by
default.

In `@crates/minimald/src/rpc.rs`:
- Around line 1340-1368: Handle unsupported RENAME_EXCHANGE errors in the
swap_result match by treating EINVAL, EOPNOTSUPP, and ENOSYS as fallback cases.
Document the non-atomic fallback, remove the existing dir with remove_dir_all,
then plain-rename staging_dir into dir; preserve the current atomic exchange and
ENOENT paths unchanged.

In `@crates/minimald/src/session.rs`:
- Around line 1346-1375: Update the activation-hook failure branch in the
session transition flow after launch_host_for_hooks(LaunchPhase::Activating) and
before returning the InvalidInput error to tear down the activation host through
the existing host-shutdown mechanism. Ensure cleanup completes before returning
Err, while preserving the current failure message and preventing the sandbox
from remaining active.

In `@crates/sessions/src/core/policy.rs`:
- Around line 528-724: Add a dedicated unit-test block covering
HooksPolicy::expand_with and ExpandedHooksPolicy::check: verify deny takes
precedence over ignore, allow yields Decided(Allowed), unmatched project roots
yield NeedsApproval, and UserLoadout/Package sources remain Allowed/Denied
regardless of patterns. Also test UserPolicy TOML round-tripping for a [hooks]
section and confirm an empty HooksPolicy is omitted during serialization,
following the existing VarsPolicy and PatchesPolicy test patterns.

In `@crates/sessions/src/daemon/composer.rs`:
- Around line 293-305: Update the V::Denied handling to build
ComposeError::Denied::what from the pending ProvenancedHook’s
hook().description(), falling back to "lifecycle hook" when no description
exists; preserve the existing source lookup and InvalidWireItem behavior for
unknown IDs.

In `@crates/sessions/src/wire/policy.rs`:
- Around line 93-119: Add a test alongside the existing verdict round-trip tests
that constructs Approved, Denied, and Ignored WireHookVerdict values with
distinct PendingId values, then passes each through the existing round_trip
helper and asserts equality with the original.

In `@scripts/session-e2e.sh`:
- Around line 864-866: Introduce a shared flag indicating that hook proofs are
initialized, set it when the seed condition enables the hook-proof block
containing hook_log_has, and replace the duplicated seed-condition checks plus
the HOOK_RESTART_SID-only assertion guard with this flag at all three sites.
Ensure hook_log_has is called only when the flag is set, including the restart
assertion path.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0cbdd9bb-b799-45a6-8a3f-7231f6c51fe7

📥 Commits

Reviewing files that changed from the base of the PR and between fdf699a and 6926926.

📒 Files selected for processing (55)
  • .minimal/minimal.toml
  • AGENTS.md
  • crates/mfile/src/lib.rs
  • crates/mfile/src/package_composable.rs
  • crates/mfile/src/project_composable.rs
  • crates/minimal-client/src/lib.rs
  • crates/minimal-tui/src/rpc.rs
  • crates/minimal-tui/tests/snapshots.rs
  • crates/minimal/src/lib.rs
  • crates/minimal/src/loadouts.rs
  • crates/minimal/src/prompt.rs
  • crates/minimal/src/task.rs
  • crates/minimal/tests/cli.rs
  • crates/minimald-rpc/src/lib.rs
  • crates/minimald/src/hooks.rs
  • crates/minimald/src/lib.rs
  • crates/minimald/src/main.rs
  • crates/minimald/src/nsenter.rs
  • crates/minimald/src/rpc.rs
  • crates/minimald/src/session.rs
  • crates/minimald/src/session_host.rs
  • crates/minimald/src/sessions.rs
  • crates/minimald/src/sessions/composables.rs
  • crates/minimald/src/store.rs
  • crates/minimald/src/test_harness.rs
  • crates/minvmd/examples/exec.rs
  • crates/sessions/docs/COMPOSITION.md
  • crates/sessions/example_project/minimal.toml
  • crates/sessions/src/client/handler.rs
  • crates/sessions/src/client/hookscripts.rs
  • crates/sessions/src/client/mod.rs
  • crates/sessions/src/core/compose.rs
  • crates/sessions/src/core/expansion.rs
  • crates/sessions/src/core/hooks.rs
  • crates/sessions/src/core/lifecyclehook.rs
  • crates/sessions/src/core/loadout.rs
  • crates/sessions/src/core/policy.rs
  • crates/sessions/src/core/primitives.rs
  • crates/sessions/src/core/source.rs
  • crates/sessions/src/daemon/composer.rs
  • crates/sessions/src/lib.rs
  • crates/sessions/src/store.rs
  • crates/sessions/src/wire/policy.rs
  • crates/sessions/src/wire/primitives.rs
  • crates/sessions/src/wire/request.rs
  • crates/sessions/tests/client_flow1.rs
  • crates/sessions/tests/client_flow2.rs
  • docs/concepts/loadouts.md
  • docs/reference/cli-min.md
  • docs/reference/loadouts.md
  • docs/reference/minimal-dot-toml.md
  • docs/reference/user-policy.md
  • scripts/e2e-attach-pty.py
  • scripts/e2e-attach-pty.sh
  • scripts/session-e2e.sh
💤 Files with no reviewable changes (1)
  • scripts/e2e-attach-pty.py

Comment thread crates/minimal-tui/src/rpc.rs
Comment thread crates/minimald/src/hooks.rs
Comment thread crates/minimald/src/hooks.rs
Comment on lines +591 to +639
fn read_script(
ctx: &HookContext<'_>,
source: &Source,
script: &HookScript,
) -> Result<Vec<u8>, String> {
match script.body() {
HookScriptBody::Inline(body) => Ok(body.clone().into_bytes()),
HookScriptBody::External(rel) => {
let staged = staged_script_path(source, rel)
.ok_or_else(|| "this source cannot declare hook scripts".to_string())?;
// Both joins below are made with a path derived from wire
// data, so neither is allowed to climb out of the directory
// it is anchored to. `staged_script_path` and
// `ConfigRelPath` each refuse the components that would let
// it; this is the check at the point of use, where the
// consequence actually lands — reading a file the session
// was never given.
if !contained_relative_path(staged.as_std_path())
|| !contained_relative_path(rel.as_utf8_path().as_std_path())
{
return Err(format!(
"hook script path `{staged}` escapes the session's staged-hooks directory"
));
}
let staged_path = ctx.hooks_dir.as_utf8_path().join(&staged);
match std::fs::read(staged_path.as_std_path()) {
Ok(b) => return Ok(b),
Err(e) if e.kind() != std::io::ErrorKind::NotFound => {
return Err(format!("reading `{staged_path}`: {e}"));
}
Err(_) => {}
}
// Not staged. For a project, the script may have arrived
// with the workspace tree instead.
if matches!(source, Source::Project { .. }) {
let in_tree = ctx.workspace.as_utf8_path().join(rel.as_utf8_path());
return std::fs::read(in_tree.as_std_path()).map_err(|e| {
format!(
"script not staged, and reading `{in_tree}` from the workspace \
failed: {e}"
)
});
}
Err(format!(
"no staged script at `{staged_path}` (was the hook-script upload skipped?)"
))
}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Move the script reads off the async worker.

read_script calls std::fs::read on Line 616 and Line 627. run_one calls it from an async context on Line 456. A stalled mount under the staged-hooks directory or the workspace then stalls a runtime worker thread, and the hook's own timeout cannot bound it because the timeout starts later.

The same applies to the terminal open on Line 488, which uses std::fs::OpenOptions.

Use tokio::fs or tokio::task::spawn_blocking for both.

Based on learnings: "In this Rust repo, avoid blocking filesystem work in async contexts (e.g., don't call std::fs directly from async tasks). If you need filesystem traversal/walks for diagnostic collectors, run that work in a blocking thread via tokio::task::spawn_blocking".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/minimald/src/hooks.rs` around lines 591 - 639, Move the blocking
filesystem operations in read_script and run_one off async runtime workers:
replace the std::fs::read calls for staged and workspace scripts and the
std::fs::OpenOptions terminal open with tokio::fs or
tokio::task::spawn_blocking, propagating their errors while preserving the
existing fallback and timeout behavior.

Source: Learnings

Comment thread crates/minimald/src/hooks.rs Outdated
Comment thread crates/sessions/src/wire/request.rs
Comment thread docs/concepts/loadouts.md
Comment on lines 125 to +128
Hooks are scripts declared to run at session transition points: `on_activate`
when the session comes up, `on_destroy` when it is torn down, and `on_failure`
when activation fails. Declare them to warm a cache, fetch grammars, or clean
up after a failed start:
when the session comes up, `on_destroy` when it is torn down, `on_attach`
when you connect to it, and `on_detach` when you disconnect. Declare them to
warm a cache, fetch grammars, or clean up when you step away:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Document teardown ordering and project hook composition.

State that hooks combine in contributor order, but on_detach and on_destroy run in reverse contributor order. Also state that project hooks declared in the project [session] block participate in the composed hook list. This prevents users from relying on declaration-order teardown or overlooking project hooks.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/concepts/loadouts.md` around lines 125 - 128, Update the hooks
documentation near the session transition hook descriptions to state that hooks
are composed in contributor order, with on_detach and on_destroy executed in
reverse contributor order. Clarify that hooks declared in the project [session]
block are included in the composed hook list.

Comment thread docs/reference/loadouts.md Outdated
Comment thread docs/reference/user-policy.md
Comment thread scripts/e2e-attach-pty.sh Outdated
Comment on lines +92 to +101
while [ "$elapsed" -lt "$DEADLINE_SECS" ]; do
if [ -z "$answered" ] && grep -qi -- "$EXIT_PROMPT" "$out" 2>/dev/null; then
printf '%s' "$answer_key" >&9
answered=1
fi
[ -s "$rc" ] && break
kill -0 "$socat_pid" 2>/dev/null || break
sleep 0.5
elapsed=$((elapsed + 1))
done

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

The deadline expires at 120 seconds, not the 240 the name states.

Each iteration sleeps 0.5 seconds but adds 1 to elapsed. So elapsed counts half-seconds while DEADLINE_SECS names seconds. The loop gives up after 120 seconds of wall-clock time.

The comment at Lines 84-86 states the wait must tolerate a slow VM sandbox bring-up. A slow lane that needs more than 120 seconds exits the loop early, records no status, and the script returns exit 1 at Line 118. The caller then reports a failed attach instead of a timeout.

🐛 Proposed fix
 answered=""
 elapsed=0
-while [ "$elapsed" -lt "$DEADLINE_SECS" ]; do
+# Half-second polls, so the bound is in half-seconds too.
+deadline_ticks=$((DEADLINE_SECS * 2))
+while [ "$elapsed" -lt "$deadline_ticks" ]; do
   if [ -z "$answered" ] && grep -qi -- "$EXIT_PROMPT" "$out" 2>/dev/null; then
     printf '%s' "$answer_key" >&9
     answered=1
   fi
   [ -s "$rc" ] && break
   kill -0 "$socat_pid" 2>/dev/null || break
   sleep 0.5
   elapsed=$((elapsed + 1))
 done
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
while [ "$elapsed" -lt "$DEADLINE_SECS" ]; do
if [ -z "$answered" ] && grep -qi -- "$EXIT_PROMPT" "$out" 2>/dev/null; then
printf '%s' "$answer_key" >&9
answered=1
fi
[ -s "$rc" ] && break
kill -0 "$socat_pid" 2>/dev/null || break
sleep 0.5
elapsed=$((elapsed + 1))
done
while [ "$elapsed" -lt "$DEADLINE_SECS" ]; do
if [ -z "$answered" ] && grep -qi -- "$EXIT_PROMPT" "$out" 2>/dev/null; then
printf '%s' "$answer_key" >&9
answered=1
fi
[ -s "$rc" ] && break
kill -0 "$socat_pid" 2>/dev/null || break
sleep 0.5
elapsed=$((elapsed + 1))
done
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/e2e-attach-pty.sh` around lines 92 - 101, Fix the elapsed-time
accounting in the wait loop around DEADLINE_SECS so elapsed advances by the
actual 0.5-second sleep interval, or otherwise compare against a wall-clock
deadline in seconds. Preserve the intended 240-second timeout and the existing
prompt, return-code, and socat termination checks.

@evanspearman
evanspearman force-pushed the lchooks branch 2 times, most recently from 6232699 to b28b65f Compare August 11, 2026 21:45

@coderabbitai coderabbitai 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.

🧹 Nitpick comments (3)
crates/minimal/src/lib.rs (3)

2622-2640: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Handle an inline body whose first line is blank.

render_hook_script takes only the first line. If the script starts with a blank line or a shebang-free leading newline, the row shows an empty body with a trailing ellipsis. Use the first non-empty line instead.

♻️ Proposed change
         WireHookScript::Inline { body, timeout_secs } => {
-            let first = body.lines().next().unwrap_or("").trim();
-            let shown = if body.lines().count() > 1 {
+            let mut lines = body.lines().map(str::trim).filter(|l| !l.is_empty());
+            let first = lines.next().unwrap_or("");
+            let shown = if lines.next().is_some() {
                 format!("{first} …")
             } else {
                 first.to_string()
             };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/minimal/src/lib.rs` around lines 2622 - 2640, Update
render_hook_script’s inline-body preview to select the first non-empty trimmed
line instead of always using body.lines().next(), while preserving the ellipsis
behavior for multi-line bodies and the existing empty preview when no non-empty
line exists.

2585-2603: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider printing the events in lifecycle order and adding a header row.

The loop prints on_activate, on_destroy, on_attach, on_detach. on_destroy appears before the attach and detach events, so the row order does not match the session lifecycle order described in the doc comment. A short header row would also make the unlabeled columns (event, type, timeout, source) readable.

♻️ Proposed ordering change
         for (event, script) in [
             ("on_activate", hook.on_activate.as_ref()),
-            ("on_destroy", hook.on_destroy.as_ref()),
             ("on_attach", hook.on_attach.as_ref()),
             ("on_detach", hook.on_detach.as_ref()),
+            ("on_destroy", hook.on_destroy.as_ref()),
         ] {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/minimal/src/lib.rs` around lines 2585 - 2603, Update the hook-printing
loop to emit events in lifecycle order: on_activate, on_attach, on_detach, then
on_destroy. Add a concise header row before iterating hooks that labels the
event, type, timeout, and source columns, while preserving the existing
description and body output.

240-249: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add parse tests for session hooks and --no-hooks.

The file tests setup-zed parsing but not the two new surfaces. Add tests that min session hooks <session> --json parses into HooksArgs, and that no_hooks defaults to false and is set by --no-hooks. These tests protect the flag that disables hook execution for the whole session lifetime.

Also applies to: 489-497

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/minimal/src/lib.rs` around lines 240 - 249, Add parser tests covering
the new session hook CLI surfaces: verify `session hooks <session> --json`
parses into `HooksArgs` with the session and JSON flag set, and verify
`no_hooks` defaults to false and becomes true when `--no-hooks` is provided.
Place these alongside the existing `setup-zed` parsing tests and use the
existing argument-parsing test conventions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@crates/minimal/src/lib.rs`:
- Around line 2622-2640: Update render_hook_script’s inline-body preview to
select the first non-empty trimmed line instead of always using
body.lines().next(), while preserving the ellipsis behavior for multi-line
bodies and the existing empty preview when no non-empty line exists.
- Around line 2585-2603: Update the hook-printing loop to emit events in
lifecycle order: on_activate, on_attach, on_detach, then on_destroy. Add a
concise header row before iterating hooks that labels the event, type, timeout,
and source columns, while preserving the existing description and body output.
- Around line 240-249: Add parser tests covering the new session hook CLI
surfaces: verify `session hooks <session> --json` parses into `HooksArgs` with
the session and JSON flag set, and verify `no_hooks` defaults to false and
becomes true when `--no-hooks` is provided. Place these alongside the existing
`setup-zed` parsing tests and use the existing argument-parsing test
conventions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 804f0308-73d8-4013-b349-21c6d9a7a2d5

📥 Commits

Reviewing files that changed from the base of the PR and between 6232699 and b28b65f.

📒 Files selected for processing (14)
  • crates/minimal-client/src/lib.rs
  • crates/minimal/src/lib.rs
  • crates/minimal/tests/cli.rs
  • crates/minimald-rpc/src/lib.rs
  • crates/minimald/src/rpc.rs
  • crates/minimald/src/test_harness.rs
  • crates/minvmd/examples/exec.rs
  • crates/minvmd/tests/minimald_session_integration.rs
  • crates/sessions/src/lib.rs
  • crates/sessions/src/store.rs
  • crates/sessions/src/wire/policy.rs
  • crates/sessions/src/wire/primitives.rs
  • crates/sessions/src/wire/request.rs
  • crates/sessions/tests/client_flow1.rs
🚧 Files skipped from review as they are similar to previous changes (13)
  • crates/minvmd/tests/minimald_session_integration.rs
  • crates/minimald/src/test_harness.rs
  • crates/minvmd/examples/exec.rs
  • crates/sessions/tests/client_flow1.rs
  • crates/sessions/src/wire/policy.rs
  • crates/sessions/src/lib.rs
  • crates/sessions/src/store.rs
  • crates/minimal/tests/cli.rs
  • crates/minimald-rpc/src/lib.rs
  • crates/sessions/src/wire/request.rs
  • crates/minimal-client/src/lib.rs
  • crates/sessions/src/wire/primitives.rs
  • crates/minimald/src/rpc.rs

@evanspearman
evanspearman force-pushed the lchooks branch 5 times, most recently from 32ff526 to 6870012 Compare August 12, 2026 17:25
Comment thread crates/minimal/src/loadouts.rs Outdated
@evanspearman
evanspearman merged commit 1a1025e into gominimal:main Aug 13, 2026
29 checks passed
bryan-minimal added a commit that referenced this pull request Aug 13, 2026
…merge main)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
bryan-minimal added a commit that referenced this pull request Aug 13, 2026
…erge

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.

2 participants