pi-agent-fleet 0.2.0 → 0.3.0

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/src/edits.ts ADDED
@@ -0,0 +1,107 @@
1
+ import { rename, stat, writeFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import type { ActiveFleet } from "./controller.js";
4
+ import { resolveModelReference, type ModelRegistryLike } from "./model-resolution.js";
5
+ import { buildWorkerPrompt } from "./prompts.js";
6
+ import { THINKING_LEVELS } from "./types.js";
7
+ import type { ThinkingLevelName } from "./types.js";
8
+
9
+ export type NodeEditKey = "model" | "effort" | "task";
10
+ export type ConfigEditKey = "max_concurrent" | "warn_cost_usd" | "model" | "effort";
11
+
12
+ export interface EditResult {
13
+ ok: boolean;
14
+ message: string;
15
+ }
16
+
17
+ async function persistSpec(fleet: ActiveFleet): Promise<void> {
18
+ await writeFile(join(fleet.fleetRoot, "fleet.json"), `${JSON.stringify(fleet.spec, null, 2)}\n`, "utf-8");
19
+ }
20
+
21
+ export async function editNode(
22
+ fleet: ActiveFleet,
23
+ nodeId: string,
24
+ key: NodeEditKey,
25
+ value: string,
26
+ registry: ModelRegistryLike,
27
+ ): Promise<EditResult> {
28
+ const worker = fleet.spec.workers.find((w) => w.id === nodeId);
29
+ const node = fleet.state.nodes[nodeId];
30
+ if (!worker || !node) return { ok: false, message: `unknown node "${nodeId}"` };
31
+ if (node.status !== "pending" && node.status !== "ready") {
32
+ return { ok: false, message: `node "${nodeId}" is ${node.status}; only pending nodes can be edited` };
33
+ }
34
+ switch (key) {
35
+ case "model": {
36
+ const r = resolveModelReference(registry, value);
37
+ if (!r.ok) return { ok: false, message: r.error };
38
+ worker.model = `${r.model.provider}/${r.model.id}`;
39
+ break;
40
+ }
41
+ case "effort": {
42
+ if (!THINKING_LEVELS.includes(value as ThinkingLevelName)) {
43
+ return { ok: false, message: `effort must be one of ${THINKING_LEVELS.join(", ")}` };
44
+ }
45
+ worker.effort = value as ThinkingLevelName;
46
+ break;
47
+ }
48
+ case "task": {
49
+ if (value.trim().length === 0) return { ok: false, message: "task must be non-empty" };
50
+ worker.task = value;
51
+ const promptPath = join(fleet.fleetRoot, "workers", nodeId, "prompt.md");
52
+ try {
53
+ await stat(promptPath);
54
+ const prompt = buildWorkerPrompt({ spec: fleet.spec, state: fleet.state, workerId: nodeId, fleetRoot: fleet.fleetRoot });
55
+ const tmp = join(fleet.fleetRoot, "workers", nodeId, `.prompt.md.${process.pid}.${Date.now()}.tmp`);
56
+ await writeFile(tmp, prompt, "utf-8");
57
+ await rename(tmp, promptPath);
58
+ } catch {
59
+ // prompt.md not written yet — nothing to regenerate
60
+ }
61
+ break;
62
+ }
63
+ default:
64
+ return { ok: false, message: `unknown node edit key "${String(key)}" (keys: model, effort, task)` };
65
+ }
66
+ await persistSpec(fleet);
67
+ return { ok: true, message: `node "${nodeId}" ${key} updated` };
68
+ }
69
+
70
+ export async function editConfig(
71
+ fleet: ActiveFleet,
72
+ key: ConfigEditKey,
73
+ value: string,
74
+ registry: ModelRegistryLike,
75
+ ): Promise<EditResult> {
76
+ switch (key) {
77
+ case "max_concurrent": {
78
+ const n = Number(value);
79
+ if (!Number.isInteger(n) || n < 1) return { ok: false, message: "max_concurrent must be an integer >= 1" };
80
+ fleet.spec.config.max_concurrent = n;
81
+ break;
82
+ }
83
+ case "warn_cost_usd": {
84
+ const n = Number(value);
85
+ if (!Number.isFinite(n) || n < 0) return { ok: false, message: "warn_cost_usd must be a number >= 0" };
86
+ fleet.spec.config.warn_cost_usd = n;
87
+ break;
88
+ }
89
+ case "model": {
90
+ const r = resolveModelReference(registry, value);
91
+ if (!r.ok) return { ok: false, message: r.error };
92
+ fleet.spec.config.model = `${r.model.provider}/${r.model.id}`;
93
+ break;
94
+ }
95
+ case "effort": {
96
+ if (!THINKING_LEVELS.includes(value as ThinkingLevelName)) {
97
+ return { ok: false, message: `effort must be one of ${THINKING_LEVELS.join(", ")}` };
98
+ }
99
+ fleet.spec.config.effort = value as ThinkingLevelName;
100
+ break;
101
+ }
102
+ default:
103
+ return { ok: false, message: `unknown config key "${String(key)}" (keys: max_concurrent, warn_cost_usd, model, effort)` };
104
+ }
105
+ await persistSpec(fleet);
106
+ return { ok: true, message: `config.${key} updated` };
107
+ }
@@ -0,0 +1,55 @@
1
+ import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
2
+ import { dirname, join } from "node:path";
3
+ import { buildWorkerPrompt } from "./prompts.js";
4
+ import { writeState } from "./state.js";
5
+ import type { FleetSpec, FleetState } from "./types.js";
6
+
7
+ export function fleetRootFor(cwd: string, name: string): string {
8
+ const ts = new Date().toISOString().replace(/[-:T]/g, "").slice(0, 14);
9
+ return join(cwd, ".fleet", `${name}-${ts}`);
10
+ }
11
+
12
+ export async function isInsideGitRepo(cwd: string): Promise<boolean> {
13
+ let dir = cwd;
14
+ while (true) {
15
+ try {
16
+ const s = await stat(join(dir, ".git"));
17
+ if (s.isFile() || s.isDirectory()) return true;
18
+ } catch {
19
+ // any stat failure (ENOENT, EACCES, etc.) is treated as not-found
20
+ }
21
+ const parent = dirname(dir);
22
+ if (parent === dir) return false;
23
+ dir = parent;
24
+ }
25
+ }
26
+
27
+ export async function ensureFleetGitignore(cwd: string): Promise<void> {
28
+ const dir = join(cwd, ".fleet");
29
+ const gitignore = join(dir, ".gitignore");
30
+ await mkdir(dir, { recursive: true });
31
+ let current = "";
32
+ try {
33
+ current = await readFile(gitignore, "utf-8");
34
+ } catch {
35
+ // created below
36
+ }
37
+ if (!current.split(/\r?\n/).includes("*")) {
38
+ const prefix = current.length > 0 && !current.endsWith("\n") ? "\n" : "";
39
+ await writeFile(gitignore, `${current}${prefix}*\n`, "utf-8");
40
+ }
41
+ }
42
+
43
+ export async function writePlanFiles(fleetRoot: string, spec: FleetSpec, state: FleetState): Promise<void> {
44
+ await mkdir(fleetRoot, { recursive: true });
45
+ await Promise.all(spec.workers.map((w) => mkdir(join(fleetRoot, "workers", w.id, "output"), { recursive: true })));
46
+ await writeFile(join(fleetRoot, "fleet.json"), `${JSON.stringify(spec, null, 2)}\n`, "utf-8");
47
+ await writeState(fleetRoot, state);
48
+ }
49
+
50
+ export async function writeWorkerPrompts(fleet: { spec: FleetSpec; state: FleetState; fleetRoot: string }): Promise<void> {
51
+ await Promise.all(fleet.spec.workers.map(async (w) => {
52
+ const prompt = buildWorkerPrompt({ spec: fleet.spec, state: fleet.state, workerId: w.id, fleetRoot: fleet.fleetRoot });
53
+ await writeFile(join(fleet.fleetRoot, "workers", w.id, "prompt.md"), prompt, "utf-8");
54
+ }));
55
+ }