planning-with-files 3.20.5 → 3.20.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/SKILL.md CHANGED
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: pi-planning-with-files
2
+ name: planning-with-files
3
3
  description: "Persistent file-based planning for multi-step AI-agent work. Keeps task_plan.md, findings.md, and progress.md on disk; lifecycle hooks inject selected project planning context. Automatic recovery reads project planning files only. Explicit session-catchup.py --metadata reads same-project local agent session records and emits aggregate counts only; --replay may emit bounded nonce-framed excerpts. Optional gated mode can request continuation only when the host supports it and never runs commands declared in Markdown. The skill has no network upload path. Use for research or work needing 5+ tool calls."
4
4
  user-invocable: true
5
5
  allowed-tools: "Read Write Edit Bash Glob Grep"
@@ -7,26 +7,28 @@ hooks:
7
7
  UserPromptSubmit:
8
8
  - hooks:
9
9
  - type: command
10
- command: "SH=\"${CLAUDE_SKILL_DIR}/scripts/skill-hook.sh\"; [ -f \"$SH\" ] || SH=$(ls \"$HOME/.claude/skills/planning-with-files/scripts/skill-hook.sh\" \"$HOME/.claude/plugins/marketplaces/planning-with-files/scripts/skill-hook.sh\" 2>/dev/null | head -1); [ -n \"$SH\" ] && [ -f \"$SH\" ] && sh \"$SH\" --event=userprompt; exit 0"
10
+ command: "[ -n \"${CLAUDE_PLUGIN_ROOT:-}\" ] && exit 0; SH=\"${CLAUDE_SKILL_DIR}/scripts/skill-hook.sh\"; [ -f \"$SH\" ] || SH=$(ls \"$HOME/.claude/skills/planning-with-files/scripts/skill-hook.sh\" \"$HOME/.claude/plugins/marketplaces/planning-with-files/scripts/skill-hook.sh\" 2>/dev/null | head -1); [ -n \"$SH\" ] && [ -f \"$SH\" ] && sh \"$SH\" --event=userprompt; exit 0"
11
11
  PreToolUse:
12
12
  - matcher: "Write|Edit|Bash|Read|Glob|Grep"
13
13
  hooks:
14
14
  - type: command
15
- command: "SH=\"${CLAUDE_SKILL_DIR}/scripts/skill-hook.sh\"; [ -f \"$SH\" ] || SH=$(ls \"$HOME/.claude/skills/planning-with-files/scripts/skill-hook.sh\" \"$HOME/.claude/plugins/marketplaces/planning-with-files/scripts/skill-hook.sh\" 2>/dev/null | head -1); [ -n \"$SH\" ] && [ -f \"$SH\" ] && sh \"$SH\" --event=pretool; exit 0"
15
+ command: "[ -n \"${CLAUDE_PLUGIN_ROOT:-}\" ] && exit 0; SH=\"${CLAUDE_SKILL_DIR}/scripts/skill-hook.sh\"; [ -f \"$SH\" ] || SH=$(ls \"$HOME/.claude/skills/planning-with-files/scripts/skill-hook.sh\" \"$HOME/.claude/plugins/marketplaces/planning-with-files/scripts/skill-hook.sh\" 2>/dev/null | head -1); [ -n \"$SH\" ] && [ -f \"$SH\" ] && sh \"$SH\" --event=pretool; exit 0"
16
16
  PostToolUse:
17
17
  - matcher: "Write|Edit"
18
18
  hooks:
19
19
  - type: command
20
- command: "SH=\"${CLAUDE_SKILL_DIR}/scripts/skill-hook.sh\"; [ -f \"$SH\" ] || SH=$(ls \"$HOME/.claude/skills/planning-with-files/scripts/skill-hook.sh\" \"$HOME/.claude/plugins/marketplaces/planning-with-files/scripts/skill-hook.sh\" 2>/dev/null | head -1); [ -n \"$SH\" ] && [ -f \"$SH\" ] && sh \"$SH\" --event=posttool; exit 0"
20
+ command: "[ -n \"${CLAUDE_PLUGIN_ROOT:-}\" ] && exit 0; SH=\"${CLAUDE_SKILL_DIR}/scripts/skill-hook.sh\"; [ -f \"$SH\" ] || SH=$(ls \"$HOME/.claude/skills/planning-with-files/scripts/skill-hook.sh\" \"$HOME/.claude/plugins/marketplaces/planning-with-files/scripts/skill-hook.sh\" 2>/dev/null | head -1); [ -n \"$SH\" ] && [ -f \"$SH\" ] && sh \"$SH\" --event=posttool; exit 0"
21
21
  Stop:
22
22
  - hooks:
23
23
  - type: command
24
- command: "SH=\"${CLAUDE_SKILL_DIR}/scripts/skill-hook.sh\"; [ -f \"$SH\" ] || SH=$(ls \"$HOME/.claude/skills/planning-with-files/scripts/skill-hook.sh\" \"$HOME/.claude/plugins/marketplaces/planning-with-files/scripts/skill-hook.sh\" 2>/dev/null | head -1); [ -n \"$SH\" ] && [ -f \"$SH\" ] && sh \"$SH\" --event=stop; exit 0"
24
+ command: "[ -n \"${CLAUDE_PLUGIN_ROOT:-}\" ] && exit 0; SH=\"${CLAUDE_SKILL_DIR}/scripts/skill-hook.sh\"; [ -f \"$SH\" ] || SH=$(ls \"$HOME/.claude/skills/planning-with-files/scripts/skill-hook.sh\" \"$HOME/.claude/plugins/marketplaces/planning-with-files/scripts/skill-hook.sh\" 2>/dev/null | head -1); [ -n \"$SH\" ] && [ -f \"$SH\" ] && sh \"$SH\" --event=stop; exit 0"
25
25
  PreCompact:
26
26
  - matcher: "*"
27
27
  hooks:
28
28
  - type: command
29
- command: "SH=\"${CLAUDE_SKILL_DIR}/scripts/skill-hook.sh\"; [ -f \"$SH\" ] || SH=$(ls \"$HOME/.claude/skills/planning-with-files/scripts/skill-hook.sh\" \"$HOME/.claude/plugins/marketplaces/planning-with-files/scripts/skill-hook.sh\" 2>/dev/null | head -1); [ -n \"$SH\" ] && [ -f \"$SH\" ] && sh \"$SH\" --event=precompact; exit 0"
29
+ command: "[ -n \"${CLAUDE_PLUGIN_ROOT:-}\" ] && exit 0; SH=\"${CLAUDE_SKILL_DIR}/scripts/skill-hook.sh\"; [ -f \"$SH\" ] || SH=$(ls \"$HOME/.claude/skills/planning-with-files/scripts/skill-hook.sh\" \"$HOME/.claude/plugins/marketplaces/planning-with-files/scripts/skill-hook.sh\" 2>/dev/null | head -1); [ -n \"$SH\" ] && [ -f \"$SH\" ] && sh \"$SH\" --event=precompact; exit 0"
30
+ metadata:
31
+ version: "3.20.7"
30
32
  ---
31
33
 
32
34
  # Planning with Files
@@ -221,7 +223,7 @@ Copy these templates to start:
221
223
  Helper scripts for automation:
222
224
 
223
225
  - `scripts/init-session.sh` — Initialize planning files. With a name arg, creates an isolated plan under `.planning/YYYY-MM-DD-<slug>/` for parallel task workflows. Without args, writes `task_plan.md` at project root (legacy mode, backward-compatible).
224
- - `scripts/set-active-plan.sh` — Switch the active plan pointer (`.planning/.active_plan`). Run with a plan ID to switch; run without args to show which plan is current.
226
+ - `scripts/set-active-plan.sh` — Switch or inspect the active plan pointer (`.planning/.active_plan`). Run with `--list` to show named plans and phase counts, with a plan ID to switch, or without args to show which plan is current.
225
227
  - `scripts/resolve-plan-dir.sh` — Resolve the active plan directory. A set `$PLAN_ID` is a binding: it resolves or resolution stops, never another plan (issue #237). With no `$PLAN_ID`, multiple named plans refuse selection. A single named plan may use `.planning/.active_plan` or discovery by mtime; otherwise resolution falls back to the project root (legacy). Used internally by hooks.
226
228
  - `scripts/check-complete.sh` — Verify all phases in the active plan are complete.
227
229
  - `scripts/session-catchup.py`: Explicit same-project session-record aggregation or bounded replay (`--metadata` / `--replay`); bare invocation does not access host history.
@@ -284,11 +286,11 @@ Not every install path ships every surface in this section. Two distinct install
284
286
  | `/plugin marketplace add OthmanAdi/planning-with-files` then `/plugin install` | SKILL.md, scripts, templates, **plus `commands/` folder** | Yes, as `/plan-goal` and `/plan-loop` |
285
287
  | `npx skills add OthmanAdi/planning-with-files` (or ClawHub) | SKILL.md, scripts, templates only | No, follow the manual fallback below |
286
288
 
287
- The PreCompact hook is registered in the SKILL.md frontmatter and works for both routes. The `/plan-goal` and `/plan-loop` slash commands live in `commands/` at the repo root, which only the plugin route copies into `~/.claude/plugins/marketplaces/`. Skill-only installs land at `~/.claude/skills/planning-with-files/` and do not see `commands/`.
289
+ Plugin installs register six lifecycle events from `hooks/hooks.json`, including quiet `SessionStart` recovery. Standalone skill installs register the five hooks in this SKILL.md frontmatter only after the skill is invoked for that session, so they have no startup recovery. The `/plan-goal` and `/plan-loop` slash commands live in `commands/` at the repository root and are available from the versioned plugin cache. Skill-only installs land at `~/.claude/skills/planning-with-files/` and do not include `commands/`.
288
290
 
289
291
  The standalone `scripts/skill-hook.sh` reads the host's JSON session identity. UserPromptSubmit emits plain context; PreToolUse and PostToolUse emit the event's `additionalContext` JSON. The progress reminder fires at most once per turn when a usable session identity and private cache are available, and repeats when those are unavailable. All five events follow the same plan selection and opt-out checks.
290
292
 
291
- Both slash commands also carry `disable-model-invocation: true`, which means the model will not auto-trigger them. You type them. Per known Claude Code behavior (anthropics/claude-code issues #26251, #41417), some sessions interpret `disable-model-invocation: true` as "I cannot use the Skill tool for this entry at all" and refuse to fire even when you type the slash. If that happens, the manual fallback below produces the same effect.
293
+ Both slash commands carry `disable-model-invocation: true`, so invoke them explicitly. If a command is unavailable on a skill-only install, the manual fallback below produces the same planning-file result.
292
294
 
293
295
  ### PreCompact hook (auto)
294
296
 
package/package.json CHANGED
@@ -1,10 +1,9 @@
1
1
  {
2
2
  "name": "planning-with-files",
3
- "version": "3.20.5",
4
- "description": "Persistent project planning with selected context injection. Automatic recovery uses project files only; explicit catchup modes read same-project local session records for aggregate counts or bounded replay. The host-aware gate never runs Markdown-declared commands. No network upload path. Ships the skill plus a Pi Coding Agent extension.",
3
+ "version": "3.20.7",
4
+ "description": "Official Planning with Files skill for AI coding agents. Keeps persistent project planning files and injects selected plan context. Automatic recovery reads project files only; explicit local session catchup has no network upload path. Includes scripts, templates, and a Pi Coding Agent extension.",
5
5
  "keywords": [
6
- "pi-package",
7
- "pi-skill",
6
+ "planning-with-files",
8
7
  "planning",
9
8
  "manus",
10
9
  "agent",
@@ -14,7 +13,8 @@
14
13
  "coding-agent",
15
14
  "context-engineering",
16
15
  "session-recovery",
17
- "long-running-agents"
16
+ "long-running-agents",
17
+ "pi-coding-agent"
18
18
  ],
19
19
  "pi": {
20
20
  "skills": [
@@ -18,6 +18,27 @@ param(
18
18
  [switch]$Gated
19
19
  )
20
20
 
21
+ # Windows PowerShell 5.1 can silently relocate a -File invocation when the
22
+ # inherited cwd contains wildcard characters such as [ or ]. Recover the
23
+ # physical cwd for a direct invocation before any project-relative paths are
24
+ # resolved.
25
+ if ($PSVersionTable.PSVersion.Major -eq 5 -and
26
+ [System.Management.Automation.WildcardPattern]::ContainsWildcardCharacters([Environment]::CurrentDirectory)) {
27
+ $processArgs = [Environment]::GetCommandLineArgs()
28
+ for ($index = 0; $index -lt ($processArgs.Length - 1); $index++) {
29
+ if ($processArgs[$index] -ieq '-File') {
30
+ $entryScript = $processArgs[$index + 1]
31
+ if (-not [IO.Path]::IsPathRooted($entryScript)) {
32
+ $entryScript = Join-Path ([Environment]::CurrentDirectory) $entryScript
33
+ }
34
+ if ([IO.Path]::GetFullPath($entryScript) -eq [IO.Path]::GetFullPath($PSCommandPath)) {
35
+ Set-Location -LiteralPath ([Environment]::CurrentDirectory)
36
+ }
37
+ break
38
+ }
39
+ }
40
+ }
41
+
21
42
  $DATE = Get-Date -Format "yyyy-MM-dd"
22
43
 
23
44
  # Resolve v3 opt-in mode. -Gated implies autonomous and is the stronger marker.
@@ -331,13 +352,37 @@ if ($UsePlanDir) {
331
352
  # Activate the named plan only after all three planning files are ready.
332
353
  # This matches init-session.sh and prevents a failed initialization from
333
354
  # leaving .active_plan pointed at a partial plan directory.
334
- $global:LASTEXITCODE = 0
335
- try {
336
- & $PlanSelector $PlanId *> $null
337
- } catch {
338
- $global:LASTEXITCODE = 1
355
+ $pointerSet = $false
356
+ for ($attempt = 1; $attempt -le 5; $attempt++) {
357
+ $global:LASTEXITCODE = 0
358
+ try {
359
+ $selectorResult = & $PlanSelector $PlanId 2>&1
360
+ } catch {
361
+ $selectorResult = $_
362
+ $global:LASTEXITCODE = 1
363
+ }
364
+ if ($LASTEXITCODE -eq 0) {
365
+ $pointerSet = $true
366
+ break
367
+ }
368
+ # Another writer can replace the pointer between the selector's
369
+ # Get-Item and final-path check. Retry only that transient result.
370
+ $transientPointerRace = $selectorResult -is [System.Management.Automation.ErrorRecord] -and
371
+ $selectorResult.Exception.Message -ceq
372
+ 'Error: could not set the active plan pointer: the active plan pointer became unsafe during replacement'
373
+ if ($attempt -eq 5 -or -not $transientPointerRace) {
374
+ break
375
+ }
376
+ Start-Sleep -Milliseconds 50
377
+ $global:LASTEXITCODE = 0
378
+ try {
379
+ & $PlanSelector -VerifyRoot *> $null
380
+ } catch {
381
+ $global:LASTEXITCODE = 1
382
+ }
383
+ if ($LASTEXITCODE -ne 0) { break }
339
384
  }
340
- if ($LASTEXITCODE -ne 0) {
385
+ if (-not $pointerSet) {
341
386
  Write-Error "Error: could not safely update the active plan pointer at $(Join-Path $PlanningRoot '.active_plan')."
342
387
  exit 1
343
388
  }
@@ -7,9 +7,10 @@
7
7
  The ONLY sanctioned concurrent-safe writer of task_plan.md status lines. The
8
8
  orchestrator owns task_plan.md; workers NEVER edit it directly. The edit is
9
9
  a read-modify-write under the portable
10
- <plan-dir>\.pwf-locks\phase-status.lock directory lock, with an atomic
11
- temp-file + move swap so a torn write can never leave a half-rewritten plan
12
- on disk (architecture C4).
10
+ <plan-dir>\.pwf-locks\phase-status.lock namespace. Ownership is granted by
11
+ atomically creating its .owner file, rather than trusting an external mkdir
12
+ exit status. The plan rewrite uses an atomic temp-file + move swap so a torn
13
+ write can never leave a half-rewritten plan on disk (architecture C4).
13
14
 
14
15
  Editing task_plan.md changes its SHA, so the orchestrator must re-attest at
15
16
  phase boundaries (see attest-plan.ps1).
@@ -102,18 +103,44 @@ function Enter-PwfDirectoryLock {
102
103
 
103
104
  $wait = [Diagnostics.Stopwatch]::StartNew()
104
105
  while ($wait.Elapsed.TotalSeconds -lt 5) {
105
- $createdByUs = $false
106
106
  try {
107
- New-Item -Path $LockDir -ItemType Directory -ErrorAction Stop | Out-Null
108
- $createdByUs = $true
109
- [System.IO.File]::WriteAllText($ownerFile, $token + "`n", $utf8NoBom)
107
+ [void][System.IO.Directory]::CreateDirectory($LockDir)
108
+ } catch {
109
+ Start-Sleep -Milliseconds 100
110
+ continue
111
+ }
112
+
113
+ $stream = $null
114
+ $createdOwner = $false
115
+ $claimed = $false
116
+ try {
117
+ # FileMode.CreateNew is the cross-runtime ownership decision. It
118
+ # fails atomically when either the shell or PowerShell writer has
119
+ # already claimed .owner, even if both observed mkdir success.
120
+ $stream = [System.IO.File]::Open(
121
+ $ownerFile,
122
+ [System.IO.FileMode]::CreateNew,
123
+ [System.IO.FileAccess]::Write,
124
+ [System.IO.FileShare]::None
125
+ )
126
+ $createdOwner = $true
127
+ $bytes = $utf8NoBom.GetBytes($token + "`n")
128
+ $stream.Write($bytes, 0, $bytes.Length)
129
+ $stream.Flush()
130
+ $claimed = $true
110
131
  return [PSCustomObject]@{
111
132
  Directory = $LockDir
112
133
  OwnerFile = $ownerFile
113
134
  Token = $token
114
135
  }
115
136
  } catch {
116
- if ($createdByUs) {
137
+ # Existing ownership is the normal contention path. Other failures
138
+ # remain fail-closed and are retried only within the bounded wait.
139
+ } finally {
140
+ if ($stream) {
141
+ $stream.Dispose()
142
+ }
143
+ if ($createdOwner -and -not $claimed) {
117
144
  try {
118
145
  if ([System.IO.File]::Exists($ownerFile)) {
119
146
  $ownerValue = [System.IO.File]::ReadAllText($ownerFile).Trim()
@@ -121,13 +148,12 @@ function Enter-PwfDirectoryLock {
121
148
  [System.IO.File]::Delete($ownerFile)
122
149
  }
123
150
  }
124
- [System.IO.Directory]::Delete($LockDir, $false)
125
151
  } catch {
126
- # Leave any directory we cannot prove is still ours intact.
152
+ # Leave any owner file we cannot prove is still ours intact.
127
153
  }
128
154
  }
129
- Start-Sleep -Milliseconds 100
130
155
  }
156
+ Start-Sleep -Milliseconds 100
131
157
  }
132
158
 
133
159
  return $null
@@ -174,7 +200,7 @@ $planDir = Split-Path -Parent $planFile
174
200
  $lockRoot = Join-Path $planDir ".pwf-locks"
175
201
  $lockDir = Join-Path $lockRoot "phase-status.lock"
176
202
 
177
- # Atomic directory creation is the common lock primitive used by both the sh
203
+ # Exclusive .owner creation is the common lock primitive used by both the sh
178
204
  # and PowerShell implementations. Failure to acquire within about five seconds
179
205
  # is fail-closed: no plan read/rewrite is attempted.
180
206
  $lock = Enter-PwfDirectoryLock -LockRoot $lockRoot -LockDir $lockDir
@@ -4,9 +4,12 @@
4
4
  # This is the ONLY sanctioned concurrent-safe writer of task_plan.md status
5
5
  # lines. The orchestrator owns task_plan.md; workers NEVER edit it directly.
6
6
  # All status edits go through this read-modify-write under the portable
7
- # <plan-dir>/.pwf-locks/phase-status.lock directory lock, with an atomic
8
- # temp-file + mv swap so a torn write can never leave a half-rewritten plan on
9
- # disk (architecture C4).
7
+ # <plan-dir>/.pwf-locks/phase-status.lock namespace. Lock ownership is the
8
+ # atomic no-clobber creation of its .owner file; the directory's mkdir exit
9
+ # status is only provisional because some Windows-native implementations can
10
+ # report success to two simultaneous creators. The plan rewrite still uses an
11
+ # atomic temp-file + mv swap so a torn write can never leave a half-rewritten
12
+ # plan on disk (architecture C4).
10
13
  #
11
14
  # Note: editing task_plan.md changes its SHA, so the orchestrator must
12
15
  # re-attest at phase boundaries (see attest-plan.sh).
@@ -106,6 +109,22 @@ release_lock() {
106
109
  LOCK_ACQUIRED=0
107
110
  }
108
111
 
112
+ claim_lock_owner() {
113
+ owner_file="${LOCK_DIR}/.owner"
114
+ # POSIX noclobber makes this redirection an exclusive create. This is the
115
+ # actual ownership decision: a Windows-native mkdir may report success
116
+ # after another process won the directory-creation race (issue #281).
117
+ if (
118
+ umask 077
119
+ set -C
120
+ printf '%s\n' "${LOCK_TOKEN}" > "${owner_file}"
121
+ ) 2>/dev/null; then
122
+ LOCK_ACQUIRED=1
123
+ return 0
124
+ fi
125
+ return 1
126
+ }
127
+
109
128
  acquire_lock() {
110
129
  mkdir -p "${LOCK_ROOT}" 2>/dev/null || {
111
130
  printf "[phase-status] Cannot create lock root %s.\n" "${LOCK_ROOT}" >&2
@@ -114,7 +133,13 @@ acquire_lock() {
114
133
  LOCK_TOKEN="phase-status-$$-$(date +%s 2>/dev/null || printf 0)"
115
134
  started_at="$(date +%s 2>/dev/null || printf 0)"
116
135
  attempts=0
117
- while ! mkdir "${LOCK_DIR}" 2>/dev/null; do
136
+ while :; do
137
+ # Directory creation is intentionally idempotent. The exclusive owner
138
+ # file below, not this external utility's exit status, grants the lock.
139
+ mkdir "${LOCK_DIR}" 2>/dev/null || true
140
+ if [ -d "${LOCK_DIR}" ] && claim_lock_owner; then
141
+ return 0
142
+ fi
118
143
  attempts=$((attempts + 1))
119
144
  now="$(date +%s 2>/dev/null || printf 0)"
120
145
  if { [ "${started_at}" -gt 0 ] 2>/dev/null \
@@ -125,13 +150,6 @@ acquire_lock() {
125
150
  fi
126
151
  sleep 0.1
127
152
  done
128
- if ! printf '%s\n' "${LOCK_TOKEN}" > "${LOCK_DIR}/.owner" 2>/dev/null; then
129
- rmdir "${LOCK_DIR}" 2>/dev/null || true
130
- printf "[phase-status] Cannot record lock ownership in %s.\n" "${LOCK_DIR}" >&2
131
- return 1
132
- fi
133
- LOCK_ACQUIRED=1
134
- return 0
135
153
  }
136
154
 
137
155
  trap 'release_lock' EXIT
@@ -128,6 +128,7 @@ function Test-SafeActiveFile {
128
128
  $linked = ([string]$item.LinkType) -in @('SymbolicLink', 'Junction')
129
129
  return -not $item.PSIsContainer -and
130
130
  ($AllowLink -or -not $linked) -and
131
+ ($AllowLink -or -not $item.IsReadOnly) -and
131
132
  (Test-WithinRoot $ActiveFile)
132
133
  }
133
134
 
@@ -316,10 +317,23 @@ try {
316
317
  $stream.Write($bytes, 0, $bytes.Length)
317
318
  $stream.Flush()
318
319
  } finally { $stream.Dispose() }
319
- if ($activeItem) {
320
- [IO.File]::Replace($tempFile, $ActiveFile, [NullString]::Value)
321
- } else {
322
- [IO.File]::Move($tempFile, $ActiveFile)
320
+ $maxAttempts = 20
321
+ for ($attempt = 1; $attempt -le $maxAttempts; $attempt++) {
322
+ try {
323
+ $currentActive = Get-Item -LiteralPath $ActiveFile -Force -ErrorAction SilentlyContinue
324
+ if ($currentActive) {
325
+ if (-not (Test-SafeActiveFile)) {
326
+ throw [InvalidOperationException]::new("the active plan pointer became unsafe during replacement")
327
+ }
328
+ [IO.File]::Replace($tempFile, $ActiveFile, [NullString]::Value)
329
+ } else {
330
+ [IO.File]::Move($tempFile, $ActiveFile)
331
+ }
332
+ break
333
+ } catch [IO.IOException] {
334
+ if ($attempt -eq $maxAttempts) { throw }
335
+ Start-Sleep -Milliseconds 25
336
+ }
323
337
  }
324
338
  } catch {
325
339
  Write-Error "Error: could not set the active plan pointer: $($_.Exception.Message)"