planning-with-files 3.18.3 → 3.19.0
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/init-session.ps1 +185 -24
- package/scripts/init-session.sh +23 -2
- package/scripts/session-catchup.py +36 -20
- package/scripts/set-active-plan.ps1 +25 -1
- package/scripts/set-active-plan.sh +36 -8
- package/templates/analytics_findings.md +67 -67
- package/templates/analytics_task_plan.md +81 -81
- 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.19.0",
|
|
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/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,45 @@ 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
|
+
}
|
|
42
82
|
|
|
43
83
|
# Validate template
|
|
44
84
|
if ($Template -ne "default" -and $Template -ne "analytics") {
|
|
@@ -46,11 +86,98 @@ if ($Template -ne "default" -and $Template -ne "analytics") {
|
|
|
46
86
|
$Template = "default"
|
|
47
87
|
}
|
|
48
88
|
|
|
89
|
+
# Match init-session.sh: zero args (or an empty name) preserve legacy root
|
|
90
|
+
# mode. A positional project name or -PlanDir creates an isolated
|
|
91
|
+
# .planning/<date>-<slug>/ plan.
|
|
92
|
+
$NamedPlan = $PSBoundParameters.ContainsKey("ProjectName") -and -not [string]::IsNullOrEmpty($ProjectName)
|
|
93
|
+
$UsePlanDir = $PlanDir -or $NamedPlan
|
|
94
|
+
if ($UsePlanDir) {
|
|
95
|
+
$PlanningRoot = Join-Path (Get-Location).Path ".planning"
|
|
96
|
+
# Match init-session.sh: set-active-plan.ps1 owns every write to the
|
|
97
|
+
# shared pointer, so a named plan cannot be created without it.
|
|
98
|
+
$PlanSelector = Join-Path $ScriptDir "set-active-plan.ps1"
|
|
99
|
+
if (-not (Test-Path -LiteralPath $PlanSelector -PathType Leaf)) {
|
|
100
|
+
Write-Error "Error: set-active-plan.ps1 is required to create a named plan safely."
|
|
101
|
+
exit 1
|
|
102
|
+
}
|
|
103
|
+
New-Item -ItemType Directory -Path $PlanningRoot -Force | Out-Null
|
|
104
|
+
# Verify the physical planning root and the existing pointer before
|
|
105
|
+
# creating anything below it. A symlink or junction that escapes the
|
|
106
|
+
# project must not redirect init writes, and a linked or non-regular
|
|
107
|
+
# pointer must be refused before a plan directory exists on disk. A
|
|
108
|
+
# script that returns without exit leaves $LASTEXITCODE alone, so reset
|
|
109
|
+
# it first and treat a thrown error as a failure too.
|
|
110
|
+
$global:LASTEXITCODE = 0
|
|
111
|
+
try {
|
|
112
|
+
& $PlanSelector -VerifyRoot *> $null
|
|
113
|
+
} catch {
|
|
114
|
+
$global:LASTEXITCODE = 1
|
|
115
|
+
}
|
|
116
|
+
if ($LASTEXITCODE -ne 0) {
|
|
117
|
+
Write-Error "Error: the planning directory or the active plan pointer is outside the project or cannot be verified."
|
|
118
|
+
exit 1
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if ($NamedPlan) {
|
|
122
|
+
$Slug = Get-PlanSlug $ProjectName
|
|
123
|
+
} else {
|
|
124
|
+
$Slug = ""
|
|
125
|
+
$ProjectName = "untitled"
|
|
126
|
+
}
|
|
127
|
+
if ([string]::IsNullOrEmpty($Slug)) {
|
|
128
|
+
$Slug = "untitled-$(Get-ShortId)"
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
$BaseId = "$DATE-$Slug"
|
|
132
|
+
$PlanId = $BaseId
|
|
133
|
+
$Counter = 2
|
|
134
|
+
while (Test-Path -LiteralPath (Join-Path $PlanningRoot $PlanId)) {
|
|
135
|
+
$PlanId = "$BaseId-$Counter"
|
|
136
|
+
$Counter++
|
|
137
|
+
}
|
|
138
|
+
$TargetDir = Join-Path $PlanningRoot $PlanId
|
|
139
|
+
New-Item -ItemType Directory -Path $TargetDir -Force | Out-Null
|
|
140
|
+
# Reuse the selector's contained, atomic pointer replacement. Set-Content
|
|
141
|
+
# would follow a reparse point and truncate a hardlinked pointer in place,
|
|
142
|
+
# overwriting whichever file shares that inode.
|
|
143
|
+
$global:LASTEXITCODE = 0
|
|
144
|
+
try {
|
|
145
|
+
& $PlanSelector $PlanId *> $null
|
|
146
|
+
} catch {
|
|
147
|
+
$global:LASTEXITCODE = 1
|
|
148
|
+
}
|
|
149
|
+
if ($LASTEXITCODE -ne 0) {
|
|
150
|
+
Write-Error "Error: could not safely update the active plan pointer at $(Join-Path $PlanningRoot '.active_plan')."
|
|
151
|
+
exit 1
|
|
152
|
+
}
|
|
153
|
+
$Mode = Get-InheritedMode $Mode
|
|
154
|
+
} else {
|
|
155
|
+
$TargetDir = (Get-Location).Path
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
$TaskPlanPath = Join-Path $TargetDir "task_plan.md"
|
|
159
|
+
$FindingsPath = Join-Path $TargetDir "findings.md"
|
|
160
|
+
$ProgressPath = Join-Path $TargetDir "progress.md"
|
|
161
|
+
if ($UsePlanDir) {
|
|
162
|
+
$TaskPlanDisplay = $TaskPlanPath
|
|
163
|
+
$FindingsDisplay = $FindingsPath
|
|
164
|
+
$ProgressDisplay = $ProgressPath
|
|
165
|
+
} else {
|
|
166
|
+
$TaskPlanDisplay = "task_plan.md"
|
|
167
|
+
$FindingsDisplay = "findings.md"
|
|
168
|
+
$ProgressDisplay = "progress.md"
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
Write-Host "Initializing planning files for: $ProjectName (template: $Template)"
|
|
172
|
+
if ($UsePlanDir) {
|
|
173
|
+
Write-Host "PLAN_ID=$PlanId"
|
|
174
|
+
}
|
|
175
|
+
|
|
49
176
|
# Create task_plan.md if it doesn't exist
|
|
50
|
-
if (-not (Test-Path
|
|
177
|
+
if (-not (Test-Path -LiteralPath $TaskPlanPath)) {
|
|
51
178
|
$AnalyticsPlan = Join-Path $TemplateDir "analytics_task_plan.md"
|
|
52
179
|
if ($Template -eq "analytics" -and (Test-Path $AnalyticsPlan)) {
|
|
53
|
-
Copy-Item $AnalyticsPlan
|
|
180
|
+
Copy-Item -LiteralPath $AnalyticsPlan -Destination $TaskPlanPath
|
|
54
181
|
} else {
|
|
55
182
|
@"
|
|
56
183
|
# Task Plan: [Brief Description]
|
|
@@ -99,18 +226,18 @@ Phase 1
|
|
|
99
226
|
## Errors Encountered
|
|
100
227
|
| Error | Resolution |
|
|
101
228
|
|-------|------------|
|
|
102
|
-
"@ | Out-File -
|
|
229
|
+
"@ | Out-File -LiteralPath $TaskPlanPath -Encoding UTF8
|
|
103
230
|
}
|
|
104
|
-
Write-Host "Created
|
|
231
|
+
Write-Host "Created $TaskPlanDisplay"
|
|
105
232
|
} else {
|
|
106
|
-
Write-Host "
|
|
233
|
+
Write-Host "$TaskPlanDisplay already exists, skipping"
|
|
107
234
|
}
|
|
108
235
|
|
|
109
236
|
# Create findings.md if it doesn't exist
|
|
110
|
-
if (-not (Test-Path
|
|
237
|
+
if (-not (Test-Path -LiteralPath $FindingsPath)) {
|
|
111
238
|
$AnalyticsFindings = Join-Path $TemplateDir "analytics_findings.md"
|
|
112
239
|
if ($Template -eq "analytics" -and (Test-Path $AnalyticsFindings)) {
|
|
113
|
-
Copy-Item $AnalyticsFindings
|
|
240
|
+
Copy-Item -LiteralPath $AnalyticsFindings -Destination $FindingsPath
|
|
114
241
|
} else {
|
|
115
242
|
@"
|
|
116
243
|
# Findings & Decisions
|
|
@@ -131,15 +258,15 @@ if (-not (Test-Path "findings.md")) {
|
|
|
131
258
|
|
|
132
259
|
## Resources
|
|
133
260
|
-
|
|
134
|
-
"@ | Out-File -
|
|
261
|
+
"@ | Out-File -LiteralPath $FindingsPath -Encoding UTF8
|
|
135
262
|
}
|
|
136
|
-
Write-Host "Created
|
|
263
|
+
Write-Host "Created $FindingsDisplay"
|
|
137
264
|
} else {
|
|
138
|
-
Write-Host "
|
|
265
|
+
Write-Host "$FindingsDisplay already exists, skipping"
|
|
139
266
|
}
|
|
140
267
|
|
|
141
268
|
# Create progress.md if it doesn't exist
|
|
142
|
-
if (-not (Test-Path
|
|
269
|
+
if (-not (Test-Path -LiteralPath $ProgressPath)) {
|
|
143
270
|
if ($Template -eq "analytics") {
|
|
144
271
|
@"
|
|
145
272
|
# Progress Log
|
|
@@ -160,7 +287,7 @@ if (-not (Test-Path "progress.md")) {
|
|
|
160
287
|
### Errors
|
|
161
288
|
| Error | Resolution |
|
|
162
289
|
|-------|------------|
|
|
163
|
-
"@ | Out-File -
|
|
290
|
+
"@ | Out-File -LiteralPath $ProgressPath -Encoding UTF8
|
|
164
291
|
} else {
|
|
165
292
|
@"
|
|
166
293
|
# Progress Log
|
|
@@ -181,23 +308,28 @@ if (-not (Test-Path "progress.md")) {
|
|
|
181
308
|
### Errors
|
|
182
309
|
| Error | Resolution |
|
|
183
310
|
|-------|------------|
|
|
184
|
-
"@ | Out-File -
|
|
311
|
+
"@ | Out-File -LiteralPath $ProgressPath -Encoding UTF8
|
|
185
312
|
}
|
|
186
|
-
Write-Host "Created
|
|
313
|
+
Write-Host "Created $ProgressDisplay"
|
|
187
314
|
} else {
|
|
188
|
-
Write-Host "
|
|
315
|
+
Write-Host "$ProgressDisplay already exists, skipping"
|
|
189
316
|
}
|
|
190
317
|
|
|
191
318
|
Write-Host ""
|
|
192
319
|
Write-Host "Planning files initialized!"
|
|
193
|
-
|
|
320
|
+
if ($UsePlanDir) {
|
|
321
|
+
Write-Host "Active plan recorded: $(Join-Path $PlanningRoot '.active_plan')"
|
|
322
|
+
Write-Host "Pin this terminal to the plan for parallel sessions:"
|
|
323
|
+
Write-Host " `$env:PLAN_ID='$PlanId'"
|
|
324
|
+
} else {
|
|
325
|
+
Write-Host "Files: task_plan.md, findings.md, progress.md"
|
|
326
|
+
}
|
|
194
327
|
|
|
195
328
|
# 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.
|
|
329
|
+
# the default path stays byte-equivalent to v2.43.0. Dotfiles live beside the
|
|
330
|
+
# selected plan; root mode therefore retains the legacy project-root behavior.
|
|
199
331
|
if ($Mode -ne "") {
|
|
200
|
-
$PlanDirPwf =
|
|
332
|
+
$PlanDirPwf = $TargetDir
|
|
201
333
|
|
|
202
334
|
# (a) reset gate block counter, drop stale gate ledger.
|
|
203
335
|
Set-Content -LiteralPath (Join-Path $PlanDirPwf ".stop_blocks") -Value "0" -Encoding ascii
|
|
@@ -216,13 +348,42 @@ if ($Mode -ne "") {
|
|
|
216
348
|
Set-Content -LiteralPath (Join-Path $PlanDirPwf ".mode") -Value $MarkerText -Encoding ascii
|
|
217
349
|
|
|
218
350
|
# (c) auto-attest (attestation default-on in v3 modes, security strand rec 1).
|
|
219
|
-
|
|
351
|
+
# attest-plan.ps1 intentionally refuses non-Windows hosts because its secure
|
|
352
|
+
# no-follow implementation uses Win32 handles. On Unix, use the POSIX
|
|
353
|
+
# attester instead. Bind slug mode to the plan we just created so an
|
|
354
|
+
# inherited PLAN_ID cannot redirect attestation to another plan.
|
|
220
355
|
$PlanFilePwf = Join-Path $PlanDirPwf "task_plan.md"
|
|
221
|
-
if (
|
|
356
|
+
if (Test-Path -LiteralPath $PlanFilePwf) {
|
|
357
|
+
$HadPlanId = Test-Path Env:PLAN_ID
|
|
358
|
+
$PreviousPlanId = $env:PLAN_ID
|
|
222
359
|
try {
|
|
223
|
-
|
|
360
|
+
if ($UsePlanDir) {
|
|
361
|
+
$env:PLAN_ID = $PlanId
|
|
362
|
+
} else {
|
|
363
|
+
Remove-Item Env:PLAN_ID -ErrorAction SilentlyContinue
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
$IsWindowsHost = [Environment]::OSVersion.Platform -eq [PlatformID]::Win32NT
|
|
367
|
+
if ($IsWindowsHost) {
|
|
368
|
+
$AttestPs1 = Join-Path $ScriptDir "attest-plan.ps1"
|
|
369
|
+
if (Test-Path -LiteralPath $AttestPs1) {
|
|
370
|
+
& $AttestPs1 *> $null
|
|
371
|
+
}
|
|
372
|
+
} else {
|
|
373
|
+
$AttestSh = Join-Path $ScriptDir "attest-plan.sh"
|
|
374
|
+
$Sh = Get-Command sh -ErrorAction SilentlyContinue
|
|
375
|
+
if ($Sh -and (Test-Path -LiteralPath $AttestSh)) {
|
|
376
|
+
& $Sh.Path $AttestSh *> $null
|
|
377
|
+
}
|
|
378
|
+
}
|
|
224
379
|
} catch {
|
|
225
380
|
# attestation failure must not abort init; the mode marker still stands.
|
|
381
|
+
} finally {
|
|
382
|
+
if ($HadPlanId) {
|
|
383
|
+
$env:PLAN_ID = $PreviousPlanId
|
|
384
|
+
} else {
|
|
385
|
+
Remove-Item Env:PLAN_ID -ErrorAction SilentlyContinue
|
|
386
|
+
}
|
|
226
387
|
}
|
|
227
388
|
}
|
|
228
389
|
|
package/scripts/init-session.sh
CHANGED
|
@@ -83,7 +83,8 @@ done
|
|
|
83
83
|
|
|
84
84
|
DATE=$(date +%Y-%m-%d)
|
|
85
85
|
|
|
86
|
-
|
|
86
|
+
# CDPATH must not redirect the cd that locates the sibling scripts.
|
|
87
|
+
SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
|
|
87
88
|
SKILL_ROOT="$(dirname "$SCRIPT_DIR")"
|
|
88
89
|
TEMPLATE_DIR="$SKILL_ROOT/templates"
|
|
89
90
|
|
|
@@ -382,6 +383,20 @@ if [ "$SLUG_MODE" -eq 1 ]; then
|
|
|
382
383
|
BASE_ID="${DATE}-${SLUG}"
|
|
383
384
|
PLAN_ID="$BASE_ID"
|
|
384
385
|
PLAN_ROOT="${PWD}/.planning"
|
|
386
|
+
PLAN_SELECTOR="${SCRIPT_DIR}/set-active-plan.sh"
|
|
387
|
+
if [ ! -f "${PLAN_SELECTOR}" ]; then
|
|
388
|
+
echo "Error: set-active-plan.sh is required to create a named plan safely." >&2
|
|
389
|
+
exit 1
|
|
390
|
+
fi
|
|
391
|
+
mkdir -p "${PLAN_ROOT}"
|
|
392
|
+
# Verify the physical planning root and the existing pointer before
|
|
393
|
+
# creating anything below it. A symlink or junction that escapes the
|
|
394
|
+
# project must not redirect init writes, and a linked or non-regular
|
|
395
|
+
# pointer must be refused before a plan directory exists on disk. The
|
|
396
|
+
# selector's check is constant time; --list would parse every plan.
|
|
397
|
+
if ! sh "${PLAN_SELECTOR}" --verify-root; then
|
|
398
|
+
exit 1
|
|
399
|
+
fi
|
|
385
400
|
counter=2
|
|
386
401
|
while [ -d "${PLAN_ROOT}/${PLAN_ID}" ]; do
|
|
387
402
|
PLAN_ID="${BASE_ID}-${counter}"
|
|
@@ -393,7 +408,13 @@ if [ "$SLUG_MODE" -eq 1 ]; then
|
|
|
393
408
|
echo "Initializing planning files for: ${PROJECT_NAME:-untitled} (template: $TEMPLATE)"
|
|
394
409
|
echo "PLAN_ID=$PLAN_ID"
|
|
395
410
|
create_files_in "$PLAN_DIR"
|
|
396
|
-
|
|
411
|
+
# Reuse the selector's contained, atomic pointer replacement. Direct shell
|
|
412
|
+
# redirection would truncate a pre-existing hardlink and could overwrite a
|
|
413
|
+
# different file that shares the same inode.
|
|
414
|
+
if ! sh "${PLAN_SELECTOR}" "${PLAN_ID}" >/dev/null; then
|
|
415
|
+
echo "Error: could not safely update ${PLAN_ROOT}/.active_plan." >&2
|
|
416
|
+
exit 1
|
|
417
|
+
fi
|
|
397
418
|
inherit_root_mode
|
|
398
419
|
apply_v3_mode "$PLAN_DIR" "${PLAN_DIR}/task_plan.md"
|
|
399
420
|
echo ""
|
|
@@ -407,9 +407,6 @@ def get_session_candidates(
|
|
|
407
407
|
return 'claude', []
|
|
408
408
|
|
|
409
409
|
|
|
410
|
-
PLANNING_LIKE_SQL = ('%task_plan.md', '%findings.md', '%progress.md')
|
|
411
|
-
|
|
412
|
-
|
|
413
410
|
def get_opencode_db_path() -> Optional[Path]:
|
|
414
411
|
"""Resolve OpenCode SQLite path. Same on all OS per xdg-basedir."""
|
|
415
412
|
xdg = os.environ.get('XDG_DATA_HOME')
|
|
@@ -576,28 +573,42 @@ def opencode_catchup(project_path: str, mode: str = 'no-history') -> None:
|
|
|
576
573
|
update_time = None
|
|
577
574
|
update_idx = -1
|
|
578
575
|
for idx, (sid, _) in enumerate(previous_sessions):
|
|
579
|
-
params = (sid,) + PLANNING_LIKE_SQL
|
|
580
576
|
cur.execute(
|
|
581
577
|
"""
|
|
582
|
-
SELECT time_created FROM part
|
|
578
|
+
SELECT time_created, data FROM part
|
|
583
579
|
WHERE session_id = ?
|
|
584
580
|
AND json_extract(data, '$.type') = 'tool'
|
|
585
581
|
AND lower(json_extract(data, '$.tool')) IN ('write', 'edit', 'patch')
|
|
586
582
|
AND (
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
583
|
+
replace(json_extract(data, '$.state.input.filePath'), char(92), '/')
|
|
584
|
+
IN ('task_plan.md', 'findings.md', 'progress.md')
|
|
585
|
+
OR replace(json_extract(data, '$.state.input.filePath'), char(92), '/')
|
|
586
|
+
GLOB '*/task_plan.md'
|
|
587
|
+
OR replace(json_extract(data, '$.state.input.filePath'), char(92), '/')
|
|
588
|
+
GLOB '*/findings.md'
|
|
589
|
+
OR replace(json_extract(data, '$.state.input.filePath'), char(92), '/')
|
|
590
|
+
GLOB '*/progress.md'
|
|
590
591
|
)
|
|
591
|
-
ORDER BY time_created DESC
|
|
592
|
-
LIMIT 1
|
|
592
|
+
ORDER BY time_created DESC, id DESC
|
|
593
593
|
""",
|
|
594
|
-
|
|
594
|
+
(sid,),
|
|
595
595
|
)
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
596
|
+
# Iterate lazily: write parts carry whole file bodies, and fetchall
|
|
597
|
+
# would materialize every planning write of the session before the
|
|
598
|
+
# first validated row ends the loop.
|
|
599
|
+
for candidate_time, data_str in cur:
|
|
600
|
+
data = json_loads(data_str)
|
|
601
|
+
if not isinstance(data, dict):
|
|
602
|
+
continue
|
|
603
|
+
state = data.get('state')
|
|
604
|
+
input_ = state.get('input') if isinstance(state, dict) else None
|
|
605
|
+
file_path = input_.get('filePath') if isinstance(input_, dict) else None
|
|
606
|
+
if planning_file_from_path(file_path):
|
|
607
|
+
update_sid = sid
|
|
608
|
+
update_time = candidate_time
|
|
609
|
+
update_idx = idx
|
|
610
|
+
break
|
|
611
|
+
if update_sid:
|
|
601
612
|
break
|
|
602
613
|
|
|
603
614
|
if not update_sid:
|
|
@@ -685,12 +696,17 @@ def parse_session_messages(session_file: Path) -> List[Dict[str, Any]]:
|
|
|
685
696
|
|
|
686
697
|
|
|
687
698
|
def planning_file_from_path(path_value: Any) -> Optional[str]:
|
|
699
|
+
"""Return a planning filename only when it is the path's exact basename.
|
|
700
|
+
|
|
701
|
+
A suffix check treats lookalikes such as ``draft_task_plan.md`` as real
|
|
702
|
+
planning updates and can anchor catchup at unrelated transcript content.
|
|
703
|
+
Normalize separators so the same boundary rule works for Unix and Windows
|
|
704
|
+
session records.
|
|
705
|
+
"""
|
|
688
706
|
if not isinstance(path_value, str):
|
|
689
707
|
return None
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
return pf
|
|
693
|
-
return None
|
|
708
|
+
basename = path_value.replace(chr(92), '/').rsplit('/', 1)[-1]
|
|
709
|
+
return basename if basename in PLANNING_FILES else None
|
|
694
710
|
|
|
695
711
|
|
|
696
712
|
def planning_file_from_paths(paths: Iterable[Any]) -> Optional[str]:
|