diff --git a/codev/projects/bugfix-1110-bugfix-artifact-canvas-test-su/status.yaml b/codev/projects/bugfix-1110-bugfix-artifact-canvas-test-su/status.yaml
new file mode 100644
index 000000000..3e7350c55
--- /dev/null
+++ b/codev/projects/bugfix-1110-bugfix-artifact-canvas-test-su/status.yaml
@@ -0,0 +1,16 @@
+id: bugfix-1110
+title: bugfix-artifact-canvas-test-su
+protocol: bugfix
+phase: pr
+plan_phases: []
+current_plan_phase: null
+gates:
+ pr:
+ status: pending
+ requested_at: '2026-06-28T06:00:05.286Z'
+iteration: 1
+build_complete: false
+history: []
+started_at: '2026-06-28T05:52:25.996Z'
+updated_at: '2026-06-28T06:00:05.287Z'
+pr_ready_for_human: true
diff --git a/codev/state/bugfix-1110_thread.md b/codev/state/bugfix-1110_thread.md
new file mode 100644
index 000000000..16ff68d0a
--- /dev/null
+++ b/codev/state/bugfix-1110_thread.md
@@ -0,0 +1,40 @@
+# bugfix-1110 thread
+
+## Investigate
+
+Issue #1110: flaky test `surfaces a synchronous FileAdapter.watch() failure via onError without throwing (D2)`
+in `packages/artifact-canvas/src/components/__tests__/artifact-canvas.test.tsx`.
+
+Verified root cause against source (lines 157-166):
+- Line 163: `await waitFor(() => expect(onError).toHaveBeenCalled());`
+- Line 164: bare synchronous `expect(document.querySelector('p[data-line]')).not.toBeNull();`
+
+The async `read()` render can resolve after `onError` fires; under CI load the DOM query at
+line 164 lands before the paragraph is in the tree. Race → intermittent failure.
+
+Sibling test (line 171) uses the correct pattern: `await waitFor(() => expect(...).not.toBeNull())`.
+
+Fix: wrap line 164's assertion in `waitFor`. One-line semantic-preserving change. Confirmed
+BUGFIX scope (trivial, mechanical, no production code change).
+
+## Fix
+
+Applied the one-line change (line 164 wrapped in `waitFor`). Verified:
+- `pnpm --filter @cluesmith/codev-artifact-canvas test` → 56/56 pass
+- D2 test stress-run 5× in isolation → 5/5 pass
+- porch checks: build ✓ (5.2s), tests ✓ (20.7s)
+Committed `[Bugfix #1110]`, advanced to PR phase.
+
+## PR
+
+PR #1111 opened (https://github.com/cluesmith/codev/pull/1111).
+Running CMAP (gemini/codex/claude) — consult needs `--issue 1110` to disambiguate
+the many projects visible in this worktree's `codev/projects/`.
+
+CMAP verdicts: gemini=APPROVE, codex=APPROVE, claude=APPROVE.
+Claude flagged the identical bare-assertion race in the sibling D2 adapter-error
+test (line 133) — the issue invited this ride-along, so applied the same waitFor
+wrap (commit 846b53dc). 56/56 pass. Posted CMAP results as PR comment.
+
+`pr` gate requested via `porch done`. Notified architect. **Waiting for human
+approval** — will merge only after `porch approve bugfix-1110 pr`.
diff --git a/packages/artifact-canvas/src/components/__tests__/artifact-canvas.test.tsx b/packages/artifact-canvas/src/components/__tests__/artifact-canvas.test.tsx
index 0daf00464..6ebfc453a 100644
--- a/packages/artifact-canvas/src/components/__tests__/artifact-canvas.test.tsx
+++ b/packages/artifact-canvas/src/components/__tests__/artifact-canvas.test.tsx
@@ -130,7 +130,7 @@ describe('ArtifactCanvas (Phase 3)', () => {
const err = vi.spyOn(console, 'error').mockImplementation(() => {});
render();
await waitFor(() => expect(onError).toHaveBeenCalled());
- expect(document.querySelector('p[data-line]')).not.toBeNull(); // still rendered, no throw
+ await waitFor(() => expect(document.querySelector('p[data-line]')).not.toBeNull()); // still rendered, no throw
err.mockRestore();
});
@@ -161,7 +161,7 @@ describe('ArtifactCanvas (Phase 3)', () => {
const err = vi.spyOn(console, 'error').mockImplementation(() => {});
expect(() => render()).not.toThrow();
await waitFor(() => expect(onError).toHaveBeenCalled());
- expect(document.querySelector('p[data-line]')).not.toBeNull(); // read succeeded → content still renders
+ await waitFor(() => expect(document.querySelector('p[data-line]')).not.toBeNull()); // read succeeded → content still renders
err.mockRestore();
});