# MAESTRO 全機能カタログ & テストカバレッジ

## サマリ

502
機能総数 (Total features)
352
FULL カバー済み
124
PARTIAL 一部のみ
26
NONE 未カバー

領域 (Area) | 機能数 | FULL | PARTIAL | NONE
--- | --- | --- | --- | ---
Engine core | 65 | 52 | 9 | 4
Tools | 56 | 32 | 22 | 2
Pieces | 32 | 18 | 13 | 1
Reflection | 61 | 51 | 10 | 0
Bridge core | 50 | 39 | 9 | 2
Bridge auth & spaces | 66 | 48 | 17 | 1
Services & DB | 87 | 69 | 13 | 5
UI | 85 | 43 | 31 | 11
合計 | 502 | 352 | 124 | 26

本ページの数値は 8 つのインベントリ表をパースして算出した実数（502 件）。設計仕様 (design spec) の概算目標値は 483 件 (FULL 318 / PARTIAL 137 / NONE 29)。差分はインベントリ確定時の項目追加・分類更新による。

## 機能一覧 (Master feature table)

ID | 領域 | 機能 | 挙動 | ソース | 既存テスト | カバレッジ | ギャップ
--- | --- | --- | --- | --- | --- | --- | ---

`ENG-001`

Engine core

Parallel read-tool execution

Consecutive PARALLEL_SAFE tools run via `Promise.all`; side-effecting tools act as a sequential barrier

`agent-loop.ts (`canExecuteInParallel`, `PARALLEL_SAFE_TOOL_NAMES`, executeMovement)`

agent-loop.test.ts "runs consecutive safe tool calls in parallel"; "keeps side-effecting tools sequential as a barrier"; "executes regular tools before transition even if transition appears mid-batch"

FULL

—

`ENG-002`

Engine core

`transition` tool (non-terminal)

`buildTransitionTool` enum = only `rules[].next`; `validateTransition` rejects targets outside rules; `processTransitionCalls` records lessons

`agent-loop.ts (`buildTransitionTool`, `validateTransition`, `processTransitionCalls`)`

agent-loop.test.ts "non-terminal transition (movement-to-movement) still works"; "transition({next_step:…})"; "lets transition/complete win even if it shares a batch with a repeated call" (tool-loop)

PARTIAL

`buildTransitionTool` / `validateTransition` exercised only indirectly via executeMovement; no unit test that an out-of-rules `next` is rejected in isolation

`ENG-003`

Engine core

`complete` tool — status routing

success→COMPLETE, aborted→ABORT, needs_user_input→ASK via `COMPLETE_STATUS_TO_NEXT` / `buildMovementResultFromComplete`

`agent-loop.ts (`buildCompleteTool`, `parseCompleteArgs`, `buildMovementResultFromComplete`)`

agent-loop.test.ts §7.1 "success status…becomes movement output"; "aborted status routes via ABORT next"; "needs_user_input routes via ASK next"

FULL

—

`ENG-004`

Engine core

`complete` arg validation

success requires non-empty result; aborted requires abort_reason; needs_user_input requires missing_info; invalid → forced retry (no accumulatedText fallback)

`agent-loop.ts (`parseCompleteArgs`, `validateCompleteArgs`)`

agent-loop.test.ts §7.1 "rejects success with empty result"; "rejects aborted without abort_reason"; (needs_user_input/missing_info path covered via routing test)

PARTIAL

No explicit test that `needs_user_input` with empty `missing_info` is rejected (validateCompleteArgs branch); covered for success/aborted only

`ENG-005` | Engine core | Multiple `complete` precedence | 0 completes → continue; >1 conflicting args → retry; >1 identical → first used | `agent-loop.ts (complete dedup logic ~L1503-1535)` | agent-loop.test.ts §7.2 "invalid native complete forces retry"; "two native completes with conflicting args → retry"; "two native completes with identical args → first one used" | FULL | —
`ENG-006` | Engine core | Conversation-history integrity on retry | Every `tool_use` id gets a matching `tool_result` even when complete is rejected | `agent-loop.ts (retry path)` | agent-loop.test.ts §7.7 "all tool_use ids get a tool_result on retry" | FULL | —

`ENG-007`

Engine core

Tool-call loop detection (intra-movement)

Identical tool-call batch repeated ≥ `maxToolLoopRepeats` (default 5) → ABORT; counter resets on arg change

`agent-loop.ts (`DEFAULT_MAX_TOOL_LOOP_REPEATS`, `buildToolLoopAbortMessage`, consecutiveToolRepeats)`

agent-loop.tool-loop.test.ts (5 tests: limit abort, custom limit, under-limit, arg-change reset, transition wins)

FULL

—

`ENG-008` | Engine core | Loop-watchdog one-shot warning | At `maxToolLoopRepeats-2` (min 2) inject a single `[loop watchdog]` reminder before aborting | `agent-loop.ts (`warnAt`, `toolLoopWarned`, `injectLoopWarning`)` | (implicit in tool-loop tests' under-limit path) | PARTIAL | No assertion that the watchdog warning message is actually injected exactly once before abort
`ENG-009` | Engine core | Max-iterations abort | Movement exceeding `safety.maxIterations` aborts with `buildMaxIterationsAbortMessage` | `agent-loop.ts (`buildMaxIterationsAbortMessage`)` | agent-loop.test.ts "aborts when maxIterations is exceeded" | FULL | —

`ENG-010`

Engine core

Text-only-response handling

NON-terminal movement aborts after N text-only turns; STRICTLY-terminal (defaultNext COMPLETE, no rules) salvages prose as completion; counter resets on tool use

`agent-loop.ts (`TEXT_ONLY_REMIND_EMPTY`, textOnlyOutcome)`

agent-loop.test.ts "aborts after text-only responses in a NON-terminal movement"; "salvages text-only prose…STRICTLY-terminal"; "does NOT salvage…still has rules"; "resets text-only counter when tool calls happen"

FULL

—

`ENG-011`

Engine core

Context-overflow forced transition (runtime usage)

ContextManager threshold 95% → `force_transition` builds result via `movement.defaultNext`; falls to ABORT when defaultNext is terminal/absent

`agent-loop.ts (`buildContextOverflowResult`, `buildForceTransitionResult`)`

agent-loop.test.ts "triggers force_transition when context manager signals exhaustion"; "fires onContextAction callback when context threshold crossed"

FULL

—

`ENG-012`

Engine core

Oversized initial prompt guard

Prompt over budget pre-send → dedup→compact→summarize; aborts/force-transitions when still oversized

`agent-loop.ts + context/prompt-guard.ts (`guardPromptBeforeSend`)`

agent-loop.test.ts "aborts when initial prompt is oversized and defaultNext is terminal"; "falls back to ABORT when oversized prompt has no defaultNext"; prompt-guard.test.ts (full stage matrix)

FULL

—

`ENG-013` | Engine core | Cancel signal handling | Already-aborted cancelSignal returns ABORT immediately; mid-movement cancel returns ABORT "cancelled" | `agent-loop.ts (cancelSignal checks)` | agent-loop.test.ts "returns ABORT immediately when cancelSignal is already aborted" | PARTIAL | Mid-iteration cancel (after work started) not asserted at agent-loop level; only the pre-loop case
`ENG-014` | Engine core | ContextManager threshold ladder | 70%→warn, 85%→prompt, 95%→force_transition; each fires once; isExhausted ≥0.99 | `context-manager.ts (`ContextManager`)` | context-manager.test.ts (13 tests: each threshold, fire-once, defaults, limitTokens, isExhausted, hasUsageData, char fallback) | FULL | —
`ENG-015` | Engine core | Ollama context-limit autodetect | `fetchOllamaContextLimit` prefers num_ctx > model_info.context_length; llama.cpp /props fallback; default on failure | `context-manager.ts` | context-manager.test.ts (5 tests incl. num_ctx precedence, /props fallback, n_ctx absent) | FULL | —

`ENG-016`

Engine core

Tool catalog injection into system prompt

`buildSystemPrompt` / `buildGlobalPreamble` auto-inject available-tools list + 1-line summaries; script section gated on Bash presence

`agent-loop.ts (`buildGlobalPreamble`, `buildSystemPrompt`)`

agent-loop.test.ts "injects the script section when Bash is among the presented tools"; "omits…when Bash is not presented"; preamble byte-stability tests

FULL

—

`ENG-017`

Engine core

Preamble/guidance split (job-stable vs movement)

`buildGlobalPreamble` byte-identical across movements; `buildMovementGuidance` carries persona/instruction/transitions

`agent-loop.ts`

agent-loop.test.ts "preamble is byte-identical across two different movements"; "guidance carries movement-specific instruction, persona, transitions"; "buildSystemPrompt wrapper still contains both halves"

FULL

—

`ENG-018`

Engine core

Progressive pressure (intra-movement revisit)

visitCount==2 → caution; >=3 → warning injected into guidance

`agent-loop.ts (`buildMovementGuidance` L672)`

agent-loop.test.ts "guidance injects progressive-pressure warning on revisit"

PARTIAL

Only the revisit-warning presence is asserted; the visitCount==2 (caution) vs >=3 (warning) distinction is not separately tested

`ENG-019` | Engine core | Checklist watchdog | One-shot reminder after 5 iterations with no checklist tool; suppressed if CreateChecklist used early | `agent-loop.ts` | agent-loop.test.ts "injects a one-shot reminder after 5 iterations"; "does NOT fire when CreateChecklist is called early"; traceability "watchdog_fire" | FULL | —
`ENG-020` | Engine core | Cross-movement Read cache | Cacheable read served to later movement; errors not cached; non-allowlisted tools (Bash) skipped | `agent-loop.ts + context/tool-result-cache.ts` | agent-loop.test.ts "returns a cached Read result…"; "does not cache error results"; "skips caching tools outside the cacheable allowlist" | FULL | —
`ENG-021` | Engine core | Cache invalidation | Edit/Write invalidate affected path; Grep entries all-evicted on edit; Bash conservatively invalidates all file entries; no invalidate on tool error | `agent-loop.ts + context/invalidation.ts + tool-result-cache.ts` | agent-loop.test.ts Phase 2/4 (7 tests); invalidation.test.ts (5); tool-result-cache.test.ts (28) | FULL | —
`ENG-022` | Engine core | Cache key construction | Deterministic v1-prefixed keys for Read/Grep/Glob/WebFetch/Office; URL scheme/host normalization; workspace isolation | `context/cache-key.ts` | cache-key.test.ts (25); tool-result-cache.test.ts key tests | FULL | —
`ENG-023` | Engine core | File-read dedup | Older Reads of same file replaced with placeholder, most-recent kept; ignores non-Read tools; idempotent | `context/file-read-dedup.ts` | file-read-dedup.test.ts (10) | FULL | —
`ENG-024` | Engine core | History summarization | `summarizeHistory` replaces middle turns with summary, preserves tail; LLM-failure → summarized=false; update-mode with prior summary | `context/history-compactor.ts` | history-compactor.test.ts (21: splitIntoTurns, buildSummaryPrompt, selectTailTurnStartIndex, summarizeHistory, summarizeForceTransition) | FULL | —
`ENG-025` | Engine core | Token estimation | char→token by script class (ASCII/CJK/other), per-message overhead, image budget, tool serialization | `context/token-estimate.ts` | token-estimate.test.ts (23) | FULL | —
`ENG-026` | Engine core | Prompt-guard staged compaction | dedup→compact→summarize ladder; reserve-cap headroom; stage-3 skipped when disabled/no isolated LLM; ok:false when all fail | `context/prompt-guard.ts` | prompt-guard.test.ts (27) | FULL | —
`ENG-027` | Engine core | Conversation class (seed/enter/persist) | seed order (preamble+guidance+task); enterMovement appends guidance w/o reset; flush/loadFrom round-trip; rewrite truncates | `context/conversation.ts` | conversation.test.ts (in-memory + persistence, 8) | FULL | —
`ENG-028` | Engine core | Conversation.replayableTurns | Strips system + control-flow tool_calls (complete/transition) + dangling/orphan tool messages; keeps valid pairs | `context/conversation.ts` | conversation.test.ts (7 replayableTurns cases) | FULL | —
`ENG-029` | Engine core | Conversation.seedContinuation / hasReplayableTranscript | Builds [preamble,guidance,...priorTurns,user] & rewrites transcript; hasReplayableTranscript guards undefined/missing/system-only | `context/conversation.ts` | conversation.test.ts (seedContinuation + 4 hasReplayableTranscript) | FULL | —
`ENG-030` | Engine core | Continuation seed in executeMovement | priorTurns replayed & handoff block suppressed; plain seed when absent | `agent-loop.ts` | agent-loop.test.ts "replays priorTurns and suppresses handoff block"; "without priorTurns, uses plain seed"; "with empty priorTurns array, behaves like plain seed" | FULL | —
`ENG-031` | Engine core | Atomic JSON persistence | writeAtomicJson (tmp+rename, mkdir parents); readSafeJson returns missing/corrupt/wrong-version; quarantineCorruptFile | `context/atomic-json.ts` | atomic-json.test.ts (11) | FULL | —
`ENG-032` | Engine core | Workspace path normalization | Rejects absolute/backslash/drive/UNC/NUL/empty; canonicalizes `..`/`.`; prefixWorkspacePath join with traversal guard | `context/path-normalize.ts` | path-normalize.test.ts (15) | FULL | —

`ENG-033`

Engine core

Piece loop guards (cross-movement)

`enforceLoopGuards`: consecutive revisits > max_consecutive_revisits (default 4) → abort; counters reset on movement change

`piece-runner.ts (`enforceLoopGuards`)`

piece-runner.test.ts "aborts when loop detection fires due to consecutive revisits"

PARTIAL

Per-movement `max_consecutive_revisits` override and the counter-reset-on-movement-change branch not separately asserted

`ENG-034`

Engine core

ASK limit fallback

ASK result resolves `default_next` or first non-self/non-ASK/non-ABORT rule; aborts when no fallback; resolves COMPLETE default_next via ASK-limit fallback

`piece-runner.ts (result.next==='ASK' branch)`

piece-runner.test.ts "falls back to default_next when ASK limit is reached"; "aborts when ASK limit reached and no fallback"; "resolves a COMPLETE default_next reached via the ASK-limit fallback"

FULL

—

`ENG-035` | Engine core | WAIT_SUBTASKS pause | result.next==='WAIT_SUBTASKS' pauses piece run (waiting_subtasks) | `piece-runner.ts (L1144)` | piece-runner.test.ts "does NOT write snapshot on waiting_subtasks (transient pause)" | PARTIAL | The pause/resume return contract is only asserted via the snapshot-suppression side effect; no direct test of the waiting_subtasks PieceRunResult shape

`ENG-036`

Engine core

SpawnSubTask depth-limit skip

Movement requiring SpawnSubTask skipped at depth limit (incl. via shared_tools); resolves COMPLETE default_next on skip

`piece-runner.ts (`pieceUsesSpawn`, spawn skip)`

piece-runner.test.ts "resolves a COMPLETE default_next when a SpawnSubTask movement is skipped at the depth limit"; "skips a SpawnSubTask movement at the depth limit even when…shared_tools"

FULL

—

`ENG-037` | Engine core | WAITING_HUMAN_BROWSER / _TOOL_REQUEST pauses | Non-ASK human-wait sentinels pause the run | `piece-runner.ts (L1159, L1176)` | (none found) | NONE | No test exercises the WAITING_HUMAN_BROWSER or WAITING_HUMAN_TOOL_REQUEST branches

`ENG-038`

Engine core

Review-feedback carry-forward

verify movement output carried into later execute/process/plan/analyze; truncated per/combined caps; git status+diff appended after verify

`piece-runner.ts (`buildInstructionWithFeedback`, `shouldCarryReviewFeedback`, truncate/trim)`

piece-runner.test.ts "carries cumulative verify feedback into later execute/analyze movements"; "appends safe git status and diff context after verify loops"

FULL

—

`ENG-039`

Engine core

Lessons accumulation & injection

transition/complete lessons accumulated, capped (MAX_LESSONS_LENGTH 2000), injected into next movement; written to lessons.jsonl

`piece-runner.ts (`buildLessonsContext`, `writeLessonLog`, lessonsAccumulator)`

(no direct test of buildLessonsContext/writeLessonLog; snapshot tests reference lessons)

PARTIAL

Lessons capture appears in cancel-snapshot tests but the injection-into-next-movement and 2000-char trim logic is not directly asserted; writeLessonLog untested

`ENG-040` | Engine core | loadPiece terminal-rule validation | rejects rules[].next ∈ {COMPLETE,ABORT,ASK}; accepts default_next COMPLETE & WAIT_SUBTASKS sentinel; all bundled pieces valid | `piece-runner.ts (`validatePieceDef`, `loadPiece`)` | piece-runner.test.ts (Phase 6b: 6 tests incl. all bundled pieces load) | FULL | —
`ENG-041` | Engine core | loadPiece multi-dir resolution | custom dir(s) win over builtin; first dir wins on dup name; string|string[] forms | `piece-runner.ts (`loadPiece`)` | piece-runner.test.ts "resolves from a list of custom dirs"; "first dir wins"; "string form still works" | FULL | —
`ENG-042` | Engine core | normalizeRequiredMcp | retains valid slugs, drops invalid, undefined when absent, coerces non-array to [] | `piece-runner.ts` | piece-runner.test.ts (4 required_mcp tests) | FULL | —
`ENG-043` | Engine core | normalizeSharedTools / mergeToolNames | shared_tools sanitized; mergeToolNames unions shared+movement first-seen order, drops dups/non-strings | `piece-runner.ts` | piece-runner.test.ts (shared_tools 4 + mergeToolNames 3) | FULL | —
`ENG-044` | Engine core | validateAllowedSshConnections | validates id format / wildcard / empty-deny; tolerates missing allowlist (A4 workspace-policy gating); reports offenders | `piece-runner.ts` | piece-runner.test.ts (allowed_ssh_connections 15 + loader-tolerance 1) | FULL | —
`ENG-045` | Engine core | Workspace tool-policy resolution & injection | resolveWorkspaceTools (safe default, Bash/browser opt-in); runPiece injects workspaceTools overriding movement.allowed_tools; grantedTools unioned | `piece-runner.ts (runPiece) + workspace-tool-policy.ts` | piece-runner.test.ts (resolveWorkspaceTools 5 + runPiece injection 4); workspace-tool-policy.test.ts | FULL | —
`ENG-046` | Engine core | runPiece max_movements default | iterates when max_movements missing / 0 / negative | `piece-runner.ts (runPiece)` | piece-runner.test.ts "still iterates when piece.max_movements is missing"; "…is 0 or negative" | FULL | —
`ENG-047` | Engine core | buildFollowupNotice | detects follow-up when output/ or subtasks/ has non-hidden files; ignores dotfiles; empty for fresh ws | `piece-runner.ts` | piece-runner.test.ts (5 buildFollowupNotice tests) | FULL | —
`ENG-048` | Engine core | Cancel/terminal memory snapshots | snapshot+meta-event on cancelled (pre/mid movement) and aborted; NOT on success or waiting_subtasks; v2 captures finalOutput/history/lessons w/ truncation | `piece-runner.ts` | piece-runner.test.ts (7 snapshot tests) | FULL | —
`ENG-049` | Engine core | Conversation continuity end-to-end | movement 2 sees movement 1 history; transcript.jsonl written; continuation job replays prior turns & suppresses LIMIT-1 handoff | `piece-runner.ts + conversation.ts` | piece-runner.conversation.test.ts (2) | FULL | —
`ENG-050` | Engine core | Piece classification prompt | `buildClassificationPrompt` includes task+descriptions+files; biases toward chat; routes workspace-app | `piece-classifier.ts` | piece-classifier.test.ts (buildClassificationPrompt 4) | FULL | —
`ENG-051` | Engine core | Classification response parsing | `parseClassificationResponse` extracts valid name, handles noise, prefers longest match, null on invalid/empty | `piece-classifier.ts` | piece-classifier.test.ts (parseClassificationResponse 5) | FULL | —
`ENG-052` | Engine core | `classifyPiece` (async LLM orchestration) | Builds prompt, calls LLM, parses + falls back to default piece | `piece-classifier.ts (`classifyPiece`)` | (none) | NONE | The end-to-end classify path (LLM call wiring, fallback when parse returns null, error handling) is untested; only its two pure helpers are
`ENG-053` | Engine core | loadAllPieceTriggers | Loads triggers/keywords from all pieces across dirs; custom wins on dup name; skips invalid pieces | `piece-runner.ts (`loadAllPieceTriggers`)` | (none) | NONE | No test; dir-precedence and skip-on-load-error branches uncovered
`ENG-054` | Engine core | buildChecklistContext | Builds checklist context block from logs root | `piece-runner.ts` | (referenced in piece-runner.test.ts import only; no describe block) | PARTIAL | Symbol imported in test file but no behavior assertion targets it specifically
`ENG-055` | Engine core | buildLocalConversationContext | Renders current instruction + recent-conversation + workspace files; interjection precedence; truncation; omitRecentConversation flag | `local-context.ts` | local-context.test.ts (7) | FULL | —
`ENG-056` | Engine core | PieceCatalog caching/layering | Built-ins + user pieces (custom wins); TTL cache; invalidate(userId); per-folder cache isolation | `piece-catalog.ts` | piece-catalog.test.ts (7) | FULL | —
`ENG-057` | Engine core | stripThinkingTokens | Strips <think>/<|thinking|>/gemma channel blocks; leaves unclosed intact; preserves unicode | `strip-thinking.ts` | strip-thinking.test.ts (11) + agent-loop.test.ts (6) | FULL | —
`ENG-058` | Engine core | prompt-coach scoring | buildCoachMessages (source inclusion + truncation); normalizeCoachResult (clamp/defaults); runPromptCoach (normalize + error propagation) | `prompt-coach.ts` | prompt-coach.test.ts (9) | FULL | —
`ENG-059` | Engine core | Console screen injection | Injects SSH console screen each iteration when SshConsole* allowed + session exists; guards on missing taskId / no session / non-console piece; tail truncation | `agent-loop.ts (buildSystemPrompt)` | agent-loop.test.ts (5 console) + agent-loop-console.test.ts (4) | FULL | —
`ENG-060` | Engine core | Handoff block construction | Static Continue block always; dynamic block only with handoffContext; null prevResult handled; long prevResult truncated head+tail | `agent-loop.ts (buildSystemPrompt)` | agent-loop.test.ts (5 handoff tests) | FULL | —
`ENG-061` | Engine core | Per-user AGENTS.md / memory / working-dir injection | Injects AGENTS.md + persistence protocol + Working Directory + approach/error-recovery sections, gated on userId/workspacePath | `agent-loop.ts (buildSystemPrompt)` | agent-loop.user-agents.test.ts (9) | FULL | —
`ENG-062` | Engine core | Existing-workspace-files injection | Injects existing input-files section when present, omits when empty | `agent-loop.ts (buildSystemPrompt)` | agent-loop.test.ts (2) | FULL | —
`ENG-063` | Engine core | Traceability event emission | movement_start/tool_call/result/movement_complete; cache_set/hit/invalidate; watchdog_fire; run_start/complete; followup_detected; shared runId | `agent-loop.ts + piece-runner.ts` | agent-loop.test.ts (T-1, 5) + piece-runner.test.ts (T-2, 2) | FULL | —
`ENG-064` | Engine core | ownerId/userId defaulting | Defaults to 'local'/synthetic when unowned; preserves real owner in auth mode | `piece-runner.ts (runPiece)` | piece-runner.test.ts "defaults ownerId and userId to…"; "preserves a real ownerId/userId" | FULL | —
`ENG-065` | Engine core | Mission char-budget trimming | `buildGlobalPreamble` trims mission text when total exceeds MISSION_TOTAL_CHAR_BUDGET | `agent-loop.ts (L428 overflow trim)` | (none) | NONE | The mission/preamble char-budget overflow trim is not asserted by any test
`TOOL-001` | Tools | Read | Read file slice (offset/limit); delegates binary detection; rejects directories (EISDIR), enforces readonly/ | `` | core.test.ts (88 it: binary detector, readonly/ enforce, symlink escape) | FULL | EISDIR/dir-reject path (MEMORY: prior crash) not explicitly named in describes — verify a dir-reject test exists
`TOOL-002` | Tools | Write | Write to workspace; `assertWritable` blocks readonly/ + outside-workspace | `` | core.test.ts (readonly/ enforcement, resolveAndGuard symlink escape) | FULL | —
`TOOL-003` | Tools | Edit | Exact-string replace; same write-guard path as Write | `` | core.test.ts | PARTIAL | Edit-specific match-fail / replace_all behavior not clearly enumerated in describes
`TOOL-004` | Tools | Bash | Shell exec; AbortSignal propagation, path-scope guard, install-pattern blocking, unrestricted mode, env scrub, history log | `` | core.test.ts (AbortSignal, checkBashPathScope, install rejection, bashUnrestricted, env scrub, history) | FULL | Strong coverage incl. cancel-traceability
`TOOL-005` | Tools | Glob | Pattern file match | `` | core.test.ts (core tools) | PARTIAL | Few dedicated Glob assertions; edge cases (no-match, ignore) unclear
`TOOL-006` | Tools | Grep | Content search | `` | core.test.ts | PARTIAL | Same — limited Grep-specific cases
`TOOL-007` | Tools | WebSearch | SearXNG search w/ fallback + history; query sanitize | `` | web.test.ts (sanitizeQuery, searchViaSearxng, fallback history, parseSearchResultsFromText) | FULL | —
`TOOL-008` | Tools | WebFetch | Fetch URL → readability text; SSRF guard; persistent context mgmt | `` | web.test.ts (web tools, persistent context), shared/ssrf.test.ts, shared/readability.test.ts, web.binary.test.ts | FULL | SSRF tested at shared layer; binary handling covered
`TOOL-009` | Tools | DownloadFile | Download to input/ or source/; source-library index.jsonl; raw log-only | `` | web.test.ts (source-library routing 2 it), raw-save | PARTIAL | Size-limit / large-file / redirect handling on download not asserted
`TOOL-010` | Tools | ReadImage | Read image → vision; usage tracking | `` | image.test.ts (13 it), image.vision-usage.test.ts | FULL | —
`TOOL-011` | Tools | AnnotateImage | Draw annotations on image | `` | image.test.ts | PARTIAL | Annotation rendering correctness lightly covered
`TOOL-012` | Tools | ReadExcel | Parse xlsx/xls/xlsm/xlsb; size limit 10MB; format-mismatch + CFB/HTML/CSV disguise rejection; optional styles | `` | office.test.ts (format rejection 8 it, styles), excel-styles.test.ts | PARTIAL | **Size-limit (>10MB) enforcement path not tested** — only format mismatch
`TOOL-013` | Tools | ReadDocx | Parse docx/docm; size limit 10MB | `` | office.test.ts | PARTIAL | Size-limit path untested; happy-path light
`TOOL-014` | Tools | ReadPdf | Extract PDF text; query/grep mode (regex, case-insensitive, no-match, empty-query); size limit 10MB; OOXML-as-pdf rejection | `` | office.test.ts (ReadPdf query mode 5 it, format rejection) | PARTIAL | Size-limit path untested
`TOOL-015` | Tools | ReadPPTX | Parse pptx/pptm; slide order resolution; notes; **ZIP-bomb detection (uncompressed >200MB)**; size limit 50MB | `` | office.test.ts | NONE | **ZIP-bomb detection AND PPTX size-limit have NO test** — highest-risk office gap
`TOOL-016` | Tools | ReadMsg | Parse Outlook .msg | `` | msg.test.ts (41 it) | FULL | —
`TOOL-017` | Tools | SplitExcelSheets | Split workbook into per-sheet files | `` | office.test.ts | PARTIAL | Split correctness lightly covered
`TOOL-018` | Tools | SplitDocxSections | Split docx into section files | `` | office.test.ts | PARTIAL | Same
`TOOL-019` | Tools | PdfToImages | Render PDF pages to images; edit-not-allowed error, missing-file error, invalid page_range error | `` | office.test.ts (3 error-path it) | FULL | Error paths covered
`TOOL-020` | Tools | SQLite | Run SQL on workspace .db; SELECT happy path, write guards (editAllowed), always-blocked DDL, PRAGMA, errors, path-traversal guard, large result sets | `` | data.test.ts (25 it across 9 describe) | FULL | Strong — guards + traversal + DDL block
`TOOL-021` | Tools | BatchReviewTextWithLLM | Per-file isolated LLM review; output-path prefix enforcement | `` | review.test.ts (3 it) | PARTIAL | Only 3 it; LLM-failure / partial-file handling not covered
`TOOL-022` | Tools | MergeReviewedResults | Merge reviewed JSON → markdown; rejects outputs outside prefixes | `` | review.test.ts | PARTIAL | Merge edge cases (malformed JSON) untested

`TOOL-023`

Tools

BrowseWeb

Playwright browse + actions mode; URL validation, SSRF/localhost block, file-URL workspace containment, traversal reject, auth-expiry, download filename sanitize, unique-path, recording, source persist

``

browser.test.ts (36 it), browser.runpageactions.test.ts, browser-frame-chain.e2e.test.ts

FULL

Validation/SSRF/recording very strong; live page-action correctness is e2e-gated (env-dependent)

`TOOL-024` | Tools | SpawnSubTask | Spawn parallel subtask job | `` | orchestration.test.ts (8 it) | FULL | —
`TOOL-025` | Tools | XSearch | X/Twitter search | `` | x.test.ts (18 it) | PARTIAL | Live transaction-id breakage (MEMORY) means runtime failures not covered by unit tests
`TOOL-026` | Tools | XUserPosts | X user timeline posts | `` | x.test.ts | PARTIAL | Same upstream breakage caveat
`TOOL-027` | Tools | XPostDetail | Single X post detail | `` | x.test.ts | PARTIAL | Same
`TOOL-028` | Tools | XFetchCardMedia | Fetch X card media | `` | x.test.ts | PARTIAL | Likely thin coverage
`TOOL-029` | Tools | XTimeline | X home/timeline | `` | x.test.ts | PARTIAL | Likely thin coverage
`TOOL-030` | Tools | SearchPlaces | Maps place search | `` | maps.test.ts (27 it) | FULL | —
`TOOL-031` | Tools | GetDirections | Maps routing | `` | maps.test.ts | FULL | —
`TOOL-032` | Tools | ReverseGeocode | Coords → address | `` | maps.test.ts | FULL | —
`TOOL-033` | Tools | GetYouTubeTranscript | Fetch YouTube transcript | `` | youtube.test.ts (20 it) | FULL | —
`TOOL-034` | Tools | SearchYouTube | YouTube search | `` | youtube.test.ts | FULL | —
`TOOL-035` | Tools | SearchAmazon | Amazon product search | `` | amazon.test.ts (13 it) | FULL | —
`TOOL-036` | Tools | TranscribeAudio | Speech-to-text | `` | speech.test.ts (14 it) | FULL | —
`TOOL-037` | Tools | CreateChecklist | Create task checklist (META) | `` | checklist.test.ts (24 it incl. META_TOOL describe) | FULL | —
`TOOL-038` | Tools | CheckItem | Mark checklist item (META) | `` | checklist.test.ts | FULL | —
`TOOL-039` | Tools | GetChecklist | Read checklist (META) | `` | checklist.test.ts | FULL | —
`TOOL-040` | Tools | ListPieces | List available pieces | `` | pieces.test.ts (11 it) | FULL | —
`TOOL-041` | Tools | GetPiece | Read a piece YAML | `` | pieces.test.ts | FULL | —
`TOOL-042` | Tools | CreatePiece | Create custom piece (per-user fork) | `` | pieces.test.ts | PARTIAL | Validation/lint reject paths (COMPLETE/ABORT in next) coverage unclear here
`TOOL-043` | Tools | UpdatePiece | Update custom piece | `` | pieces.test.ts | PARTIAL | Same
`TOOL-044` | Tools | InstallSkill | Install skill from registry (META) | `` | skills.test.ts (26 it) | FULL | —
`TOOL-045` | Tools | ReadSkill | Read skill content (META, always available) | `` | skills.test.ts | FULL | —
`TOOL-046` | Tools | ListSkills | List skills (META) | `` | skills.test.ts | FULL | —
`TOOL-047` | Tools | ReadToolDoc | Read tool doc incl. MCP tools via cache (META, always available); name-required error, bad-MCP-name error | `` | docs.test.ts (10 it) | FULL | —

`XCUT-001`

Tools

Dynamic tool loading + dispatch order

index.ts `tryLoadModule` lazy-imports each module; `executeToolInner` dispatches in fixed order (mcp → web → image → data → office → review → x → orchestration → browser → maps → … → skills); failed loads silently skipped

``

index.test.ts (1 it: SshConsole catalog)

NONE

**Dispatch order, first-match-wins, and silent-skip-on-load-failure are essentially untested** (single catalog test)

`XCUT-002` | Tools | META_TOOLS always-injected at dispatch | index.ts keeps a LARGER META_TOOLS list (incl. RequestTool, Mission, UserFolder, AppDocs, Skills) injected into getToolDefs regardless of movement allowed_tools | `` | (indirect via tool-categories.test.ts subset) | PARTIAL | index.ts's runtime META injection (20 names) not directly asserted; only tool-categories' SUBSET set is tested
`XCUT-003` | Tools | tool-categories: name→category map | first-write-wins map built from ALL_TOOL_DEFS('core') + MODULE_SPECS; memoised; listToolCategories; SENSITIVE_CATEGORIES={ssh,browser}; SENSITIVE_TOOLS={Bash}; META_TOOLS subset | `` | tool-categories.test.ts (12 it: META_TOOLS entries, sensitive sets, map) | FULL | —

`XCUT-004`

Tools

RequestTool flow

META tool; classifies request as already_available / available_not_allowed(requested) / unknown(hard-error); name+reason required; interactive→pending approval, non-interactive→auto_denied; records via ctx.recordToolRequest

``

tool-request.test.ts (9 it), db/repository.tool-requests.test.ts

FULL

All 3 classification branches + interactive/auto-deny covered

`XCUT-005`

Tools

workspace-tool-policy resolution

resolveWorkspaceTools: safe categories default-on (minus disabledSafe), sensitive categories+tools opt-in only (enabledSensitive), META always unioned, Bash special-cased, `mcp__*` always carried; parseToolPolicy tolerant JSON

``

workspace-tool-policy.test.ts, workspace-tool-policy.regression.test.ts, db/repository.tool-policy.test.ts, bridge/space-api.tool-policy.test.ts

FULL

Strong; regression test guards Bash toggle (MEMORY PR #653)

`XCUT-006`

Tools

raw-save result logging

saveRawData writes {logsRoot}/raw/<tool>-<ts>.txt + rawdata-history.jsonl for RAW_SAVE_TOOLS (web/x/youtube/amazon/speech/ms-learn); logRawDownload records DownloadFile path-only; errors swallowed to logger.warn

``

raw-save.test.ts (5 it)

PARTIAL

Helper, not a tool. Per-tool wiring (that each RAW_SAVE_TOOL actually triggers save) is not asserted end-to-end

`XCUT-007` | Tools | structured-blocks | Rich-UI structured-data save helper (no TOOL_DEFS) | `` | structured-blocks.test.ts (4 it) | PARTIAL | Helper; basic coverage
`XCUT-008` | Tools | binary-detect shared helper | Magic-byte/binary detection used by Read + WebFetch | `` | binary-detect.test.ts (26 it) | FULL | Well covered; underpins Read/WebFetch binary handling
`XCUT-009` | Tools | conflict-guard (persistent workspace) | Detects concurrent-edit conflicts in shared space | `` | conflict-guard.test.ts (9 it), core.test.ts (conflict detection) | FULL | —
`PIECE-001` | Pieces | chat |  | `` | piece-classifier.test (as default-bias target); reflection/silent-fork & applier.fuzz use 'chat'; piece-runner all-load sweep | PARTIAL | No test asserts the single-movement respond contract or its 43-tool allowlist; only used as a name fixture + load-sweep
`PIECE-002` | Pieces | general |  | `` | piece-runner.test (`loadPiece('general')` line 401, used in review-prompt/shared_tools tests); pieces-api uses 'general' fixture; load sweep | PARTIAL | Loaded & used as a fixture for unrelated assertions (review prompts, SpawnSubTask skip), but its 4-movement decompose/verify loop is not asserted as a contract
`PIECE-003` | Pieces | research |  | `` | piece-runner.test `loadPiece('research')` (line 403, review-prompt structure assertion); load sweep | PARTIAL | Review-prompt/plan-aware structure asserted for it, but the dig/analyze/verify transition logic is not exercised
`PIECE-004` | Pieces | research-sub |  | `` | load sweep only | PARTIAL | No direct test; only schema-load smoke. Spawn-from-parent linkage untested
`PIECE-005` | Pieces | data-process |  | `` | piece-classifier.test (longer-match-wins: data-process over general); load sweep | PARTIAL | Classifier disambiguation asserted; movement contract not
`PIECE-006` | Pieces | office-process |  | `` | piece-runner.test `loadPiece('office-process')` (line 402); piece-classifier fixture; load sweep | PARTIAL | Loaded for review-prompt assertion; 2-movement contract not exercised
`PIECE-007` | Pieces | slide |  | `` | piece-classifier.test (chat vs slide routing fixture); load sweep | PARTIAL | Used as classifier fixture name; movement/tool contract untested
`PIECE-008` | Pieces | brainstorming |  | `` | load sweep only | PARTIAL | No direct test; schema-load smoke only
`PIECE-009` | Pieces | sns-research |  | `` | load sweep only | PARTIAL | No direct test
`PIECE-010` | Pieces | x-ai-digest |  | `` | load sweep only | PARTIAL | No direct test
`PIECE-011` | Pieces | piece-builder |  | `` | piece-classifier.test (workspace-app routed AWAY from piece-builder); load sweep | PARTIAL | Only the negative-routing case touches it; its own contract untested
`PIECE-012` | Pieces | workspace-app |  | `` | piece-runner.test dedicated movement-structure assertions (build→verify, default_next checks, lines 542-557); piece-classifier routing test (line 41-52); workspace-app E2E harness (separate subsystem) | FULL | Movement structure + default_next + classifier routing all asserted; strongest-covered piece
`PIECE-013` | Pieces | ssh-console |  | `` | piece-runner.test dedicated assertion (interact default_next: COMPLETE, line 563-572); tools/ssh-console.test; ssh allowed_ssh_connections validation tests | FULL | Movement + ssh-connection allowlist contract asserted
`PIECE-014` | Pieces | ssh-ops |  | `` | piece-runner.test dedicated assertion (line 576+); ssh allowlist validation tests; load sweep | FULL | Loaded + structurally asserted (execute/verify); ssh allowlist format validation covered. Lighter than ssh-console but real assertions exist
`PIECE-015` | Pieces | help |  | `` | load sweep only | PARTIAL | No direct test
`PINFRA-001` | Pieces | loadPiece (custom-dir priority + builtin fallback) | Resolves piece by name across custom dirs (string|string[]), custom wins, fallback to builtin | `` | piece-runner.test "loadPiece multi-dir" (string|string[], per-user wins, first-dir wins, backward-compat); worker.test `loadPiece('local-piece')` | FULL | Multi-dir resolution well covered

`PINFRA-002`

Pieces

validatePieceDef (Phase 6b terminal-next rejection)

Throws if any `rules[].next` ∈ {COMPLETE,ABORT,ASK}; default_next exempt

``

piece-runner.test "loadPiece terminal-rule validation (Phase 6b)": rejects COMPLETE/ABORT/ASK in rules[].next, accepts default_next: COMPLETE, accepts movement-to-movement + WAIT_SUBTASKS

FULL

Core invariant directly asserted with error-message matching

`PINFRA-003` | Pieces | default_next sentinel (engine-internal fallback) | Used for context-overflow / ASK-limit / SpawnSubTask-skip; allows COMPLETE/ABORT/ASK | `` | piece-runner.test "falls back to default_next when ASK limit is reached", "resolves a COMPLETE default_next reached via ASK-limit fallback", SpawnSubTask-skip→default_next tests | FULL | All three sentinel paths exercised
`PINFRA-004` | Pieces | lint-pieces.mjs (CI lint, terminal-next ban) | Standalone script: hard-fails any piece with COMPLETE/ABORT/ASK in rules[].next | `` | **none** (no *.test for the script; runtime equivalent in validatePieceDef IS tested) | NONE | Script itself has no test; logic duplicated/covered by PINFRA-002 but the CLI (file collection, exit codes, parse-error path) is untested
`PINFRA-005` | Pieces | CreatePiece tool validation | Rejects missing/0 max_movements, non-array shared_tools, bad rules[].next; writes to dirs[0] (per-user) | `` | tools/pieces.test "CreatePiece" block (missing max_movements, max_movements:0, valid write, shared_tools array/non-array) | FULL | Validation + write-target asserted
`PINFRA-006` | Pieces | UpdatePiece tool (built-in protection) | Refuses overwriting built-in (isBuiltinOnly); allows update when custom override exists | `` | tools/pieces.test "UpdatePiece" block (refuses built-in overwrite regression, allows custom override) | FULL | Built-in immutability regression covered
`PINFRA-007` | Pieces | rules[].next / default_next domain validation (tool path) | validRuleNexts = movements ∪ WAIT_SUBTASKS; validDefaultNexts adds COMPLETE/ABORT/ASK | `` | Covered via CreatePiece tests + pieces-api "rejects rules[].next: COMPLETE" / "accepts default_next: COMPLETE" | FULL | Both accept & reject branches asserted at API + tool level
`PINFRA-008` | Pieces | ListPieces / GetPiece (custom+builtin merge, priority) | Lists from per-user + global-custom + builtin; GetPiece prefers dirs[0] | `` | tools/pieces.test "customPiecesDir as array — P1-a regression" (ListPieces both dirs, GetPiece second-dir, GetPiece prefers per-user) | FULL | Multi-dir merge + priority covered
`PINFRA-009` | Pieces | piece classifier — buildClassificationPrompt | Builds LLM prompt from task + all piece descriptions + keyword hints; biases to chat; routes workspace-app | `` | piece-classifier.test "buildClassificationPrompt" (includes text/descriptions, file names, chat bias, workspace-app routing) | FULL | Prompt construction + routing bias asserted
`PINFRA-010` | Pieces | piece classifier — parseClassificationResponse | Extracts valid piece name from noisy LLM output; longer-match-wins; null on invalid/empty | `` | piece-classifier.test "parseClassificationResponse" (valid, noisy, longer-match data-process>general, invalid→null, empty→null) | FULL | Parse edge cases covered
`PINFRA-011` | Pieces | piece catalog (loadAllPieceTriggers / catalog merge) | Enumerates all pieces' triggers; custom overrides builtin by name | `` | piece-catalog.test (builtin+custom merge, custom override wins, triggers exposed) | FULL | Catalog merge & override asserted

`PINFRA-012`

Pieces

pieces-api CRUD + per-user/builtin authz

GET/PUT/POST/DELETE with per-user custom dirs, builtin immutability, cross-user authz, ?source=

``

pieces-api.test — extensive: no-auth legacy + auth-aware (built-in 403, own-custom 200, cross-user 403, admin edit-but-not-delete builtin, duplicate-name reject, source resolution)

FULL

One of the most thoroughly tested infra areas

`PINFRA-013` | Pieces | SSH allowlist validation (allowed_ssh_connections) | Validates UUID/wildcard/empty allowlist; rejects SshExec without allowlist & bad formats | `` | piece-runner.test (id-format reject, wildcard, multi-movement offenders) + pieces-api.test (SshExec w/o allowlist, UUID, wildcard, empty, bad format, non-array) | FULL | Both engine + API layers covered
`PINFRA-014` | Pieces | shared_tools / required_mcp normalization | Drops invalid entries, warns on SSH shared_tools w/o connections | `` | piece-runner.test imports normalizeRequiredMcp/normalizeSharedTools/mergeToolNames; tools/pieces + pieces-api shared_tools array/non-array tests | FULL | Normalization + merge covered
`PINFRA-015` | Pieces | custom/default piece namespace separation (reflection silent fork) | Reflection forks built-in into data/users/{u}/pieces with forked_from_commit instead of mutating builtin | `` | reflection/silent-fork.test (silentFork chat/data-process into per-user dir) | FULL | Silent-fork path covered (reflection subsystem)
`PINFRA-016` | Pieces | all-built-in-pieces load sweep | Loops every pieces/*.yaml asserting loadPiece().not.toThrow() | `` | piece-runner.test (~line 531-537) | FULL | This is what gives every piece PARTIAL; it validates schema but NOT each piece's movement/rule semantics

`PINFRA-017`

Pieces

per-user piece resolution in worker/scheduler/continue paths

customPiecesDir threaded through worker, continue-job, classifier, pieces-api

``

worker.test `loadPiece('local-piece', piecesDir, customPieceDirs)`; partly via pieces-api auth tests

PARTIAL

Worker path has one test; full propagation across scheduler/continue paths (per the known "no-auth local fallback" checklist) not comprehensively asserted

`REFL-001` | Reflection | Enqueue: enabled gate (default OFF) | `maybeEnqueueReflection` returns early when `cfg.enabled=false` | `src/worker.ts (363)` | worker.reflection-enqueue.test.ts ("does nothing when enabled=false") | FULL | DEFAULT_REFLECTION default value (enabled:false) itself not asserted in a config test
`REFL-002` | Reflection | Enqueue: no-recursion on reflection jobs | returns early when `job.taskKind==='reflection'` | `src/worker.ts (371)` | worker.reflection-enqueue.test.ts ("does NOT recurse") | FULL | —
`REFL-003` | Reflection | Enqueue: no-auth owner fallback to 'local' | `ownerId ?? 'local'` for namespace + budget key | `src/worker.ts (377)` | enqueue.test.ts (2 cases: fallback + budget keyed to 'local') | FULL | —
`REFL-004` | Reflection | Enqueue: worker_required gate | skip when `workerRequired` and no worker has role 'reflection' | `src/worker.ts (379-388)` | enqueue.test.ts (3 cases: true+none/true+exists/false) | FULL | —
`REFL-005` | Reflection | Enqueue: per-user daily token budget cap | skip when spent >= cap; cap=0 = unlimited; UTC-day window | `src/worker.ts (392-405)` | enqueue.test.ts (at/under cap, cap=0, UTC 25h, independent users) | FULL | —
`REFL-006` | Reflection | Enqueue: space_id propagation (Space-as-Principal) | carries job.spaceId into payload (null for personal) | `src/worker.ts (407-420)` | enqueue.test.ts (carries space_id / null personal) | FULL | —
`REFL-007` | Reflection | Dispatch: reflection job routes to runReflectionJob | worker picks task_kind='reflection', imports + runs runner, role-keyed routing | `src/worker.ts (1129-1130, 2044-2073)` | worker.reflection-dispatch.test.ts (2 cases) | PARTIAL | dispatch test is small (~2 cases); gateway routing-key + 401-without-key path not asserted end-to-end
`REFL-008` | Reflection | Runner: loadReflectionInputs (memory/activity/comments/feedback + observedRevisions) | assembles inputs, builds bodyRevision CAS map | `load-inputs.ts` | load-inputs.test.ts (~8 cases, CAS) | FULL | —
`REFL-009` | Reflection | Runner: forced submit_reflection tool_call (LLM) | callReflectionLlm forces the tool, parses result, tokens | `llm-client.ts` | llm-client.test.ts (~10 cases) | FULL | —
`REFL-010` | Reflection | Runner: prompt construction | buildSystemPrompt / buildUserPrompt | `reflection-prompt.ts` | reflection-prompt.test.ts (~18 cases) | FULL | —
`REFL-011` | Reflection | Runner: schema validation of LLM output | reflection-schema parse of memory_changes/piece_changes | `reflection-schema.ts` | reflection-schema.test.ts (~10 cases, all ops) | FULL | —
`REFL-012` | Reflection | Runner: end-to-end happy path + outcome + metric row | runReflectionJob applies, snapshots, records metric | `reflection-runner.ts` | reflection-runner.test.ts (~12 cases: applied/abstained/failed, snapshot, lock, space, budget) | FULL | —
`REFL-013` | Reflection | Runner: failure fallbacks record outcome='failed' | loadInputs/LLM/apply throw → metric 'failed', return 'failed' | `reflection-runner.ts` | reflection-runner.test.ts (failed) | PARTIAL | only some of the 3 throw-sites (loadInputs/LLM/apply+snapshot) explicitly covered; writeSnapshot-throws-but-continue branch not clearly asserted
`REFL-014` | Reflection | Runner: Space-as-Principal storage resolution | space job → data/spaces/{id}; personal → data/users/{userId}; attribution stays per-user | `reflection-runner.ts` | reflection-runner.test.ts (space-principal), revisions.test.ts | FULL | —
`REFL-015` | Reflection | Validator: rejected_unknown_type | type not in {user,feedback,project,reference} | `semantic-validator.ts` | semantic-validator.test.ts; applier.fuzz.test.ts | FULL | —
`REFL-016` | Reflection | Validator: rejected_bad_name | name fails isValidMemoryName | `semantic-validator.ts` | semantic-validator.test.ts; applier.test.ts; fuzz | FULL | —
`REFL-017` | Reflection | Validator: rejected_body_too_large | body > maxBodyBytes (UTF-8) | `semantic-validator.ts` | semantic-validator.test.ts; fuzz | FULL | —
`REFL-018` | Reflection | Validator: rejected_injected_directive | prompt-injection heuristics in body/description | `semantic-validator.ts` | semantic-validator.test.ts | FULL | only validator test hits it; not exercised through applier (low risk, static check)
`REFL-019` | Reflection | Validator: rejected_missing_target | non-add op missing merge_target OR target not in index | `semantic-validator.ts` | semantic-validator.test.ts; applier.test.ts; fuzz | FULL | —
`REFL-020` | Reflection | Validator: rejected_name_collision | add with already-existing name | `semantic-validator.ts` | semantic-validator.test.ts; fuzz | FULL | —
`REFL-021` | Reflection | Validator: rejected_target_piece_mismatch | piece_changes.target_piece ≠ input.pieceName | `semantic-validator.ts` | semantic-validator.test.ts; applier.test.ts; fuzz | FULL | —
`REFL-022` | Reflection | Validator: rejected_invalid_yaml | piece new_yaml fails YAML parse | `semantic-validator.ts` | semantic-validator.test.ts; fuzz | FULL | —
`REFL-023` | Reflection | Validator: rejected_invalid_piece | new_yaml parses but fails piece-lint | `semantic-validator.ts` | semantic-validator.test.ts; fuzz | FULL | —
`REFL-024` | Reflection | Validator: rejected_dangerous_piece | COMPLETE/ABORT/ASK in rules[].next | `semantic-validator.ts` | semantic-validator.test.ts; fuzz | FULL | —
`REFL-025` | Reflection | Apply CAS: rejected_stale_target | merge_target body revision != snapshot → reject at write time | `applier.ts` | applier.test.ts; fuzz | FULL | raised by applier (not static validator); covered
`REFL-026` | Reflection | Apply op: add | upsertMemoryEntry for new name | `applier.ts (applyOne)` | applier.test.ts; schema; runner | FULL | —
`REFL-027` | Reflection | Apply op: update (+CAS) | upsert replacing body/desc/type, requires CAS pass | `applier.ts` | applier.test.ts (ops + CAS) | FULL | —
`REFL-028` | Reflection | Apply op: merge_into (+CAS) | read existing, append timestamped section, upsert merged | `applier.ts` | applier.test.ts | FULL | —
`REFL-029` | Reflection | Apply op: remove (+CAS) | removeMemoryEntry (trash + index), requires CAS | `applier.ts` | applier.test.ts | FULL | —
`REFL-030` | Reflection | Apply: missing-target-on-disk at write time | non-add op, target gone from disk → rejected_missing_target | `applier.ts` | applier.test.ts (rejected_missing_target) | FULL | —
`REFL-031` | Reflection | Apply: 3-change hard cap | memory_changes truncated to 3 with WARN | `applier.ts` | applier.test.ts / fuzz | PARTIAL | cap value 3 truncation likely covered by fuzz inputs but a dedicated ">3 dropped with WARN" assertion not confirmed
`REFL-032` | Reflection | Apply: applyOne write error → code='failed' on decision | per-change try/catch records failed | `applier.ts` | — | PARTIAL | the per-change applyOne throw path (decision.code='failed') not obviously asserted (distinct from runner-level 'failed')
`REFL-033` | Reflection | Outcome decision: abstained | abstain_reason + no applied + no rejected | `applier.ts (decideOutcome)` | applier.test.ts (abstained); runner | FULL | —
`REFL-034` | Reflection | Outcome decision: partial | any applied AND any rejected/dropped | `applier.ts` | applier.test.ts (partial) | FULL | —
`REFL-035` | Reflection | Outcome decision: applied | any applied, none rejected | `applier.ts` | applier.test.ts (applied); runner | FULL | —
`REFL-036` | Reflection | Outcome decision: rejected | any rejected/dropped, none applied | `applier.ts` | applier.test.ts (rejected); fuzz | FULL | —
`REFL-037` | Reflection | Outcome decision: fallback abstained | empty input (0 changes, no piece) | `applier.ts` | applier.test.ts | PARTIAL | the empty-input fallback branch (vs explicit abstain_reason) not clearly distinguished in tests
`REFL-038` | Reflection | Piece write: cooldown gate (2 edits/24h) | >=2 edits in window → {written:false, reason:'cooldown'} → pieceCooldownDropped | `piece-writer.ts; applier.ts` | piece-writer.test.ts (cooldown); applier.test.ts (cooldown) | FULL | —
`REFL-039` | Reflection | Piece write: cooldown drop vs semantic reject distinction | pieceCooldownDropped separate from pieceRejectCode in outcome | `applier.ts` | applier.test.ts; piece-writer.test.ts | FULL | —
`REFL-040` | Reflection | Piece write: atomic temp+rename + catalog invalidate + recordPieceEdit | writePiece tmp.{pid} → rename, repo.recordPieceEdit, catalog.invalidate | `piece-writer.ts` | piece-writer.test.ts (~8 cases) | FULL | atomic-rename crash-safety (partial-write) not stress-tested
`REFL-041` | Reflection | Silent fork of built-in piece | builtin source → copy to data/users/{userId}/pieces with forked_from_commit/forked_at; idempotent; throws if source missing | `silent-fork.ts` | silent-fork.test.ts (~9 cases) | FULL | —
`REFL-042` | Reflection | Drift detection (forked_from_commit vs latest builtin commit) | detectDrift returns drifted flag; graceful no-op without git | `drift-detect.ts` | drift-detect.test.ts (~7 cases) | FULL | —
`REFL-043` | Reflection | Snapshot write (.reflection-history/{ts}-{jobId} + index.jsonl append) | writeSnapshot persists before/after files, appends index.jsonl atomically under lock | `snapshot.ts; reflection-runner.ts` | snapshot.test.ts (~34 cases) | FULL | —
`REFL-044` | Reflection | Snapshot: before/after memory file capture | runner builds beforeFiles/afterFiles from accepted decisions (remove uses merge_target) | `reflection-runner.ts` | reflection-runner.test.ts; snapshot.test.ts | PARTIAL | the remove-op before-file naming branch (merge_target vs name) in runner not specifically asserted
`REFL-045` | Reflection | Snapshot: piece before/after YAML capture | reads custom piece path pre/post for snapshot | `reflection-runner.ts` | reflection-runner.test.ts | PARTIAL | piece YAML before/after capture lightly covered; existsSync(customPath) edge not asserted
`REFL-046` | Reflection | bodyRevision (SHA-1 of parsed body) consistency | same helper for loader observedRevisions and applier CAS | `revisions.ts` | revisions.test.ts (~8 cases) | FULL | —
`REFL-047` | Reflection | Per-user file lock (withUserLock) | serializes apply+snapshot critical section (lockfile) | `user-lock.ts; applier.ts; reflection-runner.ts` | user-lock.test.ts; applier.test.ts (lock); runner | FULL | concurrent contention/torn-write scenario only indirectly assured
`REFL-048` | Reflection | Activity log summarizer | summarizes activity.log to bounded size for prompt | `activity-summarizer.ts` | activity-summarizer.test.ts (~5 cases) | FULL | —
`REFL-049` | Reflection | DB: reflection_metrics row (outcome applied/partial/abstained/rejected/failed) | recordReflectionMetric inserts one row per reflection job | `src/db/repository.ts (~4093-4127)` | reflection-runner.test.ts; migrate.reflection-columns.test.ts | FULL | —
`REFL-050` | Reflection | DB: reflection_piece_edits cooldown tracking | recordPieceEdit insert + count-in-window query | `src/db/repository.ts (~4068-4090)` | piece-writer.test.ts; migrate test | FULL | —
`REFL-051` | Reflection | DB: aggregateReflectionMetrics (budget) | sums tokens since UTC-day start for budget gate | `src/db/repository.ts` | enqueue.test.ts (budget) | FULL | —
`REFL-052` | Reflection | DB: task_kind / payload columns + reflection job creation | migrate adds task_kind, payload; reflection job created with taskKind='reflection' | `src/db/repository.ts; migrate.ts` | migrate.reflection-columns.test.ts (~23 cases) | FULL | —
`REFL-053` | Reflection | API: GET /history (paged, owner-scoped, nextCursor) | lists snapshot index entries, most-recent-first, cursor pagination | `bridge/reflection-api.ts` | reflection-api.test.ts (~29 cases) | FULL | —
`REFL-054` | Reflection | API: GET /history/:snapshotId (404 no-leak for non-owner/missing) | detail or 404 (no existence leak) | `bridge/reflection-api.ts` | reflection-api.test.ts | FULL | —
`REFL-055` | Reflection | API: POST /history/:snapshotId/revert (idempotent) | revertSnapshotForUser; {reverted:true} first, {reverted:false} after | `bridge/reflection-api.ts; snapshot.ts` | reflection-api.test.ts (revert); snapshot.test.ts (revert) | FULL | —
`REFL-056` | Reflection | API: space-aware store resolution + canEditInSpace authz | resolves per-user vs per-space store; space history needs edit rights; 404 on mismatch | `bridge/reflection-api.ts` | reflection-api.test.ts (space-principal) | PARTIAL | canEditInSpace negative path (member-without-edit-rights → 404) not clearly asserted as a distinct multiuser case
`REFL-057` | Reflection | API: GET /metrics | aggregated reflection metrics endpoint | `bridge/reflection-api.ts` | reflection-api.test.ts | PARTIAL | endpoint exists; depth of assertion (per-outcome breakdown) unverified
`REFL-058` | Reflection | API: GET /latest-for-task/:taskId (admin) | latest snapshot for a task; null (not 404) when none; admin-gated | `bridge/reflection-api.ts` | reflection-api.test.ts | PARTIAL | admin-gating + null-vs-404 distinction not confirmed asserted
`REFL-059` | Reflection | Retention: pruneOldSnapshots (age) | delete snapshot dirs older than retentionDays; index.jsonl untouched | `retention.ts` | retention.test.ts (~17 cases) | FULL | —
`REFL-060` | Reflection | Retention: enforceDiskCap (oldest-first) | prune oldest until under capBytes | `retention.ts` | retention.test.ts | FULL | —
`REFL-061` | Reflection | Retention: runReflectionRetentionSweep (per-user + per-space) | sweeps both data/users and data/spaces trees | `retention.ts` | retention.test.ts (space-principal) | FULL | —
`APIC-001` | Bridge core | GET /api/local/tasks | List tasks filtered by `buildVisibilityWhere` (owner/admin/org/public/space members) | `local-tasks-api.ts:123` | local-tasks-api.test.ts (visibility list: owner/admin/same-org/diff-org bystander) | FULL | —
`APIC-002` | Bridge core | POST /api/local/tasks | Create task+job; visibility enum validation, org-scope check, spaceId forces private, owner from req.user (or `local` no-auth) | `local-tasks-api.ts:134` | local-tasks-api.test.ts (visibility, no-auth owner, runtime_dir, attachments, spaceId), spaces.test.ts | FULL | —
`APIC-003` | Bridge core | GET /api/local/tasks/:taskId | Get one task; `canViewTask` visibility gate | `local-tasks-api.ts:342` | space-view.test.ts (membership read gate), spaces.test.ts | FULL | —
`APIC-004` | Bridge core | GET /api/local/tasks/:taskId/tool-requests | List pending tool requests; canViewTask gate | `local-tasks-api.ts:361` | local-tasks-api.test.ts:175 (lists pending) | PARTIAL | view-gate denial path not directly asserted on this route
`APIC-005` | Bridge core | POST /api/local/tasks/:taskId/tool-requests/:reqId/decide | Approve/deny a parked tool request; write-gate (403 view-only / 404 non-member), re-queue parked job, 409 already-decided | `local-tasks-api.ts:380` | local-tasks-api.test.ts:143 (approve/deny/409/invalid value/park-contract guard) | FULL | —
`APIC-006` | Bridge core | PUT /api/local/tasks/:taskId/feedback | Submit rating/feedback; owner-or-admin (404 to others even if public) | `local-tasks-api.ts:418` | local-tasks-api.test.ts:740 (owner/admin/non-owner 404), spaces.test.ts:268 | FULL | —
`APIC-007` | Bridge core | PUT /api/local/tasks/:taskId/mission | Set/update task mission text | `local-tasks-api.ts:442` | none found (only auth.test.ts matches the word "mission") | NONE | No test exercises this route — authz + validation unverified
`APIC-008` | Bridge core | GET /api/local/tasks/:taskId/comments | List comments; canViewTask gate | `local-tasks-api.ts:474` | covered indirectly via comment-write-gate.test.ts setup; no dedicated read-gate assertion | PARTIAL | GET read-gate not directly asserted

`APIC-009`

Bridge core

POST /api/local/tasks/:taskId/comments

Post comment (user message → spawns/reuses job); **comment write gate** (owner/admin/editor 201, viewer 403, non-member 404), attachment save, tool_request gate atomicity, idle-job reuse

`local-tasks-api.ts:492`

comment-write-gate.test.ts (owner/admin/editor/viewer-403/non-member-404/personal-404), local-tasks-api.test.ts:816 (ownership, attachments, job reuse, terminal→fresh job)

FULL

—

`APIC-010` | Bridge core | PATCH /api/local/tasks/:taskId | Update task (visibility change, etc.); owner/admin, org-scope validation | `local-tasks-api.ts:623` | local-tasks-api.test.ts:451 (visibility change, invalid enum, org scope), spaces.test.ts:255 (non-member 404) | FULL | —
`APIC-011` | Bridge core | POST /api/local/tasks/:taskId/regenerate-title | Regenerate AI title; owner/admin only | `local-tasks-api.ts:681` | spaces.test.ts:296 (owner regenerate, +line 318 case) | PARTIAL | Only exercised in space context; non-owner-denied / no-generator paths not asserted
`APIC-012` | Bridge core | POST /api/local/tasks/evaluate-prompt | Prompt-coach: evaluate an instruction via LLM | `local-tasks-api.ts:717` | local-tasks-api.test.ts:1370 (no-generator, blank, happy paths) | FULL | —
`APIC-013` | Bridge core | DELETE /api/local/tasks/:taskId | Delete task; owner-or-admin (404 to others even public) | `local-tasks-api.ts:756` | local-tasks-api.test.ts:336 (owner/admin/non-owner-404), spaces.test.ts:69 | FULL | —
`APIC-014` | Bridge core | POST /api/local/tasks/:taskId/cancel | Cancel running job; write-gate (editor 200, viewer 403, non-member 404) | `local-tasks-api.ts:778` | comment-write-gate.test.ts:117 (editor/viewer-403/non-member-404), spaces.test.ts:98 | FULL | —
`APIC-015` | Bridge core | POST /api/local/tasks/:taskId/continue | Spawn child job with new piece; 409 job_in_progress / no_previous_job, handoff comment, instruction persist | `local-tasks-api.ts:818` | local-tasks-api.test.ts:1072 (happy/handoff/persist/409 in-progress/409 no-prev), spaces.test.ts:122 (space_id carry, non-member 404) | FULL | —
`APIC-016` | Bridge core | GET /api/local/tasks/:taskId/stream | SSE stream of job events (tool calls, deltas) | `local-tasks-api.ts:914` | none (`/stream` matches only in app-share/share/space-api tests) | NONE | SSE event mapping, visibility gate, reconnection all untested
`APIC-017` | Bridge core | GET /api/local/tasks/:taskId/files | List files in a section (input/output/logs/workspace); canViewTask gate; traversal 400 | `local-files-api.ts:41` | local-files-api.test.ts:74 (default section, subdir, unknown section 400, private-hidden, traversal 400, logs from runtime_dir + workspace_path fallback) | FULL | —
`APIC-018` | Bridge core | GET /api/local/tasks/:taskId/files/content | Serve file as text/plain; traversal 400, dir 400, missing 404, leading-slash strip | `local-files-api.ts:86` | local-files-api.test.ts:152 (text, requires-path, 404 not 500, dir 400, traversal 400, abs-path strip) | FULL | —

`APIC-019`

Bridge core

GET /api/local/tasks/:taskId/files/raw

Serve raw bytes by extension; `trusted=1` drops CSP sandbox (owner-trust model), `setUntrustedFileResponseHeaders`, `ensurePathWithin` traversal guard (400)

`local-files-api.ts:136`

local-files-api.test.ts:197 (type-by-ext, sandbox default, trusted for owner/admin/non-owner-shared-viewer/ownerless, no-user-still-sandboxed, no-auth, non-HTML stays sandboxed, traversal 400)

FULL

Strong coverage incl. CSP trust matrix

`APIC-020`

Bridge core

GET /api/local/tasks/:taskId/files/office-preview

Excel→cells JSON / PPTX→slide PNGs; section/path validation 400, soffice missing → 503

`local-files-api.ts:203`

office-preview.test.ts (file-type detect, spreadsheet preview incl. formula, soffice-missing throws) tests the *module*; the HTTP route itself not directly hit

PARTIAL

Module logic FULL; HTTP route (section enum 400, canViewTask gate, 503 mapping) not exercised end-to-end

`APIC-021` | Bridge core | PUT /api/local/tasks/:taskId/files/content | Write/overwrite output file; owner/admin only, output-section-only, refuse while running, traversal 400, string-content validation | `local-files-api.ts:240` | local-files-api.test.ts:290 (owner write, non-owner 404, admin, refuse-running, output-only, requires-string, traversal 400) | FULL | —

`APIC-022`

Bridge core

POST /api/local/tasks/:taskId/files/upload

Upload files to input/ (≤60mb); owner/admin, O_EXCL rename-on-collision, name sanitize, refuse-running, section/empty/traversal 400

`local-files-api.ts:324`

local-files-api.test.ts:366 (input upload, subdir, collision rename, name sanitize, non-owner 404, admin, refuse-running, logs/workspace 400, empty 400, traversal 400)

FULL

—

`APIC-023` | Bridge core | POST /api/local/tasks/:taskId/files/delete | Delete files; owner/admin, idempotent skip-missing, skip-dirs, refuse-running, validate-all-first (no partial), traversal 400 | `local-files-api.ts:392` | local-files-api.test.ts:470 (delete, single path, skip-missing/dirs, non-owner 404, refuse-running, logs 400, empty 400, traversal 400, validate-all-first) | FULL | —

`APIC-024`

Bridge core

POST /api/local/tasks/:taskId/files/download-zip

Zip selected files; read-gated (non-owner of public OK), all sections, `safeZipEntryName` zip-slip protection, traversal 400, empty 400

`local-files-api.ts:447`

local-files-api.test.ts:547 (owner zip, logs section, non-owner-public read, private 404, 404-nothing-matches, traversal 400, empty 400, zip-slip backslash, ..-strip)

FULL

—

`APIC-025` | Bridge core | GET /api/pieces | List pieces (builtin + global-custom + caller's user-custom, priority-merged, no hiding) | `pieces-api.ts:251` | pieces-api.test.ts (no-auth list, auth-aware merge, same-name no-hide, source tagging) | FULL | —
`APIC-026` | Bridge core | GET /api/pieces/:name | Get one piece; `?source=` selector, invalid source → 400, priority resolution | `pieces-api.ts:325` | pieces-api.test.ts (404 unknown, ?source=builtin, priority resolution, invalid-source typo 400) | FULL | —
`APIC-027` | Bridge core | PUT /api/pieces/:name | Update; builtin by non-admin 403, admin can edit builtin, own user-custom 200, others' 403, lint (rules[].next COMPLETE reject, SshExec allowlist, shared_tools) | `pieces-api.ts:370` | pieces-api.test.ts (builtin-non-admin 403, own 200, other-user 403, admin builtin, ?source typo 400, lint rules) | FULL | —
`APIC-028` | Bridge core | POST /api/pieces | Create; dup-name reject, builtin-name reject, no-auth→piecesDir vs user-custom, 503 when userPiecesRootDir unset for authed non-admin/admin, returns actual source | `pieces-api.ts:430` | pieces-api.test.ts (create, dup reject, builtin-name reject, 503 fallback hole, source return, no-auth local user-custom) | FULL | —
`APIC-029` | Bridge core | DELETE /api/pieces/:name | Delete; builtin non-deletable 403 (even admin), own user-custom 200, ?source typo 400 no-destructive-fallback | `pieces-api.ts:490` | pieces-api.test.ts (builtin 403, own 200, admin-cannot-delete-builtin, ?source=user-custom target, typo 400) | FULL | —
`APIC-030` | Bridge core | GET /api/config | Return v2 config + ETag; omits legacy provider/flat-storage keys; admin-only mount | `config-api.ts:87` | config-api.test.ts (v2 shape+etag, omits legacy, exposes storage.*) | FULL | Note: sensitive-masking observed in tests as worker api_key exclusion (APIC-033), not on this raw GET
`APIC-031` | Bridge core | PUT /api/config | Update with If-Match ETag optimistic lock → **409 on stale etag**; legacy-key reject 400; force config_version=2; YAML round-trip | `config-api.ts:93` | config-api.test.ts (round-trip, force-version, legacy-key 400s ×6, storage round-trip, **409 stale etag**) | FULL | —
`APIC-032` | Bridge core | POST /api/config/reload | Reload config from file (500 on parse error) | `config-api.ts:117` | config-api.test.ts:208 (reload from file) | PARTIAL | 500 error path not asserted
`APIC-033` | Bridge core | GET /api/workers | List workers, allowlisted fields only (**no api_key leak**), synthesized default, proxy/proxyType exposed; 401 unauth | `config-api.ts:126` | config-api.test.ts (default synth, allowlist fields, proxy no-key-leak, 401 unauth, 200 authed) | FULL | This is the de-facto sensitive-masking test
`APIC-034` | Bridge core | GET /api/workers/:workerId/backends | Fetch /v1/models from proxy worker; 404 unknown, direct→empty, 60s cache, scheme/URL validation (no apiKey leak to fetch), 502 upstream fail, 401 unauth | `config-api.ts:149` | config-api.test.ts (404, direct empty, fetch list, cache, file://+data: reject, malformed reject, 502, 401) | FULL | —
`APIC-035` | Bridge core | GET /api/tools | Runtime tool catalog (builtin+meta+MCP+ssh w/ availability flags); `?legacy=1` flat array; 401 unauth | `tools-api.ts:242` | tools-api.test.ts (catalog, source/category tags, meta scope, ssh avail/unavail, MCP authed/offline/no-user, 401, legacy flat, placeholder, TestWorkspaceApp) | FULL | —
`APIC-036` | Bridge core | GET /api/scheduled-tasks | List; visibility filter, `?spaceId=` scope | `scheduled-tasks-api.ts:105` | scheduled-tasks-api.test.ts (list, visibility filter owner/non-owner/admin, ?spaceId scope) | FULL | —
`APIC-037` | Bridge core | GET /api/scheduled-tasks/:id | Get one scheduled task | `scheduled-tasks-api.ts:120` | scheduled-tasks-api.test.ts (covered via list/manage setup) | PARTIAL | No dedicated GET-by-id view-gate assertion

`APIC-038`

Bridge core

POST /api/scheduled-tasks

Create; visibility enum+org-scope validation, spaceId forces private + edit-rights fallback to NULL, no-auth synthetic `local` owner, requires body

`scheduled-tasks-api.ts:133`

scheduled-tasks-api.test.ts (visibility, org reject, spaceId force-private/edit-fallback/see-not-edit-fallback, no-auth local, browserSessionProfile owner check, daily, require-body)

FULL

—

`APIC-039`

Bridge core

PATCH /api/scheduled-tasks/:id

Update; `canManageSchedule` (owner/admin/space-editor); **non-owner may manage LIFECYCLE only (pause/resume/reschedule/visibility) — content edits owner-only (P3 confused-deputy guard)**; keeps space schedule private

`scheduled-tasks-api.ts:267`

scheduled-tasks-api.test.ts (pause/resume, owner-or-admin 404 paths, admin any, space-editor manage, lost-edit-rights, keep-private)

FULL

Confused-deputy split (lifecycle vs content) is the known P3 design caveat

`APIC-040` | Bridge core | DELETE /api/scheduled-tasks/:id | Delete; owner/admin/space-editor (non-owner 404) | `scheduled-tasks-api.ts:461` | scheduled-tasks-api.test.ts (owner, admin any, non-owner 404, space-editor survives-creator-leaving) | FULL | —
`APIC-041` | Bridge core | POST /api/scheduled-tasks/:id/trigger | Manually trigger a run; canManageSchedule gate | `scheduled-tasks-api.ts:480` | scheduled-tasks-api.test.ts:203 (creator who lost edit rights can no longer trigger) | PARTIAL | Happy-path trigger spawning a run not directly asserted; only the denial path
`APIC-042` | Bridge core | GET /api/local/tasks/:id/subtasks/activities | Aggregate subtask activity tree; canViewTask gate (404 hidden, public allowed) | `subtask-activity-api.ts:28` | subtask-activity-api.test.ts (nested when waiting_subtasks, 404 not-found, 404 cannot-see, public allowed) | FULL | —
`APIC-043` | Bridge core | GET /api/local/tasks/:id/subtasks/:jobId/activity | Single subtask activity; 404 subtask-not-found | `subtask-activity-api.ts:68` | subtask-activity-api.test.ts:211 (404 subtask not found) | PARTIAL | view-gate denial on this specific route not separately asserted
`APIC-044` | Bridge core | GET /api/local/tasks/:id/subtasks/:jobId/files | List subtask worktree files; canViewTask, `isJobWithinWorkspace` containment 404, 404 no-worktree | `subtask-files-api.ts:12` | subtask-files-api.test.ts:67 (grouped listing, 400 bad id, 404 missing/private/no-worktree/outside-workspace) | FULL | —

`APIC-045`

Bridge core

GET /api/local/tasks/:id/subtasks/:jobId/files/*

Serve subtask file; `resolve`+`startsWith(base+sep)` traversal → 403, 404 missing, canViewTask, org-member allowed, sendFile

`subtask-files-api.ts:55`

subtask-files-api.test.ts:121 (serve inside worktree, 404 missing, 404 cannot-see, org-member allowed)

PARTIAL

The 403 traversal branch (resolved≠base path-escape) is not directly asserted on this route

`APIC-046` | Bridge core | `isJobWithinWorkspace` | Separator-bounded containment (rejects /12 vs /123 prefix sibling) | `local-api-helpers.ts` | local-api-helpers.test.ts:8 (descendants, sibling-prefix reject, null/empty) | FULL | —
`APIC-047` | Bridge core | `ensurePathWithin` / `isPathEscapeError` | Path-traversal + **symlink-escape** hardening, sibling-prefix bypass guard | `local-api-helpers.ts` | local-api-helpers.test.ts:92,106 (inside ok, traversal reject, sibling-prefix, symlink-escape, in-root resolve) | FULL | —
`APIC-048` | Bridge core | `safeZipEntryName` | Neutralizes .., backslash, Windows drive-letter in zip entry names (zip-slip) | `local-api-helpers.ts` | local-api-helpers.test.ts:25 (plain path, never-absolute, drive-letter→C_) | FULL | —
`APIC-049` | Bridge core | `setUntrustedFileResponseHeaders` | nosniff + CSP sandbox on untrusted file responses | `local-api-helpers.ts` | local-api-helpers.test.ts:78 | FULL | —
`APIC-050` | Bridge core | office-preview module (`getSpreadsheetPreview`/`getPresentationPreview`/`SofficeUnavailableError`) | Excel cells incl. formula results, PPTX via soffice, throws when soffice absent (→503) | `office-preview.ts` | office-preview.test.ts (file detect, spreadsheet+formula, soffice-missing throws) | FULL | Module-level only; see APIC-020 for the HTTP wiring gap
`APIS-001` | Bridge auth & spaces | OAuth2 Google login (`GET /auth/google`, callback) | Passport Google strategy, status gate (active→app, pending→/auth/pending) | `auth.ts` | auth.test.ts | FULL |
`APIS-002` | Bridge auth & spaces | OAuth2 Gitea login (`GET /auth/gitea`, callback) | Passport OAuth2 Gitea; pending→admin auto-promote if adminEmails | `auth.ts` | auth.test.ts | PARTIAL | callback strategy logic tested indirectly; live OAuth handshake not E2E
`APIS-003` | Bridge auth & spaces | Local auth login (`POST /auth/local`) | username/password login w/ rate limiter | `auth.ts, login-rate-limit.ts` | auth.local.test.ts, login-rate-limit.test.ts | FULL |
`APIS-004` | Bridge auth & spaces | Local signup (`POST /auth/local/signup`) | self-register → pending status awaiting approval | `auth.ts` | auth.local.test.ts | FULL |
`APIS-005` | Bridge auth & spaces | Login rate limiting | `LoginRateLimiter` + per-IP throttle scope | `login-rate-limit.ts, auth.ts` | login-rate-limit.test.ts | FULL |
`APIS-006` | Bridge auth & spaces | `requireAuth` middleware | authenticated AND status==='active' else 401/redirect | `auth.ts` | auth.test.ts | FULL |
`APIS-007` | Bridge auth & spaces | `requireAdmin` middleware | role==='admin' else 401/403 | `auth.ts` | auth.test.ts | PARTIAL | exercised via admin routers; few direct unit cases
`APIS-008` | Bridge auth & spaces | `GET /auth/status` / `/login` / `/pending` / `/logout` | session state pages + logout | `auth.ts` | auth.test.ts | PARTIAL | logout/pending HTML paths lightly covered
`APIS-009` | Bridge auth & spaces | Session store (persistent secret, survive restart) | auto-gen session secret, short-secret warning | `auth.ts` | auth.session-store.test.ts | FULL |
`APIS-010` | Bridge auth & spaces | Gitea orgs fetch (`/api/v1/user/orgs`) | populates session.orgIds for 'org' visibility | `auth.ts (`resolveOrgIds`)` | auth.test.ts, local-orgs.integration.test.ts | PARTIAL | fetch failure/network-error path not asserted
`APIS-011` | Bridge auth & spaces | Change password (`requireAuth` + current-password) | requires current pw, 204 on success | `auth.ts` | auth.test.ts | PARTIAL | wrong-current-pw rejection coverage thin
`APIS-012` | Bridge auth & spaces | Admin users CRUD (`GET/POST/PATCH/DELETE /api/admin/users`) | **admin-only** (guard); create/approve/disable/set-password | `admin-api.ts` | admin-api.test.ts, admin-api.local.test.ts | FULL |
`APIS-013` | Bridge auth & spaces | Admin orgs CRUD + members (`/api/admin/orgs*`) | **admin-only**; org create/edit/delete, add/remove member | `admin-api.ts` | admin-api.test.ts, local-orgs.integration.test.ts | FULL |
`APIS-014` | Bridge auth & spaces | **No-auth admin passthrough** | when authActive=false, admin guard → passthrough | `admin-api.ts, branding-api.ts` | admin-api.local.test.ts | PARTIAL | branding adminGuard no-auth path not asserted
`APIS-015` | Bridge auth & spaces | Task share create (`POST /api/local/tasks/:id/share`) | **owner-or-admin only** (`checkTaskOwnership`) → token | `share-api.ts` | share-api.test.ts | FULL |
`APIS-016` | Bridge auth & spaces | Task unshare (`DELETE .../share`) | **owner-or-admin only** | `share-api.ts` | share-api.test.ts | FULL |
`APIS-017` | Bridge auth & spaces | Public shared task read (`GET /api/shared/:token`) | token-only (no auth); invalid/revoked token → 404 | `share-api.ts` | share-api.test.ts | FULL |
`APIS-018` | Bridge auth & spaces | Shared task comments/files/raw (`/api/shared/:token/...`) | token-scoped read of files/comments/raw content | `share-api.ts` | share-api.test.ts | PARTIAL | path-escape on `/files/content` lightly tested
`APIS-019` | Bridge auth & spaces | Shared subtask activities/files (`/api/shared/:token/subtasks/*`) | token-scoped subtask containment (`isJobWithinWorkspace` prefix guard) | `share-api.ts` | share-api.test.ts | PARTIAL | wildcard `/subtasks/:jobId/files/*` traversal not directly asserted
`APIS-020` | Bridge auth & spaces | Public app-share resolve (`GET /api/app-share/:token`) | token→(spaceId,appName); revoked/invalid→404 | `app-share-api.ts` | app-share-api.test.ts | FULL |
`APIS-021` | Bridge auth & spaces | App-share file read (content/raw/list) | **server-side path containment**: outside allowed route→403; escape→403 | `app-share-api.ts` | app-share-api.test.ts | FULL | strong path-escape/forbidden coverage
`APIS-022` | Bridge auth & spaces | App-share is GET-only | no write/delete endpoints (read-only public share) | `app-share-api.ts` | app-share-api.test.ts | FULL |
`APIS-023` | Bridge auth & spaces | Space list (`GET /spaces/`) | visibility-scoped list via repo viewer gate | `space-api.ts` | space-api.test.ts | FULL |
`APIS-024` | Bridge auth & spaces | Space get (`GET /spaces/:id`) | `getSpace({viewer})` null→404 (private/org/public gate) | `space-api.ts` | space-api.test.ts | FULL |
`APIS-025` | Bridge auth & spaces | Space create (`POST /spaces/`) | authenticated user creates space (owner) | `space-api.ts` | space-api.test.ts | FULL |
`APIS-026` | Bridge auth & spaces | Space update/archive (`PATCH /:id`, `POST /:id/archive`) | **canManageSpace** (owner/admin/member-owner) else 403 | `space-api.ts` | space-api.test.ts, space-api.members.test.ts | FULL |
`APIS-027` | Bridge auth & spaces | Space file list/content/raw/office-preview (`GET /:id/files*`) | read gated by `getSpace({viewer})` visibility; not-found→404 | `space-api.ts, office-preview.ts` | space-api.test.ts, office-preview.test.ts, local-files-api.test.ts | PARTIAL | non-member-on-public read path not asserted per-endpoint
`APIS-028` | Bridge auth & spaces | Space file upload/delete (`POST /:id/files/upload`,`/delete`) | **canEditInSpace** (owner/admin/editor); viewer→403 | `space-api.ts` | space-api.write-gates.test.ts | FULL | strong write-gate authz coverage (20 authz assertions)
`APIS-029` | Bridge auth & spaces | Space file mkdir/move/download-zip (`POST /:id/files/...`) | **canEditInSpace**; protected top-dirs blocked | `space-api.ts` | space-api.write-gates.test.ts, space-api.write-endpoint.test.ts | FULL |
`APIS-030` | Bridge auth & spaces | Space file write (`POST /:id/files/write`) | **canEditInSpace** write gate | `space-api.ts` | space-api.write-endpoint.test.ts, space-api.write-gates.test.ts | FULL |
`APIS-031` | Bridge auth & spaces | Space calendar read (`GET /:id/calendar`, `/calendar/day`) | visibility-gated read | `space-api.ts, cross-calendar-api.ts` | space-api.calendar.test.ts, cross-calendar-api.test.ts | FULL |
`APIS-032` | Bridge auth & spaces | Space calendar event CRUD (`POST/PATCH/DELETE /:id/calendar/events*`) | edit gate; multi-day mirror | `space-api.ts` | space-api.calendar.test.ts | PARTIAL | non-editor write rejection coverage lighter than file gates
`APIS-033` | Bridge auth & spaces | Space members list (`GET /:id/members`) | members visible; **email masked** unless admin/owner/self-member | `space-api.ts` | space-api.members.test.ts | FULL | PII masking explicitly tested
`APIS-034` | Bridge auth & spaces | Space member add/role/remove (`POST/PATCH/DELETE /:id/members*`) | **canManageSpace**; owner-add blocked(400); self-removal allowed; non-member→404 | `space-api.ts` | space-api.members.test.ts | FULL | 27 authz assertions
`APIS-035` | Bridge auth & spaces | Space invite issue/get/revoke (`GET/POST/DELETE /:id/invite`) | **canManageSpace**; no-auth→404 | `space-api.ts` | space-api.invite.test.ts | FULL |
`APIS-036` | Bridge auth & spaces | Invite preview + accept (`GET /invite/:token`, `POST /invite/:token/accept`) | authed user joins via valid token; invalid/expired→404; no-auth→404 | `space-api.ts` | space-api.invite.test.ts | FULL | maxUses/expiry validity via `isSpaceInviteValid`
`APIS-037` | Bridge auth & spaces | App share per-space (`GET/POST/DELETE /:id/apps/:app/share`) | manage-gated issue/revoke of public app link | `space-api.ts` | space-api.app-share.test.ts | PARTIAL | 6 authz assertions; non-manager revoke path thin
`APIS-038` | Bridge auth & spaces | Space tool-policy (`GET /:id/tool-policy`, `PUT`) | GET visibility-gated; **PUT canManageSpace**; sensitive (Bash/SSH/browser/MCP) opt-in | `space-api.ts` | space-api.tool-policy.test.ts | PARTIAL | only 9 authz assertions; Bash/sensitive write-path round-trip (PR #653 regression area) under-covered
`APIS-039` | Bridge auth & spaces | Browser captcha-pool (`GET/DELETE /captcha-pool`) | captcha session pool read/clear | `browser-api.ts` | browser-api.test.ts | FULL |
`APIS-040` | Bridge auth & spaces | Browser task-session (`GET/POST /task-session/:taskId*`) | per-task browser session acquire/release | `browser-api.ts` | browser-api.test.ts | FULL |
`APIS-041` | Bridge auth & spaces | Browser profile list (`GET /` ) | space-aware list; **decryptableByViewer** flag (DEK owner-bound) | `browser-session-api.ts` | browser-session-api.test.ts | FULL | non-owner sees but cannot decrypt — asserted
`APIS-042` | Bridge auth & spaces | Browser profile create (`POST`) | space ctx → **canEditInSpace**; personal → owner-scoped; spaceId ignored when no spaceAccess | `browser-session-api.ts` | browser-session-api.test.ts | FULL |
`APIS-043` | Bridge auth & spaces | Browser profile delete (`DELETE /:id`) | space editor/owner OR personal owner-only | `browser-session-api.ts` | browser-session-api.test.ts | FULL |
`APIS-044` | Bridge auth & spaces | Browser interactive login session (`POST /profiles/:id/login`) | spawn noVNC session for owner; DEK owner-bound | `browser-session-api.ts, novnc-proxy.ts` | browser-session-api.test.ts, novnc-proxy.test.ts | PARTIAL | noVNC proxy authz/upgrade path lightly covered
`APIS-045` | Bridge auth & spaces | Console WS attach (`attachConsoleWs` upgrade) | `decideAccess`: visibility gate + canWrite (owner OR admin) | `console-ws-api.ts` | console-ws-api.test.ts | FULL | decideAccess unit-tested
`APIS-046` | Bridge auth & spaces | Console status router (`createConsoleStatusRouter`) | requireAuth; status poll for owner/admin | `console-ws-api.ts` | console-ws-api.test.ts, console-session-api.test.ts | FULL |
`APIS-047` | Bridge auth & spaces | Console session router (`createConsoleSessionRouter`) | requireAuth task-scoped session resolution | `console-ws-api.ts` | console-session-api.test.ts | FULL |
`APIS-048` | Bridge auth & spaces | Console admin (`GET /ssh/console-sessions`, `POST .../:taskId/kill`) | **requireAdmin** list/kill console sessions | `console-admin-api.ts` | console-admin-api.test.ts | FULL |
`APIS-049` | Bridge auth & spaces | Gateway admin CRUD (`POST/PATCH/DELETE /:id`, rotate/revoke keys) | **requireAdmin** backend + API-key mgmt | `admin-gateway-api.ts` | admin-gateway-api.test.ts, .budget-rate.test.ts, .metric-labels.test.ts | FULL | 18 authz assertions; budget/rate/labels covered
`APIS-050` | Bridge auth & spaces | Gateway usage (`GET /:id/usage`) | **requireAdmin** per-backend usage | `admin-gateway-api.ts` | admin-gateway-api.test.ts | FULL |
`APIS-051` | Bridge auth & spaces | Gateway status (`GET /` status) | non-admin status snapshot | `admin-gateway-status-api.ts` | admin-gateway-status-api.test.ts | FULL |
`APIS-052` | Bridge auth & spaces | Gateway mount/proxy (`mountGateway`, classifyGatewayPath) | LLM gateway request routing/classification middleware | `gateway-mount.ts` | gateway-mount.test.ts | PARTIAL | path classification + config-equiv heavily tested; auth on proxied LLM calls (Bearer/key) not asserted in this file
`APIS-053` | Bridge auth & spaces | Notifications VAPID key (`GET /api/notifications/vapid-public-key`) | requireAuth | `notifications-api.ts` | notifications-api.test.ts | FULL |
`APIS-054` | Bridge auth & spaces | Notifications subscriptions (`GET/POST/DELETE /subscriptions`) | requireAuth; web-push subscribe/unsubscribe | `notifications-api.ts` | notifications-api.test.ts | FULL |
`APIS-055` | Bridge auth & spaces | Notifications preferences (`GET/PUT /preferences`) | requireAuth pref read/write | `notifications-api.ts` | notifications-api.test.ts | FULL |
`APIS-056` | Bridge auth & spaces | Notifications test/send (`POST` send endpoints) | requireAuth push trigger | `notifications-api.ts` | notifications-api.test.ts | PARTIAL | send/test push delivery path lightly asserted
`APIS-057` | Bridge auth & spaces | Branding read (`GET /api/branding`) | public branding config read | `branding-api.ts` | branding-api.test.ts | FULL |
`APIS-058` | Bridge auth & spaces | Branding static serve (`/branding/*`) | static asset serving | `branding-api.ts` | branding-api.test.ts | PARTIAL | static traversal not asserted
`APIS-059` | Bridge auth & spaces | Branding upload/delete (`POST/DELETE /api/branding/upload`) | **adminGuard** (passthrough in no-auth) | `branding-api.ts` | branding-api.test.ts | FULL |
`APIS-060` | Bridge auth & spaces | Security headers / HSTS opt-in | HSTS default off (`max-age=0` clear), opt-in enable; 180d max-age | `security-headers.ts` | security-headers.test.ts | FULL | 15 expects (no authz; correct)
`APIS-061` | Bridge auth & spaces | Server TLS listener | HTTPS listener creation; cert handling | `server-tls-listener.ts` | server-tls-listener.test.ts, server.tls.test.ts | FULL |
`APIS-062` | Bridge auth & spaces | Dashboard API (`createDashboardApi`) | worker/node (GPU) status tabs | `dashboard-api.ts` | dashboard-api.test.ts | PARTIAL | no explicit auth guard visible in router; visibility of dashboard data not gated/asserted
`APIS-063` | Bridge auth & spaces | Dashboard workers | worker status aggregation | `dashboard-workers.ts` | dashboard-workers.test.ts | FULL |
`APIS-064` | Bridge auth & spaces | `buildVisibilityWhere` / `buildSpaceVisibilityWhere` | SQL WHERE for private/org/public | `visibility.ts` | visibility.test.ts | FULL | 53 expects
`APIS-065` | Bridge auth & spaces | `canManageSpace` / `canEditInSpace` / `canEditEntity` / `canUserSeeTask` | role/owner/admin authz predicates | `visibility.ts` | visibility.test.ts | FULL | central predicates well unit-tested
`APIS-066` | Bridge auth & spaces | Cross-space credential transfer (G3/#006) | copy MCP/SSH creds between spaces, per-user token non-copy | `(PR #642 — NOT in main)` | (n/a in main) | NONE | **Not merged to main** — no endpoint present; out of scope for current tree
`SVC-001` | Services & DB | Worker poll/claim loop | `processNext`/`acquireJobOrRequeue` claim a queued job or requeue when issue lock held | `src/worker.ts` | worker.test.ts ("requeues a claimed job when the issue lock is already held") | PARTIAL | poll cadence, `pokePoll`, empty-queue path not directly asserted
`SVC-002` | Services & DB | Role / task_class matching | `canClaimRole`/`supportsRole`/`getSupportedRoles` gate which jobs a worker claims | `src/worker.ts, src/db/repository.ts (claimNextJob)` | repository.test.ts ("claims only jobs matching a healthy worker role set"; "does not let unhealthy workers claim queued jobs") | FULL | role-set matching + healthy filter covered at repo layer
`SVC-003` | Services & DB | Title-only / reflection role filters | `roles:['title']` skips agent jobs; `roles:['reflection']` runs reflection handler | `src/worker.ts (handleReflectionJob, supportsRole)` | worker.reflection-dispatch.test.ts; worker.reflection-enqueue.test.ts (16) | PARTIAL | reflection enqueue + dispatch well covered; title-only worker skip path has no direct test
`SVC-004` | Services & DB | Job lifecycle transitions | queued→dispatching→running→succeeded/failed/waiting_human/waiting_subtasks via updateJob | `src/worker.ts, src/db/repository.ts` | worker.test.ts; repository.test.ts; spaces-task-ops (cancel flips running→cancelled) | PARTIAL | terminal mapping covered (metrics test); waiting_human/waiting_subtasks transitions only indirectly exercised
`SVC-005` | Services & DB | Retry / maxAttempts policy | terminal vs transient classification; one recovery retry for max_iterations; give up on repeat | `src/worker.ts (executeJob retry logic)` | worker.test.ts (5 retry cases incl. self-abort, max_iterations 1st/2nd hit, llm_error budget) | FULL | strong branch coverage of terminal vs transient + attempt budget
`SVC-006` | Services & DB | Retry handoff summary | `buildRetryHandoffSummary`/`writeRetryHandoffSummary` carry lessons+diagnostics into retry | `src/worker.ts` | worker.test.ts ("writes retry handoff summary"; "builds retry handoff summary from diagnostics and lessons") | FULL |
`SVC-007` | Services & DB | Deadline / hard-abort guard | `shouldDeadlineAbort` aborts past deadline; job-guard timer force-aborts | `src/worker.ts` | worker.job-guard.test.ts (8: before/after deadline, disabled=0, no re-abort, cancel mid-run, hard deadline) | FULL |
`SVC-008` | Services & DB | Cancellation check | `cancelCheck` returns true when DB job status is cancelled | `src/worker.ts, repository.requestJobCancel` | worker.test.ts ("cancelCheck returns true when cancelled"); repository.test.ts (requestJobCancel 3 cases) | FULL | running→cancelled, queued no-op, unknown id all covered
`SVC-009` | Services & DB | max_concurrency slot scheduling | runs up to N concurrent, refills freed slot, single-flight when unset | `src/worker.ts` | worker.concurrency.test.ts (5: concurrent, single-flight, waitForCompletion, decrement-on-throw) | FULL |
`SVC-010` | Services & DB | Initialize / health probe | `initialize` probes /api/tags + /v1/models, forwards Bearer, stays healthy on llama-server compat | `src/worker.ts` | worker.test.ts (3 initialize cases) | FULL |
`SVC-011` | Services & DB | Unhealthy on LLM conn error | requeues jobs + marks worker unhealthy on connection errors | `src/worker.ts` | worker.test.ts ("requeues jobs and marks the worker unhealthy on LLM connection errors") | FULL |
`SVC-012` | Services & DB | resolveToolSpaceId (per-space MCP/SSH) | NULL space + owner → personal space; keeps explicit; fails CLOSED on resolver error | `src/worker.ts` | worker.test.ts (5 resolveToolSpaceId cases) | FULL | fail-closed sentinel asserted
`SVC-013` | Services & DB | Piece resolution for null-owner job | no-auth job loads piece from data/users/local/pieces consistently | `src/worker.ts` | worker.test.ts (2 null-ownerId piece cases) | FULL |
`SVC-014` | Services & DB | prepareJobWorkspace resolution | uses stored workspacePath or falls back to legacy local/{taskId} + backfills | `src/worker.ts, src/spaces/workspace-resolver.ts` | worker.test.ts (2 cases); workspace-resolver.test.ts (6) | FULL | persistent/ephemeral/personal + backfill covered
`SVC-015` | Services & DB | maybeEnqueueReflection budget gate | enqueue/skip by enabled flag, daily UTC budget cap, worker-required, per-user keying | `src/worker.ts` | worker.reflection-enqueue.test.ts (16) | FULL | strong: cap=0, 25h window, no-recursion, independent budgets
`SVC-016` | Services & DB | resolveModel / role→model mapping | selects backend model for the job role | `src/worker.ts (resolveModel)` | worker/sticky-backend.test.ts (resolver follow-current); scheduling.test.ts (normalizeJobRole/parseUiRole) | PARTIAL | role parsing well covered; resolveModel's own fallback branches not directly tested
`SVC-017` | Services & DB | Sticky backend resolver | persists first backend, advances on CHANGE, retries on persist failure | `src/worker.ts (createStickyBackendResolver)` | worker/sticky-backend.test.ts (6) | FULL |
`SVC-018` | Services & DB | Idle-routing yield | `pickIdlerIndex` yields to strictly-idler serving sibling, ignores unhealthy/non-serving | `src/worker.ts (findIdlerCompetitor)` | worker/idle-routing.test.ts (9) | FULL |
`SVC-019` | Services & DB | answerSubtaskAsk / subtask resume | answers a subtask ASK; parent requeue when all subtasks done | `src/worker.ts, repository.requeueParentJobIfAllSubtasksDone` | (none direct in scope) | NONE | repo helper `requeueParentJobIfAllSubtasksDone` + worker `answerSubtaskAsk` untested
`SVC-020` | Services & DB | Worker shutdown hooks | installs + drains shutdown hooks in order, tolerates throwing hook | `src/worker.ts / worker-bootstrap` | worker-bootstrap.test.ts (4) | FULL |
`SVC-021` | Services & DB | Worker metrics | counters increment on labelled inc; terminal-status mapping complete | `src/worker.ts` | worker.metrics.test.ts (3) | FULL |
`SVC-022` | Services & DB | WorkerManager differential rebuild | rebuild on config change: keep busy/unchanged worker, retire changed/removed without requeue, prune retired | `src/worker-manager.ts` | worker-manager.test.ts (8) | FULL | live-job protection + def-signature diff covered
`SVC-023` | Services & DB | WorkerManager shutdown requeue | requeues running jobs only on shutdown when worker fails to drain; not on clean shutdown | `src/worker-manager.ts` | worker-manager.test.ts (2 shutdown cases) | FULL |
`SVC-024` | Services & DB | Worker dep injection setters | setMcpDeps/setWorkerMetrics/setSkillCatalog/setPushService wire shared deps | `src/worker-manager.ts` | (none direct) | NONE | low-risk wiring; uncovered
`SVC-025` | Services & DB | Scheduler cron conversion | `convertToCron` maps daily/weekly/monthly→cron; passes cron through; once special-cased | `src/scheduler.ts` | scheduler.test.ts (convertToCron 5 cases) | FULL |
`SVC-026` | Services & DB | calcNextRun / toSqliteDatetime | next-run from cron; SQLite-compatible datetime; once→null | `src/scheduler.ts` | scheduler.test.ts (calcNextRun 3 + toSqliteDatetime 2) | FULL |
`SVC-027` | Services & DB | Scheduler skip-on-in-progress | skips when prior job running/waiting_subtasks; starts new on waiting_human | `src/scheduler.ts (IN_PROGRESS_STATUSES)` | scheduler.test.ts (4 skip cases) | FULL | waiting_human-no-longer-blocks asserted
`SVC-028` | Services & DB | Schedule ownership inheritance | propagates ownerId/visibility/scopeOrg + space_id to spawned local_task; NULL owner for system | `src/scheduler.ts (executeScheduledTask)` | scheduler.test.ts (multiple ownership + space-bound vs personal cases) | FULL |
`SVC-029` | Services & DB | Scheduled pieceName / classifier | falls back to classifier when pieceName unset; preserves explicit pieceName | `src/scheduler.ts` | scheduler.test.ts (pieceName cases) | FULL |
`SVC-030` | Services & DB | Scheduled script (task_kind=script) | runs browser-macro/script, writes logs to runtimeDir + output to space tree; failure paths; gate+allowlist | `src/scheduler.ts (executeScriptScheduledTask)` | scheduler.test.ts (script cases incl. gate disabled, not-in-allowlist, audit row, missing script, null scriptName guard) | FULL |
`SVC-031` | Services & DB | executeById manual run | runs a scheduled task on demand | `src/scheduler.ts` | (none direct) | NONE | manual-trigger path untested
`CFG-001` | Services & DB | transformKeys snake↔camel | toCamel/toSnake/toSnakeKeys round-trip incl. nested/arrays | `src/config.ts` | config.test.ts (toSnakeKeys 5 cases) | FULL |
`CFG-002` | Services & DB | loadConfig provider.retry | loads from YAML or default; deprecated profiles→roles shim; proxy/proxyType defaults | `src/config.ts` | config.test.ts (provider.retry + roles/proxy cases) | FULL |
`CFG-003` | Services & DB | validateConfig | rejects bad concurrency/maxMovements/ask/maxStreamMinutes/maxJobMinutes/maxDepth/retry/worker fields/proxy type | `src/config.ts` | config.test.ts (~35 validateConfig cases) | FULL | exhaustive boundary coverage
`CFG-004` | Services & DB | Env overrides (config.ts) | OLLAMA_BASE_URL/OLLAMA_MODEL/CONCURRENCY/WORKTREE_DIR/AAO_* override loaded config | `src/config.ts` | config.audit-regression.test.ts (OLLAMA_BASE_URL targets executed worker only, leaves persisted llm + extra workers) | PARTIAL | only OLLAMA_BASE_URL asserted; CONCURRENCY/WORKTREE_DIR/OLLAMA_MODEL/AAO_* overrides untested
`CFG-005` | Services & DB | normalizeConfig version handling | v2 pass-through; missing→v1; unsupported version throws; null input→empty | `src/config-normalize.ts` | config-normalize.test.ts (version handling, ~6) | FULL |
`CFG-006` | Services & DB | normalizeConfig v1→v2 llm | proxy→connection_type, worker.model inheritance, single default worker, profiles→roles, retry/metrics map | `src/config-normalize.ts` | config-normalize.test.ts (v1→v2 block, ~10) | FULL |

`CFG-007`

Services & DB

config-normalizer storage mirror

top-level flat keys ↔ storage.* both directions; storage.* precedence (#369); worktreeDir survival

`src/config-normalize.ts, src/config.ts`

config-normalize.test.ts (storage migration + backwards-compat, ~8); config.audit-regression.test.ts (trash_retention_days takes effect)

FULL

precedence trap (default-merge-before-normalize) regression-guarded

`CFG-008` | Services & DB | Env-ref preservation | ${VAR} / env: prefix preserved verbatim through normalize | `src/config-normalize.ts` | config-normalize.test.ts (3 env-ref cases) | FULL |
`CFG-009` | Services & DB | browser.display_mode migration | legacy captchaSolve=novnc → displayMode=novnc, key removed | `src/config-normalize.ts (migrateBrowserDisplayMode)` | config-normalize.test.ts (display_mode case) | FULL |
`CFG-010` | Services & DB | normalize fixtures (real configs) | v1-single-ollama / multi-worker-proxy / gateway-with-keys / mcp-and-ssh normalize correctly | `src/config-normalize.ts` | config-normalize.test.ts (4 fixture cases) | FULL |
`CFG-011` | Services & DB | Auth config loading | parses auth section snake_case + primary_provider; undefined when absent | `src/config.ts` | config-auth.test.ts (4) | PARTIAL | parsing covered; provider validation/exclusivity not asserted here
`CFG-012` | Services & DB | ConfigManager read + ETag | getConfig/getConfigForApi; etag from mtime; reloadFromFile | `src/config-manager.ts` | config-manager.test.ts (load, etag, reload, masked-for-API) | FULL |
`CFG-013` | Services & DB | ConfigManager write + CAS | updateConfig writes YAML, rejects stale etag, creates file on fresh install, rejects unparseable | `src/config-manager.ts` | config-manager.test.ts (update, stale-etag reject, fresh-install create, invalid YAML reject) | FULL |
`CFG-014` | Services & DB | ConfigManager masking preservation | masks apiKey (`********`); preserves masked field on update; matches workers/backends by id; drops mask when no prior key | `src/config-manager.ts (mergeWithMaskPreservation)` | config-manager.test.ts (mask + preserve by-id for llm.workers + gateway.backends, drop-mask path) | FULL |
`CFG-015` | Services & DB | ConfigManager change events | emits config-changed on update | `src/config-manager.ts` | config-manager.test.ts ("emits config-changed on update") | FULL |
`CFG-016` | Services & DB | config-manager env overrides | OLLAMA_*/WORKTREE_DIR/CONCURRENCY/DB_PATH applied at runtime read | `src/config-manager.ts` | (none direct in config-manager.test.ts) | NONE | DB_PATH + runtime env override path untested at config-manager layer
`CFG-017` | Services & DB | Server TLS / listen-port config | mergeServerConfig TLS defaults, http_redirect port normalize/collision, resolveListenPort + PORT env | `src/server/config.ts` | server/config.test.ts (24) | FULL | (PORT/LOG_LEVEL env live here, not config.ts)
`CFG-018` | Services & DB | migrate-config CLI | --help/unknown flag/missing file/dry-run/in-place+backup/already-v2/version-99 | `src/scripts/migrate-config.ts` | scripts/migrate-config.test.ts (8) | FULL |
`DB-001` | Services & DB | createJob / getJob round-trip | persists job incl. space_id, runtime_dir, continued_from; subtask inherits parent space_id | `src/db/repository.ts` | spaces-resolution.test.ts; runtime-dir.test.ts; migrate.test.ts | FULL |
`DB-002` | Services & DB | claimNextJob / retry / peek | role+health-aware claim; claimNextRetryJob; peekNextClaimable | `src/db/repository.ts` | repository.test.ts (worker-aware scheduling 2) | PARTIAL | claimNextRetryJob + peekNextClaimable not individually asserted
`DB-003` | Services & DB | requeueRunningJobs / recovery | requeueRunningJobs, recoverOrphanedJobs, recoverStuckRunningJobs, resumeMcpWaitingJobs | `src/db/repository.ts` | (worker-manager shutdown exercises requeue indirectly) | PARTIAL | recoverOrphaned/recoverStuck/resumeMcp* have no direct unit test
`DB-004` | Services & DB | requestJobCancel | running→cancelled; queued no-op; unknown id false | `src/db/repository.ts` | repository.test.ts (3) | FULL |
`DB-005` | Services & DB | createLocalTask / getLocalTask | persists owner/visibility/space_id/runtime_dir; reads back | `src/db/repository.ts` | repository.test.ts; repository-auth.test.ts; spaces-resolution.test.ts | FULL |
`DB-006` | Services & DB | listLocalTasks + latestJob/subtasks | joins latest job, subtask info, ownerName/org name; ownerId filter | `src/db/repository.ts` | repository.test.ts (listLocalTasks suite); repository-auth.test.ts (ownerId filter) | FULL |
`DB-007` | Services & DB | deleteLocalTask whitelist | deletes task+jobs; refuses when running; whitelists only ephemeral/local per-task dirs; protects shared space tree | `src/db/repository.ts` | repository.test.ts (4); spaces-task-ops.test.ts (~9 delete cases incl. symlink/trailing-slash/null-space) | FULL | data-loss regression strongly guarded
`DB-008` | Services & DB | local_task_comments | addLocalTaskComment, listLocalTaskComments, getUninjected/markInjected, getLatestResultComment, attachments | `src/db/repository.ts` | repository.test.ts (Feedback/Share); migrate.test.ts (attachments column) | PARTIAL | comment inject/uninject + getLatestResultComment lifecycle not directly asserted
`DB-009` | Services & DB | buildVisibilityWhere | admin 1=1; owner/public; same-org branch; spaceColumn membership+owner OR; admin ignores spaceColumn | `src/db/repository.ts` | bridge/visibility.test.ts (33 incl. param-order, byte-identical back-compat) | FULL | param ordering + admin-ignores-membership asserted
`DB-010` | Services & DB | canManageSpace / canEditInSpace | role-gated write checks (admin/owner/member roles, viewer read-only) | `src/db/repository.ts` | bridge/visibility.test.ts (canManage + canEdit suites) | FULL |
`DB-011` | Services & DB | Spaces CRUD | createSpace/getSpace/listSpaces (visibility-filtered)/updateSpace/archiveSpace | `src/db/repository.ts` | spaces-repository.test.ts (12) | FULL |
`DB-012` | Services & DB | ensurePersonalSpace invariant | exactly one private personal space/user, idempotent; never hard-deletable | `src/db/repository.ts` | spaces-repository.test.ts (ensurePersonalSpace + hardDelete invariant) | FULL |
`DB-013` | Services & DB | hardDeleteSpace crypto-shred | shreds case space DEK on delete; never personal | `src/db/repository.ts` | spaces-repository.test.ts (crypto-shred case) | FULL |
`DB-014` | Services & DB | Space members CRUD + visibility | add/list/getRole/updateRole/remove; membership-based read; non-member leak prevention; revocation | `src/db/repository.ts` | repository.shared-space.test.ts (23) | FULL | collaboration + leak + revocation all asserted
`DB-015` | Services & DB | Personal space owner-only visibility | even admin cannot see another user's personal space (list+get); no-auth synthetic owner path | `src/db/repository.ts` | repository.personal-space-visibility.test.ts (8) | FULL |
`DB-016` | Services & DB | Per-space resource isolation | space-A task sees only space-A MCP/SSH (not space-B/global); personal sees no case-space resources | `src/db/repository.ts` | repository.spaces-resource-isolation.test.ts (4) | FULL |
`DB-017` | Services & DB | Space tool_policy persistence | default null; round-trips JSON; clear via null | `src/db/repository.ts` | repository.tool-policy.test.ts (5) | FULL |
`DB-018` | Services & DB | Space invites | create (no-expiry default), regenerate replaces, expiry validity, revoke, FK cascade | `src/db/repository.ts` | repository.space-invites.test.ts (7) | FULL |
`DB-019` | Services & DB | App share links | create/get/revoke/resolveAppShareToken | `src/db/repository.ts` | repository.app-share.test.ts (out-of-listed but present) | PARTIAL | present file not fully read; assume happy-path coverage
`DB-020` | Services & DB | Tool requests + grant overlay | record/list newest-first, de-dup by job/task/piece, idempotent decide, granted-tools round-trip, per-piece aggregate | `src/db/repository.ts` | repository.tool-requests.test.ts (7) | FULL |
`DB-021` | Services & DB | User CRUD + OAuth link | createUser/getById/getByEmail/findOrCreateUserByOAuth (link by email)/listUsers/updateUser/deleteUser cascade | `src/db/repository.ts` | repository-auth.test.ts (17) | FULL |
`DB-022` | Services & DB | Local-auth credentials | setLocalPassword/verify round-trip, per-user salt, createLocalUser email-takeover reject, upsertLocalSystemAdmin idempotent, delete refuses local user | `src/db/repository.ts` | repository-local-auth.test.ts (12) | FULL |
`DB-023` | Services & DB | Local orgs | createLocalOrg (lorg: id), rename, member add/remove idempotent, listUserLocalOrgs, delete cascade + downgrade scoped resources | `src/db/repository.ts` | repository-local-orgs.test.ts (10) | FULL |
`DB-024` | Services & DB | ensureLocalUser DEK fix | creates per-user row so DEK/SSH create succeeds; idempotent | `src/db/repository.ts` | ensure-local-user.test.ts (4) | FULL |
`DB-025` | Services & DB | ScheduledTasks CRUD | create/get/list/getDue/update/delete; space_id persisted (not dead-wired) | `src/db/repository.ts` | repository.test.ts (ScheduledTasks 5); spaces-resolution.test.ts (space_id round-trip) | FULL |
`DB-026` | Services & DB | Share token (local task) | shareLocalTask token, idempotent, unshare clears, getByShareToken null on unknown/after-unshare | `src/db/repository.ts` | repository.test.ts (Share feature 6) | FULL |
`DB-027` | Services & DB | addAuditLog | single audit_log insert helper | `src/db/repository.ts` | scheduler.test.ts (user_script_run audit row asserted via path) | PARTIAL | addAuditLog itself not unit-tested; only one consumer asserted
`DB-028` | Services & DB | Gateway virtual keys | find/list/revoke/delete/touchLastUsed + usage | `src/db/repository.ts` | repository.gateway-keys.test.ts; gateway-usage.test.ts | FULL | (gateway-adjacent; in repo file)
`DB-029` | Services & DB | LLM usage aggregation | incrementLlmUsage (daily) + hourly + queryLlmUsageDaily + org map | `src/db/repository.ts` | repository.llm-usage.test.ts; llm-usage-hourly.test.ts | FULL |
`DB-030` | Services & DB | Push subscriptions / prefs | upsert/list/get/delete/markSuccess/markFailure; notification prefs | `src/db/repository.ts` | (push-service.test.ts at higher layer) | PARTIAL | repo-level push CRUD not directly asserted
`DB-031` | Services & DB | Calendar events | getCalendarEvent/deleteCalendarEvent | `src/db/repository.ts` | repository.calendar.test.ts; cross-calendar.test.ts | FULL |
`DB-032` | Services & DB | resumeToolRequestJob | resumes a job waiting on a tool grant | `src/db/repository.ts` | (none direct) | NONE | resume-after-grant path untested at repo layer
`DB-033` | Services & DB | Schema migrations (idempotent ALTER) | runMigrations adds owner_id/continued_from/last_backend_id/attachments; idempotent | `src/db/migrate.ts, schema.sql` | migrate.test.ts (16); migrate.space-id/ssh-space/mcp-space/reflection-columns/gateway-2b/privatize/rename tests | FULL | each major migration has its own test file
`DB-034` | Services & DB | MCP table migrations | creates mcp_servers/tokens/tools/oauth_pending idempotent; BLOB cols; auth_kind default; owner index | `src/db/migrate.ts` | migrate.test.ts (MCP suite); migrate.mcp-auth-columns.test.ts | FULL |
`DB-035` | Services & DB | Space backfill migrations | backfillMcpServer/Ssh/BrowserSession space_ids; privatize space rows; rename personal title | `src/db/migrate.ts` | migrate.mcp-space/ssh-space/privatize-space-rows/rename-personal-space-title.test.ts | FULL |
`DB-036` | Services & DB | resolveTaskWorkspaceDir / dirs | persistent→space files tree; ephemeral→throwaway; personal-space; ensureWorkspaceDirs creates subdirs | `src/spaces/workspace-resolver.ts` | workspace-resolver.test.ts (6); spaces-resolution.test.ts (resolveTaskFolderContext) | FULL |
`DB-037` | Services & DB | Credential DEK encryption | encrypt under SPACE DEK for space profile, OWNER DEK for personal; migration fallback; cross-DEK isolation; throws with no material | `src/crypto/profile-dek.ts` | crypto/profile-dek.test.ts (6) | FULL | space-vs-owner DEK separation + non-decryptability asserted
`DB-038` | Services & DB | Branding storage | branding config/assets read-write (gitignored data/branding) | `src/bridge/branding-api.ts` | bridge/branding-api.test.ts | PARTIAL | API-layer test exists; storage-layer branch coverage unverified
`UI-001` | UI | Calendar month grid + bars | month grid, week split, range/time fmt, multi-day bar layout | ``lib/calendar.ts`` | `lib/calendar.test.ts` | FULL | —
`UI-002` | UI | Command palette | build/filter/group commands, hotkey gate | ``lib/command-palette.ts`` | `lib/command-palette.test.ts` | FULL | —
`UI-003` | UI | Cron → form state | parse cron string into schedule fields | ``lib/cronForm.ts`` | `lib/cronForm.test.ts` | PARTIAL | only `cronToFormState` exported/tested; reverse (form→cron) lives in `ScheduleFields.tsx` untested
`UI-004` | UI | File move resolution | basename/parentDir, resolve drag-move plan (conflicts) | ``lib/fileMove.ts`` | `lib/fileMove.test.ts` | FULL | —
`UI-005` | UI | File type categorization | split name, category, color class | ``lib/fileType.ts`` | `lib/fileType.test.ts` | FULL | —
`UI-006` | UI | File view sort/format | sort entries, toggle sort, fmt timestamp/size | ``lib/fileView.ts`` | `lib/fileView.test.ts` | FULL | —
`UI-007` | UI | Help docs frontmatter/render | split/validate frontmatter, parse, slugify, render HTML, filter | ``lib/help.ts`` | `lib/help.test.ts` | FULL | —
`UI-008` | UI | Live-workspace auto-focus | decide auto-focus on new live task | ``lib/live-workspace.ts`` | `lib/live-workspace.test.ts` | FULL | —
`UI-009` | UI | Memory summary/filter | summarize + filter memory entries by type | ``lib/memorySummary.ts`` | `lib/memorySummary.test.ts` | FULL | —
`UI-010` | UI | Notifications logic | status→event map, shouldNotify, debounce, build options | ``lib/notifications.ts`` | `lib/notifications.test.ts` | FULL | `createNotification`/permission are browser-API thin wrappers (not unit-covered, acceptable)
`UI-011` | UI | Output-path detection/linkify | regex split text by output paths, linkify escaped HTML | ``lib/output-path-detect.ts`` | `lib/output-path-detect.test.ts` | FULL | —
`UI-012` | UI | Owner display name | derive owner label | ``lib/owner.ts`` | `lib/owner.test.ts` | FULL | —
`UI-013` | UI | Public route matching | match login/join/shared public routes | ``lib/publicRoute.ts`` | `lib/publicRoute.test.ts` | FULL | —
`UI-014` | UI | Push subscribe helpers | isPushSupported/iOS/PWA, base64→Uint8Array | ``lib/push-subscribe.ts`` | `lib/push-subscribe.test.ts` | FULL | —
`UI-015` | UI | Shared view URLs | shared image base + raw url builders | ``lib/sharedView.ts`` | `lib/sharedView.test.ts` | FULL | —
`UI-016` | UI | Sharing scope preview | visibility preview text + tooltip | ``lib/sharingScope.ts`` | `lib/sharingScope.test.ts` | FULL | —
`UI-017` | UI | Space branding vars | parse color, derive brand CSS vars | ``lib/spaceBranding.ts`` | `lib/spaceBranding.test.ts` | FULL | —
`UI-018` | UI | Space rail sort | order spaces for rail | ``lib/spaceSort.ts`` | `lib/spaceSort.test.ts` | FULL | —
`UI-019` | UI | Space task filter/count | filter tasks for space, count running | ``lib/spaceTasks.ts`` | `lib/spaceTasks.test.ts` | FULL | —
`UI-020` | UI | Split pieces (custom/default) | split + resolve piece options | ``lib/splitPieces.ts`` | `lib/splitPieces.test.ts` | FULL | —
`UI-021` | UI | Streaming field extract | extract a field from partial SSE stream | ``lib/streamFieldExtract.ts`` | `lib/streamFieldExtract.test.ts` | FULL | —
`UI-022` | UI | Tab swipe physics | axis lock, resist, commit, neighbor index | ``lib/tab-swipe.ts`` | `lib/tab-swipe.test.ts` | FULL | —
`UI-023` | UI | Task list filter/sort/group | query match, group-by-status, counts, filter+sort | ``lib/taskFilter.ts`` | `lib/taskFilter.test.ts` | FULL | —
`UI-024` | UI | Task scope filter | filter tasks by scope tab | ``lib/taskScope.ts`` | `lib/taskScope.test.ts` | FULL | —
`UI-025` | UI | Theme pref + apply | resolve/store/apply theme, system-dark, events | ``lib/theme.ts` (+ `theme-css-parity.test.ts`)` | `lib/theme.test.ts`, `lib/theme-css-parity.test.ts` | FULL | —
`UI-026` | UI | Tool policy split/patch | split categories, build policy patch, count enabled | ``lib/toolPolicy.ts`` | `lib/toolPolicy.test.ts` | PARTIAL | known Bash/sensitive-tools write-path gap (MEMORY: PR #653) — verify both category + sensitiveTools systems covered
`UI-027` | UI | Unsaved-changes guard | detect unsaved, confirm discard, hook | ``lib/unsavedGuard.ts`` | `lib/unsavedGuard.test.ts` | PARTIAL | `useUnsavedGuard` hook portion needs jsdom; pure parts covered
`UI-028` | UI | URL state (useUrlState) | read/build search params, legacy ?page=tasks redirect | ``lib/urlState.ts`` | `lib/urlState.test.ts` | FULL | hook wrapper `useUrlState.ts` itself untested (jsdom) but pure core covered
`UI-029` | UI | Misc utils + activity log parse | relativeTime, status tones, previewable-by-type, parseActivityLog | ``lib/utils.ts`` | `lib/utils.test.ts` | FULL | —
`UI-030` | UI | Workspace dir role | classify input/output/logs/subtasks dir | ``lib/workspaceDirs.ts`` | `lib/workspaceDirs.test.ts` | FULL | —
`UI-031` | UI | Pet state machine | pet mood/animation state transitions | ``lib/pets/petState.ts`` | `lib/pets/petState.test.ts` | FULL | `lib/pets/toolIconMap.ts` is a static map (no test, low value)
`UI-032` | UI | i18n workspace terminology | locale keys use "ワークスペース" consistently | ``i18n/index.ts`, `i18n/locales`` | `i18n/layout-workspace.test.ts` | PARTIAL | only layout/workspace terms checked; full key-parity across locales not asserted
`UI-033` | UI | Service worker push handler | SW push event → notification payload | ``sw/push-handler.ts`` | `sw/push-handler.test.ts` | FULL | —
`UI-034` | UI | App share URL builder | build shareable app URL/token | ``components/spaces/appShareUrl.ts`` | `components/spaces/appShareUrl.test.ts` | FULL | —
`UI-035` | UI | App↔iframe postMessage bridge | validate/route postMessage between app iframe + host | ``components/spaces/app-bridge.ts`` | `components/spaces/app-bridge.test.ts` (31 cases) | FULL | heaviest pure suite; strong
`UI-036` | UI | App file gateway (shared/auth) | route file reads through gateway, prevent 401 drift | ``components/spaces/app-file-gateway.ts`` | `components/spaces/app-file-gateway.test.ts` | FULL | —
`UI-037` | UI | Chat detail split logic | split chat vs detail pane state | ``components/spaces/ChatDetailSplit.tsx` (logic)` | `components/spaces/ChatDetailSplit.test.ts` | PARTIAL | pure split logic tested; component render not
`UI-038` | UI | Space detail keying | stable key/identity for space detail | ``components/spaces/SpaceDetail.tsx` (logic)` | `components/spaces/spaceDetailKeying.test.ts` | PARTIAL | keying logic only
`UI-039` | UI | App auto-approve gate | decide auto-approve of app file ops | ``components/spaces/AppRunner.tsx` (logic)` | `components/spaces/AppRunner.autoapprove.test.tsx` | PARTIAL | only auto-approve branch; AppRunner iframe lifecycle untested
`UI-040` | UI | App-share API client | app-share fetch client behavior | ``src/api.ts` (app-share)` | `api.app-share.test.ts` | PARTIAL | only app-share endpoints; rest of `api.ts` untested
`UI-041` | UI | Harness gateway | test-harness gateway (mounts real AppRunner) | ``harness/`` | `harness/harness-gateway.test.ts` | FULL | —
`UI-042` | UI | Shared tabs (public view) | which tabs show in SharedView/SharedAppView | ``pages/SharedView.tsx`/`SharedAppView.tsx` (logic)` | `pages/sharedTabs.test.ts` | PARTIAL | tab-selection logic only
`UI-043` | UI | Detail tab selection | detail tab list / readonly gating | ``components/detail/*` (logic)` | `detail/detailTabs.test.ts`, `detail/detail-readonly.test.ts` | PARTIAL | tab + readonly logic; not full panels
`UI-044` | UI | Task data source resolution | choose live vs shared data source for task | ``components/detail/task-data-source.tsx` (logic)` | `detail/task-data-source.test.ts` | FULL | —
`UI-045` | UI | Console key handling | terminal key → escape sequences | ``detail/tabs/console/*`` | `detail/tabs/console/keys.test.ts` | PARTIAL | key mapping only; TerminalView render/websocket untested
`UI-046` | UI | TopBar collapse logic | which tabs collapse into overflow | ``components/layout/TopBar.tsx` (logic)` | `layout/topbar-collapse.test.ts` | PARTIAL | collapse math only
`UI-047` | UI | Settings sidebar sections | section list/visibility logic | ``components/settings/SettingsSidebar.tsx` (logic)` | `settings/settingsSidebar.test.ts` | PARTIAL | section logic; per-form rendering untested
`UI-048` | UI | Rotating tips | pick/rotate chat tip strings | ``components/chat/RotatingTips.tsx` (logic)` | `chat/RotatingTips.tips.test.ts` | FULL | —
`UI-049` | UI | File drag-n-drop transfer | build/read DataTransfer drag sources for files | ``lib/fileDnd.ts` (`dragSources`, `readDragSources`)` | none | NONE | pure, easy to test now; DnD bugs are user-visible
`UI-050` | UI | Files → base64 | convert File[] to base64 upload payload | ``lib/fileBase64.ts`` | none | NONE | uses FileReader (jsdom/node-FileReader needed); logic small but worth a test
`UI-051` | UI | Local date helpers | localToday, tz offset, shiftDay | ``lib/localDate.ts`` | none | NONE | pure date math — high regression risk (tz), trivially testable
`UI-052` | UI | Space color | derive deterministic space color | ``lib/spaceColor.ts`` | none | NONE | pure; deterministic-output test trivial
`UI-053` | UI | Polling/stale constants | POLLING + STALE_TIME tables | ``lib/constants.ts`` | none | NONE | low value (constants) — skip unless invariants needed
`UI-054` | UI | Help content loader | load+assemble help sections from raw md | ``lib/help-content.ts` (`loadHelpSections`)` | none | PARTIAL | `help.ts` parsing covered, but the loader/aggregation step is untested
`UI-055` | UI | FilePreview render (md/csv/jsonl/image/pdf) | render each preview type; **relative-image rewrite** to `/api/local/tasks/:id/files/raw` | ``components/files/FilePreview.tsx` (`resolvedHref`, line ~320)` | none (only `lib/utils` `isPreviewable` flags) | PARTIAL | the relative-image rewrite is core, user-visible, untested. Could extract `resolveImageHref` to lib → sandbox-testable

`UI-056`

UI

FileBrowser (grid/list, sort, select, move)

tile/detail views, multi-select, drag-move, breadcrumb

``components/files/FileBrowser.tsx` + `FileTileGrid/FileDetailList/FileBreadcrumb/MoveTargetDialog``

e2e `spaces.spec.ts` (icon grid/upload/delete/multiselect/download)

PARTIAL

core flows covered by e2e; jsdom unit tests absent (sort/select logic is in tested libs UI-006/UI-004)

`UI-056b` | UI | useFileBrowser/useFileView/useFilePreview hooks | data fetching + view state for files | ``hooks/useFileBrowser.ts`, `useFileView.ts`, `useFilePreview.ts`` | none | NONE | jsdom
`UI-057` | UI | ChatPane / ChatMessage / MovementGroup | message stream, movement grouping, tool-calls section | ``components/chat/*`` | e2e (inline chat create/open, cancel) | PARTIAL | rendering untested in jsdom; movement-group grouping logic not extracted to lib
`UI-058` | UI | Tool-request approval card | inline Approve/Deny grants + resolves request | ``components/chat/ToolRequestApproval.tsx`` | e2e `tool-request.spec.ts` (approve, deny) | FULL | jsdom unit absent; e2e covers both branches
`UI-059` | UI | Settings ConfigForm + per-section forms | edit each config.yaml section (Tools/LLM/Context/Safety/SSH/MCP/Gateway/Branding/Pets/Reflection/...) | ``components/settings/*Form.tsx` (~45 files)` | settingsSidebar (sections), toolPolicy (UI-026) | PARTIAL | huge surface; only sidebar + tool-policy logic tested. Per-form validation/round-trip largely untested
`UI-060` | UI | Memory & Learning tab | edit memory entries, reflection history + revert | ``components/settings/MemoryLearningForm.tsx`, `ReflectionForm.tsx`` | e2e `spaces.spec.ts` (memory persists A vs B), memorySummary lib (UI-009) | PARTIAL | persistence covered by e2e; revert button + reflection-history list render untested
`UI-061` | UI | Usage dashboard | per-user daily LLM usage charts | ``components/usage/UsagePage.tsx`, `settings/GatewayKeyUsagePanel.tsx`` | none | NONE | jsdom; aggregation/format logic not extracted to lib
`UI-062` | UI | Worker/Node status widgets | rail GPU panel, per-backend rows, worker pills | ``components/dashboard/*Widget.tsx`, `hooks/useWorkerStatus/useNodeStatus`` | e2e `spaces.spec.ts` (GPU panel toggle, proxy per-backend rows) | PARTIAL | e2e smoke; widget render/empty-state untested in unit
`UI-063` | UI | Browser session panel + PiP | live browser session view, picture-in-picture | ``components/browser/*`, `hooks/usePictureInPicture.ts`` | e2e `spaces.spec.ts` (BROWSER panel groups render) | PARTIAL | PiP controller untested; panel render only via e2e
`UI-064` | UI | SSH config/console UI | console terminal, grants, audit, key rotation | ``components/settings/Ssh*.tsx`, `userfolder/Ssh*.tsx`, `useConsoleSession`` | none (types only `ssh-console-types.ts`) | NONE | large untested surface; console session hook + websocket untested
`UI-065` | UI | Mobile edge-swipe / swipeable tabs | edge swipe nav, tab swipe gestures | ``hooks/useEdgeSwipe.ts`, `components/mobile/SwipeableTabs.tsx`` | `hooks/useEdgeSwipe.test.ts` (DOM — **fails in sandbox**), `lib/tab-swipe.ts` (UI-022) | PARTIAL | physics covered as lib; the DOM hook test exists but can't run here
`UI-066` | UI | Pets overlay + frame analysis | pet sprite overlay, tool-spark, frame analysis | ``components/pets/*`, `hooks/usePetFrameAnalysis/useActivePet`` | `lib/pets/petState.test.ts` (UI-031) | PARTIAL | state machine covered; overlay render + frame analysis untested
`UI-067` | UI | CreateTaskDialog + PromptCoach + ScheduleFields | new chat/task dialog, prompt coaching, schedule builder | ``components/create/*`` | e2e (＋新規 opens dialog; ephemeral warning), cronForm lib (UI-003) | PARTIAL | dialog open + ephemeral warning via e2e; prompt-coach + form→cron untested
`UI-068` | UI | Monaco file editor (userfolder) | edit AGENTS.md/MCP/skills/memory files | ``components/userfolder/MonacoFileEditor.tsx` + panels` | e2e (AGENTS.md persistence, panels render) | PARTIAL | persistence via e2e; editor component untested in unit
`UI-069` | UI | Command palette UI | open palette, fuzzy filter, execute command | ``components/command/CommandPalette.tsx`` | `lib/command-palette.test.ts` (UI-002) | PARTIAL | all logic in lib (FULL); only the render/keyboard wiring is jsdom
`UI-070` | UI | Embed cards (X/Maps/YouTube/Amazon) | render structured-block embeds + detail modals | ``components/embed/*`` | none | NONE | jsdom; parsing of block data could be extracted to lib
`UI-071` | UI | Setup wizard | first-run setup steps | ``components/setup/SetupWizard.tsx`, `hooks/useSetupState`` | none | NONE | jsdom
`UI-072` | UI | Toast / notifications hooks | toast queue, task notifications dispatch | ``hooks/useToast.ts`, `useTaskNotifications.ts`` | `lib/notifications.test.ts` (UI-010) | PARTIAL | decision logic in lib; hook dispatch/debounce wiring untested
`UI-073` | UI | Spaces foundation + rail + detail tabs | create case space, open detail, mobile rail↔detail | ``components/spaces/*`` | `e2e/spaces.spec.ts` (37 cases) | FULL | most thorough e2e suite (chat CRUD, files, calendar, settings, apps, members, MCP isolation)
`UI-074` | UI | Cross-space calendar | top-bar tab, grid, month nav, per-space dots | ``components/spaces/CrossSpaceCalendar.tsx`` | `e2e/calendar.spec.ts` | FULL | —
`UI-075` | UI | Tasks→workspace migration | Tasks tab removed, personal WS auto-select, legacy redirect | ``App.tsx`, `lib/urlState.ts`` | `e2e/tasks.spec.ts` | FULL | also lib-covered (UI-028)
`UI-076` | UI | Sharing scope (multi-user) | manager/member/stranger visibility (200/404), invite picker | `space visibility` | `e2e-auth/sharing-scope.auth.spec.ts` | FULL | requires auth seed
`UI-077` | UI | Shared space membership | invite→member row+avatars, role change, removal | ``components/spaces/SpaceMembersPanel.tsx`` | `e2e-auth/shared-space.auth.spec.ts` | FULL | —
`UI-078` | UI | Space invite links | join via token, invalid/revoked states | ``components/spaces/JoinSpace.tsx`` | `e2e-auth/space-invite.auth.spec.ts` | FULL | —
`UI-079` | UI | Workspace file input flow | input/output folders show, next-action guidance, ephemeral warning | `Files tab + create dialog` | `e2e-auth/workspace-file-input.auth.spec.ts` | FULL | —
`UI-080` | UI | App-share public view | login-free read-only shared app view | ``pages/SharedAppView.tsx`, `SharedView.tsx`` | none (only lib `appShareUrl`/`sharedTabs`/`sharedView`) | PARTIAL | public viewer flow has no e2e; logic libs covered
`UI-081` | UI | Help page | render help sections by category, search | ``pages/HelpPage.tsx`` | none (lib `help.ts` FULL) | PARTIAL | rendering + search wiring not e2e/jsdom tested
`UI-082` | UI | Pieces page (CRUD) | list/edit custom vs default pieces, movement editor | ``pages/PiecesPage.tsx`, `settings/PieceEditor.tsx`, `MovementForm.tsx`, `RulesTable.tsx`` | none (lib `splitPieces` FULL) | PARTIAL | piece editor / movement rules UI untested
`UI-083` | UI | Schedules page | list/create scheduled tasks (5 types) | ``pages/SchedulesPage.tsx`` | none (lib `cronForm` partial) | PARTIAL | schedule CRUD UI untested
`UI-084` | UI | Admin / users page | user CRUD, captcha, org form | ``pages/UsersPage.tsx`, `AdminCaptchaPage.tsx`, `admin/*`, `settings/OrgsForm.tsx`` | none | NONE | no UI tests for admin user management

## 重要ギャップ (Top gaps)

各領域インベントリの「Top gaps」を集約。最もリスクの高い未テスト挙動。

### Engine core

Highest-risk untested behaviors, ranked:

1. ENG-052 `classifyPiece` async path (NONE). Routing every task to a piece runs through this LLM call + fallback. Only the two pure helpers are tested; the wiring, the null-parse→default-piece fallback, and LLM-error handling have zero coverage. A regression here silently misroutes all tasks.
2. ENG-037 WAITING_HUMAN_BROWSER / WAITING_HUMAN_TOOL_REQUEST pauses (NONE). Two human-wait sentinel branches in piece-runner have no test. A break leaves jobs hung or, worse, falsely completed — same failure class as the documented context-overflow `default_next` trap.
3. ENG-039 Lessons injection + cap + lessons.jsonl (PARTIAL). Lessons appear in snapshot tests, but the core promise — accumulated lessons injected into the next movement's prompt and trimmed at 2000 chars — is not directly asserted, nor is `writeLessonLog`. Silent loss of cross-movement learning would be invisible.
4. ENG-053 `loadAllPieceTriggers` (NONE). Trigger/keyword loading feeds the classifier hint layer; the custom-dir precedence and skip-on-invalid-piece branches are uncovered.
5. ENG-033 cross-movement loop guard per-movement override + reset (PARTIAL) and ENG-013 mid-iteration cancel (PARTIAL). The default-revisit abort is tested, but per-movement `max_consecutive_revisits` and the counter-reset-on-movement-change branch are not — the exact knobs that prevent runaway loops. Likewise mid-work cancellation (vs pre-loop) is only covered via snapshot side effects.

### Tools

1. ReadPPTX ZIP-bomb detection & PPTX size limit (TOOL-015) — NONE. `office.ts:2124-2130` rejects total-uncompressed > 200MB and `validateOfficeFile` enforces the 50MB file cap, but `office.test.ts` contains no test that crafts an over-limit / high-compression-ratio PPTX. This is the only declared anti-DoS guard in the tools layer and it is completely unverified. A malformed/regressed check would silently allow a decompression bomb.
2. Office file-size-limit enforcement across ReadExcel/ReadDocx/ReadPdf (TOOL-012/013/014) — PARTIAL→NONE for the limit path. Tests thoroughly cover *format-mismatch* rejection (CFB/HTML/CSV/OOXML disguises) but never exercise `stats.size > maxSize` → warning emission. The `resolveMaxSize` config override (`tools.office_*_max_size_mb`) is also untested.
3. index.ts dispatch order, first-match-wins, and silent-skip-on-load-failure (XCUT-001) — NONE. A single test checks the SshConsole catalog. The core invariant — that a tool name resolves to exactly one module in a fixed precedence order, and that a module failing to import is skipped without crashing dispatch — has no test. A duplicate tool name or an import regression would pass CI.
4. index.ts runtime META_TOOLS injection (XCUT-002) — PARTIAL. The 20-name runtime META list in `index.ts:411` (RequestTool, MissionUpdate, all UserFolder/AppDocs tools, etc.) is what actually makes these always-available regardless of a movement's allowed_tools, yet only the smaller `tool-categories.ts` SUBSET is asserted. The two lists can drift (they already differ by design); nothing tests that the runtime injection list matches expectations.
5. DownloadFile robustness (TOOL-009) — PARTIAL. Only source-vs-input routing is tested. No coverage for download size limits, redirect following, content-type handling, or failure modes — the same blast radius class as WebFetch but without WebFetch's SSRF/binary test depth.

### Pieces

1. 8 of 15 pieces have ZERO direct test beyond the schema-load sweep (research-sub, brainstorming, sns-research, x-ai-digest, help) and several more are only name-fixtures (chat, slide, data-process, piece-builder). No test asserts their movement/rule/transition contract or their allowed_tools allowlist. A piece could be silently broken (wrong tool name, dropped movement, bad transition graph that still parses) and only `loadPiece().not.toThrow()` would catch it — which it won't, since those are semantic not schema errors. Highest-value targets: research-sub (spawned, complex 3-movement loop), brainstorming, sns-research, x-ai-digest.
2. No allowed_tools-vs-real-tool-registry consistency test. Pieces list tool names in `allowed_tools` as plain strings; nothing asserts every name in every piece resolves to an actual registered tool. A typo (or a renamed/removed tool) leaves a dead allowlist entry that silently never gets offered to the LLM — exactly the class of drift the maintenance-checklist warns about. This would be a cheap, high-leverage sweep test across all 15 pieces.
3. lint-pieces.mjs (PINFRA-004) has no test. The CI gate script (file collection, exit codes 0/1/2, YAML parse-error path) is entirely untested. Its core rule is mirrored by validatePieceDef (tested), but the script's own CLI behavior — including its parse-error exit(2) and multi-file argument handling — could regress without notice.
4. No transition-graph reachability / dangling-next validation for pieces. Validation only bans terminal values in rules[].next and (in the tool path) checks next ∈ movements∪WAIT_SUBTASKS. But file-backed `loadPiece`/`validatePieceDef` does NOT verify that every `rules[].next` points to an existing movement, nor that `initial_movement` exists, nor that every movement is reachable. A built-in YAML with a typo'd `next:` would load fine and only fail at runtime ("Movement not found"). No test guards this for the shipped pieces.
5. Per-user piece resolution propagation (PINFRA-017) is only partially tested. The known "no-auth 'local' fallback" / "custom-default separation" checklists call out worker, continue-job, scheduler, classifier, and pieces-api as all needing custom-dir threading. Only the worker load path and pieces-api authz have tests; the scheduler-spawn and continue-job (resume after ASK/subtasks) paths resolving the correct per-user custom piece are not directly asserted — a regression here would silently fall back to built-in pieces for custom-piece users.

### Reflection

1. REFL-032 — applyOne per-change write-failure path (`decision.code='failed'`): when a memory write throws mid-loop, the applier records that single change as `failed` and continues. This distinct per-change failure branch (vs runner-level `outcome='failed'`) is not clearly asserted. It is the only place a memory mutation can partially fail under the lock, so it directly affects `partial`/`rejected` outcome correctness.
2. REFL-056 — space authz negative path (`canEditInSpace` → 404): the reflection-api space-store resolution depends on `canEditInSpace`. Tests cover the space-principal happy path, but a space *member without edit rights* hitting history/revert (must 404, no leak) is not asserted as a distinct multiuser case — exactly the kind of cross-principal authz gap that whole-branch reviews catch but per-unit tests miss.
3. REFL-013 / REFL-044 / REFL-045 — runner snapshot-capture + non-fatal writeSnapshot failure: the runner's `writeSnapshot`-throws-but-record-metric-anyway branch, plus the before/after capture edge cases (remove-op uses `merge_target` for the before-file name; piece-YAML pre/post capture when `customPath` doesn't yet exist), are only lightly exercised. A torn snapshot here silently degrades the revert feature.
4. REFL-007 — reflection dispatch / gateway routing: `worker.reflection-dispatch.test.ts` is small (~2 cases). The gateway role-keyed routing (`reflectionRoutingKey`, `roleFallback:'reflection'`) and the "send worker API key or gateway 401s" requirement are documented in code comments but not asserted end-to-end — a deploy-shaped risk (401 storms) that unit tests don't catch.
5. REFL-031 / REFL-037 — boundary branches in applier: the ">3 memory_changes truncated with WARN" hard cap and the `decideOutcome` empty-input fallback-to-`abstained` (distinct from an explicit `abstain_reason`) lack dedicated assertions. Both are silent-correctness branches: a regression would mis-drop changes or mislabel outcomes without any test going red.

### Bridge core

1. APIC-016 GET /api/local/tasks/:taskId/stream (NONE) — The SSE event stream (tool-call/delta mapping, visibility gate, the `toolBuf`/event-name reconstruction logic around line 976) has zero direct tests. This is the live job-progress feed the UI depends on; a regression here is invisible to CI.
2. APIC-007 PUT /api/local/tasks/:taskId/mission (NONE) — Mission update route is completely untested: no authz (owner/admin vs viewer), no validation, no persistence assertion. Only superficial string match in an unrelated auth test.
3. APIC-020 office-preview HTTP route (PARTIAL) — The module is well tested, but the `/files/office-preview` endpoint wiring is not: section-enum 400, `canViewTask` gate, and the SofficeUnavailableError→503 mapping are unexercised end-to-end (only `handleOfficePreviewError` would do the 503).
4. APIC-045 subtask file 403 traversal branch (PARTIAL) — `GET /subtasks/:jobId/files/*` has the explicit `resolved !== base && !startsWith(base+sep)` → 403 escape guard, but no test drives a traversal payload to assert the 403. The containment helper is unit-tested (APIC-046) but the route-level enforcement is not.
5. APIC-011 regenerate-title (PARTIAL) — Only exercised inside the spaces test in an owner-happy context; the owner/admin-only authz denial path and the missing-generator branch are not asserted. (Secondary: APIC-041 trigger happy-path, APIC-008 comments read-gate, and APIC-037/043 GET-by-id view gates are similar PARTIAL thin spots.)

### Bridge auth & spaces

Highest risk first (authorization-weighted):

1. APIS-038 Space tool-policy write under-tested (HIGH). `PUT /:id/tool-policy` gates by `canManageSpace`, but only 9 authz assertions and — per MEMORY (PR #653) — the Bash/sensitive-tool write path (`buildPolicyPatch` only walked categories, missing the `SENSITIVE_TOOLS` separate channel) was a real save-loss bug. The read/validate/render/write round-trip for sensitive tools across both delivery systems is not asserted end-to-end. A regression here silently grants/denies Bash/SSH/browser/MCP. Add an integration test that PUTs a sensitive-tool policy as a non-manager (expect 403) and as owner (expect persisted both-channels).
2. APIS-052 Gateway proxy auth boundary (HIGH). `mountGateway` proxies LLM traffic; classification and config-equivalence are well tested, but the authentication of proxied requests (Bearer/API-key acceptance/rejection, 401 on bad key) is not asserted in gateway-mount.test.ts. An unauthenticated path slipping through the gateway mount would expose backend LLM spend. Deploy-order note in MEMORY (gateway→client) hints this boundary is operationally fragile.
3. APIS-062 Dashboard API has no visible auth guard (MEDIUM-HIGH). `createDashboardApi` router shows no `requireAuth`/`requireAdmin` in its route definitions and the test asserts 0 authz cases. Worker/node (GPU) status may leak infra topology to unauthenticated/non-admin users. Confirm guard is applied at mount site (server.ts) and add an authz assertion.
4. APIS-019 Shared-subtask wildcard traversal (MEDIUM). `GET /api/shared/:token/subtasks/:jobId/files/*` is a public token route with a wildcard path segment; containment relies on `isJobWithinWorkspace` prefix guard. No direct test asserts path-traversal/prefix-collision rejection on this wildcard. This is exactly the class fixed previously (startsWith prefix collision) — add a traversal/escape test.
5. APIS-014 No-auth admin passthrough breadth (MEDIUM). When `authActive=false`, admin guards downgrade to passthrough (admin-api covered; branding adminGuard no-auth path not asserted). If a deployment runs no-auth on a shared network, branding upload/delete is fully open. Document/assert the no-auth posture for every admin route, not just user CRUD.

Lower-priority partials worth noting: APIS-018/APIS-027 (per-endpoint path-escape / non-member-on-public read not asserted), APIS-032 (calendar non-editor write rejection lighter than file gates), APIS-044 (noVNC proxy upgrade authz), APIS-056 (push send delivery).

### Services & DB

1. Job recovery & resume helpers untested (DB-003, DB-032, SVC-019). `recoverOrphanedJobs`, `recoverStuckRunningJobs`, `resumeMcpWaitingJobs`, `resumeToolRequestJob`, and `requeueParentJobIfAllSubtasksDone` have no direct unit tests. These run on worker restart / subtask completion and directly affect whether jobs get stuck or double-run — exactly the class of bug seen in the config-rebuild double-execution incident. Highest-risk gap because failures are silent (stuck or duplicated jobs in prod).
2. Env override matrix only partially covered (CFG-004, CFG-016). Only `OLLAMA_BASE_URL` is asserted (in config.audit-regression). `CONCURRENCY`, `WORKTREE_DIR`, `OLLAMA_MODEL`, `DB_PATH`, and the `AAO_*` metrics vars have no test. Env overrides are a documented public interface and a known footgun (storage mirror precedence already bit the project in #368/#369).
3. `answerSubtaskAsk` / subtask ASK plumbing (SVC-019). The path where a parent worker answers a child subtask's ASK and the parent requeues on all-subtasks-done is untested end-to-end. This is core to the `waiting_subtasks`/`waiting_human` lifecycle and has tangled state.
4. `addAuditLog` not unit-tested (DB-027). Only one consumer (script run) is asserted indirectly. Audit log is a security/compliance surface; no test guards the row shape or that mutating ops actually write audit rows.
5. Comment injection lifecycle (DB-008). `getUninjectedComments` / `markCommentsInjected` / `getLatestResultComment` drive what the agent sees from user comments and what the user sees as the final result — no direct assertion of the inject→mark→re-query cycle, despite being on the hot path for continuation jobs.

### UI

1. UI-055 FilePreview relative-image rewrite (`resolveImageHref`/`resolvedHref`, FilePreview.tsx ~L320) — core, user-visible (broken images in markdown), and currently untested. *Sandbox-testable IF the href-rewrite function is extracted to `lib/` (pure string transform); today it's inline in a component so as-is it needs jsdom.*
2. UI-059 Settings forms (~45 *Form.tsx) — largest untested surface; only `SettingsSidebar` section logic + `toolPolicy` are covered. Per-form validation/round-trip is jsdom/e2e. Mitigation that IS sandbox-testable: extract each form's validation/serialization into pure helpers (the established pattern in this repo) and unit-test those.
3. UI-064 SSH UI (config/console/grants/audit/rotation) — large surface, only TS types tested, no behavior tests. Console session + websocket needs e2e; grant/audit list filtering + form validation are extractable to lib (sandbox-testable).
4. UI-049/UI-051/UI-052 untested pure libs (`fileDnd`, `localDate`, `spaceColor`) — immediately sandbox-testable, no extraction needed. `localDate` (tz math) is the highest regression risk of the three.
5. UI-026 toolPolicy Bash/sensitive-tools write-path — known real bug class (MEMORY: PR #653 fixed a save gap where `buildPolicyPatch` only walked categories and missed `SENSITIVE_TOOLS`/Bash). Test exists but should assert both the category system and the separate sensitiveTools array across read/validate/render/write. Sandbox-testable now (pure).