The "Patrol tools" readiness check read the cached model-readiness
snapshot's tool-protocol dimension on its own. Since the interrupted-run
handling landed (8d0d74e35, b78330405), a run cancelled after every tool
scenario already passed keeps ToolProtocol at pass while the overall
status reports not_assessed, so the check reported "Patrol ready" from an
evaluation that never completed.
The check now requires the snapshot's own overall verdict (Success)
before reporting ready. A snapshot carrying no verdict at all — overall
status not_assessed, or the interrupted or internal_error cause — is not
turned into a failure either: it falls back to the base-config classifier
exactly as an absent snapshot does, capped at a warning. That cap matters
because not_ready is a blocking status in this payload: it clears
readiness.ready, which disables the Patrol run control in
usePatrolIntelligenceState and drops the page into the setup-only view.
#1640 promises a severed or cancelled check never blames the model and
never blocks Patrol from running in Watch mode, and the runtime gate on
POST /api/ai/patrol/run (PatrolRuntimeReadiness) already treats an
unassessed mode as a warning, so a blocking tools check would have
contradicted the route that actually runs Patrol.
A completed run whose tool protocol passed while the overall verdict fell
short now warns instead of claiming ready. It must not block either: the
dimension that actually failed carries the verdict on its own check
(context quality blocks, latency warns), so blocking here would have
turned today's latency warning into a hard stop.
Regression tests: internal/api/issue1640_readiness_gate_test.go covers
the gate across interrupted, internal-error, completed-pass,
completed-fail, and short-of-pass snapshots, asserting the resulting
runnability of the readiness payload;
internal/ai/issue1640_readiness_gate_test.go drives a real evaluation
that is cancelled at the continuation probe to produce the
ToolProtocol=pass / status=not_assessed snapshot end to end and pins that
PatrolRuntimeReadiness keeps Patrol runnable. The new API test file is
registered in the subsystem verification registry.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two failures on c3fb35c8f. The subsystem lookup unit tests hold their own
hardcoded copies of the settings-shell-and-framing exact_files list, a
third snapshot surface beyond the guard test's, and registering the new
banner proof file left them stale. Synced all eight copies.
The apiClient message-precedence change broke a pre-existing pin that a
short plain-text body outranks the caller fallback on retryable errors
(useReportingPanelState). Body-over-fallback was the long-standing
behavior; what #1640 actually required was dropping markup and oversized
bodies, which stays. Restored body-wins for sane plain text, flipped the
two precedence tests introduced alongside the change, and corrected the
cloud-paid contract paragraph to describe the real order.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Follow-up to 8d0d74e35. The keepalive mechanism was right, the edges
were not.
1. The evaluation ran on a bare goroutine with no recover, so a panic in
provider streaming or validation took the whole Pulse process down.
Before that commit the same panic was on the request goroutine and the
recovery middleware turned it into a logged 500. The goroutine now
recovers, logs the panic with its stack, and answers with an ordinary
readiness result carrying the new internal_error cause and every
dimension reported as not assessed. A Pulse defect is not a model
verdict.
2. Headers were only Set, never committed, despite the comment, the
commit message, and api-contracts.md all claiming otherwise. The
status line went out with the first keepalive at +10s, so a proxy
with a sub-10s time-to-first-byte budget still severed the request.
The transport now writes and flushes WriteHeader(200) before the
ticker starts, matching the pattern the file already uses for SSE.
3. The flusher was resolved with a discarded ok, so a writer that is not
an http.Flusher silently buffered the keepalives and degraded back to
the original bug. It is now checked and logged; the response still
completes, so a warning is the right level here rather than the hard
failure the SSE handlers use.
4. TestIssue1640HandlerUsesKeepaliveTransport grepped the handler source
for substrings, which proves nothing about behaviour. Replaced with a
real httptest.NewServer test that runs a 300ms evaluation and asserts
the client sees the 200 and a body byte before the evaluation
completes, and that the padded body still parses as the expected JSON.
Added coverage for the panic path and the non-flushable writer, and
fixed the eager body[:1] that would panic when a transport regression
left the body empty.
5. The settings readiness banner had no not_assessed branch, so an
interrupted run still rendered the red "Patrol model not verified"
headline: the exact blame-the-model presentation the backend fix
removed. Tone and headline are now exported pure functions with a
neutral treatment for not_assessed and interrupted results, and an
interrupted run cannot claim verification from a max_verified_mode
recorded before the cancellation.
6. createAPIErrorFromResponse let a short plain-text body override an
explicit caller fallbackMessage. A caller passing a fallback knows
which operation it was performing; an intermediary writing the body
does not. Precedence is now canonical JSON, then caller fallback,
then body, with the HTML and oversize suppression unchanged.
7. patrolRunCancelled classified on the raw "context canceled" substring
as its first switch case. Ollama embeds that phrase in its own error
body when it aborts an upstream request, so a genuine provider
failure on a healthy run was classified interrupted and finish()
persisted it as not_assessed. Cancellation is now established from
the run itself (errors.Is(err, context.Canceled), or a cancelled run
context), never from error wording, and the readiness paths classify
through a context-aware entry point. context.DeadlineExceeded keeps
its provider-path timeout classification.
The readiness gate in HandlePatrolModelReadiness keying off ToolProtocol
alone is untouched, as agreed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The registry gained internal/monitoring/issue1638_dns_cache_test.go
(108aa4e20) and internal/api/issue1640_readiness_transport_test.go
(8d0d74e35) as registered verification files, but the expected
verification-requirement snapshots in canonical_completion_guard_test.py
were not updated alongside them, so every Canonical Governance run on
main has been failing its guard unit-test step since. Add both files to
the expected lists.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The alerts-frontend-surface accepted proof list is asserted verbatim by
canonical_completion_guard_test.py, so registering
useAlertOverridesState.test.tsx in registry.json without updating the pinned
expectation failed the guard unit tests.
Verified by running every step of the canonical-governance workflow locally
rather than only the guard I expected to trip: status, control-plane, registry
and contract audits, the Pulse Intelligence gate schema, active-target
automated and hybrid readiness proofs, and all twelve release-control unit
test modules.
Keep macOS notarization mandatory for every release candidate while requiring Windows Authenticode only for stable promotion, matching the publish workflow and RC4 release packet.
New scripts/release_control/format_staged_frontend.py mirrors the staged
Go formatter: formats staged frontend-modern/src {ts,tsx,css,json} blobs
through prettier --stdin-filepath, writes results back to the index
directly (no broad restaging), syncs the worktree only when it matches
the previously staged content, and iterates to a fixed point to absorb
prettier's occasional non-idempotence. Skips gracefully when prettier is
not installed (fresh clones, linked worktrees without node_modules).
Wired into .husky/pre-commit after the Go formatter, with unit tests in
the governance battery, a README note, and a .gitignore allowlist entry.
With the one-time sweep in the previous commits, prettier drift can no
longer re-accumulate and make format stays clean on a clean tree.
A fresh install's natural first question ("how is my machine doing?",
observed live with Ollama qwen3:8b) made the model call pulse_summarize
action=fleet without resource_ids; the tool errored and the agentic loop
surfaced a structured question asking a first-run user for "the
comma-separated list of resource IDs" — pure jargon, funnel-killing
(GitHub discussion #1042). Two root fixes:
pulse_summarize self-targets. action=fleet with resource_ids omitted now
enumerates the known fleet from the executor's unified resource provider
(infrastructure parents first, then guests, then storage; deduped;
bounded at the existing 50-resource cap with a truncation note). Both
modes resolve what models actually pass — canonical unified IDs and
unambiguous names — onto the reporting request shape the way the API
path's resolveReportSubject does: the canonical ID stays ResourceID
(findings/recovery keying) and the resolved metrics target rides
MetricsResourceID, so store queries find data instead of silently
returning zero points. Reporting types are classified from the unified
resource (agent-backed hosts "agent", pure Proxmox nodes "node" — the
documented target-type exception — Docker hosts "docker-host");
resource_type becomes an optional filter/default. Remaining error paths
instruct the model to enumerate or retry and forbid asking the operator
for resource IDs.
Ask-user policy covers tool-argument recovery. The system prompt's
resolve-before-asking section (945e2198's target policy) now extends to
failed tool calls: missing/invalid arguments are self-recovered
(enumerate, retry), and internal identifiers are never valid questions.
The pulse_question description carries the same prohibition so the
structured clarification surface cannot become an identifier elicitation
channel on small local models.
ai-runtime and api-contracts contracts updated in-commit (the
subsystem_lookup line pin follows the api-contracts insertion); full
./internal/ai/... and ./internal/agentcapabilities/... suites green.
The core.bare=true corruption of the shared repository recurred on
2026-07-17: a script that runs scratch git commands while the pre-commit
environment from a linked worktree (absolute GIT_DIR) is still exported
re-initializes the REAL repository as bare. a0fda6b26 fixed four helper
test files but missed two spots in scripts/release_control/internal:
- verify_commit_slice_test.py's git() helper only popped GIT_INDEX_FILE,
so its scratch 'git init' calls re-init the real repo when GIT_DIR is
inherited. It now scrubs via the shared repo_file_io.strip_local_git_env.
- verify_commit_slice.py's production git_env() kept the inherited hook
env even when unit tests patch REPO_ROOT to a temporary repository,
pointing git plumbing (including index writes) at the wrong repo. It
now scrubs in the test-patched branch only, matching format_staged_go.
Regression teeth:
- verify_commit_slice_test.py gains a canary test that exports the real
hook env shape (absolute GIT_DIR + GIT_INDEX_FILE, no GIT_WORK_TREE —
with GIT_WORK_TREE set the corruption does not reproduce) against a
scratch repo + linked worktree and asserts core.bare stays false.
- repo_file_io_test.py (runs in the pre-commit battery) gains a static
guard failing any release-control *_test.py that runs scratch
'git init' without referencing strip_local_git_env.
- The six hand-rolled 4-var pop loops from a0fda6b26 migrate to the
shared strip_local_git_env helper so the guard enforces one pattern.
Verified: full release-control battery green; every touched test file
also green with GIT_DIR/GIT_INDEX_FILE pointed at a canary repo's linked
worktree, canary config and status intact afterward.
Pre-commit runs from a linked git worktree export an absolute GIT_DIR.
The release-control test helpers and the test-patched branch of
git_env() only removed GIT_INDEX_FILE, so their scratch-repo commands
targeted the REAL repository: 'git init' in a tempdir re-initialized it
with core.bare=true (breaking git status/commit for every checkout and
worktree) and the subsequent scratch 'git add' failed the hook.
Scrub GIT_DIR, GIT_WORK_TREE, GIT_INDEX_FILE, and GIT_COMMON_DIR in:
- git_env() of format_staged_go.py, governance_stage_guard.py, and
subsystem_contracts.py (test-patched branch only; production hook
behavior unchanged)
- the scratch-repo git() helpers in format_staged_go_test.py,
governance_stage_guard_test.py, readiness_assertion_guard_test.py,
subsystem_contracts_test.py
Matches the pattern contract_audit_test.py and status_audit_test.py
already used. Verified the full hook test battery passes with
GIT_DIR/GIT_WORK_TREE/GIT_INDEX_FILE pointed at a canary repo, which
stays un-corrupted.
Canonical Governance failed on nearly every push today (6 of the last 8
commits) because the local pre-commit hook and the CI workflow disagreed
about the canonical completion guard in two ways.
Frontend-only commits skipped the guard entirely: the hook gated ALL
governance checks behind governance-path detection as a perf
optimization, but subsystem contracts name canonical frontend files, so
those commits landed locally and failed the same guard in CI. The hook
now always runs the (cheap) completion guard; only the multi-minute Go
test and audit battery stays path-gated.
Contract-neutral bypasses did not travel: PULSE_ALLOW_CONTRACT_NEUTRAL_COMMIT
authorized the commit in the local shell and vanished, so CI re-ran the
guard without it and went red. A new prepare-commit-msg hook records the
reason as a Contract-Neutral trailer in the commit message, and the
workflow now evaluates the guard per commit (each commit's file list
against its own parent), honoring the trailer exactly as the commit-time
hook honored the env var. Per-commit evaluation also stops a compliant
commit from being blamed for range-mates.
The new hook is registered as a worktree-sensitive governance file in
governance_stage_guard.py with a matching unit test assert.