pi-task-tracker 0.2.3 → 0.2.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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/index.ts +23 -8
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -76,7 +76,7 @@ Optional `taskTracker` key in `~/.pi/agent/settings.json` (all default to `true`
76
76
  - `readme` — maintain the `README.md` project-structure section
77
77
  - `autoCommit` — per-task commits; these are the `/rollback` checkpoints, so disabling this also disables rollback targets
78
78
  - `guard` — master switch for the built-in permission layer (default `true`). Set `false` to defer permission decisions entirely to another extension (e.g. `pi-permission-system`): no external-write checks, no git-ask prompts, no cd sandbox.
79
- - `externalWrite` — policy for tool calls that would create/modify files OUTSIDE the session directory: `"block"` (default, hard deny — external changes cannot be tracked or rolled back), `"ask"` (per-attempt confirmation), `"off"` (no enforcement; `pi-permission-system` rules still apply). Reads stay free. Copying files FROM outside INTO the session is allowed (`cp` checks only its destination); moving/deleting external paths is blocked. Everything INSIDE the session directory passes by default — the only sandbox-internal intervention is the `cd` disable. Git history protection is **inherited**: history-rewriting git (rebase/reset/amend/force-push/clean/...) prompts for confirmation right here, so `pi-permission-system` can be uninstalled without losing that layer; routine git passes untouched, an external `git init`/`clone` is allowed with a heads-up that the new repo is not tracked. Shell commands are analyzed statically (redirects, `rm/mv/cp/touch/...`, `cd` tracking, PowerShell equivalents); git protection is inherited from `pi-permission-system` — routine git passes untouched and history-rewriting git still prompts there; only redirect targets and `-C` destinations on git segments are checked. **`cd` is free inside the session directory; a `cd` that would move the working directory outside it is blocked** (that is the one operation that changes pi's effective scope). No cd-tracking is performed — resolution baseline stays the session root, so a deep in-session `cd` followed by enough `../` levels to escape may over-block, which is the fail-safe direction.
79
+ - `externalWrite` — policy for tool calls that would create/modify files OUTSIDE the session directory: `"block"` (default, hard deny — external changes cannot be tracked or rolled back), `"ask"` (per-attempt confirmation), `"off"` (no enforcement; `pi-permission-system` rules still apply). Reads stay free. Copying files FROM outside INTO the session is allowed (`cp` checks only its destination); moving/deleting external paths is blocked. Everything INSIDE the session directory passes by default — the only sandbox-internal intervention is the `cd` disable. Git history protection is **inherited**: history-rewriting git (rebase/reset/amend/force-push/clean/...) prompts for confirmation right here, so `pi-permission-system` can be uninstalled without losing that layer; routine git passes untouched, an external `git init`/`clone` is allowed with a heads-up that the new repo is not tracked. Git-Bash/MSYS paths (`/c/Users/...`, `/cygdrive/c/...`) are normalized before evaluation, so drive-absolute forms do not false-positive as external; `/tmp` and other non-drive names stay external (they resolve outside the session on Windows). Shell commands are analyzed statically (redirects, `rm/mv/cp/touch/...`, `cd` tracking, PowerShell equivalents); git protection is inherited from `pi-permission-system` — routine git passes untouched and history-rewriting git still prompts there; only redirect targets and `-C` destinations on git segments are checked. **`cd` is free inside the session directory; a `cd` that would move the working directory outside it is blocked** (that is the one operation that changes pi's effective scope). No cd-tracking is performed — resolution baseline stays the session root, so a deep in-session `cd` followed by enough `../` levels to escape may over-block, which is the fail-safe direction.
80
80
 
81
81
  ## Requirements
82
82
 
package/index.ts CHANGED
@@ -110,7 +110,7 @@ function findEnclosingRepo(dir: string): string | null {
110
110
  // ---- External-write guard ----
111
111
  // Enforces the tracking boundary as a write boundary: pi-task-tracker can
112
112
  // only track (and therefore only roll back) the session directory, so by
113
- // default (config taskTracker.externalWrite="block") tool calls that would
113
+ // default (config taskTracker.externalWrite="ask") tool calls that would
114
114
  // create/modify files OUTSIDE it are denied. Reads stay free. Git command
115
115
  // protection is inherited from pi-permission-system's bash rules (rebase/
116
116
  // reset/amend/force-push/... ask there): this guard never inspects git
@@ -142,10 +142,22 @@ const GUARD_PS_WRITE_CMDS = new Set([
142
142
  "copy-item", "move-item", "clear-content", "ni", "ri", "mi", "ac", "sc", "sp",
143
143
  ]);
144
144
 
145
+ // Normalize a Git-Bash/MSYS style path before resolution. On Windows,
146
+ // path.resolve("/c/Users/x") yields the drive-relative "C:\c\Users\x", which
147
+ // which breaks the external check. "/c/Users/x" and "/cygdrive/c/Users/x"
148
+ // become "C:\Users\x"-style drive paths. Names longer than one letter (/tmp,
149
+ // /usr, ...) are NOT drive mappings and stay untouched (outside the session
150
+ // on Windows anyway).
151
+ function normalizeShellPath(t: string): string {
152
+ const m = /^\/(?:cygdrive\/)?([a-zA-Z])\/(.*)$/.exec(t);
153
+ if (!m) return t;
154
+ const BS = String.fromCharCode(92);
155
+ return m[1].toUpperCase() + ":" + BS + m[2].split("/").join(BS);
156
+ }
145
157
  // Resolve a shell token to an absolute path; unresolvable tokens (env vars,
146
158
  // command substitution) are treated as external - conservative by design.
147
159
  function guardResolve(sessionCwd: string, token: string): { abs: string; external: boolean } {
148
- let t = token.replace(/^["']|["']$/g, "").trim();
160
+ let t = normalizeShellPath(token.replace(/^["']|["']$/g, "").trim());
149
161
  if (!t) return { abs: sessionCwd, external: false };
150
162
  if (t.startsWith("~")) return { abs: t, external: true };
151
163
  if (/[\$`]/.test(t)) return { abs: t, external: true };
@@ -244,7 +256,8 @@ function guardShellExternalWrite(cmd: string, sessionCwd: string): { block?: str
244
256
  // interpreter: block only if the command TEXT names an external path
245
257
  for (const m of seg.matchAll(/[A-Za-z]:[\/]\S*|\/(?:[\w.-]+\/)+[\w.-]+/g)) {
246
258
  if (guardIsDevNull(m[0])) continue;
247
- if (isExternalAbs(m[0], sessionCwd)) { out.block = m[0]; break; }
259
+ const np = normalizeShellPath(m[0]);
260
+ if (isExternalAbs(np, sessionCwd)) { out.block = np; break; }
248
261
  }
249
262
  continue;
250
263
  }
@@ -254,7 +267,8 @@ function guardShellExternalWrite(cmd: string, sessionCwd: string): { block?: str
254
267
  pathArgs.push(tokens[k]);
255
268
  }
256
269
  const checkList = isCopy ? pathArgs.slice(-1) : pathArgs;
257
- for (const t of checkList) {
270
+ for (const raw of checkList) {
271
+ const t = normalizeShellPath(raw);
258
272
  if (/^[A-Za-z]:[\\{1,2}]/.test(t) || t.startsWith("/")) { // absolute
259
273
  if (guardIsDevNull(t)) continue;
260
274
  if (isExternalAbs(t, sessionCwd)) out.block = t;
@@ -308,7 +322,7 @@ async function guardExternalWriteHit(
308
322
  if (ok) return null;
309
323
  return `pi-task-tracker scope guard: user denied external write to '${hit.block}'. Do not retry or bypass.`;
310
324
  }
311
- return `pi-task-tracker scope guard (externalWrite=block): this command writes to '${hit.block}' outside the session directory. ${why} Stay inside the session directory, or have the user adjust taskTracker.externalWrite / run it manually.`;
325
+ return `pi-task-tracker scope guard (externalWrite=${mode}): this command writes to '${hit.block}' outside the session directory. ${why} Stay inside the session directory, or have the user adjust taskTracker.externalWrite / run it manually.`;
312
326
  }
313
327
  return null;
314
328
  }
@@ -330,10 +344,11 @@ async function guardExternalWriteHit(
330
344
  if (ok) return "__ask__";
331
345
  return `pi-task-tracker scope guard: user denied external write to '${p}'. Do not retry or bypass.`;
332
346
  }
333
- return `pi-task-tracker scope guard (externalWrite=block): tool '${toolName}' targets '${p}' outside the session directory. ${why} Ask the user to adjust taskTracker.externalWrite or perform the change manually.`;
347
+ return `pi-task-tracker scope guard (externalWrite=${mode}): tool '${toolName}' targets '${p}' outside the session directory. ${why} Ask the user to adjust taskTracker.externalWrite or perform the change manually.`;
334
348
  }
335
349
 
336
350
  function isExternalAbs2(base: string, p: string): boolean {
351
+ p = normalizeShellPath(p);
337
352
  const abs = path.isAbsolute(p) ? p : path.resolve(base, p);
338
353
  const rel = path.relative(base, abs);
339
354
  return rel.startsWith("..") || path.isAbsolute(rel);
@@ -589,7 +604,7 @@ interface TrackerConfig {
589
604
  }
590
605
 
591
606
  function readConfig(): TrackerConfig {
592
- const fallback: TrackerConfig = { todo: true, readme: true, autoCommit: true, externalWrite: "block", guard: true };
607
+ const fallback: TrackerConfig = { todo: true, readme: true, autoCommit: true, externalWrite: "ask", guard: true };
593
608
  try {
594
609
  const dir = process.env.PI_CODING_AGENT_DIR || path.join(os.homedir(), ".pi", "agent");
595
610
  const file = process.env.PI_TRACKER_SETTINGS || path.join(dir, "settings.json");
@@ -600,7 +615,7 @@ function readConfig(): TrackerConfig {
600
615
  readme: t.readme !== false,
601
616
  autoCommit: t.autoCommit !== false,
602
617
  guard: t.guard !== false,
603
- externalWrite: t.externalWrite === "ask" || t.externalWrite === "off" ? t.externalWrite : "block",
618
+ externalWrite: t.externalWrite === "block" || t.externalWrite === "off" ? t.externalWrite : "ask",
604
619
  };
605
620
  } catch {
606
621
  return fallback;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-task-tracker",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "description": "Task workflow tracking for the pi coding agent: TODO/README maintenance, per-task git auto-commits as checkpoints, and an interactive /rollback. Hashline-aware, nested-repo aware, subagent-artifact-safe.",
5
5
  "keywords": [
6
6
  "pi-package",