pi-soly 2.1.3 → 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 +76 -9
- package/index.ts +8 -2
- 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
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
import * as fs from "node:fs";
|
|
17
17
|
import * as os from "node:os";
|
|
18
18
|
import * as path from "node:path";
|
|
19
|
+
import { fileURLToPath } from "node:url";
|
|
19
20
|
|
|
20
21
|
// Shared leaf utilities now live in util.ts; re-exported here so existing
|
|
21
22
|
// `import { ... } from "./core.ts"` call sites keep working unchanged.
|
|
@@ -58,7 +59,8 @@ export type RuleSource =
|
|
|
58
59
|
| "phase-soly"
|
|
59
60
|
| "project-agents"
|
|
60
61
|
| "global-agents"
|
|
61
|
-
| "phase-agents"
|
|
62
|
+
| "phase-agents"
|
|
63
|
+
| "built-in";
|
|
62
64
|
|
|
63
65
|
export interface RuleFrontmatter {
|
|
64
66
|
description?: string;
|
|
@@ -259,6 +261,40 @@ function loadRulesFromSource(spec: SourceSpec): RuleFile[] {
|
|
|
259
261
|
return rules;
|
|
260
262
|
}
|
|
261
263
|
|
|
264
|
+
/**
|
|
265
|
+
* Resolve the path to the extension's `built-in-rules/` directory. Lives
|
|
266
|
+
* at `<package>/built-in-rules/` relative to the compiled module. We use
|
|
267
|
+
* `fileURLToPath(import.meta.url)` so this works whether the package is
|
|
268
|
+
* loaded directly from source (Bun, tsx) or from compiled output.
|
|
269
|
+
*
|
|
270
|
+
* `here` is the directory containing this core.ts file (i.e. the package
|
|
271
|
+
* root for source, the package root for compiled output too — both files
|
|
272
|
+
* live alongside package.json), so we don't need to traverse up.
|
|
273
|
+
*/
|
|
274
|
+
export function builtInRulesDir(): string {
|
|
275
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
276
|
+
return path.resolve(here, "built-in-rules");
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Load every markdown rule from the extension's `built-in-rules/` directory.
|
|
281
|
+
* These rules ship with the soly extension and are always present in the
|
|
282
|
+
* system prompt. Source spec uses priority=10 (highest), so a user rule at
|
|
283
|
+
* the same relPath is silently dropped into `overridden[]` — built-in
|
|
284
|
+
* rules win by design. `sourceLabel: "soly"` makes them visible in
|
|
285
|
+
* `/rules list` so users know which rules come from the package itself.
|
|
286
|
+
*/
|
|
287
|
+
export function loadBuiltInRules(): RuleFile[] {
|
|
288
|
+
const dir = builtInRulesDir();
|
|
289
|
+
if (!fs.existsSync(dir)) return [];
|
|
290
|
+
return loadRulesFromSource({
|
|
291
|
+
dir,
|
|
292
|
+
source: "built-in",
|
|
293
|
+
sourceLabel: "soly",
|
|
294
|
+
priority: 10,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
262
298
|
export function loadAllRules(sources: SourceSpec[]): {
|
|
263
299
|
rules: RuleFile[];
|
|
264
300
|
overridden: string[];
|
|
@@ -537,17 +573,26 @@ export function buildRulesSection(
|
|
|
537
573
|
if (r.interactiveOnly) interactive.push(r.relPath);
|
|
538
574
|
}
|
|
539
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
|
+
|
|
540
583
|
// Optional grouping: phase rules in their own group, then everything else.
|
|
541
|
-
|
|
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[];
|
|
542
587
|
let headerHint: string;
|
|
543
588
|
if (options?.groupByPhase) {
|
|
544
589
|
const phase = options.phaseNumber;
|
|
545
|
-
const phaseRules =
|
|
546
|
-
const otherRules =
|
|
547
|
-
|
|
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)];
|
|
548
593
|
headerHint = `Phase ${phase} rules are loaded for the currently active phase; all other rules are always-on. Inline @see references are resolved recursively.`;
|
|
549
594
|
} else {
|
|
550
|
-
|
|
595
|
+
userBlocks = userRules.map(render);
|
|
551
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.`;
|
|
552
597
|
}
|
|
553
598
|
|
|
@@ -557,7 +602,26 @@ export function buildRulesSection(
|
|
|
557
602
|
.join(", ")}_`
|
|
558
603
|
: "";
|
|
559
604
|
|
|
560
|
-
|
|
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
|
+
? `
|
|
561
625
|
|
|
562
626
|
## ⚠️ MANDATORY: soly project rules
|
|
563
627
|
|
|
@@ -565,8 +629,11 @@ export function buildRulesSection(
|
|
|
565
629
|
|
|
566
630
|
${headerHint}
|
|
567
631
|
|
|
568
|
-
${
|
|
569
|
-
|
|
632
|
+
${userBlocks.join("\n\n---\n\n")}${skippedNote}
|
|
633
|
+
`
|
|
634
|
+
: "";
|
|
635
|
+
|
|
636
|
+
const section = builtInSection + projectSection;
|
|
570
637
|
|
|
571
638
|
return { section, loaded: applicable.map(ruleKey), interactive };
|
|
572
639
|
}
|
package/index.ts
CHANGED
|
@@ -34,6 +34,8 @@ import {
|
|
|
34
34
|
extractFilePathsFromPrompt,
|
|
35
35
|
formatTok,
|
|
36
36
|
loadAllRules,
|
|
37
|
+
builtInRulesDir,
|
|
38
|
+
loadBuiltInRules,
|
|
37
39
|
loadPhaseRules,
|
|
38
40
|
loadProjectState,
|
|
39
41
|
matchesGlob,
|
|
@@ -441,10 +443,14 @@ export default function solyExtension(pi: ExtensionAPI) {
|
|
|
441
443
|
// Project rules always beat global rules. `.agents/rules.local/` is
|
|
442
444
|
// gitignored — for personal overrides on top of the project's rules.
|
|
443
445
|
// `.agents/rules/` is the vendor-neutral project-level convention.
|
|
446
|
+
// Built-in rules ship with the extension (priority=10 — highest) and
|
|
447
|
+
// can't be overridden: a user rule at the same relPath is dropped
|
|
448
|
+
// silently into the `overridden[]` list (see loadAllRules).
|
|
444
449
|
ruleSources = [
|
|
445
|
-
{ dir:
|
|
446
|
-
{ dir: path.join(ctx.cwd, ".agents", "rules"), source: "project-agents", sourceLabel: "agents", priority: 3 },
|
|
450
|
+
{ dir: builtInRulesDir(), source: "built-in", sourceLabel: "soly", priority: 10 },
|
|
447
451
|
{ dir: path.join(os.homedir(), ".agents", "rules"), source: "global-agents", sourceLabel: "agents", priority: 1 },
|
|
452
|
+
{ dir: path.join(ctx.cwd, ".agents", "rules"), source: "project-agents", sourceLabel: "agents", priority: 3 },
|
|
453
|
+
{ dir: path.join(ctx.cwd, ".agents", "rules.local"), source: "project-agents", sourceLabel: "local", priority: 5 },
|
|
448
454
|
];
|
|
449
455
|
refreshRules();
|
|
450
456
|
|
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",
|