diff --git a/plugins/claude/.deepreview b/plugins/claude/.deepreview index b02b719b..090f1640 100644 --- a/plugins/claude/.deepreview +++ b/plugins/claude/.deepreview @@ -1,3 +1,34 @@ +windows_powershell_parity: + description: "PLUG-REQ-002.1: Every .sh hook script must have a functionally equivalent .ps1 PowerShell script." + match: + include: + - "plugins/claude/hooks/*.sh" + - "plugins/claude/hooks/*.ps1" + review: + strategy: matches_together + instructions: | + Review shell and PowerShell hook scripts for functional parity per + PLUG-REQ-002 (specs/deepwork/cli_plugins/PLUG-REQ-002-windows-support.md). + + For every `.sh` file under `plugins/claude/hooks/`, verify: + + 1. A corresponding `.ps1` file exists with the same base name. + 2. The `.ps1` script is functionally equivalent: same stdin JSON contract, + same stdout JSON structure, same exit code semantics. + 3. The `.ps1` script uses PowerShell-native JSON parsing (`ConvertFrom-Json`, + `ConvertTo-Json`) — NOT `jq`, `grep`, `sed`, or other Unix tools. + 4. The `.ps1` script uses `$ErrorActionPreference = 'Stop'` and try/catch + error handling that outputs `{}` on failure (matching the bash pattern). + 5. The `.ps1` script has a header comment block: description, input/output, + exit codes. + + When either a `.sh` or `.ps1` file changes, verify the pair is still in + sync — same logic, same JSON keys, same conditional behavior. + + Output Format: + - PASS: All .sh files have equivalent .ps1 files and they are in sync. + - FAIL: List each issue with the file pair and what is missing or divergent. + claude_plugin_skill_instructions: description: "PLUG-REQ-001 & REVIEW-REQ-007: Verify skill instructions adequately convey behavioral requirements." match: diff --git a/plugins/claude/hooks/deepschema_write.ps1 b/plugins/claude/hooks/deepschema_write.ps1 new file mode 100644 index 00000000..1e871392 --- /dev/null +++ b/plugins/claude/hooks/deepschema_write.ps1 @@ -0,0 +1,37 @@ +# deepschema_write.ps1 - DeepSchema write hook +# +# PostToolUse hook for Write/Edit - validates files against applicable +# DeepSchemas by piping the hook input through `deepwork hook deepschema_write`. +# +# Input (stdin): JSON from Claude Code PostToolUse hook +# Output (stdout): JSON response from deepwork hook deepschema_write +# Exit codes: +# 0 - Validation passed (or no applicable schema) +# 1 - Validation failed + +$ErrorActionPreference = 'Stop' + +try { + # ==== Read stdin ==== + $Input = @($Input) -join "`n" + if (-not $Input) { + $Input = [Console]::In.ReadToEnd() + } + + # ==== Set platform env var ==== + $env:DEEPWORK_HOOK_PLATFORM = 'claude' + + # ==== Pipe through deepwork CLI and propagate exit code ==== + $Result = $Input | deepwork hook deepschema_write + $ExitCode = $LASTEXITCODE + + if ($Result) { + Write-Output $Result + } + + exit $ExitCode +} +catch { + Write-Error $_.Exception.Message + exit 1 +} diff --git a/plugins/claude/hooks/hooks.json b/plugins/claude/hooks/hooks.json index 80ebdff4..f9f9566f 100644 --- a/plugins/claude/hooks/hooks.json +++ b/plugins/claude/hooks/hooks.json @@ -7,6 +7,11 @@ { "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/startup_context.sh" + }, + { + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/hooks/startup_context.ps1", + "shell": "powershell" } ] }, @@ -16,6 +21,11 @@ { "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/post_compact.sh" + }, + { + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/hooks/post_compact.ps1", + "shell": "powershell" } ] } @@ -27,6 +37,11 @@ { "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/startup_context.sh" + }, + { + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/hooks/startup_context.ps1", + "shell": "powershell" } ] } @@ -38,6 +53,11 @@ { "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/post_commit_reminder.sh" + }, + { + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/hooks/post_commit_reminder.ps1", + "shell": "powershell" } ] }, @@ -47,6 +67,11 @@ { "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/deepschema_write.sh" + }, + { + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/hooks/deepschema_write.ps1", + "shell": "powershell" } ] }, @@ -56,6 +81,11 @@ { "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/deepschema_write.sh" + }, + { + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/hooks/deepschema_write.ps1", + "shell": "powershell" } ] } diff --git a/plugins/claude/hooks/post_commit_reminder.ps1 b/plugins/claude/hooks/post_commit_reminder.ps1 new file mode 100644 index 00000000..c90b7af5 --- /dev/null +++ b/plugins/claude/hooks/post_commit_reminder.ps1 @@ -0,0 +1,35 @@ +<# +.SYNOPSIS + Post-commit reminder hook for DeepWork Claude Code plugin. + +.DESCRIPTION + Triggers after Bash tool uses that contain "git commit" to remind + the agent to run the review skill. + +.INPUTS + JSON object on stdin with structure: { "tool_input": { "command": "..." } } + +.OUTPUTS + JSON object on stdout with hookSpecificOutput when the command contains + "git commit". No output otherwise. + +.NOTES + Exit codes: + 0 - Success (hook matched or no match) + 0 - On error, outputs {} and exits 0 to avoid blocking the agent +#> + +$ErrorActionPreference = 'Stop' + +try { + $Input = [Console]::In.ReadToEnd() + $Parsed = $Input | ConvertFrom-Json + $Command = $Parsed.tool_input.command + + if ($Command -and $Command -match 'git commit') { + Write-Output '{"hookSpecificOutput":{"hookEventName":"PostToolUse","additionalContext":"You **MUST** use AskUserQuestion tool to offer to the user to run the `review` skill to review the changes you just committed if you have not run a review recently."}}' + } +} +catch { + Write-Output '{}' +} diff --git a/plugins/claude/hooks/post_compact.ps1 b/plugins/claude/hooks/post_compact.ps1 new file mode 100644 index 00000000..d80bf7c6 --- /dev/null +++ b/plugins/claude/hooks/post_compact.ps1 @@ -0,0 +1,122 @@ +# post_compact.ps1 - Post-compaction context restoration hook +# +# Restores DeepWork workflow context after Claude Code compacts its context. +# Registered as a SessionStart hook with matcher "compact" in hooks.json. +# +# Input (stdin): JSON from Claude Code SessionStart hook (contains .cwd) +# Output (stdout): JSON with hookSpecificOutput.additionalContext, or empty {} +# Exit codes: +# 0 - Always (failures produce empty {} response) + +$ErrorActionPreference = 'Stop' + +function Write-EmptyAndExit { + Write-Output '{}' + exit 0 +} + +try { + # ==== Parse input ==== + $Input_ = [Console]::In.ReadToEnd() + $InputObj = $Input_ | ConvertFrom-Json + $Cwd = $InputObj.cwd + + if (-not $Cwd) { + Write-EmptyAndExit + } + + # ==== Fetch active sessions ==== + try { + $StackRaw = & deepwork jobs get-stack --path $Cwd 2>$null + if ($LASTEXITCODE -ne 0) { + Write-EmptyAndExit + } + } catch { + Write-EmptyAndExit + } + + $StackJson = $StackRaw | Out-String + $Stack = $StackJson | ConvertFrom-Json + + # ==== Check for active sessions ==== + $Sessions = $Stack.active_sessions + if (-not $Sessions -or $Sessions.Count -eq 0) { + Write-EmptyAndExit + } + + $SessionCount = $Sessions.Count + + # ==== Build markdown context from active sessions ==== + $Context = @" +# DeepWork Workflow Context (Restored After Compaction) + +You are in the middle of a DeepWork workflow. Use the DeepWork MCP tools to continue. +Call ``finished_step`` with your outputs and the ``session_id`` shown below when you complete the current step. + +"@ + + for ($i = 0; $i -lt $SessionCount; $i++) { + $Session = $Sessions[$i] + + $SessionId = if ($Session.session_id) { $Session.session_id } else { '' } + $JobName = if ($Session.job_name) { $Session.job_name } else { '' } + $WorkflowName = if ($Session.workflow_name) { $Session.workflow_name } else { '' } + $Goal = if ($Session.goal) { $Session.goal } else { '' } + $CurrentStep = if ($Session.current_step_id) { $Session.current_step_id } else { '' } + $StepNum = if ($null -ne $Session.step_number) { "$($Session.step_number)" } else { '' } + $TotalSteps = if ($null -ne $Session.total_steps) { "$($Session.total_steps)" } else { '' } + $CompletedArr = if ($Session.completed_steps) { $Session.completed_steps } else { @() } + $Completed = $CompletedArr -join ', ' + $CommonInfo = if ($Session.common_job_info) { $Session.common_job_info } else { '' } + $StepInstr = if ($Session.current_step_instructions) { $Session.current_step_instructions } else { '' } + + $StepLabel = $CurrentStep + if ($StepNum -ne '' -and $TotalSteps -ne '') { + $StepLabel = "$CurrentStep (step $StepNum of $TotalSteps)" + } + + $Context += @" + +## Active Session +- **session_id**: ``$SessionId`` (pass this to ``finished_step``, ``abort_workflow``, and ``go_to_step``) +- **Workflow**: $JobName/$WorkflowName +- **Goal**: $Goal +- **Current Step**: $StepLabel +"@ + + if ($Completed -ne '') { + $Context += "`n- **Completed Steps**: $Completed" + } + + if ($CommonInfo -ne '') { + $Context += @" + +### Common Job Info +$CommonInfo +"@ + } + + if ($StepInstr -ne '') { + $Context += @" + +### Current Step Instructions +$StepInstr +"@ + } + + $Context += "`n" + } + + # ==== Output hook response ==== + $Output = @{ + hookSpecificOutput = @{ + hookEventName = 'SessionStart' + additionalContext = $Context + } + } + $Output | ConvertTo-Json -Depth 5 -Compress + exit 0 + +} catch { + Write-EmptyAndExit +} diff --git a/plugins/claude/hooks/startup_context.ps1 b/plugins/claude/hooks/startup_context.ps1 new file mode 100644 index 00000000..f7ccaa71 --- /dev/null +++ b/plugins/claude/hooks/startup_context.ps1 @@ -0,0 +1,53 @@ +# startup_context.ps1 - Inject Claude Code session and agent IDs into context +# +# Runs on SessionStart and SubagentStart to provide agents with their +# CLAUDE_CODE_SESSION_ID (and CLAUDE_CODE_AGENT_ID for sub-agents). +# These IDs are used by MCP tools to locate persistent workflow state. +# +# Input (stdin): JSON from Claude Code hook (contains .session_id, optionally .agent_id) +# Output (stdout): JSON with hookSpecificOutput.additionalContext, or empty {} +# Exit codes: +# 0 - Always (failures produce empty {} response) + +$ErrorActionPreference = 'Stop' + +try { + # ==== Parse input ==== + $Input = @($Input) -join "`n" + if (-not $Input) { + $Input = [Console]::In.ReadToEnd() + } + $Json = $Input | ConvertFrom-Json + + $SessionId = $Json.session_id + if (-not $SessionId) { + Write-Output '{}' + exit 0 + } + + $EventName = if ($Json.hook_event_name) { $Json.hook_event_name } else { 'SessionStart' } + $AgentId = $Json.agent_id + + # ==== Build context string ==== + $Ctx = "CLAUDE_CODE_SESSION_ID=$SessionId" + if ($AgentId) { + $Ctx += "`nCLAUDE_CODE_AGENT_ID=$AgentId" + } + + # Inject DeepPlan trigger for planning mode + $DeepPlanMsg = 'When you enter plan mode and begin working on a plan, start the DeepWork workflow create_deep_plan (job: deepplan) via start_workflow before doing anything else. The workflow will guide you through structured planning. Its instructions supersede the default planning phases.' + $Ctx += "`n$DeepPlanMsg" + + # ==== Output hook response ==== + $Response = @{ + hookSpecificOutput = @{ + hookEventName = $EventName + additionalContext = $Ctx + } + } + $Response | ConvertTo-Json -Depth 3 -Compress +} +catch { + Write-Output '{}' + exit 0 +} diff --git a/specs/deepwork/cli_plugins/PLUG-REQ-002-windows-support.md b/specs/deepwork/cli_plugins/PLUG-REQ-002-windows-support.md new file mode 100644 index 00000000..5b4f3b75 --- /dev/null +++ b/specs/deepwork/cli_plugins/PLUG-REQ-002-windows-support.md @@ -0,0 +1,24 @@ +# PLUG-REQ-002: Windows Support + +## Overview + +The Claude Code plugin must work on Windows, where PowerShell is the default shell. Every bash hook script in the plugin must have a functionally equivalent PowerShell (`.ps1`) script so that hooks execute correctly on Windows without requiring Git Bash, WSL, or any Unix compatibility layer. + +## Requirements + +### PLUG-REQ-002.1: PowerShell Equivalents for Hook Scripts + +1. Every `.sh` file under `plugins/claude/hooks/` MUST have a corresponding `.ps1` file in the same directory with the same base name (e.g., `startup_context.sh` → `startup_context.ps1`). +2. Each `.ps1` script MUST be functionally equivalent to its `.sh` counterpart — same stdin/stdout contract, same exit code semantics, same JSON output structure. +3. PowerShell scripts MUST NOT depend on Unix-only tools (`jq`, `grep`, `sed`, `awk`). Use PowerShell-native JSON parsing (`ConvertFrom-Json`, `ConvertTo-Json`) and string operations instead. + +### PLUG-REQ-002.2: hooks.json Platform Dispatch + +1. Every hook entry in `plugins/claude/hooks/hooks.json` MUST list both the `.sh` command (default shell) and the `.ps1` command with `"shell": "powershell"` so that Claude Code runs the correct script per OS. +2. Both hooks run in parallel; on each OS one succeeds and the other fails gracefully (non-blocking). The `.sh` and `.ps1` hooks for the same event MUST produce identical output when they succeed. + +### PLUG-REQ-002.3: PowerShell Script Conventions + +1. PowerShell scripts MUST use `$ErrorActionPreference = 'Stop'` for fail-fast behavior (equivalent to `set -euo pipefail`). +2. PowerShell scripts MUST include a header comment block matching the bash convention: description, input/output, exit codes. +3. PowerShell scripts MUST handle errors with try/catch and output `{}` or appropriate fallback JSON on failure, matching the bash error-handling pattern.