spawnfile 0.1.2 → 0.1.4

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.
Files changed (33) hide show
  1. package/README.md +3 -0
  2. package/dist/cli/index.js +0 -0
  3. package/dist/cli/runCli.d.ts +18 -2
  4. package/dist/cli/runCli.js +75 -62
  5. package/dist/cli/viewCommand.d.ts +3 -0
  6. package/dist/cli/viewCommand.js +87 -0
  7. package/dist/compiler/buildCompilePlan.js +2 -0
  8. package/dist/compiler/containerEntrypointRender.js +3 -3
  9. package/dist/compiler/index.d.ts +2 -0
  10. package/dist/compiler/index.js +2 -0
  11. package/dist/compiler/moltnetArtifacts.js +7 -3
  12. package/dist/compiler/moltnetResolution.js +29 -48
  13. package/dist/compiler/moltnetRoomMemberships.d.ts +3 -0
  14. package/dist/compiler/moltnetRoomMemberships.js +140 -0
  15. package/dist/compiler/runProject.d.ts +2 -0
  16. package/dist/compiler/runProject.js +20 -6
  17. package/dist/compiler/syncProjectAuth.js +53 -19
  18. package/dist/compiler/types.d.ts +18 -0
  19. package/dist/compiler/view/buildOrganizationView.d.ts +2 -0
  20. package/dist/compiler/view/buildOrganizationView.js +180 -0
  21. package/dist/compiler/view/index.d.ts +4 -0
  22. package/dist/compiler/view/index.js +4 -0
  23. package/dist/compiler/view/renderNetworks.d.ts +2 -0
  24. package/dist/compiler/view/renderNetworks.js +93 -0
  25. package/dist/compiler/view/renderTree.d.ts +2 -0
  26. package/dist/compiler/view/renderTree.js +59 -0
  27. package/dist/compiler/view/sourcePaths.d.ts +2 -0
  28. package/dist/compiler/view/sourcePaths.js +19 -0
  29. package/dist/compiler/view/types.d.ts +80 -0
  30. package/dist/compiler/view/types.js +1 -0
  31. package/dist/runtime/picoclaw/adapter.js +7 -0
  32. package/dist/runtime/picoclaw/runAuth.js +5 -41
  33. package/package.json +2 -2
package/README.md CHANGED
@@ -34,6 +34,7 @@ Node.js 22+ required. See [source install](#from-source) for local development.
34
34
  ```bash
35
35
  spawnfile init # scaffold an agent (defaults to openclaw)
36
36
  spawnfile validate # check the graph
37
+ spawnfile view . # read-only graph view; writes no files
37
38
  spawnfile compile # lower to runtime-native output
38
39
  spawnfile auth sync --profile dev --env-file .env
39
40
  spawnfile build --tag my-agent # compile + docker build
@@ -42,6 +43,8 @@ spawnfile run --tag my-agent --auth-profile dev
42
43
 
43
44
  Compiled output lands under `.spawn/` by default, including a `Dockerfile`, `entrypoint.sh`, `.env.example`, and a prebuilt `container/rootfs/` tree. `spawnfile build` uses the pinned runtime artifacts from `runtimes.yaml`; it does not rebuild runtimes from source.
44
45
 
46
+ Declare external credentials in `secrets:` and provide values through an ignored env file or the shell environment. `spawnfile auth sync --env-file .env` stores declared model auth and project secrets in a local auth profile; `spawnfile run --env-file .env` can inject the same values directly for a single run. This is the intended pattern for credentials like `GH_TOKEN`, MCP tokens, and provider API keys.
47
+
45
48
  ## Project structure
46
49
 
47
50
  A Spawnfile project is either an `agent` or a `team`.
package/dist/cli/index.js CHANGED
File without changes
@@ -1,12 +1,18 @@
1
1
  import { importClaudeCodeAuth, importCodexAuth, importEnvFile, requireAuthProfile } from "../auth/index.js";
2
- import { addAgentProject, addProjectSurface, addProjectModelFallback, addSubagentProject, addTeamProject, buildCompilePlan, buildProject, clearProjectModelFallbacks, compileProject, initProject, removeProjectSurface, runProject, setProjectPrimaryModel, setProjectRuntime, setProjectSurfaceAccess, showProjectSurfaces, syncProjectAuth } from "../compiler/index.js";
2
+ import { addAgentProject, addProjectSurface, addProjectModelFallback, addSubagentProject, addTeamProject, buildOrganizationView, buildCompilePlan, buildProject, clearProjectModelFallbacks, compileProject, initProject, removeProjectSurface, runProject, setProjectPrimaryModel, setProjectRuntime, setProjectSurfaceAccess, showProjectSurfaces, syncProjectAuth } from "../compiler/index.js";
3
3
  import { listRuntimeAdapters } from "../runtime/index.js";
4
4
  export interface CliStreams {
5
5
  stderr: (message: string) => void;
6
6
  stdout: (message: string) => void;
7
7
  }
8
+ export interface CliRenderEnvironment {
9
+ ci: boolean;
10
+ noColor: boolean;
11
+ stdoutIsTty: boolean;
12
+ }
8
13
  export interface CliHandlers {
9
14
  buildCompilePlan: typeof buildCompilePlan;
15
+ buildOrganizationView: typeof buildOrganizationView;
10
16
  buildProject: typeof buildProject;
11
17
  compileProject: typeof compileProject;
12
18
  addAgentProject: typeof addAgentProject;
@@ -29,4 +35,14 @@ export interface CliHandlers {
29
35
  showProjectSurfaces: typeof showProjectSurfaces;
30
36
  syncProjectAuth: typeof syncProjectAuth;
31
37
  }
32
- export declare const runCli: (argv: string[], streams?: CliStreams, handlerOverrides?: Partial<CliHandlers>) => Promise<number>;
38
+ export interface RunCliOptions {
39
+ handlers?: Partial<CliHandlers>;
40
+ renderEnvironment?: CliRenderEnvironment;
41
+ streams?: CliStreams;
42
+ }
43
+ type RunCli = {
44
+ (argv: string[], options?: RunCliOptions): Promise<number>;
45
+ (argv: string[], streams?: CliStreams, handlerOverrides?: Partial<CliHandlers>): Promise<number>;
46
+ };
47
+ export declare const runCli: RunCli;
48
+ export {};
@@ -1,39 +1,60 @@
1
1
  import { Command } from "commander";
2
2
  import { importClaudeCodeAuth, importCodexAuth, importEnvFile, requireAuthProfile } from "../auth/index.js";
3
- import { addAgentProject, addProjectSurface, addProjectModelFallback, addSubagentProject, addTeamProject, buildCompilePlan, buildProject, clearProjectModelFallbacks, compileProject, initProject, removeProjectSurface, runProject, setProjectPrimaryModel, setProjectRuntime, setProjectSurfaceAccess, showProjectSurfaces, syncProjectAuth } from "../compiler/index.js";
3
+ import { addAgentProject, addProjectSurface, addProjectModelFallback, addSubagentProject, addTeamProject, buildOrganizationView, buildCompilePlan, buildProject, clearProjectModelFallbacks, compileProject, initProject, removeProjectSurface, runProject, setProjectPrimaryModel, setProjectRuntime, setProjectSurfaceAccess, showProjectSurfaces, syncProjectAuth } from "../compiler/index.js";
4
4
  import { isSpawnfileError } from "../shared/index.js";
5
5
  import { listRuntimeAdapters } from "../runtime/index.js";
6
6
  import { registerModelCommands } from "./modelCommands.js";
7
7
  import { registerRuntimeCommands } from "./runtimeCommands.js";
8
8
  import { registerSurfaceCommands } from "./surfaceCommands.js";
9
+ import { registerViewCommand } from "./viewCommand.js";
9
10
  const createDefaultStreams = () => ({
10
11
  stderr: (message) => process.stderr.write(`${message}\n`),
11
12
  stdout: (message) => process.stdout.write(`${message}\n`)
12
13
  });
14
+ const createDefaultRenderEnvironment = () => ({
15
+ ci: process.env.CI !== undefined && process.env.CI !== "" && process.env.CI !== "0",
16
+ noColor: process.env.NO_COLOR !== undefined && process.env.NO_COLOR !== "",
17
+ stdoutIsTty: process.stdout.isTTY === true
18
+ });
13
19
  const createDefaultHandlers = () => ({
14
- buildCompilePlan,
15
- buildProject,
16
- compileProject,
17
- addAgentProject,
18
- addProjectModelFallback,
19
- addProjectSurface,
20
- addSubagentProject,
21
- addTeamProject,
22
- clearProjectModelFallbacks,
23
- importClaudeCodeAuth,
24
- importCodexAuth,
25
- importEnvFile,
26
- initProject,
27
- listRuntimeAdapters,
28
- removeProjectSurface,
29
- requireAuthProfile,
30
- runProject,
31
- setProjectPrimaryModel,
32
- setProjectRuntime,
33
- setProjectSurfaceAccess,
34
- showProjectSurfaces,
35
- syncProjectAuth
20
+ buildCompilePlan, buildOrganizationView, buildProject, compileProject,
21
+ addAgentProject, addProjectModelFallback, addProjectSurface,
22
+ addSubagentProject, addTeamProject, clearProjectModelFallbacks,
23
+ importClaudeCodeAuth, importCodexAuth, importEnvFile,
24
+ initProject, listRuntimeAdapters, removeProjectSurface, requireAuthProfile,
25
+ runProject, setProjectPrimaryModel, setProjectRuntime,
26
+ setProjectSurfaceAccess, showProjectSurfaces, syncProjectAuth
36
27
  });
28
+ const isCliStreams = (value) => {
29
+ const candidate = value;
30
+ return typeof candidate?.stderr === "function" && typeof candidate.stdout === "function";
31
+ };
32
+ const normalizeRunCliOptions = (optionsOrStreams, handlerOverrides = {}) => isCliStreams(optionsOrStreams)
33
+ ? {
34
+ handlers: handlerOverrides,
35
+ renderEnvironment: createDefaultRenderEnvironment(),
36
+ streams: optionsOrStreams
37
+ }
38
+ : {
39
+ handlers: optionsOrStreams?.handlers ?? handlerOverrides,
40
+ renderEnvironment: optionsOrStreams?.renderEnvironment ?? createDefaultRenderEnvironment(),
41
+ streams: optionsOrStreams?.streams ?? createDefaultStreams()
42
+ };
43
+ const writeCommanderOutput = (write, message) => {
44
+ const normalized = message.replace(/\n$/, "");
45
+ if (normalized.length > 0) {
46
+ write(normalized);
47
+ }
48
+ };
49
+ const isCommanderError = (error) => {
50
+ if (typeof error !== "object" || error === null) {
51
+ return false;
52
+ }
53
+ const candidate = error;
54
+ return typeof candidate.code === "string"
55
+ && candidate.code.startsWith("commander.")
56
+ && typeof candidate.exitCode === "number";
57
+ };
37
58
  const formatPlanSummary = (plan) => [
38
59
  `root: ${plan.root}`,
39
60
  `nodes: ${plan.nodes.length}`,
@@ -48,10 +69,20 @@ const formatAuthProfileSummary = (profile) => {
48
69
  `imports: ${importedKinds.length > 0 ? importedKinds.join(", ") : "none"}`
49
70
  ];
50
71
  };
51
- export const runCli = async (argv, streams = createDefaultStreams(), handlerOverrides = {}) => {
52
- const handlers = { ...createDefaultHandlers(), ...handlerOverrides };
72
+ const emitLines = (streams, lines) => lines.forEach((line) => streams.stdout(line));
73
+ const emitFileLines = (streams, label, filePaths) => emitLines(streams, filePaths.map((filePath) => `${label} ${filePath}`));
74
+ export const runCli = async (argv, optionsOrStreams, handlerOverrides = {}) => {
75
+ const cliOptions = normalizeRunCliOptions(optionsOrStreams, handlerOverrides);
76
+ const streams = cliOptions.streams;
77
+ const handlers = { ...createDefaultHandlers(), ...cliOptions.handlers };
53
78
  const program = new Command();
54
79
  program.name("spawnfile").description("Spawnfile v0.1 compiler");
80
+ program.exitOverride();
81
+ program.configureOutput({
82
+ outputError: (message, write) => write(message),
83
+ writeErr: (message) => writeCommanderOutput(streams.stderr, message),
84
+ writeOut: (message) => writeCommanderOutput(streams.stdout, message)
85
+ });
55
86
  program
56
87
  .command("compile")
57
88
  .argument("[path]", "Project directory or Spawnfile path", process.cwd())
@@ -82,12 +113,14 @@ export const runCli = async (argv, streams = createDefaultStreams(), handlerOver
82
113
  .option("-t, --tag <image>", "Docker image tag")
83
114
  .option("--auth-profile <name>", "Local Spawnfile auth profile")
84
115
  .option("--name <container>", "Docker container name")
116
+ .option("--env-file <file>", "Path to an env file for runtime secrets")
85
117
  .option("-d, --detach", "Run the container in detached mode")
86
118
  .action(async (inputPath, options) => {
87
119
  const result = await handlers.runProject(inputPath, {
88
120
  authProfile: options.authProfile,
89
121
  containerName: options.name,
90
122
  detach: options.detach,
123
+ envFilePath: options.envFile,
91
124
  imageTag: options.tag,
92
125
  outputDirectory: options.out
93
126
  });
@@ -108,9 +141,7 @@ export const runCli = async (argv, streams = createDefaultStreams(), handlerOver
108
141
  team: options.team
109
142
  });
110
143
  streams.stdout(`initialized ${result.directory}`);
111
- for (const filePath of result.createdFiles) {
112
- streams.stdout(`created ${filePath}`);
113
- }
144
+ emitFileLines(streams, "created", result.createdFiles);
114
145
  });
115
146
  const addCommand = program.command("add").description("Add children to an existing Spawnfile project");
116
147
  addCommand
@@ -124,12 +155,8 @@ export const runCli = async (argv, streams = createDefaultStreams(), handlerOver
124
155
  path: inputPath,
125
156
  runtime: options.runtime
126
157
  });
127
- for (const filePath of result.updatedFiles) {
128
- streams.stdout(`updated ${filePath}`);
129
- }
130
- for (const filePath of result.createdFiles) {
131
- streams.stdout(`created ${filePath}`);
132
- }
158
+ emitFileLines(streams, "updated", result.updatedFiles);
159
+ emitFileLines(streams, "created", result.createdFiles);
133
160
  });
134
161
  addCommand
135
162
  .command("subagent")
@@ -140,12 +167,8 @@ export const runCli = async (argv, streams = createDefaultStreams(), handlerOver
140
167
  id,
141
168
  path: inputPath
142
169
  });
143
- for (const filePath of result.updatedFiles) {
144
- streams.stdout(`updated ${filePath}`);
145
- }
146
- for (const filePath of result.createdFiles) {
147
- streams.stdout(`created ${filePath}`);
148
- }
170
+ emitFileLines(streams, "updated", result.updatedFiles);
171
+ emitFileLines(streams, "created", result.createdFiles);
149
172
  });
150
173
  addCommand
151
174
  .command("team")
@@ -156,16 +179,13 @@ export const runCli = async (argv, streams = createDefaultStreams(), handlerOver
156
179
  id,
157
180
  path: inputPath
158
181
  });
159
- for (const filePath of result.updatedFiles) {
160
- streams.stdout(`updated ${filePath}`);
161
- }
162
- for (const filePath of result.createdFiles) {
163
- streams.stdout(`created ${filePath}`);
164
- }
182
+ emitFileLines(streams, "updated", result.updatedFiles);
183
+ emitFileLines(streams, "created", result.createdFiles);
165
184
  });
166
185
  registerModelCommands(program, handlers, streams);
167
186
  registerRuntimeCommands(program, handlers, streams);
168
187
  registerSurfaceCommands(program, handlers, streams);
188
+ registerViewCommand(program, handlers, streams, cliOptions.renderEnvironment);
169
189
  program
170
190
  .command("validate")
171
191
  .argument("[path]", "Project directory or Spawnfile path", process.cwd())
@@ -192,9 +212,7 @@ export const runCli = async (argv, streams = createDefaultStreams(), handlerOver
192
212
  .option("-p, --profile <name>", "Auth profile name", "default")
193
213
  .action(async (filePath, options) => {
194
214
  const profile = await handlers.importEnvFile(options.profile, filePath);
195
- for (const line of formatAuthProfileSummary(profile)) {
196
- streams.stdout(line);
197
- }
215
+ emitLines(streams, formatAuthProfileSummary(profile));
198
216
  });
199
217
  authImportCommand
200
218
  .command("claude-code")
@@ -202,9 +220,7 @@ export const runCli = async (argv, streams = createDefaultStreams(), handlerOver
202
220
  .option("--from <directory>", "Source Claude Code config directory")
203
221
  .action(async (options) => {
204
222
  const profile = await handlers.importClaudeCodeAuth(options.profile, options.from);
205
- for (const line of formatAuthProfileSummary(profile)) {
206
- streams.stdout(line);
207
- }
223
+ emitLines(streams, formatAuthProfileSummary(profile));
208
224
  });
209
225
  authImportCommand
210
226
  .command("codex")
@@ -212,15 +228,13 @@ export const runCli = async (argv, streams = createDefaultStreams(), handlerOver
212
228
  .option("--from <directory>", "Source Codex config directory")
213
229
  .action(async (options) => {
214
230
  const profile = await handlers.importCodexAuth(options.profile, options.from);
215
- for (const line of formatAuthProfileSummary(profile)) {
216
- streams.stdout(line);
217
- }
231
+ emitLines(streams, formatAuthProfileSummary(profile));
218
232
  });
219
233
  authCommand
220
234
  .command("sync")
221
235
  .argument("[path]", "Project directory or Spawnfile path", process.cwd())
222
236
  .option("-p, --profile <name>", "Auth profile name", "default")
223
- .option("--env-file <file>", "Path to an env file with model API keys")
237
+ .option("--env-file <file>", "Path to an env file with model keys and runtime secrets")
224
238
  .option("--claude-from <directory>", "Source Claude Code config directory")
225
239
  .option("--codex-from <directory>", "Source Codex config directory")
226
240
  .action(async (inputPath, options) => {
@@ -230,24 +244,23 @@ export const runCli = async (argv, streams = createDefaultStreams(), handlerOver
230
244
  envFilePath: options.envFile,
231
245
  profileName: options.profile
232
246
  });
233
- for (const line of formatAuthProfileSummary(profile)) {
234
- streams.stdout(line);
235
- }
247
+ emitLines(streams, formatAuthProfileSummary(profile));
236
248
  });
237
249
  authCommand
238
250
  .command("show")
239
251
  .option("-p, --profile <name>", "Auth profile name", "default")
240
252
  .action(async (options) => {
241
253
  const profile = await handlers.requireAuthProfile(options.profile);
242
- for (const line of formatAuthProfileSummary(profile)) {
243
- streams.stdout(line);
244
- }
254
+ emitLines(streams, formatAuthProfileSummary(profile));
245
255
  });
246
256
  try {
247
257
  await program.parseAsync(argv, { from: "user" });
248
258
  return 0;
249
259
  }
250
260
  catch (error) {
261
+ if (isCommanderError(error)) {
262
+ return error.exitCode === 0 ? 0 : 1;
263
+ }
251
264
  const message = isSpawnfileError(error)
252
265
  ? `${error.code}: ${error.message}`
253
266
  : error instanceof Error
@@ -0,0 +1,3 @@
1
+ import { Command } from "commander";
2
+ import type { CliHandlers, CliRenderEnvironment, CliStreams } from "./runCli.js";
3
+ export declare const registerViewCommand: (program: Command, handlers: CliHandlers, streams: CliStreams, renderEnvironment: CliRenderEnvironment) => void;
@@ -0,0 +1,87 @@
1
+ import { stat } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { Option } from "commander";
4
+ import { renderOrganizationNetworks, renderOrganizationTree } from "../compiler/index.js";
5
+ import { SpawnfileError } from "../shared/index.js";
6
+ const VIEW_MODES = ["tree", "networks"];
7
+ const VIEW_SHOW_OPTIONS = ["paths", "declared"];
8
+ const VIEW_COLOR_OPTIONS = ["auto", "always", "never"];
9
+ const isViewMode = (value) => VIEW_MODES.includes(value);
10
+ const isViewShow = (value) => VIEW_SHOW_OPTIONS.includes(value);
11
+ const manifestPathFor = (inputPath) => path.basename(inputPath) === "Spawnfile"
12
+ ? path.resolve(inputPath)
13
+ : path.resolve(inputPath, "Spawnfile");
14
+ const hasResolvedSpawnfile = async (inputPath) => {
15
+ try {
16
+ return (await stat(manifestPathFor(inputPath))).isFile();
17
+ }
18
+ catch {
19
+ return false;
20
+ }
21
+ };
22
+ const suggestModeOptionForPathToken = async (inputPath) => {
23
+ if (!inputPath || !isViewMode(inputPath) || await hasResolvedSpawnfile(inputPath)) {
24
+ return;
25
+ }
26
+ throw new SpawnfileError("validation_error", `No Spawnfile found for path "${inputPath}". Did you mean "spawnfile view --mode ${inputPath}"?`);
27
+ };
28
+ const resolveColor = (color, environment) => {
29
+ if (color === "always") {
30
+ return true;
31
+ }
32
+ if (color === "never") {
33
+ return false;
34
+ }
35
+ return environment.stdoutIsTty && !environment.ci && !environment.noColor;
36
+ };
37
+ const parseShowLayers = (value) => {
38
+ const layers = new Set();
39
+ if (!value) {
40
+ return layers;
41
+ }
42
+ for (const layer of value.split(",")) {
43
+ const normalized = layer.trim();
44
+ if (!isViewShow(normalized)) {
45
+ throw new SpawnfileError("validation_error", `Unsupported view detail layer "${normalized}". Supported layers: paths, declared.`);
46
+ }
47
+ layers.add(normalized);
48
+ }
49
+ return layers;
50
+ };
51
+ const toRenderOptions = (options, environment) => {
52
+ const showLayers = parseShowLayers(options.show);
53
+ if (options.paths) {
54
+ showLayers.add("paths");
55
+ }
56
+ return {
57
+ ascii: options.ascii,
58
+ color: resolveColor(options.color, environment),
59
+ declared: showLayers.has("declared"),
60
+ paths: showLayers.has("paths")
61
+ };
62
+ };
63
+ const emitRenderedView = (streams, output) => {
64
+ for (const line of output.split("\n")) {
65
+ streams.stdout(line);
66
+ }
67
+ };
68
+ export const registerViewCommand = (program, handlers, streams, renderEnvironment) => {
69
+ program
70
+ .command("view")
71
+ .description("Render a read-only organization view")
72
+ .argument("[path]", "Project directory or Spawnfile path")
73
+ .addOption(new Option("--mode <mode>", "View mode").choices(VIEW_MODES).default("tree"))
74
+ .option("--show <show>", "Comma-separated additional details")
75
+ .option("--ascii", "Use ASCII tree connectors")
76
+ .addOption(new Option("--color <when>", "Color output").choices(VIEW_COLOR_OPTIONS).default("auto"))
77
+ .option("--paths", "Show source paths")
78
+ .action(async (inputPath, options) => {
79
+ await suggestModeOptionForPathToken(inputPath);
80
+ const renderOptions = toRenderOptions(options, renderEnvironment);
81
+ const view = await handlers.buildOrganizationView(inputPath ?? process.cwd());
82
+ const output = options.mode === "networks"
83
+ ? renderOrganizationNetworks(view, renderOptions)
84
+ : renderOrganizationTree(view, renderOptions);
85
+ emitRenderedView(streams, output);
86
+ });
87
+ };
@@ -9,6 +9,7 @@ import { assertRuntimeSupportsExecutionModelAuth } from "./modelAuth.js";
9
9
  import { assertRuntimeSupportsAgentSurfaces } from "./surfaceSupport.js";
10
10
  import { applyExecutionDefaults } from "./executionDefaults.js";
11
11
  import { resolvePlanMoltnetAttachments } from "./moltnetResolution.js";
12
+ import { resolveMoltnetRoomMemberships } from "./moltnetRoomMemberships.js";
12
13
  import { normalizeDescription, resolveDescription, resolveRuntime } from "./buildCompilePlanRuntime.js";
13
14
  import { resolveTeamExternalIds, resolveTeamNetworks, validateTeamNetworkRooms } from "./buildCompilePlanTeams.js";
14
15
  export const buildCompilePlan = async (inputPath) => {
@@ -249,6 +250,7 @@ export const buildCompilePlan = async (inputPath) => {
249
250
  root: rootManifestPath,
250
251
  runtimes
251
252
  };
253
+ compilePlan.moltnetRoomMemberships = resolveMoltnetRoomMemberships(compilePlan);
252
254
  resolvePlanMoltnetAttachments(compilePlan);
253
255
  return compilePlan;
254
256
  };
@@ -8,9 +8,9 @@ const createEnvironmentAssignments = (plan) => {
8
8
  if (plan.instancePaths.homePath) {
9
9
  envAssignments.push(`HOME=${shellQuote(plan.instancePaths.homePath)}`);
10
10
  }
11
- if (plan.runtimeName === "tinyclaw" &&
12
- plan.instancePaths.homePath &&
13
- (plan.modelAuthMethods.openai === "api_key" || plan.modelAuthMethods.openai === "codex")) {
11
+ if (plan.instancePaths.homePath && ((plan.runtimeName === "tinyclaw" &&
12
+ (plan.modelAuthMethods.openai === "api_key" || plan.modelAuthMethods.openai === "codex")) ||
13
+ (plan.runtimeName === "picoclaw" && plan.modelAuthMethods.openai === "codex"))) {
14
14
  envAssignments.push(`CODEX_HOME=${shellQuote(path.posix.join(plan.instancePaths.homePath, ".codex"))}`);
15
15
  }
16
16
  if (plan.meta.homeEnv && plan.instancePaths.homePath) {
@@ -3,9 +3,11 @@ export * from "./buildCompilePlan.js";
3
3
  export * from "./buildProject.js";
4
4
  export * from "./compileProject.js";
5
5
  export * from "./initProject.js";
6
+ export * from "./moltnetRoomMemberships.js";
6
7
  export * from "./runProject.js";
7
8
  export * from "./syncProjectAuth.js";
8
9
  export * from "./types.js";
9
10
  export * from "./updateProjectModels.js";
10
11
  export * from "./updateProjectRuntime.js";
11
12
  export * from "./updateProjectSurfaces.js";
13
+ export * from "./view/index.js";
@@ -3,9 +3,11 @@ export * from "./buildCompilePlan.js";
3
3
  export * from "./buildProject.js";
4
4
  export * from "./compileProject.js";
5
5
  export * from "./initProject.js";
6
+ export * from "./moltnetRoomMemberships.js";
6
7
  export * from "./runProject.js";
7
8
  export * from "./syncProjectAuth.js";
8
9
  export * from "./types.js";
9
10
  export * from "./updateProjectModels.js";
10
11
  export * from "./updateProjectRuntime.js";
11
12
  export * from "./updateProjectSurfaces.js";
13
+ export * from "./view/index.js";
@@ -1,5 +1,6 @@
1
1
  import { getRuntimeAdapter } from "../runtime/index.js";
2
2
  import { SpawnfileError } from "../shared/index.js";
3
+ import { listConcreteMoltnetRoomMemberIds } from "./moltnetRoomMemberships.js";
3
4
  const DEFAULT_MOLTNET_PORT = 8787;
4
5
  const DEFAULT_TINYCLAW_PORT = 3777;
5
6
  const ROOTFS_PREFIX = "container/rootfs";
@@ -86,14 +87,17 @@ export const generateMoltnetArtifacts = async (plan) => {
86
87
  const existingPlan = serverPlans.get(serverKey);
87
88
  if (existingPlan) {
88
89
  for (const room of network.rooms) {
90
+ const concreteMembers = listConcreteMoltnetRoomMemberIds(plan, teamNode.value, network.id, room);
89
91
  const existingRoom = existingPlan.rooms.find((entry) => entry.id === room.id);
90
92
  if (existingRoom) {
91
- existingRoom.members = [...new Set([...existingRoom.members, ...room.members])].sort();
93
+ existingRoom.members = [
94
+ ...new Set([...existingRoom.members, ...concreteMembers])
95
+ ].sort();
92
96
  }
93
97
  else {
94
98
  existingPlan.rooms.push({
95
99
  id: room.id,
96
- members: [...new Set(room.members)].sort()
100
+ members: concreteMembers
97
101
  });
98
102
  }
99
103
  }
@@ -107,7 +111,7 @@ export const generateMoltnetArtifacts = async (plan) => {
107
111
  port: nextPort,
108
112
  rooms: network.rooms.map((room) => ({
109
113
  id: room.id,
110
- members: [...new Set(room.members)].sort()
114
+ members: listConcreteMoltnetRoomMemberIds(plan, teamNode.value, network.id, room)
111
115
  })),
112
116
  teamSource: teamNode.value.source
113
117
  });
@@ -1,5 +1,6 @@
1
1
  import { SpawnfileError } from "../shared/index.js";
2
- import { resolveMoltnetAttachments, resolveTeamRepresentatives } from "./moltnetRepresentativeResolution.js";
2
+ import { resolveMoltnetRoomMemberships } from "./moltnetRoomMemberships.js";
3
+ import { resolveMoltnetAttachments } from "./moltnetRepresentativeResolution.js";
3
4
  export { resolveMoltnetAttachments, resolveTeamRepresentatives } from "./moltnetRepresentativeResolution.js";
4
5
  const findTeamBySource = (plan, source) => {
5
6
  const node = plan.nodes.find((entry) => entry.kind === "team" && entry.value.source === source);
@@ -32,52 +33,26 @@ const validateGlobalMemberIds = (plan) => {
32
33
  seen.set(context.memberId, label);
33
34
  }
34
35
  };
35
- const expandTeamNetworkRooms = (plan) => {
36
- const synthesizedAttachments = [];
37
- for (const node of plan.nodes) {
38
- if (node.value.kind !== "team") {
39
- continue;
40
- }
41
- const teamNode = node.value;
42
- for (const network of teamNode.networks ?? []) {
43
- for (const room of network.rooms) {
44
- const expandedMembers = [];
45
- for (const roomMemberId of room.members) {
46
- const member = teamNode.members.find((entry) => entry.id === roomMemberId);
47
- if (!member) {
48
- throw new SpawnfileError("validation_error", `Team ${teamNode.name} Moltnet room ${room.id} references unknown member ${roomMemberId}`);
49
- }
50
- if (member.kind === "agent") {
51
- expandedMembers.push(member.id);
52
- continue;
53
- }
54
- const childTeam = findTeamBySource(plan, member.nodeSource);
55
- const representatives = resolveTeamRepresentatives(plan, childTeam);
56
- if (representatives.length === 0) {
57
- throw new SpawnfileError("validation_error", `Team ${childTeam.name} has no concrete representative for Moltnet room ${room.id} on ${teamNode.name}`);
58
- }
59
- for (const representative of representatives) {
60
- expandedMembers.push(representative.memberId);
61
- synthesizedAttachments.push({
62
- contextRooms: {
63
- [teamNode.source]: [room.id]
64
- },
65
- memberId: representative.memberId,
66
- network: network.id,
67
- rooms: {
68
- [room.id]: {}
69
- },
70
- teamSource: teamNode.source
71
- });
72
- }
73
- }
74
- room.members = [...new Set(expandedMembers)];
75
- }
76
- }
77
- }
78
- return synthesizedAttachments;
36
+ const getRoomMemberships = (plan) => {
37
+ const memberships = plan.moltnetRoomMemberships ?? resolveMoltnetRoomMemberships(plan);
38
+ plan.moltnetRoomMemberships = memberships;
39
+ return memberships;
79
40
  };
41
+ const synthesizeRepresentativeAttachments = (memberships) => memberships
42
+ .filter((membership) => membership.representedSlot !== undefined)
43
+ .map((membership) => ({
44
+ contextRooms: {
45
+ [membership.declaringTeamSource]: [membership.roomId]
46
+ },
47
+ memberId: membership.concreteMemberId,
48
+ network: membership.networkId,
49
+ rooms: {
50
+ [membership.roomId]: {}
51
+ },
52
+ teamSource: membership.declaringTeamSource
53
+ }));
80
54
  const roomPolicyKey = (policy) => JSON.stringify(policy ?? {});
55
+ const hasRoomPolicy = (policy) => policy.read !== undefined || policy.reply !== undefined;
81
56
  const mergeAttachment = (target, next, nodeName) => {
82
57
  if (target.dms &&
83
58
  next.dms &&
@@ -89,11 +64,16 @@ const mergeAttachment = (target, next, nodeName) => {
89
64
  target.rooms ??= {};
90
65
  for (const [roomId, policy] of Object.entries(next.rooms ?? {})) {
91
66
  const existingPolicy = target.rooms[roomId];
92
- if (existingPolicy &&
67
+ const existingHasPolicy = existingPolicy ? hasRoomPolicy(existingPolicy) : false;
68
+ const nextHasPolicy = hasRoomPolicy(policy);
69
+ if (existingHasPolicy &&
70
+ nextHasPolicy &&
93
71
  roomPolicyKey(existingPolicy) !== roomPolicyKey(policy)) {
94
72
  throw new SpawnfileError("validation_error", `Agent ${nodeName} declares incompatible Moltnet room policy for ${next.network}/${next.memberId ?? "unknown"} room ${roomId}`);
95
73
  }
96
- target.rooms[roomId] = { ...policy };
74
+ target.rooms[roomId] = existingPolicy && existingHasPolicy && !nextHasPolicy
75
+ ? { ...existingPolicy }
76
+ : { ...policy };
97
77
  }
98
78
  if (next.contextRooms) {
99
79
  target.contextRooms ??= {};
@@ -149,7 +129,8 @@ const mergeAgentAttachments = (agentNode, attachments) => {
149
129
  };
150
130
  export const resolvePlanMoltnetAttachments = (plan) => {
151
131
  validateGlobalMemberIds(plan);
152
- const synthesizedAttachments = expandTeamNetworkRooms(plan);
132
+ const roomMemberships = getRoomMemberships(plan);
133
+ const synthesizedAttachments = synthesizeRepresentativeAttachments(roomMemberships);
153
134
  const synthesizedByAgent = new Map();
154
135
  for (const attachment of synthesizedAttachments) {
155
136
  const representativeContext = (plan.memberships ?? []).find((context) => context.memberId === attachment.memberId);
@@ -0,0 +1,3 @@
1
+ import type { CompilePlan, ResolvedMoltnetRoomMembership, ResolvedTeamNetworkRoom, ResolvedTeamNode } from "./types.js";
2
+ export declare const listConcreteMoltnetRoomMemberIds: (plan: CompilePlan, teamNode: ResolvedTeamNode, networkId: string, room: ResolvedTeamNetworkRoom, memberships?: ResolvedMoltnetRoomMembership[] | undefined) => string[];
3
+ export declare const resolveMoltnetRoomMemberships: (plan: CompilePlan) => ResolvedMoltnetRoomMembership[];