Files
inkling/docs/superpowers/plans/2026-05-09-v028-cut-a.md
altair823 7d2b8c95ec docs(v028+): F17~F25 dogfood + roadmap + Cut A~G specs + Cut A plan
v0.2.7 release 후 dogfood 9건 누적 (F17~F25) 정리:
- F17 휴지통 의미 분기 / F18 사유 입력 / F19 recall / F20 raw_text 가변
- F21 다기기 sync / F22 이미지 렌더링 (이미 v0.2.8 promoted) / F23 Ollama-less
- F24 멀티모달 vision / F25 사이드바 + 저장소

추가:
- v0.2.8+ roadmap: 7 cut 분할 (A~G), 12주 시간선, dependency graph
- Cut A~G design specs (각 cut 별 design 결정 + schema + UI + 테스트 전략)
- Cut A implementation plan (이미 v0.2.8 머지로 실행 완료, 참고 보존)

PR #26 머지 후 main 에 doc commits rebase 안 되어 manual merge 진행:
- F22 entry 는 origin/main 의 promoted 형태 우선
- 신규 9 파일 (specs/plan/roadmap) 은 origin/main 에 없는 파일
- "다음 항목 자리" 안내 F23 → F26 갱신

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 15:09:02 +09:00

22 KiB

v0.2.8 Cut A Implementation Plan — 이미지 렌더링 + 앱 아이콘

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: F22 (NoteCard 의 회색 placeholder → 실제 <img> + 클릭 시 OS viewer) + chore (앱 아이콘 SVG → ICO/ICNS/PNG 다중 size + electron-builder 통합).

Architecture: Electron renderer 보안 정책 우회를 위해 main process 에 inkling-media:// custom protocol 등록 — <profileDir>/media/<noteId>/<filename> 을 fetch 가능하게 함. NoteCard 가 protocol URL 을 <img src> 로 사용. 클릭 시 IPC inbox:open-mediashell.openPath 로 OS default viewer 열기. 앱 아이콘은 assets/icon.svg (이미 작성) 를 electron-icon-builder 로 한 번 빌드 → build/icon.ico/icns/png 산출물 git 추적 + electron-builder config 매핑.

Tech Stack: Electron 41 protocol API + React 19 + better-sqlite3 + electron-icon-builder + sharp (SVG 변환 fallback)

선행 spec: docs/superpowers/specs/2026-05-09-v028-cut-a-design.md


File Structure

신규 파일

경로 책임
src/main/protocol/inklingMedia.ts inkling-media:// scheme 권한 + handler 등록 (path traversal 검사 + inferMime)
tests/unit/inklingMedia.test.ts protocol handler 단위 테스트 (path traversal 403 / 정상 200 / 404 / mime)
assets/icon.svg (이미 v0.2.7 turn 에서 생성 — Cut A 시작 전 commit 필요 시 재확인)

수정 파일

경로 변경
src/main/index.ts top-level protocol.registerSchemesAsPrivileged + whenReady 안 registerInklingMediaProtocol(paths.profileDir) 호출 + registerInboxApi 가 신규 IPC 채널 등록
src/main/ipc/inboxApi.ts 신규 IPC inbox:open-media 핸들러 (path traversal 검사 + shell.openPath)
src/preload/index.ts inbox:open-media 채널 화이트리스트
src/shared/types.ts InboxApiopenMedia(relPath: string): Promise<{ ok: boolean; reason?: string }> 시그니처 추가
src/renderer/inbox/api.ts inboxApi 객체에 openMedia wrapper
src/renderer/inbox/components/NoteCard.tsx 회색 <div> placeholder → <img src="inkling-media://..."> + onClick
tests/unit/NoteCard.test.tsx 신규 또는 추가 — <img> 렌더 + 클릭 시 IPC 호출
package.json devDep electron-icon-builder + script build:icons + build.win/mac/linux.icon 경로
.gitignore build/icon.ico/icns/png 만 추적 (whitelist)
build/icon.ico / build/icon.icns / build/icon.png 빌드 산출물 commit
docs/superpowers/specs/2026-04-25-dogfood-feedback.md F22 진행 상태 🚀 promoted 마킹
docs/superpowers/v024-backlog.md (해당 없음 — Cut A 의 작업이 backlog # 와 매핑 X)

Phase 개요

Phase 1: F22 inkling-media protocol     (Task 1)
Phase 2: F22 NoteCard <img> + 클릭       (Task 2)
Phase 3: F22 IPC inbox:open-media         (Task 3)
Phase 4: chore 앱 아이콘 빌드 + config    (Task 4)
Phase 5: verification + version bump     (Task 5)

Phase 1: inkling-media protocol

Task 1: protocol 등록 + handler + 단위 테스트

Files:

  • Create: src/main/protocol/inklingMedia.ts

  • Create: tests/unit/inklingMedia.test.ts

  • Modify: src/main/index.ts

  • Step 1: failing test 작성

// tests/unit/inklingMedia.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { join, sep } from 'node:path';

const mockReadFile = vi.fn();
vi.mock('node:fs/promises', () => ({ readFile: mockReadFile }));

const mockHandle = vi.fn();
vi.mock('electron', () => ({
  default: {
    protocol: {
      registerSchemesAsPrivileged: vi.fn(),
      handle: mockHandle
    }
  }
}));

import { registerInklingMediaProtocol, inferMime } from '../../src/main/protocol/inklingMedia';

describe('inferMime', () => {
  it('returns image/png for .png', () => { expect(inferMime('foo.png')).toBe('image/png'); });
  it('returns image/jpeg for .jpg and .jpeg', () => {
    expect(inferMime('foo.jpg')).toBe('image/jpeg');
    expect(inferMime('foo.jpeg')).toBe('image/jpeg');
  });
  it('returns image/gif for .gif', () => { expect(inferMime('foo.gif')).toBe('image/gif'); });
  it('returns image/webp for .webp', () => { expect(inferMime('foo.webp')).toBe('image/webp'); });
  it('returns application/octet-stream for unknown', () => { expect(inferMime('foo.xyz')).toBe('application/octet-stream'); });
});

describe('inkling-media protocol handler', () => {
  beforeEach(() => { vi.clearAllMocks(); });

  function getHandler(profileDir: string): (req: Request) => Promise<Response> {
    registerInklingMediaProtocol(profileDir);
    return mockHandle.mock.calls[0][1];
  }

  it('serves valid file with correct mime', async () => {
    mockReadFile.mockResolvedValueOnce(Buffer.from([1, 2, 3]));
    const handler = getHandler('/profile');
    const res = await handler(new Request('inkling-media://media/note1/img.png'));
    expect(res.status).toBe(200);
    expect(res.headers.get('content-type')).toBe('image/png');
    expect(mockReadFile).toHaveBeenCalledWith(join('/profile', 'media', 'note1', 'img.png'));
  });

  it('returns 403 on path traversal attempt', async () => {
    const handler = getHandler('/profile');
    const res = await handler(new Request('inkling-media://media/../etc/passwd'));
    expect(res.status).toBe(403);
    expect(mockReadFile).not.toHaveBeenCalled();
  });

  it('returns 404 when file missing', async () => {
    mockReadFile.mockRejectedValueOnce(new Error('ENOENT'));
    const handler = getHandler('/profile');
    const res = await handler(new Request('inkling-media://media/note1/missing.png'));
    expect(res.status).toBe(404);
  });
});
  • Step 2: 테스트 실행 → fail
npm run rebuild:node
npx vitest run tests/unit/inklingMedia.test.ts

Expected: Cannot find module '../../src/main/protocol/inklingMedia'.

  • Step 3: protocol handler 작성
// src/main/protocol/inklingMedia.ts
import electron from 'electron';
import { promises as fs } from 'node:fs';
import { join, normalize, sep, extname } from 'node:path';

const { protocol } = electron;

export function registerSchemesAsPrivileged(): void {
  protocol.registerSchemesAsPrivileged([
    { scheme: 'inkling-media', privileges: { secure: true, supportFetchAPI: true, stream: true } }
  ]);
}

export function inferMime(filename: string): string {
  const ext = extname(filename).toLowerCase();
  switch (ext) {
    case '.png': return 'image/png';
    case '.jpg':
    case '.jpeg': return 'image/jpeg';
    case '.gif': return 'image/gif';
    case '.webp': return 'image/webp';
    default: return 'application/octet-stream';
  }
}

export function registerInklingMediaProtocol(profileDir: string): void {
  const mediaRoot = join(profileDir, 'media');
  protocol.handle('inkling-media', async (req) => {
    const url = new URL(req.url);
    // host + pathname 합쳐서 relPath 구성. inkling-media://media/<noteId>/<file> 형식.
    // URL parse 시 host = 'media', pathname = '/<noteId>/<file>'
    const relPath = decodeURIComponent((url.host + url.pathname).replace(/^media\//, 'media/'));
    const target = normalize(join(profileDir, relPath));
    if (!target.startsWith(mediaRoot + sep) && target !== mediaRoot) {
      return new Response(null, { status: 403 });
    }
    try {
      const data = await fs.readFile(target);
      return new Response(new Uint8Array(data), {
        headers: { 'content-type': inferMime(target) }
      });
    } catch {
      return new Response(null, { status: 404 });
    }
  });
}
  • Step 4: 테스트 통과 확인
npx vitest run tests/unit/inklingMedia.test.ts

Expected: 모든 test pass.

  • Step 5: src/main/index.ts 통합

src/main/index.ts 의 import 추가:

import { registerSchemesAsPrivileged, registerInklingMediaProtocol } from './protocol/inklingMedia.js';

top-level (whenReady 이전, app 생성 직후) 에서:

registerSchemesAsPrivileged();

whenReady 안에서 (paths 초기화 후):

registerInklingMediaProtocol(paths.profileDir);
  • Step 6: typecheck + 전체 회귀
npm run typecheck
npx vitest run

Expected: 0 errors + 460 → 466 (+6 inferMime 5 + handler 3).

  • Step 7: commit
git add src/main/protocol/inklingMedia.ts tests/unit/inklingMedia.test.ts src/main/index.ts
git commit -m "feat(v028): inkling-media:// custom protocol + path traversal 검사"

Phase 2: NoteCard <img> + 클릭

Task 2: NoteCard placeholder → <img> 교체

Files:

  • Modify: src/renderer/inbox/components/NoteCard.tsx

  • Create: tests/unit/NoteCard.test.tsx (없으면 신규)

  • Step 1: failing test 작성

// tests/unit/NoteCard.test.tsx
// @vitest-environment jsdom
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, fireEvent, cleanup } from '@testing-library/react';
import type { Note } from '@shared/types';

const mockOpenMedia = vi.fn(async () => ({ ok: true }));
vi.mock('../../src/renderer/inbox/api.js', () => ({
  inboxApi: {
    openMedia: mockOpenMedia,
    // 다른 inboxApi 메서드 stub (NoteCard 가 사용할 수 있음)
    permanentDeleteNote: vi.fn(),
    restoreNote: vi.fn()
  }
}));

import { NoteCard } from '../../src/renderer/inbox/components/NoteCard';

const baseNote: Note = {
  id: 'n1',
  rawText: 'test',
  title: 'T',
  summary: '',
  tags: [],
  aiStatus: 'complete',
  createdAt: '2026-05-09T00:00:00Z',
  updatedAt: '2026-05-09T00:00:00Z',
  deletedAt: null,
  media: [
    { id: 'm1', relPath: 'media/n1/img1.png', mime: 'image/png' },
    { id: 'm2', relPath: 'media/n1/img2.jpg', mime: 'image/jpeg' }
  ]
} as unknown as Note;

describe('NoteCard — image rendering', () => {
  beforeEach(() => { vi.clearAllMocks(); cleanup(); });

  it('renders <img> for each media item', () => {
    render(<NoteCard note={baseNote} isTrash={false} />);
    const imgs = screen.getAllByRole('img');
    expect(imgs).toHaveLength(2);
    expect(imgs[0].getAttribute('src')).toBe('inkling-media://media/n1/img1.png');
    expect(imgs[1].getAttribute('src')).toBe('inkling-media://media/n1/img2.jpg');
  });

  it('clicking <img> calls inboxApi.openMedia', () => {
    render(<NoteCard note={baseNote} isTrash={false} />);
    fireEvent.click(screen.getAllByRole('img')[0]);
    expect(mockOpenMedia).toHaveBeenCalledWith('media/n1/img1.png');
  });
});
  • Step 2: 테스트 실행 → fail
npx vitest run tests/unit/NoteCard.test.tsx

Expected: getAllByRole('img') 미존재 (현재 회색 div 만).

  • Step 3: NoteCard 갱신

src/renderer/inbox/components/NoteCard.tsx:334-340 의 회색 placeholder div 부분을 다음으로 교체:

{local.media.length > 0 && (
  <div style={{ marginTop: 10, display: 'flex', flexWrap: 'wrap', gap: 6 }}>
    {local.media.map((m) => (
      <img
        key={m.id}
        src={`inkling-media://${m.relPath}`}
        alt=""
        title={m.relPath}
        onClick={() => { void inboxApi.openMedia(m.relPath); }}
        style={{
          width: 48,
          height: 48,
          objectFit: 'cover',
          borderRadius: 4,
          cursor: 'pointer',
          border: '1px solid #e0e0e0'
        }}
      />
    ))}
  </div>
)}

inboxApi import 가 이미 있는지 확인 — 있으면 그대로. 없으면 추가:

import { inboxApi } from '../api.js';
  • Step 4: 테스트 통과 + typecheck
npx vitest run tests/unit/NoteCard.test.tsx
npm run typecheck

Expected: 2 test pass (openMedia 가 types.ts 에 아직 없으니 typecheck error 가능 — Task 3 에서 해결). 일단 임시로 NoteCard.test.tsx 의 mock 만 동작.

만약 typecheck error 발생: inboxApi.openMedia 가 InboxApi 인터페이스에 없음 → 일시 (inboxApi as any).openMedia(m.relPath) 로 cast. Task 3 에서 정식 시그니처 추가 후 cast 제거.

  • Step 5: commit (with TODO note for Task 3)
git add src/renderer/inbox/components/NoteCard.tsx tests/unit/NoteCard.test.tsx
git commit -m "feat(v028): NoteCard 이미지 <img> 렌더링 + onClick (openMedia 시그니처는 Task 3)"

Phase 3: IPC inbox:open-media

Task 3: main IPC 핸들러 + api.ts wrapper + types

Files:

  • Modify: src/main/ipc/inboxApi.ts

  • Modify: src/preload/index.ts

  • Modify: src/shared/types.ts

  • Modify: src/renderer/inbox/api.ts

  • Modify: src/renderer/inbox/components/NoteCard.tsx (Task 2 cast 제거)

  • Step 1: failing test 작성 (IPC handler 단위)

// tests/unit/inboxApi-openMedia.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { join, sep } from 'node:path';

const handlers: Record<string, Function> = {};
const mockOpenPath = vi.fn(async () => '');

vi.mock('electron', () => ({
  default: {
    ipcMain: { handle: (ch: string, fn: Function) => { handlers[ch] = fn; } },
    shell: { openPath: mockOpenPath }
  }
}));

import { registerInboxApi } from '../../src/main/ipc/inboxApi';

describe('inbox:open-media IPC', () => {
  beforeEach(() => { vi.clearAllMocks(); for (const k of Object.keys(handlers)) delete handlers[k]; });

  it('opens valid relPath with shell.openPath', async () => {
    // 기존 registerInboxApi 가 deps 받으므로 minimal stub 만 — paths 만 사용
    registerInboxApi({ paths: { profileDir: '/profile' } } as any);
    const r = await handlers['inbox:open-media'](null, 'media/note1/img.png');
    expect(r).toEqual({ ok: true });
    expect(mockOpenPath).toHaveBeenCalledWith(join('/profile', 'media/note1/img.png'));
  });

  it('rejects path traversal', async () => {
    registerInboxApi({ paths: { profileDir: '/profile' } } as any);
    const r = await handlers['inbox:open-media'](null, '../etc/passwd');
    expect(r.ok).toBe(false);
    expect(r.reason).toBe('invalid path');
    expect(mockOpenPath).not.toHaveBeenCalled();
  });
});
  • Step 2: 테스트 실행 → fail
npx vitest run tests/unit/inboxApi-openMedia.test.ts

Expected: handler 미등록.

  • Step 3: src/main/ipc/inboxApi.ts 핸들러 추가

기존 registerInboxApi(deps) 함수 안에 다음 추가 (다른 IPC 핸들러와 같은 패턴):

import { join, normalize, sep } from 'node:path';
import electron from 'electron';
const { ipcMain, shell } = electron;

// registerInboxApi 안:
ipcMain.handle('inbox:open-media', async (_e, relPath: string) => {
  const mediaRoot = join(deps.paths.profileDir, 'media');
  const target = normalize(join(deps.paths.profileDir, relPath));
  if (!target.startsWith(mediaRoot + sep) && target !== mediaRoot) {
    return { ok: false, reason: 'invalid path' };
  }
  await shell.openPath(target);
  return { ok: true };
});

(기존 deps 타입에 paths.profileDir 가 있는지 확인. 없으면 SettingsIpcDeps 와 비슷한 형태로 추가.)

  • Step 4: src/shared/types.ts InboxApi 갱신
// 기존 InboxApi 인터페이스 안:
openMedia(relPath: string): Promise<{ ok: true } | { ok: false; reason: string }>;
  • Step 5: preload + api.ts wrapper

src/preload/index.ts 의 invoke whitelist 또는 API expose 객체 안에 'inbox:open-media' 추가 (기존 'inbox:*' 채널 패턴 따름).

src/renderer/inbox/api.ts 의 inboxApi 객체에 추가:

async openMedia(relPath: string) {
  return await window.inkling.invoke('inbox:open-media', relPath);
}

(또는 기존 wildcard re-export 사용 시 자동 노출 — feb7c62 commit 의 onNavigate 처럼.)

  • Step 6: NoteCard.tsx cast 제거

Task 2 에서 임시 (inboxApi as any).openMedia cast 했다면 → inboxApi.openMedia 정상 사용으로 변경. typecheck 통과 확인.

  • Step 7: 테스트 + typecheck + 회귀
npx vitest run tests/unit/inboxApi-openMedia.test.ts
npm run typecheck
npx vitest run

Expected: IPC 2 test pass + 466 → 468 (+2) + 0 typecheck.

  • Step 8: commit
git add -A src/main/ipc/ src/preload/ src/shared/ src/renderer/ tests/unit/inboxApi-openMedia.test.ts
git commit -m "feat(v028): IPC inbox:open-media + path traversal + NoteCard cast 정리"

Phase 4: 앱 아이콘 빌드 + config

Task 4: electron-icon-builder + 산출물 + builder config

Files:

  • Modify: package.json

  • Modify: .gitignore

  • Create: build/icon.ico, build/icon.icns, build/icon.png (빌드 산출물)

  • (조건부) Create: scripts/svg-to-png.mjs (SVG → PNG fallback if needed)

  • Step 1: devDep 설치

npm install --save-dev electron-icon-builder

확인: package.json 의 devDependencies 에 "electron-icon-builder": "^2.x.x" 추가됨.

  • Step 2: package.json scripts + builder config

package.json 의 scripts 블록에 추가:

"build:icons": "electron-icon-builder --input=assets/icon.svg --output=build --flatten"

(만약 SVG 직접 input 안 되면 — Step 3 에서 sharp fallback 으로 분기.)

build 블록 갱신 (기존 win/mac/linux 에 icon 키 추가):

"win": { "icon": "build/icon.ico", ... 기존 그대로 ... },
"mac": { "icon": "build/icon.icns", ... 기존 그대로 ... },
"linux": { "icon": "build/icon.png", ... 기존 그대로 ... }
  • Step 3: 빌드 실행
npm run build:icons

Expected:

  • build/icon.ico (Win)
  • build/icon.icns (macOS)
  • build/icon.png (1024x1024, Linux)

만약 SVG 직접 안 되면 (electron-icon-builder 가 PNG 만 input 받음):

  1. scripts/svg-to-png.mjs 작성:
import sharp from 'sharp';
import { readFileSync, writeFileSync } from 'node:fs';

const [,, input, output, size = '1024'] = process.argv;
const svg = readFileSync(input);
const png = await sharp(svg).resize(Number(size), Number(size)).png().toBuffer();
writeFileSync(output, png);
console.log(`OK: ${output} (${size}x${size})`);
  1. package.json scripts 갱신:
"build:icons:png": "node scripts/svg-to-png.mjs assets/icon.svg build/icon-source.png 1024",
"build:icons": "npm run build:icons:png && electron-icon-builder --input=build/icon-source.png --output=build --flatten"
  1. npm install --save-dev sharp (이미 있으면 skip).

  2. 다시 npm run build:icons 실행.

  • Step 4: .gitignore 갱신 (build/ 안 산출물 whitelist)

.gitignorebuild/ 또는 dist/ 항목 확인. 만약 build/ 가 ignore 되어 있다면:

build/
!build/icon.ico
!build/icon.icns
!build/icon.png

(만약 build/ 가 ignore 안 되어 있다면 — 모두 추적 가능 — Step 4 skip.)

  • Step 5: 산출물 확인 + commit
ls -la build/icon.ico build/icon.icns build/icon.png

Expected: 3 파일 모두 size 양수 (수십 KB 이상).

git add package.json package-lock.json .gitignore scripts/svg-to-png.mjs build/icon.ico build/icon.icns build/icon.png
git commit -m "chore(v028): 앱 아이콘 (assets/icon.svg → ICO/ICNS/PNG) + electron-builder config"

(scripts/svg-to-png.mjs 는 sharp fallback 사용 시만 추가.)

  • Step 6: typecheck + 전체 회귀
npm run typecheck
npx vitest run

Expected: 0 errors + 468 pass (이전 + Task 1-3 변경 포함). 아이콘 변경은 테스트 영향 X.


Phase 5: verification + version bump

Task 5: 회귀 + dogfood-feedback 마킹 + version bump

Files:

  • Modify: docs/superpowers/specs/2026-04-25-dogfood-feedback.md (F22 promoted 마킹)

  • Modify: package.json (version 0.2.7 → 0.2.8)

  • Step 1: 단위 + e2e + typecheck 일괄

npm run rebuild:node
npm test
npm run typecheck
npm run rebuild:electron
npm run test:e2e

Expected: 모두 pass. 단위 460 → 약 468 (+8: inferMime 5 + protocol handler 3 + NoteCard img 2 + IPC 2 = 12, 일부 mock 충돌 가능 — 실제 카운트 ±). e2e 1/1.

  • Step 2: 수동 launch 검증 (Win + macOS 가능 시)
npm run start

체크리스트:

  • inbox 의 capture-with-image 노트의 thumbnail = 실제 이미지 (회색 사각형 X)

  • thumbnail 클릭 → OS default viewer (예: Win Photos / macOS Preview) 열림

  • 새 아이콘이 트레이 / Windows taskbar / dock 정확 표시

  • 다중 이미지 노트의 grid layout (flex-wrap) 자연스러움

  • Step 3: dogfood-feedback.md F22 promoted 마킹

docs/superpowers/specs/2026-04-25-dogfood-feedback.md 의 F22 entry 헤더 갱신:

## F22. NoteCard 이미지가 회색 placeholder 만 표시 (🚀 promoted → docs/superpowers/specs/2026-05-09-v028-cut-a-design.md)

진행 상태 line 도 🚀 promoted 로 갱신.

  • Step 4: package.json version bump
"version": "0.2.8"
  • Step 5: commit
git add docs/superpowers/specs/2026-04-25-dogfood-feedback.md package.json
git commit -m "chore(release): v0.2.8 — Cut A (이미지 렌더링 + 앱 아이콘)"

Self-Review

Spec coverage:

Spec 섹션 task
§3-1 protocol 등록 Task 1
§3-2 NoteCard <img> Task 2
§3-3 IPC inbox:open-media Task 3
§3-4 보안 (path traversal) Task 1 + Task 3
§4-1 devDep + scripts Task 4
§4-2 builder config Task 4
§4-3 산출물 git 추적 Task 4
§4-4 SVG → PNG fallback Task 4 (조건부)
§5 테스트 각 task 안 단위 + Task 5 회귀
§6 Risk Task 4 fallback 분기

모든 spec 요구가 task 매핑됨.

Placeholder scan: "TBD" / "TODO" / "implement later" 없음. 각 step 의 코드/명령 실행 가능 형태.

Type consistency:

  • InboxApi.openMedia(relPath: string): Promise<{ ok: true } | { ok: false; reason: string }> — Task 3 정의, Task 2 cast → Task 3 cast 제거 일관.
  • inferMime(filename: string): string — Task 1 정의, Task 1 안 사용.
  • registerInklingMediaProtocol(profileDir: string): void / registerSchemesAsPrivileged(): void — Task 1 export, src/main/index.ts import 일관.

이슈 없음.


Execution Handoff

Plan 작성 완료, docs/superpowers/plans/2026-05-09-v028-cut-a.md 저장.

두 가지 실행 옵션:

1. Subagent-Driven (recommended) — fresh subagent per task, two-stage review (spec compliance + code quality), 빠른 iteration

2. Inline Execution — 본 세션에서 task 일괄 실행 + checkpoint 마다 review

어느 쪽?