Files
pbakaus_impeccable/crates/hook/src/hook.rs
T
a3bb21cbde Address review: one Go per tab, declines for a granted miss, and grace per overlay
Four review threads on the agent-target protocol and the hook stand-down.

Overlay: a tab acting on one target is busy for every other target
(`agent_target_in_flight`), so two held generate requests can never both
be claimed by one tab and the second Go can never overwrite the session
the first one minted. Every exit from actOnAgentTarget ends the acting
state, and teardown clears the target ledger, so a Go that never happened
does not refuse the next connection's targets. A miss after a granted
claim now declines (handing the lease back so another page or a remount
can serve) instead of posting a result that ended the request for every
tab.

Hook: the live-preview marker probe runs before the per-session edit cap,
so a file already past the cap stands down for a variants wrap instead
of emitting the suppression notice.

Server: each overlay's first no_match word extends the resolution grace
by the full window (its watch re-reports do not), so an overlay that
reports after another page's grace lapsed still gets its late-mount
watch instead of completing the roll call with a no_match verdict.

Tests: a Rust and a Node protocol case for the late overlay's grace, a
hook case for the cap-then-wrap order, and contract pins for the busy
check, the decline on a granted miss, and the teardown clear.

Written with AI assistance (Claude).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-15 05:45:49 +05:00

902 lines
33 KiB
Rust

//! JS: skill/scripts/hook.mjs (`impeccable hook`) and hook-lib.mjs
//! `runHook` / `runStopHook`: the PostToolUse per-edit pass and the Stop deep
//! pass. Always exits 0; stdout is one JSON document or nothing.
use impeccable_core::findings::Finding;
use impeccable_core::js;
use serde_json::{Map, Value};
use std::collections::HashMap;
use crate::hook_lib::*;
use crate::stop_baseline;
use crate::util::{
exists, iso_now, jsp, node_read_error, now_ms, str_field, truthy_value, utf16_len,
};
/// JS `runHook` / `runStopHook` result: `{ exitCode: 0, stdout, audit }`.
pub struct RunResult {
pub stdout: String,
pub audit: Map<String, Value>,
}
fn ms_since(started: f64) -> Value {
Value::from((now_ms() - started).max(0.0) as u64)
}
fn with(audit: &Map<String, Value>, extra: Vec<(&str, Value)>) -> Map<String, Value> {
let mut out = audit.clone();
for (k, v) in extra {
out.insert(k.to_string(), v);
}
out
}
fn result(audit: &Map<String, Value>, extra: Vec<(&str, Value)>) -> RunResult {
RunResult {
stdout: String::new(),
audit: with(audit, extra),
}
}
/// Parse stdin the way `runHook` does: `Err` for malformed JSON, `Ok(None)`
/// for a parsed non-object.
fn parse_event(stdin: &str) -> Result<Option<Map<String, Value>>, ()> {
match serde_json::from_str::<Value>(stdin) {
Ok(Value::Object(o)) => Ok(Some(o)),
Ok(_) => Ok(None),
Err(_) => Err(()),
}
}
/// `event.session_id || 'unknown'` as the cache key; `None` when absent.
fn session_id_of(event: &Map<String, Value>) -> Option<Value> {
event
.get("session_id")
.filter(|v| truthy_value(Some(v)))
.cloned()
}
fn session_key(v: &Option<Value>) -> String {
match v {
Some(v) => crate::util::js_string(v),
None => "unknown".to_string(),
}
}
/// JS: runHook({ stdinJson, env, cwd })
pub fn run_hook(rt: &Runtime, stdin: &str) -> RunResult {
let mut audit: Map<String, Value> = Map::new();
audit.insert("ts".into(), Value::String(iso_now()));
audit.insert("event".into(), Value::String("PostToolUse".into()));
if depth_is_set(rt.env("IMPECCABLE_HOOK_DEPTH")) || depth_is_set(rt.env("CLAUDE_HOOK_DEPTH")) {
return result(
&audit,
vec![
("reentrant", Value::Bool(true)),
("durationMs", Value::from(0)),
],
);
}
if truthy(rt.env("IMPECCABLE_HOOK_DISABLED")) {
return result(
&audit,
vec![
("skipped", Value::from("env-disabled")),
("durationMs", Value::from(0)),
],
);
}
let started = now_ms();
let event = match parse_event(stdin) {
Err(()) => {
return result(
&audit,
vec![
("skipped", Value::from("stdin-malformed")),
("durationMs", ms_since(started)),
],
)
}
Ok(None) => {
return result(
&audit,
vec![
("skipped", Value::from("stdin-empty")),
("durationMs", ms_since(started)),
],
)
}
Ok(Some(e)) => e,
};
let harness = resolve_harness(rt, Some(&event));
let event = normalize_hook_event(rt, &event, &rt.proc_cwd, harness);
audit.insert("harness".into(), Value::from(harness));
let session_cwd = str_field(&event, "cwd").unwrap_or(&rt.proc_cwd).to_string();
let primary_files = normalize_scan_targets(
rt,
&resolve_target_files(rt, &event, &session_cwd),
&session_cwd,
);
let project_cwd =
resolve_cache_cwd(rt, primary_files.first().map(String::as_str), &session_cwd);
audit.insert("cwd".into(), Value::String(project_cwd.clone()));
let target_files = expand_scan_targets(rt, &primary_files, &project_cwd);
let session_value = session_id_of(&event);
audit.insert(
"session".into(),
session_value.clone().unwrap_or(Value::Null),
);
if let Some(tool) = event.get("tool_name").filter(|v| truthy_value(Some(v))) {
audit.insert("tool".into(), tool.clone());
}
if target_files.is_empty() {
return result(
&audit,
vec![
("skipped", Value::from("no-file-path")),
("durationMs", ms_since(started)),
],
);
}
let config = read_config(&project_cwd);
if !config.enabled {
return result(
&audit,
vec![
("skipped", Value::from("config-disabled")),
("durationMs", ms_since(started)),
],
);
}
let platform = resolve_project_platform(rt, &project_cwd);
if is_native_platform(platform.as_deref()) {
return result(
&audit,
vec![
("skipped", Value::from("native-platform")),
("platform", Value::String(platform.unwrap_or_default())),
("durationMs", ms_since(started)),
],
);
}
let mut cache = read_cache(&project_cwd);
let session_id = session_key(&session_value);
let mut scans = HashMap::new();
let tiered = per_edit_tiering_active(&config, harness);
struct Pending {
file_path: String,
known: Vec<String>,
}
let mut pending_winner: Option<Pending> = None;
let mut clean_winner: Option<String> = None;
let mut fresh_groups: Vec<Group> = Vec::new();
let mut suppression_winner: Option<String> = None;
let mut clean_ack_deduped = false;
let mut skipped_bytes: u64 = 0;
let quiet_mode = truthy(rt.env("IMPECCABLE_HOOK_QUIET")) || config.quiet;
let mut detector_threw_any = false;
let mut last_skip = "no-scannable-file";
let mut live_preview_edit: Option<String> = None;
let mut suppressed_hit = false;
let mut cache_dirty = false;
let mut deferred_total: usize = 0;
for file_path in &target_files {
audit.insert("file".into(), Value::String(file_path.clone()));
if has_path_traversal(file_path) || is_sensitive_path(file_path) {
last_skip = "sensitive";
continue;
}
if is_generated_path(file_path) {
last_skip = "generated";
continue;
}
let ext = js::to_lower_case(&jsp::extname(file_path));
let configured = match_configured_extension(file_path, &config.extensions);
audit.insert(
"ext".into(),
Value::String(
configured
.map(|c| c.ext.clone())
.unwrap_or_else(|| ext.clone()),
),
);
if !ALLOWED_EXTS.contains(&ext.as_str()) && configured.is_none() {
last_skip = "extension";
continue;
}
let rel_for_match = relativize(rt, file_path, &project_cwd);
if matches_any_glob_list(&rel_for_match, &config.ignore_files)
|| matches_any_glob_list(file_path, &config.ignore_files)
{
last_skip = "config-ignore-file";
continue;
}
if !exists(file_path) {
last_skip = "file-missing";
continue;
}
if !is_scan_target_inside_project(rt, file_path, &project_cwd) {
last_skip = "outside-project";
continue;
}
let max_file_bytes = config.limits.max_file_bytes;
if max_file_bytes > 0.0 {
let size = std::fs::metadata(file_path).map(|m| m.len()).unwrap_or(0);
if size as f64 > max_file_bytes {
skipped_bytes = size;
last_skip = "too-large";
continue;
}
}
// A live variant session owns a file carrying preview markers: stand
// down before the per-session edit cap can turn the variants wrap
// into a suppression notice.
if primary_files.contains(file_path) {
if let Ok(bytes) = std::fs::read(file_path) {
if crate::hook_lib::has_live_preview_markers(&String::from_utf8_lossy(&bytes)) {
if live_preview_edit.is_none() {
live_preview_edit = Some(file_path.clone());
}
last_skip = "live-preview";
continue;
}
}
}
let use_html_engine = match configured {
Some(c) => c.engine == "html",
None => ext == ".html" || ext == ".htm",
};
if primary_files.contains(file_path) {
if harness == "claude" {
stop_baseline::capture(rt, &event, &mut cache, &session_id, file_path, use_html_engine);
}
let edit_count = bump_edit_count(&mut cache, &session_id, file_path);
cache_dirty = true;
audit.insert("editCount".into(), Value::from(edit_count as u64));
if edit_count > EDIT_COUNT_THRESHOLD as f64 {
stop_baseline::invalidate(&mut cache, &session_id, file_path);
let just_crossed = edit_count == (EDIT_COUNT_THRESHOLD + 1) as f64;
if just_crossed && suppression_winner.is_none() {
suppression_winner = Some(file_path.clone());
}
last_skip = "suppressed";
suppressed_hit = true;
continue;
}
}
let content = match std::fs::read(file_path) {
Ok(bytes) => String::from_utf8_lossy(&bytes).into_owned(),
Err(e) => {
// JS: fs.readFileSync throws out of runHook's try; the catch
// records the error and emits nothing.
return RunResult {
stdout: String::new(),
audit: with(
&audit,
vec![("error", Value::String(node_read_error(file_path, &e)))],
),
};
}
};
if crate::hook_lib::has_live_preview_markers(&content) {
// A live variant session owns this file. When it is the edited
// (primary) file, the whole event stands down, co-scanned
// stylesheets included: a clean ack or a finding about the
// companion file is the same mid-session noise the stand-down
// exists to prevent.
if primary_files.contains(file_path) && live_preview_edit.is_none() {
live_preview_edit = Some(file_path.clone());
}
last_skip = "live-preview";
continue;
}
let scan = scans.entry(file_path.clone()).or_insert_with(|| {
design_system_options_for_file(rt, &config, &project_cwd, file_path)
});
let mut detector_threw = false;
let findings: Vec<Finding> = if use_html_engine {
match detector_detect_html(rt, file_path, scan) {
Ok(f) => f,
Err(_) => {
detector_threw = true;
vec![]
}
}
} else {
detector_detect_text(&content, file_path, scan)
};
if !detector_threw && !use_html_engine {
stop_baseline::reconcile(&mut cache, &session_id, file_path, &findings);
}
let raw_count = findings.len();
let filtered = filter_findings(findings, &config);
let (immediate, deferred) = if tiered {
split_findings_by_tier(filtered)
} else {
(filtered, vec![])
};
if !deferred.is_empty() {
touch_file(&mut cache, &session_id, file_path);
cache_dirty = true;
deferred_total += deferred.len();
}
let fresh = dedupe_against_cache(&immediate, &mut cache, &session_id, file_path);
audit.insert("findings".into(), Value::from(raw_count));
audit.insert("freshFindings".into(), Value::from(fresh.len()));
if deferred_total > 0 {
audit.insert("deferred".into(), Value::from(deferred_total));
}
if detector_threw {
detector_threw_any = true;
continue;
}
// JS: Grok ignores PostToolUse stdout, so Stop is the user-visible
// pass. Remembering here would dedupe those findings out of Stop.
// Touch the file so Stop has it, and leave the finding list empty
// (#646).
if harness == "grok" {
touch_file(&mut cache, &session_id, file_path);
} else {
remember_findings(&mut cache, &session_id, file_path, &immediate);
}
cache_dirty = true;
if !fresh.is_empty() {
fresh_groups.push(Group {
file_path: file_path.clone(),
findings: fresh,
});
continue;
}
if !immediate.is_empty() && pending_winner.is_none() {
pending_winner = Some(Pending {
file_path: file_path.clone(),
known: immediate.iter().map(finding_cache_key).collect(),
});
} else if immediate.is_empty() && clean_winner.is_none() {
if quiet_mode || !should_emit_ack_for_file(file_path, &config) {
clean_winner = Some(file_path.clone());
} else if truthy_value(
ensure_file(&mut cache, &session_id, file_path).get("cleanAcked"),
) {
clean_ack_deduped = true;
} else {
ensure_file(&mut cache, &session_id, file_path)
.insert("cleanAcked".into(), Value::Bool(true));
clean_winner = Some(file_path.clone());
clean_ack_deduped = false;
}
}
}
if let Some(file) = live_preview_edit {
audit.insert("file".into(), Value::String(file));
return result(
&audit,
vec![
("emitted", Value::Bool(false)),
("skipped", Value::from("live-preview")),
("durationMs", ms_since(started)),
],
);
}
if !fresh_groups.is_empty() {
let scan = &scans[&fresh_groups[0].file_path];
let short = footer_mode_short(&mut cache, &session_id);
let reserve = design_note_reserve(rt, scan, &mut cache, &session_id);
let rendered = render_grouped_template(
rt,
&fresh_groups,
&config,
&RenderOpts {
cwd: Some(project_cwd.clone()),
short_footer: short,
reserve_chars: reserve,
},
);
let text =
append_design_system_note_once(rt, &rendered, scan, &mut cache, &session_id, &config);
commit_footer_shown(rt, &mut cache, &session_id, &text);
persist_cache(rt, &project_cwd, &cache);
let all: usize = fresh_groups.iter().map(|g| g.findings.len()).sum();
return RunResult {
stdout: payload(&text, "PostToolUse", harness),
audit: with(
&audit,
vec![
("file", Value::String(fresh_groups[0].file_path.clone())),
("emitted", Value::Bool(true)),
("freshFiles", Value::from(fresh_groups.len())),
("freshFindings", Value::from(all)),
("chars", Value::from(utf16_len(&text))),
("durationMs", ms_since(started)),
],
),
};
}
enum Ack {
Pending(String),
Clean(String),
}
let mut ack: Option<Ack> = None;
if !quiet_mode {
if let Some(p) = pending_winner
.as_ref()
.filter(|p| should_emit_ack_for_file(&p.file_path, &config))
{
let base = render_pending_ack(rt, &p.file_path, &p.known, &project_cwd);
let scan = &scans[&p.file_path];
ack = Some(Ack::Pending(append_design_system_note_once(
rt,
&base,
scan,
&mut cache,
&session_id,
&config,
)));
} else if suppression_winner.is_none() && !clean_ack_deduped {
if let Some(c) = clean_winner
.as_ref()
.filter(|c| should_emit_ack_for_file(c, &config))
{
let base = render_clean_ack(rt, c, &project_cwd);
let scan = &scans[c];
ack = Some(Ack::Clean(append_design_system_note_once(
rt,
&base,
scan,
&mut cache,
&session_id,
&config,
)));
}
}
}
// JS: an already-present `.impeccable/` dir marks a project that opted
// in (issues #344, #305). An existing cache file also counts as opted
// in: under IMPECCABLE_CACHE_ROOT (issue #422) state lives outside the
// project, so the project dir alone can't carry the marker — without
// this, clean-edit editCount bumps would stop persisting the moment
// state relocates. Under stock paths the cache sits inside
// `.impeccable/`, so the extra check changes nothing there.
if deferred_total > 0
|| (cache_dirty
&& (exists(&jsp::join(&[&project_cwd, ".impeccable"])) || exists(&get_cache_path(&project_cwd))))
{
persist_cache(rt, &project_cwd, &cache);
}
if detector_threw_any && pending_winner.is_none() && clean_winner.is_none() {
return result(
&audit,
vec![
("emitted", Value::Bool(false)),
("error", Value::from("detector-threw")),
("durationMs", ms_since(started)),
],
);
}
if quiet_mode {
return result(
&audit,
vec![
("emitted", Value::Bool(false)),
("quiet", Value::Bool(true)),
("durationMs", ms_since(started)),
],
);
}
if let Some(Ack::Pending(text)) = &ack {
let p = pending_winner.as_ref().unwrap();
return RunResult {
stdout: payload(text, "PostToolUse", harness),
audit: with(
&audit,
vec![
("file", Value::String(p.file_path.clone())),
("emitted", Value::Bool(true)),
("kind", Value::from("pending")),
("pending", Value::from(p.known.len())),
("chars", Value::from(utf16_len(text))),
("durationMs", ms_since(started)),
],
),
};
}
if let Some(sw) = &suppression_winner {
let text = suppression_notice(rt, &relativize(rt, sw, &project_cwd));
return RunResult {
stdout: payload(&text, "PostToolUse", harness),
audit: with(
&audit,
vec![
("file", Value::String(sw.clone())),
("suppressed", Value::Bool(true)),
("emitted", Value::Bool(true)),
("durationMs", ms_since(started)),
],
),
};
}
if let Some(Ack::Clean(text)) = &ack {
let c = clean_winner.as_ref().unwrap();
return RunResult {
stdout: payload(text, "PostToolUse", harness),
audit: with(
&audit,
vec![
("file", Value::String(c.clone())),
("emitted", Value::Bool(true)),
("kind", Value::from("clean")),
("chars", Value::from(utf16_len(text))),
("durationMs", ms_since(started)),
],
),
};
}
if pending_winner.is_some() || clean_winner.is_some() {
return result(
&audit,
vec![
("emitted", Value::Bool(false)),
("skipped", Value::from("non-ui-ack")),
("durationMs", ms_since(started)),
],
);
}
if clean_ack_deduped {
return result(
&audit,
vec![
("emitted", Value::Bool(false)),
("skipped", Value::from("clean-ack-deduped")),
("durationMs", ms_since(started)),
],
);
}
if suppressed_hit {
return result(
&audit,
vec![
("suppressed", Value::Bool(true)),
("emitted", Value::Bool(false)),
("durationMs", ms_since(started)),
],
);
}
let mut extra = vec![("skipped", Value::from(last_skip))];
if last_skip == "too-large" {
extra.push(("bytes", Value::from(skipped_bytes)));
}
extra.push(("durationMs", ms_since(started)));
result(&audit, extra)
}
/// JS: runStopHook({ stdinJson, env, cwd })
pub fn run_stop_hook(rt: &Runtime, stdin: &str) -> RunResult {
let mut audit: Map<String, Value> = Map::new();
audit.insert("ts".into(), Value::String(iso_now()));
audit.insert("event".into(), Value::String("Stop".into()));
if depth_is_set(rt.env("IMPECCABLE_HOOK_DEPTH")) || depth_is_set(rt.env("CLAUDE_HOOK_DEPTH")) {
return result(
&audit,
vec![
("reentrant", Value::Bool(true)),
("durationMs", Value::from(0)),
],
);
}
if truthy(rt.env("IMPECCABLE_HOOK_DISABLED")) {
return result(
&audit,
vec![
("skipped", Value::from("env-disabled")),
("durationMs", Value::from(0)),
],
);
}
let started = now_ms();
let event = match parse_event(stdin) {
Err(()) => {
return result(
&audit,
vec![
("skipped", Value::from("stdin-malformed")),
("durationMs", ms_since(started)),
],
)
}
Ok(None) => {
return result(
&audit,
vec![
("skipped", Value::from("stdin-empty")),
("durationMs", ms_since(started)),
],
)
}
Ok(Some(e)) => e,
};
let harness = resolve_harness(rt, Some(&event));
audit.insert("harness".into(), Value::from(harness));
let event = normalize_hook_event(rt, &event, &rt.proc_cwd, harness);
// Stop-hook re-entry guard (#400): Claude Code and Codex send
// `stop_hook_active`; Grok sends `stopHookActive`, copied onto the
// snake_case field by the normalizer. Cursor and GitHub Copilot omit
// the field, so the strict `=== true` is a no-op for them.
if event.get("stop_hook_active") == Some(&Value::Bool(true)) {
return result(
&audit,
vec![
("skipped", Value::from("stop-hook-active")),
("durationMs", ms_since(started)),
],
);
}
// JS: Grok fires Stop twice: `end_turn` (the gate that can inject
// additionalContext) then an observe-only `shutdown`. A second deep
// pass would re-emit the same findings. Claude omits `reason`; only
// skip when Grok named a reason that is not end_turn (#646).
if harness == "grok" {
if let Some(Value::String(reason)) = event.get("reason") {
if reason != "end_turn" {
return result(
&audit,
vec![
("skipped", Value::from("stop-reason")),
("reason", Value::String(reason.clone())),
("durationMs", ms_since(started)),
],
);
}
}
}
let project_cwd = rt.resolve(&[str_field(&event, "cwd").unwrap_or(&rt.proc_cwd)]);
audit.insert("cwd".into(), Value::String(project_cwd.clone()));
let session_value = session_id_of(&event);
let session_id = session_key(&session_value);
audit.insert(
"session".into(),
session_value.unwrap_or_else(|| Value::from("unknown")),
);
let config = read_config(&project_cwd);
if !config.enabled {
return result(
&audit,
vec![
("skipped", Value::from("config-disabled")),
("durationMs", ms_since(started)),
],
);
}
let mut cache = read_cache(&project_cwd);
let touched = touched_files(&cache, &session_id);
if touched.is_empty() {
return result(
&audit,
vec![
("skipped", Value::from("no-touched-files")),
("durationMs", ms_since(started)),
],
);
}
let platform = resolve_project_platform(rt, &project_cwd);
if is_native_platform(platform.as_deref()) {
return result(
&audit,
vec![
("skipped", Value::from("native-platform")),
("platform", Value::String(platform.unwrap_or_default())),
("durationMs", ms_since(started)),
],
);
}
let mut scans = HashMap::new();
let mut fresh_groups: Vec<Group> = Vec::new();
let mut scanned = 0usize;
let mut cache_dirty = false;
let mut pre_existing = 0usize;
let mut new_findings = 0usize;
let mut unknown = 0usize;
for file_path in &touched {
if scanned >= STOP_MAX_FILES {
break;
}
if has_path_traversal(file_path)
|| is_sensitive_path(file_path)
|| is_generated_path(file_path)
{
continue;
}
let ext = js::to_lower_case(&jsp::extname(file_path));
let configured = match_configured_extension(file_path, &config.extensions);
if !ALLOWED_EXTS.contains(&ext.as_str()) && configured.is_none() {
continue;
}
let rel = relativize(rt, file_path, &project_cwd);
if matches_any_glob_list(&rel, &config.ignore_files)
|| matches_any_glob_list(file_path, &config.ignore_files)
{
continue;
}
if !exists(file_path) || !is_scan_target_inside_project(rt, file_path, &project_cwd) {
continue;
}
scanned += 1;
let content = match std::fs::read(file_path) {
Ok(b) => String::from_utf8_lossy(&b).into_owned(),
Err(_) => continue,
};
if crate::hook_lib::has_live_preview_markers(&content) {
continue;
}
let use_html_engine = match configured {
Some(c) => c.engine == "html",
None => ext == ".html" || ext == ".htm",
};
let scan = scans.entry(file_path.clone()).or_insert_with(|| {
design_system_options_for_file(rt, &config, &project_cwd, file_path)
});
// JS: a detector failure tells us nothing about the file. Leave
// whatever was remembered alone rather than recording an empty scan
// as truth. (detectText cannot throw here: the Rust engine returns
// findings directly.)
let findings = if use_html_engine {
match detector_detect_html(rt, file_path, scan) {
Ok(f) => f,
Err(_) => continue,
}
} else {
detector_detect_text(&content, file_path, scan)
};
if !use_html_engine {
stop_baseline::reconcile(&mut cache, &session_id, file_path, &findings);
}
let filtered = filter_findings(findings, &config);
let classified = stop_baseline::classify(&cache, &session_id, file_path, use_html_engine, filtered.clone());
pre_existing += classified.pre_existing;
new_findings += classified.new;
unknown += classified.unknown;
let fresh = dedupe_against_cache(&classified.findings, &mut cache, &session_id, file_path);
// JS: sync to the live scan, including empty. Remembering only
// `fresh` (or skipping the write on a clean Stop) left stale keys in
// place, so a finding that was fixed and later reintroduced never
// fired again.
remember_findings(&mut cache, &session_id, file_path, &filtered);
cache_dirty = true;
if !fresh.is_empty() {
fresh_groups.push(Group {
file_path: file_path.clone(),
findings: fresh,
});
}
}
audit.insert("scannedFiles".into(), Value::from(scanned));
audit.insert("preExistingFindings".into(), Value::from(pre_existing));
audit.insert("newFindings".into(), Value::from(new_findings));
audit.insert("unknownFindings".into(), Value::from(unknown));
if fresh_groups.is_empty() {
if cache_dirty {
persist_cache(rt, &project_cwd, &cache);
}
return result(
&audit,
vec![
("emitted", Value::Bool(false)),
("skipped", Value::from("stop-clean")),
("durationMs", ms_since(started)),
],
);
}
let scan = &scans[&fresh_groups[0].file_path];
let short = footer_mode_short(&mut cache, &session_id);
let first_unknown = fresh_groups.iter().flat_map(|group| &group.findings)
.position(|f| f.name.starts_with("[attribution unknown]"));
let mut attribution_note = if first_unknown.is_some() {
format!("{ENVELOPE_PREFIX} {}", stop_baseline::UNKNOWN_NOTE)
} else {
String::new()
};
// Findings and attribution take priority. Append the lower-priority stale
// DESIGN.md notice only if it fits, without consuming its session flag.
let render = |note: &str, render_config: &HookConfig| render_grouped_template(
rt,
&fresh_groups,
render_config,
&RenderOpts {
cwd: Some(project_cwd.clone()),
short_footer: short,
reserve_chars: if note.is_empty() { 0.0 } else { (utf16_len(note) + 2) as f64 },
},
);
let mut rendered = render(&attribution_note, &config);
if !attribution_note.is_empty() && !rendered.lines().any(|line| {
line.starts_with("- ") && (line.contains("[attribution unknown]") || line.contains("[new]"))
}) {
// At the minimum budget, a grouped header and policy footer may crowd
// out even the first finding. Shorten the notice before losing it.
attribution_note = format!("{ENVELOPE_PREFIX} {}", stop_baseline::COMPACT_UNKNOWN_NOTE);
rendered = render(&attribution_note, &config);
}
// maxFindings / maxChars may also remove all unknown findings. Do not
// attach their guidance to an output that only shows confirmed new debt.
let shows_unknown = rendered.lines().any(|line| {
line.starts_with("- ") && line.contains("[attribution unknown]")
});
if !shows_unknown {
if let Some(prefix @ 1..) = first_unknown {
// Reclaim the unused notice budget for the known-new prefix.
// Keep the unknown suffix omitted: simply expanding the budget
// could reveal an unknown finding without its required guidance.
let mut visible_config = config.clone();
visible_config.limits.max_findings = cap_of(&config).min(prefix) as f64;
rendered = render("", &visible_config);
}
}
let text = if shows_unknown { format!("{attribution_note}\n\n{rendered}") } else { rendered };
let text =
append_design_system_note_once(rt, &text, scan, &mut cache, &session_id, &config);
commit_footer_shown(rt, &mut cache, &session_id, &text);
persist_cache(rt, &project_cwd, &cache);
let all: usize = fresh_groups.iter().map(|g| g.findings.len()).sum();
RunResult {
stdout: payload(&text, "Stop", harness),
audit: with(
&audit,
vec![
("emitted", Value::Bool(true)),
("freshFiles", Value::from(fresh_groups.len())),
("freshFindings", Value::from(all)),
("chars", Value::from(utf16_len(&text))),
("durationMs", ms_since(started)),
],
),
}
}
/// JS: hook.mjs#isStopEvent(stdinJson)
fn is_stop_event(stdin: &str) -> bool {
// JS: hook.mjs#stdinIsStop routes on the raw stdin via hook-lib's
// isStopEvent, which matches Claude's `hook_event_name: "Stop"` and
// Grok Build's `hookEventName: "stop"`.
match serde_json::from_str::<Value>(stdin) {
Ok(Value::Object(o)) => crate::hook_lib::is_stop_event(&o),
_ => false,
}
}
/// `impeccable hook` (hook.mjs main). Returns the exit code (always 0).
pub fn run(rt: &Runtime, stdin: &str, io: &mut impeccable_common::Io) -> i32 {
// JS: process.env.IMPECCABLE_HOOK_DEPTH = process.env.IMPECCABLE_HOOK_DEPTH || '1'
// is exported for child processes; this binary spawns none, so the
// pre-mutation snapshot in `rt.env` is the only value that matters.
let result = if is_stop_event(stdin) {
run_stop_hook(rt, stdin)
} else {
run_hook(rt, stdin)
};
write_audit_log(rt, &result.audit, &rt.proc_cwd);
if !result.stdout.is_empty() {
io.out(&result.stdout);
}
0
}