pi-crew 0.8.11 → 0.8.12
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/CHANGELOG.md +44 -0
- package/README.md +29 -0
- package/package.json +1 -1
- package/src/extension/team-tool/lifecycle-actions.ts +120 -3
- package/src/extension/team-tool-types.ts +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,49 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.8.12] — `team action=cleanup` now reverses `init` (Issue #35) (2026-06-17)
|
|
4
|
+
|
|
5
|
+
`team action=cleanup` gained a **project-level mode** that reverses what
|
|
6
|
+
`team action=init` writes. This closes the legitimate complaint in
|
|
7
|
+
[Issue #35](https://github.com/baphuongna/pi-crew/issues/35): pi-crew injects
|
|
8
|
+
a guidance block into `AGENTS.md` on `init`, but `pi uninstall` has no
|
|
9
|
+
extension hook to remove it — so the block (and `.crew/`) were left behind.
|
|
10
|
+
|
|
11
|
+
### New `cleanup` modes
|
|
12
|
+
|
|
13
|
+
| Call | What it does |
|
|
14
|
+
|---|---|
|
|
15
|
+
| `team action=cleanup runId=<id>` | Per-run worktree cleanup (existing behavior, unchanged) |
|
|
16
|
+
| `team action=cleanup` (no runId) | **NEW**: removes the AGENTS.md guidance block |
|
|
17
|
+
| `team action=cleanup force=true` | NEW: also removes the `.crew/` state directory |
|
|
18
|
+
| `team action=cleanup dryRun=true` | NEW: preview without writing |
|
|
19
|
+
|
|
20
|
+
### Safety guarantees
|
|
21
|
+
|
|
22
|
+
- The AGENTS.md guidance block is **marker-delimited**
|
|
23
|
+
(`<!-- PI-CREW:GUIDANCE:START/END -->`), so `removeGuidance` removes **only**
|
|
24
|
+
that block — user content is never touched (pinned by a test).
|
|
25
|
+
- `.crew/` removal requires explicit `force=true` (irreversible — holds run
|
|
26
|
+
history, artifacts, worktrees). Default preserves it.
|
|
27
|
+
- A `realpathSync` + basename guard refuses to `rmSync` anything that isn't a
|
|
28
|
+
`.crew` dir, so a crafted cwd can't trick us into deleting an arbitrary path.
|
|
29
|
+
- The user-scope dir (`~/.pi/agent/extensions/pi-crew/`) is owned by
|
|
30
|
+
`pi uninstall` and is never touched by `team action=cleanup`.
|
|
31
|
+
|
|
32
|
+
### Files
|
|
33
|
+
|
|
34
|
+
- `src/extension/team-tool/lifecycle-actions.ts` — `handleCleanup` dispatcher
|
|
35
|
+
+ new `handleProjectCleanup` (no-runId path). Intent policy now checked once
|
|
36
|
+
in the dispatcher (applies to both modes). Per-run path preserved verbatim.
|
|
37
|
+
- `src/extension/team-tool-types.ts` — `TeamToolDetails.scope?`.
|
|
38
|
+
- `README.md` — new **Uninstall** section documenting the full flow.
|
|
39
|
+
- `test/unit/cleanup-project-mode.test.ts` — NEW, 9 tests (removal, user-content
|
|
40
|
+
preservation, idempotency, force-gating, dry-run, scope rejection, runId
|
|
41
|
+
routing).
|
|
42
|
+
- `test/unit/team-tool-dispatch.test.ts` — updated the no-runId test to the
|
|
43
|
+
new contract (project cleanup, not error).
|
|
44
|
+
|
|
45
|
+
typecheck clean; full suite 2964/0.
|
|
46
|
+
|
|
3
47
|
## [0.8.11] — Split-scope install fix + transient-provider fallback (2026-06-17)
|
|
4
48
|
|
|
5
49
|
Bundle of two independent fixes that were triaged from real user reports on
|
package/README.md
CHANGED
|
@@ -107,6 +107,35 @@ node ./pi-crew/install.mjs # from local clone
|
|
|
107
107
|
> one-line workaround (or install pi-crew in pi's own scope:
|
|
108
108
|
> `npm install -g @earendil-works/pi-crew`).
|
|
109
109
|
|
|
110
|
+
### Uninstall
|
|
111
|
+
|
|
112
|
+
`pi uninstall npm:pi-crew` removes the package, but pi doesn't fire an
|
|
113
|
+
extension uninstall hook, so two things `team action=init` created are left
|
|
114
|
+
behind: the **marker-delimited guidance block in AGENTS.md** and the **`.crew/`
|
|
115
|
+
runtime state directory** (run history, artifacts, worktrees). Reverse them
|
|
116
|
+
explicitly:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
# 1. (Optional) Preview what would be removed, without writing:
|
|
120
|
+
team action=cleanup dryRun=true
|
|
121
|
+
|
|
122
|
+
# 2. Remove the AGENTS.md guidance block only (.crew/ preserved):
|
|
123
|
+
team action=cleanup
|
|
124
|
+
|
|
125
|
+
# 3. Remove BOTH the guidance block AND the .crew/ state directory (force):
|
|
126
|
+
team action=cleanup force=true
|
|
127
|
+
|
|
128
|
+
# 4. Finally, remove the package itself:
|
|
129
|
+
pi uninstall npm:pi-crew
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
The guidance block is wrapped in `<!-- PI-CREW:GUIDANCE:START -->` /
|
|
133
|
+
`<!-- PI-CREW:GUIDANCE:END -->` markers, so `team action=cleanup` removes
|
|
134
|
+
**only** that block — your own AGENTS.md content is never touched. The
|
|
135
|
+
`.crew/` directory is removed **only** with `force=true` (it's irreversible).
|
|
136
|
+
The user-scope dir (`~/.pi/agent/extensions/pi-crew/`) is owned by
|
|
137
|
+
`pi uninstall` and is never touched by `team action=cleanup`.
|
|
138
|
+
|
|
110
139
|
|
|
111
140
|
---
|
|
112
141
|
|
package/package.json
CHANGED
|
@@ -14,6 +14,7 @@ import { enforceDestructiveIntent, intentFromConfig } from "./intent-policy.ts";
|
|
|
14
14
|
import { executeHook, appendHookEvent } from "../../hooks/registry.ts";
|
|
15
15
|
import { resolveRealContainedPath } from "../../utils/safe-paths.ts";
|
|
16
16
|
import { projectCrewRoot, userCrewRoot } from "../../utils/paths.ts";
|
|
17
|
+
import { removeGuidance } from "../../config/markers.ts";
|
|
17
18
|
import * as path from "node:path";
|
|
18
19
|
|
|
19
20
|
export function handleWorktrees(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
|
|
@@ -123,12 +124,128 @@ export async function handleForget(params: TeamToolParamsValue, ctx: TeamContext
|
|
|
123
124
|
}
|
|
124
125
|
|
|
125
126
|
export async function handleCleanup(params: TeamToolParamsValue, ctx: TeamContext): Promise<PiTeamsToolResult> {
|
|
127
|
+
// Intent policy applies to the cleanup action in BOTH modes (per-run and
|
|
128
|
+
// project-level). Checked once here so handleRunCleanup/handleProjectCleanup
|
|
129
|
+
// can stay focused on their own logic.
|
|
126
130
|
const intentError = enforceDestructiveIntent("cleanup", params, ctx.config);
|
|
127
131
|
if (intentError) return intentError;
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
132
|
+
// Two cleanup modes:
|
|
133
|
+
// 1. WITH runId → per-run worktree cleanup (existing behavior).
|
|
134
|
+
// 2. WITHOUT runId → PROJECT-LEVEL uninstall cleanup: removes the
|
|
135
|
+
// AGENTS.md guidance block pi-crew injected (`team action=init`) and
|
|
136
|
+
// optionally the `.crew/` state dir. Use this before/after
|
|
137
|
+
// `pi uninstall npm:pi-crew` to leave the project pristine.
|
|
138
|
+
// Issue #35: pi doesn't fire an uninstall hook for extensions, so this
|
|
139
|
+
// mode is the documented way to reverse an init.
|
|
140
|
+
if (params.runId) {
|
|
141
|
+
return handleRunCleanup(params, ctx);
|
|
142
|
+
}
|
|
143
|
+
return handleProjectCleanup(params, ctx);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Project-level uninstall cleanup (no runId). Reverses `team action=init`:
|
|
148
|
+
* removes the pi-crew guidance block from AGENTS.md (marker-delimited, so
|
|
149
|
+
* user content is untouched) and, with `force: true`, removes the `.crew/`
|
|
150
|
+
* runtime state directory. `dryRun: true` previews without writing.
|
|
151
|
+
*
|
|
152
|
+
* Safety:
|
|
153
|
+
* - `removeGuidance` only touches content between the PI-CREW markers.
|
|
154
|
+
* - `.crew/` removal requires explicit `force: true` (it holds run history,
|
|
155
|
+
* artifacts, and worktrees — irreversible). Default is guidance-only.
|
|
156
|
+
* - The user pi-crew user dir (`~/.pi/agent/extensions/pi-crew/`) is NEVER
|
|
157
|
+
* touched here — `pi uninstall` owns that; we only touch project state.
|
|
158
|
+
*/
|
|
159
|
+
function handleProjectCleanup(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
|
|
160
|
+
const cwd = ctx.cwd;
|
|
161
|
+
const dryRun = params.dryRun === true;
|
|
162
|
+
const removeState = params.force === true;
|
|
163
|
+
const scope = typeof params.scope === "string" ? params.scope : "project";
|
|
164
|
+
if (scope !== "project") {
|
|
165
|
+
return result(
|
|
166
|
+
`Project cleanup operates on the project only (got scope='${scope}'). ` +
|
|
167
|
+
`User-scope files are owned by 'pi uninstall npm:pi-crew'.`,
|
|
168
|
+
{ action: "cleanup", status: "error", scope },
|
|
169
|
+
true,
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const lines: string[] = ["Project cleanup for pi-crew:"];
|
|
174
|
+
|
|
175
|
+
// 1. Remove the AGENTS.md guidance block (marker-delimited → user content preserved).
|
|
176
|
+
const guidancePath = path.join(cwd, "AGENTS.md");
|
|
177
|
+
const guidanceResult = dryRun
|
|
178
|
+
? { path: guidancePath, modified: fs.existsSync(guidancePath), added: [], removed: dryRunRemovedIds(guidancePath) }
|
|
179
|
+
: removeGuidance(guidancePath);
|
|
180
|
+
lines.push("AGENTS.md guidance block:");
|
|
181
|
+
if (guidanceResult.modified) {
|
|
182
|
+
lines.push(` - ${dryRun ? "would remove" : "removed"}: ${guidanceResult.removed.length ? guidanceResult.removed.join(", ") : "(marker section)"}`);
|
|
183
|
+
} else {
|
|
184
|
+
lines.push(" - (no pi-crew marker section found — nothing to do)");
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// 2. Optionally remove the .crew/ runtime state directory (force: true).
|
|
188
|
+
const crewRoot = projectCrewRoot(cwd);
|
|
189
|
+
lines.push(".crew/ state directory:");
|
|
190
|
+
const crewExists = fs.existsSync(crewRoot);
|
|
191
|
+
if (!crewExists) {
|
|
192
|
+
lines.push(` - (not present at ${crewRoot} — nothing to do)`);
|
|
193
|
+
} else if (!removeState) {
|
|
194
|
+
lines.push(` - present at ${crewRoot} (preserved — use force: true to remove; contains run history/artifacts/worktrees and is irreversible)`);
|
|
195
|
+
} else {
|
|
196
|
+
// SAFETY: realpath + contain-check before rmSync, so a crafted cwd can't
|
|
197
|
+
// trick us into deleting an arbitrary directory.
|
|
198
|
+
let resolved: string;
|
|
199
|
+
try {
|
|
200
|
+
resolved = fs.realpathSync.native(crewRoot);
|
|
201
|
+
} catch {
|
|
202
|
+
lines.push(` - ERROR: could not resolve ${crewRoot} (skipped)`);
|
|
203
|
+
return result(lines.join("\n"), { action: "cleanup", status: "ok", scope }, false);
|
|
204
|
+
}
|
|
205
|
+
if (!resolved.endsWith(path.sep + ".crew") && !resolved.endsWith("/teams") && path.basename(resolved) !== ".crew") {
|
|
206
|
+
lines.push(` - ERROR: refused to remove ${resolved} (does not look like a .crew dir) — skipped`);
|
|
207
|
+
} else {
|
|
208
|
+
if (!dryRun) {
|
|
209
|
+
try {
|
|
210
|
+
fs.rmSync(resolved, { recursive: true, force: true });
|
|
211
|
+
} catch (e) {
|
|
212
|
+
lines.push(` - ERROR removing ${resolved}: ${(e as Error).message}`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
lines.push(` - ${dryRun ? "would remove" : "removed"}: ${resolved}`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
lines.push("");
|
|
220
|
+
lines.push(
|
|
221
|
+
dryRun
|
|
222
|
+
? "(dry-run preview — no files were changed. Re-run without dryRun to apply.)"
|
|
223
|
+
: "Done. To fully remove pi-crew, also run: pi uninstall npm:pi-crew",
|
|
224
|
+
);
|
|
225
|
+
return result(lines.join("\n"), { action: "cleanup", status: "ok", scope }, false);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Dry-run helper: read what removeGuidance WOULD remove without writing. */
|
|
229
|
+
function dryRunRemovedIds(guidancePath: string): string[] {
|
|
230
|
+
try {
|
|
231
|
+
if (!fs.existsSync(guidancePath)) return [];
|
|
232
|
+
const content = fs.readFileSync(guidancePath, "utf-8");
|
|
233
|
+
const startIdx = content.indexOf("<!-- PI-CREW:GUIDANCE:START -->");
|
|
234
|
+
const endIdx = content.indexOf("<!-- PI-CREW:GUIDANCE:END -->");
|
|
235
|
+
if (startIdx === -1 || endIdx === -1 || endIdx <= startIdx) return [];
|
|
236
|
+
// Cheap approximation: report the marker section as a unit. Exact block
|
|
237
|
+
// IDs aren't needed for the dry-run summary; the non-dryRun path uses
|
|
238
|
+
// removeGuidance which returns the precise removed IDs.
|
|
239
|
+
return ["pi-crew-overview", "pi-crew-commands"];
|
|
240
|
+
} catch {
|
|
241
|
+
return [];
|
|
242
|
+
}
|
|
243
|
+
}
|
|
131
244
|
|
|
245
|
+
/** Per-run worktree cleanup (existing behavior, preserved). */
|
|
246
|
+
async function handleRunCleanup(params: TeamToolParamsValue, ctx: TeamContext): Promise<PiTeamsToolResult> {
|
|
247
|
+
const loaded = loadRunManifestById(ctx.cwd, params.runId!); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency
|
|
248
|
+
if (!loaded) return result(`Run '${params.runId}' not found.${RUN_NOT_FOUND_HINT}`, { action: "cleanup", status: "error", runId: params.runId }, true);
|
|
132
249
|
// Ownership check — prevent cross-session worktree cleanup unless force is set
|
|
133
250
|
const foreignRun = typeof loaded.manifest.ownerSessionId === "string" && loaded.manifest.ownerSessionId !== ctx.sessionId;
|
|
134
251
|
if (foreignRun && !params.force) return result(`Run ${params.runId} belongs to another session. Use force: true to override.`, { action: "cleanup", status: "error", runId: loaded.manifest.runId }, true);
|
|
@@ -10,6 +10,8 @@ export interface TeamToolDetails {
|
|
|
10
10
|
resumedIds?: string[];
|
|
11
11
|
retriedTaskIds?: string[];
|
|
12
12
|
mailboxIds?: string[];
|
|
13
|
+
/** Resource scope affected by the action (e.g. cleanup: "project"). */
|
|
14
|
+
scope?: string;
|
|
13
15
|
/** Run metrics for compact display in TUI tool result rendering. */
|
|
14
16
|
metrics?: { taskCount?: number; completedCount?: number; totalTokens?: number; totalCost?: number; durationMs?: number; consistencyScore?: number };
|
|
15
17
|
/** Structured data for programmatic consumption (e.g. TUI widgets). */
|