Commit Graph
1424 Commits
Author SHA1 Message Date
Martin Vogel 50a6a67b89 Streamline local agent controls and preserve proactive chat
Separate agent configuration from chat visibility, keep chat compact, and
resume selection explanations after manual questions. Restore project
conversations locally for 24 hours with bounded storage and clear history.

Move daemon configuration into System and report persisted index metadata
and live watcher registration alongside its configuration control.

Validated with 303 focused frontend tests, TypeScript/Vite production build,
160 native watcher/HTTP tests, regression checks, and local browser checks.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-01 20:24:06 +02:00
Martin Vogel ea931e7647 feat(ui): refine graph exploration and add local agent configuration
Add focused galaxy tracing, edge filters, organic cluster layouts and cached
graph exploration with compact architecture and impact summaries.

Make the browser-local agent explain the current selection proactively,
and add a Config panel with persisted daemon overrides, shared browser
preferences and a reference for external configuration inputs.

Validation: 182 focused frontend tests and TypeScript passed. Reused the
recorded production build and 485 passing native checks on unchanged
source. The pre-existing capped-layout loading-note test failure remains
documented separately; the full local CI ladder was waived.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-09-28 18:39:48 +02:00
Martin Vogel d0f148bb5a feat(ui): expand architecture exploration and streamline workspaces
Add source-backed system structure and behavior analysis, container service maps, and graph navigation with directional edge styling. Refine repository metadata, graph focus resets, and local browser chat with a resizable Explorer graph.

Move index coverage into its own workspace and diagnostic history into System; simplify Explorer actions and header controls.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-09-13 22:10:10 +02:00
Martin Vogel 79c37032cc feat(ui): add 3D architecture and route exploration
Add spatial overview, dependency, entry-point and route views with source drilldown, directed relationship evidence, bounded graph expansion, and a plan view. Keep existing architecture details available and expose coverage and display limits.

Validation: frontend build, 2503 unit tests, 203 acceptance tests, and browser workflows on indexed dependency-cruiser, LikeC4 and CBM passed. Broader Linux and Windows native checks still have recorded failures.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-09-10 10:21:35 +02:00
Bernhard Jackiewicz 4372d9fada feat(ui): integrate repository evidence and change analysis on one daemon
Add source-backed repository navigation, selection context and a shared change
analysis workspace for symbols, working-tree changes and Git comparisons.
Persist agent events and project logs through the existing daemon with SQLite
schema 4, bounded polling, retention and restart-safe deduplication.

Repair compact RPC decoding, path-search races, source pagination and identity,
large map responses, live error propagation and explicit local diagnosis flows.
Include the real-repository browser runners and focused regression coverage.

Validation: 2473 frontend tests, 203 acceptance checks, production build,
style/promise gates, 5 reference-boundary checks and 7 local hook tests pass.
The complete repository-map browser flow also passes after daemon restart.
Global C lint and cross-platform release validation remain open and are
recorded in the accompanying handoff report.

Signed-off-by: Bernhard Jackiewicz <bj@techport.io>
2026-09-10 00:08:19 +02:00
Martin Vogel 80f4f41f9b Merge CodeAtlas logging with the exploration frontend
Retain the existing frontend diagnostics and unreadable-file reporting
alongside the selected-code inspector, graph navigation, and local chat.
Keep both process telemetry and log-tail models, and preserve the HTTP
logging tests alongside the browser model download policy checks.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-09-09 19:59:56 +02:00
Martin Vogel a47e0dadb3 feat(ui): graph exploration workspaces with local browser chat
Add Explore, Galaxy, Architecture, Agents, and System workspaces with a
persistent, resizable local chat dock. Attach exact source selections and
graph context, render Markdown, and offer pinned opt-in browser models.

Keep the galaxy visible below chat and focused on the active file or code
selection. Restore hierarchy root selection, resolve projection identities
by symbol, and retain loaded search matches after an empty index reply.

Expose process measurement units, embed browser runtime MJS/WASM assets,
and constrain binary-audit exceptions to reviewed dependency strings.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-09-09 00:37:21 +02:00
Martin Vogel 9dc882a82f feat(ui): frontend log file, written by the page and served by the server
The browser console was the only place Atlas wrote to, and it dies with
the tab: a reader who saw "get_code_snippet returned no source" had
nothing to hand over, and an agent looking into it had nothing to read.
The page now keeps a second copy of everything it logs, on the server,
next to the daemon's own records.

Server (src/ui/http_server.c):

- POST /api/ui-log takes {page, session, entries[]} and appends one JSON
  line per entry to <cache_dir>/logs/ui.log (received, page, session,
  seq, ts, level, source, message, detail, stack, url, line, col).
  Bounds: 64 KiB per post, 100 entries per post, 4096 characters per
  text field cut on a UTF-8 boundary and marked, the level normalised to
  a known word, every string escaped again on the way out. The file
  rotates once to ui.log.1 at CBM_UI_LOG_ROTATE_BYTES (default 5 MiB).
  Warnings and errors are mirrored into the ring GET /api/logs serves,
  so both sides of one failure sit in one list; the chatter stays in
  the file. The reply names the file and says whether it could be
  written.
- GET /api/ui-log?lines=N tails the file (default 200, at most 1000,
  from the last 256 KiB) and names the file and its rotated
  predecessor; `partial` says when the lines do not start at the top.
- cbm_ui_log_file_path() is the one place the path is built.

Frontend (graph-ui):

- app/ui-log.ts: a bounded buffer (200 entries, oldest dropped first
  and the loss named at the head of the next batch), batched posts
  (25 entries or 1.5 s), backoff on a refused post, never writes to the
  console itself. app/ui-log-transport.ts posts with fetch, and with a
  beacon when the page is going away.
- app/ui-log-install.ts wraps the five console methods (they still
  print; the copy is taken after), listens to window error and
  unhandledrejection, and flushes on pagehide. Installed first in
  main.tsx, so nothing can fail before the copy exists.
- provider/error-observer.ts is the seam for failures the panels catch
  and show as a sentence: the /rpc transport and the /api client
  announce every failed call there before throwing, with a level that
  tells a tool the server does not offer (info) from a tool that said
  no (warn) from a broken or unreachable server (error). The reader
  announces a file the index does not know.
- The projects panel shows the tail under "This server" as sentences,
  names the file to attach to a report, and reloads it with the rest.
- README (both) and INVENTAR.md name the file and the routes.

Tests: three httpd cases (JSON lines and tail, refused bodies and
dropped entries, rotation); vitest for the buffer, the wiring in jsdom,
the transport, the seam through both clients, the API method, the model
and the panel. Verified live against a real project: the reader's
failure appears in the file and in the panel within two seconds.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-09-08 01:26:33 +02:00
Martin Vogel 7cd5c93136 Merge feat/atlas-r1 (current with main) into feat/codeatlas-web
Re-stacks the CodeAtlasWeb branch on the updated base (feat/atlas-r1 at
a44732b6, which merges main 7b0f553c). Ten conflicts, all in the seam
between the previous frontend and its replacement:

- README.md: main's surface count (45) with this branch's Atlas line.
- graph-ui/package-lock.json, graph-ui/src/App.tsx: CodeAtlasWeb's own
  files stay; the previous frontend's edits to the same paths do not
  apply.
- Seven previous-frontend files main had modified (GraphTab,
  NodeDetailPanel and its test, StatsTab test, useProjects, rationale
  and its test) stay deleted, and two tests main added for that
  frontend (App.test.tsx, useProjects.test.tsx) are not brought in;
  their subjects no longer exist in this tree.

Nothing under graph-ui changes relative to this branch; the C side
takes main's lean-output work and the branch's merge resolution.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-09-06 03:23:48 +02:00
Martin Vogel da7235ab39 Merge origin/main into feat/atlas-r1
Brings the Atlas branch up to current main (7b0f553c). Eight files
conflicted; how each was settled:

- src/mcp/mcp.c: the real ingest_traces description stays, in main's tool
  row shape (main dropped the title column); index_repository keeps the
  truthful abort branches (aborted_previous_preserved, persist_failed)
  and gains main's format_migration flag in the success branch.
- src/ui/http_server.c and tests/test_httpd.c: the branch's twelve-tool
  read allowlist for /rpc (#1663) over main's three, with main's %zu
  and initialised counter.
- README.md: main's surface count (45), the branch's Atlas line.
- graph-ui (the previous frontend, replaced by the stacked #2068 but
  kept building and green here): the branch's node-selection wiring
  (onSelectNode) is kept consistently, so main's duplicate
  handleSelectPath and its 3-argument call go; main's version chip is
  added to the branch's header; useProjects keeps the branch's
  render-first flow on top of main's paginated JSON helpers
  (fetchAllProjects, fetchFullSchema), which the lean output contract
  requires, and a project whose size the server did not send now gets
  its schema too. main's GraphTab.selection.test.tsx, written against
  main's GraphTab, is dropped in favour of the branch's own #1197
  coverage in GraphTab.regions.test.tsx ("reports selected nodes to
  the route for deep links"); main's useProjects pagination test waits
  for the lazy schema enrichment instead of asserting before it runs.

Verified: mcp, httpd and ui suites 414 passed; the old frontend builds
and its 149 tests pass.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-09-06 03:03:19 +02:00
Martin Vogel 62af5b87ee feat(ui): wire CodeAtlasWeb into the build, the dev-proxy contract and the CSP
Maintainer glue for the imported frontend; no product behaviour of the
UI itself changes.

- Makefile.cbm: `npm ci` runs with PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1.
  The UI's dev dependencies include playwright for its local browser
  proofs, and a plain install would download browsers that no build or
  gate launches.
- graph-ui/vite.config.ts: the dev server listens on 5173 with the two
  literal loopback origins, which is what
  tests/test_ui_dev_proxy_security.sh pins.
- graph-ui/index.html: lang="en"; the catalog is English.
- CSP: the directive string moves to http_server.h as CBM_UI_CSP_VALUE
  so a test can pin it, and connect-src now admits the two loopback
  services the UI may talk to when the reader starts them, the
  local-model sidecar on 127.0.0.1:4141 and the agent-event bridge on
  127.0.0.1:4142. Under connect-src 'self' both fetches were silently
  blocked in the embedded build. Every other host stays forbidden; a
  new httpd test asserts that the only hosts in the policy are those
  two loopback ports.
- scripts/security-ui.sh: two false positives that bit on the imported
  source. The analytics pattern matched the English word "plausible"
  (now plausible.io), and `\s` inside an ERE bracket is a literal
  backslash and 's', which cut every localhost URL at its first 's' and
  then failed the allowlist (now [:space:]).

Verified: httpd suite 64 passed; dev-proxy contract and UI security
audit pass; graph-ui builds and its 2024 vitest tests pass.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-09-05 20:15:13 +02:00
Martin VogelandDustin Persek b7e18c8940 fix(mcp): harden semantic search inputs and store-error handling
Land the four residual hardening points from #938 on the current
semantic-search design (semantic_limit/offset, the certified top-K
fetch loop):

- extract_semantic_keywords rejects a semantic_query array that holds a
  non-string element instead of silently running the string subset; the
  caller gets the same isError as for a bare-string semantic_query.
- run_semantic_query_core fails closed when the vector scan itself
  fails ("semantic search failed", isError) instead of rendering a store
  error as zero matches plus the broaden-your-keywords hint.
  cbm_store_vector_search now tells the two states apart: a store with
  no node_vectors table (lean index) is CBM_STORE_NOT_FOUND and keeps
  the empty-page + moderate/full-index hint that main relied on by
  swallowing the "no such table" prepare failure; a failing scan is
  CBM_STORE_ERR.
- vs_min_cosine_score hoists the keyword norms (once per search) and
  the node norm (once per node) out of the per-(node, keyword) loop.
  The integer sums are exact, so every score stays bit-identical.
- vs_append_result checks its strdup results, frees the partial row on
  failure and counts a row only once every copy succeeded, so the
  caller's free path stays valid.

Two reproduce-first tests in the mcp suite fail on main and pass with
the change: non-string elements produce the type error, and a forced
cbm_cosine_i8 failure produces the fail-closed error for semantic-only
and combined calls while a missing node_vectors table still yields the
lean-index hint.

Refs #915

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Co-authored-by: Dustin Persek <dustin.persek@protonmail.com>
2026-09-05 20:02:07 +02:00
Martin Vogel 9427f43515 test(log): restore the level a test found instead of the old quiet default
With the library default back at INFO, the branch's log and progress-sink
tests were still handing the process a WARN level on their way out —
main's `/* restore */` lines had been rewritten to WARN — so every suite
that ran after them in the same process lost its INFO records (the CI
shard that runs `log` before `index_format` is exactly that order).

test_log.c keeps main's tests and restores verbatim; the two branch
tests (the CBM_LOG_LEVEL opt-in and the startup policy) save the level on
entry and hand it back. The progress-sink tests pin the quiet level they
exercise explicitly and restore it, instead of assuming the process
entered at WARN.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-09-05 07:59:14 +02:00
Martin Vogel a15d99e39f test(mcp): drive the fail-closed search test through the scan seam
The unreadable-operand scenario used mode-0 permission bits, which
Windows cannot express and root ignores (the Colima legs run as root), so
the test skipped on both — and bare SKIP() is forbidden. The scoped file
list deliberately drops every missing or non-regular indexed path before
grep sees it, so a refusal can only come from the scan itself. The scan
seam now delivers one plausible hit and then grep's error status, on every
platform, and the test asserts that neither that hit nor zero matches is
ever reported as a complete page.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-09-05 06:05:27 +02:00
Martin Vogel c2b68d40b5 test(windows): settle the lean-output guards on current main
- test_daemon_lifecycle.py: daemon coordination is account-wide and
  CBM_CACHE_DIR does not move the rendezvous (main's CBM_RUNTIME_DIR
  relocation), so a private CBM_RUNTIME_DIR per cache is what makes the
  --verbose call cold and lets it observe the spawn hint.
- test_hook_augment.py: the CLI answers in the lean tree form by default;
  the guard parses the JSON projection, so it asks for it.
- test_mcp.c: the fail-closed search test skipped on Windows past the
  teardown that names its POSIX-only response.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-09-05 06:05:27 +02:00
Martin Vogel 619c683480 fix(mcp): settle the lean-output branch on current main
Fix-forward after rebasing onto main (545 commits, 26 conflicted-file
resolutions). Where main had shipped a behaviour in the meantime, main
wins; the PR keeps its output contract on top of it:

- search_code: the scan runs under main's supervised, deadline- and
  output-bounded runner and feeds the branch's streaming classifier from
  the runner's output file; the POSIX wrappers map grep's no-match to 0,
  so every non-zero exit fails closed (no exit-1 special case).
- trace_path: strategy/confidence precede args in header and rows (#1542),
  in both the tree table and the json legs.
- cypher: relationship expansion is never capped (a cap falsified
  aggregates) and unlabeled MATCH scans every candidate; `truncated` is
  raised only by a real ceiling — a clamped hop range is probed one hop
  beyond the cap and reports truncated only when a candidate exists there.
  The plain BFS and the trail BFS both probe one row past max_results so
  saturation is observable instead of silent.
- search_code full mode and the final json use main's U+FFFD sanitizer
  instead of '?' replacement.
- search_graph json keeps an empty `groups` array for semantic-only calls;
  list_projects json echoes offset/limit next to next_offset and accepts
  main's include_details spelling for the stats projection.
- discovery budget: main added get_file_outline, compare_graphs, manage_adr
  set_sections and the debug/include_details parameters; their descriptions
  are trimmed to the lean style and the ceiling moves from 15 to 18 KiB.

Tests that asserted a JSON default now request format:"json"; the
fail-closed search test uses an unreadable regular file (a missing or
non-regular indexed path is deliberately not a scan operand on main); the
non-regular fixture gives its real sources File nodes, as every indexed
project has; list_projects total counts listable projects (the 0-byte
ghost is never a row, so it is not a page either).

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-09-05 06:05:08 +02:00
Martin Vogel 7a1e069b29 fix(search): make scoped file patterns portable
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-09-05 06:05:08 +02:00
Martin Vogel e93db6a95a test(cli): isolate activation runtime
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>

# Conflicts:
#	tests/test_cli.c
2026-09-05 06:05:08 +02:00
Martin Vogel cc9c2f4c0f fix(search): scope scans to canonical source files
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-09-05 06:05:08 +02:00
Martin Vogel 0735fdccb7 fix(mcp): keep compact pagination lossless
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-09-05 06:05:08 +02:00
Martin Vogel 4cbc0536ac fix(store): bind pagination to published generations
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-09-05 06:05:08 +02:00
Martin Vogel 48149cac08 fix(output): align discovery defaults and help
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-09-05 06:05:08 +02:00
Martin Vogel 56516eff2d fix(mcp): propagate fallback discovery failures
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-09-05 06:05:08 +02:00
Martin Vogel 26650255ef fix(mcp): scan code search without hit loss
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-09-05 06:05:08 +02:00
Martin Vogel daa5dc8247 fix(mcp): make semantic search contracts explicit
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-09-05 06:05:08 +02:00
Martin Vogel 7be95be570 fix(output): require token-shaped directory savings
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-09-05 06:05:08 +02:00
Martin Vogel 10af29d50c fix(mcp): preserve malformed raw source bytes
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-09-05 06:05:08 +02:00
Martin Vogel f30ae39eb7 fix(mcp): preserve deletion snapshot cursors
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-09-05 06:05:08 +02:00
Martin Vogel 5630f11f21 fix(mcp): fail closed on incomplete snapshots
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-09-05 06:05:08 +02:00
Martin Vogel 14c229b158 feat(mcp): add lossless semantic pagination
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-09-05 06:05:08 +02:00
Martin Vogel a31d2679c1 fix(mcp): make search code pagination lossless
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-09-05 06:05:08 +02:00
Martin Vogel 4a9fcd97d0 feat(cli): add explicit quiet output mode
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-09-05 06:05:08 +02:00
Martin Vogel 240f252b70 fix(mcp): bind live pagination to snapshots
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-09-05 06:05:08 +02:00
Martin Vogel 10ece1bb86 fix(output): preserve non-utf8 identities
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-09-05 06:05:08 +02:00
Martin Vogel 17d61ea9be test(mcp): bind lean detect-changes accounting
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-09-05 06:05:08 +02:00
Martin Vogel 5f58e233dc fix(output): preserve compact output safety signals
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-09-05 06:05:08 +02:00
Martin Vogel e842fd78cf fix(mcp): preserve lean output continuations
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-09-05 06:05:08 +02:00
Martin Vogel 3909e5a0e1 feat(mcp): make tool output lean and lossless
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-09-05 06:05:08 +02:00
Martin Vogel ceba42728a Merge pull request #1796 from liuchong/fix/daemon-start-survives-ui-config
fix(cli): a refused UI configuration is not a failed daemon start
2026-09-05 06:02:36 +02:00
Martin Vogel b3d898e12f Merge pull request #1914 from DeusData/fix/elf-hardening-relro-now
fix(build): put the whole GOT inside the RELRO window on ELF targets
2026-09-05 05:30:45 +02:00
Martin Vogel b6e4beb9b1 Merge pull request #1849 from DeusData/fix/issue-1733-windows-hook-wrapper-resume
fix: register Claude hooks without a shell
2026-09-05 05:15:47 +02:00
Martin Vogel 202c8e42b5 Merge pull request #2055 from DeusData/fix/test-wave-descendant-probe-budget
test: give the Windows descendant probe its own budget
2026-09-05 04:44:26 +02:00
Martin Vogel a1e1beb242 Merge pull request #1851 from DeusData/fix/issue-391-sparse-absolute-loss
fix(dump): detect sparse absolute persistence loss
2026-09-05 03:23:27 +02:00
Martin Vogel ad479b61e9 Merge branch 'main' into fix/daemon-start-survives-ui-config 2026-09-04 22:51:27 +02:00
Martin Vogel 39c74bdd5c test: converge hook tests and smoke on the exec-form registration
The rebase onto main brought the Bash-enabled PreToolUse matcher
(Grep|Glob|Bash), which the exec-form tests still counted as Grep|Glob.

The Windows lifecycle test seeded the .cmd shell spelling as the converged
identity; the shell-free registration converges every owned shell spelling
onto one exec-form entry per matcher instead. The test now asserts that,
plus their removal on uninstall, and hands cbm_install_agent_configs the
'/'-spelled binary path cbm_cmd_install registers (cbm_mkdtemp returns a
native '\' temp root on Windows, which uninstall never derives).

Smoke 8aq proved Claude's gate registration by grepping the shim name out
of settings.json; the exec form carries no shim name, so it now checks the
binary-plus-'hook-augment' PreToolUse hook. 9d's shim-name marker would
otherwise pass vacuously after uninstall; the 'hook-augment' argument is an
owned marker there too.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-09-04 22:34:30 +02:00
Martin Vogel 7b92d2a621 ci(composition): scope A1d-bind-now to release links, declare fixtures by name
Step 0o (MCPB bundle contract) and Step 0q (release candidate derivation)
hand the composition gate a stub compiled straight from a .c file with the
compiler's defaults. That stub exists to exercise packaging, format detection
and the needle scans; it is not linked with -z now and never ships. A1d
measured eager binding on it anyway, so on a toolchain that binds lazily by
default (test-diag: clang-22) Step 0o died deterministically with

  A1d-bind-now stage/codebase-memory-mcp: GOT section(s) .got.plt fall
  outside PT_GNU_RELRO [15824,16384) — they stay WRITABLE after relocation

The property A1d protects is a property of the release link (-z now in
Makefile.cbm), so the assertion is scoped to it: a caller that feeds the gate
a packaging fixture declares so with CBM_COMPOSITION_FIXTURE=1 and A1d is
reported n/a — printed on every run as a named exemption, never a silent
skip. Every other assertion still runs on the fixture unchanged, and release
derivation never sets the variable.

Verified in the Linux arm64 container (Ubuntu 24.04, gcc 13.3): a stub
linked -z lazy reproduces the CI failure text on the gate without the
variable and reports n/a with it; both contracts pass under a CC wrapper
that links lazily (the shape that was red), and unchanged on macOS.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-09-04 22:02:47 +02:00
Martin Vogel 7d20ff9398 fix: register Claude hooks without a shell
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-09-04 22:00:37 +02:00
Martin Vogel 20c7dd3dbb Merge branch 'main' into fix/issue-391-sparse-absolute-loss 2026-09-04 21:54:36 +02:00
Martin Vogel 132e8fc323 test: give the Windows descendant probe its own budget
When a suite leader has already exited, taskkill /T cannot walk its tree,
so run-test-wave.py proves cleanup by asking Windows whether anything is
still parented to the dead PID. That probe was timed with --kill-grace,
which the parallel gate passes as 1s. A cold powershell.exe + CIM start
routinely costs more than that on a runner, so TimeoutExpired -- a fact
about interpreter start-up, not about the process tree -- was read as
"assume the worst" and failed an already-clean shard with exit 2.

Give the probe its own stable-state budget (15s) plus one retry, keeping
the fail-closed semantics: an exhausted or failed probe still counts as
"cannot prove absence". The refusal now names which of the two happened,
an unfinished probe or a counted set of live descendants, instead of
reporting both as a leaked tree.

The contract test pins this structurally -- no sleeps, no timing
thresholds: the probe must be bounded by the module constant rather than
by the kill-grace argument, must retry a timed-out probe, must never read
an unusable probe as absence, and must report the two failures
distinctly. Its Windows leader-exit case now waits on the scheduler's
refusal rather than on a deadline shorter than the new budget.

Refs #1741

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-09-04 17:58:37 +02:00
Martin Vogel 7e47043acc fix(tests): make the lock-registry absolute-deadline fixture deterministic
lock_registry_absolute_deadline_survives_repeated_wakes has failed on the
test-unix (macos-15-intel) leg of three unrelated pull requests in one
week -- #1342 (08-28), #1811 (09-02) and #1819 (09-03, run 33799475629,
job 100823626965) -- always with the same signature:

  FAIL tests/test_lock_registry.c:1153: ASSERT(tail_queued)
  7648 passed / 1 failed

The registry is not racy. cbm_lock_registry_acquire enqueues the waiter
synchronously under the registry mutex before any wait, so waiter_count
and the attempting count are exact. The defect is in the fixture: it
raced two wall-clock windows against each other, both anchored to a
timestamp the observer thread took before the tail thread had even been
scheduled.

  deadline_start = cbm_now_ms();
  tail.deadline_ms = deadline_start + 200;  /* the tail's acquire deadline */
  queue_deadline   = deadline_start + 100;  /* the observer's budget */

Because both windows start before the tail runs, a loaded runner breaks
the fixture two different ways:

  * the tail is scheduled inside its deadline but after the observer's
    100 ms budget has expired -- the queued state existed and was simply
    no longer being looked at; or
  * the tail is scheduled more than 200 ms late, in which case its
    deadline has already passed when it finally calls acquire, the
    pre-registration deadline check in lock_registry_acquire_internal
    returns BUSY immediately, and the tail never enqueues at all -- the
    asserted state can then never occur, however long the observer waits.

lock_registry is in the parallel wave of run-tests-parallel.sh, not the
serial tail, so on a small CI runner it competes with a full wave of
sanitized suites -- exactly the scheduling delay both paths need.

Widening the observer's budget would only paper over the first path, and
the state it waits for is transient by construction: it exists only
between the tail's enqueue and the tail's own deadline. So the fixture is
rebuilt to observe states that cannot evaporate.

  * The tail anchors its absolute deadline itself, in its own thread,
    immediately before the acquire it bounds. Scheduling delay can no
    longer consume the deadline before the call starts, so the enqueue is
    unconditional, and elapsed is measured from the tail's own anchor --
    it now times the registry instead of timing the scheduler.

  * Enqueue is exposed as a monotonic counter,
    cbm_lock_registry_waiter_enqueue_count_for_test, next to the existing
    test_condition_wait_calls counter it is modelled on. Because the
    count only ever grows, the observer reads it once after the tail has
    returned rather than trying to catch a live queue depth: the polling
    loop, and with it the window, is gone.

  * The fixture's remaining 500 ms and 600 ms budgets become the file's
    LOCK_REGISTRY_TEST_TIMEOUT_MS backstop, and each loop exits on the
    state it waits for instead of on the clock, so the backstop only
    fires when the product is actually broken.

The contract is unchanged: the tail must still return at its absolute
deadline (150 <= elapsed < 350 ms for a 200 ms deadline) despite ~40
unrelated cancel broadcasts, still with BUSY and no lease, with the head
still holding the attempt. The broadcast loop now starts immediately
after the tail is released, so it overlaps the tail's wait at least as
much as it did before.

Verification, all on macOS arm64 with the sanitized runner:

  * The mechanism was reproduced locally by delaying only the tail thread
    after its start gate, with production untouched. A 120 ms delay (the
    state exists, outside the observer's budget) and a 250 ms delay (the
    tail never enqueues) each produced exactly one failure,
    ASSERT(tail_queued) -- the CI signature.
  * After the rebuild the same injections are green at 120, 250, 400 and
    900 ms: an arbitrary scheduling delay no longer decides the verdict.
  * lock_registry 30/30 green plain and 30/30 green under four CPU hogs;
    16/16 inside the saturated 18-job parallel wave.
  * private_file_lock, lock_registry, daemon, project_lock and the
    daemon_* suites: 222 passed, 1 skipped.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-09-04 15:42:15 +02:00