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.
@@ -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
- let blocks: string[];
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 = applicable.filter((r) => r.phaseNumber === phase);
582
- const otherRules = applicable.filter((r) => r.phaseNumber !== phase);
583
- blocks = [...phaseRules.map(render), ...otherRules.map(render)];
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
- blocks = applicable.map(render);
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
- const section = `
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
- ${blocks.join("\n\n---\n\n")}${skippedNote}
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.4",
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",