fix: implement P0 architecture reliability fixes#31
Conversation
There was a problem hiding this comment.
Pull request overview
This PR focuses on improving reliability and architectural boundaries across the readiness/batch workflows and Copilot SDK session handling, reducing extension-facing coupling and tightening error/session lifecycle behavior.
Changes:
- Extracted per-repo batch readiness processing into
src/services/batch.tsand simplified the Batch Readiness TUI to call the service. - Propagated repo
analysisandappsthrough the readiness → policy engine bridge to avoid stubbed readiness contexts. - Improved Copilot SDK session error handling by capturing auth/login session errors and ensuring sessions are always destroyed; exported the default permission handler and updated tests to use the real implementation.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| src/ui/BatchReadinessTui.tsx | Removes inline clone/readiness logic; delegates per-repo work to a service and fixes stale state updates in input handlers. |
| src/services/batch.ts | Adds processBatchReadinessRepo() + result types and reuses token-bearing error sanitization. |
| src/services/analyzer.ts | Removes dependency on services/git (and transitively simple-git) by inlining git-repo detection. |
| src/services/readiness.ts | Passes analysis and apps into the policy engine context for shadow runs. |
| src/services/policy/types.ts | Extends PolicyContext to optionally include analysis and apps. |
| src/services/policy/compiler.ts | Forwards real analysis/apps into the readiness bridge when available; otherwise uses safe fallbacks. |
| src/services/instructions.ts | Captures auth/login session errors from SDK callbacks; always destroys sessions via finally. |
| src/services/evalScaffold.ts | Same session-error capture pattern; throws captured auth/login errors after session teardown. |
| src/services/copilotSdk.ts | Exports attachDefaultPermissionHandler so it can be tested directly. |
| src/services/tests/copilotSdk.test.ts | Updates tests to import and validate the real permission handler implementation. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 12 out of 12 changed files in this pull request and generated 3 comments.
Comments suppressed due to low confidence (1)
src/services/instructions.ts:373
- Same issue as
generateCopilotInstructions:sessionErroris only thrown after a successfulsendAndWait. IfsendAndWaitthrows (e.g., due to an auth/login failure), the code won’t reach theif (sessionError)line, so callers may see an unhelpful/raw error instead of the intended friendly message. Handle this by catchingsendAndWaiterrors and preferringsessionErrorwhen present.
progress(`Analyzing area "${area.name}"...`);
try {
await session.sendAndWait({ prompt }, 180000);
} finally {
await session.destroy();
}
if (sessionError) throw sessionError;
…sendAndWait rejects
| export async function analyzeRepo(repoPath: string): Promise<RepoAnalysis> { | ||
| const files = await safeReadDir(repoPath); | ||
| let isGit = false; | ||
| try { | ||
| await fs.access(path.join(repoPath, ".git")); | ||
| isGit = true; | ||
| } catch { | ||
| // not a git repo | ||
| } | ||
| const analysis: RepoAnalysis = { | ||
| path: repoPath, | ||
| isGitRepo: await isGitRepo(repoPath), | ||
| isGitRepo: isGit, |
There was a problem hiding this comment.
This changes isGitRepo semantics from “any path inside a git work tree” to “repoPath has a .git entry”. That will incorrectly report false when analyzing a subdirectory within a repo (common in monorepos / area paths), and can also diverge from previous behavior where git-worktree detection was more robust. Consider implementing a lightweight check that preserves the prior semantics without reintroducing the ./git dependency (e.g., invoking git rev-parse --is-inside-work-tree via child_process, or walking up parent directories looking for .git).
| function policyCtxToReadinessCtx(ctx: PolicyContext): ReadinessContext { | ||
| return { | ||
| repoPath: ctx.repoPath, | ||
| analysis: { | ||
| analysis: ctx.analysis ?? { | ||
| path: ctx.repoPath, | ||
| isGitRepo: true, | ||
| languages: [], | ||
| frameworks: [], | ||
| isMonorepo: false | ||
| }, | ||
| apps: [], | ||
| apps: ctx.apps ?? [], |
There was a problem hiding this comment.
When ctx.analysis is absent, the stub sets isGitRepo: true, which can produce objectively incorrect downstream behavior (e.g., git-scoped checks passing/triggering when running against a non-git directory). A safer fallback would be false (or a lightweight check), keeping the stub conservative rather than optimistic.
| setSelectedOrgIndices((prev) => { | ||
| const next = new Set(prev); | ||
| if (next.has(cursorIndex)) { | ||
| next.delete(cursorIndex); | ||
| } else { | ||
| next.add(cursorIndex); | ||
| } | ||
| return next; | ||
| }); |
There was a problem hiding this comment.
The selection toggle still relies on cursorIndex from the render closure. If the input handler can fire faster than React/Ink commits the cursor update (e.g., rapid key presses), this can toggle the wrong item even though setCursorIndex is now functional. Consider reading the current cursor from a ref that is kept in sync with state (or otherwise ensuring the handler always references the latest cursor index) before mutating the selection set.
| try { | ||
| await session.sendAndWait({ prompt }, 180000); | ||
| } catch (err) { | ||
| sendError = err; | ||
| } finally { | ||
| await session.destroy(); | ||
| } | ||
| if (sessionError) throw sessionError; | ||
| if (sendError !== undefined) | ||
| throw sendError instanceof Error ? sendError : new Error(String(sendError)); |
There was a problem hiding this comment.
If session.destroy() rejects, it will throw from the finally block and can mask the more actionable sessionError/sendError being captured. Consider wrapping session.destroy() in its own try/catch (or capturing destroy errors separately) so the primary failure reason is preserved. The same pattern appears in other session-based helpers (e.g., eval scaffold).
| try { | |
| await session.sendAndWait({ prompt }, 180000); | |
| } catch (err) { | |
| sendError = err; | |
| } finally { | |
| await session.destroy(); | |
| } | |
| if (sessionError) throw sessionError; | |
| if (sendError !== undefined) | |
| throw sendError instanceof Error ? sendError : new Error(String(sendError)); | |
| let destroyError: unknown; | |
| try { | |
| await session.sendAndWait({ prompt }, 180000); | |
| } catch (err) { | |
| sendError = err; | |
| } finally { | |
| try { | |
| await session.destroy(); | |
| } catch (err) { | |
| destroyError = err; | |
| } | |
| } | |
| if (sessionError) throw sessionError; | |
| if (sendError !== undefined) | |
| throw sendError instanceof Error ? sendError : new Error(String(sendError)); | |
| if (destroyError !== undefined) | |
| throw destroyError instanceof Error ? destroyError : new Error(String(destroyError)); |
| include: | ||
| - node: 22 | ||
| os: windows-latest |
There was a problem hiding this comment.
This removes macOS from the CI matrix, reducing platform coverage. If this is intentional (cost/flake mitigation), consider adding a brief comment in the workflow explaining why macOS was dropped (or limiting macOS to a scheduled workflow) to avoid accidental long-term regressions on that platform.
Summary
analyzer -> git.tscoupling for git-repo detection to avoid pullingsimple-gitinto extension-facing analyzer usageBatchReadinessTuikeyboard handlersanalysisandappsthroughPolicyContextand compiler readiness bridgingBatchReadinessTuiintosrc/services/batch.tsand sanitize token-bearing errorsattachDefaultPermissionHandlerimplementation by exporting and importing it incopilotSdk.test.tsValidation
npm run typecheck— passesnpm run lint— passesnpm run test— 529/529 tests pass