planning-with-files 3.10.2 → 3.11.2
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/extensions/planning-with-files/__tests__/plan-anchor.test.ts +228 -228
- package/extensions/planning-with-files/package.json +17 -17
- package/extensions/planning-with-files/runtime.ts +788 -788
- package/package.json +1 -1
- package/scripts/attest-plan.ps1 +137 -137
- package/scripts/attest-plan.sh +206 -206
- package/scripts/check-complete.ps1 +253 -253
- package/scripts/check-complete.sh +253 -253
- package/scripts/gate-stop.sh +32 -32
- package/scripts/ledger-append.ps1 +180 -180
- package/scripts/ledger-append.sh +337 -337
- package/scripts/ledger-summary.ps1 +128 -128
- package/scripts/phase-status.ps1 +175 -175
- package/scripts/phase-status.sh +158 -158
- package/scripts/session-catchup.py +876 -876
- package/scripts/set-active-plan.ps1 +51 -51
- package/scripts/set-active-plan.sh +50 -50
|
@@ -1,128 +1,128 @@
|
|
|
1
|
-
#requires -Version 5.0
|
|
2
|
-
<#
|
|
3
|
-
.SYNOPSIS
|
|
4
|
-
Emit a fixed-shape, cache-stable run-ledger summary (PowerShell mirror, v3).
|
|
5
|
-
|
|
6
|
-
.DESCRIPTION
|
|
7
|
-
Replaces raw progress.md tail injection in autonomous mode. Output is
|
|
8
|
-
synthesized from the machine ledger and task_plan.md status counts only:
|
|
9
|
-
NO free text from disk reaches model context, and NO timestamps, so the
|
|
10
|
-
injected block is KV-cache stable by construction (architecture C3).
|
|
11
|
-
|
|
12
|
-
Plan-dir resolution matches resolve-plan-dir.ps1:
|
|
13
|
-
1. $env:PLAN_ID -> .\.planning\$PLAN_ID\
|
|
14
|
-
2. .\.planning\.active_plan
|
|
15
|
-
3. Newest .\.planning\<dir>\ by LastWriteTime
|
|
16
|
-
4. Legacy: project root
|
|
17
|
-
|
|
18
|
-
Output block (stable shape):
|
|
19
|
-
=== RUN LEDGER ===
|
|
20
|
-
entries: <N>
|
|
21
|
-
phases: <complete>/<total> complete
|
|
22
|
-
in_progress: <phase heading or none>
|
|
23
|
-
agent <name>: <last event type>
|
|
24
|
-
==================
|
|
25
|
-
#>
|
|
26
|
-
[CmdletBinding()]
|
|
27
|
-
param()
|
|
28
|
-
|
|
29
|
-
$ErrorActionPreference = "Stop"
|
|
30
|
-
|
|
31
|
-
function Resolve-PlanDir {
|
|
32
|
-
$planRoot = Join-Path (Get-Location) ".planning"
|
|
33
|
-
|
|
34
|
-
if ($env:PLAN_ID) {
|
|
35
|
-
$candidate = Join-Path $planRoot $env:PLAN_ID
|
|
36
|
-
if (Test-Path -LiteralPath $candidate -PathType Container) { return $candidate }
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
$activePointer = Join-Path $planRoot ".active_plan"
|
|
40
|
-
if (Test-Path -LiteralPath $activePointer) {
|
|
41
|
-
$planId = (Get-Content -LiteralPath $activePointer -Raw).Trim()
|
|
42
|
-
if ($planId) {
|
|
43
|
-
$candidate = Join-Path $planRoot $planId
|
|
44
|
-
if (Test-Path -LiteralPath $candidate -PathType Container) { return $candidate }
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
if (Test-Path -LiteralPath $planRoot -PathType Container) {
|
|
49
|
-
$newest = Get-ChildItem -LiteralPath $planRoot -Directory -ErrorAction SilentlyContinue |
|
|
50
|
-
Where-Object { -not $_.Name.StartsWith(".") } |
|
|
51
|
-
Where-Object { Test-Path -LiteralPath (Join-Path $_.FullName "task_plan.md") } |
|
|
52
|
-
Sort-Object LastWriteTime -Descending |
|
|
53
|
-
Select-Object -First 1
|
|
54
|
-
if ($newest) { return $newest.FullName }
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
return (Get-Location).Path
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
$planDir = Resolve-PlanDir
|
|
61
|
-
$planFile = Join-Path $planDir "task_plan.md"
|
|
62
|
-
|
|
63
|
-
# --- Phase counts: same patterns as check-complete.ps1 ---
|
|
64
|
-
$TOTAL = 0
|
|
65
|
-
$COMPLETE = 0
|
|
66
|
-
$IN_PROGRESS = 0
|
|
67
|
-
$inProgressHeading = "none"
|
|
68
|
-
|
|
69
|
-
if (Test-Path -LiteralPath $planFile) {
|
|
70
|
-
$content = Get-Content -LiteralPath $planFile -Raw
|
|
71
|
-
$TOTAL = ([regex]::Matches($content, "### Phase")).Count
|
|
72
|
-
$COMPLETE = ([regex]::Matches($content, "\*\*Status:\*\* complete")).Count
|
|
73
|
-
$IN_PROGRESS = ([regex]::Matches($content, "\*\*Status:\*\* in_progress")).Count
|
|
74
|
-
|
|
75
|
-
if ($COMPLETE -eq 0 -and $IN_PROGRESS -eq 0) {
|
|
76
|
-
$c2 = ([regex]::Matches($content, "\[complete\]")).Count
|
|
77
|
-
$i2 = ([regex]::Matches($content, "\[in_progress\]")).Count
|
|
78
|
-
if ($c2 -gt 0 -or $i2 -gt 0) {
|
|
79
|
-
$COMPLETE = $c2
|
|
80
|
-
$IN_PROGRESS = $i2
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
# Heading of the first phase block whose status is in_progress.
|
|
85
|
-
$heading = ""
|
|
86
|
-
foreach ($line in (Get-Content -LiteralPath $planFile)) {
|
|
87
|
-
if ($line -match "^### Phase") {
|
|
88
|
-
$heading = $line
|
|
89
|
-
} elseif ($line -match "\*\*Status:\*\* in_progress" -or $line -match "\[in_progress\]") {
|
|
90
|
-
if ($heading) {
|
|
91
|
-
$inProgressHeading = $heading
|
|
92
|
-
break
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
# --- Ledger stats ---
|
|
99
|
-
$totalEntries = 0
|
|
100
|
-
$ledgerFiles = Get-ChildItem -LiteralPath $planDir -Filter "ledger-*.jsonl" -File -ErrorAction SilentlyContinue
|
|
101
|
-
foreach ($f in $ledgerFiles) {
|
|
102
|
-
$lines = Get-Content -LiteralPath $f.FullName -ErrorAction SilentlyContinue
|
|
103
|
-
foreach ($line in $lines) {
|
|
104
|
-
if ($line -match '"tick"') { $totalEntries++ }
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
Write-Output "=== RUN LEDGER ==="
|
|
109
|
-
Write-Output ("entries: " + $totalEntries)
|
|
110
|
-
Write-Output ("phases: " + $COMPLETE + "/" + $TOTAL + " complete")
|
|
111
|
-
Write-Output ("in_progress: " + $inProgressHeading)
|
|
112
|
-
|
|
113
|
-
foreach ($f in $ledgerFiles) {
|
|
114
|
-
$agent = $f.Name -replace '^ledger-', '' -replace '\.jsonl$', ''
|
|
115
|
-
# @(...) forces array semantics: a single-line file returns a string from
|
|
116
|
-
# Get-Content and $lines[-1] would otherwise index the last character.
|
|
117
|
-
$lines = @(Get-Content -LiteralPath $f.FullName -ErrorAction SilentlyContinue)
|
|
118
|
-
$lastEvent = "none"
|
|
119
|
-
if ($lines.Count -gt 0) {
|
|
120
|
-
$lastLine = $lines[$lines.Count - 1]
|
|
121
|
-
$m = [regex]::Match($lastLine, '"event"\s*:\s*"([A-Za-z_]+)"')
|
|
122
|
-
if ($m.Success) { $lastEvent = $m.Groups[1].Value }
|
|
123
|
-
}
|
|
124
|
-
Write-Output ("agent " + $agent + ": " + $lastEvent)
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
Write-Output "=================="
|
|
128
|
-
exit 0
|
|
1
|
+
#requires -Version 5.0
|
|
2
|
+
<#
|
|
3
|
+
.SYNOPSIS
|
|
4
|
+
Emit a fixed-shape, cache-stable run-ledger summary (PowerShell mirror, v3).
|
|
5
|
+
|
|
6
|
+
.DESCRIPTION
|
|
7
|
+
Replaces raw progress.md tail injection in autonomous mode. Output is
|
|
8
|
+
synthesized from the machine ledger and task_plan.md status counts only:
|
|
9
|
+
NO free text from disk reaches model context, and NO timestamps, so the
|
|
10
|
+
injected block is KV-cache stable by construction (architecture C3).
|
|
11
|
+
|
|
12
|
+
Plan-dir resolution matches resolve-plan-dir.ps1:
|
|
13
|
+
1. $env:PLAN_ID -> .\.planning\$PLAN_ID\
|
|
14
|
+
2. .\.planning\.active_plan
|
|
15
|
+
3. Newest .\.planning\<dir>\ by LastWriteTime
|
|
16
|
+
4. Legacy: project root
|
|
17
|
+
|
|
18
|
+
Output block (stable shape):
|
|
19
|
+
=== RUN LEDGER ===
|
|
20
|
+
entries: <N>
|
|
21
|
+
phases: <complete>/<total> complete
|
|
22
|
+
in_progress: <phase heading or none>
|
|
23
|
+
agent <name>: <last event type>
|
|
24
|
+
==================
|
|
25
|
+
#>
|
|
26
|
+
[CmdletBinding()]
|
|
27
|
+
param()
|
|
28
|
+
|
|
29
|
+
$ErrorActionPreference = "Stop"
|
|
30
|
+
|
|
31
|
+
function Resolve-PlanDir {
|
|
32
|
+
$planRoot = Join-Path (Get-Location) ".planning"
|
|
33
|
+
|
|
34
|
+
if ($env:PLAN_ID) {
|
|
35
|
+
$candidate = Join-Path $planRoot $env:PLAN_ID
|
|
36
|
+
if (Test-Path -LiteralPath $candidate -PathType Container) { return $candidate }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
$activePointer = Join-Path $planRoot ".active_plan"
|
|
40
|
+
if (Test-Path -LiteralPath $activePointer) {
|
|
41
|
+
$planId = (Get-Content -LiteralPath $activePointer -Raw).Trim()
|
|
42
|
+
if ($planId) {
|
|
43
|
+
$candidate = Join-Path $planRoot $planId
|
|
44
|
+
if (Test-Path -LiteralPath $candidate -PathType Container) { return $candidate }
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (Test-Path -LiteralPath $planRoot -PathType Container) {
|
|
49
|
+
$newest = Get-ChildItem -LiteralPath $planRoot -Directory -ErrorAction SilentlyContinue |
|
|
50
|
+
Where-Object { -not $_.Name.StartsWith(".") } |
|
|
51
|
+
Where-Object { Test-Path -LiteralPath (Join-Path $_.FullName "task_plan.md") } |
|
|
52
|
+
Sort-Object LastWriteTime -Descending |
|
|
53
|
+
Select-Object -First 1
|
|
54
|
+
if ($newest) { return $newest.FullName }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return (Get-Location).Path
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
$planDir = Resolve-PlanDir
|
|
61
|
+
$planFile = Join-Path $planDir "task_plan.md"
|
|
62
|
+
|
|
63
|
+
# --- Phase counts: same patterns as check-complete.ps1 ---
|
|
64
|
+
$TOTAL = 0
|
|
65
|
+
$COMPLETE = 0
|
|
66
|
+
$IN_PROGRESS = 0
|
|
67
|
+
$inProgressHeading = "none"
|
|
68
|
+
|
|
69
|
+
if (Test-Path -LiteralPath $planFile) {
|
|
70
|
+
$content = Get-Content -LiteralPath $planFile -Raw
|
|
71
|
+
$TOTAL = ([regex]::Matches($content, "### Phase")).Count
|
|
72
|
+
$COMPLETE = ([regex]::Matches($content, "\*\*Status:\*\* complete")).Count
|
|
73
|
+
$IN_PROGRESS = ([regex]::Matches($content, "\*\*Status:\*\* in_progress")).Count
|
|
74
|
+
|
|
75
|
+
if ($COMPLETE -eq 0 -and $IN_PROGRESS -eq 0) {
|
|
76
|
+
$c2 = ([regex]::Matches($content, "\[complete\]")).Count
|
|
77
|
+
$i2 = ([regex]::Matches($content, "\[in_progress\]")).Count
|
|
78
|
+
if ($c2 -gt 0 -or $i2 -gt 0) {
|
|
79
|
+
$COMPLETE = $c2
|
|
80
|
+
$IN_PROGRESS = $i2
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
# Heading of the first phase block whose status is in_progress.
|
|
85
|
+
$heading = ""
|
|
86
|
+
foreach ($line in (Get-Content -LiteralPath $planFile)) {
|
|
87
|
+
if ($line -match "^### Phase") {
|
|
88
|
+
$heading = $line
|
|
89
|
+
} elseif ($line -match "\*\*Status:\*\* in_progress" -or $line -match "\[in_progress\]") {
|
|
90
|
+
if ($heading) {
|
|
91
|
+
$inProgressHeading = $heading
|
|
92
|
+
break
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
# --- Ledger stats ---
|
|
99
|
+
$totalEntries = 0
|
|
100
|
+
$ledgerFiles = Get-ChildItem -LiteralPath $planDir -Filter "ledger-*.jsonl" -File -ErrorAction SilentlyContinue
|
|
101
|
+
foreach ($f in $ledgerFiles) {
|
|
102
|
+
$lines = Get-Content -LiteralPath $f.FullName -ErrorAction SilentlyContinue
|
|
103
|
+
foreach ($line in $lines) {
|
|
104
|
+
if ($line -match '"tick"') { $totalEntries++ }
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
Write-Output "=== RUN LEDGER ==="
|
|
109
|
+
Write-Output ("entries: " + $totalEntries)
|
|
110
|
+
Write-Output ("phases: " + $COMPLETE + "/" + $TOTAL + " complete")
|
|
111
|
+
Write-Output ("in_progress: " + $inProgressHeading)
|
|
112
|
+
|
|
113
|
+
foreach ($f in $ledgerFiles) {
|
|
114
|
+
$agent = $f.Name -replace '^ledger-', '' -replace '\.jsonl$', ''
|
|
115
|
+
# @(...) forces array semantics: a single-line file returns a string from
|
|
116
|
+
# Get-Content and $lines[-1] would otherwise index the last character.
|
|
117
|
+
$lines = @(Get-Content -LiteralPath $f.FullName -ErrorAction SilentlyContinue)
|
|
118
|
+
$lastEvent = "none"
|
|
119
|
+
if ($lines.Count -gt 0) {
|
|
120
|
+
$lastLine = $lines[$lines.Count - 1]
|
|
121
|
+
$m = [regex]::Match($lastLine, '"event"\s*:\s*"([A-Za-z_]+)"')
|
|
122
|
+
if ($m.Success) { $lastEvent = $m.Groups[1].Value }
|
|
123
|
+
}
|
|
124
|
+
Write-Output ("agent " + $agent + ": " + $lastEvent)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
Write-Output "=================="
|
|
128
|
+
exit 0
|
package/scripts/phase-status.ps1
CHANGED
|
@@ -1,175 +1,175 @@
|
|
|
1
|
-
#requires -Version 5.0
|
|
2
|
-
<#
|
|
3
|
-
.SYNOPSIS
|
|
4
|
-
Set the status of one phase in task_plan.md (PowerShell mirror, v3).
|
|
5
|
-
|
|
6
|
-
.DESCRIPTION
|
|
7
|
-
The ONLY sanctioned concurrent-safe writer of task_plan.md status lines. The
|
|
8
|
-
orchestrator owns task_plan.md; workers NEVER edit it directly. The edit is
|
|
9
|
-
a read-modify-write under an exclusive lock on the <plan-dir>\.write_lock
|
|
10
|
-
sentinel, with an atomic temp-file + move swap so a torn write can never
|
|
11
|
-
leave a half-rewritten plan on disk (architecture C4).
|
|
12
|
-
|
|
13
|
-
Editing task_plan.md changes its SHA, so the orchestrator must re-attest at
|
|
14
|
-
phase boundaries (see attest-plan.ps1).
|
|
15
|
-
|
|
16
|
-
Plan-dir resolution matches resolve-plan-dir.ps1:
|
|
17
|
-
1. $env:PLAN_ID -> .\.planning\$PLAN_ID\
|
|
18
|
-
2. .\.planning\.active_plan
|
|
19
|
-
3. Newest .\.planning\<dir>\ by LastWriteTime
|
|
20
|
-
4. Legacy: project root .\task_plan.md
|
|
21
|
-
|
|
22
|
-
Exits 1 with a message if the phase does not exist or the status is invalid.
|
|
23
|
-
|
|
24
|
-
.PARAMETER Phase
|
|
25
|
-
Phase number (positive integer).
|
|
26
|
-
|
|
27
|
-
.PARAMETER Status
|
|
28
|
-
New status: pending, in_progress, or complete.
|
|
29
|
-
#>
|
|
30
|
-
[CmdletBinding()]
|
|
31
|
-
param(
|
|
32
|
-
[Parameter(Mandatory = $true, Position = 0)]
|
|
33
|
-
[string] $Phase,
|
|
34
|
-
|
|
35
|
-
[Parameter(Mandatory = $true, Position = 1)]
|
|
36
|
-
[string] $Status
|
|
37
|
-
)
|
|
38
|
-
|
|
39
|
-
$ErrorActionPreference = "Stop"
|
|
40
|
-
|
|
41
|
-
function Resolve-PlanFile {
|
|
42
|
-
$planRoot = Join-Path (Get-Location) ".planning"
|
|
43
|
-
|
|
44
|
-
if ($env:PLAN_ID) {
|
|
45
|
-
$candidate = Join-Path $planRoot $env:PLAN_ID
|
|
46
|
-
$planFile = Join-Path $candidate "task_plan.md"
|
|
47
|
-
if (Test-Path -LiteralPath $planFile) { return (Resolve-Path -LiteralPath $planFile).Path }
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
$activePointer = Join-Path $planRoot ".active_plan"
|
|
51
|
-
if (Test-Path -LiteralPath $activePointer) {
|
|
52
|
-
$planId = (Get-Content -LiteralPath $activePointer -Raw).Trim()
|
|
53
|
-
if ($planId) {
|
|
54
|
-
$candidate = Join-Path $planRoot $planId
|
|
55
|
-
$planFile = Join-Path $candidate "task_plan.md"
|
|
56
|
-
if (Test-Path -LiteralPath $planFile) { return (Resolve-Path -LiteralPath $planFile).Path }
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
if (Test-Path -LiteralPath $planRoot -PathType Container) {
|
|
61
|
-
$newest = Get-ChildItem -LiteralPath $planRoot -Directory -ErrorAction SilentlyContinue |
|
|
62
|
-
Where-Object { -not $_.Name.StartsWith(".") } |
|
|
63
|
-
Where-Object { Test-Path -LiteralPath (Join-Path $_.FullName "task_plan.md") } |
|
|
64
|
-
Sort-Object LastWriteTime -Descending |
|
|
65
|
-
Select-Object -First 1
|
|
66
|
-
if ($newest) {
|
|
67
|
-
return (Resolve-Path -LiteralPath (Join-Path $newest.FullName "task_plan.md")).Path
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
$legacy = Join-Path (Get-Location) "task_plan.md"
|
|
72
|
-
if (Test-Path -LiteralPath $legacy) {
|
|
73
|
-
return (Resolve-Path -LiteralPath $legacy).Path
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
return $null
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
# Validate phase number is a positive integer.
|
|
80
|
-
if ($Phase -notmatch '^[0-9]+$') {
|
|
81
|
-
Write-Error ("[phase-status] phase number must be a positive integer, got '" + $Phase + "'.")
|
|
82
|
-
exit 1
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
# Validate status value against the allowlist.
|
|
86
|
-
$validStatus = @("pending", "in_progress", "complete")
|
|
87
|
-
if ($validStatus -notcontains $Status) {
|
|
88
|
-
Write-Error ("[phase-status] invalid status '" + $Status + "' (allowed: pending, in_progress, complete).")
|
|
89
|
-
exit 1
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
$planFile = Resolve-PlanFile
|
|
93
|
-
if (-not $planFile) {
|
|
94
|
-
Write-Error "[phase-status] No task_plan.md found. Create a plan first."
|
|
95
|
-
exit 1
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
$planDir = Split-Path -Parent $planFile
|
|
99
|
-
$lockFile = Join-Path $planDir ".write_lock"
|
|
100
|
-
|
|
101
|
-
# Acquire an exclusive lock on the sentinel so concurrent writers serialize.
|
|
102
|
-
$fs = $null
|
|
103
|
-
$acquired = $false
|
|
104
|
-
for ($i = 0; $i -lt 50 -and -not $acquired; $i++) {
|
|
105
|
-
try {
|
|
106
|
-
$fs = [System.IO.File]::Open($lockFile, [System.IO.FileMode]::OpenOrCreate, [System.IO.FileAccess]::ReadWrite, [System.IO.FileShare]::None)
|
|
107
|
-
$acquired = $true
|
|
108
|
-
} catch {
|
|
109
|
-
Start-Sleep -Milliseconds 100
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
$tmpFile = $planFile + ".tmp." + $PID
|
|
114
|
-
$rc = 0
|
|
115
|
-
try {
|
|
116
|
-
$lines = Get-Content -LiteralPath $planFile
|
|
117
|
-
|
|
118
|
-
# Confirm the phase heading exists.
|
|
119
|
-
$headingRe = '^### Phase ' + $Phase + '([^0-9]|$)'
|
|
120
|
-
if (-not ($lines | Where-Object { $_ -match $headingRe })) {
|
|
121
|
-
Write-Error ("[phase-status] Phase " + $Phase + " not found in " + $planFile + ".")
|
|
122
|
-
$rc = 1
|
|
123
|
-
} else {
|
|
124
|
-
$inBlock = $false
|
|
125
|
-
$done = $false
|
|
126
|
-
$out = New-Object System.Collections.Generic.List[string]
|
|
127
|
-
foreach ($line in $lines) {
|
|
128
|
-
$emit = $line
|
|
129
|
-
if ($line -match '^### Phase ') {
|
|
130
|
-
$rest = $line -replace '^### Phase ', ''
|
|
131
|
-
$num = $rest -replace '[^0-9].*$', ''
|
|
132
|
-
if (($num -eq $Phase) -and (-not $done)) {
|
|
133
|
-
$inBlock = $true
|
|
134
|
-
} else {
|
|
135
|
-
$inBlock = $false
|
|
136
|
-
}
|
|
137
|
-
} elseif ($inBlock -and (-not $done) -and ($line -match '\*\*Status:\*\*')) {
|
|
138
|
-
$prefix = $line -replace '\*\*Status:\*\*.*$', ''
|
|
139
|
-
$emit = $prefix + '**Status:** ' + $Status
|
|
140
|
-
$inBlock = $false
|
|
141
|
-
$done = $true
|
|
142
|
-
}
|
|
143
|
-
$out.Add($emit)
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
if (-not $done) {
|
|
147
|
-
Write-Error ("[phase-status] No **Status:** line found for Phase " + $Phase + ".")
|
|
148
|
-
$rc = 1
|
|
149
|
-
} else {
|
|
150
|
-
# Atomic-enough swap: write temp, then move over the target.
|
|
151
|
-
# Write BOM-less UTF-8 (platform-major): Set-Content -Encoding utf8 on
|
|
152
|
-
# Windows PowerShell 5.1 prepends a UTF-8 BOM (EF BB BF). The temp file
|
|
153
|
-
# then replaces task_plan.md, so every phase-status call from PS 5.1
|
|
154
|
-
# changes the file's leading bytes. If the plan was created on Linux or
|
|
155
|
-
# macOS (no BOM), the stored attestation SHA-256 no longer matches and
|
|
156
|
-
# inject-plan.sh blocks all further injection as [PLAN TAMPERED]. A
|
|
157
|
-
# UTF8Encoding constructed with $false emits no BOM on every PS version.
|
|
158
|
-
$utf8NoBom = New-Object System.Text.UTF8Encoding($false)
|
|
159
|
-
[System.IO.File]::WriteAllLines($tmpFile, $out, $utf8NoBom)
|
|
160
|
-
Move-Item -LiteralPath $tmpFile -Destination $planFile -Force
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
} catch {
|
|
164
|
-
Write-Error ("[phase-status] " + $_.Exception.Message)
|
|
165
|
-
$rc = 1
|
|
166
|
-
} finally {
|
|
167
|
-
if ($fs) { $fs.Close(); $fs.Dispose() }
|
|
168
|
-
if (Test-Path -LiteralPath $lockFile) { Remove-Item -LiteralPath $lockFile -Force -ErrorAction SilentlyContinue }
|
|
169
|
-
if (Test-Path -LiteralPath $tmpFile) { Remove-Item -LiteralPath $tmpFile -Force -ErrorAction SilentlyContinue }
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
if ($rc -ne 0) { exit 1 }
|
|
173
|
-
|
|
174
|
-
Write-Output ("[phase-status] Phase " + $Phase + " -> " + $Status + " in " + $planFile)
|
|
175
|
-
exit 0
|
|
1
|
+
#requires -Version 5.0
|
|
2
|
+
<#
|
|
3
|
+
.SYNOPSIS
|
|
4
|
+
Set the status of one phase in task_plan.md (PowerShell mirror, v3).
|
|
5
|
+
|
|
6
|
+
.DESCRIPTION
|
|
7
|
+
The ONLY sanctioned concurrent-safe writer of task_plan.md status lines. The
|
|
8
|
+
orchestrator owns task_plan.md; workers NEVER edit it directly. The edit is
|
|
9
|
+
a read-modify-write under an exclusive lock on the <plan-dir>\.write_lock
|
|
10
|
+
sentinel, with an atomic temp-file + move swap so a torn write can never
|
|
11
|
+
leave a half-rewritten plan on disk (architecture C4).
|
|
12
|
+
|
|
13
|
+
Editing task_plan.md changes its SHA, so the orchestrator must re-attest at
|
|
14
|
+
phase boundaries (see attest-plan.ps1).
|
|
15
|
+
|
|
16
|
+
Plan-dir resolution matches resolve-plan-dir.ps1:
|
|
17
|
+
1. $env:PLAN_ID -> .\.planning\$PLAN_ID\
|
|
18
|
+
2. .\.planning\.active_plan
|
|
19
|
+
3. Newest .\.planning\<dir>\ by LastWriteTime
|
|
20
|
+
4. Legacy: project root .\task_plan.md
|
|
21
|
+
|
|
22
|
+
Exits 1 with a message if the phase does not exist or the status is invalid.
|
|
23
|
+
|
|
24
|
+
.PARAMETER Phase
|
|
25
|
+
Phase number (positive integer).
|
|
26
|
+
|
|
27
|
+
.PARAMETER Status
|
|
28
|
+
New status: pending, in_progress, or complete.
|
|
29
|
+
#>
|
|
30
|
+
[CmdletBinding()]
|
|
31
|
+
param(
|
|
32
|
+
[Parameter(Mandatory = $true, Position = 0)]
|
|
33
|
+
[string] $Phase,
|
|
34
|
+
|
|
35
|
+
[Parameter(Mandatory = $true, Position = 1)]
|
|
36
|
+
[string] $Status
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
$ErrorActionPreference = "Stop"
|
|
40
|
+
|
|
41
|
+
function Resolve-PlanFile {
|
|
42
|
+
$planRoot = Join-Path (Get-Location) ".planning"
|
|
43
|
+
|
|
44
|
+
if ($env:PLAN_ID) {
|
|
45
|
+
$candidate = Join-Path $planRoot $env:PLAN_ID
|
|
46
|
+
$planFile = Join-Path $candidate "task_plan.md"
|
|
47
|
+
if (Test-Path -LiteralPath $planFile) { return (Resolve-Path -LiteralPath $planFile).Path }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
$activePointer = Join-Path $planRoot ".active_plan"
|
|
51
|
+
if (Test-Path -LiteralPath $activePointer) {
|
|
52
|
+
$planId = (Get-Content -LiteralPath $activePointer -Raw).Trim()
|
|
53
|
+
if ($planId) {
|
|
54
|
+
$candidate = Join-Path $planRoot $planId
|
|
55
|
+
$planFile = Join-Path $candidate "task_plan.md"
|
|
56
|
+
if (Test-Path -LiteralPath $planFile) { return (Resolve-Path -LiteralPath $planFile).Path }
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (Test-Path -LiteralPath $planRoot -PathType Container) {
|
|
61
|
+
$newest = Get-ChildItem -LiteralPath $planRoot -Directory -ErrorAction SilentlyContinue |
|
|
62
|
+
Where-Object { -not $_.Name.StartsWith(".") } |
|
|
63
|
+
Where-Object { Test-Path -LiteralPath (Join-Path $_.FullName "task_plan.md") } |
|
|
64
|
+
Sort-Object LastWriteTime -Descending |
|
|
65
|
+
Select-Object -First 1
|
|
66
|
+
if ($newest) {
|
|
67
|
+
return (Resolve-Path -LiteralPath (Join-Path $newest.FullName "task_plan.md")).Path
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
$legacy = Join-Path (Get-Location) "task_plan.md"
|
|
72
|
+
if (Test-Path -LiteralPath $legacy) {
|
|
73
|
+
return (Resolve-Path -LiteralPath $legacy).Path
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return $null
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
# Validate phase number is a positive integer.
|
|
80
|
+
if ($Phase -notmatch '^[0-9]+$') {
|
|
81
|
+
Write-Error ("[phase-status] phase number must be a positive integer, got '" + $Phase + "'.")
|
|
82
|
+
exit 1
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
# Validate status value against the allowlist.
|
|
86
|
+
$validStatus = @("pending", "in_progress", "complete")
|
|
87
|
+
if ($validStatus -notcontains $Status) {
|
|
88
|
+
Write-Error ("[phase-status] invalid status '" + $Status + "' (allowed: pending, in_progress, complete).")
|
|
89
|
+
exit 1
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
$planFile = Resolve-PlanFile
|
|
93
|
+
if (-not $planFile) {
|
|
94
|
+
Write-Error "[phase-status] No task_plan.md found. Create a plan first."
|
|
95
|
+
exit 1
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
$planDir = Split-Path -Parent $planFile
|
|
99
|
+
$lockFile = Join-Path $planDir ".write_lock"
|
|
100
|
+
|
|
101
|
+
# Acquire an exclusive lock on the sentinel so concurrent writers serialize.
|
|
102
|
+
$fs = $null
|
|
103
|
+
$acquired = $false
|
|
104
|
+
for ($i = 0; $i -lt 50 -and -not $acquired; $i++) {
|
|
105
|
+
try {
|
|
106
|
+
$fs = [System.IO.File]::Open($lockFile, [System.IO.FileMode]::OpenOrCreate, [System.IO.FileAccess]::ReadWrite, [System.IO.FileShare]::None)
|
|
107
|
+
$acquired = $true
|
|
108
|
+
} catch {
|
|
109
|
+
Start-Sleep -Milliseconds 100
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
$tmpFile = $planFile + ".tmp." + $PID
|
|
114
|
+
$rc = 0
|
|
115
|
+
try {
|
|
116
|
+
$lines = Get-Content -LiteralPath $planFile
|
|
117
|
+
|
|
118
|
+
# Confirm the phase heading exists.
|
|
119
|
+
$headingRe = '^### Phase ' + $Phase + '([^0-9]|$)'
|
|
120
|
+
if (-not ($lines | Where-Object { $_ -match $headingRe })) {
|
|
121
|
+
Write-Error ("[phase-status] Phase " + $Phase + " not found in " + $planFile + ".")
|
|
122
|
+
$rc = 1
|
|
123
|
+
} else {
|
|
124
|
+
$inBlock = $false
|
|
125
|
+
$done = $false
|
|
126
|
+
$out = New-Object System.Collections.Generic.List[string]
|
|
127
|
+
foreach ($line in $lines) {
|
|
128
|
+
$emit = $line
|
|
129
|
+
if ($line -match '^### Phase ') {
|
|
130
|
+
$rest = $line -replace '^### Phase ', ''
|
|
131
|
+
$num = $rest -replace '[^0-9].*$', ''
|
|
132
|
+
if (($num -eq $Phase) -and (-not $done)) {
|
|
133
|
+
$inBlock = $true
|
|
134
|
+
} else {
|
|
135
|
+
$inBlock = $false
|
|
136
|
+
}
|
|
137
|
+
} elseif ($inBlock -and (-not $done) -and ($line -match '\*\*Status:\*\*')) {
|
|
138
|
+
$prefix = $line -replace '\*\*Status:\*\*.*$', ''
|
|
139
|
+
$emit = $prefix + '**Status:** ' + $Status
|
|
140
|
+
$inBlock = $false
|
|
141
|
+
$done = $true
|
|
142
|
+
}
|
|
143
|
+
$out.Add($emit)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (-not $done) {
|
|
147
|
+
Write-Error ("[phase-status] No **Status:** line found for Phase " + $Phase + ".")
|
|
148
|
+
$rc = 1
|
|
149
|
+
} else {
|
|
150
|
+
# Atomic-enough swap: write temp, then move over the target.
|
|
151
|
+
# Write BOM-less UTF-8 (platform-major): Set-Content -Encoding utf8 on
|
|
152
|
+
# Windows PowerShell 5.1 prepends a UTF-8 BOM (EF BB BF). The temp file
|
|
153
|
+
# then replaces task_plan.md, so every phase-status call from PS 5.1
|
|
154
|
+
# changes the file's leading bytes. If the plan was created on Linux or
|
|
155
|
+
# macOS (no BOM), the stored attestation SHA-256 no longer matches and
|
|
156
|
+
# inject-plan.sh blocks all further injection as [PLAN TAMPERED]. A
|
|
157
|
+
# UTF8Encoding constructed with $false emits no BOM on every PS version.
|
|
158
|
+
$utf8NoBom = New-Object System.Text.UTF8Encoding($false)
|
|
159
|
+
[System.IO.File]::WriteAllLines($tmpFile, $out, $utf8NoBom)
|
|
160
|
+
Move-Item -LiteralPath $tmpFile -Destination $planFile -Force
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
} catch {
|
|
164
|
+
Write-Error ("[phase-status] " + $_.Exception.Message)
|
|
165
|
+
$rc = 1
|
|
166
|
+
} finally {
|
|
167
|
+
if ($fs) { $fs.Close(); $fs.Dispose() }
|
|
168
|
+
if (Test-Path -LiteralPath $lockFile) { Remove-Item -LiteralPath $lockFile -Force -ErrorAction SilentlyContinue }
|
|
169
|
+
if (Test-Path -LiteralPath $tmpFile) { Remove-Item -LiteralPath $tmpFile -Force -ErrorAction SilentlyContinue }
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if ($rc -ne 0) { exit 1 }
|
|
173
|
+
|
|
174
|
+
Write-Output ("[phase-status] Phase " + $Phase + " -> " + $Status + " in " + $planFile)
|
|
175
|
+
exit 0
|