---
phase: 52-runtime-alignment-failure-closure
plan: 001
type: execute
wave: 1
depends_on: [51]
files_modified:
  - manifest.json
  - paperforge/plugin/manifest.json
  - paperforge/plugin/versions.json
  - paperforge/plugin/main.js
  - paperforge/plugin/styles.css
  - pyproject.toml
autonomous: true
requirements: [RUNTIME-04, RUNTIME-05, CLEAN-02, CLEAN-03, CLEAN-04]
user_setup: []

must_haves:
  truths:
    - "User can see 'Plugin vX → Python package vY' in Settings → Runtime Health with a match/mismatch badge"
    - "User can click 'Sync Runtime' in Settings to align Python package version with plugin version"
    - "Dashboard shows a persistent (non-blocking) warning banner when plugin and Python package versions drift"
    - "Install/setup failures display one of 10 classified error categories instead of raw stderr"
    - "User can click 'Copy diagnostic' on setup failure to copy error category + details + env info to clipboard"
    - "Both root and plugin manifest.json have minAppVersion >= 1.9.0"
    - "bump.py correctly updates both root and plugin manifests from the canonical version source"
    - "PyYAML is listed in pyproject.toml dependencies and doctor's yaml check aligns with declared deps"
  artifacts:
    - path: "manifest.json"
      provides: "Root manifest with minAppVersion >= 1.9.0"
      contains: "minAppVersion"
    - path: "paperforge/plugin/manifest.json"
      provides: "Plugin manifest with minAppVersion >= 1.9.0"
      contains: "minAppVersion"
    - path: "paperforge/plugin/versions.json"
      provides: "Version compatibility mapping for new minAppVersion"
      contains: "1.4.17rc2"
    - path: "paperforge/plugin/main.js"
      provides: "Runtime Health section, drift banner, extended error classification, Copy diagnostic"
      exports: ["PaperForgeSettingTab.display", "PaperForgeStatusView._renderGlobalMode", "_formatSetupError"]
    - path: "pyproject.toml"
      provides: "Declared PyYAML dependency"
      contains: "pyyaml"
  key_links:
    - from: "PaperForgeSettingTab.display() after Python path row"
      to: "this.plugin.settings.setup_complete"
      via: "Async version fetch like _fetchVersion pattern"
      pattern: "execFile.*python.*paperforge.__version__"
    - from: "Sync Runtime button in settings"
      to: "pip install --upgrade paperforge@<pluginVersion>"
      via: "spawn(pythonExe, ['-m', 'pip', 'install', '--upgrade', url])"
      pattern: "pip.*install.*upgrade"
    - from: "Dashboard _renderGlobalMode()"
      to: "this._paperforgeVersion vs this.app.plugins.plugins['paperforge'].manifest.version"
      via: "Drift check on each dashboard render"
      pattern: "_paperforgeVersion"
    - from: "_formatSetupError()"
      to: "Setup wizard error display area"
      via: "Copy diagnostic button collecting category + raw stderr + env info"
      pattern: "_formatSetupError"

---

<objective>
**Objective:** Close all Phase 52 requirements — Runtime Health UI, dashboard drift warning, extended failure classification, manifest source alignment, minAppVersion bump, and PyYAML dependency fix.

**Purpose:** Users can see at a glance whether their Python runtime is aligned with the plugin, get actionable error categories when installs fail, and trust that version metadata is consistent across the package tree.

**Output:** Modified manifest files, updated pyproject.toml, and extended main.js with Runtime Health settings section, dashboard drift banner, extended error patterns, and Copy diagnostic button.
</objective>

<execution_context>
@C:/Users/Lin/.opencode/get-shit-done/workflows/execute-plan.md
@C:/Users/Lin/.opencode/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/REQUIREMENTS.md
@.planning/phases/52-runtime-alignment-failure-closure/52-CONTEXT.md

<interfaces>
Below are the key types/interfaces from main.js that Task 2 and Task 3 extend. The executor must match these patterns.

=== i18n system (main.js:16-160) ===
The `LANG` object has `en` and `zh` sub-objects with `Object.assign()` calls appending keys to each.
Usage: `t('key_name')` — falls back to en, then the key itself.
Add new keys to the `Object.assign(LANG.en, {...})` block (line 24) and `Object.assign(LANG.zh, {...})` block (line 91).

=== Settings tab class (main.js:1573-1747) ===
`PaperForgeSettingTab` extends `PluginSettingTab`.
`display()` builds the settings page top-to-bottom: header → setup status → Python interpreter row (line 1632) → custom path + validate (line 1638) → Preparation guide (line 1674) → Install button (line 1694) → Usage guide (line 1712) → Config summary (line 1726).
Runtime Health section goes between Python interpreter section (ending around line 1671) and Preparation guide (line 1674).

=== Dashboard view class (main.js:276 onward) ===
`PaperForgeStatusView` extends `ItemView`.
`_renderGlobalMode()` (line 883-899) creates: metrics area, OCR section, then triggers fetch.
`_fetchVersion()` (line 374-385) fetches paperforge.__version__ and stores in `this._paperforgeVersion`.
`_renderStats()` (line 541-568) renders metric cards.

=== Setup wizard error display ===
`_formatSetupError()` (line 2423-2434) returns classified error string.
Called at line 2388 inside the install catch block: `this._log(t('install_failed') + this._formatSetupError(err.message))`.
The `_log()` method (line 2394) appends text to `this._installLog` div.

=== Error pattern format ===
Current patterns (line 2424-2430):
```javascript
const patterns = [
    { match: /command not found|No such file|not recognized/i, msg: 'Python not found' },
    { match: /paperforge.*not found|cannot import|ModuleNotFoundError|No module named/i, msg: 'PaperForge not installed' },
    { match: /permission denied|EACCES/i, msg: 'Permission denied' },
    { match: /ENOENT/i, msg: 'Path not found' },
    { match: /timeout|timed out/i, msg: 'Timeout' },
];
```

=== manifest files (existing minAppVersion: "1.0.0") ===
- root: manifest.json
- plugin: paperforge/plugin/manifest.json
- versions: paperforge/plugin/versions.json

=== bump.py manifest source chain ===
`scripts/bump.py` reads version from `paperforge/__init__.py` via regex, then calls `update_file()` for each file in `FILES_TO_UPDATE` dict (line 22-27). Both root manifest and plugin manifest are in this dict. Update is pure string replacement.
</interfaces>
</context>

<tasks>

<!-- ====================================================================== -->
<!-- TASK 1: CLEAN items — manifest alignment, minAppVersion, PyYAML         -->
<!-- ====================================================================== -->
<task type="auto">
  <name>Task 1: Align manifest source (CLEAN-02), bump minAppVersion (CLEAN-03), add PyYAML dep (CLEAN-04)</name>
  <files>
    scripts/bump.py
    manifest.json
    paperforge/plugin/manifest.json
    paperforge/plugin/versions.json
    pyproject.toml
  </files>

  <action>
    === CLEAN-02: Verify manifest source consistency ===
    bump.py (lines 22-27) already declares both `root_manifest` and `plugin_manifest` in its `FILES_TO_UPDATE` dict, and `main()` (line 91-92) iterates over all entries calling `update_file()`. The canonical source is `paperforge/__init__.py` (`__version__`). Both manifests receive the same version string via the same update mechanism.
    
    1a. Verify this by tracing the flow: `read_current_version()` → `update_file()` for each path. No changes needed — the mechanism is correct.
    1b. Add a comment at line 22 of bump.py: `# Both root_manifest and plugin_manifest are updated from the canonical __init__.py version — never edit version directly in manifest.json`.

    === CLEAN-03: Update minAppVersion in both manifests ===
    Research finding: Obsidian Bases was introduced in v1.9.0 (May 21, 2025, early access). Since PaperForge relies on `.base` files for its Base views as a core workflow feature, the minAppVersion must be at least `1.9.0`.

    2a. Update `manifest.json` (root): change `"minAppVersion": "1.0.0"` → `"minAppVersion": "1.9.0"`
    2b. Update `paperforge/plugin/manifest.json`: change `"minAppVersion": "1.0.0"` → `"minAppVersion": "1.9.0"`
    2c. Update `paperforge/plugin/versions.json`: Change the existing `"1.4.17rc2": "1.0.0"` to `"1.4.17rc2": "1.9.0"` and `"1.4.3": "1.0.0"` to `"1.4.3": "1.9.0"`. This tells Obsidian that v1.4.17rc2 (and all later versions) require at least Obsidian 1.9.0.

    === CLEAN-04: Add PyYAML to pyproject.toml ===
    `paperforge/worker/status.py` line 221 checks for `"yaml"` in `required_modules`. This is imported via `import yaml`.
    3a. Add `"pyyaml>=6.0"` to the `dependencies` list in pyproject.toml (after the existing entries, before the closing `]`). PyYAML 6.0 is the first version compatible with Python 3.10+ (our minimum Python version).
    3b. The doctor check at status.py:221 already aligns with this — it checks for `"yaml"`. No change needed in status.py.
    3c. Verify there are no other `import yaml` or `from yaml` usages that don't go through the standard import path:
       ```bash
       grep -rn "import yaml\|from yaml" paperforge/
       ```
       If any file imports yaml outside of try/except, add a try/except wrapper with a clear error message.
  </action>

  <verify>
    <automated>
      # Verify minAppVersion
      python -c "import json; d=json.load(open('manifest.json')); assert d['minAppVersion']>='1.9.0', f'got {d[\"minAppVersion\"]}'"
      python -c "import json; d=json.load(open('paperforge/plugin/manifest.json')); assert d['minAppVersion']>='1.9.0', f'got {d[\"minAppVersion\"]}'"
      # Verify PyYAML in deps
      python -c "import tomllib; d=tomllib.load(open('pyproject.toml','rb')); deps=d['project']['dependencies']; assert any('pyyaml' in d.lower() for d in deps), 'PyYAML not found in deps'"
    </automated>
  </verify>

  <done>
    - Root manifest.json minAppVersion is "1.9.0"
    - Plugin manifest.json minAppVersion is "1.9.0"
    - versions.json maps current version to "1.9.0"
    - bump.py has clarifying comment about canonical source
    - PyYAML >= 6.0 listed in pyproject.toml dependencies
    - No bare yaml imports outside of try/except wrappers
  </done>
</task>


<!-- ====================================================================== -->
<!-- TASK 2: Runtime Health section in settings + Dashboard drift banner     -->
<!-- ====================================================================== -->
<task type="auto">
  <name>Task 2: Add Runtime Health section to settings and dashboard drift warning (RUNTIME-04)</name>
  <files>
    paperforge/plugin/main.js
    paperforge/plugin/styles.css
  </files>

  <action>
    === PART A: Add i18n translation keys ===
    Add these keys to Object.assign(LANG.en, {...}) (around line 24-89):
    - `runtime_health`: "Runtime Health"
    - `runtime_health_desc`: "Check whether the installed paperforge Python package matches the plugin version"
    - `runtime_health_plugin_ver`: "Plugin v{0}"
    - `runtime_health_package_ver`: "Python package v{0}"
    - `runtime_health_match`: "Match"
    - `runtime_health_mismatch`: "Mismatch"
    - `runtime_health_sync`: "Sync Runtime"
    - `runtime_health_syncing`: "Syncing..."
    - `runtime_health_sync_done`: "Runtime synced to v{0}"
    - `runtime_health_sync_fail`: "Sync failed: {0}"
    - `dashboard_drift_warning`: "PaperForge CLI (v{0}) differs from plugin (v{1}). Open Settings → Runtime Health to sync."

    Add matching keys to Object.assign(LANG.zh, {...}) (around line 91-140):
    - `runtime_health`: "运行时状态"
    - `runtime_health_desc`: "检查已安装的 paperforge Python 包是否与插件版本一致"
    - `runtime_health_plugin_ver`: "插件 v{0}"
    - `runtime_health_package_ver`: "Python 包 v{0}"
    - `runtime_health_match`: "一致"
    - `runtime_health_mismatch`: "不一致"
    - `runtime_health_sync`: "同步运行时"
    - `runtime_health_syncing`: "正在同步..."
    - `runtime_health_sync_done`: "运行时已同步至 v{0}"
    - `runtime_health_sync_fail`: "同步失败: {0}"
    - `dashboard_drift_warning`: "PaperForge CLI (v{0}) 与插件 (v{1}) 版本不一致，请打开设置 → 运行时状态进行同步。"

    === PART B: Add Runtime Health section to PaperForgeSettingTab.display() ===
    Insert the Runtime Health section in `display()` method (around line 1671, between the Python custom path validation button and the Preparation guide header at line 1673).

    Create the section using the pattern established by existing settings (Setting class, createEl):
    
    ```javascript
    /* ── Runtime Health Section ── */
    containerEl.createEl('h3', { text: t('runtime_health') });
    containerEl.createEl('p', { text: t('runtime_health_desc'), cls: 'paperforge-settings-desc' });
    
    // Version comparison row
    const versionRow = new Setting(containerEl)
        .setName('PaperForge')
        .setDesc('Checking...');
    
    // Status badge
    const badgeEl = versionRow.descEl.createEl('span', { cls: 'paperforge-runtime-badge' });
    
    // Sync button
    versionRow.addButton(btn => {
        btn.setButtonText(t('runtime_health_sync'))
            .setDisabled(true)
            .onClick(() => this._syncRuntime(btn));
    });
    ```
    
    Add async version fetch. After creating the section, trigger the comparison:
    
    ```javascript
    // Fetch version asynchronously (same pattern as _fetchVersion at line 374)
    const vp = this.app.vault.adapter.basePath;
    const { path: pythonExe, extraArgs = [] } = resolvePythonExecutable(vp, this.plugin.settings);
    const pluginVer = this.plugin.manifest.version || '?';
    
    execFile(pythonExe, [...extraArgs, '-c', 'import paperforge; print(paperforge.__version__)'], { cwd: vp, timeout: 10000 }, (err, stdout) => {
        const pyVer = (!err && stdout) ? stdout.trim() : null;
        // Update desc: "Plugin vX → Python vY"
        const descText = pyVer
            ? `${t('runtime_health_plugin_ver').replace('{0}', pluginVer)} → ${t('runtime_health_package_ver').replace('{0}', pyVer)}`
            : `Plugin v${pluginVer} → Python package not installed`;
        versionRow.setDesc(descText);
        // Update badge: match (green) / mismatch (red) / missing
        if (pyVer === pluginVer) {
            badgeEl.setText(t('runtime_health_match'));
            badgeEl.className = 'paperforge-runtime-badge match';
            btn.setDisabled(true);
        } else if (pyVer) {
            badgeEl.setText(t('runtime_health_mismatch'));
            badgeEl.className = 'paperforge-runtime-badge mismatch';
            btn.setDisabled(false);
        } else {
            badgeEl.setText('Not installed');
            badgeEl.className = 'paperforge-runtime-badge missing';
            btn.setDisabled(false);
        }
    });
    ```

    === PART C: Add _syncRuntime method to PaperForgeSettingTab ===
    Add a `_syncRuntime(btn)` method to `PaperForgeSettingTab` (after `_validatePythonOverride` at ~line 1772):
    
    ```javascript
    _syncRuntime(btn) {
        const vp = this.app.vault.adapter.basePath;
        const { path: pythonExe, extraArgs = [] } = resolvePythonExecutable(vp, this.plugin.settings);
        const ver = this.plugin.manifest.version || '1.4.17rc2';
        const url = `git+https://github.com/LLLin000/PaperForge.git@${ver}`;
        const spawn = require('node:child_process').spawn;
        
        btn.setDisabled(true);
        btn.setButtonText(t('runtime_health_syncing'));
        
        const child = spawn(pythonExe, [...extraArgs, '-m', 'pip', 'install', '--upgrade', url], { cwd: vp, timeout: 120000 });
        child.on('close', (code) => {
            if (code === 0) {
                new Notice(t('runtime_health_sync_done').replace('{0}', ver), 5000);
                // Re-trigger display to refresh status
                this.display();
            } else {
                btn.setDisabled(false);
                btn.setButtonText(t('runtime_health_sync'));
                new Notice(t('runtime_health_sync_fail').replace('{0}', 'pip exit code ' + code), 8000);
            }
        });
        child.on('error', (e) => {
            btn.setDisabled(false);
            btn.setButtonText(t('runtime_health_sync'));
            new Notice(t('runtime_health_sync_fail').replace('{0}', e.message), 8000);
        });
    }
    ```
    
    Reuse the same `pip install --upgrade git+...@${ver}` pattern from `_autoUpdate()` (line 2573-2600). The key difference: this is user-initiated and provides visible feedback in the browser.

    === PART D: Add drift warning banner to Dashboard ===
    In `PaperForgeStatusView._renderGlobalMode()` (line 883), add a drift warning banner at the top of the dashboard content, before the metrics section. Create it only when version drift is detected.

    After line 885 (`this._metricsEl = this._contentEl.createEl('div', { cls: 'paperforge-metrics' });`), add:
    
    ```javascript
    /* ── Runtime Drift Warning Banner ── */
    this._driftBannerEl = this._contentEl.createEl('div', { cls: 'paperforge-drift-banner' });
    this._driftBannerEl.style.display = 'none'; // hidden by default, shown when drift detected
    ```
    
    In `_fetchVersion()` (line 374), after the version comparison at line 382, add drift check logic:
    
    ```javascript
    // After setting this._versionBadge text, also check drift for banner
    const pluginVer = this.app.plugins.plugins['paperforge']?.manifest?.version;
    if (this._driftBannerEl && pluginVer && this._paperforgeVersion && this._paperforgeVersion !== 'v' + pluginVer.replace(/^v/, '')) {
        this._driftBannerEl.style.display = 'block';
        this._driftBannerEl.setText(t('dashboard_drift_warning')
            .replace('{0}', this._paperforgeVersion)
            .replace('{1}', 'v' + pluginVer.replace(/^v/, '')));
    } else if (this._driftBannerEl) {
        this._driftBannerEl.style.display = 'none';
    }
    ```
    
    Note: `_fetchVersion` calls `execFile` asynchronously, so the banner check happens after the callback. The `_driftBannerEl` is created during `_renderGlobalMode()`, so it must exist by the time `_fetchVersion` callback fires. Since `_fetchVersion` is called from `onOpen()` (line 302) before `_detectAndSwitch()` (line 304), and the callback may fire after `_renderGlobalMode()`, store the reference and guard with null check. Also re-check in `_fetchStats` callback when rendering stats.

    === PART E: Add CSS for Runtime Health section and Dashboard drift banner ===
    Append to `styles.css`:
    
    ```css
    /* ── Runtime Health ── */
    .paperforge-runtime-badge {
        display: inline-block;
        padding: 2px 8px;
        border-radius: 4px;
        font-size: 0.85em;
        font-weight: 600;
        margin-left: 8px;
    }
    .paperforge-runtime-badge.match {
        background: var(--color-green, #4caf50);
        color: #fff;
    }
    .paperforge-runtime-badge.mismatch {
        background: var(--color-red, #f44336);
        color: #fff;
    }
    .paperforge-runtime-badge.missing {
        background: var(--color-orange, #ff9800);
        color: #fff;
    }
    
    /* ── Dashboard Drift Banner ── */
    .paperforge-drift-banner {
        background: var(--color-yellow, #ffc107);
        color: var(--background-primary, #000);
        padding: 8px 14px;
        border-radius: 6px;
        font-size: 0.9em;
        margin-bottom: 16px;
        cursor: pointer;
    }
    .paperforge-drift-banner:hover {
        opacity: 0.9;
    }
    ```
    
    Use existing CSS variable names from Obsidian theme. The banner should be yellow/amber to signal warning without alarm.
  </action>

  <verify>
    <automated>
      # Verify i18n keys are added
      node -e "const fs=require('fs'); const c=fs.readFileSync('paperforge/plugin/main.js','utf-8'); const keys=['runtime_health','runtime_health_match','runtime_health_mismatch','runtime_health_sync','runtime_health_syncing','dashboard_drift_warning']; keys.forEach(k=>{if(!c.includes(`'${k}'`)&&!c.includes(`\"${k}\"`)){console.error('MISSING KEY:',k);process.exit(1)}}); console.log('All i18n keys present')"
      # Verify _syncRuntime method exists
      node -e "const fs=require('fs'); const c=fs.readFileSync('paperforge/plugin/main.js','utf-8'); if(!c.includes('_syncRuntime(')){console.error('_syncRuntime not found');process.exit(1)}; console.log('_syncRuntime found')"
      # Verify drift banner class in CSS
      node -e "const fs=require('fs'); const c=fs.readFileSync('paperforge/plugin/styles.css','utf-8'); if(!c.includes('paperforge-drift-banner')){console.error('drift-banner CSS class not found');process.exit(1)}; if(!c.includes('paperforge-runtime-badge')){console.error('runtime-badge CSS class not found');process.exit(1)}; console.log('CSS classes found')"
    </automated>
  </verify>

  <done>
    - Runtime Health section renders after Python path in settings with version comparison
    - Match/mismatch badge shows green/red with correct label
    - "Sync Runtime" button triggers pip install with visible output feedback
    - Dashboard shows yellow warning banner when versions drift
    - Banner does not block Sync/OCR operations
    - All i18n keys in both en and zh
    - CSS classes defined for badge states and banner
  </done>
</task>


<!-- ====================================================================== -->
<!-- TASK 3: Extended failure classification + Copy diagnostic button        -->
<!-- ====================================================================== -->
<task type="auto">
  <name>Task 3: Extend failure classification (RUNTIME-05) and add Copy diagnostic button</name>
  <files>
    paperforge/plugin/main.js
    paperforge/plugin/styles.css
  </files>

  <action>
    === PART A: Extend _formatSetupError with 5 new categories ===
    In `_formatSetupError()` (line 2423-2434), extend the `patterns` array. The new order (logical priority — most specific first) should be:

    ```javascript
    _formatSetupError(raw) {
        const patterns = [
            // New: pip not found
            { match: /pip.*not found|No module named.*pip|command not found.*pip/i, msg: 'pip not found' },
            // Existing: Python not found (keep broad)
            { match: /command not found|No such file|not recognized/i, msg: 'Python not found' },
            // New: Network error (DNS resolution, connection refused, etc.)
            { match: /resolve host|getaddrinfo.*nodename|connect ETIMEDOUT|connect ECONNREFUSED|fetch failed|Network error|ENOTFOUND|ECONNREFUSED|ECONNRESET/i, msg: 'Network error' },
            // New: SSL certificate
            { match: /certificate verify failed|SSL.*certificate|self.signed.cert|CERTIFICATE_VERIFY_FAILED/i, msg: 'SSL certificate error' },
            // New: Disk full
            { match: /No space left on device|disk full|ENOSPC/i, msg: 'Disk full' },
            // Existing: PaperForge not installed
            { match: /paperforge.*not found|cannot import|ModuleNotFoundError|No module named/i, msg: 'PaperForge not installed' },
            // Existing + New: Permission denied (expand pattern)
            { match: /permission denied|EACCES|EPERM/i, msg: 'Permission denied' },
            // Existing: Path not found
            { match: /ENOENT/i, msg: 'Path not found' },
            // Existing: Timeout
            { match: /timeout|timed out/i, msg: 'Timeout' },
        ];
        for (const p of patterns) {
            if (p.match.test(raw)) return p.msg;
        }
        // Keep existing fallback
        const fallback = raw.split('\n').filter(Boolean).slice(0, 3).join(' | ');
        return fallback.slice(0, 200) || 'Unknown error';
    }
    ```

    The order matters: `pip` check before generic `command not found`, network errors before timeout (timeouts often have more specific patterns).

    === PART B: Add "Copy diagnostic" button to setup wizard error area ===
    In the setup wizard's install step (around line 2386-2390, the catch block), after `this._log(...)`, add a "Copy diagnostic" button beside the error area.

    Modify the catch block at line 2386:
    ```javascript
    } catch (err) {
        console.error('PaperForge setup failed:', err.message);
        const errorMsg = this._formatSetupError(err.message);
        this._log(t('install_failed') + errorMsg);
        
        // Add "Copy diagnostic" button
        const diagBtn = this._installLog.parentElement?.createEl('button', {
            cls: 'paperforge-copy-diag-btn',
            text: t('error_copy_diagnostic') || 'Copy diagnostic',
        });
        if (diagBtn) {
            const rawError = err.message;
            const pyInfo = this.plugin?.settings?.python_path || 'auto';
            const pluginVer = this.plugin?.manifest?.version || '?';
            const osInfo = process.platform + ' ' + process.arch;
            const diagnostic = [
                `[PaperForge Diagnostic]`,
                `Category: ${errorMsg}`,
                `Plugin version: ${pluginVer}`,
                `Python: ${pyInfo}`,
                `OS: ${osInfo}`,
                `--- Raw error ---`,
                rawError.slice(0, 2000),
            ].join('\n');
            diagBtn.addEventListener('click', () => {
                navigator.clipboard.writeText(diagnostic).then(() => {
                    diagBtn.setText(t('error_copied') || 'Copied!');
                    setTimeout(() => {
                        diagBtn.setText(t('error_copy_diagnostic') || 'Copy diagnostic');
                    }, 3000);
                }).catch(() => {
                    new Notice('[!!] Clipboard write failed', 6000);
                });
            });
        }
        
        btn.disabled = false;
        btn.textContent = t('install_btn_retry');
    }
    ```

    Also add i18n keys for the button text. Append to the existing Object.assign calls:
    
    `Object.assign(LANG.en, {...})` — add:
    - `error_copy_diagnostic`: "Copy diagnostic"
    - `error_copied`: "Copied!"
    
    `Object.assign(LANG.zh, {...})` — add:
    - `error_copy_diagnostic`: "复制诊断信息"
    - `error_copied`: "已复制"

    === PART C: Add CSS for the Copy diagnostic button ===
    Append to `styles.css`:
    
    ```css
    /* ── Copy Diagnostic Button ── */
    .paperforge-copy-diag-btn {
        margin-top: 8px;
        padding: 4px 12px;
        font-size: 0.85em;
        background: var(--interactive-normal, #e0e0e0);
        color: var(--text-normal, #000);
        border: 1px solid var(--background-modifier-border, #ccc);
        border-radius: 4px;
        cursor: pointer;
    }
    .paperforge-copy-diag-btn:hover {
        background: var(--interactive-hover, #d0d0d0);
    }
    ```

    === PART D: Verify status.py doctor check aligns with PyYAML ===
    No changes needed to `paperforge/worker/status.py` — line 221 already checks for `"yaml"`. This aligns with the `pyyaml>=6.0` dependency now declared in pyproject.toml. Confirm by reading the line to ensure it remains correct.
  </action>

  <verify>
    <automated>
      # Verify all 10 error patterns exist (5 original + 5 new)
      node -e "
        const fs=require('fs');
        const c=fs.readFileSync('paperforge/plugin/main.js','utf-8');
        const patterns=[{match:'pip.*not found',name:'pip'},{match:'command not found|No such file',name:'python'},{match:'resolve host|ENOTFOUND',name:'network'},{match:'certificate verify',name:'ssl'},{match:'No space left|ENOSPC',name:'disk'},{match:'paperforge.*not found|cannot import',name:'paperforge'},{match:'permission denied|EACCES|EPERM',name:'permission'},{match:'ENOENT',name:'path'},{match:'timeout|timed out',name:'timeout'}];
        const funcMatch=c.match(/_formatSetupError\(raw\)\s*\{([^}]+)\}/s);
        if(!funcMatch){console.error('_formatSetupError function not found');process.exit(1)}
        const body=funcMatch[1];
        for(const p of patterns){
          // Check each pattern's match regex appears in the patterns array  
          if(!body.includes(p.match.split('|')[0])){
            console.error('Missing pattern:', p.name, '- regex part:', p.match.split('|')[0]);
            process.exit(1);
          }
        }
        console.log('All 9+ error patterns present');
      "
      # Verify Copy diagnostic button exists
      node -e "
        const fs=require('fs');
        const c=fs.readFileSync('paperforge/plugin/main.js','utf-8');
        if(!c.includes('Copy diagnostic')) console.log('WARN: exact text not found (may be i18n)');
        if(!c.includes('error_copy_diagnostic')){console.error('i18n key error_copy_diagnostic missing');process.exit(1)}
        console.log('Copy diagnostic i18n key found');
      "
      # Verify diagnostic format includes Category, Plugin version, Python, OS
      node -e "
        const fs=require('fs');
        const c=fs.readFileSync('paperforge/plugin/main.js','utf-8');
        const diag=['Category:','Plugin version','Python:','OS:','Raw error'];
        for(const d of diag){
          if(!c.includes(d)){console.error('Missing diagnostic field:', d);process.exit(1)}
        }
        console.log('All diagnostic fields present');
      "
    </automated>
  </verify>

  <done>
    - _formatSetupError classifies 10 error categories (5 original + 5 new): pip not found, Python not found, Network error, SSL certificate error, Disk full, PaperForge not installed, Permission denied, Path not found, Timeout, Unknown error
    - Order prioritizes specific patterns before generic ones
    - "Copy diagnostic" button appears in setup wizard error area
    - Button copies: category + plugin version + python path + OS + raw error excerpt
    - Button shows "Copied!" feedback for 3 seconds after click
    - i18n keys for button text in both en and zh
    - CSS for button styling
    - status.py yaml check confirmed aligned with pyproject.toml dependency
  </done>
</task>

</tasks>

<verification>
=== Plan-level Verification ===

1. **RUNTIME-04** — Runtime drift detection and sync:
   - Runtime Health section in settings shows Plugin vX → Python vY with match/mismatch badge (Task 2)
   - "Sync Runtime" button runs pip install --upgrade to align (Task 2)
   - Dashboard drift warning banner shown on version mismatch (Task 2)

2. **RUNTIME-05** — Install and sync failure classification:
   - _formatSetupError extended to 10 categories (Task 3)
   - Categories: pip not found, Python not found, Network error, SSL certificate, Disk full, PaperForge not installed, Permission denied, Path not found, Timeout, Unknown (fallback)
   - "Copy diagnostic" button captures category + env + raw error to clipboard (Task 3)

3. **CLEAN-02** — Manifest source consistency:
   - bump.py FILES_TO_UPDATE already includes root_manifest and plugin_manifest (verified, Task 1)
   - Comment added to document canonical source chain (Task 1)

4. **CLEAN-03** — minAppVersion:
   - Both manifests updated to "1.9.0" (Obsidian Bases minimum) (Task 1)
   - versions.json updated to reflect new compatibility mapping (Task 1)

5. **CLEAN-04** — PyYAML dependency:
   - pyyaml>=6.0 added to pyproject.toml dependencies (Task 1)
   - status.py "yaml" check confirmed aligned (Task 3, verified)

=== File Conflict Check ===
- Task 1: JSON files, pyproject.toml, bump.py — no overlap with Tasks 2, 3
- Task 2: main.js (settings tab + dashboard view + CSS) — shares main.js with Task 3 but in different areas
- Task 3: main.js (error formatting function + setup wizard) — shares main.js with Task 2 but in different functions
- Tasks 2 and 3 both also touch styles.css — append-only, no conflict

The tasks within this plan modify main.js in different class methods (PaperForgeSettingTab, PaperForgeStatusView, setup wizard helper). They can be executed sequentially without conflicts since each task modifies its own functions.

=== Context Budget ===
- Task 1: ~10% (3 config files, 2 JSON files, verification)
- Task 2: ~25% (main.js settings section, dashboard, CSS, i18n)
- Task 3: ~15% (main.js error function, setup wizard, CSS, i18n)
- Total: ~50% — within budget
</verification>

<success_criteria>
- [ ] All 5 requirements (RUNTIME-04, RUNTIME-05, CLEAN-02, CLEAN-03, CLEAN-04) addressed
- [ ] minAppVersion updated to "1.9.0" in both manifests and versions.json
- [ ] PyYAML added to pyproject.toml dependencies
- [ ] Runtime Health section displays below Python path in settings
- [ ] Version comparison shows match (green) / mismatch (red) badge
- [ ] "Sync Runtime" button triggers pip install with feedback
- [ ] Dashboard shows yellow drift warning banner when versions mismatch
- [ ] _formatSetupError covers 10 categories with correct priority ordering
- [ ] "Copy diagnostic" button appears on setup failure with working clipboard copy
- [ ] All i18n keys in both English and Chinese
- [ ] CSS classes defined for badge states, banner, and diagnostic button
- [ ] Automated verification commands pass
</success_criteria>

<output>
After completion, create `.planning/phases/52-runtime-alignment-failure-closure/52-001-SUMMARY.md`
</output>
