attest-plan.ps1 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118
  1. #requires -Version 5.0
  2. <#
  3. .SYNOPSIS
  4. Lock the current task_plan.md content with a SHA-256 attestation.
  5. .DESCRIPTION
  6. Use after you finalise (or intentionally edit) a plan. The hooks then refuse
  7. to inject plan content into the model context if the file diverges from the
  8. attested hash, surfacing a "[PLAN TAMPERED]" warning instead.
  9. Plan resolution:
  10. 1. $env:PLAN_ID -> ./.planning/$PLAN_ID/
  11. 2. ./.planning/.active_plan
  12. 3. Newest ./.planning/<dir>/ by LastWriteTime
  13. 4. Legacy ./task_plan.md at project root
  14. .PARAMETER Show
  15. Print the stored hash for the active plan.
  16. .PARAMETER Clear
  17. Remove the attestation (re-open the plan).
  18. #>
  19. [CmdletBinding(DefaultParameterSetName = "Attest")]
  20. param(
  21. [Parameter(ParameterSetName = "Show")]
  22. [switch] $Show,
  23. [Parameter(ParameterSetName = "Clear")]
  24. [switch] $Clear
  25. )
  26. $ErrorActionPreference = "Stop"
  27. function Resolve-PlanFile {
  28. $planRoot = Join-Path (Get-Location) ".planning"
  29. if ($env:PLAN_ID) {
  30. $candidate = Join-Path $planRoot $env:PLAN_ID
  31. $planFile = Join-Path $candidate "task_plan.md"
  32. if (Test-Path -LiteralPath $planFile) { return (Resolve-Path -LiteralPath $planFile).Path }
  33. }
  34. $activePointer = Join-Path $planRoot ".active_plan"
  35. if (Test-Path -LiteralPath $activePointer) {
  36. $planId = (Get-Content -LiteralPath $activePointer -Raw).Trim()
  37. if ($planId) {
  38. $candidate = Join-Path $planRoot $planId
  39. $planFile = Join-Path $candidate "task_plan.md"
  40. if (Test-Path -LiteralPath $planFile) { return (Resolve-Path -LiteralPath $planFile).Path }
  41. }
  42. }
  43. if (Test-Path -LiteralPath $planRoot) {
  44. $newest = Get-ChildItem -LiteralPath $planRoot -Directory -ErrorAction SilentlyContinue |
  45. Where-Object { -not $_.Name.StartsWith(".") } |
  46. Where-Object { Test-Path -LiteralPath (Join-Path $_.FullName "task_plan.md") } |
  47. Sort-Object LastWriteTime -Descending |
  48. Select-Object -First 1
  49. if ($newest) {
  50. return (Resolve-Path -LiteralPath (Join-Path $newest.FullName "task_plan.md")).Path
  51. }
  52. }
  53. $legacy = Join-Path (Get-Location) "task_plan.md"
  54. if (Test-Path -LiteralPath $legacy) {
  55. return (Resolve-Path -LiteralPath $legacy).Path
  56. }
  57. return $null
  58. }
  59. function Get-AttestationPath {
  60. param([string] $PlanFile)
  61. $planDir = Split-Path -Parent $PlanFile
  62. $cwd = (Get-Location).Path
  63. if ($planDir -eq $cwd) {
  64. return (Join-Path $cwd ".plan-attestation")
  65. }
  66. return (Join-Path $planDir ".attestation")
  67. }
  68. $planFile = Resolve-PlanFile
  69. if (-not $planFile) {
  70. Write-Error "[plan-attest] No task_plan.md found. Create a plan first."
  71. exit 1
  72. }
  73. $attestationFile = Get-AttestationPath -PlanFile $planFile
  74. if ($Show) {
  75. if (Test-Path -LiteralPath $attestationFile) {
  76. Write-Output "Plan: $planFile"
  77. Write-Output "Attestation: $attestationFile"
  78. Write-Output ("SHA-256: " + (Get-Content -LiteralPath $attestationFile -Raw).Trim())
  79. } else {
  80. Write-Output "[plan-attest] No attestation set for $planFile."
  81. exit 1
  82. }
  83. exit 0
  84. }
  85. if ($Clear) {
  86. if (Test-Path -LiteralPath $attestationFile) {
  87. Remove-Item -LiteralPath $attestationFile -Force
  88. Write-Output "[plan-attest] Cleared attestation for $planFile."
  89. } else {
  90. Write-Output "[plan-attest] No attestation to clear."
  91. }
  92. exit 0
  93. }
  94. $hashVal = (Get-FileHash -LiteralPath $planFile -Algorithm SHA256).Hash.ToLowerInvariant()
  95. Set-Content -LiteralPath $attestationFile -Value $hashVal -NoNewline -Encoding ascii
  96. $short = $hashVal.Substring(0, 12)
  97. Write-Output "[plan-attest] Locked $planFile"
  98. Write-Output "[plan-attest] SHA-256: $short... (stored in $attestationFile)"
  99. Write-Output "[plan-attest] Hooks will block injection if the file is modified without re-running this command."
  100. exit 0