mirror of
https://github.com/OthmanAdi/planning-with-files.git
synced 2026-09-14 16:09:40 +08:00
b49ea488fb
The Stop scalar never dispatched on macOS or Linux (the PowerShell branch was always selected because check-complete.ps1 ships on every platform) and was a silent no-op anywhere CLAUDE_SKILL_DIR was unset (the :- install path fallback can never substitute because the probed variable is always a non-empty string). Both the legacy completion advisory and the v3 completion gate were dead in those environments. The scalar now selects targets by file existence and dispatches by platform, PowerShell only under MINGW/MSYS/CYGWIN, sh elsewhere. Windows output is unchanged. Patched in all 14 SKILL.md variants that carry the scalar. session-catchup probed a ~/.claude/projects name that Claude Code never writes for POSIX paths (leading dash stripped) or paths containing an underscore (replaced with a dash), so recovery after /clear silently found nothing there. The mapper now probes the exact spelling first, keeps both legacy spellings for stores created by older versions, and settles ambiguity via the cwd recorded in the newest session file. Propagated to every shipped copy including the older-generation root, .hermes, and .mastracode scripts. Also: the attestation SHA cache is keyed on the absolute plan path so two projects can no longer share a slot and report a false PLAN TAMPERED; resolve-plan-dir.ps1 reaches parity with the sh resolver (slug filter, task_plan.md requirement in the newest-dir scan, fail-closed containment); ledger-append.sh no longer truncates summaries mid-codepoint (UTF-8-safe trim via iconv with a pure-sh fallback). New long-run features, all opt-in or additive: structure-aware injection (PWF_INJECT=smart or an inject-smart .mode token) keeps the active phase, phase counts, and the last three decisions in the window late in long plans; a Next Step section in both plan templates plus a sixth reboot question; session-catchup annotates tool results (ok or FAILED with the first error line) instead of only listing attempts. Infrastructure: macos-latest joins the CI matrix, a BSD-userland simulation harness runs the script fleet without realpath, readlink, flock, or sha256sum on the Linux leg, and .gitattributes pins LF for scripts. SEO surfaces: llms.txt rewritten as a Q&A page, three problem-query docs pages, plugin.json and CITATION.cff keyword hygiene. Suite grows from 217 to 301 passed, 11 skipped, on Windows and both existing CI legs; default hook output stays byte-identical to v2.43 (legacy invariant proven by the existing invariant tests).
87 lines
3.2 KiB
PowerShell
87 lines
3.2 KiB
PowerShell
# planning-with-files: resolve active plan directory (PowerShell mirror).
|
|
#
|
|
# Resolution order matches scripts/resolve-plan-dir.sh:
|
|
# 1. $env:PLAN_ID -> .\.planning\$PLAN_ID\
|
|
# 2. .\.planning\.active_plan content
|
|
# 3. Newest .\.planning\<dir>\ by LastWriteTime
|
|
# 4. Empty (legacy fallback to .\task_plan.md handled by caller)
|
|
#
|
|
# v3.8.0 parity with the sh resolver: slug validation on every branch, the
|
|
# newest-dir scan requires task_plan.md inside the candidate (a sessions/ or
|
|
# artifacts/ dir must never win), and containment fails CLOSED when
|
|
# canonicalization fails. Only successful canonicalization can rule out a
|
|
# junction/symlink escape; slug validation alone blocks textual traversal.
|
|
|
|
param(
|
|
[string]$PlanRoot = (Join-Path (Get-Location) ".planning")
|
|
)
|
|
|
|
$projectRoot = (Get-Location).Path
|
|
|
|
# Same shape as the sh resolver's slug_is_valid: first char [A-Za-z0-9_],
|
|
# rest [A-Za-z0-9._-]. Blocks traversal tokens before any path is built.
|
|
function Test-ValidSlug {
|
|
param([string]$Name)
|
|
if (-not $Name) { return $false }
|
|
return $Name -match '^[A-Za-z0-9_][A-Za-z0-9._-]*$'
|
|
}
|
|
|
|
# Containment guard (security A1.3): a resolved plan dir must canonicalize to
|
|
# a path under the project root. A directory symlink/junction inside a valid
|
|
# slug pointing outside the workspace would otherwise let the hooks hash and
|
|
# inject an arbitrary file. Resolve-Path follows reparse points; we compare
|
|
# the real paths. Fails CLOSED on canonicalization failure, matching
|
|
# resolve-plan-dir.sh.
|
|
function Test-WithinRoot {
|
|
param([string]$Candidate)
|
|
try {
|
|
$rootReal = (Resolve-Path -LiteralPath $projectRoot -ErrorAction Stop).Path
|
|
$candReal = (Resolve-Path -LiteralPath $Candidate -ErrorAction Stop).Path
|
|
} catch {
|
|
return $false
|
|
}
|
|
if (-not $rootReal -or -not $candReal) { return $false }
|
|
$rootNorm = $rootReal.TrimEnd('\', '/')
|
|
$candNorm = $candReal.TrimEnd('\', '/')
|
|
if ($candNorm -eq $rootNorm) { return $true }
|
|
return $candNorm.StartsWith($rootNorm + [System.IO.Path]::DirectorySeparatorChar, [System.StringComparison]::OrdinalIgnoreCase)
|
|
}
|
|
|
|
$activeFile = Join-Path $PlanRoot ".active_plan"
|
|
|
|
if ($env:PLAN_ID) {
|
|
if (Test-ValidSlug $env:PLAN_ID) {
|
|
$candidate = Join-Path $PlanRoot $env:PLAN_ID
|
|
if ((Test-Path $candidate -PathType Container) -and (Test-WithinRoot $candidate)) {
|
|
Write-Output $candidate
|
|
exit 0
|
|
}
|
|
}
|
|
}
|
|
|
|
if (Test-Path $activeFile) {
|
|
$planId = (Get-Content $activeFile -Raw).Trim()
|
|
if ($planId -and (Test-ValidSlug $planId)) {
|
|
$candidate = Join-Path $PlanRoot $planId
|
|
if ((Test-Path $candidate -PathType Container) -and (Test-WithinRoot $candidate)) {
|
|
Write-Output $candidate
|
|
exit 0
|
|
}
|
|
}
|
|
}
|
|
|
|
if (Test-Path $PlanRoot -PathType Container) {
|
|
$latest = Get-ChildItem -Path $PlanRoot -Directory |
|
|
Where-Object { -not $_.Name.StartsWith('.') } |
|
|
Where-Object { Test-ValidSlug $_.Name } |
|
|
Where-Object { Test-Path (Join-Path $_.FullName "task_plan.md") -PathType Leaf } |
|
|
Where-Object { Test-WithinRoot $_.FullName } |
|
|
Sort-Object LastWriteTime -Descending |
|
|
Select-Object -First 1
|
|
if ($latest) {
|
|
Write-Output $latest.FullName
|
|
}
|
|
}
|
|
|
|
exit 0
|