planning-with-files 3.18.3 → 3.20.5
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 +83 -83
- package/package.json +1 -1
- package/scripts/attest-plan.ps1 +10 -2
- package/scripts/init-session.ps1 +267 -26
- package/scripts/init-session.sh +85 -10
- package/scripts/inject-plan.py +17 -5
- package/scripts/inject-plan.sh +7 -3
- package/scripts/ledger-append.ps1 +1 -1
- package/scripts/ledger-summary.ps1 +1 -1
- package/scripts/phase-status.ps1 +1 -1
- package/scripts/resolve-plan-dir.ps1 +51 -12
- package/scripts/resolve-plan-dir.sh +10 -1
- package/scripts/session-catchup.py +44 -23
- package/scripts/set-active-plan.ps1 +38 -3
- package/scripts/set-active-plan.sh +44 -9
- package/templates/analytics_findings.md +67 -67
- package/templates/analytics_task_plan.md +7 -1
- package/templates/findings.md +47 -47
- package/templates/progress.md +58 -58
- package/templates/task_plan.md +89 -89
package/README.md
CHANGED
|
@@ -1,25 +1,25 @@
|
|
|
1
|
-
<div align="center">
|
|
2
|
-
<img src="https://raw.githubusercontent.com/OthmanAdi/planning-with-files/master/media/v3-banner-1400.jpg" alt="planning-with-files: task_plan.md, findings.md, and progress.md as three stone tablets" width="100%">
|
|
3
|
-
</div>
|
|
4
|
-
|
|
5
|
-
<h1 align="center">Planning with Files</h1>
|
|
6
|
-
|
|
7
|
-
<p align="center">
|
|
8
|
-
<strong>The planning skill your agent cannot ignore.</strong><br>
|
|
9
|
-
Your agent's context window dies. The plan does not.
|
|
10
|
-
</p>
|
|
11
|
-
|
|
12
|
-
Persistent file-based planning for AI coding agents. Keep the plan, research and progress in your project so work can continue after context loss, `/clear`, crashes or compaction.
|
|
13
|
-
|
|
14
|
-
| File | Purpose |
|
|
15
|
-
| --- | --- |
|
|
16
|
-
| `task_plan.md` | Goals, phases and decisions |
|
|
17
|
-
| `findings.md` | Research and discoveries |
|
|
18
|
-
| `progress.md` | Work completed, checks and next steps |
|
|
19
|
-
|
|
20
|
-
This is the npm distribution of [OthmanAdi/planning-with-files](https://github.com/OthmanAdi/planning-with-files), available across 60+ agents via the Agent Skills standard. It includes the planning skill, scripts and templates. Supported agent integrations add lifecycle hooks that bring selected planning context back into the session.
|
|
21
|
-
|
|
22
|
-
Automatic recovery reads project files only. Reading same-project local session records for aggregate counts or bounded replay requires an explicit catchup mode.
|
|
1
|
+
<div align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/OthmanAdi/planning-with-files/master/media/v3-banner-1400.jpg" alt="planning-with-files: task_plan.md, findings.md, and progress.md as three stone tablets" width="100%">
|
|
3
|
+
</div>
|
|
4
|
+
|
|
5
|
+
<h1 align="center">Planning with Files</h1>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<strong>The planning skill your agent cannot ignore.</strong><br>
|
|
9
|
+
Your agent's context window dies. The plan does not.
|
|
10
|
+
</p>
|
|
11
|
+
|
|
12
|
+
Persistent file-based planning for AI coding agents. Keep the plan, research and progress in your project so work can continue after context loss, `/clear`, crashes or compaction.
|
|
13
|
+
|
|
14
|
+
| File | Purpose |
|
|
15
|
+
| --- | --- |
|
|
16
|
+
| `task_plan.md` | Goals, phases and decisions |
|
|
17
|
+
| `findings.md` | Research and discoveries |
|
|
18
|
+
| `progress.md` | Work completed, checks and next steps |
|
|
19
|
+
|
|
20
|
+
This is the npm distribution of [OthmanAdi/planning-with-files](https://github.com/OthmanAdi/planning-with-files), available across 60+ agents via the Agent Skills standard. It includes the planning skill, scripts and templates. Supported agent integrations add lifecycle hooks that bring selected planning context back into the session.
|
|
21
|
+
|
|
22
|
+
Automatic recovery reads project files only. Reading same-project local session records for aggregate counts or bounded replay requires an explicit catchup mode.
|
|
23
23
|
|
|
24
24
|
## Installation
|
|
25
25
|
|
|
@@ -31,40 +31,40 @@ npm install planning-with-files
|
|
|
31
31
|
|
|
32
32
|
Places the skill, scripts and templates under `node_modules/planning-with-files/`. Use this to pin an exact version into a project, or to copy `SKILL.md` and `scripts/` into your agent's skills directory yourself. It does not register hooks on its own.
|
|
33
33
|
|
|
34
|
-
### Agent integrations
|
|
35
|
-
|
|
36
|
-
Claude Code gets the full surface (skill, hooks, slash commands) through the plugin route, and 60+ other agents install in one line. See the [main README](https://github.com/OthmanAdi/planning-with-files#quick-install).
|
|
37
|
-
|
|
38
|
-
## Usage
|
|
39
|
-
|
|
40
|
-
Once the skill is installed for your agent, start with:
|
|
41
|
-
|
|
42
|
-
```text
|
|
43
|
-
Use the planning-with-files skill to help me with this task.
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
The workflow centers on three files in your project:
|
|
47
|
-
|
|
48
|
-
```text
|
|
49
|
-
your-project/
|
|
50
|
-
├── task_plan.md
|
|
51
|
-
├── findings.md
|
|
52
|
-
└── progress.md
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
## Pi Coding Agent integration
|
|
56
|
-
|
|
57
|
-
The package also bundles a [Pi Coding Agent](https://pi.dev) extension for lifecycle automation and a planning status bar.
|
|
58
|
-
|
|
59
|
-
### Install in Pi
|
|
60
|
-
|
|
61
|
-
```bash
|
|
62
|
-
pi install npm:planning-with-files
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
Pi discovers the skill and extension from the installed package.
|
|
66
|
-
|
|
67
|
-
For a local repository checkout:
|
|
34
|
+
### Agent integrations
|
|
35
|
+
|
|
36
|
+
Claude Code gets the full surface (skill, hooks, slash commands) through the plugin route, and 60+ other agents install in one line. See the [main README](https://github.com/OthmanAdi/planning-with-files#quick-install).
|
|
37
|
+
|
|
38
|
+
## Usage
|
|
39
|
+
|
|
40
|
+
Once the skill is installed for your agent, start with:
|
|
41
|
+
|
|
42
|
+
```text
|
|
43
|
+
Use the planning-with-files skill to help me with this task.
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The workflow centers on three files in your project:
|
|
47
|
+
|
|
48
|
+
```text
|
|
49
|
+
your-project/
|
|
50
|
+
├── task_plan.md
|
|
51
|
+
├── findings.md
|
|
52
|
+
└── progress.md
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Pi Coding Agent integration
|
|
56
|
+
|
|
57
|
+
The package also bundles a [Pi Coding Agent](https://pi.dev) extension for lifecycle automation and a planning status bar.
|
|
58
|
+
|
|
59
|
+
### Install in Pi
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pi install npm:planning-with-files
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Pi discovers the skill and extension from the installed package.
|
|
66
|
+
|
|
67
|
+
For a local repository checkout:
|
|
68
68
|
|
|
69
69
|
```bash
|
|
70
70
|
# From the planning-with-files repo root
|
|
@@ -78,17 +78,17 @@ Or add to `.pi/settings.json`:
|
|
|
78
78
|
}
|
|
79
79
|
```
|
|
80
80
|
|
|
81
|
-
You can also invoke the skill directly in Pi:
|
|
81
|
+
You can also invoke the skill directly in Pi:
|
|
82
82
|
|
|
83
83
|
```text
|
|
84
84
|
/skill:planning-with-files
|
|
85
85
|
```
|
|
86
86
|
|
|
87
|
-
### Lifecycle hooks
|
|
87
|
+
### Lifecycle hooks
|
|
88
88
|
|
|
89
89
|
The bundled extension maps Claude-style behavior onto Pi events:
|
|
90
90
|
|
|
91
|
-
- `session_start` - project-file recovery with no host session-store access
|
|
91
|
+
- `session_start` - project-file recovery with no host session-store access
|
|
92
92
|
- passive plan status before approval
|
|
93
93
|
- `before_agent_start` - plan reminder/injection after `/plan-execute`
|
|
94
94
|
- `tool_call` - pre-tool recitation equivalent after `/plan-execute`
|
|
@@ -102,7 +102,7 @@ Attestation is supported. If `task_plan.md` differs from approved hash, plan inj
|
|
|
102
102
|
[planning-with-files] [PLAN TAMPERED - injection blocked]
|
|
103
103
|
```
|
|
104
104
|
|
|
105
|
-
### Modes
|
|
105
|
+
### Modes
|
|
106
106
|
|
|
107
107
|
`planningWithFiles.mode` supports:
|
|
108
108
|
|
|
@@ -127,7 +127,7 @@ Or settings:
|
|
|
127
127
|
}
|
|
128
128
|
```
|
|
129
129
|
|
|
130
|
-
### Commands
|
|
130
|
+
### Commands
|
|
131
131
|
|
|
132
132
|
- `/plan-status`
|
|
133
133
|
- `/plan-attest [--show|--clear]`
|
|
@@ -136,25 +136,25 @@ Or settings:
|
|
|
136
136
|
- `/plan-goal <text|default|clear>`
|
|
137
137
|
- `/plan-loop [interval] [prompt]` (`stop` to cancel)
|
|
138
138
|
|
|
139
|
-
Draft and review `task_plan.md` first. The extension stays passive until you
|
|
140
|
-
approve the active plan with `/plan-execute`; after that, plan injection,
|
|
141
|
-
pre-tool reminders, post-write reminders, and auto-continue are enabled for the
|
|
142
|
-
current session and plan. Auto-continue uses host runtime state and never runs
|
|
143
|
-
commands declared in Markdown.
|
|
144
|
-
|
|
145
|
-
## Session Recovery
|
|
146
|
-
|
|
147
|
-
Bare invocation and lifecycle hooks do not inspect agent session stores. To
|
|
148
|
-
inspect same-project local history deliberately, choose one mode:
|
|
149
|
-
|
|
150
|
-
```bash
|
|
151
|
-
# Aggregate counts only; no transcript, tool-command, or path bytes
|
|
152
|
-
python3 node_modules/planning-with-files/scripts/session-catchup.py --metadata .
|
|
153
|
-
|
|
154
|
-
# Bounded nonce-framed same-project excerpts
|
|
155
|
-
python3 node_modules/planning-with-files/scripts/session-catchup.py --replay .
|
|
156
|
-
```
|
|
157
|
-
|
|
158
|
-
Treat replayed excerpts as untrusted data. The catchup path contains no network
|
|
159
|
-
request or upload operation. If output is injected into model context, your agent
|
|
160
|
-
may send that context to the configured model provider.
|
|
139
|
+
Draft and review `task_plan.md` first. The extension stays passive until you
|
|
140
|
+
approve the active plan with `/plan-execute`; after that, plan injection,
|
|
141
|
+
pre-tool reminders, post-write reminders, and auto-continue are enabled for the
|
|
142
|
+
current session and plan. Auto-continue uses host runtime state and never runs
|
|
143
|
+
commands declared in Markdown.
|
|
144
|
+
|
|
145
|
+
## Session Recovery
|
|
146
|
+
|
|
147
|
+
Bare invocation and lifecycle hooks do not inspect agent session stores. To
|
|
148
|
+
inspect same-project local history deliberately, choose one mode:
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
# Aggregate counts only; no transcript, tool-command, or path bytes
|
|
152
|
+
python3 node_modules/planning-with-files/scripts/session-catchup.py --metadata .
|
|
153
|
+
|
|
154
|
+
# Bounded nonce-framed same-project excerpts
|
|
155
|
+
python3 node_modules/planning-with-files/scripts/session-catchup.py --replay .
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Treat replayed excerpts as untrusted data. The catchup path contains no network
|
|
159
|
+
request or upload operation. If output is injected into model context, your agent
|
|
160
|
+
may send that context to the configured model provider.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "planning-with-files",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.20.5",
|
|
4
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.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
package/scripts/attest-plan.ps1
CHANGED
|
@@ -116,7 +116,13 @@ public static class PwfAttestationNative {
|
|
|
116
116
|
handle, FileAttributeTagInfo, out tag,
|
|
117
117
|
(uint)Marshal.SizeOf(typeof(FILE_ATTRIBUTE_TAG_INFO))))
|
|
118
118
|
throw new Win32Exception(Marshal.GetLastWin32Error());
|
|
119
|
-
|
|
119
|
+
// Refuse only name-surrogate reparse points (symlinks, junctions, and any
|
|
120
|
+
// unknown tag with bit 29 set): those are what path parsing follows. A
|
|
121
|
+
// OneDrive Files On-Demand placeholder (tag 0x9000601A) is a regular file
|
|
122
|
+
// that every other route reads; refusing it broke attestation, --show
|
|
123
|
+
// and --clear in every project under OneDrive (#275).
|
|
124
|
+
if ((tag.FileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0 &&
|
|
125
|
+
(tag.ReparseTag & 0x20000000) != 0)
|
|
120
126
|
throw new IOException("Refusing a reparse-point file.");
|
|
121
127
|
if ((tag.FileAttributes & (uint)FileAttributes.Directory) != 0)
|
|
122
128
|
throw new IOException("Refusing a directory where a regular file is required.");
|
|
@@ -218,8 +224,10 @@ if ($script:IsWindowsHost) {
|
|
|
218
224
|
$securityRootPath = (Get-Location).Path
|
|
219
225
|
if ($env:PWF_PLAN_ROOT) {
|
|
220
226
|
$pin = $env:PWF_PLAN_ROOT
|
|
227
|
+
# Windows PowerShell 5.1 has no IsPathFullyQualified; a drive-qualified
|
|
228
|
+
# local path is the only accepted shape, as in resolve-plan-dir.ps1.
|
|
221
229
|
$isUnc = $pin.StartsWith('\\') -or $pin.StartsWith('//')
|
|
222
|
-
if (-
|
|
230
|
+
if ($isUnc -or ($pin -notmatch '^[A-Za-z]:[\\/]')) {
|
|
223
231
|
throw "[plan-attest] PWF_PLAN_ROOT must be an absolute local path."
|
|
224
232
|
}
|
|
225
233
|
$securityRootPath = $pin
|
package/scripts/init-session.ps1
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# Initialize planning files for a new session
|
|
2
2
|
# Usage: .\init-session.ps1 [-Template TYPE] [project-name]
|
|
3
|
+
# .\init-session.ps1 -PlanDir # isolated plan with generated slug
|
|
3
4
|
# .\init-session.ps1 -Autonomous # v3 autonomous mode (opt-in)
|
|
4
5
|
# .\init-session.ps1 -Gated # v3 gated mode (opt-in, implies autonomous)
|
|
5
6
|
# Templates: default, analytics
|
|
@@ -12,6 +13,7 @@
|
|
|
12
13
|
param(
|
|
13
14
|
[string]$ProjectName = "project",
|
|
14
15
|
[string]$Template = "default",
|
|
16
|
+
[switch]$PlanDir,
|
|
15
17
|
[switch]$Autonomous,
|
|
16
18
|
[switch]$Gated
|
|
17
19
|
)
|
|
@@ -38,7 +40,65 @@ function Get-Nonce {
|
|
|
38
40
|
($bytes | ForEach-Object { $_.ToString("x2") }) -join ""
|
|
39
41
|
}
|
|
40
42
|
|
|
41
|
-
|
|
43
|
+
function Get-PlanSlug([string]$Name) {
|
|
44
|
+
# -creplace: the case-insensitive -replace lets letters that .NET folds to
|
|
45
|
+
# ASCII (a dotted capital I, the Kelvin sign) survive into the plan id.
|
|
46
|
+
$slug = $Name.ToLowerInvariant() -creplace '[^a-z0-9]', '-'
|
|
47
|
+
$slug = $slug -creplace '-{2,}', '-'
|
|
48
|
+
$slug = $slug.Trim('-')
|
|
49
|
+
if ($slug.Length -gt 40) {
|
|
50
|
+
$slug = $slug.Substring(0, 40).TrimEnd('-')
|
|
51
|
+
}
|
|
52
|
+
# The resolvers and the selector only accept ASCII plan ids.
|
|
53
|
+
if ($slug -cnotmatch '^[a-z0-9-]*$') {
|
|
54
|
+
return ""
|
|
55
|
+
}
|
|
56
|
+
return $slug
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function Get-ShortId {
|
|
60
|
+
return ([Guid]::NewGuid().ToString('N')).Substring(0, 8)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function Get-InheritedMode([string]$CurrentMode) {
|
|
64
|
+
$RootModePath = Join-Path (Get-Location).Path ".mode"
|
|
65
|
+
if (-not (Test-Path -LiteralPath $RootModePath)) {
|
|
66
|
+
return $CurrentMode
|
|
67
|
+
}
|
|
68
|
+
if ($CurrentMode -eq "gated") {
|
|
69
|
+
return $CurrentMode
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
$RootMode = Get-Content -LiteralPath $RootModePath -Raw -ErrorAction SilentlyContinue
|
|
73
|
+
# Case-sensitive like inherit_root_mode in init-session.sh and the injector.
|
|
74
|
+
if ($RootMode -cmatch 'gate') {
|
|
75
|
+
return "gated"
|
|
76
|
+
}
|
|
77
|
+
if ($RootMode -cmatch 'autonomous') {
|
|
78
|
+
return "autonomous"
|
|
79
|
+
}
|
|
80
|
+
return $CurrentMode
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function Format-AttestationFailureReason {
|
|
84
|
+
param([object[]]$Output, [string]$Fallback)
|
|
85
|
+
|
|
86
|
+
$Reason = @(
|
|
87
|
+
$Output |
|
|
88
|
+
ForEach-Object {
|
|
89
|
+
if ($null -ne $_) { ($_.ToString()).Trim() }
|
|
90
|
+
} |
|
|
91
|
+
Where-Object { $_ }
|
|
92
|
+
) -join " "
|
|
93
|
+
$Reason = ($Reason -replace '\s+', ' ').Trim()
|
|
94
|
+
if ([string]::IsNullOrWhiteSpace($Reason)) {
|
|
95
|
+
return $Fallback
|
|
96
|
+
}
|
|
97
|
+
if ($Reason.Length -gt 300) {
|
|
98
|
+
return ($Reason.Substring(0, 297) + "...")
|
|
99
|
+
}
|
|
100
|
+
return $Reason
|
|
101
|
+
}
|
|
42
102
|
|
|
43
103
|
# Validate template
|
|
44
104
|
if ($Template -ne "default" -and $Template -ne "analytics") {
|
|
@@ -46,11 +106,86 @@ if ($Template -ne "default" -and $Template -ne "analytics") {
|
|
|
46
106
|
$Template = "default"
|
|
47
107
|
}
|
|
48
108
|
|
|
109
|
+
# Match init-session.sh: zero args (or an empty name) preserve legacy root
|
|
110
|
+
# mode. A positional project name or -PlanDir creates an isolated
|
|
111
|
+
# .planning/<date>-<slug>/ plan.
|
|
112
|
+
$NamedPlan = $PSBoundParameters.ContainsKey("ProjectName") -and -not [string]::IsNullOrEmpty($ProjectName)
|
|
113
|
+
$UsePlanDir = $PlanDir -or $NamedPlan
|
|
114
|
+
if ($UsePlanDir) {
|
|
115
|
+
$PlanningRoot = Join-Path (Get-Location).Path ".planning"
|
|
116
|
+
# Match init-session.sh: set-active-plan.ps1 owns every write to the
|
|
117
|
+
# shared pointer, so a named plan cannot be created without it.
|
|
118
|
+
$PlanSelector = Join-Path $ScriptDir "set-active-plan.ps1"
|
|
119
|
+
if (-not (Test-Path -LiteralPath $PlanSelector -PathType Leaf)) {
|
|
120
|
+
Write-Error "Error: set-active-plan.ps1 is required to create a named plan safely."
|
|
121
|
+
exit 1
|
|
122
|
+
}
|
|
123
|
+
New-Item -ItemType Directory -Path $PlanningRoot -Force | Out-Null
|
|
124
|
+
# Verify the physical planning root and the existing pointer before
|
|
125
|
+
# creating anything below it. A symlink or junction that escapes the
|
|
126
|
+
# project must not redirect init writes, and a linked or non-regular
|
|
127
|
+
# pointer must be refused before a plan directory exists on disk. A
|
|
128
|
+
# script that returns without exit leaves $LASTEXITCODE alone, so reset
|
|
129
|
+
# it first and treat a thrown error as a failure too.
|
|
130
|
+
$global:LASTEXITCODE = 0
|
|
131
|
+
try {
|
|
132
|
+
& $PlanSelector -VerifyRoot *> $null
|
|
133
|
+
} catch {
|
|
134
|
+
$global:LASTEXITCODE = 1
|
|
135
|
+
}
|
|
136
|
+
if ($LASTEXITCODE -ne 0) {
|
|
137
|
+
Write-Error "Error: the planning directory or the active plan pointer is outside the project or cannot be verified."
|
|
138
|
+
exit 1
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if ($NamedPlan) {
|
|
142
|
+
$Slug = Get-PlanSlug $ProjectName
|
|
143
|
+
} else {
|
|
144
|
+
$Slug = ""
|
|
145
|
+
$ProjectName = "untitled"
|
|
146
|
+
}
|
|
147
|
+
if ([string]::IsNullOrEmpty($Slug)) {
|
|
148
|
+
$Slug = "untitled-$(Get-ShortId)"
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
$BaseId = "$DATE-$Slug"
|
|
152
|
+
$PlanId = $BaseId
|
|
153
|
+
$Counter = 2
|
|
154
|
+
while (Test-Path -LiteralPath (Join-Path $PlanningRoot $PlanId)) {
|
|
155
|
+
$PlanId = "$BaseId-$Counter"
|
|
156
|
+
$Counter++
|
|
157
|
+
}
|
|
158
|
+
$TargetDir = Join-Path $PlanningRoot $PlanId
|
|
159
|
+
New-Item -ItemType Directory -Path $TargetDir -Force -ErrorAction Stop | Out-Null
|
|
160
|
+
$Mode = Get-InheritedMode $Mode
|
|
161
|
+
} else {
|
|
162
|
+
$TargetDir = (Get-Location).Path
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
$TaskPlanPath = Join-Path $TargetDir "task_plan.md"
|
|
166
|
+
$FindingsPath = Join-Path $TargetDir "findings.md"
|
|
167
|
+
$ProgressPath = Join-Path $TargetDir "progress.md"
|
|
168
|
+
if ($UsePlanDir) {
|
|
169
|
+
$TaskPlanDisplay = $TaskPlanPath
|
|
170
|
+
$FindingsDisplay = $FindingsPath
|
|
171
|
+
$ProgressDisplay = $ProgressPath
|
|
172
|
+
} else {
|
|
173
|
+
$TaskPlanDisplay = "task_plan.md"
|
|
174
|
+
$FindingsDisplay = "findings.md"
|
|
175
|
+
$ProgressDisplay = "progress.md"
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
Write-Host "Initializing planning files for: $ProjectName (template: $Template)"
|
|
179
|
+
if ($UsePlanDir) {
|
|
180
|
+
Write-Host "PLAN_ID=$PlanId"
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
try {
|
|
49
184
|
# Create task_plan.md if it doesn't exist
|
|
50
|
-
if (-not (Test-Path
|
|
185
|
+
if (-not (Test-Path -LiteralPath $TaskPlanPath)) {
|
|
51
186
|
$AnalyticsPlan = Join-Path $TemplateDir "analytics_task_plan.md"
|
|
52
187
|
if ($Template -eq "analytics" -and (Test-Path $AnalyticsPlan)) {
|
|
53
|
-
Copy-Item $AnalyticsPlan
|
|
188
|
+
Copy-Item -LiteralPath $AnalyticsPlan -Destination $TaskPlanPath -ErrorAction Stop
|
|
54
189
|
} else {
|
|
55
190
|
@"
|
|
56
191
|
# Task Plan: [Brief Description]
|
|
@@ -99,18 +234,18 @@ Phase 1
|
|
|
99
234
|
## Errors Encountered
|
|
100
235
|
| Error | Resolution |
|
|
101
236
|
|-------|------------|
|
|
102
|
-
"@ | Out-File -
|
|
237
|
+
"@ | Out-File -LiteralPath $TaskPlanPath -Encoding UTF8 -ErrorAction Stop
|
|
103
238
|
}
|
|
104
|
-
Write-Host "Created
|
|
239
|
+
Write-Host "Created $TaskPlanDisplay"
|
|
105
240
|
} else {
|
|
106
|
-
Write-Host "
|
|
241
|
+
Write-Host "$TaskPlanDisplay already exists, skipping"
|
|
107
242
|
}
|
|
108
243
|
|
|
109
244
|
# Create findings.md if it doesn't exist
|
|
110
|
-
if (-not (Test-Path
|
|
245
|
+
if (-not (Test-Path -LiteralPath $FindingsPath)) {
|
|
111
246
|
$AnalyticsFindings = Join-Path $TemplateDir "analytics_findings.md"
|
|
112
247
|
if ($Template -eq "analytics" -and (Test-Path $AnalyticsFindings)) {
|
|
113
|
-
Copy-Item $AnalyticsFindings
|
|
248
|
+
Copy-Item -LiteralPath $AnalyticsFindings -Destination $FindingsPath -ErrorAction Stop
|
|
114
249
|
} else {
|
|
115
250
|
@"
|
|
116
251
|
# Findings & Decisions
|
|
@@ -131,15 +266,15 @@ if (-not (Test-Path "findings.md")) {
|
|
|
131
266
|
|
|
132
267
|
## Resources
|
|
133
268
|
-
|
|
134
|
-
"@ | Out-File -
|
|
269
|
+
"@ | Out-File -LiteralPath $FindingsPath -Encoding UTF8 -ErrorAction Stop
|
|
135
270
|
}
|
|
136
|
-
Write-Host "Created
|
|
271
|
+
Write-Host "Created $FindingsDisplay"
|
|
137
272
|
} else {
|
|
138
|
-
Write-Host "
|
|
273
|
+
Write-Host "$FindingsDisplay already exists, skipping"
|
|
139
274
|
}
|
|
140
275
|
|
|
141
276
|
# Create progress.md if it doesn't exist
|
|
142
|
-
if (-not (Test-Path
|
|
277
|
+
if (-not (Test-Path -LiteralPath $ProgressPath)) {
|
|
143
278
|
if ($Template -eq "analytics") {
|
|
144
279
|
@"
|
|
145
280
|
# Progress Log
|
|
@@ -160,7 +295,7 @@ if (-not (Test-Path "progress.md")) {
|
|
|
160
295
|
### Errors
|
|
161
296
|
| Error | Resolution |
|
|
162
297
|
|-------|------------|
|
|
163
|
-
"@ | Out-File -
|
|
298
|
+
"@ | Out-File -LiteralPath $ProgressPath -Encoding UTF8 -ErrorAction Stop
|
|
164
299
|
} else {
|
|
165
300
|
@"
|
|
166
301
|
# Progress Log
|
|
@@ -181,23 +316,48 @@ if (-not (Test-Path "progress.md")) {
|
|
|
181
316
|
### Errors
|
|
182
317
|
| Error | Resolution |
|
|
183
318
|
|-------|------------|
|
|
184
|
-
"@ | Out-File -
|
|
319
|
+
"@ | Out-File -LiteralPath $ProgressPath -Encoding UTF8 -ErrorAction Stop
|
|
185
320
|
}
|
|
186
|
-
Write-Host "Created
|
|
321
|
+
Write-Host "Created $ProgressDisplay"
|
|
187
322
|
} else {
|
|
188
|
-
Write-Host "
|
|
323
|
+
Write-Host "$ProgressDisplay already exists, skipping"
|
|
324
|
+
}
|
|
325
|
+
} catch {
|
|
326
|
+
Write-Error "Error: could not initialize planning files in '$TargetDir': $($_.Exception.Message)"
|
|
327
|
+
exit 1
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if ($UsePlanDir) {
|
|
331
|
+
# Activate the named plan only after all three planning files are ready.
|
|
332
|
+
# This matches init-session.sh and prevents a failed initialization from
|
|
333
|
+
# 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
|
|
339
|
+
}
|
|
340
|
+
if ($LASTEXITCODE -ne 0) {
|
|
341
|
+
Write-Error "Error: could not safely update the active plan pointer at $(Join-Path $PlanningRoot '.active_plan')."
|
|
342
|
+
exit 1
|
|
343
|
+
}
|
|
189
344
|
}
|
|
190
345
|
|
|
191
346
|
Write-Host ""
|
|
192
347
|
Write-Host "Planning files initialized!"
|
|
193
|
-
|
|
348
|
+
if ($UsePlanDir) {
|
|
349
|
+
Write-Host "Active plan recorded: $(Join-Path $PlanningRoot '.active_plan')"
|
|
350
|
+
Write-Host "Pin this terminal to the plan for parallel sessions:"
|
|
351
|
+
Write-Host " `$env:PLAN_ID='$PlanId'"
|
|
352
|
+
} else {
|
|
353
|
+
Write-Host "Files: task_plan.md, findings.md, progress.md"
|
|
354
|
+
}
|
|
194
355
|
|
|
195
356
|
# v3 opt-in mode side effects. No-op when -Autonomous/-Gated were not passed, so
|
|
196
|
-
# the default path stays byte-equivalent to v2.43.0.
|
|
197
|
-
#
|
|
198
|
-
# .plan-attestation at the project root.
|
|
357
|
+
# the default path stays byte-equivalent to v2.43.0. Dotfiles live beside the
|
|
358
|
+
# selected plan; root mode therefore retains the legacy project-root behavior.
|
|
199
359
|
if ($Mode -ne "") {
|
|
200
|
-
$PlanDirPwf =
|
|
360
|
+
$PlanDirPwf = $TargetDir
|
|
201
361
|
|
|
202
362
|
# (a) reset gate block counter, drop stale gate ledger.
|
|
203
363
|
Set-Content -LiteralPath (Join-Path $PlanDirPwf ".stop_blocks") -Value "0" -Encoding ascii
|
|
@@ -216,15 +376,96 @@ if ($Mode -ne "") {
|
|
|
216
376
|
Set-Content -LiteralPath (Join-Path $PlanDirPwf ".mode") -Value $MarkerText -Encoding ascii
|
|
217
377
|
|
|
218
378
|
# (c) auto-attest (attestation default-on in v3 modes, security strand rec 1).
|
|
219
|
-
|
|
379
|
+
# attest-plan.ps1 intentionally refuses non-Windows hosts because its secure
|
|
380
|
+
# no-follow implementation uses Win32 handles. On Unix, use the POSIX
|
|
381
|
+
# attester instead. Slug mode binds PWF_PLAN_ROOT and PLAN_ID to the plan
|
|
382
|
+
# we just created so an inherited pin or slug cannot redirect attestation
|
|
383
|
+
# to another project or plan (#261, #237). Root mode clears both instead:
|
|
384
|
+
# the attester only falls back to the legacy ./task_plan.md when no
|
|
385
|
+
# selector is set, and a bound pin would make it refuse the root plan.
|
|
386
|
+
$AttestationSucceeded = $false
|
|
387
|
+
$AttestationCommand = "attest-plan"
|
|
388
|
+
$AttestationReason = "task_plan.md was not available for attestation"
|
|
220
389
|
$PlanFilePwf = Join-Path $PlanDirPwf "task_plan.md"
|
|
221
|
-
if (
|
|
390
|
+
if (Test-Path -LiteralPath $PlanFilePwf -PathType Leaf) {
|
|
391
|
+
$HadPlanId = Test-Path Env:PLAN_ID
|
|
392
|
+
$PreviousPlanId = $env:PLAN_ID
|
|
393
|
+
$HadPlanRoot = Test-Path Env:PWF_PLAN_ROOT
|
|
394
|
+
$PreviousPlanRoot = $env:PWF_PLAN_ROOT
|
|
222
395
|
try {
|
|
223
|
-
|
|
396
|
+
if ($UsePlanDir) {
|
|
397
|
+
$env:PWF_PLAN_ROOT = (Get-Location).Path
|
|
398
|
+
$env:PLAN_ID = $PlanId
|
|
399
|
+
} else {
|
|
400
|
+
Remove-Item Env:PWF_PLAN_ROOT -ErrorAction SilentlyContinue
|
|
401
|
+
Remove-Item Env:PLAN_ID -ErrorAction SilentlyContinue
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
# A called script can return without changing $LASTEXITCODE, so
|
|
405
|
+
# clear the inherited value before every attempt. Both a non-zero
|
|
406
|
+
# status and a terminating exception mean the plan is not attested.
|
|
407
|
+
$global:LASTEXITCODE = 0
|
|
408
|
+
$AttestOutput = @()
|
|
409
|
+
$IsWindowsHost = [Environment]::OSVersion.Platform -eq [PlatformID]::Win32NT
|
|
410
|
+
if ($IsWindowsHost) {
|
|
411
|
+
$AttestationCommand = "attest-plan.ps1"
|
|
412
|
+
$AttestPs1 = Join-Path $ScriptDir $AttestationCommand
|
|
413
|
+
if (Test-Path -LiteralPath $AttestPs1 -PathType Leaf) {
|
|
414
|
+
$AttestOutput = @(& $AttestPs1 2>&1)
|
|
415
|
+
$AttestExitCode = $LASTEXITCODE
|
|
416
|
+
if ($AttestExitCode -eq 0) {
|
|
417
|
+
$AttestationSucceeded = $true
|
|
418
|
+
$AttestationReason = ""
|
|
419
|
+
} else {
|
|
420
|
+
$AttestationReason = Format-AttestationFailureReason `
|
|
421
|
+
-Output $AttestOutput `
|
|
422
|
+
-Fallback "$AttestationCommand exited with code $AttestExitCode"
|
|
423
|
+
}
|
|
424
|
+
} else {
|
|
425
|
+
$AttestationReason = "$AttestationCommand was not found beside init-session.ps1"
|
|
426
|
+
}
|
|
427
|
+
} else {
|
|
428
|
+
$AttestationCommand = "attest-plan.sh"
|
|
429
|
+
$AttestSh = Join-Path $ScriptDir $AttestationCommand
|
|
430
|
+
$Sh = Get-Command sh -ErrorAction SilentlyContinue
|
|
431
|
+
if ($Sh -and (Test-Path -LiteralPath $AttestSh -PathType Leaf)) {
|
|
432
|
+
$AttestOutput = @(& $Sh.Path $AttestSh 2>&1)
|
|
433
|
+
$AttestExitCode = $LASTEXITCODE
|
|
434
|
+
if ($AttestExitCode -eq 0) {
|
|
435
|
+
$AttestationSucceeded = $true
|
|
436
|
+
$AttestationReason = ""
|
|
437
|
+
} else {
|
|
438
|
+
$AttestationReason = Format-AttestationFailureReason `
|
|
439
|
+
-Output $AttestOutput `
|
|
440
|
+
-Fallback "$AttestationCommand exited with code $AttestExitCode"
|
|
441
|
+
}
|
|
442
|
+
} elseif (-not $Sh) {
|
|
443
|
+
$AttestationReason = "sh was not found; $AttestationCommand could not run"
|
|
444
|
+
} else {
|
|
445
|
+
$AttestationReason = "$AttestationCommand was not found beside init-session.ps1"
|
|
446
|
+
}
|
|
447
|
+
}
|
|
224
448
|
} catch {
|
|
225
|
-
|
|
449
|
+
$AttestationReason = Format-AttestationFailureReason `
|
|
450
|
+
-Output @($_) `
|
|
451
|
+
-Fallback "$AttestationCommand failed"
|
|
452
|
+
} finally {
|
|
453
|
+
if ($HadPlanId) {
|
|
454
|
+
$env:PLAN_ID = $PreviousPlanId
|
|
455
|
+
} else {
|
|
456
|
+
Remove-Item Env:PLAN_ID -ErrorAction SilentlyContinue
|
|
457
|
+
}
|
|
458
|
+
if ($HadPlanRoot) {
|
|
459
|
+
$env:PWF_PLAN_ROOT = $PreviousPlanRoot
|
|
460
|
+
} else {
|
|
461
|
+
Remove-Item Env:PWF_PLAN_ROOT -ErrorAction SilentlyContinue
|
|
462
|
+
}
|
|
226
463
|
}
|
|
227
464
|
}
|
|
228
465
|
|
|
229
|
-
|
|
466
|
+
if ($AttestationSucceeded) {
|
|
467
|
+
Write-Host "Mode: $MarkerText (attested, gate counter reset)"
|
|
468
|
+
} else {
|
|
469
|
+
Write-Host "Mode: $MarkerText (NOT attested: $AttestationReason; run $AttestationCommand before the first hook fire)"
|
|
470
|
+
}
|
|
230
471
|
}
|