zob-harness 0.3.0 → 0.3.1

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 (50) hide show
  1. package/.pi/extensions/zob-harness/src/domains/coms/coms-v2/registry.ts +44 -1
  2. package/.pi/extensions/zob-harness/src/domains/coms/coms-v2/zpeer.ts +32 -4
  3. package/.pi/extensions/zob-harness/src/domains/coms/mission-control.ts +4 -1
  4. package/.pi/extensions/zob-harness/src/runtime/commands.ts +2 -2
  5. package/.pi/extensions/zob-harness/src/runtime/events.ts +6 -5
  6. package/.pi/extensions/zob-harness/src/runtime/widget.ts +2 -2
  7. package/.pi/skills/zob-agentic-spec-team/SKILL.md +4 -1
  8. package/.pi/skills/zob-coms-safety/SKILL.md +13 -0
  9. package/.pi/skills/zob-coms-v2-live/SKILL.md +11 -0
  10. package/.pi/skills/zob-factory/SKILL.md +21 -0
  11. package/.pi/skills/zob-harness/SKILL.md +14 -0
  12. package/.pi/skills/zob-zagent-creator/SKILL.md +10 -0
  13. package/.pi/zagents/agent-factory-pacman-chief.json +22 -0
  14. package/.pi/zagents/agent-factory-pacman-engine-builder.json +21 -0
  15. package/.pi/zagents/agent-factory-pacman-frontend-builder.json +21 -0
  16. package/.pi/zagents/agent-factory-pacman-game-architect.json +21 -0
  17. package/.pi/zagents/agent-factory-pacman-game-designer.json +21 -0
  18. package/.pi/zagents/agent-factory-pacman-qa-oracle.json +21 -0
  19. package/.pi/zagents/prompts/agent-factory-pacman-chief.md +53 -0
  20. package/.pi/zagents/prompts/agent-factory-pacman-engine-builder.md +41 -0
  21. package/.pi/zagents/prompts/agent-factory-pacman-frontend-builder.md +40 -0
  22. package/.pi/zagents/prompts/agent-factory-pacman-game-architect.md +41 -0
  23. package/.pi/zagents/prompts/agent-factory-pacman-game-designer.md +43 -0
  24. package/.pi/zagents/prompts/agent-factory-pacman-qa-oracle.md +51 -0
  25. package/.pi/zteams/agent-factory-pacman-multiplayer-runtime.mjs +384 -0
  26. package/.pi/zteams/agent-factory-pacman-multiplayer.json +42 -0
  27. package/.pi/zteams/agent-factory-pacman-multiplayer.tmux.sh +256 -0
  28. package/.pi/zteams/templates/agent-factory-pacman-chief-kickoff.template.md +71 -0
  29. package/.pi/zteams/templates/agent-factory-pacman-worker-kickoff.template.md +59 -0
  30. package/README.md +183 -110
  31. package/SOURCE_INDEX.md +4 -0
  32. package/examples/agent-factory-mission-control/AGENTS.md +10 -0
  33. package/examples/agent-factory-mission-control/README.md +17 -0
  34. package/examples/agent-factory-mission-control/apps/api/AGENTS.md +3 -0
  35. package/examples/agent-factory-mission-control/apps/dashboard/AGENTS.md +3 -0
  36. package/examples/agent-factory-mission-control/mission.md +3 -0
  37. package/examples/agent-factory-mission-control/output-contract.md +3 -0
  38. package/examples/agent-factory-mission-control/packages/domain/AGENTS.md +3 -0
  39. package/examples/agent-factory-mission-control/packages/snapshot-reader/AGENTS.md +3 -0
  40. package/examples/agent-factory-pacman-multiplayer/AGENTS.md +27 -0
  41. package/examples/agent-factory-pacman-multiplayer/README.md +84 -0
  42. package/examples/agent-factory-pacman-multiplayer/mission.md +43 -0
  43. package/examples/agent-factory-pacman-multiplayer/output-contract.md +58 -0
  44. package/examples/agent-factory-tmux-comms/README.md +146 -0
  45. package/examples/agent-factory-tmux-comms/chief-kickoff.template.md +54 -0
  46. package/examples/agent-factory-tmux-comms/simple-agent-factory.team.json +92 -0
  47. package/examples/agent-factory-tmux-comms/simple-agent-factory.tmux.sh +248 -0
  48. package/examples/agent-factory-tmux-comms/worker-kickoff.template.md +43 -0
  49. package/package.json +9 -3
  50. package/scripts/zpeer-local-e2e-smoke.mjs +6 -0
@@ -1,4 +1,4 @@
1
- import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
1
+ import { existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { dirname, join } from "node:path";
4
4
 
@@ -10,6 +10,8 @@ import { readZobComsV2Policy, zobComsRegistryEnabled } from "./policy.js";
10
10
  import type { ZobLivePeerCard, ZobLivePeerStatus, ZobLiveRegistrySnapshot } from "./types.js";
11
11
 
12
12
  const FORBIDDEN_PERSISTED_KEYS = new Set(["body", "task", "prompt", "output", "content", "message", "rationale", "text", "diff", "patch"]);
13
+ const DEFAULT_OFFLINE_PEER_RETENTION_MS = 24 * 60 * 60 * 1000;
14
+ const MIN_OFFLINE_PEER_RETENTION_MS = 5 * 60 * 1000;
13
15
 
14
16
  function registryRoot(): { path: string; kind: "user_runtime" | "env_override" } {
15
17
  const override = process.env.ZOB_COMS_REGISTRY_ROOT;
@@ -54,6 +56,19 @@ function readPeerCardsFromAgentsDir(dir: string, nowMs: number, teamName?: strin
54
56
  .map((peer) => ({ ...peer, status: derivePeerStatus(peer, nowMs) }));
55
57
  }
56
58
 
59
+ function boundedOfflinePeerRetentionMs(value: number | undefined): number {
60
+ const env = Number.parseInt(process.env.ZOB_COMS_OFFLINE_PEER_RETENTION_MS ?? "", 10);
61
+ const raw = typeof value === "number" && Number.isFinite(value) ? value : Number.isFinite(env) ? env : DEFAULT_OFFLINE_PEER_RETENTION_MS;
62
+ return Math.max(MIN_OFFLINE_PEER_RETENTION_MS, Math.floor(raw));
63
+ }
64
+
65
+ function offlinePeerExpired(peer: ZobLivePeerCard, nowMs: number, retentionMs: number): boolean {
66
+ if (derivePeerStatus(peer, nowMs) !== "offline") return false;
67
+ const heartbeatMs = Date.parse(peer.heartbeatAt);
68
+ if (!Number.isFinite(heartbeatMs)) return true;
69
+ return nowMs - heartbeatMs >= Math.max(peer.offlineAfterMs, retentionMs);
70
+ }
71
+
57
72
  function allProjectAgentsDirs(): string[] {
58
73
  const root = registryRoot();
59
74
  const projectsDir = join(root.path, "projects");
@@ -94,6 +109,34 @@ function derivePeerStatus(peer: ZobLivePeerCard, nowMs: number): ZobLivePeerStat
94
109
  return "online";
95
110
  }
96
111
 
112
+ export function pruneExpiredZobLivePeers(repoRoot: string, input: { teamName?: string; nowMs?: number; retentionMs?: number } = {}): { schema: "zob.live-registry-prune.v1"; pruned: number; retained: number; retentionMs: number; bodyStored: false } {
113
+ const { dir } = projectAgentsDir(repoRoot);
114
+ const nowMs = input.nowMs ?? Date.now();
115
+ const retentionMs = boundedOfflinePeerRetentionMs(input.retentionMs);
116
+ let pruned = 0;
117
+ let retained = 0;
118
+ if (!existsSync(dir)) return { schema: "zob.live-registry-prune.v1", pruned, retained, retentionMs, bodyStored: false };
119
+ for (const entry of readdirSync(dir).filter((name) => name.endsWith(".json"))) {
120
+ const filePath = join(dir, entry);
121
+ try {
122
+ const peer = parsePeerCard(JSON.parse(readFileSync(filePath, "utf8")) as unknown);
123
+ if (!peer || (input.teamName && peer.team !== input.teamName)) {
124
+ retained += 1;
125
+ continue;
126
+ }
127
+ if (offlinePeerExpired(peer, nowMs, retentionMs)) {
128
+ unlinkSync(filePath);
129
+ pruned += 1;
130
+ } else {
131
+ retained += 1;
132
+ }
133
+ } catch {
134
+ retained += 1;
135
+ }
136
+ }
137
+ return { schema: "zob.live-registry-prune.v1", pruned, retained, retentionMs, bodyStored: false };
138
+ }
139
+
97
140
  export function writeZobLivePeerCard(repoRoot: string, peer: ZobLivePeerCard): ZobLivePeerCard {
98
141
  if (hasForbiddenPersistedKey(peer)) throw new Error("Refusing to persist ZOB live peer card with forbidden body-like keys");
99
142
  if (peer.bodyStored !== false) throw new Error("ZOB live peer card bodyStored must be false");
@@ -26,6 +26,9 @@ export interface ZpeerRoomSummary {
26
26
  stale: number;
27
27
  offline: number;
28
28
  aliases: string[];
29
+ onlineAliases: string[];
30
+ staleAliases: string[];
31
+ offlineAliases: string[];
29
32
  duplicateAliases: string[];
30
33
  membershipCount?: number;
31
34
  localOnly: true;
@@ -248,9 +251,15 @@ function peersInRoom(repoRoot: string, roomId: string): ZpeerRoomPeer[] {
248
251
 
249
252
  function buildZpeerRoomSummaryFromPeers(projectId: string, self: ZobLivePeerCard | undefined, roomId: string, peers: ZpeerRoomPeer[]): ZpeerRoomSummary {
250
253
  const counts: Record<ZobLivePeerStatus, number> = { online: 0, stale: 0, offline: 0 };
254
+ const statusAliases: Record<ZobLivePeerStatus, string[]> = { online: [], stale: [], offline: [] };
251
255
  const aliases = peers.map((entry) => entry.membership.alias).sort();
252
- for (const entry of peers) counts[zpeerReachableStatus(entry.peer)] += 1;
253
- const duplicateAliases = aliases.filter((alias, index) => aliases.indexOf(alias) !== index).filter((alias, index, all) => all.indexOf(alias) === index);
256
+ for (const entry of peers) {
257
+ const status = zpeerReachableStatus(entry.peer);
258
+ counts[status] += 1;
259
+ statusAliases[status].push(entry.membership.alias);
260
+ }
261
+ const onlineAliases = statusAliases.online.sort();
262
+ const duplicateAliases = onlineAliases.filter((alias, index) => onlineAliases.indexOf(alias) !== index).filter((alias, index, all) => all.indexOf(alias) === index);
254
263
  return {
255
264
  schema: "zob.zpeer-room-summary.v1",
256
265
  projectId,
@@ -261,6 +270,9 @@ function buildZpeerRoomSummaryFromPeers(projectId: string, self: ZobLivePeerCard
261
270
  stale: counts.stale,
262
271
  offline: counts.offline,
263
272
  aliases,
273
+ onlineAliases,
274
+ staleAliases: statusAliases.stale.sort(),
275
+ offlineAliases: statusAliases.offline.sort(),
264
276
  duplicateAliases,
265
277
  membershipCount: self ? zpeerMembershipsForPeer(self).length : undefined,
266
278
  localOnly: true,
@@ -471,8 +483,24 @@ export async function sendZpeerPrompt(repoRoot: string, self: ZobLivePeerCard, t
471
483
  const candidates = peersInRoom(repoRoot, roomId).filter((entry) => entry.membership.alias === targetAlias && entry.peer.sessionHash !== self.sessionHash);
472
484
  if (targetAlias === senderAlias) return finish("attempt", { status: "blocked", reason: "cannot send to self", targetAlias, taskHash, bodyStored: false });
473
485
  if (candidates.length === 0) return finish("attempt", { status: "blocked", reason: `peer @${targetAlias} not found in room '${roomId}'`, targetAlias, taskHash, bodyStored: false }, 0);
474
- if (candidates.length > 1) return finish("attempt", { status: "blocked", reason: `duplicate alias @${targetAlias} in room '${roomId}'`, targetAlias, taskHash, bodyStored: false }, candidates.length);
475
- const target = candidates[0];
486
+ let liveCandidates = candidates.filter((entry) => zpeerReachableStatus(entry.peer) === "online");
487
+ if (liveCandidates.length > 1) {
488
+ const responsiveCandidates: ZpeerRoomPeer[] = [];
489
+ for (const entry of liveCandidates) {
490
+ if (await peerRespondsToAliasPing(entry.peer)) {
491
+ responsiveCandidates.push(entry);
492
+ } else {
493
+ try { writeZobLivePeerCard(repoRoot, { ...entry.peer, heartbeatAt: new Date().toISOString(), status: "offline" }); } catch { /* best-effort ghost alias release */ }
494
+ }
495
+ }
496
+ liveCandidates = responsiveCandidates;
497
+ }
498
+ if (liveCandidates.length === 0) {
499
+ const statuses = [...new Set(candidates.map((entry) => zpeerReachableStatus(entry.peer)))].sort().join("/") || "offline";
500
+ return finish("attempt", { status: "blocked", reason: `peer @${targetAlias} is ${statuses}`, targetAlias, taskHash, bodyStored: false }, candidates.length);
501
+ }
502
+ if (liveCandidates.length > 1) return finish("attempt", { status: "blocked", reason: `duplicate live alias @${targetAlias} in room '${roomId}'`, targetAlias, taskHash, bodyStored: false }, liveCandidates.length);
503
+ const target = liveCandidates[0];
476
504
  const targetReachableStatus = zpeerReachableStatus(target.peer);
477
505
  if (targetReachableStatus !== "online") return finish("attempt", { status: "blocked", reason: `peer @${targetAlias} is ${targetReachableStatus}`, targetAlias, taskHash, bodyStored: false }, 1);
478
506
  const topologyBlocker = validateZpeerTopology(repoRoot, self, target.peer, roomId, senderAlias, target.membership.alias);
@@ -63,6 +63,7 @@ function summarizeZpeerRooms(peers: Array<Record<string, unknown>>): Array<Recor
63
63
  }
64
64
  return [...rooms.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([roomId, roomPeers]) => {
65
65
  const aliases = roomPeers.map((entry) => entry.alias).filter((alias): alias is string => Boolean(alias)).sort();
66
+ const onlineAliases = roomPeers.filter((entry) => entry.peer.status === "online").map((entry) => entry.alias).filter((alias): alias is string => Boolean(alias)).sort();
66
67
  const sessionHashes = roomPeers.map((entry) => typeof entry.peer.sessionHash === "string" ? entry.peer.sessionHash : undefined).filter((sessionHash): sessionHash is string => Boolean(sessionHash));
67
68
  return {
68
69
  schema: "zob.zpeer-room-summary.v1",
@@ -73,7 +74,9 @@ function summarizeZpeerRooms(peers: Array<Record<string, unknown>>): Array<Recor
73
74
  stale: roomPeers.filter((entry) => entry.peer.status === "stale").length,
74
75
  offline: roomPeers.filter((entry) => entry.peer.status === "offline").length,
75
76
  aliasHashes: aliases.map((alias) => sha256(alias)),
76
- duplicateAliasHashes: aliases.filter((alias, index) => aliases.indexOf(alias) !== index).filter((alias, index, all) => all.indexOf(alias) === index).map((alias) => sha256(alias)),
77
+ onlineAliasHashes: onlineAliases.map((alias) => sha256(alias)),
78
+ duplicateAliasHashes: onlineAliases.filter((alias, index) => onlineAliases.indexOf(alias) !== index).filter((alias, index, all) => all.indexOf(alias) === index).map((alias) => sha256(alias)),
79
+ duplicateAliasScope: "online_only",
77
80
  localOnly: true,
78
81
  networkEnabled: false,
79
82
  bodyStored: false,
@@ -1032,9 +1032,9 @@ export function registerHarnessCommands(pi: ExtensionAPI, state: HarnessRuntimeS
1032
1032
  });
1033
1033
  emitZpeerEvent({ kind: "status", roomId: summary.roomId, fromAlias: summary.selfAlias, status: `online=${summary.online}/${summary.peerCount}`, reason: `stale=${summary.stale} offline=${summary.offline}` });
1034
1034
  renderHarnessWidget(pi, state, ctx);
1035
- const availableAliases = summary.aliases.filter((alias) => alias !== summary.selfAlias).map((alias) => `@${alias}`).join(", ") || "none";
1035
+ const availableAliases = summary.onlineAliases.filter((alias) => alias !== summary.selfAlias).map((alias) => `@${alias}`).join(", ") || "none";
1036
1036
  const unavailable = summary.stale + summary.offline;
1037
- ctx.ui.notify(`zpeer room=${summary.roomId} memberships=${summary.membershipCount ?? zpeerMembershipsForPeer(self).length} self=@${summary.selfAlias ?? "?"} onlinePeers=${Math.max(0, summary.online - 1)} unavailable=${unavailable} peers=${availableAliases} · usage: /zpeer @alias <prompt> | /zpeer in <room> @alias <prompt> · safety: local-only/hash-only/bodyStored=false`, "info");
1037
+ ctx.ui.notify(`zpeer room=${summary.roomId} memberships=${summary.membershipCount ?? zpeerMembershipsForPeer(self).length} self=@${summary.selfAlias ?? "?"} onlinePeers=${Math.max(0, summary.online - 1)} unavailable=${unavailable} livePeers=${availableAliases} · usage: /zpeer @alias <prompt> | /zpeer in <room> @alias <prompt> · safety: local-only/hash-only/bodyStored=false`, "info");
1038
1038
  return;
1039
1039
  }
1040
1040
  const parts = trimmed.split(/\s+/);
@@ -8,7 +8,7 @@ import { buildZobLiveAckEnvelope, buildZobLiveErrorEnvelope, buildZobLivePongEnv
8
8
  import { appendLiveCompletedRef } from "../domains/coms/coms-v2/ledger-bridge.js";
9
9
  import { bindZobLocalEndpoint, makeZobLocalEndpoint, sendZobLocalEnvelope } from "../domains/coms/coms-v2/local-transport.js";
10
10
  import { readZobComsV2Policy } from "../domains/coms/coms-v2/policy.js";
11
- import { registerCurrentZobLivePeer, touchCurrentZobLivePeer, unregisterCurrentZobLivePeer, writeZobLivePeerCard } from "../domains/coms/coms-v2/registry.js";
11
+ import { pruneExpiredZobLivePeers, registerCurrentZobLivePeer, touchCurrentZobLivePeer, unregisterCurrentZobLivePeer, writeZobLivePeerCard } from "../domains/coms/coms-v2/registry.js";
12
12
  import { clearZpeerNewCarryoverProfile, readZpeerLocalProfile, readZpeerNewCarryoverProfile, writeZpeerLocalProfileFromPeer, writeZpeerNewCarryoverProfile, zpeerProfileIdIsSharedFallback } from "../domains/coms/coms-v2/zpeer-profile.js";
13
13
  import { buildZpeerPeerRoomSummaries, ensureZpeerFields, refreshZpeerSelf } from "../domains/coms/coms-v2/zpeer.js";
14
14
  import type { ZpeerRoomMembership } from "../domains/coms/coms-v2/types.js";
@@ -317,16 +317,16 @@ function buildZpeerAwarenessPrompt(state: HarnessRuntimeState, repoRoot: string)
317
317
  const memberships = (state.zobLive.peerCard.zpeerMemberships?.length ?? summaries.length) || 1;
318
318
  const roomLines = summaries.slice(0, 6).map((summary) => {
319
319
  const selfAlias = summary.selfAlias ?? "?";
320
- const peerAliases = summary.aliases.filter((alias) => alias !== selfAlias).slice(0, 6).map((alias) => `@${alias}`);
320
+ const peerAliases = summary.onlineAliases.filter((alias) => alias !== selfAlias).slice(0, 6).map((alias) => `@${alias}`);
321
321
  const unavailable = summary.stale + summary.offline;
322
- const duplicateText = summary.duplicateAliases.length > 0 ? ` duplicates=${summary.duplicateAliases.map((alias) => `@${alias}`).join(",")}` : "";
322
+ const duplicateText = summary.duplicateAliases.length > 0 ? ` liveDuplicates=${summary.duplicateAliases.map((alias) => `@${alias}`).join(",")}` : "";
323
323
  return ` - ${summary.active ? "*" : " "} ${summary.roomId}: self=@${selfAlias}; online=${peerAliases.join(",") || "none"}; unavailable=${unavailable} (stale=${summary.stale}, offline=${summary.offline})${duplicateText}`;
324
324
  });
325
325
  if (summaries.length > 6) roomLines.push(` - +${summaries.length - 6} more room${summaries.length - 6 === 1 ? "" : "s"}`);
326
326
  const activeSelfAlias = activeSummary?.selfAlias ?? "?";
327
- const activePeerAliases = (activeSummary?.aliases ?? []).filter((alias) => alias !== activeSelfAlias).slice(0, 8).map((alias) => `@${alias}`);
327
+ const activePeerAliases = (activeSummary?.onlineAliases ?? []).filter((alias) => alias !== activeSelfAlias).slice(0, 8).map((alias) => `@${alias}`);
328
328
  const activeUnavailable = (activeSummary?.stale ?? 0) + (activeSummary?.offline ?? 0);
329
- const activeDuplicateLine = activeSummary && activeSummary.duplicateAliases.length > 0 ? `\n- duplicate aliases: ${activeSummary.duplicateAliases.map((alias) => `@${alias}`).join(", ")}` : "";
329
+ const activeDuplicateLine = activeSummary && activeSummary.duplicateAliases.length > 0 ? `\n- duplicate live aliases: ${activeSummary.duplicateAliases.map((alias) => `@${alias}`).join(", ")}` : "";
330
330
  return `\n\nZPEER AWARENESS (transient, rebuilt each turn)\n- active room: ${activeSummary?.roomId ?? "default"}\n- memberships: ${memberships}\n- self: @${activeSelfAlias}\n- online peers: ${activePeerAliases.join(", ") || "none"}\n- unavailable peers: ${activeUnavailable} (stale=${activeSummary?.stale ?? 0}, offline=${activeSummary?.offline ?? 0})${activeDuplicateLine}\n- rooms:\n${roomLines.join("\n") || " - none"}\n- Use zpeer_ask with explicit roomId when targeting a non-active room.\n- posture: local_socket-only, room-scoped, hash-only durable ledgers, bodyStored=false, networkEnabled=false\n- For non-trivial review/debug/planning peer coordination, agents may use zpeer_ask with mode=\"async\" so the request is visible, governed, and non-blocking; /zpeer remains the interactive command path.\n- Passive wait rule: if the only remaining action is waiting for ZPeer/coms replies, stop the turn and remain idle; do not poll, call tools, or continue just to wait.\n- Use ZPeer only when useful or user-requested; avoid spam, duplicate asks, and reply loops; do not use it for hidden free chat or to bypass topology/safety gates.\n- Raw ZPeer bodies are transient; durable records must remain hash-only/bodyStored=false.\n- last ZPeer event: ${formatZpeerLastEvent(state.zobLive.lastEvent)}`;
331
331
  }
332
332
 
@@ -389,6 +389,7 @@ function handleSameAgentModeIntent(pi: ExtensionAPI, state: HarnessRuntimeState,
389
389
  async function startOrRefreshZobLiveRuntime(pi: ExtensionAPI, state: HarnessRuntimeState, ctx: ExtensionContext): Promise<void> {
390
390
  const repoRoot = ctx.cwd;
391
391
  const profileId = zpeerRuntimeProfileId(ctx);
392
+ try { pruneExpiredZobLivePeers(repoRoot); } catch { /* best-effort ghost peer cleanup; live runtime must remain available */ }
392
393
  const policy = readZobComsV2Policy(repoRoot);
393
394
  if (policy.mode === "off" || policy.mode === "required_network") {
394
395
  safelyUpdateZobLivePeer(repoRoot, state.zobLive.peerCard ? "touch" : "register");
@@ -353,8 +353,8 @@ export function renderHarnessWidget(pi: ExtensionAPI, state: HarnessRuntimeState
353
353
  const marker = summary.active ? "*" : " ";
354
354
  const selfAlias = `@${summary.selfAlias ?? "?"}`;
355
355
  const peerState = `${summary.online}/${summary.peerCount}${summary.stale > 0 ? ` s${summary.stale}` : ""}${summary.offline > 0 ? ` off${summary.offline}` : ""}`;
356
- const peerAliases = summary.aliases.filter((alias) => alias !== summary.selfAlias).slice(0, 2).map((alias) => `@${alias}`);
357
- const aliasOverflow = Math.max(0, summary.aliases.length - (summary.selfAlias && summary.aliases.includes(summary.selfAlias) ? 1 : 0) - peerAliases.length);
356
+ const peerAliases = summary.onlineAliases.filter((alias) => alias !== summary.selfAlias).slice(0, 2).map((alias) => `@${alias}`);
357
+ const aliasOverflow = Math.max(0, summary.onlineAliases.length - (summary.selfAlias && summary.onlineAliases.includes(summary.selfAlias) ? 1 : 0) - peerAliases.length);
358
358
  const aliasText = peerAliases.length > 0 ? `${peerAliases.join(" ")}${aliasOverflow > 0 ? ` +${aliasOverflow}` : ""}` : "no peers";
359
359
  return theme.fg("muted", truncateToWidth(`${marker} ${summary.roomId} ${selfAlias} ${peerState} ${aliasText}`, 52, "…"));
360
360
  });
@@ -8,15 +8,18 @@ description: Use when launching, running, reviewing, or extending the Agentic Sp
8
8
 
9
9
  Use this skill to run a reusable ZOB-native spec production workflow. It ingests a mission plus explicit source paths, coordinates a run-scoped ZTeam, asks the human only through `spec-chief`, and produces a detailed, testable, traceable implementation spec.
10
10
 
11
- V1 packaging is composite and deliberately **not** a runtime extension:
11
+ V1 packaging is composite and deliberately **not** a runtime extension. It is a specialized Agent Factory pattern:
12
12
 
13
13
  ```text
14
14
  Skill = rules and operating contract
15
15
  Factory = repeatable checkpoints, sentinels, validators
16
16
  ZTeam/ZAgents = run-scoped human-facing team
17
17
  CLI = scriptable entrypoint and tmux automation
18
+ Coms = parent-visible async asks, blockers, evidence refs, and oracle/no-ship routing
18
19
  ```
19
20
 
21
+ Use the generic `examples/agent-factory-tmux-comms/` pattern for a smaller teaching version of the same shape.
22
+
20
23
  ## Entry points
21
24
 
22
25
  ```bash
@@ -30,6 +30,17 @@ ZOB coms live transport may be transient, but ZOB audit must stay metadata-only.
30
30
  - No silent fallback from required live delivery to append-only success.
31
31
  - No token/secret logging.
32
32
 
33
+ ## Tmux Agent Factory safety
34
+
35
+ For tmux-backed ZAgent teams:
36
+
37
+ - Tmux is a local launch/observation surface, not the durable source of truth.
38
+ - Startup kickoff files are allowed when they stay bounded and avoid secrets/raw private dumps; post-start pane paste is not reliable proof of delivery.
39
+ - Pane capture may be useful for live diagnosis, but do not persist captured prompt/output bodies into ledgers, reports, Mission Control, or skills.
40
+ - Supervisors/watchdogs must be bounded and local-only; they may detect stale windows or nudge a specific owed-response agent only with hash/body-free durable records.
41
+ - Team communication must remain parent-visible. Prefer one `control` room with lanes/tags/artifact sections unless a bounded owner-approved route exists.
42
+ - Completion still requires artifacts, validation evidence, and oracle/no-ship review when applicable; an active tmux session or chat reply is not completion evidence.
43
+
33
44
  ## Goal Room and owner-pool safety
34
45
 
35
46
  For parallel owner micro-worker pools:
@@ -48,6 +59,8 @@ No-ship if:
48
59
  - live send succeeds while receiver is absent/stale/offline;
49
60
  - await treats timeout/stale/offline as success;
50
61
  - hidden worker-to-worker free chat works outside a typed parent-visible Goal Room;
62
+ - tmux pane paste/capture is treated as reliable durable communication or completion evidence;
63
+ - a supervisor/watchdog nudges indefinitely, starts broad work, or stores raw pane bodies;
51
64
  - a non-owner writes owned paths instead of sending an owner request;
52
65
  - a worker applies or merges directly into the main workspace;
53
66
  - network starts without explicit auth/locality policy;
@@ -35,6 +35,17 @@ Use this skill when:
35
35
  - Do not bypass topology guards.
36
36
  - Do not enable network transport without explicit auth/locality policy.
37
37
 
38
+ ## Tmux/ZAgent communication pattern
39
+
40
+ For tmux-backed Agent Factory teams:
41
+
42
+ - Treat each tmux window as a local Pi/ZAgent session with its own identity and room membership.
43
+ - Use tmux only to start, attach, inspect status, or close the local session; do not use pane paste as the primary communication transport.
44
+ - Prefer startup files (`pi @chief-kickoff.md`, `pi @worker-kickoff.md`) for initial instructions so Pi receives one bounded message block.
45
+ - Use async ZPeer/Goal Room-style asks for normal coordination; do not block in polling loops after sending a non-blocking ask.
46
+ - Keep messages short and actionable with `CONTEXT`, `ASK`, `EVIDENCE`, `URGENCY`, and `BLOCKER` fields.
47
+ - When a reply affects scope, ownership, merge readiness, oracle status, or completion, mirror only body-free metadata/artifact refs into durable records.
48
+
38
49
  ## Expected pattern
39
50
 
40
51
  ```text
@@ -17,6 +17,19 @@ Use when the task is repeated, batchable, or should become a reusable system rat
17
17
  5. Pilot: 10 items max with bounded concurrency.
18
18
  6. Batch: only after pilot sentinel and oracle gate.
19
19
 
20
+ ## Agent Factory shape
21
+
22
+ Some factories produce not only code or reports, but a supervised team workflow. Treat these as **Agent Factories**. A safe Agent Factory defines:
23
+
24
+ - ZAgent roles, prompts, default modes, and allowed authority;
25
+ - ZTeam topology, aliases, rooms, entry agent, and `parentVisible` communication policy;
26
+ - optional manual tmux launcher with start/attach/status/close only;
27
+ - startup kickoff templates or rendered kickoff files passed as `pi @kickoff.md`;
28
+ - run artifacts such as `run-manifest.json`, workgraph/status/iteration logs, readiness or kickoff-dispatch records;
29
+ - validation and oracle/no-ship gates before completion claims.
30
+
31
+ Do not treat launching a tmux team as factory success. Success comes from evidence artifacts, validators, and oracle review.
32
+
20
33
  ## Required artifacts
21
34
 
22
35
  - `manifest.json`
@@ -26,3 +39,11 @@ Use when the task is repeated, batchable, or should become a reusable system rat
26
39
  - `validation.json`
27
40
  - phase sentinel (`SMOKE_PASSED.sentinel`, `PILOT_PASSED.sentinel`, or `BATCH_PASSED.sentinel`)
28
41
  - `DONE.sentinel` only after validation passes
42
+
43
+ For Agent Factories, also prefer:
44
+
45
+ - team manifest or generated run-scoped team manifest
46
+ - kickoff templates or rendered startup kickoff files
47
+ - `autonomous-workgraph.md` / `autonomous-status.md` / `iteration-log.md`
48
+ - `kickoff-dispatch.json` or equivalent body-free startup proof
49
+ - communication protocol and no-ship policy
@@ -9,12 +9,26 @@ description: Use when working inside the ZOB Pi harness, designing agentic workf
9
9
  Use this skill for any task involving:
10
10
  - Pi extensions, prompt templates, skills, or agent definitions.
11
11
  - Multi-agent delegation workflows.
12
+ - Agent Factory design: ZAgents, ZTeams, tmux launchers, kickoff files, visible coms, workgraphs, and oracle gates.
12
13
  - Safety gates and damage-control policy.
13
14
  - Software-factory design from repeated manual workflows.
14
15
  - Runtime tool/command routing via `.pi/capabilities/zob-public-runtime-capabilities.json`.
15
16
 
16
17
  For routing behavior, load `zob-tool-router` before non-trivial or tool-ambiguous work. For compaction/recovery behavior, load `zob-compaction-policy` before changing compaction hooks or resuming from a compacted long-running goal. For domain behavior, load the domain skill named by the registry instead of inlining details here: `zob-goal-todo-tree`, `zob-coms-v2-live`, `zob-coms-safety`, `zob-mission-control-coms`, `zob-autonomous-runtime`, `zob-factory`, `zob-sandbox`, `zob-oracle`, or `zob-spec` as applicable.
17
18
 
19
+ ## Agent Factory posture
20
+
21
+ Treat ZOB as a governed Agent Factory when the owner wants persistent local agent roles rather than one transient assistant. An Agent Factory may include:
22
+
23
+ - ZAgent role definitions and prompts;
24
+ - ZTeam topology, rooms, aliases, entry agent, and communication policy;
25
+ - optional tmux launchers for manual local startup/attach/status/close;
26
+ - startup kickoff files passed as `pi @kickoff.md`, not post-start pane paste as the primary transport;
27
+ - run artifacts such as `run-manifest.json`, workgraph/status/iteration logs, validation reports, and `kickoff-dispatch.json`;
28
+ - oracle/no-ship gates before completion claims.
29
+
30
+ Communication is a core deliverable. Prefer one parent-visible control room by default; use lanes/tags/artifact sections instead of hidden worker rooms unless the owner explicitly approves a bounded route. The generic teaching example lives in `examples/agent-factory-tmux-comms/`.
31
+
18
32
  ## Operating model
19
33
 
20
34
  1. Classify the task as one of: `explore`, `plan`, `implement`, `oracle`, `factory`, `orchestrator`.
@@ -40,10 +40,18 @@ Generated definitions must stay project-local and are not harness-global:
40
40
 
41
41
  Never write generated ZAgent, ZTeam, prompt, or tmux launcher artifacts outside those directories unless the owner explicitly provides a different project-local allowed path.
42
42
 
43
+ ## Documentation examples vs active project teams
44
+
45
+ When creating documentation-only Agent Factory examples, write them under an explicit example path such as `examples/agent-factory-tmux-comms/` and mark them as inert/example-only. Do not place example manifests under `.pi/zagents/` or `.pi/zteams/` unless the owner is asking to create an active project-local team.
46
+
47
+ Example files may illustrate a team manifest, manual tmux launcher, and kickoff templates, but they must state that real activation requires owner review and adaptation into `.pi/zagents/`, `.pi/zagents/prompts/`, and `.pi/zteams/`.
48
+
43
49
  ## Optional tmux launcher mode
44
50
 
45
51
  When the owner starts or qualifies the natural-language request with `tmux`, generate a project-local tmux launcher script alongside the generated ZTeam manifests. This is a convenience artifact only: the assistant must write the script and report manual commands, but must not run tmux, start Pi sessions, attach to tmux, or close tmux sessions automatically.
46
52
 
53
+ For Agent Factory launchers, prefer startup kickoff files (`pi @chief-kickoff.md`, `pi @worker-kickoff.md`) over post-start tmux pane paste. Tmux is a local launch/observation wrapper; communication and durable evidence still belong to ZPeer/Goal Room-style visible coordination and run artifacts.
54
+
47
55
  Accepted owner request patterns include:
48
56
 
49
57
  - `/skill:zob-zagent-creator tmux ...`
@@ -292,6 +300,7 @@ ZAgent manifest mode shape:
292
300
  - When the ask mentions model choice or cost/quality tradeoffs, read the model catalog/routing files and record a justified per-ZAgent `model` plus metadata instead of guessing.
293
301
  - Keep definitions minimal, auditable, and project-local.
294
302
  - When generating tmux launchers, treat multi-team requests as bundles, deduplicate shared agents by `zagentId`, and document included teams, unique agents, and bridge/shared agents.
303
+ - For Agent Factory teams, include or reference the communication policy: `parentVisible: true`, `hiddenPeerChat: false`, `bodyStored: false`, `networkEnabled: false`, and owner/oracle gates for completion.
295
304
  - Preserve existing runtime code and safety policy unless the owner explicitly asks for a separate implementation task.
296
305
  - Ask for clarification when authority, launch conditions, write permissions, or external access are ambiguous.
297
306
 
@@ -303,6 +312,7 @@ ZAgent manifest mode shape:
303
312
  - Do not create manifests, prompts, or tmux launchers outside `.pi/zagents/`, `.pi/zagents/prompts/`, or `.pi/zteams/`.
304
313
  - Do not grant broad filesystem, network, browser, secret, commit, push, or destructive-command authority by default.
305
314
  - Do not generate tmux launchers that duplicate shared ZAgents per team, use `killall`, broad process kills, install daemons, access credentials, or perform global cleanup.
315
+ - Do not present a tmux launcher or kickoff template as proof that agents launched, communicated, validated, or completed work.
306
316
  - Do not enable live/global model routing or store provider credentials/API keys while selecting ZAgent models.
307
317
  - Do not choose `vanilla` as a default mode unless the owner explicitly requested vanilla/base Pi/direct unrestricted behavior.
308
318
  - Do not treat ZAgent creation as delivery success for live communication or mission execution.
@@ -0,0 +1,22 @@
1
+ {
2
+ "schema": "zob.zagent.v1",
3
+ "id": "agent-factory-pacman-chief",
4
+ "team": "agent-factory-pacman-multiplayer",
5
+ "role": "orchestrator",
6
+ "alias": "pacman_chief",
7
+ "description": "Owner-facing chief for the generative Pac-Man multiplayer Agent Factory demo; coordinates workers, workgraph, communication, and final synthesis.",
8
+ "promptRef": ".pi/zagents/prompts/agent-factory-pacman-chief.md",
9
+ "defaultMode": "orchestrator",
10
+ "defaultRoom": "pacman-factory",
11
+ "activeRoom": "pacman-factory",
12
+ "rooms": [{ "id": "pacman-factory", "alias": "pacman_chief", "role": "lead", "active": true }],
13
+ "allowedTools": ["read", "bash", "write", "edit", "zpeer_ask"],
14
+ "allowedPaths": ["examples/agent-factory-pacman-multiplayer/", "reports/agent-factory-pacman-runs/", ".pi/zteams/agent-factory-pacman-multiplayer.json", ".pi/zteams/agent-factory-pacman-multiplayer.tmux.sh", ".pi/zteams/agent-factory-pacman-multiplayer-runtime.mjs", ".pi/zteams/templates/agent-factory-pacman-*.template.md"],
15
+ "forbiddenPaths": [".env", ".env.*", "node_modules/", "dist/", "build/", "coverage/", ".git/", ".pi/sessions/", ".pi/agent-sessions/", ".pi/coms/"],
16
+ "approvalGates": { "externalAccess": "human", "externalWrite": "human+oracle", "commitOrPush": "human" },
17
+ "communicationPolicy": { "zpeerContact": true, "allowedRooms": ["pacman-factory"], "parentVisible": true, "hiddenPeerChat": false, "bodyStored": false },
18
+ "localOnly": true,
19
+ "networkEnabled": false,
20
+ "bodyStored": false,
21
+ "metadata": { "modeSelection": { "reason": "Chief coordinates the autonomous team and workgraph.", "authorityNote": "defaultMode sets initial posture only and does not expand permissions." } }
22
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "schema": "zob.zagent.v1",
3
+ "id": "agent-factory-pacman-engine-builder",
4
+ "team": "agent-factory-pacman-multiplayer",
5
+ "role": "engine-builder",
6
+ "alias": "engine_builder",
7
+ "description": "Generates the deterministic TypeScript Pac-Man game engine, movement/collision/scoring logic, and tests under the run project directory.",
8
+ "promptRef": ".pi/zagents/prompts/agent-factory-pacman-engine-builder.md",
9
+ "defaultMode": "implement",
10
+ "defaultRoom": "pacman-factory",
11
+ "activeRoom": "pacman-factory",
12
+ "rooms": [{ "id": "pacman-factory", "alias": "engine_builder", "role": "engine", "active": true }],
13
+ "allowedTools": ["read", "bash", "write", "edit", "zpeer_ask"],
14
+ "allowedPaths": ["examples/agent-factory-pacman-multiplayer/", "reports/agent-factory-pacman-runs/"],
15
+ "forbiddenPaths": [".env", ".env.*", "node_modules/", "dist/", "build/", "coverage/", ".git/", ".pi/sessions/", ".pi/agent-sessions/", ".pi/coms/"],
16
+ "approvalGates": { "externalAccess": "human", "externalWrite": "human+oracle", "commitOrPush": "human" },
17
+ "communicationPolicy": { "zpeerContact": true, "allowedRooms": ["pacman-factory"], "parentVisible": true, "hiddenPeerChat": false, "bodyStored": false },
18
+ "localOnly": true,
19
+ "networkEnabled": false,
20
+ "bodyStored": false
21
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "schema": "zob.zagent.v1",
3
+ "id": "agent-factory-pacman-frontend-builder",
4
+ "team": "agent-factory-pacman-multiplayer",
5
+ "role": "frontend-builder",
6
+ "alias": "frontend_builder",
7
+ "description": "Generates the browser game UI, rendering, local multiplayer controls, HUD, and launcher under the run project directory.",
8
+ "promptRef": ".pi/zagents/prompts/agent-factory-pacman-frontend-builder.md",
9
+ "defaultMode": "implement",
10
+ "defaultRoom": "pacman-factory",
11
+ "activeRoom": "pacman-factory",
12
+ "rooms": [{ "id": "pacman-factory", "alias": "frontend_builder", "role": "frontend", "active": true }],
13
+ "allowedTools": ["read", "bash", "write", "edit", "zpeer_ask"],
14
+ "allowedPaths": ["examples/agent-factory-pacman-multiplayer/", "reports/agent-factory-pacman-runs/"],
15
+ "forbiddenPaths": [".env", ".env.*", "node_modules/", "dist/", "build/", "coverage/", ".git/", ".pi/sessions/", ".pi/agent-sessions/", ".pi/coms/"],
16
+ "approvalGates": { "externalAccess": "human", "externalWrite": "human+oracle", "commitOrPush": "human" },
17
+ "communicationPolicy": { "zpeerContact": true, "allowedRooms": ["pacman-factory"], "parentVisible": true, "hiddenPeerChat": false, "bodyStored": false },
18
+ "localOnly": true,
19
+ "networkEnabled": false,
20
+ "bodyStored": false
21
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "schema": "zob.zagent.v1",
3
+ "id": "agent-factory-pacman-game-architect",
4
+ "team": "agent-factory-pacman-multiplayer",
5
+ "role": "game-architect",
6
+ "alias": "game_architect",
7
+ "description": "Designs the generated Pac-Man game architecture, engine/UI boundaries, state contracts, validation ladder, and implementation order.",
8
+ "promptRef": ".pi/zagents/prompts/agent-factory-pacman-game-architect.md",
9
+ "defaultMode": "plan",
10
+ "defaultRoom": "pacman-factory",
11
+ "activeRoom": "pacman-factory",
12
+ "rooms": [{ "id": "pacman-factory", "alias": "game_architect", "role": "architecture", "active": true }],
13
+ "allowedTools": ["read", "bash", "write", "edit", "zpeer_ask"],
14
+ "allowedPaths": ["examples/agent-factory-pacman-multiplayer/", "reports/agent-factory-pacman-runs/"],
15
+ "forbiddenPaths": [".env", ".env.*", "node_modules/", "dist/", "build/", "coverage/", ".git/", ".pi/sessions/", ".pi/agent-sessions/", ".pi/coms/"],
16
+ "approvalGates": { "externalAccess": "human", "externalWrite": "human+oracle", "commitOrPush": "human" },
17
+ "communicationPolicy": { "zpeerContact": true, "allowedRooms": ["pacman-factory"], "parentVisible": true, "hiddenPeerChat": false, "bodyStored": false },
18
+ "localOnly": true,
19
+ "networkEnabled": false,
20
+ "bodyStored": false
21
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "schema": "zob.zagent.v1",
3
+ "id": "agent-factory-pacman-game-designer",
4
+ "team": "agent-factory-pacman-multiplayer",
5
+ "role": "game-designer",
6
+ "alias": "game_designer",
7
+ "description": "Defines Pac-Man multiplayer gameplay rules, controls, scoring, round flow, and playability acceptance criteria.",
8
+ "promptRef": ".pi/zagents/prompts/agent-factory-pacman-game-designer.md",
9
+ "defaultMode": "plan",
10
+ "defaultRoom": "pacman-factory",
11
+ "activeRoom": "pacman-factory",
12
+ "rooms": [{ "id": "pacman-factory", "alias": "game_designer", "role": "game_design", "active": true }],
13
+ "allowedTools": ["read", "bash", "write", "edit", "zpeer_ask"],
14
+ "allowedPaths": ["examples/agent-factory-pacman-multiplayer/", "reports/agent-factory-pacman-runs/"],
15
+ "forbiddenPaths": [".env", ".env.*", "node_modules/", "dist/", "build/", "coverage/", ".git/", ".pi/sessions/", ".pi/agent-sessions/", ".pi/coms/"],
16
+ "approvalGates": { "externalAccess": "human", "externalWrite": "human+oracle", "commitOrPush": "human" },
17
+ "communicationPolicy": { "zpeerContact": true, "allowedRooms": ["pacman-factory"], "parentVisible": true, "hiddenPeerChat": false, "bodyStored": false },
18
+ "localOnly": true,
19
+ "networkEnabled": false,
20
+ "bodyStored": false
21
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "schema": "zob.zagent.v1",
3
+ "id": "agent-factory-pacman-qa-oracle",
4
+ "team": "agent-factory-pacman-multiplayer",
5
+ "role": "qa-oracle",
6
+ "alias": "qa_oracle",
7
+ "description": "Skeptically validates generated Pac-Man multiplayer project playability, tests, structure, safety, and no-ship posture.",
8
+ "promptRef": ".pi/zagents/prompts/agent-factory-pacman-qa-oracle.md",
9
+ "defaultMode": "oracle",
10
+ "defaultRoom": "pacman-factory",
11
+ "activeRoom": "pacman-factory",
12
+ "rooms": [{ "id": "pacman-factory", "alias": "qa_oracle", "role": "oracle", "active": true }],
13
+ "allowedTools": ["read", "bash", "zpeer_ask"],
14
+ "allowedPaths": ["examples/agent-factory-pacman-multiplayer/", "reports/agent-factory-pacman-runs/", ".pi/zagents/agent-factory-pacman-*.json", ".pi/zteams/agent-factory-pacman-multiplayer.json", ".pi/zteams/agent-factory-pacman-multiplayer-runtime.mjs", ".pi/zteams/agent-factory-pacman-multiplayer.tmux.sh"],
15
+ "forbiddenPaths": [".env", ".env.*", "node_modules/", "dist/", "build/", "coverage/", ".git/", ".pi/sessions/", ".pi/agent-sessions/", ".pi/coms/"],
16
+ "approvalGates": { "externalAccess": "human", "externalWrite": "human+oracle", "commitOrPush": "human" },
17
+ "communicationPolicy": { "zpeerContact": true, "allowedRooms": ["pacman-factory"], "parentVisible": true, "hiddenPeerChat": false, "bodyStored": false },
18
+ "localOnly": true,
19
+ "networkEnabled": false,
20
+ "bodyStored": false
21
+ }
@@ -0,0 +1,53 @@
1
+ # Agent Factory Pac-Man Chief
2
+
3
+ You are `agent-factory-pacman-chief`, alias `pacman_chief`, the owner-facing coordinator for the Pac-Man multiplayer generative demo.
4
+
5
+ ## Mission
6
+
7
+ Coordinate a local ZTeam that **generates** a playable Pac-Man-inspired multiplayer browser game under the run target:
8
+
9
+ ```text
10
+ reports/agent-factory-pacman-runs/<run_id>/project/
11
+ ```
12
+
13
+ Do not build the game inside `examples/agent-factory-pacman-multiplayer/`; that folder is only the source brief.
14
+
15
+ ## Required communication posture
16
+
17
+ Use only the parent-visible `pacman-factory` room. Communicate proactively. Do not wait silently.
18
+
19
+ Message shape:
20
+
21
+ ```text
22
+ CONTEXT:
23
+ ASK:
24
+ EVIDENCE:
25
+ URGENCY:
26
+ BLOCKER:
27
+ ```
28
+
29
+ You must dispatch and track:
30
+
31
+ - gameplay questions to `@game_designer`;
32
+ - architecture decisions to `@game_architect`;
33
+ - engine/state/collision tasks to `@engine_builder`;
34
+ - rendering/input/HUD tasks to `@frontend_builder`;
35
+ - playability/no-ship/validation review to `@qa_oracle`.
36
+
37
+ ## First turn
38
+
39
+ 1. Read the kickoff file, mission, output contract, and run manifest.
40
+ 2. Confirm `READY` in `pacman-factory`.
41
+ 3. Update the run workgraph/status/iteration log.
42
+ 4. Ask `@game_designer` for rules, controls, scoring, and acceptance criteria.
43
+ 5. Ask `@game_architect` for engine/UI/state architecture and validation ladder.
44
+ 6. Ask builders to wait for gameplay + architecture handoff before creating broad module boundaries.
45
+ 7. Ask `@qa_oracle` for validation/no-ship criteria.
46
+
47
+ ## Must not
48
+
49
+ - Do not commit/push/tag.
50
+ - Do not read secrets.
51
+ - Do not launch hidden rooms.
52
+ - Do not write generated game code outside the run `project/` directory.
53
+ - Do not claim completion without validation evidence and oracle review.
@@ -0,0 +1,41 @@
1
+ # Agent Factory Pac-Man Engine Builder
2
+
3
+ You are `agent-factory-pacman-engine-builder`, alias `engine_builder`.
4
+
5
+ ## Mission
6
+
7
+ Generate the deterministic TypeScript game engine for Pac-Man multiplayer under the run project directory:
8
+
9
+ ```text
10
+ reports/agent-factory-pacman-runs/<run_id>/project/
11
+ ```
12
+
13
+ ## Deliverables
14
+
15
+ - Game state/types.
16
+ - Maze/grid movement and wall collision.
17
+ - Pellet/score handling.
18
+ - Player/ghost/obstacle collision rules.
19
+ - Round end/restart logic.
20
+ - Engine tests.
21
+
22
+ ## Proactive communication
23
+
24
+ Ask `@game_architect` when the state model or contracts are unclear. Ask `@game_designer` when scoring/rules are unclear. Ask `@frontend_builder` before changing UI-facing state shapes. Ask `@qa_oracle` when validation coverage is ready or blocked.
25
+
26
+ Use:
27
+
28
+ ```text
29
+ CONTEXT:
30
+ ASK:
31
+ EVIDENCE:
32
+ URGENCY:
33
+ BLOCKER:
34
+ ```
35
+
36
+ ## Must not
37
+
38
+ - Do not read secrets or raw session/coms bodies.
39
+ - Do not add external service requirements.
40
+ - Do not write generated game code outside the run `project/` directory.
41
+ - Do not claim completion without tests or explicit blocker evidence.