Skip to content

fix(audit): resolve issue #157 — coordinator replay, store cycles, CI gates#177

Merged
johannesjo merged 4 commits into
johannesjo:mainfrom
ASRagab:fix/quality-audit-findings
Jun 15, 2026
Merged

fix(audit): resolve issue #157 — coordinator replay, store cycles, CI gates#177
johannesjo merged 4 commits into
johannesjo:mainfrom
ASRagab:fix/quality-audit-findings

Conversation

@ASRagab

@ASRagab ASRagab commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Closes the highest-priority correctness and quality-gate items from issue #157. Each commit maps to one finding; tests, typecheck, lint, lint:arch, lint:dead, and format:check all pass locally.

Summary

  • Finding 1 — wait_for_signal_done timeout bypassed the replay cache. Collapsed the two terminal paths (resolver and timeout) into a single idempotent complete(result) so timer cleanup, replay-cache write, anySignalResolvers removal, finishSignalWait, and promise resolution all happen once. Removed the now-redundant finishSignalWait calls in the three external paths that consume a resolver (PTY exit handler, cleanupChildTask, signalDone) so the active-wait count is not double-decremented. unregisterCoordinator now snapshots the resolver list before fan-out because complete splices itself out of the live array.
  • Finding 2 — store import cycles. Extracted the focus-registry getters/setters into src/store/focused-panel.ts and moved removeProjectWithTasks into src/store/project-cleanup.ts. projects.ts no longer imports closeTask, and navigation/tasks/terminals/ui import focus helpers directly from focused-panel instead of round-tripping through focus.ts. focus.ts re-exports the moved symbols for existing component imports. The shared electron/mcp/prompt-detect.ts (pure regex helpers reused by the renderer task-status pipeline) is whitelisted alongside channels.ts in .dependency-cruiser.cjs.
  • Finding 3 — CI gates. Replaced the typecheck/lint/format-only job with npm run check (which also runs compile), npm run lint:arch, npm run lint:dead, and npm test.
  • Finding 7 — Knip. Dropped the unused @types/dompurify devDep (dompurify v3 ships its own types) and registered semgrep/gitleaks under ignoreBinaries. npm run lint:dead now exits 0.
  • Finding 38 — OpenSpec. No code change required: upstream commit 1a65f3a already retired the OpenSpec workflow, and the only remainder (openspec/changes/add-focus-mode-shortcut/.claude/) is gitignored.
  • Finding 41 — .npmrc warnings and semgrep preflight. Removed the pnpm-only onlyBuiltDependencies block that triggered Unknown project config warnings on every npm invocation, and added a semgrep --version preflight to scripts/test-semgrep-filesystem-safety.mjs so a missing binary yields an install hint instead of a spawnSync ENOENT node stack trace.

Test plan

  • npm run check (compile + typecheck + lint + format:check)
  • npm run lint:archno dependency violations found (346 modules, 1096 dependencies cruised)
  • npm run lint:dead — exits 0 (only knip "Configuration hints" remain, which are informational)
  • npm test1413 passed | 24 skipped (1437)
  • Added retry-after-timeout coverage for waitForSignalDone in electron/mcp/coordinator.test.ts
  • node scripts/test-semgrep-filesystem-safety.mjs with semgrep absent now prints an install hint and exits 1 (instead of dumping internals)

🤖 Generated with Claude Code

ASRagab and others added 4 commits June 13, 2026 13:38
…fy types

- .npmrc: remove pnpm-only `onlyBuiltDependencies` block that triggered
  `Unknown project config` warnings on every npm invocation.
- scripts/test-semgrep-filesystem-safety.mjs: add preflight that exits 1
  with an install hint when `semgrep` is missing, instead of spawnSync
  ENOENT with a wall of node internals.
- knip: declare `semgrep`/`gitleaks` as ignored binaries and drop the
  unused `@types/dompurify` devDep (dompurify v3 ships its own types).

Addresses findings 7 and 41 of issue johannesjo#157.

Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
…rator

`npm run lint:arch` failed with 7 circular store imports plus a renderer-
imports-main violation. Untangled without behavior changes:

- `src/store/focused-panel.ts` (new): the focus-registry getters/setters
  (`triggerFocus`, `getTaskFocusedPanel`, `setTaskFocusedPanel`,
  `isPanelFocused`, `defaultPanelFor`, `aiTerminalPanels`, etc.) live
  here. `focus.ts` keeps grid navigation and re-exports the moved
  symbols for backward compatibility with existing call sites.
- `src/store/project-cleanup.ts` (new): `removeProjectWithTasks` is now
  the lone orchestrator that depends on both projects and tasks, so
  `projects.ts` no longer needs to import `closeTask`.
- Updated `navigation`, `tasks`, `terminals`, `ui` (and `ui.test.ts`
  mock target) to import focused-panel helpers from the new module
  instead of round-tripping through `focus.ts`.

After this change the projects↔tasks, persistence chain, navigation↔
focus, and focus↔tasks cycles are gone.

- .dependency-cruiser.cjs: `electron/mcp/prompt-detect.ts` is a pure
  regex module already shared with the renderer task-status pipeline;
  whitelist it next to `electron/ipc/channels.ts` so the rule's intent
  (block Node/Electron-bound code) remains while the documented shared
  module is allowed.

Addresses finding 2 of issue johannesjo#157.

Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
…lete() path

`waitForSignalDone` had two terminal paths: the registered `wrapped`
resolver (which cleared the timer, wrote the replay cache, and
resolved) and the timeout callback (which removed the resolver,
finished the wait, but \*did not\* write the replay cache before
resolving). A client retry that reused the same requestId after a
timeout would therefore re-enter the wait path and register a fresh
resolver, inflating `activeSignalWaitCounts` and re-blocking the
coordinator's notification batching.

- Collapse both paths into a single idempotent `complete(result)` that
  clears the timer, removes itself from `anySignalResolvers`, calls
  `finishSignalWait`, writes the replay cache when `requestId` is set,
  and resolves the promise. The `settled` guard makes repeated calls a
  no-op so external callers can keep shifting the resolver out before
  invoking it.
- Drop the now-redundant `finishSignalWait` calls in the three external
  paths that consume a resolver (PTY exit, cleanupChildTask, signalDone)
  — `complete` handles bookkeeping itself, so leaving the manual call
  in place would double-decrement the wait count.
- In `unregisterCoordinator`, snapshot the resolvers before fan-out
  because `complete` now splices itself from the live array.
- Add a coordinator.test.ts case: same requestId after a timeout
  returns the cached timed-out result quickly instead of starting a
  new live wait.

Addresses finding 1 of issue johannesjo#157.

Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
CI previously ran only typecheck/lint/format and skipped both the
electron compile (`npm run compile` is the closest thing to a build
gate for main-process code) and the test suite. After the cycle fix
and knip cleanup, the static gates are green, so wire them in:

- `npm run check` — covers compile + typecheck + lint + format:check.
- `npm run lint:arch` — depcruise enforcement, now green.
- `npm run lint:dead` — knip, now green.
- `npm test` — vitest run across renderer and main suites.

Addresses finding 3 of issue johannesjo#157.

Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
@johannesjo

Copy link
Copy Markdown
Owner

Thank you very much! <3

@johannesjo johannesjo merged commit f539d96 into johannesjo:main Jun 15, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants