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.
@@ -1,180 +1,180 @@
1
- #requires -Version 5.0
2
- <#
3
- .SYNOPSIS
4
- Append one structured entry to the run-ledger (PowerShell mirror, v3).
5
-
6
- .DESCRIPTION
7
- The run-ledger is the machine layer of progress tracking: an append-only
8
- JSON-lines file per agent under the active plan dir. Workers append here;
9
- the orchestrator owns progress.md and task_plan.md. See architecture C3.
10
-
11
- Plan-dir resolution (matches resolve-plan-dir.ps1):
12
- 1. $env:PLAN_ID -> .\.planning\$PLAN_ID\
13
- 2. .\.planning\.active_plan
14
- 3. Newest .\.planning\<dir>\ by LastWriteTime
15
- 4. Legacy: project root (ledger lands beside .\task_plan.md)
16
-
17
- Writes ONE JSON line to <plan-dir>\ledger-<agent>.jsonl. tick = 1 + max tick
18
- across ALL ledger-*.jsonl in the plan dir so concurrent agents share a
19
- monotonic counter.
20
-
21
- .PARAMETER Event
22
- One of: progress phase_complete error gate_block attest note.
23
-
24
- .PARAMETER Summary
25
- Free text, truncated to 200 chars, newlines stripped.
26
-
27
- .PARAMETER Agent
28
- Ledger owner (default "main"); sanitized to [A-Za-z0-9_-].
29
-
30
- .PARAMETER Phase
31
- Phase number/name this entry concerns.
32
-
33
- .PARAMETER Files
34
- Comma-separated file list recorded as a JSON array.
35
- #>
36
- [CmdletBinding()]
37
- param(
38
- [Parameter(Mandatory = $true, Position = 0)]
39
- [string] $Event,
40
-
41
- [Parameter(Mandatory = $true, Position = 1)]
42
- [string] $Summary,
43
-
44
- [string] $Agent = "main",
45
-
46
- [string] $Phase = "",
47
-
48
- [string] $Files = ""
49
- )
50
-
51
- $ErrorActionPreference = "Stop"
52
-
53
- $validEvents = @("progress", "phase_complete", "error", "gate_block", "attest", "note")
54
-
55
- function Resolve-PlanDir {
56
- $planRoot = Join-Path (Get-Location) ".planning"
57
-
58
- if ($env:PLAN_ID) {
59
- $candidate = Join-Path $planRoot $env:PLAN_ID
60
- if (Test-Path -LiteralPath $candidate -PathType Container) { return $candidate }
61
- }
62
-
63
- $activePointer = Join-Path $planRoot ".active_plan"
64
- if (Test-Path -LiteralPath $activePointer) {
65
- $planId = (Get-Content -LiteralPath $activePointer -Raw).Trim()
66
- if ($planId) {
67
- $candidate = Join-Path $planRoot $planId
68
- if (Test-Path -LiteralPath $candidate -PathType Container) { return $candidate }
69
- }
70
- }
71
-
72
- if (Test-Path -LiteralPath $planRoot -PathType Container) {
73
- $newest = Get-ChildItem -LiteralPath $planRoot -Directory -ErrorAction SilentlyContinue |
74
- Where-Object { -not $_.Name.StartsWith(".") } |
75
- Where-Object { Test-Path -LiteralPath (Join-Path $_.FullName "task_plan.md") } |
76
- Sort-Object LastWriteTime -Descending |
77
- Select-Object -First 1
78
- if ($newest) { return $newest.FullName }
79
- }
80
-
81
- # Legacy single-file mode: ledger lives beside .\task_plan.md at root.
82
- return (Get-Location).Path
83
- }
84
-
85
- function ConvertTo-JsonString {
86
- param([string] $Value)
87
- $sb = New-Object System.Text.StringBuilder
88
- foreach ($ch in $Value.ToCharArray()) {
89
- switch ($ch) {
90
- '"' { [void]$sb.Append('\"') }
91
- '\' { [void]$sb.Append('\\') }
92
- "`n" { [void]$sb.Append(' ') }
93
- "`r" { [void]$sb.Append(' ') }
94
- "`t" { [void]$sb.Append(' ') }
95
- default {
96
- if ([int]$ch -lt 32) {
97
- [void]$sb.Append(' ')
98
- } else {
99
- [void]$sb.Append($ch)
100
- }
101
- }
102
- }
103
- }
104
- return $sb.ToString()
105
- }
106
-
107
- function Get-MaxTick {
108
- param([string] $Dir)
109
- $max = 0
110
- $pattern = '"tick"\s*:\s*(\d+)'
111
- Get-ChildItem -LiteralPath $Dir -Filter "ledger-*.jsonl" -File -ErrorAction SilentlyContinue | ForEach-Object {
112
- foreach ($line in (Get-Content -LiteralPath $_.FullName -ErrorAction SilentlyContinue)) {
113
- $m = [regex]::Match($line, $pattern)
114
- if ($m.Success) {
115
- $t = [int]$m.Groups[1].Value
116
- if ($t -gt $max) { $max = $t }
117
- }
118
- }
119
- }
120
- return $max
121
- }
122
-
123
- # Validate event against the allowlist.
124
- if ($validEvents -notcontains $Event) {
125
- Write-Error ("[ledger] invalid event '" + $Event + "' (allowed: " + ($validEvents -join ' ') + ")")
126
- exit 2
127
- }
128
-
129
- # Sanitize agent name to [A-Za-z0-9_-]; empty result falls back to "main".
130
- $agentClean = ($Agent -replace '[^A-Za-z0-9_-]', '')
131
- if (-not $agentClean) { $agentClean = "main" }
132
-
133
- # Truncate summary to the 200-character budget before escaping, matching the
134
- # sh twin. .NET Substring counts characters, never bytes, so multibyte input
135
- # cannot be clipped mid-codepoint here and no UTF-8 tail repair is needed.
136
- if ($Summary.Length -gt 200) { $Summary = $Summary.Substring(0, 200) }
137
-
138
- $planDir = Resolve-PlanDir
139
- $ledgerFile = Join-Path $planDir ("ledger-" + $agentClean + ".jsonl")
140
- $lockFile = Join-Path $planDir ".ledger_lock"
141
-
142
- $ts = [DateTime]::UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ")
143
-
144
- # Build the files JSON array from the comma-separated list.
145
- $filesJson = "[]"
146
- if ($Files) {
147
- $parts = $Files.Split(",") | Where-Object { $_ -ne "" }
148
- $escaped = $parts | ForEach-Object { '"' + (ConvertTo-JsonString $_) + '"' }
149
- $filesJson = "[" + ($escaped -join ",") + "]"
150
- }
151
-
152
- $summaryEsc = ConvertTo-JsonString $Summary
153
- $phaseEsc = ConvertTo-JsonString $Phase
154
-
155
- # Acquire an exclusive lock on a sidecar so concurrent appenders do not pick
156
- # the same tick number, then compute tick and append inside the locked window.
157
- # Atomic append of a single <4KB line is the real guarantee; the lock just
158
- # serializes the read-tick / write-line pair.
159
- $fs = $null
160
- $acquired = $false
161
- for ($i = 0; $i -lt 50 -and -not $acquired; $i++) {
162
- try {
163
- $fs = [System.IO.File]::Open($lockFile, [System.IO.FileMode]::OpenOrCreate, [System.IO.FileAccess]::ReadWrite, [System.IO.FileShare]::None)
164
- $acquired = $true
165
- } catch {
166
- Start-Sleep -Milliseconds 100
167
- }
168
- }
169
-
170
- try {
171
- $tick = (Get-MaxTick $planDir) + 1
172
- $line = '{"tick":' + $tick + ',"ts":"' + $ts + '","agent":"' + $agentClean + '","phase":"' + $phaseEsc + '","event":"' + $Event + '","summary":"' + $summaryEsc + '","files":' + $filesJson + '}'
173
- Add-Content -LiteralPath $ledgerFile -Value $line -Encoding utf8
174
- } finally {
175
- if ($fs) { $fs.Close(); $fs.Dispose() }
176
- if (Test-Path -LiteralPath $lockFile) { Remove-Item -LiteralPath $lockFile -Force -ErrorAction SilentlyContinue }
177
- }
178
-
179
- Write-Output ("[ledger] tick " + $tick + " -> " + $ledgerFile + " (event=" + $Event + " agent=" + $agentClean + ")")
180
- exit 0
1
+ #requires -Version 5.0
2
+ <#
3
+ .SYNOPSIS
4
+ Append one structured entry to the run-ledger (PowerShell mirror, v3).
5
+
6
+ .DESCRIPTION
7
+ The run-ledger is the machine layer of progress tracking: an append-only
8
+ JSON-lines file per agent under the active plan dir. Workers append here;
9
+ the orchestrator owns progress.md and task_plan.md. See architecture C3.
10
+
11
+ Plan-dir resolution (matches resolve-plan-dir.ps1):
12
+ 1. $env:PLAN_ID -> .\.planning\$PLAN_ID\
13
+ 2. .\.planning\.active_plan
14
+ 3. Newest .\.planning\<dir>\ by LastWriteTime
15
+ 4. Legacy: project root (ledger lands beside .\task_plan.md)
16
+
17
+ Writes ONE JSON line to <plan-dir>\ledger-<agent>.jsonl. tick = 1 + max tick
18
+ across ALL ledger-*.jsonl in the plan dir so concurrent agents share a
19
+ monotonic counter.
20
+
21
+ .PARAMETER Event
22
+ One of: progress phase_complete error gate_block attest note.
23
+
24
+ .PARAMETER Summary
25
+ Free text, truncated to 200 chars, newlines stripped.
26
+
27
+ .PARAMETER Agent
28
+ Ledger owner (default "main"); sanitized to [A-Za-z0-9_-].
29
+
30
+ .PARAMETER Phase
31
+ Phase number/name this entry concerns.
32
+
33
+ .PARAMETER Files
34
+ Comma-separated file list recorded as a JSON array.
35
+ #>
36
+ [CmdletBinding()]
37
+ param(
38
+ [Parameter(Mandatory = $true, Position = 0)]
39
+ [string] $Event,
40
+
41
+ [Parameter(Mandatory = $true, Position = 1)]
42
+ [string] $Summary,
43
+
44
+ [string] $Agent = "main",
45
+
46
+ [string] $Phase = "",
47
+
48
+ [string] $Files = ""
49
+ )
50
+
51
+ $ErrorActionPreference = "Stop"
52
+
53
+ $validEvents = @("progress", "phase_complete", "error", "gate_block", "attest", "note")
54
+
55
+ function Resolve-PlanDir {
56
+ $planRoot = Join-Path (Get-Location) ".planning"
57
+
58
+ if ($env:PLAN_ID) {
59
+ $candidate = Join-Path $planRoot $env:PLAN_ID
60
+ if (Test-Path -LiteralPath $candidate -PathType Container) { return $candidate }
61
+ }
62
+
63
+ $activePointer = Join-Path $planRoot ".active_plan"
64
+ if (Test-Path -LiteralPath $activePointer) {
65
+ $planId = (Get-Content -LiteralPath $activePointer -Raw).Trim()
66
+ if ($planId) {
67
+ $candidate = Join-Path $planRoot $planId
68
+ if (Test-Path -LiteralPath $candidate -PathType Container) { return $candidate }
69
+ }
70
+ }
71
+
72
+ if (Test-Path -LiteralPath $planRoot -PathType Container) {
73
+ $newest = Get-ChildItem -LiteralPath $planRoot -Directory -ErrorAction SilentlyContinue |
74
+ Where-Object { -not $_.Name.StartsWith(".") } |
75
+ Where-Object { Test-Path -LiteralPath (Join-Path $_.FullName "task_plan.md") } |
76
+ Sort-Object LastWriteTime -Descending |
77
+ Select-Object -First 1
78
+ if ($newest) { return $newest.FullName }
79
+ }
80
+
81
+ # Legacy single-file mode: ledger lives beside .\task_plan.md at root.
82
+ return (Get-Location).Path
83
+ }
84
+
85
+ function ConvertTo-JsonString {
86
+ param([string] $Value)
87
+ $sb = New-Object System.Text.StringBuilder
88
+ foreach ($ch in $Value.ToCharArray()) {
89
+ switch ($ch) {
90
+ '"' { [void]$sb.Append('\"') }
91
+ '\' { [void]$sb.Append('\\') }
92
+ "`n" { [void]$sb.Append(' ') }
93
+ "`r" { [void]$sb.Append(' ') }
94
+ "`t" { [void]$sb.Append(' ') }
95
+ default {
96
+ if ([int]$ch -lt 32) {
97
+ [void]$sb.Append(' ')
98
+ } else {
99
+ [void]$sb.Append($ch)
100
+ }
101
+ }
102
+ }
103
+ }
104
+ return $sb.ToString()
105
+ }
106
+
107
+ function Get-MaxTick {
108
+ param([string] $Dir)
109
+ $max = 0
110
+ $pattern = '"tick"\s*:\s*(\d+)'
111
+ Get-ChildItem -LiteralPath $Dir -Filter "ledger-*.jsonl" -File -ErrorAction SilentlyContinue | ForEach-Object {
112
+ foreach ($line in (Get-Content -LiteralPath $_.FullName -ErrorAction SilentlyContinue)) {
113
+ $m = [regex]::Match($line, $pattern)
114
+ if ($m.Success) {
115
+ $t = [int]$m.Groups[1].Value
116
+ if ($t -gt $max) { $max = $t }
117
+ }
118
+ }
119
+ }
120
+ return $max
121
+ }
122
+
123
+ # Validate event against the allowlist.
124
+ if ($validEvents -notcontains $Event) {
125
+ Write-Error ("[ledger] invalid event '" + $Event + "' (allowed: " + ($validEvents -join ' ') + ")")
126
+ exit 2
127
+ }
128
+
129
+ # Sanitize agent name to [A-Za-z0-9_-]; empty result falls back to "main".
130
+ $agentClean = ($Agent -replace '[^A-Za-z0-9_-]', '')
131
+ if (-not $agentClean) { $agentClean = "main" }
132
+
133
+ # Truncate summary to the 200-character budget before escaping, matching the
134
+ # sh twin. .NET Substring counts characters, never bytes, so multibyte input
135
+ # cannot be clipped mid-codepoint here and no UTF-8 tail repair is needed.
136
+ if ($Summary.Length -gt 200) { $Summary = $Summary.Substring(0, 200) }
137
+
138
+ $planDir = Resolve-PlanDir
139
+ $ledgerFile = Join-Path $planDir ("ledger-" + $agentClean + ".jsonl")
140
+ $lockFile = Join-Path $planDir ".ledger_lock"
141
+
142
+ $ts = [DateTime]::UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ")
143
+
144
+ # Build the files JSON array from the comma-separated list.
145
+ $filesJson = "[]"
146
+ if ($Files) {
147
+ $parts = $Files.Split(",") | Where-Object { $_ -ne "" }
148
+ $escaped = $parts | ForEach-Object { '"' + (ConvertTo-JsonString $_) + '"' }
149
+ $filesJson = "[" + ($escaped -join ",") + "]"
150
+ }
151
+
152
+ $summaryEsc = ConvertTo-JsonString $Summary
153
+ $phaseEsc = ConvertTo-JsonString $Phase
154
+
155
+ # Acquire an exclusive lock on a sidecar so concurrent appenders do not pick
156
+ # the same tick number, then compute tick and append inside the locked window.
157
+ # Atomic append of a single <4KB line is the real guarantee; the lock just
158
+ # serializes the read-tick / write-line pair.
159
+ $fs = $null
160
+ $acquired = $false
161
+ for ($i = 0; $i -lt 50 -and -not $acquired; $i++) {
162
+ try {
163
+ $fs = [System.IO.File]::Open($lockFile, [System.IO.FileMode]::OpenOrCreate, [System.IO.FileAccess]::ReadWrite, [System.IO.FileShare]::None)
164
+ $acquired = $true
165
+ } catch {
166
+ Start-Sleep -Milliseconds 100
167
+ }
168
+ }
169
+
170
+ try {
171
+ $tick = (Get-MaxTick $planDir) + 1
172
+ $line = '{"tick":' + $tick + ',"ts":"' + $ts + '","agent":"' + $agentClean + '","phase":"' + $phaseEsc + '","event":"' + $Event + '","summary":"' + $summaryEsc + '","files":' + $filesJson + '}'
173
+ Add-Content -LiteralPath $ledgerFile -Value $line -Encoding utf8
174
+ } finally {
175
+ if ($fs) { $fs.Close(); $fs.Dispose() }
176
+ if (Test-Path -LiteralPath $lockFile) { Remove-Item -LiteralPath $lockFile -Force -ErrorAction SilentlyContinue }
177
+ }
178
+
179
+ Write-Output ("[ledger] tick " + $tick + " -> " + $ledgerFile + " (event=" + $Event + " agent=" + $agentClean + ")")
180
+ exit 0