---
phase: 02-diagnostics-documentation
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - src/cli/hook.rs
autonomous: true
requirements: [HOOK-01, HOOK-02, HOOK-03, DOCS-01, DOCS-02]

must_haves:
  truths:
    - "NoSidecar adoption log entry distinguishes sidecar_port_missing from sidecar_port_stale and includes project_root"
    - "SYMFORGE_HOOK_VERBOSE=1 causes stderr diagnostic output during hook execution"
    - "First NoSidecar event per session writes a one-time hint to stderr explaining how to start the sidecar"
    - "docs/codex-integration-ceiling.md exists documenting Codex capabilities and limitations"
    - "CLAUDE.md references the Codex ceiling doc"
  artifacts:
    - path: "src/cli/hook.rs"
      provides: "is_hook_verbose, maybe_emit_sidecar_hint, enhanced NoSidecarDetail with project_root"
      contains: "fn is_hook_verbose"
    - path: "docs/codex-integration-ceiling.md"
      provides: "Codex ceiling documentation"
      min_lines: 50
  key_links:
    - from: "src/cli/hook.rs::run_hook"
      to: "src/cli/hook.rs::is_hook_verbose"
      via: "env var check gating stderr output"
      pattern: "is_hook_verbose\\(\\)"
    - from: "src/cli/hook.rs::run_hook"
      to: "src/cli/hook.rs::maybe_emit_sidecar_hint"
      via: "called in both missing-port and stale-port NoSidecar paths"
      pattern: "maybe_emit_sidecar_hint"
    - from: "src/cli/hook.rs::record_hook_outcome_with_detail"
      to: "src/cli/hook.rs::NoSidecarDetail"
      via: "structured detail with reason + project_root fields"
      pattern: "project_root"
---

<objective>
Commit Kilo Code's hook diagnostics implementation, fix a gap (missing-port path needs `maybe_emit_sidecar_hint` call), and add tests to verify all three HOOK requirements. Mark DOCS-01/DOCS-02 as pre-completed.

Purpose: The implementation for HOOK-01/02/03 already exists as uncommitted changes by the Kilo Code agent. This plan commits that work, verifies correctness, and adds test coverage to lock the behavior — matching the pattern established in Phase 1.

Output: Committed implementation + tests for HOOK-01/02/03. DOCS-01/DOCS-02 marked complete.
</objective>

<execution_context>
@E:/project/symforge/.claude/get-shit-done/workflows/execute-plan.md
@E:/project/symforge/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/02-diagnostics-documentation/02-CONTEXT.md

<interfaces>
<!-- Key types and contracts already in src/cli/hook.rs (uncommitted Kilo Code changes). -->

```rust
/// Structured detail for no-sidecar adoption log entries.
struct NoSidecarDetail<'a> {
    reason: &'a str,        // "sidecar_port_missing" or "sidecar_port_stale"
    searched_path: &'a str,
    suggestion: &'a str,    // "start_mcp_session" or "restart_sidecar"
    project_root: &'a str,  // NEW: project root path for HOOK-01
}

/// Check whether verbose hook diagnostics are enabled (HOOK-02).
fn is_hook_verbose() -> bool;

/// Emit a one-time hint to stderr when sidecar is not running (HOOK-03).
/// Uses marker file `.symforge/hook-hint-shown` with 30-min freshness window.
fn maybe_emit_sidecar_hint(repo_root: &Path);

/// Emit detailed diagnostic to stderr — gated behind SYMFORGE_HOOK_VERBOSE=1 (HOOK-02).
fn emit_no_sidecar_diagnostic(repo_root: &Path, port_file_path: &Path);

/// Log format with detail (tab-separated):
/// session_id \t workflow \t outcome \t reason=X \t searched_path=X \t suggestion=X \t project_root=X
fn append_hook_adoption_event_with_detail(...);
```

Log file path: `.symforge/hook-adoption.log` (constant `ADOPTION_LOG_FILE`)
Hint marker: `.symforge/hook-hint-shown` (constant `HOOK_HINT_MARKER`)
Freshness window: 30 minutes (constant `HOOK_HINT_FRESHNESS`)
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Commit and verify Kilo Code's hook diagnostics implementation</name>
  <files>src/cli/hook.rs</files>
  <read_first>
    - src/cli/hook.rs (full file — verify the uncommitted Kilo Code changes are present and correct)
  </read_first>
  <action>
    1. Read and verify the uncommitted changes in `src/cli/hook.rs` cover all three requirements:

       **HOOK-01 verification points:**
       - `NoSidecarDetail` struct has `project_root: &'a str` field
       - Two call sites in `run_hook`: one with `reason: "sidecar_port_missing"` and one with `reason: "sidecar_port_stale"`
       - `append_hook_adoption_event_with_detail` format string includes `project_root={}`
       - Both call sites pass `project_root: &repo_root.to_string_lossy()`

       **HOOK-02 verification points:**
       - `is_hook_verbose()` function exists, checks `SYMFORGE_HOOK_VERBOSE` env var equals `"1"`
       - `emit_no_sidecar_diagnostic` has early return `if !is_hook_verbose() { return; }`
       - Multiple `if verbose { eprintln!(...) }` calls throughout `run_hook` for detailed tracing
       - All stderr output uses `[symforge-hook]` prefix

       **HOOK-03 verification points:**
       - `HOOK_HINT_MARKER` constant is `".symforge/hook-hint-shown"`
       - `HOOK_HINT_FRESHNESS` is 30 minutes
       - `maybe_emit_sidecar_hint` checks marker file freshness, writes hint to stderr, touches marker
       - Called from BOTH the missing-port path (line ~293 after `emit_no_sidecar_diagnostic`) AND the stale-port path (line ~336 after HTTP failure)

    2. Run `cargo fmt -- --check` to verify formatting is clean.

    3. Run `cargo check` to verify compilation.

    4. Run `cargo test --all-targets -- --test-threads=1` to verify no regressions.

    5. If all checks pass, stage and commit `src/cli/hook.rs` with message:
       `feat(02-01): hook diagnostics — verbose mode, port-missing vs stale, one-time hint`

       This commits Kilo Code's implementation work. Use:
       ```
       git add src/cli/hook.rs
       git commit -m "feat(02-01): hook diagnostics — verbose mode, port-missing vs stale, one-time hint

       HOOK-01: NoSidecar log entries now distinguish sidecar_port_missing from
       sidecar_port_stale and include the project_root path.

       HOOK-02: SYMFORGE_HOOK_VERBOSE=1 enables detailed stderr diagnostics
       throughout hook execution with [symforge-hook] prefix.

       HOOK-03: First NoSidecar event per session writes a one-time hint
       explaining how to start the sidecar (marker file with 30-min freshness).

       Implementation by Kilo Code agent, committed and verified.

       Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>"
       ```

    6. If any issues found during verification (formatting, compilation, logic gaps), fix them before committing. Specifically check:
       - `maybe_emit_sidecar_hint` is called in the missing-port path (not just stale-port)
       - All `eprintln!` calls use `[symforge-hook]` prefix consistently
       - No accidental stdout output (would break fail-open JSON contract)
  </action>
  <verify>
    <automated>cd E:/project/symforge && cargo fmt -- --check && cargo check && cargo test --all-targets -- --test-threads=1</automated>
  </verify>
  <acceptance_criteria>
    - `git log --oneline -1` shows the commit message starting with "feat(02-01)"
    - `git diff HEAD -- src/cli/hook.rs` shows no uncommitted changes
    - `cargo test` passes with 0 failures
    - `grep -c "fn is_hook_verbose" src/cli/hook.rs` returns 1
    - `grep -c "fn maybe_emit_sidecar_hint" src/cli/hook.rs` returns 1
    - `grep -c "project_root" src/cli/hook.rs` returns at least 4 (field def + format string + 2 call sites)
    - `grep -c "sidecar_port_missing\|sidecar_port_stale" src/cli/hook.rs` returns at least 2
  </acceptance_criteria>
  <done>
    Kilo Code's HOOK-01/02/03 implementation committed cleanly. Compiles, formats, and passes all existing tests.
  </done>
</task>

<task type="auto" tdd="true">
  <name>Task 2: Add tests for HOOK-01/02/03 behavior</name>
  <files>tests/hook_diagnostics.rs, src/cli/hook.rs</files>
  <read_first>
    - src/cli/hook.rs (focus on: is_hook_verbose, maybe_emit_sidecar_hint, NoSidecarDetail, append_hook_adoption_event_with_detail, HOOK_HINT_MARKER, HOOK_HINT_FRESHNESS)
    - tests/hook_enrichment_integration.rs (reference for test patterns: tempdir usage, CWD_LOCK, test helper style)
  </read_first>
  <behavior>
    HOOK-01 tests:
    - Test: append_hook_adoption_event_with_detail with NoSidecarDetail writes log line containing "reason=sidecar_port_missing" and "project_root="
    - Test: append_hook_adoption_event_with_detail with reason "sidecar_port_stale" writes log line containing "reason=sidecar_port_stale"
    - Test: log line contains both "searched_path=" and "suggestion=" fields
    - Test: log line without detail (None) does NOT contain "reason=" or "project_root="

    HOOK-02 tests:
    - Test: is_hook_verbose returns false when env var is unset
    - Test: is_hook_verbose returns true when SYMFORGE_HOOK_VERBOSE=1
    - Test: is_hook_verbose returns false when SYMFORGE_HOOK_VERBOSE=0 or any other value

    HOOK-03 tests:
    - Test: maybe_emit_sidecar_hint creates marker file at .symforge/hook-hint-shown
    - Test: maybe_emit_sidecar_hint does not re-emit when marker file is fresh (< 30 min)
    - Test: maybe_emit_sidecar_hint re-emits when marker file is stale (> 30 min)
  </behavior>
  <action>
    1. To make the functions testable, add `pub(crate)` visibility to the following functions in `src/cli/hook.rs` (they are currently private):
       - `is_hook_verbose` -> `pub(crate) fn is_hook_verbose`
       - `maybe_emit_sidecar_hint` -> `pub(crate) fn maybe_emit_sidecar_hint`
       - `append_hook_adoption_event_with_detail` -> `pub(crate) fn append_hook_adoption_event_with_detail`
       - `NoSidecarDetail` -> `pub(crate) struct NoSidecarDetail`

    2. Ensure these symbols are re-exported from `src/cli/mod.rs` if needed for the test crate to access them. The test file at `tests/hook_diagnostics.rs` will need `use symforge::cli::hook::{...}` or equivalent path. Check the existing `tests/hook_enrichment_integration.rs` for the import pattern used.

       If the module structure doesn't allow direct import of `cli::hook` internals from integration tests, write the tests as `#[cfg(test)] mod tests { ... }` inside `src/cli/hook.rs` itself (unit tests). This is actually the better approach since:
       - `is_hook_verbose` depends on env vars (unit test can control with `std::env::set_var`)
       - `maybe_emit_sidecar_hint` operates on filesystem (unit test with tempdir)
       - `append_hook_adoption_event_with_detail` operates on filesystem (unit test with tempdir)
       - No need to change visibility of private functions

    3. Create the test module at the bottom of `src/cli/hook.rs`:

       ```rust
       #[cfg(test)]
       mod tests {
           use super::*;
           use std::io::Read;
           use tempfile::TempDir;

           // ---- HOOK-02: is_hook_verbose ----

           #[test]
           fn hook_verbose_returns_false_when_unset() {
               std::env::remove_var("SYMFORGE_HOOK_VERBOSE");
               assert!(!is_hook_verbose());
           }

           #[test]
           fn hook_verbose_returns_true_when_set_to_1() {
               std::env::set_var("SYMFORGE_HOOK_VERBOSE", "1");
               let result = is_hook_verbose();
               std::env::remove_var("SYMFORGE_HOOK_VERBOSE");
               assert!(result);
           }

           #[test]
           fn hook_verbose_returns_false_for_other_values() {
               for val in &["0", "true", "yes", "2", ""] {
                   std::env::set_var("SYMFORGE_HOOK_VERBOSE", val);
                   assert!(!is_hook_verbose(), "should be false for SYMFORGE_HOOK_VERBOSE={val}");
               }
               std::env::remove_var("SYMFORGE_HOOK_VERBOSE");
           }

           // ---- HOOK-01: adoption log detail fields ----

           #[test]
           fn adoption_log_missing_port_includes_reason_and_project_root() {
               let tmp = TempDir::new().unwrap();
               let log_path = tmp.path().join("hook-adoption.log");
               let detail = NoSidecarDetail {
                   reason: "sidecar_port_missing",
                   searched_path: "/repo/.symforge/sidecar.port",
                   suggestion: "start_mcp_session",
                   project_root: "/repo",
               };
               append_hook_adoption_event_with_detail(
                   &log_path, Some("sess-1"), "source-read", "no-sidecar", Some(detail),
               ).unwrap();
               let content = std::fs::read_to_string(&log_path).unwrap();
               assert!(content.contains("reason=sidecar_port_missing"), "missing reason field");
               assert!(content.contains("project_root=/repo"), "missing project_root field");
               assert!(content.contains("searched_path=/repo/.symforge/sidecar.port"));
               assert!(content.contains("suggestion=start_mcp_session"));
           }

           #[test]
           fn adoption_log_stale_port_has_distinct_reason() {
               let tmp = TempDir::new().unwrap();
               let log_path = tmp.path().join("hook-adoption.log");
               let detail = NoSidecarDetail {
                   reason: "sidecar_port_stale",
                   searched_path: "/repo/.symforge/sidecar.port",
                   suggestion: "restart_sidecar",
                   project_root: "/repo",
               };
               append_hook_adoption_event_with_detail(
                   &log_path, Some("sess-2"), "source-read", "no-sidecar", Some(detail),
               ).unwrap();
               let content = std::fs::read_to_string(&log_path).unwrap();
               assert!(content.contains("reason=sidecar_port_stale"), "should have stale reason");
               assert!(content.contains("project_root=/repo"));
           }

           #[test]
           fn adoption_log_without_detail_has_no_reason_or_project_root() {
               let tmp = TempDir::new().unwrap();
               let log_path = tmp.path().join("hook-adoption.log");
               append_hook_adoption_event_with_detail(
                   &log_path, Some("sess-3"), "source-read", "routed", None,
               ).unwrap();
               let content = std::fs::read_to_string(&log_path).unwrap();
               assert!(!content.contains("reason="), "should have no reason without detail");
               assert!(!content.contains("project_root="), "should have no project_root without detail");
           }

           // ---- HOOK-03: one-time sidecar hint ----

           #[test]
           fn sidecar_hint_creates_marker_file() {
               let tmp = TempDir::new().unwrap();
               let marker = tmp.path().join(HOOK_HINT_MARKER);
               assert!(!marker.exists());
               maybe_emit_sidecar_hint(tmp.path());
               assert!(marker.exists(), "marker file should be created");
           }

           #[test]
           fn sidecar_hint_skips_when_marker_fresh() {
               let tmp = TempDir::new().unwrap();
               let marker = tmp.path().join(HOOK_HINT_MARKER);
               std::fs::create_dir_all(marker.parent().unwrap()).unwrap();
               std::fs::write(&marker, "").unwrap();
               // Marker was just created — should be fresh.
               // We can't easily capture stderr in a unit test, but we can verify
               // the marker file's mtime is NOT updated (proving the function returned early).
               let mtime_before = std::fs::metadata(&marker).unwrap().modified().unwrap();
               std::thread::sleep(std::time::Duration::from_millis(50));
               maybe_emit_sidecar_hint(tmp.path());
               let mtime_after = std::fs::metadata(&marker).unwrap().modified().unwrap();
               assert_eq!(mtime_before, mtime_after, "marker mtime should not change when fresh");
           }
       }
       ```

    4. Run `cargo test --all-targets -- --test-threads=1` — all tests must pass.

    5. Run `cargo fmt` to fix any formatting, then `cargo fmt -- --check` to verify.

    6. Commit the test additions:
       ```
       git add src/cli/hook.rs
       git commit -m "test(02-01): add unit tests for HOOK-01/02/03 hook diagnostics

       - 3 tests for HOOK-02: is_hook_verbose env var behavior
       - 3 tests for HOOK-01: adoption log detail fields (missing/stale/none)
       - 2 tests for HOOK-03: sidecar hint marker creation and freshness skip

       Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>"
       ```
  </action>
  <verify>
    <automated>cd E:/project/symforge && cargo test --all-targets -- --test-threads=1 hook 2>&1 | grep -E "test result|test .*(hook|verbose|sidecar_hint|adoption_log)"</automated>
  </verify>
  <acceptance_criteria>
    - `cargo test -- --test-threads=1` passes with 0 failures
    - `grep -c "#\[test\]" src/cli/hook.rs` returns at least 8 (the new tests)
    - `grep "hook_verbose_returns_false_when_unset\|hook_verbose_returns_true\|hook_verbose_returns_false_for_other\|adoption_log_missing_port\|adoption_log_stale_port\|adoption_log_without_detail\|sidecar_hint_creates_marker\|sidecar_hint_skips_when_marker" src/cli/hook.rs | wc -l` returns at least 8
    - `cargo fmt -- --check` returns clean
    - `git log --oneline -1` shows "test(02-01)" commit
  </acceptance_criteria>
  <done>
    8 unit tests covering HOOK-01 (log detail fields distinguish missing vs stale with project_root), HOOK-02 (is_hook_verbose env var behavior), and HOOK-03 (marker file creation and freshness gating) all pass. Behavior locked against regressions.
  </done>
</task>

<task type="auto">
  <name>Task 3: Mark DOCS-01 and DOCS-02 complete in REQUIREMENTS.md</name>
  <files>.planning/REQUIREMENTS.md</files>
  <read_first>
    - .planning/REQUIREMENTS.md
    - docs/codex-integration-ceiling.md (verify it exists and has substantive content)
    - CLAUDE.md (verify it references the codex ceiling doc)
  </read_first>
  <action>
    1. Verify `docs/codex-integration-ceiling.md` exists and has at least 50 lines of content covering: what works in Codex (MCP tools), what doesn't (hooks, sidecar), and what requires Codex changes.

    2. Verify `CLAUDE.md` contains a reference/link to the Codex ceiling doc (the CONTEXT.md notes it's at line 144).

    3. Update `.planning/REQUIREMENTS.md`:
       - Change `- [ ] **DOCS-01**:` to `- [x] **DOCS-01**:` (add note: "Completed by Kilo Code — docs/codex-integration-ceiling.md")
       - Change `- [ ] **DOCS-02**:` to `- [x] **DOCS-02**:` (add note: "Completed by Kilo Code — CLAUDE.md references doc")
       - Change `- [ ] **HOOK-01**:` to `- [x] **HOOK-01**:`
       - Change `- [ ] **HOOK-02**:` to `- [x] **HOOK-02**:`
       - Change `- [ ] **HOOK-03**:` to `- [x] **HOOK-03**:`
       - Update the Traceability table: set all 5 items to "Complete"

    4. Do NOT commit yet — this will be included in the SUMMARY commit by the executor.
  </action>
  <verify>
    <automated>cd E:/project/symforge && grep -c "\[x\]" .planning/REQUIREMENTS.md</automated>
  </verify>
  <acceptance_criteria>
    - `grep "\[x\].*DOCS-01" .planning/REQUIREMENTS.md` matches
    - `grep "\[x\].*DOCS-02" .planning/REQUIREMENTS.md` matches
    - `grep "\[x\].*HOOK-01" .planning/REQUIREMENTS.md` matches
    - `grep "\[x\].*HOOK-02" .planning/REQUIREMENTS.md` matches
    - `grep "\[x\].*HOOK-03" .planning/REQUIREMENTS.md` matches
    - `grep -c "\[ \]" .planning/REQUIREMENTS.md` returns 0 (no remaining incomplete items)
    - `wc -l docs/codex-integration-ceiling.md` returns at least 50
  </acceptance_criteria>
  <done>
    All 8 requirements (SYMB-01/02/03 + HOOK-01/02/03 + DOCS-01/02) marked complete in REQUIREMENTS.md. Zero remaining incomplete items.
  </done>
</task>

</tasks>

<verification>
After all tasks complete:
1. `cargo test --all-targets -- --test-threads=1` — all tests pass (existing + 8 new)
2. `cargo fmt -- --check` — no formatting issues
3. `git log --oneline -3` — shows feat commit, test commit, and docs update
4. `grep -c "\[ \]" .planning/REQUIREMENTS.md` — returns 0
5. `grep "reason=sidecar_port_missing\|reason=sidecar_port_stale" src/cli/hook.rs` — both patterns present
6. `grep "SYMFORGE_HOOK_VERBOSE" src/cli/hook.rs` — env var check present
7. `grep "hook-hint-shown" src/cli/hook.rs` — marker file constant present
</verification>

<success_criteria>
- HOOK-01: NoSidecar log entries distinguish "sidecar_port_missing" from "sidecar_port_stale" and include project_root — verified by 3 unit tests
- HOOK-02: SYMFORGE_HOOK_VERBOSE=1 enables stderr diagnostics — verified by 3 unit tests
- HOOK-03: One-time sidecar hint with 30-min freshness gating — verified by 2 unit tests
- DOCS-01: docs/codex-integration-ceiling.md exists with Codex capability documentation
- DOCS-02: CLAUDE.md references the Codex ceiling doc
- All existing tests continue to pass
- All 8 milestone requirements marked complete
</success_criteria>

<output>
After completion, create `.planning/phases/02-diagnostics-documentation/02-01-SUMMARY.md`
</output>
