Hooks¶
13 hook events fire across the autonomous loop and the lifecycle commands. Your plugin declares the events it handles in [hooks]; the framework calls the named function with a typed payload.
Hook events¶
| Event | Fired by | Payload (in) | Return (out) | Default action |
|---|---|---|---|---|
init.before | samuel init | {project_root, force, minimal} | {ok} | none |
init.after | samuel init | {project_root, plugins_installed[]} | {ok} | none |
sync.before | samuel sync | {project_root, dry_run} | {ok, skip?} | none |
sync.after | samuel sync | {project_root, files_written[]} | {ok} | none |
before:loop | samuel run start | {prd_path, max_iterations} | {ok} | acquire lock, log start |
after:loop | samuel run start | {iterations_run, exit_reason} | {ok} | release lock, log summary |
before:iteration | each iteration | {iteration, task} | {ok} | reload prd.toon |
after:iteration | each iteration | {iteration, task, outcome} | {ok} | append progress.md |
iteration.gate | each iteration | {iteration, consec_fails, queue_size} | {continue} | enforce caps |
context.snapshot | each iteration | {project_root} | {toon_blob} | walk repo, emit snapshot |
context.progress | each iteration | {progress_path} | {md_blob} | rotate at 500 lines |
context.task | each iteration | {task} | {toon_blob} | impl vs discovery shape |
context.extra | each iteration | {task} | {extras[]} | none — plugin-only |
before:agent.invoke | per agent call | {agent, prompt, env, sandbox} | {agent, env, sandbox} | apply env allowlist |
agent.invoke | per agent call | {agent, prompt, env, sandbox} | {exit_code, stdout, stderr} | shell out |
after:agent.invoke | per agent call | {exit_code, stdout, stderr} | {ok} | parse exit |
quality.check | post-iteration | {commands[]} | {ok, failures[]} | run commands sequentially |
(Events init.* and sync.* are lifecycle events; the rest are loop events.)
Declaring a hook¶
In samuel-plugin.toml:
[hooks]
"sync.after" = "mirror_claude_md"
"context.extra" = "inject_design_doc"
"quality.check" = "run_security_scan"
The value is the exported function name in your WASM/OCI entrypoint, or the script path (relative to the plugin root) for skill plugins.
Strict, timeout, order¶
User overrides in samuel.toml can tighten or relax handler behaviour:
[methodology.ralph.hooks."quality.check"]
handlers = ["samuel-security:run_security_scan", "default", "samuel-lint:check"]
strict = true
timeout = "60s"
handlersorders the chain explicitly.defaultis the built-in.strict = trueaborts the loop on any handler failure.timeoutis per-handler — exceeding it kills the handler and treats it as failed.
When no override is present, handler order is built-in default → plugins (manifest declaration order).
Payload + return contracts¶
Payloads are JSON over the WASM host ABI / gRPC bridge. Plugins MUST:
- Treat unknown payload keys as forward-compatible (don't error).
- Return all required keys in the response envelope (
{ok}is the minimum;ok = falseplus areasonstring is a failure). - Stay within declared capabilities — calling
samuel.fs_writewithoutfilesystem.writegranted is denied at the host boundary, not at validate time.
Return-value composition¶
When multiple handlers fire for the same event:
- Bool returns (
{ok}) — all must beok = truefor the event to succeed. - Single-blob returns (
context.snapshot,context.progress,context.task) — last writer wins; user override usually pins which plugin owns the slot. - List-merge returns (
context.extra,quality.check.failures) — concatenated across handlers. - Transform chains (
before:agent.invoke) — each handler receives the previous handler's output as input; chain order matters.
See RFD 0004 for the dispatch implementation.