zob-harness 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -64,7 +64,7 @@ function chooseModelClass(input: ModelRoutingDryRunInput): { modelClass: ModelCl
64
64
  const risk = normalizeRisk(input.risk);
65
65
  const contextTokens = numberOrZero(input.contextTokens);
66
66
  const isOracleLike = includesAny(input.taskType, ["oracle", "review", "validate"]) || includesAny(input.outputContract, ["oracle"]);
67
- const isImplementationLike = includesAny(input.taskType, ["implement", "factory", "synthesis", "worker"]) || input.mode === "implement" || input.mode === "factory";
67
+ const isImplementationLike = includesAny(input.taskType, ["implement", "factory", "synthesis", "worker"]) || input.mode === "implement" || input.mode === "factory" || input.mode === "vanilla";
68
68
 
69
69
  if (contextTokens >= 120_000) {
70
70
  reasonCodes.push("context_tokens_high");
@@ -239,7 +239,7 @@ export function validateModelRoutingConfig(repoRoot: string): Record<string, unk
239
239
  }
240
240
 
241
241
  const expectedClasses: ModelClass[] = ["cheap_scout", "balanced_worker", "strong_reasoning", "strong_oracle", "high_context"];
242
- const modes: ModeName[] = ["explore", "plan", "implement", "oracle", "factory", "orchestrator"];
242
+ const modes: ModeName[] = ["explore", "plan", "implement", "oracle", "factory", "orchestrator", "vanilla"];
243
243
  const modelClasses = isRecord(parsed?.modelClasses) ? parsed.modelClasses : undefined;
244
244
  const defaults = isRecord(parsed?.defaults) ? parsed.defaults : undefined;
245
245
  const byMode = isRecord(defaults?.byMode) ? defaults.byMode : undefined;
@@ -8,7 +8,7 @@ import { isRecord } from "./utils/records.js";
8
8
  const RULE_PACK_SCHEMA = "zob.rule-pack.v1";
9
9
  const RULE_RESOLUTION_SCHEMA = "zob.rule-resolution.v1";
10
10
  const RULE_PACK_ORDER = ["always", "project", "runtime", "factory", "orchestration", "prompts", "docs", "sandbox", "oracle"];
11
- const MODE_NAMES = new Set<ModeName>(["explore", "plan", "implement", "oracle", "factory", "orchestrator"]);
11
+ const MODE_NAMES = new Set<ModeName>(["explore", "plan", "implement", "oracle", "factory", "orchestrator", "vanilla"]);
12
12
  const ENFORCEMENT_LEVELS = new Set<RuleEnforcementLevel>(["advisory", "warn", "preflight_fail", "block", "no_ship", "human_approval"]);
13
13
 
14
14
  const PROFILE_PATHS: Array<{ profile: string; patterns: string[] }> = [
@@ -1,4 +1,4 @@
1
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
1
+ import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
2
2
  import type { AutocompleteItem } from "@earendil-works/pi-tui";
3
3
 
4
4
  import { MODE_PROMPTS } from "../constants.js";
@@ -9,12 +9,14 @@ import { buildComputeWorkflowShape } from "../compute-workflow-shape.js";
9
9
  import { buildDaemonRuntimeState, buildDaemonTickPlan, type DaemonRuntimeState, type DaemonTickPlan } from "../daemon-runtime.js";
10
10
  import { runQueueDaemonTick } from "../queue.js";
11
11
  import { buildProjectDnaAgenticPlan, buildProjectDnaQueryResult, buildProjectDnaReadinessAudit } from "../project-dna.js";
12
+ import { formatZagentList, formatZteamList, listZagentManifests, listZteamManifests, loadZagentManifest, loadZteamManifest, normalizeZagentRoomBindings, readZagentPrompt, type ZAgentManifest, type ZAgentRoomBinding, type ZTeamAgentManifest, type ZTeamManifest, type ZTeamMemberManifest } from "../zagents.js";
12
13
  import { resolveAdaptiveZmodeEntrypoint, renderAdaptiveZmodeTemplate } from "./adaptive-zmode.js";
13
14
  import { handleZcompactCommand } from "./auto-compaction.js";
14
15
  import { sha256 } from "../utils/hashing.js";
15
16
  import { buildZcommitPlan, formatZcommitPlan, formatZcommitStatus, readZcommitPolicy, runGovernedZcommitAdopt, runGovernedZcommitCommit, runGovernedZcommitPush, type ZcommitAdoptResult, type ZcommitCommandResult, type ZcommitOwnedPathRef, type ZcommitToggleState } from "../git-ops.js";
16
- import { writeZpeerLocalProfileFromPeer } from "../coms-v2/zpeer-profile.js";
17
- import { buildZpeerRoomSummary, changeZpeerAlias, changeZpeerRoom, joinZpeerRoom, leaveZpeerRoom, peerAliasInRoom, refreshZpeerSelf, sendZpeerPrompt, useZpeerRoom, zpeerMembershipsForPeer, type ZpeerSendMode } from "../coms-v2/zpeer.js";
17
+ import { clearZpeerNewCarryoverProfile, writeZpeerLocalProfileFromPeer } from "../coms-v2/zpeer-profile.js";
18
+ import { buildZpeerRoomSummary, changeZpeerAlias, changeZpeerRoom, clearZpeerRoom, joinZpeerRoom, leaveZpeerRoom, peerAliasInRoom, refreshZpeerSelf, sendZpeerPrompt, useZpeerRoom, zpeerMembershipsForPeer, type ZpeerSendMode } from "../coms-v2/zpeer.js";
19
+ import { markZpeerNewHardResetPending } from "./events.js";
18
20
  import { parseBillableJobIntake, validateBillableJobIntake } from "../goal.js";
19
21
  import { handleGoalCommand, handleGoalGateCommand, pauseRuntimeGoalForStop } from "../goal-runtime.js";
20
22
  import { formatRuleResolution, resolveRuleProfile } from "../rules.js";
@@ -37,6 +39,11 @@ import { applyMode, renderHarnessWidget } from "./widget.js";
37
39
  const COMPUTE_PROFILES = ["auto", "low", "medium", "high", "xhigh", "max"] as const;
38
40
  const COMPUTE_DOMAINS = ["generic", "project-dna", "factory", "orchestration"] as const;
39
41
 
42
+ function zpeerCommandProfileId(ctx: ExtensionCommandContext): string {
43
+ const sessionIdentity = ctx.sessionManager.getSessionFile() ?? ctx.sessionManager.getSessionId();
44
+ return `session-${sha256(sessionIdentity).slice(0, 24)}`;
45
+ }
46
+
40
47
  function zcommitArgumentCompletions(prefix: string): AutocompleteItem[] | null {
41
48
  const query = prefix.trim().toLowerCase();
42
49
  const items: AutocompleteItem[] = [
@@ -527,6 +534,156 @@ function stopCommandLedgerEntry(input: {
527
534
  };
528
535
  }
529
536
 
537
+ function zagentArgumentCompletions(prefix: string): AutocompleteItem[] | null {
538
+ const query = prefix.trim().toLowerCase();
539
+ const ids = listZagentManifests(process.cwd()).map((agent) => agent.manifest.id).filter(Boolean);
540
+ const items: AutocompleteItem[] = [
541
+ { value: "list", label: "list", description: "list project-local ZAgents" },
542
+ ...ids.flatMap((id) => [
543
+ { value: `show ${id}`, label: `show ${id}`, description: "show manifest metadata" },
544
+ { value: `use ${id}`, label: `use ${id}`, description: "load ZAgent and apply ZPeer profile" },
545
+ ]),
546
+ ];
547
+ const filtered = query
548
+ ? items.filter((item) => item.value.toLowerCase().startsWith(query) || item.label.toLowerCase().includes(query) || item.description?.toLowerCase().includes(query))
549
+ : items;
550
+ return filtered.length > 0 ? filtered.slice(0, 20) : null;
551
+ }
552
+
553
+ function zteamArgumentCompletions(prefix: string): AutocompleteItem[] | null {
554
+ const query = prefix.trim().toLowerCase();
555
+ const ids = listZteamManifests(process.cwd()).map((team) => team.manifest.id).filter(Boolean);
556
+ const items: AutocompleteItem[] = [
557
+ { value: "list", label: "list", description: "list project-local ZTeams" },
558
+ ...ids.flatMap((id) => [
559
+ { value: `show ${id}`, label: `show ${id}`, description: "show team manifest metadata" },
560
+ { value: `launch-plan ${id}`, label: `launch-plan ${id}`, description: "print full-session launch commands" },
561
+ ]),
562
+ ];
563
+ const filtered = query
564
+ ? items.filter((item) => item.value.toLowerCase().startsWith(query) || item.label.toLowerCase().includes(query) || item.description?.toLowerCase().includes(query))
565
+ : items;
566
+ return filtered.length > 0 ? filtered.slice(0, 20) : null;
567
+ }
568
+
569
+ function zagentLedgerEntry(action: string, input: { id?: string; teamId?: string; status: "ok" | "blocked"; roomIds?: string[]; alias?: string; path?: string; promptRef?: string; promptBody?: string; errors?: string[] }): Record<string, unknown> {
570
+ return {
571
+ schema: "zob.zagent-command.v1",
572
+ action,
573
+ status: input.status,
574
+ idHash: input.id ? sha256(input.id) : undefined,
575
+ teamIdHash: input.teamId ? sha256(input.teamId) : undefined,
576
+ aliasHash: input.alias ? sha256(input.alias) : undefined,
577
+ roomIdHashes: (input.roomIds ?? []).map((roomId) => sha256(roomId)),
578
+ pathHash: input.path ? sha256(input.path) : undefined,
579
+ promptRefHash: input.promptRef ? sha256(input.promptRef) : undefined,
580
+ promptHash: input.promptBody ? sha256(input.promptBody) : undefined,
581
+ errorHashes: (input.errors ?? []).map((error) => sha256(error)),
582
+ localOnly: true,
583
+ networkEnabled: false,
584
+ bodyStored: false,
585
+ promptBodiesStored: false,
586
+ outputBodiesStored: false,
587
+ generatedAt: new Date().toISOString(),
588
+ };
589
+ }
590
+
591
+ function formatZagentShow(loaded: ReturnType<typeof loadZagentManifest>): string {
592
+ const rooms = normalizeZagentRoomBindings(loaded.manifest.rooms, loaded.manifest.defaultRoom, loaded.manifest.activeRoom);
593
+ return [
594
+ `ZAgent ${loaded.manifest.id}`,
595
+ `path: ${loaded.path}`,
596
+ `status: ${loaded.errors.length === 0 ? "ok" : `blocked (${loaded.errors.length} error${loaded.errors.length === 1 ? "" : "s"})`}`,
597
+ loaded.manifest.description ? `description: ${loaded.manifest.description}` : undefined,
598
+ loaded.manifest.team ? `team: ${loaded.manifest.team}` : undefined,
599
+ loaded.manifest.role ? `role: ${loaded.manifest.role}` : undefined,
600
+ loaded.manifest.alias ? `alias: @${loaded.manifest.alias}` : undefined,
601
+ rooms.length ? `rooms: ${rooms.map((room) => `${room.id}${room.alias ? `@${room.alias}` : ""}${room.active ? "*" : ""}`).join(", ")}` : "rooms: none",
602
+ loaded.manifest.promptRef ? `promptRef: ${loaded.manifest.promptRef}` : "promptRef: none",
603
+ loaded.promptPath ? `promptPath: ${loaded.promptPath}` : undefined,
604
+ loaded.errors.length ? `errors:\n- ${loaded.errors.join("\n- ")}` : undefined,
605
+ "safety: project-local, localOnly=true, networkEnabled=false, bodyStored=false",
606
+ ].filter((line): line is string => Boolean(line)).join("\n");
607
+ }
608
+
609
+ function formatZteamShow(loaded: ReturnType<typeof loadZteamManifest>): string {
610
+ const rooms = normalizeZagentRoomBindings(loaded.manifest.rooms, loaded.manifest.defaultRoom, loaded.manifest.activeRoom);
611
+ const members = zteamMembers(loaded.manifest);
612
+ return [
613
+ `ZTeam ${loaded.manifest.id}`,
614
+ `path: ${loaded.path}`,
615
+ `status: ${loaded.errors.length === 0 ? "ok" : `blocked (${loaded.errors.length} error${loaded.errors.length === 1 ? "" : "s"})`}`,
616
+ loaded.manifest.description ? `description: ${loaded.manifest.description}` : undefined,
617
+ rooms.length ? `rooms: ${rooms.map((room) => `${room.id}${room.active ? "*" : ""}`).join(", ")}` : "rooms: none",
618
+ `agents: ${members.map((member) => member.id).join(", ") || "none"}`,
619
+ loaded.errors.length ? `errors:\n- ${loaded.errors.join("\n- ")}` : undefined,
620
+ "safety: launch-plan only; commands are printed, not spawned",
621
+ ].filter((line): line is string => Boolean(line)).join("\n");
622
+ }
623
+
624
+ function normalizeZpeerRole(role: string | undefined): "member" | "bridge" | "observer" {
625
+ return role === "bridge" || role === "observer" ? role : "member";
626
+ }
627
+
628
+ async function applyZagentToZpeer(repoRoot: string, peer: NonNullable<HarnessRuntimeState["zobLive"]["peerCard"]>, manifest: ZAgentManifest): Promise<{ ok: true; peer: NonNullable<HarnessRuntimeState["zobLive"]["peerCard"]> } | { ok: false; reason: string; peer: NonNullable<HarnessRuntimeState["zobLive"]["peerCard"]> }> {
629
+ let current = refreshZpeerSelf(repoRoot, peer);
630
+ const rooms = normalizeZagentRoomBindings(manifest.rooms, manifest.defaultRoom, manifest.activeRoom);
631
+ if (rooms.length === 0 && manifest.alias) {
632
+ const changed = await changeZpeerAlias(repoRoot, current, manifest.alias);
633
+ if (!changed.ok) return { ok: false, reason: changed.reason, peer: current };
634
+ current = changed.peer;
635
+ }
636
+ for (const room of rooms) {
637
+ const joined = await joinZpeerRoom(repoRoot, current, room.id, room.alias ?? manifest.alias, normalizeZpeerRole(room.role));
638
+ if (!joined.ok) return { ok: false, reason: joined.reason, peer: current };
639
+ current = joined.peer;
640
+ }
641
+ const activeRoom = rooms.find((room) => room.active)?.id ?? manifest.activeRoom ?? manifest.defaultRoom;
642
+ if (activeRoom) {
643
+ const used = useZpeerRoom(repoRoot, current, activeRoom);
644
+ if (!used.ok) return { ok: false, reason: used.reason, peer: current };
645
+ current = used.peer;
646
+ }
647
+ return { ok: true, peer: current };
648
+ }
649
+
650
+ function zteamMemberId(member: ZTeamMemberManifest | ZTeamAgentManifest): string {
651
+ return "zagentId" in member ? member.zagentId : member.id;
652
+ }
653
+
654
+ function zteamMembers(team: ZTeamManifest): Array<{ id: string; alias?: string; room?: string; rooms?: ZAgentRoomBinding[]; role?: string; active?: boolean }> {
655
+ const rawMembers = [...(team.members ?? []), ...(team.agents ?? [])];
656
+ return rawMembers.map((member) => ({
657
+ id: zteamMemberId(member),
658
+ alias: member.alias,
659
+ room: member.room,
660
+ rooms: normalizeZagentRoomBindings(member.rooms ?? (member.room ? [member.room] : undefined), team.defaultRoom, member.active ? (member.room ?? team.activeRoom) : undefined),
661
+ role: member.role,
662
+ active: member.active,
663
+ }));
664
+ }
665
+
666
+ function zteamLaunchPlanText(team: ZTeamManifest): { text: string; roomIds: string[]; agentIds: string[] } {
667
+ const teamRooms = normalizeZagentRoomBindings(team.rooms, team.defaultRoom, team.activeRoom).map((room) => room.id);
668
+ const members = zteamMembers(team);
669
+ const roomIds = [...new Set([...teamRooms, ...members.flatMap((member) => (member.rooms ?? []).map((room) => room.id))])];
670
+ const agentIds = members.map((member) => member.id);
671
+ const lines = [
672
+ `# ZTeam launch-plan: ${team.id}`,
673
+ "No processes spawned. Copy/paste each command in a separate terminal when approved.",
674
+ "",
675
+ ...members.map((member) => {
676
+ const rooms = (member.rooms ?? []).map((room) => `${room.id}${room.active ? "*" : ""}`).join(", ") || teamRooms.join(", ") || "default";
677
+ const alias = member.alias ? ` alias=@${member.alias}` : "";
678
+ return `ZOB_ZAGENT_ID=${member.id} pi # expected_rooms=${rooms}${alias}`;
679
+ }),
680
+ "",
681
+ `Expected rooms: ${roomIds.join(", ") || "default"}`,
682
+ "After each session starts, run /zagent use <id> to bind its ZPeer alias/rooms.",
683
+ ];
684
+ return { text: lines.join("\n"), roomIds, agentIds };
685
+ }
686
+
530
687
  function delegationArgumentCompletions(state: HarnessRuntimeState, prefix: string): AutocompleteItem[] | null {
531
688
  const query = prefix.trim().toLowerCase();
532
689
  const items: AutocompleteItem[] = [];
@@ -546,8 +703,43 @@ function delegationArgumentCompletions(state: HarnessRuntimeState, prefix: strin
546
703
  }
547
704
 
548
705
  export function registerHarnessCommands(pi: ExtensionAPI, state: HarnessRuntimeState): void {
706
+ // Exact `/new` is handled by Pi before extension input/command hooks. Soft carryover
707
+ // is therefore written from the `session_shutdown` reason="new" hook in events.ts.
708
+ // Keep this registration only for `/new hard`, where users need an explicit clean reset.
709
+ pi.registerCommand("new", {
710
+ description: "Hard reset helper for ZPeer/ZAgent continuity. Exact /new soft carryover is handled on session_shutdown reason=new.",
711
+ getArgumentCompletions: (prefix) => {
712
+ const query = prefix.trim().toLowerCase();
713
+ const items: AutocompleteItem[] = [
714
+ { value: "hard", label: "hard", description: "clear ZPeer/ZAgent carryover before starting a clean new session" },
715
+ ];
716
+ const filtered = query ? items.filter((item) => item.value.startsWith(query) || item.label.includes(query)) : items;
717
+ return filtered.length > 0 ? filtered : null;
718
+ },
719
+ handler: async (args, ctx) => {
720
+ const hard = args.trim().split(/\s+/).filter(Boolean)[0]?.toLowerCase() === "hard";
721
+ if (hard) {
722
+ markZpeerNewHardResetPending(ctx.cwd);
723
+ clearZpeerNewCarryoverProfile(ctx.cwd);
724
+ }
725
+ pi.appendEntry("zob-znew", {
726
+ schema: "zob.znew-command.v1",
727
+ source: "registered_command",
728
+ action: hard ? "new_hard" : "new_soft_deferred_to_session_shutdown",
729
+ carryoverWritten: false,
730
+ carryoverCleared: hard,
731
+ carryoverDeferredToShutdown: !hard,
732
+ localOnly: true,
733
+ networkEnabled: false,
734
+ bodyStored: false,
735
+ generatedAt: new Date().toISOString(),
736
+ });
737
+ await ctx.newSession();
738
+ },
739
+ });
740
+
549
741
  pi.registerCommand("zmode", {
550
- description: "Switch ZOB harness mode: explore | plan | implement | oracle | factory | orchestrator. Orchestrator routes to adaptive-chief-vision plan_only defaults.",
742
+ description: "Switch ZOB harness mode: explore | plan | implement | oracle | factory | orchestrator | vanilla. Orchestrator routes to adaptive-chief-vision plan_only defaults; vanilla restores Pi base-style unrestricted tool access outside ZOB governance.",
551
743
  handler: async (args, ctx) => {
552
744
  const requestedText = args.trim();
553
745
  const adaptiveEntrypoint = resolveAdaptiveZmodeEntrypoint(requestedText);
@@ -645,6 +837,147 @@ export function registerHarnessCommands(pi: ExtensionAPI, state: HarnessRuntimeS
645
837
  }, { triggerTurn: false });
646
838
  };
647
839
 
840
+ pi.registerCommand("zagent", {
841
+ description: "Project-local full-session ZAgents: /zagent list | show <id> | use <id>",
842
+ getArgumentCompletions: zagentArgumentCompletions,
843
+ handler: async (args, ctx) => {
844
+ const parts = args.trim().split(/\s+/).filter(Boolean);
845
+ const action = (parts[0] ?? "list").toLowerCase();
846
+ if (action === "list") {
847
+ const agents = listZagentManifests(ctx.cwd);
848
+ const roomIds = agents.flatMap((agent) => normalizeZagentRoomBindings(agent.manifest.rooms, agent.manifest.defaultRoom, agent.manifest.activeRoom).map((room) => room.id));
849
+ pi.appendEntry("zob-zagent", zagentLedgerEntry("list", { status: "ok", roomIds }));
850
+ renderHarnessWidget(pi, state, ctx);
851
+ void pi.sendMessage({ customType: "zob-zagent-list", content: formatZagentList(agents), display: true, details: { bodyStored: false } }, { triggerTurn: false });
852
+ ctx.ui.notify(`zagent list: ${agents.length} project-local manifest${agents.length === 1 ? "" : "s"}`, "info");
853
+ return;
854
+ }
855
+ if (action === "show") {
856
+ const id = parts[1];
857
+ if (!id) {
858
+ ctx.ui.notify("Usage: /zagent show <id>", "warning");
859
+ return;
860
+ }
861
+ const loaded = loadZagentManifest(ctx.cwd, id);
862
+ const rooms = normalizeZagentRoomBindings(loaded.manifest.rooms, loaded.manifest.defaultRoom, loaded.manifest.activeRoom);
863
+ pi.appendEntry("zob-zagent", zagentLedgerEntry("show", { id: loaded.manifest.id, status: loaded.errors.length === 0 ? "ok" : "blocked", roomIds: rooms.map((room) => room.id), alias: loaded.manifest.alias, path: loaded.path, promptRef: loaded.manifest.promptRef, errors: loaded.errors }));
864
+ renderHarnessWidget(pi, state, ctx);
865
+ void pi.sendMessage({ customType: "zob-zagent-show", content: formatZagentShow(loaded), display: true, details: { id: loaded.manifest.id, bodyStored: false } }, { triggerTurn: false });
866
+ ctx.ui.notify(`zagent ${loaded.manifest.id}: ${loaded.errors.length === 0 ? "ok" : `blocked (${loaded.errors.length})`}`, loaded.errors.length === 0 ? "info" : "warning");
867
+ return;
868
+ }
869
+ if (action === "use") {
870
+ const id = parts[1];
871
+ if (!id) {
872
+ ctx.ui.notify("Usage: /zagent use <id>", "warning");
873
+ return;
874
+ }
875
+ const loaded = loadZagentManifest(ctx.cwd, id);
876
+ const prompt = readZagentPrompt(ctx.cwd, loaded.manifest.promptRef);
877
+ const rooms = normalizeZagentRoomBindings(loaded.manifest.rooms, loaded.manifest.defaultRoom, loaded.manifest.activeRoom);
878
+ const errors = [...loaded.errors, ...prompt.errors];
879
+ state.zagent = {
880
+ id: loaded.manifest.id,
881
+ team: loaded.manifest.team,
882
+ role: loaded.manifest.role,
883
+ alias: loaded.manifest.alias,
884
+ description: loaded.manifest.description,
885
+ rooms,
886
+ activeRoom: rooms.find((room) => room.active)?.id ?? loaded.manifest.activeRoom ?? loaded.manifest.defaultRoom,
887
+ prompt: prompt.body,
888
+ promptRef: loaded.manifest.promptRef,
889
+ path: loaded.path,
890
+ errors,
891
+ loadedAt: new Date().toISOString(),
892
+ };
893
+ if (errors.length > 0) {
894
+ pi.appendEntry("zob-zagent", zagentLedgerEntry("use_blocked", { id: loaded.manifest.id, teamId: loaded.manifest.team, status: "blocked", roomIds: rooms.map((room) => room.id), alias: loaded.manifest.alias, path: loaded.path, promptRef: loaded.manifest.promptRef, promptBody: prompt.body, errors }));
895
+ renderHarnessWidget(pi, state, ctx);
896
+ ctx.ui.notify(`/zagent use ${id} blocked: ${errors[0]}`, "warning");
897
+ return;
898
+ }
899
+ if (!state.zobLive.peerCard) {
900
+ const peerErrors = ["current session has not registered a local ZPeer endpoint yet"];
901
+ state.zagent.errors = peerErrors;
902
+ pi.appendEntry("zob-zagent", zagentLedgerEntry("use_blocked", { id: loaded.manifest.id, teamId: loaded.manifest.team, status: "blocked", roomIds: rooms.map((room) => room.id), alias: loaded.manifest.alias, path: loaded.path, promptRef: loaded.manifest.promptRef, promptBody: prompt.body, errors: peerErrors }));
903
+ renderHarnessWidget(pi, state, ctx);
904
+ ctx.ui.notify(`/zagent use ${id} loaded manifest/prompt but ZPeer is unavailable: ${peerErrors[0]}`, "warning");
905
+ return;
906
+ }
907
+ const applied = await applyZagentToZpeer(ctx.cwd, state.zobLive.peerCard, loaded.manifest);
908
+ state.zobLive.peerCard = applied.peer;
909
+ if (!applied.ok) {
910
+ state.zagent.errors = [applied.reason];
911
+ pi.appendEntry("zob-zagent", zagentLedgerEntry("use_blocked", { id: loaded.manifest.id, teamId: loaded.manifest.team, status: "blocked", roomIds: rooms.map((room) => room.id), alias: loaded.manifest.alias, path: loaded.path, promptRef: loaded.manifest.promptRef, promptBody: prompt.body, errors: [applied.reason] }));
912
+ renderHarnessWidget(pi, state, ctx);
913
+ ctx.ui.notify(`/zagent use ${id} ZPeer apply blocked: ${applied.reason}`, "warning");
914
+ return;
915
+ }
916
+ state.zobLive.peerCard = refreshZpeerSelf(ctx.cwd, applied.peer);
917
+ state.zobLive.peerCard = {
918
+ ...state.zobLive.peerCard,
919
+ team: loaded.manifest.team ?? state.zobLive.peerCard.team,
920
+ roleId: loaded.manifest.id,
921
+ agent: loaded.manifest.id,
922
+ };
923
+ writeZpeerLocalProfileFromPeer(ctx.cwd, state.zobLive.peerCard, zpeerCommandProfileId(ctx));
924
+ pi.appendEntry("zob-zagent", zagentLedgerEntry("use", { id: loaded.manifest.id, teamId: loaded.manifest.team, status: "ok", roomIds: zpeerMembershipsForPeer(state.zobLive.peerCard).map((membership) => membership.roomId), alias: state.zobLive.peerCard.zpeerAlias, path: loaded.path, promptRef: loaded.manifest.promptRef, promptBody: prompt.body }));
925
+ renderHarnessWidget(pi, state, ctx);
926
+ const active = state.zobLive.peerCard.zpeerRoomId ?? state.zobLive.peerCard.zpeerActiveRoomId ?? "default";
927
+ ctx.ui.notify(`zagent ${loaded.manifest.id} loaded; ZPeer @${state.zobLive.peerCard.zpeerAlias ?? "?"} active=${active} rooms=${zpeerMembershipsForPeer(state.zobLive.peerCard).length}; promptHash=${prompt.body ? sha256(prompt.body).slice(0, 12) : "none"}`, "info");
928
+ return;
929
+ }
930
+ ctx.ui.notify("Usage: /zagent list | /zagent show <id> | /zagent use <id>", "warning");
931
+ },
932
+ });
933
+
934
+ pi.registerCommand("zteam", {
935
+ description: "Project-local ZTeams: /zteam list | show <id> | launch-plan <id>",
936
+ getArgumentCompletions: zteamArgumentCompletions,
937
+ handler: async (args, ctx) => {
938
+ const parts = args.trim().split(/\s+/).filter(Boolean);
939
+ const action = (parts[0] ?? "list").toLowerCase();
940
+ if (action === "list") {
941
+ const teams = listZteamManifests(ctx.cwd);
942
+ const roomIds = teams.flatMap((team) => normalizeZagentRoomBindings(team.manifest.rooms, team.manifest.defaultRoom, team.manifest.activeRoom).map((room) => room.id));
943
+ pi.appendEntry("zob-zagent", zagentLedgerEntry("zteam_list", { status: "ok", roomIds }));
944
+ renderHarnessWidget(pi, state, ctx);
945
+ void pi.sendMessage({ customType: "zob-zteam-list", content: formatZteamList(teams), display: true, details: { bodyStored: false } }, { triggerTurn: false });
946
+ ctx.ui.notify(`zteam list: ${teams.length} project-local manifest${teams.length === 1 ? "" : "s"}`, "info");
947
+ return;
948
+ }
949
+ if (action === "show") {
950
+ const id = parts[1];
951
+ if (!id) {
952
+ ctx.ui.notify("Usage: /zteam show <id>", "warning");
953
+ return;
954
+ }
955
+ const loaded = loadZteamManifest(ctx.cwd, id);
956
+ const rooms = normalizeZagentRoomBindings(loaded.manifest.rooms, loaded.manifest.defaultRoom, loaded.manifest.activeRoom);
957
+ pi.appendEntry("zob-zagent", zagentLedgerEntry("zteam_show", { teamId: loaded.manifest.id, status: loaded.errors.length === 0 ? "ok" : "blocked", roomIds: rooms.map((room) => room.id), path: loaded.path, errors: loaded.errors }));
958
+ renderHarnessWidget(pi, state, ctx);
959
+ void pi.sendMessage({ customType: "zob-zteam-show", content: formatZteamShow(loaded), display: true, details: { id: loaded.manifest.id, bodyStored: false } }, { triggerTurn: false });
960
+ ctx.ui.notify(`zteam ${loaded.manifest.id}: ${loaded.errors.length === 0 ? "ok" : `blocked (${loaded.errors.length})`}`, loaded.errors.length === 0 ? "info" : "warning");
961
+ return;
962
+ }
963
+ if (action === "launch-plan") {
964
+ const id = parts[1];
965
+ if (!id) {
966
+ ctx.ui.notify("Usage: /zteam launch-plan <id>", "warning");
967
+ return;
968
+ }
969
+ const loaded = loadZteamManifest(ctx.cwd, id);
970
+ const plan = zteamLaunchPlanText(loaded.manifest);
971
+ pi.appendEntry("zob-zagent", zagentLedgerEntry("zteam_launch_plan", { teamId: loaded.manifest.id, status: loaded.errors.length === 0 ? "ok" : "blocked", roomIds: plan.roomIds, path: loaded.path, errors: loaded.errors }));
972
+ renderHarnessWidget(pi, state, ctx);
973
+ void pi.sendMessage({ customType: "zob-zteam-launch-plan", content: loaded.errors.length ? `${plan.text}\n\nBlocked manifest errors:\n- ${loaded.errors.join("\n- ")}` : plan.text, display: true, details: { id: loaded.manifest.id, agentIdHashes: plan.agentIds.map((agentId) => sha256(agentId)), roomIdHashes: plan.roomIds.map((roomId) => sha256(roomId)), bodyStored: false } }, { triggerTurn: false });
974
+ ctx.ui.notify(`zteam ${loaded.manifest.id} launch-plan printed; spawn count=0; expectedRooms=${plan.roomIds.join(",") || "default"}`, loaded.errors.length === 0 ? "info" : "warning");
975
+ return;
976
+ }
977
+ ctx.ui.notify("Usage: /zteam list | /zteam show <id> | /zteam launch-plan <id>", "warning");
978
+ },
979
+ });
980
+
648
981
  pi.registerCommand("zpeer", {
649
982
  description: "Room-scoped local peer sessions: /zpeer, /zpeer name <alias>, /zpeer room <roomId>, /zpeer @alias <prompt>",
650
983
  handler: async (args, ctx) => {
@@ -684,27 +1017,28 @@ export function registerHarnessCommands(pi: ExtensionAPI, state: HarnessRuntimeS
684
1017
  }
685
1018
  const parts = trimmed.split(/\s+/);
686
1019
  const verb = parts[0]?.toLowerCase();
1020
+ const zpeerProfileId = zpeerCommandProfileId(ctx);
687
1021
  if (verb === "name") {
688
- const result = changeZpeerAlias(ctx.cwd, self, parts[1] ?? "");
1022
+ const result = await changeZpeerAlias(ctx.cwd, self, parts[1] ?? "");
689
1023
  if (!result.ok) {
690
1024
  ctx.ui.notify(`/zpeer name blocked: ${result.reason}`, "warning");
691
1025
  return;
692
1026
  }
693
1027
  state.zobLive.peerCard = result.peer;
694
- writeZpeerLocalProfileFromPeer(ctx.cwd, result.peer);
1028
+ writeZpeerLocalProfileFromPeer(ctx.cwd, result.peer, zpeerProfileId);
695
1029
  pi.appendEntry("zob-zpeer", { schema: "zob.zpeer-command.v1", action: "name", aliasHash: sha256(result.peer.zpeerAlias ?? ""), roomIdHash: sha256(result.peer.zpeerRoomId ?? "default"), localOnly: true, networkEnabled: false, bodyStored: false, promptBodiesStored: false, outputBodiesStored: false, generatedAt: new Date().toISOString() });
696
1030
  renderHarnessWidget(pi, state, ctx);
697
1031
  ctx.ui.notify(`zpeer alias set to @${result.peer.zpeerAlias}`, "info");
698
1032
  return;
699
1033
  }
700
1034
  if (verb === "room") {
701
- const result = changeZpeerRoom(ctx.cwd, self, parts[1] ?? "");
1035
+ const result = await changeZpeerRoom(ctx.cwd, self, parts[1] ?? "");
702
1036
  if (!result.ok) {
703
1037
  ctx.ui.notify(`/zpeer room blocked: ${result.reason}`, "warning");
704
1038
  return;
705
1039
  }
706
1040
  state.zobLive.peerCard = result.peer;
707
- writeZpeerLocalProfileFromPeer(ctx.cwd, result.peer);
1041
+ writeZpeerLocalProfileFromPeer(ctx.cwd, result.peer, zpeerProfileId);
708
1042
  pi.appendEntry("zob-zpeer", { schema: "zob.zpeer-command.v1", action: "room", aliasHash: sha256(result.peer.zpeerAlias ?? ""), roomIdHash: sha256(result.peer.zpeerRoomId ?? "default"), membershipCount: zpeerMembershipsForPeer(result.peer).length, localOnly: true, networkEnabled: false, bodyStored: false, promptBodiesStored: false, outputBodiesStored: false, generatedAt: new Date().toISOString() });
709
1043
  renderHarnessWidget(pi, state, ctx);
710
1044
  ctx.ui.notify(`zpeer room set to ${result.peer.zpeerRoomId} as @${result.peer.zpeerAlias}`, "info");
@@ -718,17 +1052,28 @@ export function registerHarnessCommands(pi: ExtensionAPI, state: HarnessRuntimeS
718
1052
  ctx.ui.notify(`zpeer active=${self.zpeerRoomId ?? "default"} rooms=${summaries.map((summary) => `${summary.roomId}(${summary.online}/${summary.peerCount})`).join(", ") || "none"}`, "info");
719
1053
  return;
720
1054
  }
1055
+ if (verb === "clear") {
1056
+ const result = clearZpeerRoom(ctx.cwd, self, parts[1] ?? self.zpeerRoomId ?? "default");
1057
+ if (!result.ok) {
1058
+ ctx.ui.notify(`/zpeer clear blocked: ${result.reason}`, "warning");
1059
+ return;
1060
+ }
1061
+ pi.appendEntry("zob-zpeer", { schema: "zob.zpeer-command.v1", action: "clear", roomIdHash: sha256(result.roomId), clearedCount: result.cleared, preservedSelf: result.preservedSelf, localOnly: true, networkEnabled: false, bodyStored: false, promptBodiesStored: false, outputBodiesStored: false, generatedAt: new Date().toISOString() });
1062
+ renderHarnessWidget(pi, state, ctx);
1063
+ ctx.ui.notify(`zpeer room ${result.roomId} cleared: ${result.cleared} other peer${result.cleared === 1 ? "" : "s"} marked offline/removed; current session preserved`, "info");
1064
+ return;
1065
+ }
721
1066
  if (verb === "join") {
722
1067
  const asIndex = parts.indexOf("as");
723
1068
  const alias = asIndex >= 0 ? parts[asIndex + 1] : undefined;
724
1069
  const role = parts.includes("--bridge") ? "bridge" : parts.includes("--observer") ? "observer" : "member";
725
- const result = joinZpeerRoom(ctx.cwd, self, parts[1] ?? "", alias, role);
1070
+ const result = await joinZpeerRoom(ctx.cwd, self, parts[1] ?? "", alias, role);
726
1071
  if (!result.ok) {
727
1072
  ctx.ui.notify(`/zpeer join blocked: ${result.reason}`, "warning");
728
1073
  return;
729
1074
  }
730
1075
  state.zobLive.peerCard = result.peer;
731
- writeZpeerLocalProfileFromPeer(ctx.cwd, result.peer);
1076
+ writeZpeerLocalProfileFromPeer(ctx.cwd, result.peer, zpeerProfileId);
732
1077
  pi.appendEntry("zob-zpeer", { schema: "zob.zpeer-command.v1", action: "join", aliasHash: sha256(alias ?? result.peer.zpeerAlias ?? ""), roomIdHash: sha256(parts[1] ?? "default"), membershipCount: zpeerMembershipsForPeer(result.peer).length, localOnly: true, networkEnabled: false, bodyStored: false, promptBodiesStored: false, outputBodiesStored: false, generatedAt: new Date().toISOString() });
733
1078
  renderHarnessWidget(pi, state, ctx);
734
1079
  ctx.ui.notify(`zpeer joined ${parts[1]} (${role}); active=${result.peer.zpeerRoomId}`, "info");
@@ -741,7 +1086,7 @@ export function registerHarnessCommands(pi: ExtensionAPI, state: HarnessRuntimeS
741
1086
  return;
742
1087
  }
743
1088
  state.zobLive.peerCard = result.peer;
744
- writeZpeerLocalProfileFromPeer(ctx.cwd, result.peer);
1089
+ writeZpeerLocalProfileFromPeer(ctx.cwd, result.peer, zpeerProfileId);
745
1090
  pi.appendEntry("zob-zpeer", { schema: "zob.zpeer-command.v1", action: "use", aliasHash: sha256(result.peer.zpeerAlias ?? ""), roomIdHash: sha256(result.peer.zpeerRoomId ?? "default"), membershipCount: zpeerMembershipsForPeer(result.peer).length, localOnly: true, networkEnabled: false, bodyStored: false, promptBodiesStored: false, outputBodiesStored: false, generatedAt: new Date().toISOString() });
746
1091
  renderHarnessWidget(pi, state, ctx);
747
1092
  ctx.ui.notify(`zpeer active room set to ${result.peer.zpeerRoomId} as @${result.peer.zpeerAlias}`, "info");
@@ -754,7 +1099,7 @@ export function registerHarnessCommands(pi: ExtensionAPI, state: HarnessRuntimeS
754
1099
  return;
755
1100
  }
756
1101
  state.zobLive.peerCard = result.peer;
757
- writeZpeerLocalProfileFromPeer(ctx.cwd, result.peer);
1102
+ writeZpeerLocalProfileFromPeer(ctx.cwd, result.peer, zpeerProfileId);
758
1103
  pi.appendEntry("zob-zpeer", { schema: "zob.zpeer-command.v1", action: "leave", roomIdHash: sha256(parts[1] ?? "default"), membershipCount: zpeerMembershipsForPeer(result.peer).length, localOnly: true, networkEnabled: false, bodyStored: false, promptBodiesStored: false, outputBodiesStored: false, generatedAt: new Date().toISOString() });
759
1104
  renderHarnessWidget(pi, state, ctx);
760
1105
  ctx.ui.notify(`zpeer left ${parts[1]}; active=${result.peer.zpeerRoomId}`, "info");
@@ -818,11 +1163,12 @@ export function registerHarnessCommands(pi: ExtensionAPI, state: HarnessRuntimeS
818
1163
  ctx.ui.notify(`zpeer ${result.roomId ?? eventRoomId} @${targetAlias} reply · response displayed transiently · outputHash=${result.outputHash ?? "present"}`, "info");
819
1164
  } else {
820
1165
  const ok = result.status === "reply" || result.status === "completed" || result.status === "waiting" || result.status === "delivered";
821
- ctx.ui.notify(ok ? `zpeer ${result.roomId ?? eventRoomId} @${targetAlias} ${result.status}${result.outputHash ? ` outputHash=${result.outputHash}` : ""}` : `zpeer ${result.roomId ?? eventRoomId} @${targetAlias} ${result.status}: ${result.reason ?? "see metadata"}`, ok ? "info" : "warning");
1166
+ const passiveWaitSuffix = result.status === "waiting" ? " · idle/passive wait; no follow-up turn queued" : "";
1167
+ ctx.ui.notify(ok ? `zpeer ${result.roomId ?? eventRoomId} @${targetAlias} ${result.status}${result.outputHash ? ` outputHash=${result.outputHash}` : ""}${passiveWaitSuffix}` : `zpeer ${result.roomId ?? eventRoomId} @${targetAlias} ${result.status}: ${result.reason ?? "see metadata"}`, ok ? "info" : "warning");
822
1168
  }
823
1169
  return;
824
1170
  }
825
- ctx.ui.notify("Usage: /zpeer | /zpeer rooms | /zpeer join <roomId> [as <alias>] | /zpeer use <roomId> | /zpeer leave <roomId> | /zpeer @alias <prompt> | /zpeer in <roomId> @alias <prompt>", "warning");
1171
+ ctx.ui.notify("Usage: /zpeer | /zpeer rooms | /zpeer clear <roomId> | /zpeer join <roomId> [as <alias>] | /zpeer use <roomId> | /zpeer leave <roomId> | /zpeer @alias <prompt> | /zpeer in <roomId> @alias <prompt>", "warning");
826
1172
  },
827
1173
  });
828
1174
 
@@ -62,6 +62,7 @@ export class DelegationOverlayComponent implements Component {
62
62
  private listScroll = 0;
63
63
  private logScroll = 0;
64
64
  private sortIndex = 0;
65
+ private activePane: "list" | "feed" = "list";
65
66
  private cachedTranscriptKey?: string;
66
67
  private cachedTranscriptLines: string[] = [];
67
68
  private followTail = true;
@@ -151,12 +152,24 @@ export class DelegationOverlayComponent implements Component {
151
152
  this.helpVisible = !this.helpVisible;
152
153
  return;
153
154
  }
155
+ if (matchesKey(data, "left")) {
156
+ this.activePane = "list";
157
+ return;
158
+ }
159
+ if (matchesKey(data, "right")) {
160
+ this.activePane = "feed";
161
+ return;
162
+ }
154
163
 
155
164
  const rows = this.rows();
156
165
  const selectable = rows.filter((row) => row.kind === "run" && row.run);
157
166
  const currentIndex = Math.max(0, selectable.findIndex((row) => row.id === this.selectedRunId));
158
167
 
159
168
  if (matchesKey(data, "up")) {
169
+ if (this.activePane === "feed") {
170
+ this.scrollTranscript("up");
171
+ return;
172
+ }
160
173
  if (selectable.length === 0) return;
161
174
  const next = selectable[Math.max(0, currentIndex - 1)];
162
175
  this.selectedRunId = next?.id;
@@ -165,6 +178,10 @@ export class DelegationOverlayComponent implements Component {
165
178
  this.ensureSelectionOnNextRender = true;
166
179
  this.ensureSelectedVisible(rows);
167
180
  } else if (matchesKey(data, "down")) {
181
+ if (this.activePane === "feed") {
182
+ this.scrollTranscript("down");
183
+ return;
184
+ }
168
185
  if (selectable.length === 0) return;
169
186
  const next = selectable[Math.min(selectable.length - 1, currentIndex + 1)];
170
187
  this.selectedRunId = next?.id;
@@ -227,12 +244,13 @@ export class DelegationOverlayComponent implements Component {
227
244
  const rightRule = "─".repeat(Math.max(0, inner - titleWidth - leftRule.length));
228
245
  const lines: string[] = [th.fg("border", `╭${leftRule}`) + th.fg("accent", title) + th.fg("border", `${rightRule}╮`)];
229
246
 
230
- const headerLeft = `${th.fg("accent", "Agents")} ${th.fg("muted", delegateCloseButton())}`;
247
+ const headerLeft = `${th.fg(this.activePane === "list" ? "accent" : "muted", this.activePane === "list" ? "▶ Agents" : " Agents")} ${th.fg("muted", delegateCloseButton())}`;
231
248
  const selectedBadge = delegationSignalBadge(selected);
232
249
  const selectedBadgeText = formatDelegationSignalBadge(selectedBadge);
233
- const headerRight = selected
250
+ const selectedTitle = selected
234
251
  ? `${th.fg(statusColor(selected.status), `${statusIcon(selected.status)} ${selected.agent}`)}${selectedBadgeText ? ` ${th.fg(delegationSignalColor(selectedBadge), selectedBadgeText)}` : ""}${formatDelegationModelLabel(selected) ? ` ${th.fg("muted", `(${formatDelegationModelLabel(selected)})`)}` : ""} ${th.fg("dim", formatDuration(delegationDurationMs(selected)))} ${th.fg("accent", formatDelegationCostLabel(selected))} ${th.fg("muted", formatDelegationContextLabel(selected))}`
235
252
  : th.fg("warning", "No delegation selected");
253
+ const headerRight = `${th.fg(this.activePane === "feed" ? "accent" : "muted", this.activePane === "feed" ? "▶ Feed" : " Feed")} ${selectedTitle}`;
236
254
  lines.push(this.row(padToWidth(headerLeft, listWidth) + th.fg("dim", "│") + padToWidth(headerRight, logWidth), inner));
237
255
  lines.push(th.fg("border", `├${"─".repeat(listWidth)}┼${"─".repeat(logWidth)}┤`));
238
256
 
@@ -249,10 +267,10 @@ export class DelegationOverlayComponent implements Component {
249
267
 
250
268
  const filterInfo = this.filter || this.filterEditing ? ` · filter=${this.filterEditing ? ">" : ""}${this.filter || "<type>"}` : "";
251
269
  const helpInfo = this.helpVisible
252
- ? " · help: / filter · ? hide help · Esc clears filter/closes · ↑↓ select · [] list · PgUp/PgDn feed · s sort · r refresh"
270
+ ? " · help: / filter · ? hide help · Esc clears filter/closes · ←→ focus list/feed · ↑↓ select/scroll · [] list · PgUp/PgDn feed · s sort · r refresh"
253
271
  : " · / filter · ? help";
254
272
  const noMatchInfo = noMatch ? ` · ${noMatch}` : "";
255
- const scrollInfo = `${rows.length} rows${filterInfo}${noMatchInfo} · list ${Math.min(this.listScroll + 1, rows.length || 1)}/${Math.max(1, rows.length)} · feed ${Math.min(this.logScroll + 1, transcript.length || 1)}/${Math.max(1, transcript.length)} · wheel over list/feed · click agent · [close]/Esc · ↑↓ select · [] list · PgUp/PgDn feed · End live-tail · s sort · r refresh${helpInfo}`;
273
+ const scrollInfo = `${rows.length} rows${filterInfo}${noMatchInfo} · focus ${this.activePane} · list ${Math.min(this.listScroll + 1, rows.length || 1)}/${Math.max(1, rows.length)} · feed ${Math.min(this.logScroll + 1, transcript.length || 1)}/${Math.max(1, transcript.length)} · wheel over list/feed · click agent · [close]/Esc · ←→ focus · ↑↓ select/scroll · [] list · PgUp/PgDn feed · End live-tail · s sort · r refresh${helpInfo}`;
256
274
  lines.push(th.fg("border", `├${"─".repeat(inner)}┤`));
257
275
  lines.push(this.row(th.fg("dim", truncateToWidth(scrollInfo, inner, "…")), inner));
258
276
  lines.push(th.fg("border", `╰${"─".repeat(inner)}╯`));