User story
As a developer, I want workspace discovery to finish within an explicit bound and explain any failure, so that Madar cannot hang or silently treat a failed Git command as a non-repository directory.
Acceptance criteria
Status and boundary — 10 September 2026
Open; implementation remains paused. Bounding execution and identifying failure are one user outcome. Limit work to workspace discovery and its callers; this is not a repository-wide subprocess rewrite.
Static inspection still finds unbounded execution and catch-all fallback in next workspace discovery. No test was rerun for this triage.
Independent of the product-usefulness story #754. It blocks that work only if discovery actually prevents execution. Planning updates do not resume coding; future source/test implementation remains assigned to Codex CLI.
Historical issue body — preserved; current disposition above takes precedence
Current disposition — 5 September 2026
OPEN — independent process-policy maintenance. Current roadmap: #740. This issue retains its bounded workspace Git discovery policy scope and remains independent of #741's research and evaluation-contract preparation. It is not a demonstrated cause of the #736 product result and blocks a future evaluation only if workspace discovery actually prevents that evaluation. Refresh the invocation inventory before implementation. This reconciliation does not authorize coding or automatically resume implementation.
The original scope and acceptance criteria below, and all existing comments, are preserved as history.
Parent: #654
Surfaced by: review of PR #696 (#695)
Target branch: next
Outcome
Give synchronous workspace Git discovery an explicit process policy: a bounded execution time and an error contract that distinguishes "not a Git repository" from "Git was killed after exceeding its bound".
Both halves are required. Fixing either alone makes the code worse.
Confirmed evidence
src/shared/workspace.ts:30:
function gitPath(rootPath: string, args: string[]): string | null {
try {
const value = execFileSync('git', ['-C', rootPath, ...args], {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe'],
windowsHide: true,
}).trim()
return value.length > 0 ? value : null
} catch {
return null
}
}
Verified directly: grep -c timeout src/shared/workspace.ts → 0, with 4 gitPath() call sites.
Two independent defects sit in those twelve lines.
No bound. execFileSync sets encoding, stdio and windowsHide but no timeout, so a wedged Git blocks the calling thread indefinitely. Every resolveMadarWorkspace() spawns up to three of these, and generateGraph() reaches the same path through resolveMadarOutputDirectory() (src/infrastructure/generate.ts:360). Any Madar command performing workspace discovery can hang with no recovery.
Errors are swallowed. catch { return null } collapses every failure — missing Git binary, permission error, corrupt repository, killed process — into the same null that legitimately means "this is not a Git checkout".
These interact, which is why they must be fixed together. Adding a timeout on its own would convert a hang into a silent null: the caller could not distinguish a directory that is not a repository from one where Git was killed mid-discovery, and Madar would proceed with a confidently wrong workspace resolution instead of hanging visibly. Trading a detectable hang for an undetectable wrong answer is not an improvement.
Why this is separate from #695
PR #696 is deliberately test-only. It bounds the Git commands its own fixture invokes and now states plainly that indirect discovery is unbounded — that boundary is documented rather than papered over.
The observed Windows failure that motivated #695 was an aggregate elapsed-time verdict, not a hung Git process: the slowest individual Git command across the six-lane matrix was ~177 ms. So no evidence currently links this gap to that failure.
It is nonetheless the larger exposure. Measurements in #695 showed indirect spawns dominate Windows cost, so the unbounded path is exactly where the risk concentrates.
Scope
Cover every workspace Git command, not only gitPath(). Decide and document:
- timeout and termination semantics, per command class rather than one arbitrary global value;
- what a timeout returns or throws, and how callers distinguish it from "not a repository";
- error compatibility for existing callers that rely on
null;
- platform behavior, given that Windows process handling differs materially;
- direct and indirect callers, including
resolveMadarWorkspace() and generateGraph();
- production and test diagnostics — a killed Git should be attributable;
- cancellation, if a caller can abandon discovery.
Note that some production Git calls elsewhere already pass timeout, so the codebase is inconsistent rather than uniformly unbounded. Reconcile deliberately.
Explicit non-goals
Acceptance criteria
Relationship to the roadmap
This does not block #654 unless new evidence shows an accepted qualification failure originated in one of these indirect commands. No such evidence exists today.
Related parent: #654. Refs #695.
User story
As a developer, I want workspace discovery to finish within an explicit bound and explain any failure, so that Madar cannot hang or silently treat a failed Git command as a non-repository directory.
Acceptance criteria
Status and boundary — 10 September 2026
Open; implementation remains paused. Bounding execution and identifying failure are one user outcome. Limit work to workspace discovery and its callers; this is not a repository-wide subprocess rewrite.
Static inspection still finds unbounded execution and catch-all fallback in next workspace discovery. No test was rerun for this triage.
Independent of the product-usefulness story #754. It blocks that work only if discovery actually prevents execution. Planning updates do not resume coding; future source/test implementation remains assigned to Codex CLI.
Historical issue body — preserved; current disposition above takes precedence
Current disposition — 5 September 2026
OPEN — independent process-policy maintenance. Current roadmap: #740. This issue retains its bounded workspace Git discovery policy scope and remains independent of #741's research and evaluation-contract preparation. It is not a demonstrated cause of the #736 product result and blocks a future evaluation only if workspace discovery actually prevents that evaluation. Refresh the invocation inventory before implementation. This reconciliation does not authorize coding or automatically resume implementation.
The original scope and acceptance criteria below, and all existing comments, are preserved as history.
Parent: #654
Surfaced by: review of PR #696 (#695)
Target branch:
nextOutcome
Give synchronous workspace Git discovery an explicit process policy: a bounded execution time and an error contract that distinguishes "not a Git repository" from "Git was killed after exceeding its bound".
Both halves are required. Fixing either alone makes the code worse.
Confirmed evidence
src/shared/workspace.ts:30:Verified directly:
grep -c timeout src/shared/workspace.ts→ 0, with 4gitPath()call sites.Two independent defects sit in those twelve lines.
No bound.
execFileSyncsetsencoding,stdioandwindowsHidebut notimeout, so a wedged Git blocks the calling thread indefinitely. EveryresolveMadarWorkspace()spawns up to three of these, andgenerateGraph()reaches the same path throughresolveMadarOutputDirectory()(src/infrastructure/generate.ts:360). Any Madar command performing workspace discovery can hang with no recovery.Errors are swallowed.
catch { return null }collapses every failure — missing Git binary, permission error, corrupt repository, killed process — into the samenullthat legitimately means "this is not a Git checkout".These interact, which is why they must be fixed together. Adding a
timeouton its own would convert a hang into a silentnull: the caller could not distinguish a directory that is not a repository from one where Git was killed mid-discovery, and Madar would proceed with a confidently wrong workspace resolution instead of hanging visibly. Trading a detectable hang for an undetectable wrong answer is not an improvement.Why this is separate from #695
PR #696 is deliberately test-only. It bounds the Git commands its own fixture invokes and now states plainly that indirect discovery is unbounded — that boundary is documented rather than papered over.
The observed Windows failure that motivated #695 was an aggregate elapsed-time verdict, not a hung Git process: the slowest individual Git command across the six-lane matrix was ~177 ms. So no evidence currently links this gap to that failure.
It is nonetheless the larger exposure. Measurements in #695 showed indirect spawns dominate Windows cost, so the unbounded path is exactly where the risk concentrates.
Scope
Cover every workspace Git command, not only
gitPath(). Decide and document:null;resolveMadarWorkspace()andgenerateGraph();Note that some production Git calls elsewhere already pass
timeout, so the codebase is inconsistent rather than uniformly unbounded. Reconcile deliberately.Explicit non-goals
Acceptance criteria
nullare audited and updated or explicitly preserved.Relationship to the roadmap
This does not block #654 unless new evidence shows an accepted qualification failure originated in one of these indirect commands. No such evidence exists today.
Related parent: #654. Refs #695.