planning-with-files 3.9.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 +131 -0
- package/SKILL.md +262 -0
- package/examples.md +202 -0
- package/extensions/planning-with-files/README.md +35 -0
- package/extensions/planning-with-files/__tests__/attestation.test.ts +79 -0
- package/extensions/planning-with-files/__tests__/plan-anchor.test.ts +228 -0
- package/extensions/planning-with-files/__tests__/runtime.test.ts +688 -0
- package/extensions/planning-with-files/attestation.ts +55 -0
- package/extensions/planning-with-files/constants.ts +31 -0
- package/extensions/planning-with-files/index.ts +6 -0
- package/extensions/planning-with-files/package.json +17 -0
- package/extensions/planning-with-files/plan.ts +263 -0
- package/extensions/planning-with-files/runtime.ts +788 -0
- package/package.json +46 -0
- package/reference.md +218 -0
- package/scripts/attest-plan.ps1 +137 -0
- package/scripts/attest-plan.sh +206 -0
- package/scripts/check-complete.ps1 +253 -0
- package/scripts/check-complete.sh +253 -0
- package/scripts/init-session.ps1 +230 -0
- package/scripts/init-session.sh +370 -0
- package/scripts/plan-doctor.sh +148 -0
- package/scripts/resolve-plan-dir.ps1 +106 -0
- package/scripts/resolve-plan-dir.sh +263 -0
- package/scripts/session-catchup.py +876 -0
- package/scripts/set-active-plan.ps1 +51 -0
- package/scripts/set-active-plan.sh +50 -0
- package/templates/analytics_findings.md +85 -0
- package/templates/analytics_task_plan.md +106 -0
- package/templates/findings.md +95 -0
- package/templates/progress.md +114 -0
- package/templates/task_plan.md +140 -0
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "planning-with-files",
|
|
3
|
+
"version": "3.9.0",
|
|
4
|
+
"description": "Manus-style file-based planning for Pi Coding Agent",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pi-package",
|
|
7
|
+
"planning",
|
|
8
|
+
"manus",
|
|
9
|
+
"agent",
|
|
10
|
+
"pi-skill"
|
|
11
|
+
],
|
|
12
|
+
"pi": {
|
|
13
|
+
"skills": [
|
|
14
|
+
"SKILL.md"
|
|
15
|
+
],
|
|
16
|
+
"extensions": [
|
|
17
|
+
"extensions/planning-with-files/index.ts"
|
|
18
|
+
]
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"README.md",
|
|
22
|
+
"SKILL.md",
|
|
23
|
+
"examples.md",
|
|
24
|
+
"reference.md",
|
|
25
|
+
"scripts/",
|
|
26
|
+
"templates/",
|
|
27
|
+
"extensions/",
|
|
28
|
+
"!**/node_modules",
|
|
29
|
+
"!**/__pycache__",
|
|
30
|
+
"!**/*.pyc",
|
|
31
|
+
"!extensions/planning-with-files/package-lock.json"
|
|
32
|
+
],
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"@earendil-works/pi-coding-agent": "*"
|
|
35
|
+
},
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "git+https://github.com/OthmanAdi/planning-with-files.git"
|
|
39
|
+
},
|
|
40
|
+
"author": "Ahmad Othman Ammar Adi",
|
|
41
|
+
"license": "MIT",
|
|
42
|
+
"bugs": {
|
|
43
|
+
"url": "https://github.com/OthmanAdi/planning-with-files/issues"
|
|
44
|
+
},
|
|
45
|
+
"homepage": "https://github.com/OthmanAdi/planning-with-files#readme"
|
|
46
|
+
}
|
package/reference.md
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# Reference: Manus Context Engineering Principles
|
|
2
|
+
|
|
3
|
+
This skill is based on context engineering principles from Manus, the AI agent company acquired by Meta for $2 billion in December 2025.
|
|
4
|
+
|
|
5
|
+
## The 6 Manus Principles
|
|
6
|
+
|
|
7
|
+
### Principle 1: Design Around KV-Cache
|
|
8
|
+
|
|
9
|
+
> "KV-cache hit rate is THE single most important metric for production AI agents."
|
|
10
|
+
|
|
11
|
+
**Statistics:**
|
|
12
|
+
- ~100:1 input-to-output token ratio
|
|
13
|
+
- Cached tokens: $0.30/MTok vs Uncached: $3/MTok
|
|
14
|
+
- 10x cost difference!
|
|
15
|
+
|
|
16
|
+
**Implementation:**
|
|
17
|
+
- Keep prompt prefixes STABLE (single-token change invalidates cache)
|
|
18
|
+
- NO timestamps in system prompts
|
|
19
|
+
- Make context APPEND-ONLY with deterministic serialization
|
|
20
|
+
|
|
21
|
+
### Principle 2: Mask, Don't Remove
|
|
22
|
+
|
|
23
|
+
Don't dynamically remove tools (breaks KV-cache). Use logit masking instead.
|
|
24
|
+
|
|
25
|
+
**Best Practice:** Use consistent action prefixes (e.g., `browser_`, `shell_`, `file_`) for easier masking.
|
|
26
|
+
|
|
27
|
+
### Principle 3: Filesystem as External Memory
|
|
28
|
+
|
|
29
|
+
> "Markdown is my 'working memory' on disk."
|
|
30
|
+
|
|
31
|
+
**The Formula:**
|
|
32
|
+
```
|
|
33
|
+
Context Window = RAM (volatile, limited)
|
|
34
|
+
Filesystem = Disk (persistent, unlimited)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
**Compression Must Be Restorable:**
|
|
38
|
+
- Keep URLs even if web content is dropped
|
|
39
|
+
- Keep file paths when dropping document contents
|
|
40
|
+
- Never lose the pointer to full data
|
|
41
|
+
|
|
42
|
+
### Principle 4: Manipulate Attention Through Recitation
|
|
43
|
+
|
|
44
|
+
> "Creates and updates todo.md throughout tasks to push global plan into model's recent attention span."
|
|
45
|
+
|
|
46
|
+
**Problem:** After ~50 tool calls, models forget original goals ("lost in the middle" effect).
|
|
47
|
+
|
|
48
|
+
**Solution:** Re-read `task_plan.md` before each decision. Goals appear in the attention window.
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
Start of context: [Original goal - far away, forgotten]
|
|
52
|
+
...many tool calls...
|
|
53
|
+
End of context: [Recently read task_plan.md - gets ATTENTION!]
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Principle 5: Keep the Wrong Stuff In
|
|
57
|
+
|
|
58
|
+
> "Leave the wrong turns in the context."
|
|
59
|
+
|
|
60
|
+
**Why:**
|
|
61
|
+
- Failed actions with stack traces let model implicitly update beliefs
|
|
62
|
+
- Reduces mistake repetition
|
|
63
|
+
- Error recovery is "one of the clearest signals of TRUE agentic behavior"
|
|
64
|
+
|
|
65
|
+
### Principle 6: Don't Get Few-Shotted
|
|
66
|
+
|
|
67
|
+
> "Uniformity breeds fragility."
|
|
68
|
+
|
|
69
|
+
**Problem:** Repetitive action-observation pairs cause drift and hallucination.
|
|
70
|
+
|
|
71
|
+
**Solution:** Introduce controlled variation:
|
|
72
|
+
- Vary phrasings slightly
|
|
73
|
+
- Don't copy-paste patterns blindly
|
|
74
|
+
- Recalibrate on repetitive tasks
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## The 3 Context Engineering Strategies
|
|
79
|
+
|
|
80
|
+
Based on Lance Martin's analysis of Manus architecture.
|
|
81
|
+
|
|
82
|
+
### Strategy 1: Context Reduction
|
|
83
|
+
|
|
84
|
+
**Compaction:**
|
|
85
|
+
```
|
|
86
|
+
Tool calls have TWO representations:
|
|
87
|
+
├── FULL: Raw tool content (stored in filesystem)
|
|
88
|
+
└── COMPACT: Reference/file path only
|
|
89
|
+
|
|
90
|
+
RULES:
|
|
91
|
+
- Apply compaction to STALE (older) tool results
|
|
92
|
+
- Keep RECENT results FULL (to guide next decision)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
**Summarization:**
|
|
96
|
+
- Applied when compaction reaches diminishing returns
|
|
97
|
+
- Generated using full tool results
|
|
98
|
+
- Creates standardized summary objects
|
|
99
|
+
|
|
100
|
+
### Strategy 2: Context Isolation (Multi-Agent)
|
|
101
|
+
|
|
102
|
+
**Architecture:**
|
|
103
|
+
```
|
|
104
|
+
┌─────────────────────────────────┐
|
|
105
|
+
│ PLANNER AGENT │
|
|
106
|
+
│ └─ Assigns tasks to sub-agents │
|
|
107
|
+
├─────────────────────────────────┤
|
|
108
|
+
│ KNOWLEDGE MANAGER │
|
|
109
|
+
│ └─ Reviews conversations │
|
|
110
|
+
│ └─ Determines filesystem store │
|
|
111
|
+
├─────────────────────────────────┤
|
|
112
|
+
│ EXECUTOR SUB-AGENTS │
|
|
113
|
+
│ └─ Perform assigned tasks │
|
|
114
|
+
│ └─ Have own context windows │
|
|
115
|
+
└─────────────────────────────────┘
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
**Key Insight:** Manus originally used `todo.md` for task planning but found ~33% of actions were spent updating it. Shifted to dedicated planner agent calling executor sub-agents.
|
|
119
|
+
|
|
120
|
+
### Strategy 3: Context Offloading
|
|
121
|
+
|
|
122
|
+
**Tool Design:**
|
|
123
|
+
- Use <20 atomic functions total
|
|
124
|
+
- Store full results in filesystem, not context
|
|
125
|
+
- Use `glob` and `grep` for searching
|
|
126
|
+
- Progressive disclosure: load information only as needed
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## The Agent Loop
|
|
131
|
+
|
|
132
|
+
Manus operates in a continuous 7-step loop:
|
|
133
|
+
|
|
134
|
+
```
|
|
135
|
+
┌─────────────────────────────────────────┐
|
|
136
|
+
│ 1. ANALYZE CONTEXT │
|
|
137
|
+
│ - Understand user intent │
|
|
138
|
+
│ - Assess current state │
|
|
139
|
+
│ - Review recent observations │
|
|
140
|
+
├─────────────────────────────────────────┤
|
|
141
|
+
│ 2. THINK │
|
|
142
|
+
│ - Should I update the plan? │
|
|
143
|
+
│ - What's the next logical action? │
|
|
144
|
+
│ - Are there blockers? │
|
|
145
|
+
├─────────────────────────────────────────┤
|
|
146
|
+
│ 3. SELECT TOOL │
|
|
147
|
+
│ - Choose ONE tool │
|
|
148
|
+
│ - Ensure parameters available │
|
|
149
|
+
├─────────────────────────────────────────┤
|
|
150
|
+
│ 4. EXECUTE ACTION │
|
|
151
|
+
│ - Tool runs in sandbox │
|
|
152
|
+
├─────────────────────────────────────────┤
|
|
153
|
+
│ 5. RECEIVE OBSERVATION │
|
|
154
|
+
│ - Result appended to context │
|
|
155
|
+
├─────────────────────────────────────────┤
|
|
156
|
+
│ 6. ITERATE │
|
|
157
|
+
│ - Return to step 1 │
|
|
158
|
+
│ - Continue until complete │
|
|
159
|
+
├─────────────────────────────────────────┤
|
|
160
|
+
│ 7. DELIVER OUTCOME │
|
|
161
|
+
│ - Send results to user │
|
|
162
|
+
│ - Attach all relevant files │
|
|
163
|
+
└─────────────────────────────────────────┘
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## File Types Manus Creates
|
|
169
|
+
|
|
170
|
+
| File | Purpose | When Created | When Updated |
|
|
171
|
+
|------|---------|--------------|--------------|
|
|
172
|
+
| `task_plan.md` | Phase tracking, progress | Task start | After completing phases |
|
|
173
|
+
| `findings.md` | Discoveries, decisions | After ANY discovery | After viewing images/PDFs |
|
|
174
|
+
| `progress.md` | Session log, what's done | At breakpoints | Throughout session |
|
|
175
|
+
| Code files | Implementation | Before execution | After errors |
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## Critical Constraints
|
|
180
|
+
|
|
181
|
+
- **Single-Action Execution (Manus 2025 original constraint):** ONE tool call per turn, no parallel execution. This documents Manus's 2025 sandbox practice. **2026 update:** modern hosts (Claude Code, Codex CLI) support parallel tool calls and subagents, so this constraint no longer applies as written. The plan file, not the one-call-per-turn rule, remains the coordination point: parallel calls and subagents share state through the durable markdown plan on disk.
|
|
182
|
+
- **Plan is Required:** Agent must ALWAYS know: goal, current phase, remaining phases
|
|
183
|
+
- **Files are Memory:** Context = volatile. Filesystem = persistent.
|
|
184
|
+
- **Never Repeat Failures:** If action failed, next action MUST be different
|
|
185
|
+
- **Communication is a Tool:** Message types: `info` (progress), `ask` (blocking), `result` (terminal)
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## Manus Statistics
|
|
190
|
+
|
|
191
|
+
| Metric | Value |
|
|
192
|
+
|--------|-------|
|
|
193
|
+
| Average tool calls per task | ~50 |
|
|
194
|
+
| Input-to-output token ratio | 100:1 |
|
|
195
|
+
| Acquisition price | $2 billion |
|
|
196
|
+
| Time to $100M revenue | 8 months |
|
|
197
|
+
| Framework refactors since launch | 5 times |
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## Key Quotes
|
|
202
|
+
|
|
203
|
+
> "Context window = RAM (volatile, limited). Filesystem = Disk (persistent, unlimited). Anything important gets written to disk."
|
|
204
|
+
|
|
205
|
+
> "if action_failed: next_action != same_action. Track what you tried. Mutate the approach."
|
|
206
|
+
|
|
207
|
+
> "Error recovery is one of the clearest signals of TRUE agentic behavior."
|
|
208
|
+
|
|
209
|
+
> "KV-cache hit rate is the single most important metric for a production-stage AI agent."
|
|
210
|
+
|
|
211
|
+
> "Leave the wrong turns in the context."
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
## Source
|
|
216
|
+
|
|
217
|
+
Based on Manus's official context engineering documentation:
|
|
218
|
+
https://manus.im/blog/Context-Engineering-for-AI-Agents-Lessons-from-Building-Manus
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
#requires -Version 5.0
|
|
2
|
+
<#
|
|
3
|
+
.SYNOPSIS
|
|
4
|
+
Lock the current task_plan.md content with a SHA-256 attestation.
|
|
5
|
+
|
|
6
|
+
.DESCRIPTION
|
|
7
|
+
Use after you finalise (or intentionally edit) a plan. The hooks then refuse
|
|
8
|
+
to inject plan content into the model context if the file diverges from the
|
|
9
|
+
attested hash, surfacing a "[PLAN TAMPERED]" warning instead.
|
|
10
|
+
|
|
11
|
+
Plan resolution:
|
|
12
|
+
1. $env:PLAN_ID -> ./.planning/$PLAN_ID/
|
|
13
|
+
2. ./.planning/.active_plan
|
|
14
|
+
3. Newest ./.planning/<dir>/ by LastWriteTime
|
|
15
|
+
4. Legacy ./task_plan.md at project root
|
|
16
|
+
|
|
17
|
+
.PARAMETER Show
|
|
18
|
+
Print the stored hash for the active plan.
|
|
19
|
+
|
|
20
|
+
.PARAMETER Clear
|
|
21
|
+
Remove the attestation (re-open the plan).
|
|
22
|
+
#>
|
|
23
|
+
[CmdletBinding(DefaultParameterSetName = "Attest")]
|
|
24
|
+
param(
|
|
25
|
+
[Parameter(ParameterSetName = "Show")]
|
|
26
|
+
[switch] $Show,
|
|
27
|
+
|
|
28
|
+
[Parameter(ParameterSetName = "Clear")]
|
|
29
|
+
[switch] $Clear
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
$ErrorActionPreference = "Stop"
|
|
33
|
+
|
|
34
|
+
function Resolve-PlanFile {
|
|
35
|
+
$planRoot = Join-Path (Get-Location) ".planning"
|
|
36
|
+
|
|
37
|
+
if ($env:PLAN_ID) {
|
|
38
|
+
$candidate = Join-Path $planRoot $env:PLAN_ID
|
|
39
|
+
$planFile = Join-Path $candidate "task_plan.md"
|
|
40
|
+
if (Test-Path -LiteralPath $planFile) { return (Resolve-Path -LiteralPath $planFile).Path }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
$activePointer = Join-Path $planRoot ".active_plan"
|
|
44
|
+
if (Test-Path -LiteralPath $activePointer) {
|
|
45
|
+
$planId = (Get-Content -LiteralPath $activePointer -Raw).Trim()
|
|
46
|
+
if ($planId) {
|
|
47
|
+
$candidate = Join-Path $planRoot $planId
|
|
48
|
+
$planFile = Join-Path $candidate "task_plan.md"
|
|
49
|
+
if (Test-Path -LiteralPath $planFile) { return (Resolve-Path -LiteralPath $planFile).Path }
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (Test-Path -LiteralPath $planRoot) {
|
|
54
|
+
$newest = Get-ChildItem -LiteralPath $planRoot -Directory -ErrorAction SilentlyContinue |
|
|
55
|
+
Where-Object { -not $_.Name.StartsWith(".") } |
|
|
56
|
+
Where-Object { Test-Path -LiteralPath (Join-Path $_.FullName "task_plan.md") } |
|
|
57
|
+
Sort-Object LastWriteTime -Descending |
|
|
58
|
+
Select-Object -First 1
|
|
59
|
+
if ($newest) {
|
|
60
|
+
return (Resolve-Path -LiteralPath (Join-Path $newest.FullName "task_plan.md")).Path
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
$legacy = Join-Path (Get-Location) "task_plan.md"
|
|
65
|
+
if (Test-Path -LiteralPath $legacy) {
|
|
66
|
+
return (Resolve-Path -LiteralPath $legacy).Path
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return $null
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function Get-AttestationPath {
|
|
73
|
+
param([string] $PlanFile)
|
|
74
|
+
$planDir = Split-Path -Parent $PlanFile
|
|
75
|
+
$cwd = (Get-Location).Path
|
|
76
|
+
if ($planDir -eq $cwd) {
|
|
77
|
+
return (Join-Path $cwd ".plan-attestation")
|
|
78
|
+
}
|
|
79
|
+
return (Join-Path $planDir ".attestation")
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
$planFile = Resolve-PlanFile
|
|
83
|
+
if (-not $planFile) {
|
|
84
|
+
Write-Error "[plan-attest] No task_plan.md found. Create a plan first."
|
|
85
|
+
exit 1
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
$attestationFile = Get-AttestationPath -PlanFile $planFile
|
|
89
|
+
|
|
90
|
+
if ($Show) {
|
|
91
|
+
if (Test-Path -LiteralPath $attestationFile) {
|
|
92
|
+
Write-Output "Plan: $planFile"
|
|
93
|
+
Write-Output "Attestation: $attestationFile"
|
|
94
|
+
Write-Output ("SHA-256: " + (Get-Content -LiteralPath $attestationFile -Raw).Trim())
|
|
95
|
+
# Nonce (security A1.4): surface the per-plan nonce if init-session
|
|
96
|
+
# generated one next to the attestation. Informational only here; the
|
|
97
|
+
# hooks consume it to build collision-proof BEGIN/END delimiters.
|
|
98
|
+
$nonceFile = Join-Path (Split-Path -Parent $attestationFile) ".nonce"
|
|
99
|
+
if (Test-Path -LiteralPath $nonceFile) {
|
|
100
|
+
$nonceVal = (Get-Content -LiteralPath $nonceFile -Raw).Trim()
|
|
101
|
+
if ($nonceVal) { Write-Output "Nonce: $nonceVal" }
|
|
102
|
+
}
|
|
103
|
+
} else {
|
|
104
|
+
Write-Output "[plan-attest] No attestation set for $planFile."
|
|
105
|
+
exit 1
|
|
106
|
+
}
|
|
107
|
+
exit 0
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if ($Clear) {
|
|
111
|
+
if (Test-Path -LiteralPath $attestationFile) {
|
|
112
|
+
Remove-Item -LiteralPath $attestationFile -Force
|
|
113
|
+
Write-Output "[plan-attest] Cleared attestation for $planFile."
|
|
114
|
+
} else {
|
|
115
|
+
Write-Output "[plan-attest] No attestation to clear."
|
|
116
|
+
}
|
|
117
|
+
exit 0
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
$hashVal = (Get-FileHash -LiteralPath $planFile -Algorithm SHA256).Hash.ToLowerInvariant()
|
|
121
|
+
Set-Content -LiteralPath $attestationFile -Value $hashVal -NoNewline -Encoding ascii
|
|
122
|
+
|
|
123
|
+
# Integrity verification (security A2.1): confirm the on-disk attestation
|
|
124
|
+
# matches the intended hash before reporting success. A silent write failure
|
|
125
|
+
# (permissions, full disk) must not leave a stale attestation and exit clean.
|
|
126
|
+
$storedHash = (Get-Content -LiteralPath $attestationFile -Raw -ErrorAction SilentlyContinue)
|
|
127
|
+
if ($null -ne $storedHash) { $storedHash = $storedHash.Trim() }
|
|
128
|
+
if ($storedHash -ne $hashVal) {
|
|
129
|
+
Write-Error "[plan-attest] Attestation write verification FAILED for $attestationFile. Expected $hashVal, found $storedHash. The plan is NOT attested."
|
|
130
|
+
exit 1
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
$short = $hashVal.Substring(0, 12)
|
|
134
|
+
Write-Output "[plan-attest] Locked $planFile"
|
|
135
|
+
Write-Output "[plan-attest] SHA-256: $short... (stored in $attestationFile)"
|
|
136
|
+
Write-Output "[plan-attest] Hooks will block injection if the file is modified without re-running this command."
|
|
137
|
+
exit 0
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# planning-with-files: lock the current task_plan.md content with a SHA-256 attestation.
|
|
3
|
+
#
|
|
4
|
+
# Use after you finalise (or intentionally edit) a plan. The hooks then refuse
|
|
5
|
+
# to inject plan content into the model context if the file diverges from the
|
|
6
|
+
# attested hash, surfacing a "[PLAN TAMPERED]" warning instead.
|
|
7
|
+
#
|
|
8
|
+
# Resolution:
|
|
9
|
+
# 1. $PLAN_ID env var → ./.planning/$PLAN_ID/
|
|
10
|
+
# 2. ./.planning/.active_plan
|
|
11
|
+
# 3. Newest ./.planning/<dir>/ by mtime
|
|
12
|
+
# 4. Legacy ./task_plan.md at project root
|
|
13
|
+
#
|
|
14
|
+
# Usage:
|
|
15
|
+
# sh scripts/attest-plan.sh # attest the active plan
|
|
16
|
+
# sh scripts/attest-plan.sh --show # print the stored hash
|
|
17
|
+
# sh scripts/attest-plan.sh --clear # remove the attestation (re-open the plan)
|
|
18
|
+
|
|
19
|
+
set -u
|
|
20
|
+
|
|
21
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
22
|
+
RESOLVER="${SCRIPT_DIR}/resolve-plan-dir.sh"
|
|
23
|
+
|
|
24
|
+
resolve_plan_file() {
|
|
25
|
+
plan_dir=""
|
|
26
|
+
if [ -f "${RESOLVER}" ]; then
|
|
27
|
+
plan_dir="$(sh "${RESOLVER}" 2>/dev/null)"
|
|
28
|
+
fi
|
|
29
|
+
if [ -n "${plan_dir}" ] && [ -f "${plan_dir}/task_plan.md" ]; then
|
|
30
|
+
printf "%s\n" "${plan_dir}/task_plan.md"
|
|
31
|
+
return 0
|
|
32
|
+
fi
|
|
33
|
+
if [ -f "./task_plan.md" ]; then
|
|
34
|
+
printf "%s\n" "./task_plan.md"
|
|
35
|
+
return 0
|
|
36
|
+
fi
|
|
37
|
+
return 1
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
attestation_path_for() {
|
|
41
|
+
plan_file="$1"
|
|
42
|
+
plan_dir="$(dirname "${plan_file}")"
|
|
43
|
+
if [ "${plan_dir}" = "." ]; then
|
|
44
|
+
# Legacy mode: store at project root.
|
|
45
|
+
printf "%s\n" "./.plan-attestation"
|
|
46
|
+
else
|
|
47
|
+
printf "%s\n" "${plan_dir}/.attestation"
|
|
48
|
+
fi
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
compute_hash() {
|
|
52
|
+
target="$1"
|
|
53
|
+
if command -v sha256sum >/dev/null 2>&1; then
|
|
54
|
+
sha256sum "${target}" | awk '{print $1}'
|
|
55
|
+
elif command -v shasum >/dev/null 2>&1; then
|
|
56
|
+
shasum -a 256 "${target}" | awk '{print $1}'
|
|
57
|
+
else
|
|
58
|
+
printf "ERROR: no sha256 utility available\n" >&2
|
|
59
|
+
return 1
|
|
60
|
+
fi
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
mode="attest"
|
|
64
|
+
case "${1:-}" in
|
|
65
|
+
--show) mode="show" ;;
|
|
66
|
+
--clear) mode="clear" ;;
|
|
67
|
+
"") mode="attest" ;;
|
|
68
|
+
*)
|
|
69
|
+
printf "Usage: %s [--show|--clear]\n" "$0" >&2
|
|
70
|
+
exit 2
|
|
71
|
+
;;
|
|
72
|
+
esac
|
|
73
|
+
|
|
74
|
+
plan_file="$(resolve_plan_file)" || {
|
|
75
|
+
printf "[plan-attest] No task_plan.md found. Create a plan first.\n" >&2
|
|
76
|
+
exit 1
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
attestation_file="$(attestation_path_for "${plan_file}")"
|
|
80
|
+
|
|
81
|
+
case "${mode}" in
|
|
82
|
+
show)
|
|
83
|
+
if [ -f "${attestation_file}" ]; then
|
|
84
|
+
printf "Plan: %s\n" "${plan_file}"
|
|
85
|
+
printf "Attestation: %s\n" "${attestation_file}"
|
|
86
|
+
printf "SHA-256: %s\n" "$(cat "${attestation_file}")"
|
|
87
|
+
# Nonce (security A1.4): if init-session generated a per-plan nonce
|
|
88
|
+
# next to the attestation, surface it. Informational only here; the
|
|
89
|
+
# hooks consume it to build collision-proof BEGIN/END delimiters.
|
|
90
|
+
nonce_file="$(dirname "${attestation_file}")/.nonce"
|
|
91
|
+
if [ -f "${nonce_file}" ]; then
|
|
92
|
+
printf "Nonce: %s\n" "$(tr -d '\r\n[:space:]' < "${nonce_file}" 2>/dev/null)"
|
|
93
|
+
fi
|
|
94
|
+
else
|
|
95
|
+
printf "[plan-attest] No attestation set for %s.\n" "${plan_file}"
|
|
96
|
+
exit 1
|
|
97
|
+
fi
|
|
98
|
+
;;
|
|
99
|
+
clear)
|
|
100
|
+
if [ -f "${attestation_file}" ]; then
|
|
101
|
+
rm -f "${attestation_file}"
|
|
102
|
+
printf "[plan-attest] Cleared attestation for %s.\n" "${plan_file}"
|
|
103
|
+
else
|
|
104
|
+
printf "[plan-attest] No attestation to clear.\n"
|
|
105
|
+
fi
|
|
106
|
+
;;
|
|
107
|
+
attest)
|
|
108
|
+
hash_val="$(compute_hash "${plan_file}")" || exit 1
|
|
109
|
+
|
|
110
|
+
# v2.40: protect the write with an advisory flock when available so
|
|
111
|
+
# concurrent legacy-mode sessions (no PLAN_ID, both at the same project
|
|
112
|
+
# root) cannot corrupt the .plan-attestation file mid-write. Atomic
|
|
113
|
+
# rename of a temp file is the real guarantee on POSIX; flock is the
|
|
114
|
+
# cooperative gate around the rename for slow-disk writes.
|
|
115
|
+
#
|
|
116
|
+
# Note: legacy single-file mode is inherently racey across concurrent
|
|
117
|
+
# sessions because both can edit task_plan.md without coordination. The
|
|
118
|
+
# canonical parallel-session pattern is slug-mode under
|
|
119
|
+
# .planning/<slug>/, where each session pins PLAN_ID and gets its own
|
|
120
|
+
# .attestation file. We surface a hint when concurrent activity is
|
|
121
|
+
# detected.
|
|
122
|
+
if [ -f "${attestation_file}" ]; then
|
|
123
|
+
mtime_now="$(date +%s 2>/dev/null || echo 0)"
|
|
124
|
+
mtime_prev="$(stat -c '%Y' "${attestation_file}" 2>/dev/null \
|
|
125
|
+
|| stat -f '%m' "${attestation_file}" 2>/dev/null \
|
|
126
|
+
|| echo 0)"
|
|
127
|
+
age=$((mtime_now - mtime_prev))
|
|
128
|
+
if [ "${age}" -ge 0 ] && [ "${age}" -lt 30 ] 2>/dev/null; then
|
|
129
|
+
# If we're in legacy mode (root .plan-attestation) and another
|
|
130
|
+
# session just wrote, warn. Slug-mode files in .planning/<slug>/
|
|
131
|
+
# are per-session by construction; no need to warn there.
|
|
132
|
+
case "${attestation_file}" in
|
|
133
|
+
*./.plan-attestation|*/.plan-attestation)
|
|
134
|
+
case "${attestation_file}" in
|
|
135
|
+
*./.planning/*) : ;; # slug-mode, ignore
|
|
136
|
+
*)
|
|
137
|
+
printf "[plan-attest] Note: %s was modified %ss ago by another process.\n" \
|
|
138
|
+
"${attestation_file}" "${age}" >&2
|
|
139
|
+
printf "[plan-attest] For parallel sessions, prefer slug-mode (init-session.sh <name>) so each session gets its own .attestation file.\n" >&2
|
|
140
|
+
;;
|
|
141
|
+
esac
|
|
142
|
+
;;
|
|
143
|
+
esac
|
|
144
|
+
fi
|
|
145
|
+
fi
|
|
146
|
+
|
|
147
|
+
tmp_file="${attestation_file}.tmp.$$"
|
|
148
|
+
printf "%s\n" "${hash_val}" > "${tmp_file}" 2>/dev/null || {
|
|
149
|
+
printf "[plan-attest] Failed to write %s\n" "${tmp_file}" >&2
|
|
150
|
+
exit 1
|
|
151
|
+
}
|
|
152
|
+
mv_ok=1
|
|
153
|
+
if command -v flock >/dev/null 2>&1; then
|
|
154
|
+
# Advisory lock around the rename. lock_dir is the dir containing
|
|
155
|
+
# the target file. The {} subshell pattern keeps the lock scoped to
|
|
156
|
+
# the mv call.
|
|
157
|
+
lock_dir="$(dirname "${attestation_file}")"
|
|
158
|
+
(
|
|
159
|
+
flock -w 5 9 || true
|
|
160
|
+
mv -f "${tmp_file}" "${attestation_file}"
|
|
161
|
+
) 9>"${lock_dir}/.attestation.lock" 2>/dev/null || mv_ok=0
|
|
162
|
+
rm -f "${lock_dir}/.attestation.lock" 2>/dev/null
|
|
163
|
+
else
|
|
164
|
+
mv -f "${tmp_file}" "${attestation_file}" 2>/dev/null || mv_ok=0
|
|
165
|
+
fi
|
|
166
|
+
|
|
167
|
+
# Integrity gap fix (security A2.1): a failed atomic rename must not be
|
|
168
|
+
# allowed to silently leave a stale attestation when the target already
|
|
169
|
+
# existed. The old fallback only wrote when the file was absent, so a
|
|
170
|
+
# cross-device or permission-denied mv on an existing attestation left
|
|
171
|
+
# the OLD hash in place with a success exit. On mv failure we re-write
|
|
172
|
+
# the intended hash through a second atomic rename (never a bare
|
|
173
|
+
# redirect onto the live file, which would expose torn reads to
|
|
174
|
+
# concurrent verifiers), then verify the on-disk content.
|
|
175
|
+
if [ "${mv_ok}" -eq 0 ] || [ ! -f "${attestation_file}" ]; then
|
|
176
|
+
fb_tmp="${attestation_file}.fb.$$"
|
|
177
|
+
printf "%s\n" "${hash_val}" > "${fb_tmp}" 2>/dev/null \
|
|
178
|
+
&& mv -f "${fb_tmp}" "${attestation_file}" 2>/dev/null || {
|
|
179
|
+
rm -f "${fb_tmp}" "${tmp_file}" 2>/dev/null
|
|
180
|
+
printf "[plan-attest] Failed to write attestation %s\n" "${attestation_file}" >&2
|
|
181
|
+
exit 1
|
|
182
|
+
}
|
|
183
|
+
fi
|
|
184
|
+
rm -f "${tmp_file}" 2>/dev/null
|
|
185
|
+
|
|
186
|
+
# Read-back verification. Both write paths above are atomic renames, so
|
|
187
|
+
# a concurrent verifier always reads a complete 64-hex hash — either our
|
|
188
|
+
# own or an identical one from a peer attesting the same plan content.
|
|
189
|
+
# A mismatch here therefore means our intended hash genuinely did not
|
|
190
|
+
# land (stale content, failed write); fail loudly with a nonzero exit so
|
|
191
|
+
# callers never trust a stale attestation.
|
|
192
|
+
stored_hash="$(tr -d '\r\n[:space:]' < "${attestation_file}" 2>/dev/null)"
|
|
193
|
+
if [ "${stored_hash}" != "${hash_val}" ]; then
|
|
194
|
+
printf "[plan-attest] Attestation write verification FAILED for %s\n" "${attestation_file}" >&2
|
|
195
|
+
printf "[plan-attest] Expected %s, found %s. The plan is NOT attested.\n" "${hash_val}" "${stored_hash}" >&2
|
|
196
|
+
exit 1
|
|
197
|
+
fi
|
|
198
|
+
|
|
199
|
+
short_hash="$(printf "%s" "${hash_val}" | cut -c1-12)"
|
|
200
|
+
printf "[plan-attest] Locked %s\n" "${plan_file}"
|
|
201
|
+
printf "[plan-attest] SHA-256: %s... (stored in %s)\n" "${short_hash}" "${attestation_file}"
|
|
202
|
+
printf "[plan-attest] Hooks will block injection if the file is modified without re-running this command.\n"
|
|
203
|
+
;;
|
|
204
|
+
esac
|
|
205
|
+
|
|
206
|
+
exit 0
|