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/README.md +858 -132
- package/SKILL.md +11 -9
- package/package.json +5 -5
- package/scripts/init-session.ps1 +51 -6
- package/scripts/phase-status.ps1 +38 -12
- package/scripts/phase-status.sh +29 -11
- package/scripts/set-active-plan.ps1 +18 -4
package/SKILL.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
name:
|
|
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
|
|
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
|
-
|
|
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
|
|
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.
|
|
4
|
-
"description": "
|
|
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
|
-
"
|
|
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": [
|
package/scripts/init-session.ps1
CHANGED
|
@@ -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
|
-
$
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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 (
|
|
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
|
}
|
package/scripts/phase-status.ps1
CHANGED
|
@@ -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
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
#
|
|
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
|
package/scripts/phase-status.sh
CHANGED
|
@@ -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
|
|
8
|
-
#
|
|
9
|
-
#
|
|
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
|
|
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
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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)"
|