spawnfile 0.1.2 → 0.1.3

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/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
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())
@@ -108,9 +139,7 @@ export const runCli = async (argv, streams = createDefaultStreams(), handlerOver
108
139
  team: options.team
109
140
  });
110
141
  streams.stdout(`initialized ${result.directory}`);
111
- for (const filePath of result.createdFiles) {
112
- streams.stdout(`created ${filePath}`);
113
- }
142
+ emitFileLines(streams, "created", result.createdFiles);
114
143
  });
115
144
  const addCommand = program.command("add").description("Add children to an existing Spawnfile project");
116
145
  addCommand
@@ -124,12 +153,8 @@ export const runCli = async (argv, streams = createDefaultStreams(), handlerOver
124
153
  path: inputPath,
125
154
  runtime: options.runtime
126
155
  });
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
- }
156
+ emitFileLines(streams, "updated", result.updatedFiles);
157
+ emitFileLines(streams, "created", result.createdFiles);
133
158
  });
134
159
  addCommand
135
160
  .command("subagent")
@@ -140,12 +165,8 @@ export const runCli = async (argv, streams = createDefaultStreams(), handlerOver
140
165
  id,
141
166
  path: inputPath
142
167
  });
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
- }
168
+ emitFileLines(streams, "updated", result.updatedFiles);
169
+ emitFileLines(streams, "created", result.createdFiles);
149
170
  });
150
171
  addCommand
151
172
  .command("team")
@@ -156,16 +177,13 @@ export const runCli = async (argv, streams = createDefaultStreams(), handlerOver
156
177
  id,
157
178
  path: inputPath
158
179
  });
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
- }
180
+ emitFileLines(streams, "updated", result.updatedFiles);
181
+ emitFileLines(streams, "created", result.createdFiles);
165
182
  });
166
183
  registerModelCommands(program, handlers, streams);
167
184
  registerRuntimeCommands(program, handlers, streams);
168
185
  registerSurfaceCommands(program, handlers, streams);
186
+ registerViewCommand(program, handlers, streams, cliOptions.renderEnvironment);
169
187
  program
170
188
  .command("validate")
171
189
  .argument("[path]", "Project directory or Spawnfile path", process.cwd())
@@ -192,9 +210,7 @@ export const runCli = async (argv, streams = createDefaultStreams(), handlerOver
192
210
  .option("-p, --profile <name>", "Auth profile name", "default")
193
211
  .action(async (filePath, options) => {
194
212
  const profile = await handlers.importEnvFile(options.profile, filePath);
195
- for (const line of formatAuthProfileSummary(profile)) {
196
- streams.stdout(line);
197
- }
213
+ emitLines(streams, formatAuthProfileSummary(profile));
198
214
  });
199
215
  authImportCommand
200
216
  .command("claude-code")
@@ -202,9 +218,7 @@ export const runCli = async (argv, streams = createDefaultStreams(), handlerOver
202
218
  .option("--from <directory>", "Source Claude Code config directory")
203
219
  .action(async (options) => {
204
220
  const profile = await handlers.importClaudeCodeAuth(options.profile, options.from);
205
- for (const line of formatAuthProfileSummary(profile)) {
206
- streams.stdout(line);
207
- }
221
+ emitLines(streams, formatAuthProfileSummary(profile));
208
222
  });
209
223
  authImportCommand
210
224
  .command("codex")
@@ -212,9 +226,7 @@ export const runCli = async (argv, streams = createDefaultStreams(), handlerOver
212
226
  .option("--from <directory>", "Source Codex config directory")
213
227
  .action(async (options) => {
214
228
  const profile = await handlers.importCodexAuth(options.profile, options.from);
215
- for (const line of formatAuthProfileSummary(profile)) {
216
- streams.stdout(line);
217
- }
229
+ emitLines(streams, formatAuthProfileSummary(profile));
218
230
  });
219
231
  authCommand
220
232
  .command("sync")
@@ -230,24 +242,23 @@ export const runCli = async (argv, streams = createDefaultStreams(), handlerOver
230
242
  envFilePath: options.envFile,
231
243
  profileName: options.profile
232
244
  });
233
- for (const line of formatAuthProfileSummary(profile)) {
234
- streams.stdout(line);
235
- }
245
+ emitLines(streams, formatAuthProfileSummary(profile));
236
246
  });
237
247
  authCommand
238
248
  .command("show")
239
249
  .option("-p, --profile <name>", "Auth profile name", "default")
240
250
  .action(async (options) => {
241
251
  const profile = await handlers.requireAuthProfile(options.profile);
242
- for (const line of formatAuthProfileSummary(profile)) {
243
- streams.stdout(line);
244
- }
252
+ emitLines(streams, formatAuthProfileSummary(profile));
245
253
  });
246
254
  try {
247
255
  await program.parseAsync(argv, { from: "user" });
248
256
  return 0;
249
257
  }
250
258
  catch (error) {
259
+ if (isCommanderError(error)) {
260
+ return error.exitCode === 0 ? 0 : 1;
261
+ }
251
262
  const message = isSpawnfileError(error)
252
263
  ? `${error.code}: ${error.message}`
253
264
  : 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
  };
@@ -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[];
@@ -0,0 +1,140 @@
1
+ import { SpawnfileError } from "../shared/index.js";
2
+ import { resolveTeamRepresentatives } from "./moltnetRepresentativeResolution.js";
3
+ const hasOwn = (value, key) => Object.prototype.hasOwnProperty.call(value, key);
4
+ const findAgentBySource = (plan, source) => {
5
+ const node = plan.nodes.find((entry) => entry.kind === "agent" && entry.value.source === source);
6
+ if (!node || node.value.kind !== "agent") {
7
+ throw new SpawnfileError("compile_error", `Unable to find agent node at ${source}`);
8
+ }
9
+ return node.value;
10
+ };
11
+ const findTeamBySource = (plan, source) => {
12
+ const node = plan.nodes.find((entry) => entry.kind === "team" && entry.value.source === source);
13
+ if (!node || node.value.kind !== "team") {
14
+ throw new SpawnfileError("compile_error", `Unable to find team node at ${source}`);
15
+ }
16
+ return node.value;
17
+ };
18
+ const clonePolicy = (policy) => ({
19
+ ...(policy.read ? { read: policy.read } : {}),
20
+ ...(policy.reply ? { reply: policy.reply } : {})
21
+ });
22
+ const findDirectRoomPolicy = (plan, teamNode, agentSource, memberId, networkId, roomId) => {
23
+ const agentNode = findAgentBySource(plan, agentSource);
24
+ for (const attachment of agentNode.surfaces?.moltnet ?? []) {
25
+ if (attachment.network !== networkId) {
26
+ continue;
27
+ }
28
+ if (attachment.teamSource && attachment.teamSource !== teamNode.source) {
29
+ continue;
30
+ }
31
+ if (attachment.memberId && attachment.memberId !== memberId) {
32
+ continue;
33
+ }
34
+ if (!attachment.rooms || !hasOwn(attachment.rooms, roomId)) {
35
+ continue;
36
+ }
37
+ return clonePolicy(attachment.rooms[roomId] ?? {});
38
+ }
39
+ return undefined;
40
+ };
41
+ const compareRoomMemberships = (left, right) => [
42
+ left.declaringTeamSource.localeCompare(right.declaringTeamSource),
43
+ left.networkId.localeCompare(right.networkId),
44
+ left.roomId.localeCompare(right.roomId),
45
+ left.declaredSlot.localeCompare(right.declaredSlot),
46
+ (left.representativePath ?? []).join("/").localeCompare((right.representativePath ?? []).join("/")),
47
+ left.concreteMemberId.localeCompare(right.concreteMemberId),
48
+ left.agentSource.localeCompare(right.agentSource)
49
+ ].find((result) => result !== 0) ?? 0;
50
+ export const listConcreteMoltnetRoomMemberIds = (plan, teamNode, networkId, room, memberships = plan.moltnetRoomMemberships) => {
51
+ if (memberships) {
52
+ return [
53
+ ...new Set(memberships
54
+ .filter((membership) => membership.declaringTeamSource === teamNode.source
55
+ && membership.networkId === networkId
56
+ && membership.roomId === room.id)
57
+ .map((membership) => membership.concreteMemberId))
58
+ ].sort();
59
+ }
60
+ const concreteMembers = [];
61
+ for (const declaredSlot of room.members) {
62
+ const member = teamNode.members.find((entry) => entry.id === declaredSlot);
63
+ if (!member) {
64
+ throw new SpawnfileError("validation_error", `Team ${teamNode.name} Moltnet room ${room.id} references unknown member ${declaredSlot}`);
65
+ }
66
+ if (member.kind === "agent") {
67
+ concreteMembers.push(member.id);
68
+ continue;
69
+ }
70
+ const childTeam = findTeamBySource(plan, member.nodeSource);
71
+ const representatives = resolveTeamRepresentatives(plan, childTeam);
72
+ if (representatives.length === 0) {
73
+ throw new SpawnfileError("validation_error", `Team ${childTeam.name} has no concrete representative for Moltnet room ${room.id} on ${teamNode.name}`);
74
+ }
75
+ concreteMembers.push(...representatives.map((representative) => representative.memberId));
76
+ }
77
+ return [...new Set(concreteMembers)].sort();
78
+ };
79
+ export const resolveMoltnetRoomMemberships = (plan) => {
80
+ const memberships = [];
81
+ for (const node of plan.nodes) {
82
+ if (node.value.kind !== "team") {
83
+ continue;
84
+ }
85
+ const teamNode = node.value;
86
+ for (const network of teamNode.networks ?? []) {
87
+ for (const room of network.rooms) {
88
+ for (const declaredSlot of room.members) {
89
+ const declaredMember = teamNode.members.find((member) => member.id === declaredSlot);
90
+ if (!declaredMember) {
91
+ throw new SpawnfileError("validation_error", `Team ${teamNode.name} Moltnet room ${room.id} references unknown member ${declaredSlot}`);
92
+ }
93
+ if (declaredMember.kind === "agent") {
94
+ const agentNode = findAgentBySource(plan, declaredMember.nodeSource);
95
+ const policy = findDirectRoomPolicy(plan, teamNode, agentNode.source, declaredSlot, network.id, room.id);
96
+ memberships.push({
97
+ agentName: agentNode.name,
98
+ agentSource: agentNode.source,
99
+ concreteMemberId: declaredSlot,
100
+ declaredSlot,
101
+ declaringTeamName: teamNode.name,
102
+ declaringTeamSource: teamNode.source,
103
+ directTeamName: teamNode.name,
104
+ directTeamSource: teamNode.source,
105
+ networkId: network.id,
106
+ ...(policy ? { policy } : {}),
107
+ roomId: room.id
108
+ });
109
+ continue;
110
+ }
111
+ const childTeam = findTeamBySource(plan, declaredMember.nodeSource);
112
+ const representatives = resolveTeamRepresentatives(plan, childTeam);
113
+ if (representatives.length === 0) {
114
+ throw new SpawnfileError("validation_error", `Team ${childTeam.name} has no concrete representative for Moltnet room ${room.id} on ${teamNode.name}`);
115
+ }
116
+ for (const representative of representatives) {
117
+ const agentNode = findAgentBySource(plan, representative.agentSource);
118
+ memberships.push({
119
+ agentName: agentNode.name,
120
+ agentSource: agentNode.source,
121
+ concreteMemberId: representative.memberId,
122
+ declaredSlot,
123
+ declaringTeamName: teamNode.name,
124
+ declaringTeamSource: teamNode.source,
125
+ directTeamName: representative.sourceTeamName,
126
+ directTeamSource: representative.sourceTeamSource,
127
+ networkId: network.id,
128
+ representedSlot: declaredSlot,
129
+ representedTeamName: childTeam.name,
130
+ representedTeamSource: childTeam.source,
131
+ representativePath: [declaredSlot, ...representative.path],
132
+ roomId: room.id
133
+ });
134
+ }
135
+ }
136
+ }
137
+ }
138
+ }
139
+ return memberships.sort(compareRoomMemberships);
140
+ };
@@ -189,9 +189,27 @@ export interface ResolvedTeamMembershipContext {
189
189
  teamName: string;
190
190
  teamSource: string;
191
191
  }
192
+ export interface ResolvedMoltnetRoomMembership {
193
+ agentName: string;
194
+ agentSource: string;
195
+ concreteMemberId: string;
196
+ declaredSlot: string;
197
+ declaringTeamName: string;
198
+ declaringTeamSource: string;
199
+ directTeamName: string;
200
+ directTeamSource: string;
201
+ networkId: string;
202
+ policy?: ResolvedMoltnetRoomPolicy;
203
+ representedSlot?: string;
204
+ representedTeamName?: string;
205
+ representedTeamSource?: string;
206
+ representativePath?: string[];
207
+ roomId: string;
208
+ }
192
209
  export interface CompilePlan {
193
210
  edges: CompilePlanEdge[];
194
211
  memberships?: ResolvedTeamMembershipContext[];
212
+ moltnetRoomMemberships?: ResolvedMoltnetRoomMembership[];
195
213
  nodes: CompilePlanNode[];
196
214
  root: string;
197
215
  runtimes: Record<string, {
@@ -0,0 +1,2 @@
1
+ import type { OrganizationView } from "./types.js";
2
+ export declare const buildOrganizationView: (inputPath: string) => Promise<OrganizationView>;
@@ -0,0 +1,180 @@
1
+ import path from "node:path";
2
+ import { SpawnfileError } from "../../shared/index.js";
3
+ import { buildCompilePlan } from "../buildCompilePlan.js";
4
+ import { resolveMoltnetRoomMemberships } from "../moltnetRoomMemberships.js";
5
+ const createNameCounts = (plan) => {
6
+ const counts = new Map();
7
+ for (const node of plan.nodes) {
8
+ const key = `${node.kind}:${node.value.name}`;
9
+ counts.set(key, (counts.get(key) ?? 0) + 1);
10
+ }
11
+ return counts;
12
+ };
13
+ const formatDisplayName = (node, nameCounts) => {
14
+ const key = `${node.kind}:${node.value.name}`;
15
+ return (nameCounts.get(key) ?? 0) > 1
16
+ ? `${node.value.name} [${node.id}]`
17
+ : node.value.name;
18
+ };
19
+ const groupEdgesBySource = (edges) => {
20
+ const groups = new Map();
21
+ for (const edge of edges) {
22
+ const group = groups.get(edge.from) ?? [];
23
+ group.push(edge);
24
+ groups.set(edge.from, group);
25
+ }
26
+ return groups;
27
+ };
28
+ const buildTreeNetworkSummaries = (node) => {
29
+ if (node.value.kind !== "team") {
30
+ return [];
31
+ }
32
+ return (node.value.networks ?? []).map((network) => ({
33
+ expose: network.expose ?? false,
34
+ id: network.id,
35
+ name: network.name,
36
+ provider: network.provider,
37
+ rooms: network.rooms.map((room) => ({
38
+ declaredMembers: [...room.members],
39
+ id: room.id
40
+ }))
41
+ }));
42
+ };
43
+ const buildTreeNode = (node, nodeById, edgesBySource, nameCounts, ancestors = []) => {
44
+ if (ancestors.includes(node.id)) {
45
+ throw new SpawnfileError("compile_error", `Cycle detected while building view tree for ${node.id}`);
46
+ }
47
+ const children = (edgesBySource.get(node.id) ?? []).map((edge) => {
48
+ const child = nodeById.get(edge.to);
49
+ if (!child) {
50
+ throw new SpawnfileError("compile_error", `Unable to find view node ${edge.to}`);
51
+ }
52
+ return {
53
+ label: edge.label,
54
+ node: buildTreeNode(child, nodeById, edgesBySource, nameCounts, [...ancestors, node.id]),
55
+ relation: edge.kind
56
+ };
57
+ });
58
+ return {
59
+ children,
60
+ displayName: formatDisplayName(node, nameCounts),
61
+ ...(node.value.kind === "team"
62
+ ? {
63
+ external: [...node.value.external],
64
+ lead: node.value.lead,
65
+ mode: node.value.mode
66
+ }
67
+ : {}),
68
+ id: node.id,
69
+ kind: node.kind,
70
+ name: node.value.name,
71
+ networks: buildTreeNetworkSummaries(node),
72
+ runtimeName: node.runtimeName,
73
+ source: node.value.source
74
+ };
75
+ };
76
+ const sortNetworkMembers = (declaredMembers, members) => {
77
+ const declaredOrder = new Map(declaredMembers.map((member, index) => [member, index]));
78
+ return [...members].sort((left, right) => (declaredOrder.get(left.declaredSlot) ?? Number.MAX_SAFE_INTEGER)
79
+ - (declaredOrder.get(right.declaredSlot) ?? Number.MAX_SAFE_INTEGER)
80
+ || (left.representativePath ?? []).join("/").localeCompare((right.representativePath ?? []).join("/"))
81
+ || left.concreteMemberId.localeCompare(right.concreteMemberId));
82
+ };
83
+ const toNetworkMemberView = (membership) => ({
84
+ agentName: membership.agentName,
85
+ agentSource: membership.agentSource,
86
+ concreteMemberId: membership.concreteMemberId,
87
+ declaredSlot: membership.declaredSlot,
88
+ directTeamName: membership.directTeamName,
89
+ directTeamSource: membership.directTeamSource,
90
+ ...(membership.policy ? { policy: { ...membership.policy } } : {}),
91
+ ...(membership.representedSlot ? { representedSlot: membership.representedSlot } : {}),
92
+ ...(membership.representedTeamName
93
+ ? { representedTeamName: membership.representedTeamName }
94
+ : {}),
95
+ ...(membership.representedTeamSource
96
+ ? { representedTeamSource: membership.representedTeamSource }
97
+ : {}),
98
+ ...(membership.representativePath
99
+ ? { representativePath: [...membership.representativePath] }
100
+ : {})
101
+ });
102
+ const createNetworkKey = (provider, networkId) => `${provider}::${networkId}`;
103
+ const buildNetworkDeclaration = (teamNode, network, roomMemberships) => ({
104
+ declaringTeamName: teamNode.name,
105
+ declaringTeamSource: teamNode.source,
106
+ expose: network.expose ?? false,
107
+ name: network.name,
108
+ rooms: network.rooms.map((room) => {
109
+ const members = roomMemberships
110
+ .filter((membership) => membership.declaringTeamSource === teamNode.source
111
+ && membership.networkId === network.id
112
+ && membership.roomId === room.id)
113
+ .map(toNetworkMemberView);
114
+ return {
115
+ declaredMembers: [...room.members],
116
+ id: room.id,
117
+ members: sortNetworkMembers(room.members, members)
118
+ };
119
+ })
120
+ });
121
+ const buildNetworks = (plan) => {
122
+ const roomMemberships = plan.moltnetRoomMemberships
123
+ ?? resolveMoltnetRoomMemberships(plan);
124
+ const groups = new Map();
125
+ for (const node of plan.nodes) {
126
+ if (node.value.kind !== "team") {
127
+ continue;
128
+ }
129
+ const teamNode = node.value;
130
+ for (const network of teamNode.networks ?? []) {
131
+ const key = createNetworkKey(network.provider, network.id);
132
+ const declaration = buildNetworkDeclaration(teamNode, network, roomMemberships);
133
+ const existing = groups.get(key);
134
+ if (existing) {
135
+ existing.declarations = [...(existing.declarations ?? []), declaration];
136
+ continue;
137
+ }
138
+ groups.set(key, {
139
+ declaringTeamName: declaration.declaringTeamName,
140
+ declaringTeamSource: declaration.declaringTeamSource,
141
+ declarations: [declaration],
142
+ expose: declaration.expose,
143
+ id: network.id,
144
+ name: declaration.name,
145
+ provider: network.provider,
146
+ rooms: declaration.rooms
147
+ });
148
+ }
149
+ }
150
+ const networks = [...groups.values()];
151
+ for (const network of networks) {
152
+ const firstDeclaration = network.declarations?.[0];
153
+ if (firstDeclaration) {
154
+ network.declaringTeamName = firstDeclaration.declaringTeamName;
155
+ network.declaringTeamSource = firstDeclaration.declaringTeamSource;
156
+ network.expose = firstDeclaration.expose;
157
+ network.name = firstDeclaration.name;
158
+ network.rooms = firstDeclaration.rooms;
159
+ }
160
+ }
161
+ return networks;
162
+ };
163
+ export const buildOrganizationView = async (inputPath) => {
164
+ const plan = await buildCompilePlan(inputPath);
165
+ const rootNode = plan.nodes.find((node) => node.value.source === plan.root);
166
+ if (!rootNode) {
167
+ throw new SpawnfileError("compile_error", `Unable to find root view node for ${plan.root}`);
168
+ }
169
+ const nodeById = new Map(plan.nodes.map((node) => [node.id, node]));
170
+ const root = buildTreeNode(rootNode, nodeById, groupEdgesBySource(plan.edges), createNameCounts(plan));
171
+ return {
172
+ contexts: [],
173
+ diagnostics: [],
174
+ inputPath,
175
+ networks: buildNetworks(plan),
176
+ projectRoot: path.dirname(plan.root),
177
+ root,
178
+ runtimes: []
179
+ };
180
+ };
@@ -0,0 +1,4 @@
1
+ export * from "./buildOrganizationView.js";
2
+ export * from "./renderNetworks.js";
3
+ export * from "./renderTree.js";
4
+ export * from "./types.js";
@@ -0,0 +1,4 @@
1
+ export * from "./buildOrganizationView.js";
2
+ export * from "./renderNetworks.js";
3
+ export * from "./renderTree.js";
4
+ export * from "./types.js";
@@ -0,0 +1,2 @@
1
+ import type { OrganizationView, RenderOrganizationViewOptions } from "./types.js";
2
+ export declare const renderOrganizationNetworks: (view: OrganizationView, options?: RenderOrganizationViewOptions) => string;
@@ -0,0 +1,93 @@
1
+ import { formatSourceMeta } from "./sourcePaths.js";
2
+ const color = (value, code, options) => options.color ? `\u001b[${code}m${value}\u001b[0m` : value;
3
+ const glyphsFor = (options) => options.ascii
4
+ ? { branch: "|-- ", last: "`-- ", pipe: "| ", space: " " }
5
+ : { branch: "├── ", last: "└── ", pipe: "│ ", space: " " };
6
+ const formatPolicy = (member) => {
7
+ if (!member.policy) {
8
+ return [];
9
+ }
10
+ return [
11
+ member.policy.read ? `read=${member.policy.read}` : undefined,
12
+ member.policy.reply ? `reply=${member.policy.reply}` : undefined
13
+ ].filter((entry) => entry !== undefined);
14
+ };
15
+ const formatMember = (member, options, projectRoot) => {
16
+ const metadata = member.representedSlot
17
+ ? [
18
+ `represents=${member.representedSlot}`,
19
+ member.representedTeamName && member.representedTeamName !== member.representedSlot
20
+ ? `team=${member.representedTeamName}`
21
+ : undefined,
22
+ `member=${member.concreteMemberId}`
23
+ ]
24
+ : [
25
+ `team=${member.directTeamName}`,
26
+ `member=${member.concreteMemberId}`
27
+ ];
28
+ const source = options.paths
29
+ ? formatSourceMeta("source", member.agentSource, projectRoot)
30
+ : "";
31
+ const details = [
32
+ ...metadata,
33
+ ...formatPolicy(member)
34
+ ].filter((entry) => entry !== undefined);
35
+ return `${member.agentName} ${details.join(" ")}${source}`;
36
+ };
37
+ const formatNetwork = (network, options) => {
38
+ const id = color(network.id, "36", options);
39
+ return `${network.provider} ${id}`;
40
+ };
41
+ const getDeclarations = (network) => network.declarations ?? [
42
+ {
43
+ declaringTeamName: network.declaringTeamName,
44
+ declaringTeamSource: network.declaringTeamSource,
45
+ expose: network.expose,
46
+ name: network.name,
47
+ rooms: network.rooms
48
+ }
49
+ ];
50
+ const formatDeclaration = (network, declaration, options, projectRoot) => {
51
+ const exposed = declaration.expose ? " exposed" : "";
52
+ const source = options.paths
53
+ ? formatSourceMeta("declared_source", declaration.declaringTeamSource, projectRoot)
54
+ : "";
55
+ return `${network.id} "${declaration.name}" on ${declaration.declaringTeamName}${exposed}${source}`;
56
+ };
57
+ export const renderOrganizationNetworks = (view, options = {}) => {
58
+ if (view.networks.length === 0) {
59
+ return "No Moltnet networks.";
60
+ }
61
+ const glyphs = glyphsFor(options);
62
+ const lines = ["Moltnet networks"];
63
+ view.networks.forEach((network, networkIndex) => {
64
+ const networkLast = networkIndex === view.networks.length - 1;
65
+ const networkPrefix = networkLast ? glyphs.space : glyphs.pipe;
66
+ lines.push(`${networkLast ? glyphs.last : glyphs.branch}${formatNetwork(network, options)}`);
67
+ const declarations = getDeclarations(network);
68
+ declarations.forEach((declaration, declarationIndex) => {
69
+ const declarationLast = declarationIndex === declarations.length - 1;
70
+ const declarationPrefix = `${networkPrefix}${declarationLast ? glyphs.space : glyphs.pipe}`;
71
+ lines.push(`${networkPrefix}${declarationLast ? glyphs.last : glyphs.branch}${formatDeclaration(network, declaration, options, view.projectRoot)}`);
72
+ declaration.rooms.forEach((room, roomIndex) => {
73
+ const roomLast = roomIndex === declaration.rooms.length - 1;
74
+ const roomPrefix = `${declarationPrefix}${roomLast ? glyphs.space : glyphs.pipe}`;
75
+ const legacyDeclared = options.declared && !view.projectRoot
76
+ ? ` room ${room.id} declared [${room.declaredMembers.join(", ")}]`
77
+ : "";
78
+ lines.push(`${declarationPrefix}${roomLast ? glyphs.last : glyphs.branch}#${room.id}${legacyDeclared}`);
79
+ if (options.declared) {
80
+ const declaredMembers = room.declaredMembers.length > 0
81
+ ? room.declaredMembers.join(", ")
82
+ : "(none)";
83
+ lines.push(`${roomPrefix}${glyphs.branch}declared members: ${declaredMembers}`);
84
+ }
85
+ room.members.forEach((member, memberIndex) => {
86
+ const memberLast = memberIndex === room.members.length - 1;
87
+ lines.push(`${roomPrefix}${memberLast ? glyphs.last : glyphs.branch}${formatMember(member, options, view.projectRoot)}`);
88
+ });
89
+ });
90
+ });
91
+ });
92
+ return lines.join("\n");
93
+ };
@@ -0,0 +1,2 @@
1
+ import type { OrganizationView, RenderOrganizationViewOptions } from "./types.js";
2
+ export declare const renderOrganizationTree: (view: OrganizationView, options?: RenderOrganizationViewOptions) => string;
@@ -0,0 +1,59 @@
1
+ import { formatSourceMeta } from "./sourcePaths.js";
2
+ const color = (value, code, options) => options.color ? `\u001b[${code}m${value}\u001b[0m` : value;
3
+ const formatNode = (node, options, projectRoot) => {
4
+ const kind = color(node.kind, node.kind === "team" ? "36" : "32", options);
5
+ const metadata = node.kind === "team"
6
+ ? [
7
+ node.mode ? `mode=${node.mode}` : undefined,
8
+ node.lead ? `lead=${node.lead}` : undefined,
9
+ node.external && node.external.length > 0
10
+ ? `external=${node.external.join(",")}`
11
+ : undefined
12
+ ]
13
+ : [
14
+ node.runtimeName ? `[${node.runtimeName}]` : undefined,
15
+ node.runtimeName ? `runtime=${node.runtimeName}` : undefined
16
+ ];
17
+ const details = metadata.filter((entry) => entry !== undefined);
18
+ const source = options.paths ? formatSourceMeta("source", node.source, projectRoot) : "";
19
+ const separator = details[0]?.startsWith("[") ? " " : " ";
20
+ return `${kind} ${node.displayName}${details.length > 0 ? `${separator}${details.join(" ")}` : ""}${source}`;
21
+ };
22
+ const formatEdgeLabel = (edge) => edge.relation === "subagent"
23
+ ? `subagent ${edge.label}`
24
+ : edge.label;
25
+ const formatNetworkSummary = (network, options) => {
26
+ const id = color(network.id, "36", options);
27
+ const exposed = network.expose ? " exposed" : "";
28
+ const rooms = network.rooms
29
+ .map((room) => `${room.id} [${room.declaredMembers.join(", ")}]`)
30
+ .join("; ");
31
+ return `network ${id} "${network.name}"${exposed}: ${rooms}`;
32
+ };
33
+ const renderChildren = (node, options, projectRoot, prefix = "") => {
34
+ const glyphs = options.ascii
35
+ ? { branch: "|-- ", last: "`-- ", pipe: "| ", space: " " }
36
+ : { branch: "├── ", last: "└── ", pipe: "│ ", space: " " };
37
+ const items = [
38
+ ...(node.networks ?? []).map((network) => ({ kind: "network", network })),
39
+ ...node.children.map((edge) => ({ kind: "edge", edge }))
40
+ ];
41
+ return items.flatMap((item, index) => {
42
+ const isLast = index === items.length - 1;
43
+ const connector = isLast ? glyphs.last : glyphs.branch;
44
+ const nextPrefix = `${prefix}${isLast ? glyphs.space : glyphs.pipe}`;
45
+ if (item.kind === "network") {
46
+ return [`${prefix}${connector}${formatNetworkSummary(item.network, options)}`];
47
+ }
48
+ const edge = item.edge;
49
+ const line = `${prefix}${connector}${formatEdgeLabel(edge)}: ${formatNode(edge.node, options, projectRoot)}`;
50
+ return [
51
+ line,
52
+ ...renderChildren(edge.node, options, projectRoot, nextPrefix)
53
+ ];
54
+ });
55
+ };
56
+ export const renderOrganizationTree = (view, options = {}) => [
57
+ formatNode(view.root, options, view.projectRoot),
58
+ ...renderChildren(view.root, options, view.projectRoot)
59
+ ].join("\n");
@@ -0,0 +1,2 @@
1
+ export declare const formatSourcePath: (sourcePath: string, projectRoot: string | undefined) => string;
2
+ export declare const formatSourceMeta: (label: string, sourcePath: string, projectRoot: string | undefined) => string;
@@ -0,0 +1,19 @@
1
+ import path from "node:path";
2
+ export const formatSourcePath = (sourcePath, projectRoot) => {
3
+ if (!projectRoot || !path.isAbsolute(sourcePath) || !path.isAbsolute(projectRoot)) {
4
+ return sourcePath;
5
+ }
6
+ const relativePath = path.relative(projectRoot, sourcePath);
7
+ if (relativePath === ""
8
+ || relativePath.startsWith("..")
9
+ || path.isAbsolute(relativePath)) {
10
+ return sourcePath;
11
+ }
12
+ return relativePath.split(path.sep).join("/");
13
+ };
14
+ export const formatSourceMeta = (label, sourcePath, projectRoot) => {
15
+ const formattedPath = formatSourcePath(sourcePath, projectRoot);
16
+ return projectRoot
17
+ ? ` ${label}=${formattedPath}`
18
+ : ` <${formattedPath}>`;
19
+ };
@@ -0,0 +1,80 @@
1
+ import type { ResolvedMoltnetRoomPolicy } from "../types.js";
2
+ export interface OrganizationViewTreeNode {
3
+ children: OrganizationViewTreeEdge[];
4
+ displayName: string;
5
+ external?: string[];
6
+ id: string;
7
+ kind: "agent" | "team";
8
+ lead?: string | null;
9
+ mode?: "hierarchical" | "swarm";
10
+ name: string;
11
+ networks?: OrganizationTreeNetworkSummary[];
12
+ runtimeName: string | null;
13
+ source: string;
14
+ }
15
+ export interface OrganizationTreeNetworkRoomSummary {
16
+ declaredMembers: string[];
17
+ id: string;
18
+ }
19
+ export interface OrganizationTreeNetworkSummary {
20
+ expose: boolean;
21
+ id: string;
22
+ name: string;
23
+ provider: "moltnet";
24
+ rooms: OrganizationTreeNetworkRoomSummary[];
25
+ }
26
+ export interface OrganizationViewTreeEdge {
27
+ label: string;
28
+ node: OrganizationViewTreeNode;
29
+ relation: "subagent" | "team_member";
30
+ }
31
+ export interface OrganizationNetworkMemberView {
32
+ agentName: string;
33
+ agentSource: string;
34
+ concreteMemberId: string;
35
+ declaredSlot: string;
36
+ directTeamName: string;
37
+ directTeamSource: string;
38
+ policy?: ResolvedMoltnetRoomPolicy;
39
+ representedSlot?: string;
40
+ representedTeamName?: string;
41
+ representedTeamSource?: string;
42
+ representativePath?: string[];
43
+ }
44
+ export interface OrganizationNetworkDeclarationView {
45
+ declaringTeamName: string;
46
+ declaringTeamSource: string;
47
+ expose: boolean;
48
+ name: string;
49
+ rooms: OrganizationNetworkRoomView[];
50
+ }
51
+ export interface OrganizationNetworkRoomView {
52
+ declaredMembers: string[];
53
+ id: string;
54
+ members: OrganizationNetworkMemberView[];
55
+ }
56
+ export interface OrganizationNetworkView {
57
+ declaringTeamName: string;
58
+ declaringTeamSource: string;
59
+ expose: boolean;
60
+ id: string;
61
+ name: string;
62
+ provider: "moltnet";
63
+ rooms: OrganizationNetworkRoomView[];
64
+ declarations?: OrganizationNetworkDeclarationView[];
65
+ }
66
+ export interface OrganizationView {
67
+ contexts: [];
68
+ diagnostics: [];
69
+ inputPath: string;
70
+ networks: OrganizationNetworkView[];
71
+ projectRoot?: string;
72
+ root: OrganizationViewTreeNode;
73
+ runtimes: [];
74
+ }
75
+ export interface RenderOrganizationViewOptions {
76
+ ascii?: boolean;
77
+ color?: boolean;
78
+ declared?: boolean;
79
+ paths?: boolean;
80
+ }
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spawnfile",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Canonical source compiler for autonomous agents and teams.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -16,7 +16,7 @@
16
16
  },
17
17
  "scripts": {
18
18
  "blueprints": "./scripts/blueprints.sh",
19
- "build": "rm -rf dist && tsc --project tsconfig.build.json && node ./scripts/copy-runtime-scaffold-assets.mjs",
19
+ "build": "rm -rf dist && tsc --project tsconfig.build.json && chmod +x dist/cli/index.js && node ./scripts/copy-runtime-scaffold-assets.mjs",
20
20
  "clean": "rm -rf coverage dist",
21
21
  "coverage": "vitest run --coverage",
22
22
  "dev": "tsx src/cli/index.ts",