pi-soly 2.1.4 → 2.1.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/built-in-rules/temp-files.md +173 -0
- package/core.ts +39 -8
- package/package.json +3 -2
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# Temporary Files Rule
|
|
2
|
+
|
|
3
|
+
> **OS-aware temp paths.** Never hardcode `/tmp` — it's wrong on Windows
|
|
4
|
+
> (`%TEMP%` resolves to `C:\Users\<user>\AppData\Local\Temp`, not `/tmp`),
|
|
5
|
+
> wrong on macOS (`$TMPDIR` is per-user, e.g. `/var/folders/xx/yy/T/`),
|
|
6
|
+
> and fragile in multi-tenant / CI scenarios. Always resolve the
|
|
7
|
+
> OS-correct temp dir at runtime and put files under a project-scoped
|
|
8
|
+
> subfolder.
|
|
9
|
+
|
|
10
|
+
## The rule
|
|
11
|
+
|
|
12
|
+
When creating temporary files — intermediate build artifacts, scratch
|
|
13
|
+
data, debug dumps, test fixtures, atomic-write staging, anything that
|
|
14
|
+
isn't meant to outlive the process:
|
|
15
|
+
|
|
16
|
+
1. **Resolve the OS temp dir at runtime.** Never hardcode `/tmp`,
|
|
17
|
+
`/var/folders`, `C:\Temp`, `~/tmp`, or any other literal path.
|
|
18
|
+
2. **Use a project-scoped subfolder** like `<tempdir>/<project>-<hash>/`
|
|
19
|
+
so multiple projects don't collide and cleanup is easy.
|
|
20
|
+
3. **Clean up after yourself** — when the work is done, remove the
|
|
21
|
+
directory (or use `try`/`finally` / `trap EXIT` / `defer`).
|
|
22
|
+
4. **Prefer atomic-write patterns** for files that will be renamed into
|
|
23
|
+
place: write to a temp file in the **same directory** as the target,
|
|
24
|
+
`fsync` if durability matters, then rename. Avoids half-written
|
|
25
|
+
files on crash and avoids cross-device rename failures.
|
|
26
|
+
|
|
27
|
+
## How to get the temp dir per runtime
|
|
28
|
+
|
|
29
|
+
| Runtime | API | Notes |
|
|
30
|
+
|---------------|------------------------------|------------------------------------|
|
|
31
|
+
| Node.js / Bun | `os.tmpdir()` | already OS-aware; returns absolute |
|
|
32
|
+
| Python | `tempfile.gettempdir()` | stdlib |
|
|
33
|
+
| Go | `os.TempDir()` | stdlib |
|
|
34
|
+
| Rust | `std::env::temp_dir()` | stdlib; returns `PathBuf` |
|
|
35
|
+
| .NET | `Path.GetTempPath()` | returns trailing slash |
|
|
36
|
+
| Java | `System.getProperty("java.io.tmpdir")` | JVM-resolved |
|
|
37
|
+
| Bash (POSIX) | `${TMPDIR:-/tmp}` | fallback only if env unset |
|
|
38
|
+
| PowerShell | `$env:TEMP` | Windows |
|
|
39
|
+
| cmd.exe | `%TEMP%` | Windows |
|
|
40
|
+
|
|
41
|
+
**Do not** `cd /tmp && touch foo`. Use the API for the runtime you're in.
|
|
42
|
+
|
|
43
|
+
## Why this matters
|
|
44
|
+
|
|
45
|
+
- **Windows**: `C:\Temp` may not exist or be writable; the real temp is
|
|
46
|
+
`C:\Users\<user>\AppData\Local\Temp`. Hardcoding `/tmp` either fails
|
|
47
|
+
outright or pollutes the wrong location.
|
|
48
|
+
- **macOS**: `$TMPDIR` is per-user and changes per reboot. `/tmp` is a
|
|
49
|
+
shared system directory, not appropriate for app scratch data.
|
|
50
|
+
- **Linux / multi-user**: `/tmp` is shared across users on the host.
|
|
51
|
+
Mixing files across users is a data-leak / collision risk.
|
|
52
|
+
- **CI/CD**: runners typically wipe `/tmp` between jobs but leave the
|
|
53
|
+
per-user temp dir intact, so using the right dir keeps artifacts
|
|
54
|
+
available for inspection post-run.
|
|
55
|
+
- **Sandboxing**: macOS sandbox and certain Linux sandboxes redirect
|
|
56
|
+
`/tmp` to a per-process private dir. The runtime API follows these
|
|
57
|
+
redirects; hardcoded `/tmp` does not.
|
|
58
|
+
|
|
59
|
+
## Examples
|
|
60
|
+
|
|
61
|
+
### Node.js / Bun
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
import * as fs from "node:fs";
|
|
65
|
+
import * as os from "node:os";
|
|
66
|
+
import * as path from "node:path";
|
|
67
|
+
|
|
68
|
+
const projectId = "pi-soly"; // or hash(repoUrl) for uniqueness
|
|
69
|
+
const scratchDir = path.join(os.tmpdir(), `${projectId}-${process.pid}`);
|
|
70
|
+
fs.mkdirSync(scratchDir, { recursive: true });
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
// ... use scratchDir for intermediate files ...
|
|
74
|
+
} finally {
|
|
75
|
+
fs.rmSync(scratchDir, { recursive: true, force: true });
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
For atomic writes to a target file:
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
import * as fs from "node:fs";
|
|
83
|
+
import * as os from "node:os";
|
|
84
|
+
import * as path from "node:path";
|
|
85
|
+
|
|
86
|
+
function atomicWrite(target: string, data: string | Uint8Array): void {
|
|
87
|
+
const dir = path.dirname(target);
|
|
88
|
+
const tmp = path.join(dir, `.${path.basename(target)}.${process.pid}.tmp`);
|
|
89
|
+
const fh = fs.openSync(tmp, "w");
|
|
90
|
+
try {
|
|
91
|
+
fs.writeSync(fh, data);
|
|
92
|
+
fs.fsyncSync(fh);
|
|
93
|
+
} finally {
|
|
94
|
+
fs.closeSync(fh);
|
|
95
|
+
}
|
|
96
|
+
fs.renameSync(tmp, target); // atomic on same filesystem
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Python
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
import shutil
|
|
104
|
+
import tempfile
|
|
105
|
+
|
|
106
|
+
scratch = tempfile.mkdtemp(prefix="myproject-")
|
|
107
|
+
try:
|
|
108
|
+
# ... use scratch ...
|
|
109
|
+
pass
|
|
110
|
+
finally:
|
|
111
|
+
shutil.rmtree(scratch, ignore_errors=True)
|
|
112
|
+
|
|
113
|
+
# Atomic write:
|
|
114
|
+
import os
|
|
115
|
+
def atomic_write(path: str, data: bytes) -> None:
|
|
116
|
+
tmp = f"{path}.{os.getpid()}.tmp"
|
|
117
|
+
with open(tmp, "wb") as f:
|
|
118
|
+
f.write(data)
|
|
119
|
+
f.flush()
|
|
120
|
+
os.fsync(f.fileno())
|
|
121
|
+
os.replace(tmp, path)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Bash (POSIX: Linux / macOS)
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
SCRATCH="${TMPDIR:-/tmp}/myproject-$$"
|
|
128
|
+
mkdir -p "$SCRATCH"
|
|
129
|
+
trap 'rm -rf "$SCRATCH"' EXIT
|
|
130
|
+
# ... work in "$SCRATCH" ...
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### PowerShell (Windows)
|
|
134
|
+
|
|
135
|
+
```powershell
|
|
136
|
+
$scratch = Join-Path $env:TEMP "myproject-$PID"
|
|
137
|
+
New-Item -ItemType Directory -Path $scratch -Force | Out-Null
|
|
138
|
+
try {
|
|
139
|
+
# ... work in $scratch ...
|
|
140
|
+
} finally {
|
|
141
|
+
Remove-Item -Recurse -Force $scratch -ErrorAction SilentlyContinue
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Forbidden patterns
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
// ❌ Hardcoded path — breaks on Windows, breaks sandboxing
|
|
149
|
+
const dir = "/tmp/myapp";
|
|
150
|
+
const dir = "C:\\Temp\\myapp";
|
|
151
|
+
const dir = path.join("/tmp", "myapp");
|
|
152
|
+
|
|
153
|
+
// ❌ mkdir without cleanup — leaks across runs
|
|
154
|
+
fs.mkdirSync("/tmp/myapp", { recursive: true });
|
|
155
|
+
// ... no rmSync ...
|
|
156
|
+
|
|
157
|
+
// ❌ Atomic write to /tmp when target is elsewhere — cross-device rename fails
|
|
158
|
+
const tmp = `/tmp/${path.basename(target)}`;
|
|
159
|
+
fs.writeFileSync(tmp, data);
|
|
160
|
+
fs.renameSync(tmp, target); // EXDEV on different filesystems
|
|
161
|
+
|
|
162
|
+
// ✓ Correct: resolve temp dir at runtime, project-scoped, cleaned up
|
|
163
|
+
const dir = path.join(os.tmpdir(), `${projectId}-${process.pid}`);
|
|
164
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
165
|
+
try { /* work */ } finally { fs.rmSync(dir, { recursive: true, force: true }); }
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Override
|
|
169
|
+
|
|
170
|
+
This rule is **built-in to the soly extension** and ships with every
|
|
171
|
+
install. It has highest priority and cannot be overridden by user rules.
|
|
172
|
+
If you need a different convention, name your rule differently
|
|
173
|
+
(e.g., `temp-files-windows-only.md` so the paths don't collide).
|
package/core.ts
CHANGED
|
@@ -573,17 +573,26 @@ export function buildRulesSection(
|
|
|
573
573
|
if (r.interactiveOnly) interactive.push(r.relPath);
|
|
574
574
|
}
|
|
575
575
|
|
|
576
|
+
// Split into built-in (shipped with soly) vs project/user rules. The two
|
|
577
|
+
// groups render in two distinct sections so the LLM can see the priority
|
|
578
|
+
// structure at a glance: vendor rules at the top (cannot be overridden),
|
|
579
|
+
// user rules below (can be edited locally).
|
|
580
|
+
const builtInRules = applicable.filter((r) => r.source === "built-in");
|
|
581
|
+
const userRules = applicable.filter((r) => r.source !== "built-in");
|
|
582
|
+
|
|
576
583
|
// Optional grouping: phase rules in their own group, then everything else.
|
|
577
|
-
|
|
584
|
+
// Phase grouping only applies to user rules — built-in rules are always
|
|
585
|
+
// always-on and don't move with phases.
|
|
586
|
+
let userBlocks: string[];
|
|
578
587
|
let headerHint: string;
|
|
579
588
|
if (options?.groupByPhase) {
|
|
580
589
|
const phase = options.phaseNumber;
|
|
581
|
-
const phaseRules =
|
|
582
|
-
const otherRules =
|
|
583
|
-
|
|
590
|
+
const phaseRules = userRules.filter((r) => r.phaseNumber === phase);
|
|
591
|
+
const otherRules = userRules.filter((r) => r.phaseNumber !== phase);
|
|
592
|
+
userBlocks = [...phaseRules.map(render), ...otherRules.map(render)];
|
|
584
593
|
headerHint = `Phase ${phase} rules are loaded for the currently active phase; all other rules are always-on. Inline @see references are resolved recursively.`;
|
|
585
594
|
} else {
|
|
586
|
-
|
|
595
|
+
userBlocks = userRules.map(render);
|
|
587
596
|
headerHint = `The following rules are loaded from \`.agents/rules/\` and \`~/.agents/rules/\` and are mandatory. Follow them strictly. Inline @see references are resolved recursively.`;
|
|
588
597
|
}
|
|
589
598
|
|
|
@@ -593,7 +602,26 @@ export function buildRulesSection(
|
|
|
593
602
|
.join(", ")}_`
|
|
594
603
|
: "";
|
|
595
604
|
|
|
596
|
-
|
|
605
|
+
// Built-in section (shipped with soly, cannot be overridden).
|
|
606
|
+
// Renders ABOVE the MANDATORY project-rules section so the LLM sees the
|
|
607
|
+
// vendor content first, with a stronger "this is system-managed" framing.
|
|
608
|
+
const builtInSection = builtInRules.length
|
|
609
|
+
? `
|
|
610
|
+
|
|
611
|
+
## 🔒 Built-in rules (shipped with soly)
|
|
612
|
+
|
|
613
|
+
**The following rules ship with the soly extension and apply to every
|
|
614
|
+
session.** They are the highest-priority rules and cannot be overridden
|
|
615
|
+
by user rules in \`.agents/rules/\` or \`~/.agents/rules/\` (a user rule
|
|
616
|
+
at the same relPath is dropped silently). Follow them strictly.
|
|
617
|
+
|
|
618
|
+
${builtInRules.map(render).join("\n\n---\n\n")}
|
|
619
|
+
`
|
|
620
|
+
: "";
|
|
621
|
+
|
|
622
|
+
// Project/user rules section (the existing MANDATORY contract).
|
|
623
|
+
const projectSection = userBlocks.length
|
|
624
|
+
? `
|
|
597
625
|
|
|
598
626
|
## ⚠️ MANDATORY: soly project rules
|
|
599
627
|
|
|
@@ -601,8 +629,11 @@ export function buildRulesSection(
|
|
|
601
629
|
|
|
602
630
|
${headerHint}
|
|
603
631
|
|
|
604
|
-
${
|
|
605
|
-
|
|
632
|
+
${userBlocks.join("\n\n---\n\n")}${skippedNote}
|
|
633
|
+
`
|
|
634
|
+
: "";
|
|
635
|
+
|
|
636
|
+
const section = builtInSection + projectSection;
|
|
606
637
|
|
|
607
638
|
return { section, loaded: applicable.map(ruleKey), interactive };
|
|
608
639
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-soly",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.5",
|
|
4
4
|
"description": "Project management + workflow framework for pi-coding-agent. Plans, state, MANDATORY rules, self-review, multi-question picker, native footer + welcome chrome — one npm install, zero config. The LLM drives the workflow inline via the soly_workflow tool — no external subagent plugin.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -60,7 +60,8 @@
|
|
|
60
60
|
"init.ts",
|
|
61
61
|
"codemap.ts",
|
|
62
62
|
"state.ts",
|
|
63
|
-
"hotreload.ts"
|
|
63
|
+
"hotreload.ts",
|
|
64
|
+
"built-in-rules"
|
|
64
65
|
],
|
|
65
66
|
"keywords": [
|
|
66
67
|
"pi",
|