---
phase: quick
plan: 260326-n1n
type: execute
wave: 1
depends_on: []
files_modified:
  - plugins/itp-hooks/hooks/posttooluse-ty-type-check.ts
  - plugins/itp-hooks/hooks/stop-ty-project-check.ts
  - plugins/itp-hooks/hooks/hooks.json
  - plugins/itp-hooks/CLAUDE.md
autonomous: true
must_haves:
  truths:
    - "ty runs with --python-version 3.13 on every .py edit (never defaults to 3.14)"
    - "ty output uses concise format with parsed error/warning counts"
    - "Stop hook runs project-wide ty check only when Python files were edited"
    - "Stop hook is silent when ty not installed (no install reminder)"
    - "All hooks fail-open on errors, timeouts, and edge cases"
  artifacts:
    - path: "plugins/itp-hooks/hooks/posttooluse-ty-type-check.ts"
      provides: "Upgraded per-file ty hook with --python-version 3.13 and concise output"
    - path: "plugins/itp-hooks/hooks/stop-ty-project-check.ts"
      provides: "Session-end project-wide ty check"
    - path: "plugins/itp-hooks/hooks/hooks.json"
      provides: "Registration for stop-ty-project-check"
  key_links:
    - from: "posttooluse-ty-type-check.ts"
      to: "stop-ty-project-check.ts"
      via: "gate file in /tmp/.claude-ty-edits/ tracking .py edits per session"
      pattern: "mkdirSync.*claude-ty-edits"
---

<objective>
Upgrade the ty type checker hook suite with 5 improvements: pin --python-version 3.13 (CRITICAL policy), use concise output format with parsed diagnostics, add a Stop hook for project-wide cross-file checks, document recommended ty.toml config, and register all hooks correctly.

Purpose: ty defaults to Python 3.14 which violates project policy. Concise format gives cleaner one-line diagnostics. Project-wide Stop check catches cross-file type errors missed by per-file checks.
Output: Upgraded posttooluse hook, new stop hook, updated hooks.json, updated CLAUDE.md docs.
</objective>

<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@plugins/itp-hooks/hooks/posttooluse-ty-type-check.ts
@plugins/itp-hooks/hooks/stop-hook-error-summary.ts
@plugins/itp-hooks/hooks/posttooluse-oxlint-check.ts
@plugins/itp-hooks/hooks/posttooluse-tsgo-type-check.ts
@plugins/itp-hooks/hooks/hooks.json
@plugins/itp-hooks/CLAUDE.md
</context>

<tasks>

<task type="auto">
  <name>Task 1: Upgrade posttooluse-ty-type-check.ts with --python-version 3.13, concise output, and edge case hardening</name>
  <files>plugins/itp-hooks/hooks/posttooluse-ty-type-check.ts</files>
  <action>
Rewrite the ty PostToolUse hook with these changes:

1. **--python-version 3.13 flag** (CRITICAL): Add `--python-version`, `3.13` to the ty check command args. This overrides ty's default of 3.14. Per project policy, Python 3.13 ONLY.

2. **--output-format concise**: Add `--output-format`, `concise` to args. Concise format outputs one diagnostic per line in format: `file:line:col: severity: message [rule-code]`.

3. **Parse concise output** to count errors vs warnings:
   - Split output by newlines
   - Count lines containing `: error:` and `: warning:`
   - Format block message as: `[TY] {N} error(s), {M} warning(s) in {filename}:\n\n{diagnostics}`
   - If only warnings (no errors), still block to inform Claude but use softer language

4. **Gate file for Stop hook tracking**: After successful ty invocation (regardless of result), touch a gate file at `/tmp/.claude-ty-edits/{sessionId}.edited` to signal the Stop hook that Python files were edited. Use `mkdirSync` + `writeFileSync` (create if not exists). Fail-open on gate file errors.

5. **Edge case hardening** (all fail-open with `process.exit(0)`):
   - Check `existsSync(filePath)` before running ty — file may be deleted between Write/Edit and hook
   - Skip files containing `/.venv/` or `/node_modules/` in path
   - Also accept `.pyi` stub files (they can have type errors)
   - Exit code 2 (config error) and 101 (internal bug): treat as ty issue, not type error — exit 0 silently
   - Truncate output to first 30 lines if longer (prevent context flooding), append `... ({total} total diagnostics, showing first 30)`
   - Use `node:fs` and `node:path` imports (not bare `fs`/`path`) per oxlint pattern

6. **Keep existing patterns**: Same stdin parsing, same `which ty` check, same once-per-session install reminder with gate file, same 4s timeout, same `blockWithReminder` output format.
   </action>

  <verify>
    <automated>bun plugins/itp-hooks/hooks/posttooluse-ty-type-check.ts <<< '{}' ; echo "exit: $?"</automated>
  </verify>
  <done>
    - ty command includes `--python-version 3.13` and `--output-format concise`
    - Concise output parsed into error/warning counts
    - Gate file written to /tmp/.claude-ty-edits/ for Stop hook
    - .venv/ and node_modules/ paths skipped
    - Exit codes 2 and 101 handled silently
    - Output truncated at 30 lines
    - .pyi files accepted
    - File existence check before ty invocation
  </done>
</task>

<task type="auto">
  <name>Task 2: Create stop-ty-project-check.ts Stop hook for cross-file type checking</name>
  <files>plugins/itp-hooks/hooks/stop-ty-project-check.ts</files>
  <action>
Create a new Stop hook following the pattern from `stop-hook-error-summary.ts`. Key design:

**Output format**: Stop hooks output JSON to stdout. Use `{ additionalContext: "..." }` for informational output (non-blocking). Never use `{ decision: "block" }` from a Stop hook.

**Gate check** (skip if no Python edits):

- Check for existence of any file matching `/tmp/.claude-ty-edits/*.edited` (use `readdirSync` on the gate dir)
- If gate dir doesn't exist or is empty, exit with `console.log(JSON.stringify({}))` — no Python files were edited
- Note: Stop hooks don't receive session_id via stdin like PostToolUse hooks. Check for ANY .edited file in the gate dir (conservative: run if any session edited Python). Alternative: use `process.env.CLAUDE_SESSION_ID` if available, else check any.

**Python project detection**:

- Check CWD for `pyproject.toml` OR any `.py` file in CWD (use `readdirSync` + `.some()`)
- If neither found, skip silently

**ty installation check**:

- `which ty` — if not found, exit silently (no install reminder from Stop hooks)

**Run ty check**:

- Command: `["ty", "check", ".", "--output-format", "concise", "--python-version", "3.13", "--exit-zero"]`
- `--exit-zero` ensures ty never returns non-zero (prevents hook failure)
- CWD: `process.cwd()` (default)
- Timeout: 15000ms (project-wide check takes longer)
- stdout: "pipe", stderr: "pipe"

**Parse and summarize output**:

- Split stdout by newlines, filter empty lines
- Count lines with `: error:` and `: warning:`
- If zero diagnostics: output empty JSON (clean project)
- If diagnostics exist:
  - Show summary line: `[TY] Project type check: {E} error(s), {W} warning(s) across {F} file(s)`
  - Count unique files from the `file:line:col:` prefix
  - Show at most 20 diagnostic lines, then `... ({total} total, showing first 20)`
  - Wrap in `{ additionalContext: summary }`

**Edge cases** (all fail-open with `console.log(JSON.stringify({}))`):

- Gate dir read fails → skip
- ty not in PATH → skip
- ty timeout (15s) → skip
- ty produces empty output → skip
- Hundreds of diagnostics → truncate to 20 lines + count
- Wrap entire main() in try/catch → `console.log(JSON.stringify({}))`

**Cleanup**: After running (success or fail), remove gate files from `/tmp/.claude-ty-edits/` to prevent stale state. Use `rmSync` with `{ recursive: true, force: true }` on the gate dir. Fail-open on cleanup errors.

**Shebang and structure**: `#!/usr/bin/env bun` at top. Synchronous `main()` function (Stop hooks don't receive stdin). Use `node:fs` and `node:path` imports.
</action>
<verify>
<automated>bun plugins/itp-hooks/hooks/stop-ty-project-check.ts ; echo "exit: $?"</automated>
</verify>
<done> - Stop hook outputs valid JSON to stdout - Skips when no Python files were edited (no gate files) - Skips when ty not installed - Skips when not in a Python project - Runs `ty check . --output-format concise --python-version 3.13 --exit-zero` - Summarizes errors/warnings with file count - Truncates output at 20 lines - Cleans up gate files after run - Fail-open on all errors
</done>
</task>

<task type="auto">
  <name>Task 3: Register stop hook in hooks.json and update CLAUDE.md documentation</name>
  <files>plugins/itp-hooks/hooks/hooks.json, plugins/itp-hooks/CLAUDE.md</files>
  <action>
**hooks.json changes:**

1. Add `stop-ty-project-check.ts` to the Stop hooks section. Add it as a new entry in the Stop[0].hooks array:

```json
{
  "type": "command",
  "command": "bun ${CLAUDE_PLUGIN_ROOT}/hooks/stop-ty-project-check.ts",
  "timeout": 15000,
  "description": "ty project-wide type check on session exit (only if .py files were edited)"
}
```

1. Verify the existing posttooluse-ty-type-check.ts registration is correct (it is — Write|Edit matcher, 5000ms timeout).

**CLAUDE.md changes:**

1. Update the PostToolUse hooks table entry for `posttooluse-ty-type-check.ts`:
   - Change purpose to: `ty type checker on .py/.pyi files with --python-version 3.13, concise output (every edit)`

2. Add `stop-ty-project-check.ts` to the Stop Hooks table:
   - Hook: `stop-ty-project-check.ts`
   - Purpose: `Project-wide ty type check on exit (only if .py files were edited, --python-version 3.13)`

3. Add a new section "## ty Type Checker Configuration" after the "Code Correctness Philosophy" section with:
   - Explain that ty runs per-file on every edit (PostToolUse) and project-wide on session exit (Stop)
   - Document recommended ty.toml configuration for projects:

     ```toml
     [environment]
     python-version = "3.13"

     [terminal]
     output-format = "concise"
     ```

   - Note: The hooks pass --python-version 3.13 explicitly, but projects should also pin it in ty.toml for consistency when running ty manually
   - Note the "silent failures only" philosophy: hooks never block on ty config errors or internal bugs (exit codes 2, 101)
     </action>

  <verify>
    <automated>bun -e "const h = require('./plugins/itp-hooks/hooks/hooks.json'); const stops = h.hooks.Stop[0].hooks; const found = stops.some(h => h.command.includes('stop-ty-project-check')); console.log('stop hook registered:', found); if (!found) process.exit(1);"</automated>
  </verify>
  <done>
    - hooks.json has stop-ty-project-check.ts in Stop section with 15000ms timeout
    - CLAUDE.md PostToolUse table updated with --python-version 3.13 mention
    - CLAUDE.md Stop Hooks table includes stop-ty-project-check.ts
    - CLAUDE.md has ty configuration section with recommended ty.toml
    - Existing posttooluse registration unchanged and correct
  </done>
</task>

</tasks>

<verification>
1. `bun plugins/itp-hooks/hooks/posttooluse-ty-type-check.ts <<< '{}'` exits 0 (no file_path = skip)
2. `bun plugins/itp-hooks/hooks/stop-ty-project-check.ts` outputs valid JSON (empty or with context)
3. `grep -c "python-version" plugins/itp-hooks/hooks/posttooluse-ty-type-check.ts` returns >= 1
4. `grep -c "python-version" plugins/itp-hooks/hooks/stop-ty-project-check.ts` returns >= 1
5. `grep -c "concise" plugins/itp-hooks/hooks/posttooluse-ty-type-check.ts` returns >= 1
6. hooks.json is valid JSON: `bun -e "JSON.parse(require('fs').readFileSync('plugins/itp-hooks/hooks/hooks.json','utf8')); console.log('valid')"`
</verification>

<success_criteria>

- ty hook always runs with --python-version 3.13 (never defaults to 3.14)
- Concise output format used and parsed into error/warning counts
- Stop hook runs project-wide check only when .py files were edited in session
- Stop hook silent when ty not installed, not in Python project, or no edits
- All hooks fail-open on every error path
- hooks.json valid and includes new Stop hook
- CLAUDE.md documents the full ty integration including recommended ty.toml
  </success_criteria>

<output>
After completion, create `.planning/quick/260326-n1n-upgrade-ty-hook-python-version-concise-o/260326-n1n-SUMMARY.md`
</output>
