pi-task-tracker 0.1.2 → 0.1.4
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/README.md +2 -1
- package/index.ts +169 -61
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -13,7 +13,8 @@ Task workflow tracking for the [pi coding agent](https://www.npmjs.com/package/@
|
|
|
13
13
|
|
|
14
14
|
- **Hashline-aware** — records edits made through `pi-hashline-edit-pro`'s `replace`/`insert` tools, not just the native `edit` tool (most setups that replace native edit would otherwise go completely untracked).
|
|
15
15
|
- **Shell-aware** — captures files created/modified by `bash` **and** `powershell` tool calls via before/after `git status` diffing.
|
|
16
|
-
- **Nested-repo aware** — changes inside a sub-repository (a `.git` below your session root) are committed *there* — the innermost repo wins — and never pollute the outer repo.
|
|
16
|
+
- **Nested-repo aware** — changes inside a sub-repository (a `.git` below your session root) are committed *there* — the innermost repo wins — and never pollute the outer repo. The outer task commit records anchors (`Nested: <repo>@<sha>`), so `/rollback` restores nested repos to their exact matching commit.
|
|
17
|
+
- **Subdirectory start** — start pi in a subdirectory of an existing repo and no fresh `.git` is created there: all session changes (including TODO.md / README.md) are recorded in the enclosing repository, and `/rollback` restores the enclosing repo scoped to the session subtree.
|
|
17
18
|
- **Subagent-safe** — `.pi-subagents/` session artifacts are never recorded, listed, or committed.
|
|
18
19
|
- **~zero per-turn cost** — one incremental `git add` of touched files per task; no full-worktree scans, no background snapshot daemons.
|
|
19
20
|
|
package/index.ts
CHANGED
|
@@ -76,18 +76,36 @@ function isSubagentPath(rel: string): boolean {
|
|
|
76
76
|
// repo by initGit). A .git found below cwd (nested/embedded repository) wins,
|
|
77
77
|
// so changes made inside sub-repositories are attributed to the innermost
|
|
78
78
|
// repo's history instead of polluting the outer one.
|
|
79
|
-
|
|
79
|
+
// Nearest enclosing git worktree root for an absolute path, searching upward
|
|
80
|
+
// from the file's own directory and stopping at `stopAt` (the session's git
|
|
81
|
+
// root). A .git found below stopAt (nested/embedded repository) wins, so
|
|
82
|
+
// changes made inside sub-repositories are attributed to the innermost
|
|
83
|
+
// repo's history instead of polluting the outer one.
|
|
84
|
+
function ownerRepoRoot(absPath: string, stopAt: string): string {
|
|
80
85
|
let dir = path.dirname(absPath);
|
|
81
86
|
for (;;) {
|
|
82
87
|
if (fs.existsSync(path.join(dir, ".git"))) return dir;
|
|
83
|
-
const rel = path.relative(dir,
|
|
84
|
-
if (rel === "" || rel.startsWith("..") || path.isAbsolute(rel)) return
|
|
88
|
+
const rel = path.relative(dir, stopAt);
|
|
89
|
+
if (rel === "" || rel.startsWith("..") || path.isAbsolute(rel)) return stopAt;
|
|
85
90
|
const parent = path.dirname(dir);
|
|
86
|
-
if (parent === dir) return
|
|
91
|
+
if (parent === dir) return stopAt;
|
|
87
92
|
dir = parent;
|
|
88
93
|
}
|
|
89
94
|
}
|
|
90
95
|
|
|
96
|
+
// If `dir` itself has no .git, find the repository that contains it (walking
|
|
97
|
+
// upward). Used when pi is started in a subdirectory of an existing repo:
|
|
98
|
+
// that repo is reused instead of creating a fresh one in the subdirectory.
|
|
99
|
+
function findEnclosingRepo(dir: string): string | null {
|
|
100
|
+
let d = dir;
|
|
101
|
+
for (;;) {
|
|
102
|
+
const parent = path.dirname(d);
|
|
103
|
+
if (parent === d) return null;
|
|
104
|
+
d = parent;
|
|
105
|
+
if (fs.existsSync(path.join(d, ".git"))) return d;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
91
109
|
function ensureGitIdentity(cwd: string): void {
|
|
92
110
|
// ensure local identity so commits never fail on a fresh machine
|
|
93
111
|
if (!git(["config", "user.name"], cwd)) {
|
|
@@ -103,7 +121,7 @@ const GITIGNORE_SEED =
|
|
|
103
121
|
|
|
104
122
|
function initGit(cwd: string): void {
|
|
105
123
|
// 1. init repo if absent
|
|
106
|
-
|
|
124
|
+
if (!fs.existsSync(path.join(cwd, ".git"))) {
|
|
107
125
|
gitOk(["init"], cwd);
|
|
108
126
|
}
|
|
109
127
|
ensureGitIdentity(cwd);
|
|
@@ -125,7 +143,8 @@ function initGit(cwd: string): void {
|
|
|
125
143
|
// repo wins) and never staged in the outer repo; subagent working data
|
|
126
144
|
// (.pi-subagents/) is never committed at all.
|
|
127
145
|
function gitCommitTask(
|
|
128
|
-
|
|
146
|
+
sessionCwd: string,
|
|
147
|
+
gitRoot: string,
|
|
129
148
|
task: string | null,
|
|
130
149
|
created: string[],
|
|
131
150
|
edited: string[]
|
|
@@ -133,9 +152,11 @@ function gitCommitTask(
|
|
|
133
152
|
const groups = new Map<string, Set<string>>();
|
|
134
153
|
const consider = (relPath: string) => {
|
|
135
154
|
if (isSubagentPath(relPath)) return;
|
|
136
|
-
const abs = path.resolve(
|
|
137
|
-
|
|
138
|
-
|
|
155
|
+
const abs = path.resolve(sessionCwd, relPath);
|
|
156
|
+
const fromSession = path.relative(sessionCwd, abs);
|
|
157
|
+
if (fromSession.startsWith("..") || path.isAbsolute(fromSession)) return;
|
|
158
|
+
// innermost repo wins; the session repo is the enclosing one (gitRoot)
|
|
159
|
+
const root = ownerRepoRoot(abs, gitRoot);
|
|
139
160
|
const rel = path.relative(root, abs).replace(/\\/g, "/");
|
|
140
161
|
if (!rel || rel.startsWith("..") || path.isAbsolute(rel)) return;
|
|
141
162
|
if (!groups.has(root)) groups.set(root, new Set());
|
|
@@ -143,29 +164,44 @@ function gitCommitTask(
|
|
|
143
164
|
};
|
|
144
165
|
for (const f of created) consider(f);
|
|
145
166
|
for (const f of edited) consider(f);
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
rootFiles.
|
|
150
|
-
|
|
167
|
+
// workflow trackers ride along on the session repo's commit; they live in
|
|
168
|
+
// sessionCwd, which may be a subdirectory of gitRoot
|
|
169
|
+
if (!groups.has(gitRoot)) groups.set(gitRoot, new Set());
|
|
170
|
+
const rootFiles = groups.get(gitRoot)!;
|
|
171
|
+
const relOf = (f: string) => path.relative(gitRoot, path.join(sessionCwd, f)).replace(/\\/g, "/");
|
|
172
|
+
rootFiles.add(relOf("TODO.md"));
|
|
173
|
+
rootFiles.add(relOf("README.md"));
|
|
174
|
+
if (gitRoot === sessionCwd) rootFiles.add(".gitignore");
|
|
151
175
|
|
|
152
176
|
const subject = task ? task.slice(0, 72) : "chore: workflow auto-commit";
|
|
153
177
|
const visible = (f: string) => !isSubagentPath(f);
|
|
154
|
-
|
|
178
|
+
// Inner repos commit first, the session repo last, so the outer commit can
|
|
179
|
+
// carry anchors ("Nested: <rel>@<sha>") pointing at the exact nested-repo
|
|
180
|
+
// commits of this task - /rollback uses them for precise restoration.
|
|
181
|
+
const innerCommits: string[] = [];
|
|
182
|
+
const orderedRoots = [...groups.keys()].filter((r) => r !== gitRoot);
|
|
183
|
+
orderedRoots.push(gitRoot);
|
|
184
|
+
for (const root of orderedRoots) {
|
|
185
|
+
const files = groups.get(root)!;
|
|
155
186
|
ensureGitIdentity(root);
|
|
156
187
|
for (const f of files) {
|
|
157
188
|
gitOk(["add", "--", f], root);
|
|
158
189
|
}
|
|
159
|
-
|
|
160
|
-
if (!
|
|
190
|
+
const staged = git(["diff", "--cached", "--name-only"], root);
|
|
191
|
+
if (!staged && !(root === gitRoot && innerCommits.length > 0)) continue;
|
|
161
192
|
const parts: string[] = [];
|
|
162
|
-
if (root ===
|
|
193
|
+
if (root === gitRoot) {
|
|
163
194
|
if (created.some(visible)) parts.push(`Created: ${created.filter(visible).join(", ")}`);
|
|
164
195
|
if (edited.some(visible)) parts.push(`Edited: ${edited.filter(visible).join(", ")}`);
|
|
196
|
+
for (const ic of innerCommits) parts.push(`Nested: ${ic}`);
|
|
165
197
|
}
|
|
166
198
|
const args = ["commit", "-m", subject];
|
|
167
199
|
if (parts.length > 0) args.push("-m", parts.join("\n"));
|
|
168
|
-
|
|
200
|
+
if (!staged) args.push("--allow-empty");
|
|
201
|
+
if (gitOk(args, root) && root !== gitRoot) {
|
|
202
|
+
const rel = path.relative(gitRoot, root).replace(/\\/g, "/");
|
|
203
|
+
innerCommits.push(`${rel}@${git(["rev-parse", "--short", "HEAD"], root)}`);
|
|
204
|
+
}
|
|
169
205
|
}
|
|
170
206
|
}
|
|
171
207
|
|
|
@@ -212,6 +248,62 @@ function normalizeReadme(readmePath: string, name: string): void {
|
|
|
212
248
|
} catch {}
|
|
213
249
|
}
|
|
214
250
|
|
|
251
|
+
// ---- Rollback helpers ----
|
|
252
|
+
// Compute the worktree actions needed to bring `root`'s tracked files back to
|
|
253
|
+
// `sha`: restores (M/D paths) and removes (A paths). Subagent data excluded.
|
|
254
|
+
function repoDiffActions(root: string, sha: string, subtreeRel?: string): { restores: string[]; removes: string[] } {
|
|
255
|
+
const diff = git(["diff", "--name-status", "--no-renames", `${sha}..HEAD`], root);
|
|
256
|
+
const restores: string[] = [];
|
|
257
|
+
const removes: string[] = [];
|
|
258
|
+
for (const line of diff.split("\n")) {
|
|
259
|
+
const tab = line.indexOf("\t");
|
|
260
|
+
if (tab === -1) continue;
|
|
261
|
+
const status = line.slice(0, tab).trim()[0];
|
|
262
|
+
const file = line.slice(tab + 1).trim();
|
|
263
|
+
if (!file || file.includes("->")) continue;
|
|
264
|
+
// when operating on a repo subdirectory, only touch paths inside it
|
|
265
|
+
if (subtreeRel !== undefined && !(file === subtreeRel || file.startsWith(subtreeRel + "/"))) continue;
|
|
266
|
+
if (isSubagentPath(file)) continue;
|
|
267
|
+
if (status === "A") removes.push(file);
|
|
268
|
+
else if (status === "M" || status === "D") restores.push(file);
|
|
269
|
+
}
|
|
270
|
+
return { restores, removes };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Apply actions computed by repoDiffActions (worktree only; chunked for
|
|
274
|
+
// Windows command-length limits).
|
|
275
|
+
function applyRepoRollback(root: string, sha: string, actions: { restores: string[]; removes: string[] }): void {
|
|
276
|
+
for (let i = 0; i < actions.restores.length; i += 50) {
|
|
277
|
+
gitOk(["restore", "--source=" + sha, "--worktree", "--", ...actions.restores.slice(i, i + 50)], root);
|
|
278
|
+
}
|
|
279
|
+
for (const f of actions.removes) {
|
|
280
|
+
try {
|
|
281
|
+
fs.rmSync(path.join(root, f), { force: true });
|
|
282
|
+
} catch {}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Find the commit in a nested repo matching the rolled-back task. One task
|
|
287
|
+
// produces commits with the SAME subject in every repo it touched, so match
|
|
288
|
+
// by subject AND occurrence ordinal (task names may repeat over time):
|
|
289
|
+
// outerIndexNewer = how many commits with this subject are newer than the
|
|
290
|
+
// outer target; pick the same ordinal (newest-first) in the nested history.
|
|
291
|
+
function findInnerTarget(root: string, subject: string, outerDateISO: string): string | null {
|
|
292
|
+
const log = git(["log", "--max-count=200", "--format=%H|%cI|%s"], root);
|
|
293
|
+
if (!log) return null;
|
|
294
|
+
const outerDate = Date.parse(outerDateISO);
|
|
295
|
+
if (Number.isNaN(outerDate)) return null;
|
|
296
|
+
let best: { sha: string; t: number } | null = null;
|
|
297
|
+
for (const line of log.split("\n")) {
|
|
298
|
+
if (!line) continue;
|
|
299
|
+
const [sha, date, ...rest] = line.split("|");
|
|
300
|
+
if (rest.join("|") !== subject) continue;
|
|
301
|
+
const t = Date.parse(date);
|
|
302
|
+
if (Number.isNaN(t) || t > outerDate) continue;
|
|
303
|
+
if (!best || t > best.t) best = { sha, t };
|
|
304
|
+
}
|
|
305
|
+
return best ? best.sha : null;
|
|
306
|
+
}
|
|
215
307
|
// ---- Session helpers ----
|
|
216
308
|
// Flatten an AgentMessage's content to plain text (string or content blocks).
|
|
217
309
|
function messageText(m: { content?: unknown }): string {
|
|
@@ -259,6 +351,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
259
351
|
let todoPath = "";
|
|
260
352
|
let readmePath = "";
|
|
261
353
|
let cwd = "";
|
|
354
|
+
let gitRoot = '';
|
|
262
355
|
let config: TrackerConfig = readConfig();
|
|
263
356
|
let currentTask: string | null = null;
|
|
264
357
|
const createdFiles: string[] = [];
|
|
@@ -272,18 +365,22 @@ export default function (pi: ExtensionAPI) {
|
|
|
272
365
|
const nestedRoots = new Set<string>();
|
|
273
366
|
// learn the owning repo of a touched path and remember nested roots
|
|
274
367
|
const watchRootFor = (absPath: string) => {
|
|
275
|
-
const root = ownerRepoRoot(absPath,
|
|
276
|
-
if (root !== cwd) nestedRoots.add(root);
|
|
368
|
+
const root = ownerRepoRoot(absPath, gitRoot);
|
|
369
|
+
if (root !== cwd && root !== gitRoot) nestedRoots.add(root);
|
|
277
370
|
};
|
|
278
371
|
const snapshotAll = (): Map<string, Map<string, string>> => {
|
|
279
372
|
const m = new Map<string, Map<string, string>>();
|
|
280
|
-
|
|
373
|
+
// git 2.5x 'status --porcelain' emits repo-root-relative paths; run it at the
|
|
374
|
+
// repo root so the keys are unambiguous, then rebase below.
|
|
375
|
+
m.set(cwd, gitStatusSet(gitRoot));
|
|
281
376
|
for (const root of nestedRoots) m.set(root, gitStatusSet(root));
|
|
282
377
|
return m;
|
|
283
378
|
};
|
|
284
379
|
|
|
285
380
|
pi.on("session_start", async (_event, ctx) => {
|
|
286
381
|
cwd = ctx.cwd;
|
|
382
|
+
// reuse an enclosing repository instead of creating one in a subdirectory
|
|
383
|
+
gitRoot = fs.existsSync(path.join(cwd, ".git")) ? cwd : findEnclosingRepo(cwd) || cwd;
|
|
287
384
|
config = readConfig();
|
|
288
385
|
todoPath = config.todo ? path.join(cwd, "TODO.md") : "";
|
|
289
386
|
readmePath = config.readme ? path.join(cwd, "README.md") : "";
|
|
@@ -313,7 +410,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
313
410
|
}
|
|
314
411
|
|
|
315
412
|
// bootstrap git (safe on existing repos: only sets missing config/.gitignore)
|
|
316
|
-
if (config.autoCommit) initGit(cwd);
|
|
413
|
+
if (config.autoCommit && gitRoot === cwd) initGit(cwd);
|
|
317
414
|
});
|
|
318
415
|
|
|
319
416
|
pi.on("before_agent_start", async (event) => {
|
|
@@ -386,14 +483,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
386
483
|
if (p === "TODO.md" || p === "README.md") continue; // handled at commit time
|
|
387
484
|
// Normalize to a cwd-relative path. Inner-repo entries come relative
|
|
388
485
|
// to the inner root and need rebasing onto cwd.
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
// Guard: never track paths outside cwd (git status emits "../" paths
|
|
395
|
-
// when cwd is a subdirectory of a parent repo), nor subagent data.
|
|
396
|
-
if (p === ".." || p.startsWith("../") || path.isAbsolute(p)) continue;
|
|
486
|
+
// Rebase the repo-root-relative status key onto sessionCwd; skip
|
|
487
|
+
// anything outside the session subtree.
|
|
488
|
+
const base = root === cwd ? gitRoot : root;
|
|
489
|
+
let rel = path.relative(cwd, path.join(base, p)).replace(/\\/g, "/");
|
|
490
|
+
if (!rel || rel.startsWith("..") || path.isAbsolute(rel)) continue;
|
|
397
491
|
if (isSubagentPath(rel)) continue; // subagent session data is never tracked
|
|
398
492
|
if (prev === undefined) {
|
|
399
493
|
// newly appeared in the working tree
|
|
@@ -485,7 +579,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
485
579
|
|
|
486
580
|
// commit a snapshot of this task's changes (fresh and existing repos alike)
|
|
487
581
|
if (cwd && config.autoCommit) {
|
|
488
|
-
gitCommitTask(cwd, taskSnapshot, createdFiles, editedFiles);
|
|
582
|
+
gitCommitTask(cwd, gitRoot, taskSnapshot, createdFiles, editedFiles);
|
|
489
583
|
}
|
|
490
584
|
|
|
491
585
|
createdFiles.length = 0;
|
|
@@ -505,13 +599,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
505
599
|
description: "Roll the working tree back to a previous commit (worktree-only, nothing is committed; task commits are the checkpoints)",
|
|
506
600
|
handler: async (_args, ctx) => {
|
|
507
601
|
if (!cwd) return;
|
|
508
|
-
if (!fs.existsSync(path.join(
|
|
602
|
+
if (!gitRoot || !fs.existsSync(path.join(gitRoot, ".git"))) {
|
|
509
603
|
ctx?.ui.notify("Not a git repository - nothing to roll back", "warning");
|
|
510
604
|
return;
|
|
511
605
|
}
|
|
512
606
|
|
|
513
607
|
// 1. list recent commits as rollback targets
|
|
514
|
-
const log = git(["log", "--max-count=30", "--date=format:%m-%d %H:%M", "--format=%h|%ad|%s"],
|
|
608
|
+
const log = git(["log", "--max-count=30", "--date=format:%m-%d %H:%M", "--format=%h|%ad|%s"], gitRoot);
|
|
515
609
|
if (!log) {
|
|
516
610
|
ctx?.ui.notify("No commits yet - nothing to roll back", "warning");
|
|
517
611
|
return;
|
|
@@ -529,40 +623,54 @@ export default function (pi: ExtensionAPI) {
|
|
|
529
623
|
const target = entries.find((e) => e.label === chosen);
|
|
530
624
|
if (!target) return;
|
|
531
625
|
|
|
532
|
-
// 2. what changed between target and HEAD
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
626
|
+
// 2. what changed between target and HEAD in the session repo (rename
|
|
627
|
+
// pairs split into A+D), plus every nested repo touched this session:
|
|
628
|
+
// one task commits the same subject in each repo it touched, so nested
|
|
629
|
+
// repos roll back to their own matching commit (time-aligned).
|
|
630
|
+
const relCwd = path.relative(gitRoot, cwd).replace(/\\/g, "/");
|
|
631
|
+
const actions = repoDiffActions(gitRoot, target.sha, relCwd === "" ? undefined : relCwd);
|
|
632
|
+
const innerRollbacks: { root: string; sha: string; name: string; restores: number; removes: number }[] = [];
|
|
633
|
+
const innerApply: { root: string; sha: string; actions: { restores: string[]; removes: string[] } }[] = [];
|
|
634
|
+
if (nestedRoots.size > 0) {
|
|
635
|
+
// exact anchors recorded in the outer commit body: Nested: <rel>@<sha>
|
|
636
|
+
const body = git(["show", "-s", "--format=%B", target.sha], gitRoot);
|
|
637
|
+
const anchored = new Map<string, string>();
|
|
638
|
+
for (const line of body.split("\n")) {
|
|
639
|
+
const m = /^Nested: (.+)@([0-9a-f]{7,40})$/.exec(line.trim());
|
|
640
|
+
if (m) anchored.set(m[1], m[2]);
|
|
641
|
+
}
|
|
642
|
+
const subject = target.subject || git(["show", "-s", "--format=%s", target.sha], gitRoot);
|
|
643
|
+
const outerDate = git(["show", "-s", "--format=%cI", target.sha], gitRoot);
|
|
644
|
+
for (const root of nestedRoots) {
|
|
645
|
+
const rel = path.relative(gitRoot, root).replace(/\\/g, "/");
|
|
646
|
+
const innerSha = anchored.get(rel) || findInnerTarget(root, subject, outerDate);
|
|
647
|
+
if (!innerSha) continue;
|
|
648
|
+
const a = repoDiffActions(root, innerSha);
|
|
649
|
+
if (a.restores.length > 0 || a.removes.length > 0) {
|
|
650
|
+
innerRollbacks.push({ root, sha: innerSha, name: path.basename(root), restores: a.restores.length, removes: a.removes.length });
|
|
651
|
+
innerApply.push({ root, sha: innerSha, actions: a });
|
|
652
|
+
}
|
|
653
|
+
}
|
|
546
654
|
}
|
|
547
|
-
|
|
655
|
+
const totalRestores = actions.restores.length + innerRollbacks.reduce((n, r) => n + r.restores, 0);
|
|
656
|
+
const totalRemoves = actions.removes.length + innerRollbacks.reduce((n, r) => n + r.removes, 0);
|
|
657
|
+
if (totalRestores === 0 && totalRemoves === 0) {
|
|
548
658
|
ctx.ui.notify("No tracked file changes since the target commit - nothing to roll back", "info");
|
|
549
659
|
return;
|
|
550
660
|
}
|
|
551
661
|
|
|
552
|
-
// 3. confirm
|
|
553
|
-
const dirty = git(["status", "--porcelain"],
|
|
554
|
-
const
|
|
662
|
+
// 3. confirm - warn about uncommitted changes that would be overwritten
|
|
663
|
+
const dirty = git(["status", "--porcelain"], gitRoot).split("\n").filter(Boolean).length;
|
|
664
|
+
const scope = [`main repo${relCwd ? ` [./${relCwd}]` : ""}: restore ${actions.restores.length}, remove ${actions.removes.length}`];
|
|
665
|
+
for (const r of innerRollbacks) scope.push(`${r.name}: restore ${r.restores}, remove ${r.removes}`);
|
|
666
|
+
const summary = `Back to ${target.sha} - ${scope.join('; ')}${dirty > 0 ? ` (WARNING: ${dirty} uncommitted change(s) - affected paths will be overwritten)` : ' (clean worktree)'}`;
|
|
555
667
|
const ok = await ctx.ui.select(`${summary}. Proceed? (worktree only, nothing is committed)`, ["Cancel", "Roll back"]);
|
|
556
668
|
if (ok !== "Roll back") return;
|
|
557
669
|
|
|
558
|
-
// 4. apply (worktree only
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
for (const f of removes) {
|
|
563
|
-
try {
|
|
564
|
-
fs.rmSync(path.join(cwd, f), { force: true });
|
|
565
|
-
} catch {}
|
|
670
|
+
// 4. apply everywhere (worktree only)
|
|
671
|
+
applyRepoRollback(gitRoot, target.sha, actions);
|
|
672
|
+
for (const r of innerApply) {
|
|
673
|
+
applyRepoRollback(r.root, r.sha, r.actions);
|
|
566
674
|
}
|
|
567
675
|
|
|
568
676
|
// 5. drop accumulated file records — they no longer reflect the worktree
|
|
@@ -577,7 +685,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
577
685
|
// in the session tree — /tree can navigate back at any time.
|
|
578
686
|
let conversationNote = "";
|
|
579
687
|
try {
|
|
580
|
-
const subject = git(["show", "-s", "--format=%s", target.sha],
|
|
688
|
+
const subject = git(["show", "-s", "--format=%s", target.sha], gitRoot);
|
|
581
689
|
if (subject && ctx.sessionManager) {
|
|
582
690
|
const norm = (t: string) => t.replace(/\s+/g, " ").trim();
|
|
583
691
|
const branch = ctx.sessionManager.getBranch();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-task-tracker",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
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",
|