pi-crew 0.1.38 → 0.1.39
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
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.1.39
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Made CI test execution deterministic across Node 22/macOS/Linux/Windows by running Node test files sequentially to avoid cross-file environment races.
|
|
10
|
+
- Fixed live-agent durable control symlink-file rejection to return an API error instead of throwing from the tool handler.
|
|
11
|
+
- Tightened symlink artifact security assertions so tests check leaked file contents rather than safe metadata paths.
|
|
12
|
+
|
|
5
13
|
## 0.1.38
|
|
6
14
|
|
|
7
15
|
### Added
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-crew",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.39",
|
|
4
4
|
"description": "Pi extension for coordinated AI teams, workflows, worktrees, and async task orchestration",
|
|
5
5
|
"author": "baphuongna",
|
|
6
6
|
"license": "MIT",
|
|
@@ -47,8 +47,8 @@
|
|
|
47
47
|
"ci": "npm run typecheck && npm test && npm pack --dry-run",
|
|
48
48
|
"typecheck": "tsc --noEmit && node --experimental-strip-types -e \"await import('./index.ts'); console.log('strip-types import ok')\"",
|
|
49
49
|
"test": "npm run test:unit && npm run test:integration",
|
|
50
|
-
"test:unit": "node --experimental-strip-types --test --test-timeout=30000 test/unit/*.test.ts",
|
|
51
|
-
"test:integration": "node --experimental-strip-types --test --test-timeout=120000 test/integration/*.test.ts",
|
|
50
|
+
"test:unit": "node --experimental-strip-types --test --test-concurrency=1 --test-timeout=30000 test/unit/*.test.ts",
|
|
51
|
+
"test:integration": "node --experimental-strip-types --test --test-concurrency=1 --test-timeout=120000 test/integration/*.test.ts",
|
|
52
52
|
"smoke:pi": "pi install ."
|
|
53
53
|
},
|
|
54
54
|
"exports": {
|
|
@@ -22,7 +22,7 @@ function readEntry(root: string, scope: "project" | "user", runId: string): Impo
|
|
|
22
22
|
let summaryPath: string;
|
|
23
23
|
try {
|
|
24
24
|
const entryRoot = resolveRealContainedPath(root, runId);
|
|
25
|
-
bundlePath = resolveRealContainedPath(root, path.join(
|
|
25
|
+
bundlePath = resolveRealContainedPath(root, path.join(runId, "run-export.json"));
|
|
26
26
|
summaryPath = path.join(entryRoot, "README.md");
|
|
27
27
|
} catch {
|
|
28
28
|
return undefined;
|
|
@@ -133,8 +133,11 @@ export async function handleApi(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
133
133
|
const agent = agentId ? agents.find((item) => item.id === agentId || item.taskId === agentId) : agents[0];
|
|
134
134
|
if (!agent) return result("API read-agent-transcript requires config.agentId matching an agent id or task id, or at least one agent in the run.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
135
135
|
const artifactTranscriptPath = safeContainedPath(loaded.manifest.artifactsRoot, agent.transcriptPath);
|
|
136
|
-
const
|
|
137
|
-
const
|
|
136
|
+
const fallbackPath = agentOutputPath(loaded.manifest, agent.taskId);
|
|
137
|
+
const artifactText = artifactTranscriptPath ? safeReadContainedFile(loaded.manifest.artifactsRoot, artifactTranscriptPath) ?? "" : "";
|
|
138
|
+
const fallbackText = artifactText ? "" : safeReadContainedFile(loaded.manifest.stateRoot, fallbackPath) ?? "";
|
|
139
|
+
const transcriptPath = artifactText ? artifactTranscriptPath : fallbackPath;
|
|
140
|
+
const text = artifactText || fallbackText;
|
|
138
141
|
return result(text || `(no transcript at ${transcriptPath})`, { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
139
142
|
}
|
|
140
143
|
if (operation === "read-agent-output") {
|
|
@@ -191,11 +194,16 @@ export async function handleApi(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
191
194
|
const task = loaded.tasks.find((item) => item.id === agent.taskId);
|
|
192
195
|
if (!task) return result(`API ${operation} agent '${agentId}' does not match a run task.`, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
193
196
|
if (operation === "resume-agent" && !prompt) return result("API resume-agent requires config.prompt or config.message.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
197
|
+
try {
|
|
198
|
+
const request = appendLiveAgentControlRequest(loaded.manifest, { taskId: task.id, agentId: agent.id, operation: operation === "resume-agent" ? "resume" : operation === "steer-agent" ? "steer" : "stop", message: operation === "resume-agent" ? prompt : message });
|
|
199
|
+
publishLiveControlRealtime(request);
|
|
200
|
+
ctx.events?.emit?.("pi-crew:live-control", liveControlRealtimeMessage(request));
|
|
201
|
+
appendEvent(loaded.manifest.eventsPath, { type: "agent.control.queued", runId: loaded.manifest.runId, taskId: agent.taskId, message: `Queued ${request.operation} control request for live agent.`, data: { request, realtime: true } });
|
|
202
|
+
return result(JSON.stringify({ queued: true, request }, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
203
|
+
} catch (queueError) {
|
|
204
|
+
const message = queueError instanceof Error ? queueError.message : String(queueError);
|
|
205
|
+
return result(message, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
206
|
+
}
|
|
199
207
|
}
|
|
200
208
|
}
|
|
201
209
|
if (operation === "read-mailbox") {
|
|
@@ -107,8 +107,8 @@ export function buildTeamDoctorReport(input: TeamDoctorReportInput): TeamDoctorR
|
|
|
107
107
|
const userWritable = checkWritableDir(userCrewRoot());
|
|
108
108
|
const projectWritable = checkWritableDir(projectCrewRoot(input.cwd));
|
|
109
109
|
return [
|
|
110
|
-
{ label: "user state", ok: userWritable.ok, detail: userWritable.detail },
|
|
111
|
-
{ label: "project state", ok: projectWritable.ok, detail: projectWritable.detail },
|
|
110
|
+
{ label: "user state", ok: userWritable.ok || userWritable.detail.endsWith(": missing"), detail: userWritable.detail },
|
|
111
|
+
{ label: "project state", ok: projectWritable.ok || projectWritable.detail.endsWith(": missing"), detail: projectWritable.detail },
|
|
112
112
|
{ label: "project state root", ok: true, detail: path.join(projectCrewRoot(input.cwd), DEFAULT_PATHS.state.runsSubdir) },
|
|
113
113
|
{ label: "artifacts root", ok: true, detail: path.join(projectCrewRoot(input.cwd), DEFAULT_PATHS.state.artifactsSubdir) },
|
|
114
114
|
];
|
|
@@ -58,13 +58,22 @@ function parseManifest(filePath: string): TeamRunManifest | undefined {
|
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
function sameFilesystemPath(left: string, right: string): boolean {
|
|
62
|
+
if (path.resolve(left) === path.resolve(right)) return true;
|
|
63
|
+
try {
|
|
64
|
+
return fs.realpathSync.native(left) === fs.realpathSync.native(right);
|
|
65
|
+
} catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
61
70
|
function validateManifestForRoot(root: string, runId: string, manifest: TeamRunManifest): boolean {
|
|
62
71
|
try {
|
|
63
72
|
if (!isSafePathId(runId)) return false;
|
|
64
73
|
const stateRoot = resolveContainedRelativePath(root, runId, "runId");
|
|
65
74
|
const crewRoot = path.dirname(path.dirname(root));
|
|
66
75
|
const artifactsRoot = resolveContainedRelativePath(path.join(crewRoot, DEFAULT_PATHS.state.artifactsSubdir), runId, "runId");
|
|
67
|
-
if (manifest.runId !== runId || manifest.stateRoot
|
|
76
|
+
if (manifest.runId !== runId || !sameFilesystemPath(manifest.stateRoot, stateRoot) || !sameFilesystemPath(manifest.tasksPath, path.join(stateRoot, DEFAULT_PATHS.state.tasksFile)) || !sameFilesystemPath(manifest.eventsPath, path.join(stateRoot, DEFAULT_PATHS.state.eventsFile)) || !sameFilesystemPath(manifest.artifactsRoot, artifactsRoot)) return false;
|
|
68
77
|
if (fs.existsSync(artifactsRoot)) {
|
|
69
78
|
if (fs.lstatSync(artifactsRoot).isSymbolicLink()) return false;
|
|
70
79
|
resolveRealContainedPath(path.dirname(artifactsRoot), path.basename(artifactsRoot));
|
package/src/utils/paths.ts
CHANGED
|
@@ -29,14 +29,15 @@ export function findRepoRoot(cwd: string): string | undefined {
|
|
|
29
29
|
let current = path.resolve(cwd);
|
|
30
30
|
const root = path.parse(current).root;
|
|
31
31
|
const home = path.resolve(os.homedir());
|
|
32
|
+
const tempRoot = path.resolve(os.tmpdir());
|
|
32
33
|
while (current !== root) {
|
|
33
|
-
if (current === home) return undefined;
|
|
34
|
+
if (current === home || current === tempRoot) return undefined;
|
|
34
35
|
if (hasProjectMarker(current)) return current;
|
|
35
36
|
const parent = path.dirname(current);
|
|
36
37
|
if (parent === current) break;
|
|
37
38
|
current = parent;
|
|
38
39
|
}
|
|
39
|
-
if (current === home) return undefined;
|
|
40
|
+
if (current === home || current === tempRoot) return undefined;
|
|
40
41
|
if (hasProjectMarker(root)) return root;
|
|
41
42
|
return undefined;
|
|
42
43
|
}
|