pi-crew 0.1.37 → 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/AGENTS.md +1 -1
- package/CHANGELOG.md +27 -0
- package/README.md +5 -0
- package/agents/analyst.md +11 -11
- package/agents/critic.md +11 -11
- package/agents/executor.md +11 -11
- package/agents/explorer.md +11 -11
- package/agents/planner.md +11 -11
- package/agents/reviewer.md +11 -11
- package/agents/security-reviewer.md +11 -11
- package/agents/test-engineer.md +11 -11
- package/agents/verifier.md +11 -11
- package/agents/writer.md +11 -11
- package/docs/refactor-tasks-phase3.md +394 -394
- package/docs/refactor-tasks-phase4.md +564 -564
- package/docs/refactor-tasks-phase5.md +402 -402
- package/docs/refactor-tasks-phase6.md +662 -662
- package/docs/research-extension-examples.md +297 -297
- package/docs/research-extension-system.md +324 -324
- package/docs/research-optimization-plan.md +548 -548
- package/docs/research-pi-coding-agent.md +357 -357
- package/docs/research-source-pi-crew-reference.md +174 -174
- package/docs/resource-formats.md +10 -8
- package/docs/runtime-flow.md +148 -148
- package/docs/source-runtime-refactor-map.md +83 -83
- package/docs/usage.md +6 -0
- package/index.ts +6 -6
- package/package.json +3 -3
- package/schema.json +2 -2
- package/src/agents/agent-serializer.ts +34 -34
- package/src/config/config.ts +8 -4
- package/src/extension/cross-extension-rpc.ts +82 -82
- package/src/extension/import-index.ts +18 -2
- package/src/extension/register.ts +11 -1
- package/src/extension/registration/compaction-guard.ts +125 -125
- package/src/extension/registration/subagent-helpers.ts +30 -6
- package/src/extension/registration/subagent-tools.ts +8 -3
- package/src/extension/result-watcher.ts +98 -98
- package/src/extension/run-import.ts +12 -2
- package/src/extension/run-index.ts +12 -2
- package/src/extension/run-maintenance.ts +24 -24
- package/src/extension/team-tool/api.ts +54 -14
- package/src/extension/team-tool/cancel.ts +31 -31
- package/src/extension/team-tool/doctor.ts +179 -179
- package/src/extension/team-tool/inspect.ts +41 -41
- package/src/extension/team-tool/lifecycle-actions.ts +79 -79
- package/src/extension/team-tool/plan.ts +19 -19
- package/src/extension/team-tool/status.ts +73 -73
- package/src/observability/correlation.ts +35 -35
- package/src/observability/event-to-metric.ts +54 -54
- package/src/observability/exporters/adapter.ts +24 -24
- package/src/observability/exporters/otlp-exporter.ts +65 -65
- package/src/observability/exporters/prometheus-exporter.ts +47 -47
- package/src/observability/metric-registry.ts +72 -72
- package/src/observability/metric-retention.ts +46 -46
- package/src/observability/metric-sink.ts +51 -51
- package/src/observability/metrics-primitives.ts +166 -166
- package/src/prompt/prompt-runtime.ts +68 -68
- package/src/runtime/agent-control.ts +64 -64
- package/src/runtime/agent-memory.ts +72 -72
- package/src/runtime/agent-observability.ts +114 -113
- package/src/runtime/async-marker.ts +26 -26
- package/src/runtime/background-runner.ts +53 -53
- package/src/runtime/crash-recovery.ts +56 -56
- package/src/runtime/crew-agent-records.ts +54 -9
- package/src/runtime/crew-agent-runtime.ts +58 -58
- package/src/runtime/deadletter.ts +36 -36
- package/src/runtime/direct-run.ts +35 -35
- package/src/runtime/foreground-control.ts +82 -82
- package/src/runtime/green-contract.ts +46 -46
- package/src/runtime/group-join.ts +88 -88
- package/src/runtime/heartbeat-gradient.ts +28 -28
- package/src/runtime/heartbeat-watcher.ts +80 -80
- package/src/runtime/live-agent-control.ts +87 -78
- package/src/runtime/live-agent-manager.ts +85 -85
- package/src/runtime/live-control-realtime.ts +36 -36
- package/src/runtime/live-session-runtime.ts +299 -299
- package/src/runtime/manifest-cache.ts +248 -212
- package/src/runtime/model-fallback.ts +261 -261
- package/src/runtime/parallel-research.ts +44 -44
- package/src/runtime/parallel-utils.ts +99 -99
- package/src/runtime/pi-json-output.ts +111 -111
- package/src/runtime/policy-engine.ts +78 -78
- package/src/runtime/post-exit-stdio-guard.ts +86 -86
- package/src/runtime/process-status.ts +56 -56
- package/src/runtime/progress-event-coalescer.ts +43 -43
- package/src/runtime/recovery-recipes.ts +74 -74
- package/src/runtime/retry-executor.ts +59 -59
- package/src/runtime/role-permission.ts +39 -39
- package/src/runtime/session-usage.ts +79 -79
- package/src/runtime/sidechain-output.ts +28 -28
- package/src/runtime/subagent-manager.ts +80 -12
- package/src/runtime/task-display.ts +38 -38
- package/src/runtime/task-output-context.ts +127 -106
- package/src/runtime/task-runner/live-executor.ts +98 -98
- package/src/runtime/task-runner/progress.ts +111 -111
- package/src/runtime/task-runner/result-utils.ts +14 -14
- package/src/runtime/task-runner/state-helpers.ts +22 -22
- package/src/runtime/team-runner.ts +1 -1
- package/src/runtime/worker-heartbeat.ts +21 -21
- package/src/runtime/worker-startup.ts +57 -57
- package/src/schema/config-schema.ts +21 -21
- package/src/schema/team-tool-schema.ts +100 -100
- package/src/state/artifact-store.ts +122 -108
- package/src/state/contracts.ts +105 -105
- package/src/state/jsonl-writer.ts +77 -77
- package/src/state/mailbox.ts +67 -22
- package/src/state/state-store.ts +36 -5
- package/src/state/task-claims.ts +42 -42
- package/src/state/usage.ts +29 -29
- package/src/subagents/async-entry.ts +1 -1
- package/src/subagents/index.ts +3 -3
- package/src/subagents/live/control.ts +1 -1
- package/src/subagents/live/manager.ts +1 -1
- package/src/subagents/live/realtime.ts +1 -1
- package/src/subagents/live/session-runtime.ts +1 -1
- package/src/subagents/manager.ts +1 -1
- package/src/subagents/spawn.ts +1 -1
- package/src/teams/discover-teams.ts +27 -5
- package/src/teams/team-serializer.ts +38 -36
- package/src/types/diff.d.ts +18 -18
- package/src/ui/crew-footer.ts +101 -101
- package/src/ui/crew-select-list.ts +111 -111
- package/src/ui/dashboard-panes/metrics-pane.ts +34 -34
- package/src/ui/dynamic-border.ts +25 -25
- package/src/ui/layout-primitives.ts +106 -106
- package/src/ui/loaders.ts +158 -158
- package/src/ui/mascot.ts +441 -441
- package/src/ui/render-diff.ts +119 -119
- package/src/ui/run-dashboard.ts +5 -2
- package/src/ui/run-snapshot-cache.ts +19 -8
- package/src/ui/spinner.ts +17 -17
- package/src/ui/status-colors.ts +54 -54
- package/src/ui/syntax-highlight.ts +116 -116
- package/src/ui/transcript-viewer.ts +15 -1
- package/src/utils/completion-dedupe.ts +63 -63
- package/src/utils/file-coalescer.ts +84 -84
- package/src/utils/frontmatter.ts +36 -36
- package/src/utils/fs-watch.ts +31 -31
- package/src/utils/git.ts +262 -262
- package/src/utils/ids.ts +12 -12
- package/src/utils/names.ts +26 -26
- package/src/utils/paths.ts +3 -2
- package/src/utils/safe-paths.ts +34 -0
- package/src/utils/sleep.ts +32 -32
- package/src/utils/timings.ts +31 -31
- package/src/utils/visual.ts +159 -159
- package/src/workflows/discover-workflows.ts +30 -3
- package/src/workflows/validate-workflow.ts +40 -40
- package/src/worktree/branch-freshness.ts +45 -45
- package/teams/default.team.md +12 -12
- package/teams/fast-fix.team.md +11 -11
- package/teams/implementation.team.md +18 -18
- package/teams/parallel-research.team.md +14 -14
- package/teams/research.team.md +11 -11
- package/teams/review.team.md +12 -12
- package/workflows/default.workflow.md +29 -29
- package/workflows/fast-fix.workflow.md +22 -22
- package/workflows/implementation.workflow.md +38 -38
- package/workflows/parallel-research.workflow.md +46 -46
- package/workflows/research.workflow.md +22 -22
- package/workflows/review.workflow.md +30 -30
|
@@ -1,77 +1,77 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
|
-
|
|
3
|
-
export interface DrainableSource {
|
|
4
|
-
pause(): void;
|
|
5
|
-
resume(): void;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export interface JsonlWriteStream {
|
|
9
|
-
write(chunk: string): boolean;
|
|
10
|
-
once(event: "drain", listener: () => void): JsonlWriteStream;
|
|
11
|
-
end(callback?: () => void): void;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const DEFAULT_MAX_JSONL_BYTES = 50 * 1024 * 1024;
|
|
15
|
-
|
|
16
|
-
export interface JsonlWriterDeps {
|
|
17
|
-
createWriteStream?: (filePath: string) => JsonlWriteStream;
|
|
18
|
-
maxBytes?: number;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export interface JsonlWriter {
|
|
22
|
-
writeLine(line: string): void;
|
|
23
|
-
close(): Promise<void>;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function createJsonlWriter(filePath: string | undefined, source: DrainableSource, deps: JsonlWriterDeps = {}): JsonlWriter {
|
|
27
|
-
if (!filePath) {
|
|
28
|
-
return {
|
|
29
|
-
writeLine() {},
|
|
30
|
-
async close() {},
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const createWriteStream = deps.createWriteStream ?? ((targetPath: string) => fs.createWriteStream(targetPath, { flags: "a" }));
|
|
35
|
-
let stream: JsonlWriteStream | undefined;
|
|
36
|
-
try {
|
|
37
|
-
stream = createWriteStream(filePath);
|
|
38
|
-
} catch {
|
|
39
|
-
return {
|
|
40
|
-
writeLine() {},
|
|
41
|
-
async close() {},
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
let backpressured = false;
|
|
46
|
-
let closed = false;
|
|
47
|
-
let bytesWritten = 0;
|
|
48
|
-
const maxBytes = deps.maxBytes ?? DEFAULT_MAX_JSONL_BYTES;
|
|
49
|
-
|
|
50
|
-
return {
|
|
51
|
-
writeLine(line: string) {
|
|
52
|
-
if (!stream || closed || !line.trim()) return;
|
|
53
|
-
const chunk = `${line}\n`;
|
|
54
|
-
const chunkBytes = Buffer.byteLength(chunk, "utf-8");
|
|
55
|
-
if (bytesWritten + chunkBytes > maxBytes) return;
|
|
56
|
-
try {
|
|
57
|
-
const ok = stream.write(chunk);
|
|
58
|
-
bytesWritten += chunkBytes;
|
|
59
|
-
if (!ok && !backpressured) {
|
|
60
|
-
backpressured = true;
|
|
61
|
-
source.pause();
|
|
62
|
-
stream.once("drain", () => {
|
|
63
|
-
backpressured = false;
|
|
64
|
-
if (!closed) source.resume();
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
} catch {}
|
|
68
|
-
},
|
|
69
|
-
async close() {
|
|
70
|
-
if (!stream || closed) return;
|
|
71
|
-
closed = true;
|
|
72
|
-
const current = stream;
|
|
73
|
-
stream = undefined;
|
|
74
|
-
await new Promise<void>((resolve) => current.end(() => resolve()));
|
|
75
|
-
},
|
|
76
|
-
};
|
|
77
|
-
}
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
|
|
3
|
+
export interface DrainableSource {
|
|
4
|
+
pause(): void;
|
|
5
|
+
resume(): void;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface JsonlWriteStream {
|
|
9
|
+
write(chunk: string): boolean;
|
|
10
|
+
once(event: "drain", listener: () => void): JsonlWriteStream;
|
|
11
|
+
end(callback?: () => void): void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const DEFAULT_MAX_JSONL_BYTES = 50 * 1024 * 1024;
|
|
15
|
+
|
|
16
|
+
export interface JsonlWriterDeps {
|
|
17
|
+
createWriteStream?: (filePath: string) => JsonlWriteStream;
|
|
18
|
+
maxBytes?: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface JsonlWriter {
|
|
22
|
+
writeLine(line: string): void;
|
|
23
|
+
close(): Promise<void>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function createJsonlWriter(filePath: string | undefined, source: DrainableSource, deps: JsonlWriterDeps = {}): JsonlWriter {
|
|
27
|
+
if (!filePath) {
|
|
28
|
+
return {
|
|
29
|
+
writeLine() {},
|
|
30
|
+
async close() {},
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const createWriteStream = deps.createWriteStream ?? ((targetPath: string) => fs.createWriteStream(targetPath, { flags: "a" }));
|
|
35
|
+
let stream: JsonlWriteStream | undefined;
|
|
36
|
+
try {
|
|
37
|
+
stream = createWriteStream(filePath);
|
|
38
|
+
} catch {
|
|
39
|
+
return {
|
|
40
|
+
writeLine() {},
|
|
41
|
+
async close() {},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let backpressured = false;
|
|
46
|
+
let closed = false;
|
|
47
|
+
let bytesWritten = 0;
|
|
48
|
+
const maxBytes = deps.maxBytes ?? DEFAULT_MAX_JSONL_BYTES;
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
writeLine(line: string) {
|
|
52
|
+
if (!stream || closed || !line.trim()) return;
|
|
53
|
+
const chunk = `${line}\n`;
|
|
54
|
+
const chunkBytes = Buffer.byteLength(chunk, "utf-8");
|
|
55
|
+
if (bytesWritten + chunkBytes > maxBytes) return;
|
|
56
|
+
try {
|
|
57
|
+
const ok = stream.write(chunk);
|
|
58
|
+
bytesWritten += chunkBytes;
|
|
59
|
+
if (!ok && !backpressured) {
|
|
60
|
+
backpressured = true;
|
|
61
|
+
source.pause();
|
|
62
|
+
stream.once("drain", () => {
|
|
63
|
+
backpressured = false;
|
|
64
|
+
if (!closed) source.resume();
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
} catch {}
|
|
68
|
+
},
|
|
69
|
+
async close() {
|
|
70
|
+
if (!stream || closed) return;
|
|
71
|
+
closed = true;
|
|
72
|
+
const current = stream;
|
|
73
|
+
stream = undefined;
|
|
74
|
+
await new Promise<void>((resolve) => current.end(() => resolve()));
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
package/src/state/mailbox.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import type { TeamRunManifest } from "./types.ts";
|
|
4
|
+
import { resolveRealContainedPath } from "../utils/safe-paths.ts";
|
|
4
5
|
|
|
5
6
|
export type MailboxDirection = "inbox" | "outbox";
|
|
6
7
|
export type MailboxMessageStatus = "queued" | "delivered" | "acknowledged";
|
|
@@ -43,33 +44,78 @@ function mailboxDir(manifest: TeamRunManifest): string {
|
|
|
43
44
|
return path.join(manifest.stateRoot, "mailbox");
|
|
44
45
|
}
|
|
45
46
|
|
|
46
|
-
function
|
|
47
|
-
|
|
47
|
+
function safeMailboxDir(manifest: TeamRunManifest, create = false): string {
|
|
48
|
+
const dir = mailboxDir(manifest);
|
|
49
|
+
if (create) fs.mkdirSync(dir, { recursive: true });
|
|
50
|
+
if (!fs.existsSync(dir)) return dir;
|
|
51
|
+
if (fs.lstatSync(dir).isSymbolicLink()) throw new Error(`Invalid mailbox directory: ${dir}`);
|
|
52
|
+
return resolveRealContainedPath(manifest.stateRoot, "mailbox");
|
|
48
53
|
}
|
|
49
54
|
|
|
50
|
-
function
|
|
51
|
-
|
|
55
|
+
function safeTaskId(taskId: string): string {
|
|
56
|
+
if (!/^[\w.-]+$/.test(taskId) || taskId.includes("..") || path.isAbsolute(taskId)) throw new Error(`Invalid mailbox task id: ${taskId}`);
|
|
57
|
+
return taskId;
|
|
52
58
|
}
|
|
53
59
|
|
|
54
|
-
function
|
|
55
|
-
|
|
60
|
+
function safeMailboxTasksRoot(manifest: TeamRunManifest, create = false): string {
|
|
61
|
+
const root = path.join(safeMailboxDir(manifest, create), "tasks");
|
|
62
|
+
if (create) fs.mkdirSync(root, { recursive: true });
|
|
63
|
+
if (!fs.existsSync(root)) return root;
|
|
64
|
+
if (fs.lstatSync(root).isSymbolicLink()) throw new Error(`Invalid mailbox tasks directory: ${root}`);
|
|
65
|
+
return resolveRealContainedPath(safeMailboxDir(manifest), "tasks");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function taskMailboxDir(manifest: TeamRunManifest, taskId: string, create = false): string {
|
|
69
|
+
const tasksRoot = safeMailboxTasksRoot(manifest, create);
|
|
70
|
+
const normalizedTaskId = safeTaskId(taskId);
|
|
71
|
+
const resolved = path.resolve(tasksRoot, normalizedTaskId);
|
|
72
|
+
const relative = path.relative(tasksRoot, resolved);
|
|
73
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) throw new Error(`Invalid mailbox task id: ${taskId}`);
|
|
74
|
+
if (create) fs.mkdirSync(resolved, { recursive: true });
|
|
75
|
+
if (!fs.existsSync(resolved)) return resolved;
|
|
76
|
+
if (fs.lstatSync(resolved).isSymbolicLink()) throw new Error(`Invalid mailbox task directory: ${resolved}`);
|
|
77
|
+
return resolveRealContainedPath(tasksRoot, normalizedTaskId);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function mailboxPath(manifest: TeamRunManifest, direction: MailboxDirection, taskId?: string, create = false): string {
|
|
81
|
+
return taskId ? path.join(taskMailboxDir(manifest, taskId, create), `${direction}.jsonl`) : path.join(safeMailboxDir(manifest, create), `${direction}.jsonl`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function deliveryPath(manifest: TeamRunManifest, create = false): string {
|
|
85
|
+
return path.join(safeMailboxDir(manifest, create), "delivery.json");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function safeMailboxFile(filePath: string, parentDir: string): string {
|
|
89
|
+
if (!fs.existsSync(filePath)) return filePath;
|
|
90
|
+
if (fs.lstatSync(filePath).isSymbolicLink()) throw new Error(`Invalid mailbox file: ${filePath}`);
|
|
91
|
+
return resolveRealContainedPath(parentDir, path.basename(filePath));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function mailboxFile(manifest: TeamRunManifest, direction: MailboxDirection, taskId?: string, create = false): string {
|
|
95
|
+
const parent = taskId ? taskMailboxDir(manifest, taskId, create) : safeMailboxDir(manifest, create);
|
|
96
|
+
return safeMailboxFile(path.join(parent, `${direction}.jsonl`), parent);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function deliveryFile(manifest: TeamRunManifest, create = false): string {
|
|
100
|
+
const parent = safeMailboxDir(manifest, create);
|
|
101
|
+
return safeMailboxFile(path.join(parent, "delivery.json"), parent);
|
|
56
102
|
}
|
|
57
103
|
|
|
58
104
|
function ensureRunMailbox(manifest: TeamRunManifest): void {
|
|
59
|
-
|
|
105
|
+
safeMailboxDir(manifest, true);
|
|
60
106
|
for (const direction of ["inbox", "outbox"] as const) {
|
|
61
|
-
const filePath =
|
|
107
|
+
const filePath = mailboxFile(manifest, direction, undefined, true);
|
|
62
108
|
if (!fs.existsSync(filePath)) fs.writeFileSync(filePath, "", "utf-8");
|
|
63
109
|
}
|
|
64
|
-
const delivery =
|
|
110
|
+
const delivery = deliveryFile(manifest, true);
|
|
65
111
|
if (!fs.existsSync(delivery)) fs.writeFileSync(delivery, `${JSON.stringify({ messages: {}, updatedAt: new Date().toISOString() }, null, 2)}\n`, "utf-8");
|
|
66
112
|
}
|
|
67
113
|
|
|
68
114
|
function ensureTaskMailbox(manifest: TeamRunManifest, taskId: string): void {
|
|
69
115
|
ensureRunMailbox(manifest);
|
|
70
|
-
|
|
116
|
+
taskMailboxDir(manifest, taskId, true);
|
|
71
117
|
for (const direction of ["inbox", "outbox"] as const) {
|
|
72
|
-
const filePath =
|
|
118
|
+
const filePath = mailboxFile(manifest, direction, taskId, true);
|
|
73
119
|
if (!fs.existsSync(filePath)) fs.writeFileSync(filePath, "", "utf-8");
|
|
74
120
|
}
|
|
75
121
|
}
|
|
@@ -112,25 +158,24 @@ function safeReadMailboxFile(filePath: string, direction: MailboxDirection): Mai
|
|
|
112
158
|
|
|
113
159
|
export function readMailbox(manifest: TeamRunManifest, direction?: MailboxDirection, taskId?: string): MailboxMessage[] {
|
|
114
160
|
const directions = direction ? [direction] : ["inbox", "outbox"] as const;
|
|
115
|
-
return directions.flatMap((item) => safeReadMailboxFile(
|
|
161
|
+
return directions.flatMap((item) => safeReadMailboxFile(mailboxFile(manifest, item, taskId), item)).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
116
162
|
}
|
|
117
163
|
|
|
118
164
|
function readAllInboxMessages(manifest: TeamRunManifest): MailboxMessage[] {
|
|
119
|
-
const messages = [...safeReadMailboxFile(
|
|
120
|
-
const tasksDir =
|
|
165
|
+
const messages = [...safeReadMailboxFile(mailboxFile(manifest, "inbox"), "inbox")];
|
|
166
|
+
const tasksDir = safeMailboxTasksRoot(manifest);
|
|
121
167
|
if (fs.existsSync(tasksDir)) {
|
|
122
168
|
for (const entry of fs.readdirSync(tasksDir, { withFileTypes: true })) {
|
|
123
169
|
if (!entry.isDirectory()) continue;
|
|
124
|
-
messages.push(...safeReadMailboxFile(
|
|
170
|
+
messages.push(...safeReadMailboxFile(mailboxFile(manifest, "inbox", entry.name), "inbox"));
|
|
125
171
|
}
|
|
126
172
|
}
|
|
127
173
|
return messages.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
128
174
|
}
|
|
129
175
|
|
|
130
176
|
export function readDeliveryState(manifest: TeamRunManifest): MailboxDeliveryState {
|
|
131
|
-
ensureRunMailbox(manifest);
|
|
132
177
|
try {
|
|
133
|
-
const raw = JSON.parse(fs.readFileSync(
|
|
178
|
+
const raw = JSON.parse(fs.readFileSync(deliveryFile(manifest), "utf-8")) as unknown;
|
|
134
179
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new Error("Invalid delivery state.");
|
|
135
180
|
const obj = raw as Record<string, unknown>;
|
|
136
181
|
const messages: Record<string, MailboxMessageStatus> = {};
|
|
@@ -145,7 +190,7 @@ export function readDeliveryState(manifest: TeamRunManifest): MailboxDeliverySta
|
|
|
145
190
|
|
|
146
191
|
function writeDeliveryState(manifest: TeamRunManifest, state: MailboxDeliveryState): void {
|
|
147
192
|
ensureRunMailbox(manifest);
|
|
148
|
-
fs.writeFileSync(
|
|
193
|
+
fs.writeFileSync(deliveryFile(manifest, true), `${JSON.stringify(state, null, 2)}\n`, "utf-8");
|
|
149
194
|
}
|
|
150
195
|
|
|
151
196
|
export function appendMailboxMessage(manifest: TeamRunManifest, message: Omit<MailboxMessage, "id" | "runId" | "createdAt" | "status"> & { id?: string; status?: MailboxMessageStatus }): MailboxMessage {
|
|
@@ -163,7 +208,7 @@ export function appendMailboxMessage(manifest: TeamRunManifest, message: Omit<Ma
|
|
|
163
208
|
status: message.status ?? "queued",
|
|
164
209
|
taskId: message.taskId,
|
|
165
210
|
};
|
|
166
|
-
fs.appendFileSync(
|
|
211
|
+
fs.appendFileSync(mailboxFile(manifest, complete.direction, complete.taskId), `${JSON.stringify(complete)}\n`, "utf-8");
|
|
167
212
|
const delivery = readDeliveryState(manifest);
|
|
168
213
|
delivery.messages[complete.id] = complete.status;
|
|
169
214
|
delivery.updatedAt = createdAt;
|
|
@@ -196,7 +241,7 @@ export function validateMailbox(manifest: TeamRunManifest, options: { repair?: b
|
|
|
196
241
|
const issues: MailboxValidationIssue[] = [];
|
|
197
242
|
const repaired: string[] = [];
|
|
198
243
|
for (const direction of ["inbox", "outbox"] as const) {
|
|
199
|
-
const filePath =
|
|
244
|
+
const filePath = mailboxFile(manifest, direction);
|
|
200
245
|
const lines = fs.readFileSync(filePath, "utf-8").split(/\r?\n/).filter(Boolean);
|
|
201
246
|
const validLines: string[] = [];
|
|
202
247
|
for (const line of lines) {
|
|
@@ -217,12 +262,12 @@ export function validateMailbox(manifest: TeamRunManifest, options: { repair?: b
|
|
|
217
262
|
}
|
|
218
263
|
const delivery = readDeliveryState(manifest);
|
|
219
264
|
const allMessages = readMailbox(manifest);
|
|
220
|
-
for (const message of allMessages) if (!delivery.messages[message.id]) issues.push({ level: "warning", path:
|
|
265
|
+
for (const message of allMessages) if (!delivery.messages[message.id]) issues.push({ level: "warning", path: deliveryFile(manifest), message: `Missing delivery entry for ${message.id}.` });
|
|
221
266
|
if (options.repair) {
|
|
222
267
|
for (const message of allMessages) delivery.messages[message.id] ??= message.status;
|
|
223
268
|
delivery.updatedAt = new Date().toISOString();
|
|
224
269
|
writeDeliveryState(manifest, delivery);
|
|
225
|
-
repaired.push(
|
|
270
|
+
repaired.push(deliveryFile(manifest));
|
|
226
271
|
}
|
|
227
272
|
return { issues, repaired };
|
|
228
273
|
}
|
package/src/state/state-store.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { appendEvent } from "./event-log.ts";
|
|
|
7
7
|
import { DEFAULT_CACHE, DEFAULT_PATHS } from "../config/defaults.ts";
|
|
8
8
|
import { createRunId, createTaskId } from "../utils/ids.ts";
|
|
9
9
|
import { findRepoRoot, projectCrewRoot, userCrewRoot } from "../utils/paths.ts";
|
|
10
|
+
import { assertSafePathId, resolveContainedRelativePath, resolveRealContainedPath } from "../utils/safe-paths.ts";
|
|
10
11
|
import type { TeamConfig } from "../teams/team-config.ts";
|
|
11
12
|
import type { WorkflowConfig } from "../workflows/workflow-config.ts";
|
|
12
13
|
|
|
@@ -53,14 +54,40 @@ function scopeBaseRoot(cwd: string): string {
|
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
function resolveRunStateRoot(cwd: string, runId: string): string | undefined {
|
|
56
|
-
|
|
57
|
-
|
|
57
|
+
assertSafePathId("runId", runId);
|
|
58
|
+
const runsRoot = path.join(scopeBaseRoot(cwd), DEFAULT_PATHS.state.runsSubdir);
|
|
59
|
+
const scopedPath = resolveContainedRelativePath(runsRoot, runId, "runId");
|
|
60
|
+
if (!fs.existsSync(scopedPath)) return undefined;
|
|
61
|
+
try {
|
|
62
|
+
if (fs.lstatSync(scopedPath).isSymbolicLink()) return undefined;
|
|
63
|
+
resolveRealContainedPath(runsRoot, runId);
|
|
64
|
+
} catch {
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
return scopedPath;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function validateRunManifestPaths(cwd: string, runId: string, manifest: TeamRunManifest, stateRoot: string, tasksPath: string): boolean {
|
|
71
|
+
if (manifest.runId !== runId || manifest.stateRoot !== stateRoot || manifest.tasksPath !== tasksPath || manifest.eventsPath !== path.join(stateRoot, "events.jsonl")) return false;
|
|
72
|
+
const artifactsParent = path.join(scopeBaseRoot(cwd), DEFAULT_PATHS.state.artifactsSubdir);
|
|
73
|
+
const expectedArtifactsRoot = resolveContainedRelativePath(artifactsParent, runId, "runId");
|
|
74
|
+
if (manifest.artifactsRoot !== expectedArtifactsRoot) return false;
|
|
75
|
+
if (fs.existsSync(expectedArtifactsRoot)) {
|
|
76
|
+
try {
|
|
77
|
+
if (fs.lstatSync(expectedArtifactsRoot).isSymbolicLink()) return false;
|
|
78
|
+
resolveRealContainedPath(artifactsParent, runId);
|
|
79
|
+
} catch {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return true;
|
|
58
84
|
}
|
|
59
85
|
|
|
60
86
|
export function createRunPaths(cwd: string, runId = createRunId()): RunPaths {
|
|
87
|
+
assertSafePathId("runId", runId);
|
|
61
88
|
const baseRoot = scopeBaseRoot(cwd);
|
|
62
|
-
const stateRoot = path.join(baseRoot, DEFAULT_PATHS.state.runsSubdir, runId);
|
|
63
|
-
const artifactsRoot = path.join(baseRoot, DEFAULT_PATHS.state.artifactsSubdir, runId);
|
|
89
|
+
const stateRoot = resolveContainedRelativePath(path.join(baseRoot, DEFAULT_PATHS.state.runsSubdir), runId, "runId");
|
|
90
|
+
const artifactsRoot = resolveContainedRelativePath(path.join(baseRoot, DEFAULT_PATHS.state.artifactsSubdir), runId, "runId");
|
|
64
91
|
return {
|
|
65
92
|
runId,
|
|
66
93
|
stateRoot,
|
|
@@ -222,11 +249,15 @@ export function loadRunManifestById(cwd: string, runId: string): { manifest: Tea
|
|
|
222
249
|
&& cached.tasksMtimeMs === tasksMtimeMs
|
|
223
250
|
&& cached.tasksSize === (tasksStat?.size ?? 0)
|
|
224
251
|
) {
|
|
252
|
+
if (!validateRunManifestPaths(cwd, runId, cached.manifest, stateRoot, tasksPath)) {
|
|
253
|
+
manifestCache.delete(stateRoot);
|
|
254
|
+
return undefined;
|
|
255
|
+
}
|
|
225
256
|
return { manifest: cached.manifest, tasks: cached.tasks };
|
|
226
257
|
}
|
|
227
258
|
|
|
228
259
|
const manifest = readJsonFile<TeamRunManifest>(manifestPath);
|
|
229
|
-
if (!manifest) return undefined;
|
|
260
|
+
if (!manifest || !validateRunManifestPaths(cwd, runId, manifest, stateRoot, tasksPath)) return undefined;
|
|
230
261
|
const tasks = readJsonFile<TeamTaskState[]>(tasksPath) ?? [];
|
|
231
262
|
setManifestCache(stateRoot, {
|
|
232
263
|
manifest,
|
package/src/state/task-claims.ts
CHANGED
|
@@ -1,42 +1,42 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
2
|
-
import type { TeamTaskState } from "./types.ts";
|
|
3
|
-
|
|
4
|
-
export interface TaskClaimState {
|
|
5
|
-
owner: string;
|
|
6
|
-
token: string;
|
|
7
|
-
leasedUntil: string;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export function createTaskClaim(owner: string, leaseMs = 5 * 60_000, now = new Date()): TaskClaimState {
|
|
11
|
-
return { owner, token: randomUUID(), leasedUntil: new Date(now.getTime() + leaseMs).toISOString() };
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function isTaskClaimExpired(claim: TaskClaimState | undefined, now = new Date()): boolean {
|
|
15
|
-
if (!claim) return false;
|
|
16
|
-
return Date.parse(claim.leasedUntil) <= now.getTime();
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function canUseTaskClaim(task: Pick<TeamTaskState, "claim">, owner: string, token: string, now = new Date()): boolean {
|
|
20
|
-
return task.claim?.owner === owner && task.claim.token === token && !isTaskClaimExpired(task.claim, now);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function claimTask<T extends TeamTaskState>(task: T, owner: string, leaseMs?: number, now = new Date()): T {
|
|
24
|
-
if (task.claim && !isTaskClaimExpired(task.claim, now)) {
|
|
25
|
-
throw new Error(`Task '${task.id}' is already claimed by '${task.claim.owner}'.`);
|
|
26
|
-
}
|
|
27
|
-
return { ...task, claim: createTaskClaim(owner, leaseMs, now) };
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function releaseTaskClaim<T extends TeamTaskState>(task: T, owner: string, token: string, now = new Date()): T {
|
|
31
|
-
if (!canUseTaskClaim(task, owner, token, now)) {
|
|
32
|
-
throw new Error(`Task '${task.id}' claim is not held by '${owner}' or has expired.`);
|
|
33
|
-
}
|
|
34
|
-
return { ...task, claim: undefined };
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function transitionClaimedTaskStatus<T extends TeamTaskState>(task: T, owner: string, token: string, status: T["status"], now = new Date()): T {
|
|
38
|
-
if (!canUseTaskClaim(task, owner, token, now)) {
|
|
39
|
-
throw new Error(`Task '${task.id}' claim is not held by '${owner}' or has expired.`);
|
|
40
|
-
}
|
|
41
|
-
return { ...task, status };
|
|
42
|
-
}
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import type { TeamTaskState } from "./types.ts";
|
|
3
|
+
|
|
4
|
+
export interface TaskClaimState {
|
|
5
|
+
owner: string;
|
|
6
|
+
token: string;
|
|
7
|
+
leasedUntil: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function createTaskClaim(owner: string, leaseMs = 5 * 60_000, now = new Date()): TaskClaimState {
|
|
11
|
+
return { owner, token: randomUUID(), leasedUntil: new Date(now.getTime() + leaseMs).toISOString() };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function isTaskClaimExpired(claim: TaskClaimState | undefined, now = new Date()): boolean {
|
|
15
|
+
if (!claim) return false;
|
|
16
|
+
return Date.parse(claim.leasedUntil) <= now.getTime();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function canUseTaskClaim(task: Pick<TeamTaskState, "claim">, owner: string, token: string, now = new Date()): boolean {
|
|
20
|
+
return task.claim?.owner === owner && task.claim.token === token && !isTaskClaimExpired(task.claim, now);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function claimTask<T extends TeamTaskState>(task: T, owner: string, leaseMs?: number, now = new Date()): T {
|
|
24
|
+
if (task.claim && !isTaskClaimExpired(task.claim, now)) {
|
|
25
|
+
throw new Error(`Task '${task.id}' is already claimed by '${task.claim.owner}'.`);
|
|
26
|
+
}
|
|
27
|
+
return { ...task, claim: createTaskClaim(owner, leaseMs, now) };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function releaseTaskClaim<T extends TeamTaskState>(task: T, owner: string, token: string, now = new Date()): T {
|
|
31
|
+
if (!canUseTaskClaim(task, owner, token, now)) {
|
|
32
|
+
throw new Error(`Task '${task.id}' claim is not held by '${owner}' or has expired.`);
|
|
33
|
+
}
|
|
34
|
+
return { ...task, claim: undefined };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function transitionClaimedTaskStatus<T extends TeamTaskState>(task: T, owner: string, token: string, status: T["status"], now = new Date()): T {
|
|
38
|
+
if (!canUseTaskClaim(task, owner, token, now)) {
|
|
39
|
+
throw new Error(`Task '${task.id}' claim is not held by '${owner}' or has expired.`);
|
|
40
|
+
}
|
|
41
|
+
return { ...task, status };
|
|
42
|
+
}
|
package/src/state/usage.ts
CHANGED
|
@@ -1,29 +1,29 @@
|
|
|
1
|
-
import type { TeamTaskState, UsageState } from "./types.ts";
|
|
2
|
-
|
|
3
|
-
export function aggregateUsage(tasks: TeamTaskState[]): UsageState | undefined {
|
|
4
|
-
const total: UsageState = {};
|
|
5
|
-
let found = false;
|
|
6
|
-
for (const task of tasks) {
|
|
7
|
-
if (!task.usage) continue;
|
|
8
|
-
found = true;
|
|
9
|
-
total.input = (total.input ?? 0) + (task.usage.input ?? 0);
|
|
10
|
-
total.output = (total.output ?? 0) + (task.usage.output ?? 0);
|
|
11
|
-
total.cacheRead = (total.cacheRead ?? 0) + (task.usage.cacheRead ?? 0);
|
|
12
|
-
total.cacheWrite = (total.cacheWrite ?? 0) + (task.usage.cacheWrite ?? 0);
|
|
13
|
-
total.cost = (total.cost ?? 0) + (task.usage.cost ?? 0);
|
|
14
|
-
total.turns = (total.turns ?? 0) + (task.usage.turns ?? 0);
|
|
15
|
-
}
|
|
16
|
-
return found ? total : undefined;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function formatUsage(usage: UsageState | undefined): string {
|
|
20
|
-
if (!usage) return "(none)";
|
|
21
|
-
const parts: string[] = [];
|
|
22
|
-
if (usage.input !== undefined) parts.push(`input=${usage.input}`);
|
|
23
|
-
if (usage.output !== undefined) parts.push(`output=${usage.output}`);
|
|
24
|
-
if (usage.cacheRead !== undefined) parts.push(`cacheRead=${usage.cacheRead}`);
|
|
25
|
-
if (usage.cacheWrite !== undefined) parts.push(`cacheWrite=${usage.cacheWrite}`);
|
|
26
|
-
if (usage.cost !== undefined) parts.push(`cost=${usage.cost.toFixed(6)}`);
|
|
27
|
-
if (usage.turns !== undefined) parts.push(`turns=${usage.turns}`);
|
|
28
|
-
return parts.join(", ") || "(none)";
|
|
29
|
-
}
|
|
1
|
+
import type { TeamTaskState, UsageState } from "./types.ts";
|
|
2
|
+
|
|
3
|
+
export function aggregateUsage(tasks: TeamTaskState[]): UsageState | undefined {
|
|
4
|
+
const total: UsageState = {};
|
|
5
|
+
let found = false;
|
|
6
|
+
for (const task of tasks) {
|
|
7
|
+
if (!task.usage) continue;
|
|
8
|
+
found = true;
|
|
9
|
+
total.input = (total.input ?? 0) + (task.usage.input ?? 0);
|
|
10
|
+
total.output = (total.output ?? 0) + (task.usage.output ?? 0);
|
|
11
|
+
total.cacheRead = (total.cacheRead ?? 0) + (task.usage.cacheRead ?? 0);
|
|
12
|
+
total.cacheWrite = (total.cacheWrite ?? 0) + (task.usage.cacheWrite ?? 0);
|
|
13
|
+
total.cost = (total.cost ?? 0) + (task.usage.cost ?? 0);
|
|
14
|
+
total.turns = (total.turns ?? 0) + (task.usage.turns ?? 0);
|
|
15
|
+
}
|
|
16
|
+
return found ? total : undefined;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function formatUsage(usage: UsageState | undefined): string {
|
|
20
|
+
if (!usage) return "(none)";
|
|
21
|
+
const parts: string[] = [];
|
|
22
|
+
if (usage.input !== undefined) parts.push(`input=${usage.input}`);
|
|
23
|
+
if (usage.output !== undefined) parts.push(`output=${usage.output}`);
|
|
24
|
+
if (usage.cacheRead !== undefined) parts.push(`cacheRead=${usage.cacheRead}`);
|
|
25
|
+
if (usage.cacheWrite !== undefined) parts.push(`cacheWrite=${usage.cacheWrite}`);
|
|
26
|
+
if (usage.cost !== undefined) parts.push(`cost=${usage.cost.toFixed(6)}`);
|
|
27
|
+
if (usage.turns !== undefined) parts.push(`turns=${usage.turns}`);
|
|
28
|
+
return parts.join(", ") || "(none)";
|
|
29
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export * from "../runtime/async-runner.ts";
|
|
1
|
+
export * from "../runtime/async-runner.ts";
|
package/src/subagents/index.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export * from "./spawn.ts";
|
|
2
|
-
export * from "./manager.ts";
|
|
3
|
-
export * from "./async-entry.ts";
|
|
1
|
+
export * from "./spawn.ts";
|
|
2
|
+
export * from "./manager.ts";
|
|
3
|
+
export * from "./async-entry.ts";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export * from "../../runtime/live-agent-control.ts";
|
|
1
|
+
export * from "../../runtime/live-agent-control.ts";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export * from "../../runtime/live-agent-manager.ts";
|
|
1
|
+
export * from "../../runtime/live-agent-manager.ts";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export * from "../../runtime/live-control-realtime.ts";
|
|
1
|
+
export * from "../../runtime/live-control-realtime.ts";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export * from "../../runtime/live-session-runtime.ts";
|
|
1
|
+
export * from "../../runtime/live-session-runtime.ts";
|
package/src/subagents/manager.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export * from "../runtime/subagent-manager.ts";
|
|
1
|
+
export * from "../runtime/subagent-manager.ts";
|
package/src/subagents/spawn.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export * from "../runtime/child-pi.ts";
|
|
1
|
+
export * from "../runtime/child-pi.ts";
|
|
@@ -12,19 +12,41 @@ export interface TeamDiscoveryResult {
|
|
|
12
12
|
project: TeamConfig[];
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
function parseRoleSkills(value: string | undefined): string[] | false | undefined {
|
|
16
|
+
if (!value) return undefined;
|
|
17
|
+
if (value === "false") return false;
|
|
18
|
+
const skills = value.split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
19
|
+
return skills.length ? skills : undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
15
22
|
function parseRoleLine(line: string): TeamRole | undefined {
|
|
16
23
|
const trimmed = line.trim();
|
|
17
24
|
if (!trimmed.startsWith("-")) return undefined;
|
|
18
25
|
const value = trimmed.slice(1).trim();
|
|
19
26
|
if (!value) return undefined;
|
|
20
|
-
const
|
|
21
|
-
const
|
|
27
|
+
const separator = value.indexOf(":");
|
|
28
|
+
const namePart = separator >= 0 ? value.slice(0, separator) : value;
|
|
29
|
+
const restPart = separator >= 0 ? value.slice(separator + 1) : "";
|
|
30
|
+
const name = namePart.trim();
|
|
22
31
|
if (!name) return undefined;
|
|
23
|
-
const
|
|
32
|
+
const metadata: Record<string, string> = {};
|
|
33
|
+
let descriptionSource = restPart.replace(/\bskills\s*=\s*([\w-]+(?:\s*,\s*[\w-]+)*)/g, (_match, raw: string) => {
|
|
34
|
+
metadata.skills = raw.replace(/\s*,\s*/g, ",").trim();
|
|
35
|
+
return "";
|
|
36
|
+
});
|
|
37
|
+
descriptionSource = descriptionSource.replace(/\b(agent|model|maxConcurrency)\s*=\s*(\S+)/g, (_match, key: string, raw: string) => {
|
|
38
|
+
metadata[key] = raw.trim();
|
|
39
|
+
return "";
|
|
40
|
+
});
|
|
41
|
+
const description = descriptionSource.replace(/\s+/g, " ").trim() || undefined;
|
|
42
|
+
const maxConcurrency = metadata.maxConcurrency ? Number.parseInt(metadata.maxConcurrency, 10) : undefined;
|
|
24
43
|
return {
|
|
25
44
|
name,
|
|
26
|
-
agent:
|
|
27
|
-
description
|
|
45
|
+
agent: metadata.agent ?? name,
|
|
46
|
+
description,
|
|
47
|
+
model: metadata.model,
|
|
48
|
+
skills: parseRoleSkills(metadata.skills),
|
|
49
|
+
maxConcurrency: maxConcurrency && maxConcurrency > 0 ? maxConcurrency : undefined,
|
|
28
50
|
};
|
|
29
51
|
}
|
|
30
52
|
|