Problem to solve
When madar watch is running and a git operation removes files (branch switch,
merge, rebase, reset), the incremental rebuild throws ENOENT and the watcher
latches into status: "failed" permanently. From that point serve --auto-refresh
rejects every query, even though a valid out/graph.json is still on disk.
A file disappearing mid-rebuild is a normal race with git — not an error condition.
Version: @lubab/madar@0.32.0 (global install, Node 24.13.0, macOS)
Reproduction
-
Start madar watch on a repo, and an MCP client with serve --stdio --auto-refresh.
-
git checkout <other-branch> where the target branch does not contain some file
present on the current branch (in our case a *.spec.ts deleted between branches).
-
The watcher logs:
[madar watch] Rebuild failed: ENOENT: no such file or directory, open ''
-
Every subsequent MCP call fails with:
Madar auto-refresh cannot guarantee a fresh graph
(status=idle, coverage=complete, policy=match,
background_failure=[madar watch] Rebuild failed: ENOENT: ...)
-
Restarting the client does not help — the watcher wedges again on the next
git operation.
Observed out/watcher-state.json:
{
"status": "failed",
"coverage": "failed",
"reconciliation_count": 7982,
"failure_reason": "[madar watch] Rebuild failed: ENOENT: no such file or directory,
open '.../password-validation.util.spec.ts'"
}
In a day of normal branch work this made the MCP server unusable for hours at a time.
Root cause
dist/src/infrastructure/watch.js (~L426) has exactly one benign-error branch:
catch (error) {
const message = error instanceof Error ? error.message : String(error);
if (message.includes('No supported files were found')) {
output.log('[madar watch] No supported files found - nothing to rebuild.');
return false;
}
output.error([madar watch] Rebuild failed: ${message}); // ENOENT lands here
return false;
}
There is no ENOENT handling anywhere in watch.js. The failure is then persisted
(~L820):
state.status = 'failed';
state.failure_reason = 'Automatic graph rebuild failed; the graph must not be treated
as fresh.';
and serve --auto-refresh refuses to answer while that flag is set.
Two separate problems compound here:
- A file vanishing during rebuild is treated as fatal, though it is expected
whenever git rewrites the working tree.
- The failure is fail-closed — a stale-but-valid graph is withheld entirely.
Without (2), (1) would degrade to "slightly stale" instead of "completely down".
Proposed solution
Suggested fixes
- Skip vanished files — treat error.code === 'ENOENT' during a rebuild as a
skip-and-continue, not a failure. The next reconciliation picks up the new tree anyway.
- Don't latch failed — let a subsequent successful rebuild clear the flag, and
retry on the next filesystem event rather than requiring manual
madar generate . --update.
- Prefer fail-soft on read — when the graph is stale, respond with the last good
graph plus a stale: true marker so callers can decide. Refusing all queries is a
heavy penalty for a transient condition.
(1) alone fixes the reported symptom; (2) and (3) make the watcher resilient to the
whole class of "rebuild hit a transient error" situations.
Workaround
Drop --auto-refresh from the serve args and refresh the graph out-of-band
(we run madar generate . --update from a session hook). Without --auto-refresh
the gate is never consulted, so stale-but-valid results keep flowing.
Alternatives considered
No response
Area
extraction / parsers
What would success look like?
Graph update would be done successfully
Problem to solve
When
madar watchis running and a git operation removes files (branch switch,merge, rebase, reset), the incremental rebuild throws
ENOENTand the watcherlatches into
status: "failed"permanently. From that pointserve --auto-refreshrejects every query, even though a valid
out/graph.jsonis still on disk.A file disappearing mid-rebuild is a normal race with git — not an error condition.
Version:
@lubab/madar@0.32.0(global install, Node 24.13.0, macOS)Reproduction
Start
madar watchon a repo, and an MCP client withserve --stdio --auto-refresh.git checkout <other-branch>where the target branch does not contain some filepresent on the current branch (in our case a
*.spec.tsdeleted between branches).The watcher logs:
[madar watch] Rebuild failed: ENOENT: no such file or directory, open ''
Every subsequent MCP call fails with:
Madar auto-refresh cannot guarantee a fresh graph
(status=idle, coverage=complete, policy=match,
background_failure=[madar watch] Rebuild failed: ENOENT: ...)
Restarting the client does not help — the watcher wedges again on the next
git operation.
Observed
out/watcher-state.json:{ "status": "failed", "coverage": "failed", "reconciliation_count": 7982, "failure_reason": "[madar watch] Rebuild failed: ENOENT: no such file or directory, open '.../password-validation.util.spec.ts'" }In a day of normal branch work this made the MCP server unusable for hours at a time.
Root cause
dist/src/infrastructure/watch.js (~L426) has exactly one benign-error branch:
catch (error) {
const message = error instanceof Error ? error.message : String(error);
if (message.includes('No supported files were found')) {
output.log('[madar watch] No supported files found - nothing to rebuild.');
return false;
}
output.error(
[madar watch] Rebuild failed: ${message}); // ENOENT lands herereturn false;
}
There is no ENOENT handling anywhere in watch.js. The failure is then persisted
(~L820):
state.status = 'failed';
state.failure_reason = 'Automatic graph rebuild failed; the graph must not be treated
as fresh.';
and serve --auto-refresh refuses to answer while that flag is set.
Two separate problems compound here:
whenever git rewrites the working tree.
Without (2), (1) would degrade to "slightly stale" instead of "completely down".
Proposed solution
Suggested fixes
skip-and-continue, not a failure. The next reconciliation picks up the new tree anyway.
retry on the next filesystem event rather than requiring manual
madar generate . --update.
graph plus a stale: true marker so callers can decide. Refusing all queries is a
heavy penalty for a transient condition.
(1) alone fixes the reported symptom; (2) and (3) make the watcher resilient to the
whole class of "rebuild hit a transient error" situations.
Workaround
Drop --auto-refresh from the serve args and refresh the graph out-of-band
(we run madar generate . --update from a session hook). Without --auto-refresh
the gate is never consulted, so stale-but-valid results keep flowing.
Alternatives considered
No response
Area
extraction / parsers
What would success look like?
Graph update would be done successfully