pi-task-tracker 0.2.0 → 0.2.1

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 +20 -18
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -75,7 +75,7 @@ Optional `taskTracker` key in `~/.pi/agent/settings.json` (all default to `true`
75
75
  - `todo` — maintain `TODO.md`
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
- - `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. 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.
78
+ - `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. 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`/`Set-Location` are disabled outright** (sandbox design: pi's bash tool runs each command in a fresh child process pinned to the session directory anyway, so a `cd` can never move the session — denying it keeps the analysis simple and the writable scope exactly the trackable scope).
79
79
 
80
80
  ## Requirements
81
81
 
package/index.ts CHANGED
@@ -144,15 +144,15 @@ const GUARD_PS_WRITE_CMDS = new Set([
144
144
 
145
145
  // Resolve a shell token to an absolute path; unresolvable tokens (env vars,
146
146
  // command substitution) are treated as external - conservative by design.
147
- function guardResolve(effCwd: string, sessionCwd: string, token: string): { abs: string; external: boolean } {
147
+ function guardResolve(sessionCwd: string, token: string): { abs: string; external: boolean } {
148
148
  let t = token.replace(/^["']|["']$/g, "").trim();
149
- if (!t) return { abs: effCwd, external: false };
149
+ if (!t) return { abs: sessionCwd, external: false };
150
150
  if (t.startsWith("~")) return { abs: t, external: true };
151
151
  if (/[\$`]/.test(t)) return { abs: t, external: true };
152
152
  if (/^[A-Za-z]:[\\/]/.test(t) || t.startsWith("/")) {
153
153
  return { abs: t, external: isExternalAbs(t, sessionCwd) };
154
154
  }
155
- const abs = path.resolve(effCwd, t);
155
+ const abs = path.resolve(sessionCwd, t);
156
156
  return { abs, external: isExternalAbs(abs, sessionCwd) };
157
157
  }
158
158
 
@@ -172,30 +172,31 @@ function guardIsDevNull(t: string): boolean {
172
172
  // write-command arguments (copy-like: destination only), interpreter payloads
173
173
  // mentioning external absolute paths, cd tracking. Git arguments are NOT
174
174
  // analyzed (inherited protection: only history-rewriting git asks).
175
+ // Analyze one shell command. cd/Set-Location are DISABLED outright (sandbox
176
+ // design: the process cwd is pinned to the session directory, which also
177
+ // removes any need for cd-tracking in this analysis). Returns
178
+ // { block, ask, notify } hits. External-write analysis is best effort:
179
+ // redirect targets, write-command arguments (copy-like: destination only),
180
+ // interpreter payloads mentioning external absolute paths. Git arguments are
181
+ // NOT analyzed (inherited protection: only history-rewriting git asks).
175
182
  function guardShellExternalWrite(cmd: string, sessionCwd: string): { block?: string; ask?: string; notify?: string } | null {
176
183
  const segments = cmd.split(/\n|&&|\|\||;|\|/).map((s) => s.trim()).filter(Boolean);
177
- let effCwd = sessionCwd;
178
184
  const out: { block?: string; ask?: string; notify?: string } = {};
179
185
  for (const seg of segments) {
180
- const cd = seg.match(/^cd\s+(.+)$/i) || seg.match(/^sl\s+(.+)$/i) || seg.match(/^set-location\s+(.+)$/i);
181
- if (cd) {
182
- const r = guardResolve(effCwd, sessionCwd, cd[1]);
183
- // switch to the real effective cwd even when external: only WRITE ops
184
- // are analyzed below, so `cd elsewhere && cat f` still passes while
185
- // `cd elsewhere && touch a.txt` is correctly judged external.
186
- effCwd = r.abs;
187
- continue;
186
+ if (/^(cd|sl|set-location)\s/i.test(seg)) {
187
+ out.block = "cd is disabled in this sandbox; run commands from the session directory";
188
+ return out;
188
189
  }
189
190
  // 2. redirects: > >> 2> 2>> &> &>> >| (not heredocs, not fd dups like 2>&1)
190
191
  for (const m of seg.matchAll(/(?:^|[^<>&=(])(?:\d?>{1,2}|&>>?|>\|)\s*([^\s;&|]+)/g)) {
191
192
  const tgt = m[1];
192
193
  if (/^&?\d+$/.test(tgt) || guardIsDevNull(tgt)) continue;
193
- const r = guardResolve(effCwd, sessionCwd, tgt);
194
+ const r = guardResolve(sessionCwd, tgt);
194
195
  if (r.external) out.block = r.abs;
195
196
  }
196
197
  const isGit = /^git(\s|$)/.test(seg);
197
198
  // 1. inherited git protection: history-rewriting git asks (matches
198
- // pi-permissions.jsonc bash patterns; prefix match like "git rebase*")
199
+ // the old pi-permissions.jsonc bash patterns; prefix match)
199
200
  if (isGit) {
200
201
  const low = seg.toLowerCase();
201
202
  if (GUARD_GIT_ASK.some((p) => low.startsWith(p))) {
@@ -203,7 +204,8 @@ function guardShellExternalWrite(cmd: string, sessionCwd: string): { block?: str
203
204
  continue;
204
205
  }
205
206
  if (/^git\s+(init|clone)/.test(low)) {
206
- const r = guardResolve(effCwd, sessionCwd, seg.split(/\s+/)[1] === "clone" ? (seg.match(/\s+(\S+)\s+\S+$/) || [])[1] || "." : ".");
207
+ const target = (seg.split(/\s+/).filter((x) => x && !x.startsWith("-")).pop()) || ".";
208
+ const r = guardResolve(sessionCwd, target);
207
209
  if (r.external) {
208
210
  out.notify = `git ${seg.startsWith("git clone") ? "clone" : "init"} outside the session directory (${r.abs}). pi-task-tracker does NOT track that directory; start a separate pi session there if you want it tracked.`;
209
211
  }
@@ -211,7 +213,7 @@ function guardShellExternalWrite(cmd: string, sessionCwd: string): { block?: str
211
213
  // external -C destination on any git command
212
214
  const c = seg.match(/\s-C\s+(\S+)/);
213
215
  if (c) {
214
- const r = guardResolve(effCwd, sessionCwd, c[1]);
216
+ const r = guardResolve(sessionCwd, c[1]);
215
217
  if (r.external) out.block = r.abs;
216
218
  }
217
219
  continue;
@@ -241,13 +243,13 @@ function guardShellExternalWrite(cmd: string, sessionCwd: string): { block?: str
241
243
  }
242
244
  const checkList = isCopy ? pathArgs.slice(-1) : pathArgs;
243
245
  for (const t of checkList) {
244
- if (/^[A-Za-z]:[\{1,2}]/.test(t) || t.startsWith("/")) { // absolute
246
+ if (/^[A-Za-z]:[\\{1,2}]/.test(t) || t.startsWith("/")) { // absolute
245
247
  if (guardIsDevNull(t)) continue;
246
248
  if (isExternalAbs(t, sessionCwd)) out.block = t;
247
249
  } else if (/[\$`~]/.test(t)) {
248
250
  out.block = t; // unresolvable -> conservative
249
251
  } else {
250
- const abs = path.resolve(effCwd, t);
252
+ const abs = path.resolve(sessionCwd, t);
251
253
  if (isExternalAbs(abs, sessionCwd)) out.block = t;
252
254
  }
253
255
  if (out.block) break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-task-tracker",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
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",