agents-pod 0.0.0-stage → 0.2.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/package.json CHANGED
@@ -1,6 +1,41 @@
1
1
  {
2
2
  "name": "agents-pod",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.0",
4
+ "description": "Supervision for long-running coding agents: detached pods, heartbeat liveness, cron-ticked reconcile, and a Pi extension for spawning and handoff",
5
+ "license": "AGPL-3.0-or-later",
6
+ "type": "module",
7
+ "bin": {
8
+ "agents-pod": "bin/agents-pod.mjs"
9
+ },
10
+ "pi": {
11
+ "extensions": [
12
+ "./index.mjs"
13
+ ]
14
+ },
15
+ "files": [
16
+ "bin",
17
+ "src",
18
+ "index.mjs",
19
+ "CHANGELOG.md"
20
+ ],
21
+ "engines": {
22
+ "node": ">=22.19.0"
23
+ },
24
+ "scripts": {
25
+ "test": "node --test \"tests/**/*.test.ts\"",
26
+ "typecheck": "tsc --noEmit",
27
+ "check": "npm run typecheck && npm test"
28
+ },
29
+ "devDependencies": {
30
+ "@earendil-works/pi-ai": "^1.5.0",
31
+ "@earendil-works/pi-coding-agent": "^1.5.0",
32
+ "@types/node": "^24.0.0",
33
+ "typebox": "^1.4.3",
34
+ "typescript": "^5.9.0"
35
+ },
36
+ "peerDependencies": {
37
+ "@earendil-works/pi-ai": "*",
38
+ "@earendil-works/pi-coding-agent": "*",
39
+ "typebox": "*"
40
+ }
41
+ }
@@ -0,0 +1,88 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
3
+ import { dirname } from "node:path";
4
+ import { agentsFile } from "./paths.ts";
5
+
6
+ /** Adapter types are types, not agents: every wake-up names an agent (ADR 0012). */
7
+ export type AgentType = "pi" | "claude" | "codex";
8
+
9
+ /**
10
+ * A definition is five fields and nothing else in P0 (ADR 0012): id, type,
11
+ * optional model, inline role instructions, optional display name. Role
12
+ * instructions are text, never a live path — a standing pod must not starve
13
+ * because a file moved.
14
+ */
15
+ export interface AgentDefinition {
16
+ id: string;
17
+ type: AgentType;
18
+ model?: string;
19
+ instructions: string;
20
+ displayName?: string;
21
+ }
22
+
23
+ /**
24
+ * `default` is a pre-seeded ordinary row, not a reserved word (ADR 0012): the
25
+ * user may edit or delete it, and a missing row falls back to this built-in.
26
+ */
27
+ export const BUILTIN_DEFAULT: AgentDefinition = {
28
+ id: "default",
29
+ type: "pi",
30
+ instructions: "",
31
+ };
32
+
33
+ export function isAgentType(value: string): value is AgentType {
34
+ return value === "pi" || value === "claude" || value === "codex";
35
+ }
36
+
37
+ /**
38
+ * Adapter coverage is Pi-only in 0.1.0, and the type is a promise the wrapper
39
+ * has to keep: it launches one command line — pi's `--print --mode json
40
+ * --session` — whatever the definition says. Accepting `--type claude` and
41
+ * then running pi would be a silent wrong agent, so the type is refused at the
42
+ * surface and again at launch, where a store written by an older build or by
43
+ * hand cannot slip through (ADR 0023).
44
+ */
45
+ export const UNSUPPORTED_AGENT_TYPE =
46
+ "0.1.0 wakes pods with pi only: claude and codex adapters land in a later release";
47
+
48
+ export function agentTypeRefusal(type: AgentType): string | undefined {
49
+ return type === "pi" ? undefined : UNSUPPORTED_AGENT_TYPE;
50
+ }
51
+
52
+ /**
53
+ * The one resolution rule (ADR 0012): an exact id match in the store; a
54
+ * missing `default` falls back to the built-in; anything else is unknown.
55
+ */
56
+ export function resolveAgentDefinition(
57
+ agents: AgentDefinition[],
58
+ id: string,
59
+ ): AgentDefinition | undefined {
60
+ const found = agents.find((agent) => agent.id === id);
61
+ if (found) return found;
62
+ return id === BUILTIN_DEFAULT.id ? { ...BUILTIN_DEFAULT } : undefined;
63
+ }
64
+
65
+ export async function readAgents(): Promise<AgentDefinition[]> {
66
+ try {
67
+ const parsed: unknown = JSON.parse(await readFile(agentsFile(), "utf8"));
68
+ return Array.isArray(parsed) ? (parsed as AgentDefinition[]) : [];
69
+ } catch (error) {
70
+ // A store that has never been written is seeded here — the init-time
71
+ // pre-seed, observable without a separate init command (ADR 0012).
72
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") {
73
+ const seeded = [{ ...BUILTIN_DEFAULT }];
74
+ await writeAgents(seeded);
75
+ return seeded;
76
+ }
77
+ throw error;
78
+ }
79
+ }
80
+
81
+ /** Atomic 0600 write: a crash mid-write never leaves a half-written store. */
82
+ export async function writeAgents(agents: AgentDefinition[]): Promise<void> {
83
+ const file = agentsFile();
84
+ await mkdir(dirname(file), { recursive: true });
85
+ const tmp = `${file}.${randomBytes(4).toString("hex")}.tmp`;
86
+ await writeFile(tmp, `${JSON.stringify(agents, null, 2)}\n`, { mode: 0o600 });
87
+ await rename(tmp, file);
88
+ }