Skip to content

chore: sync public mirror from internal#420

Merged
haasonsaas merged 1 commit into
mainfrom
sync/public-release-mirror
May 16, 2026
Merged

chore: sync public mirror from internal#420
haasonsaas merged 1 commit into
mainfrom
sync/public-release-mirror

Conversation

@haasonsaas
Copy link
Copy Markdown
Contributor

@haasonsaas haasonsaas commented May 16, 2026

Summary

  • sync the sanitized public tree from evalops/maestro-internal
  • keep evalops/maestro as a generated public mirror of the private source of truth
  • preserve public-owned CI and trusted-publishing workflows from the public checkout
  • internal source SHA: 070671068b00bf119ebcf83bffb8a0149447aa7d
  • last generated public sync base: b0bc8c532b6e802adb61715775a9e1d27a9a4aca
  • previewed public-tree drift: 17 file(s) to copy/update and 0 stale file(s) to delete
  • public-only commits since last generated sync: 4

Source-of-truth status

Public Mirror Drift Audit

  • package: @evalops/maestro
  • private source: https://github.com/evalops/maestro-internal@main (070671068b00)
  • public projection: https://github.com/evalops/maestro@main (39c0c810c5a5)
  • files to copy or update: 17
  • stale files to delete: 0
  • result: drift detected
  • invariant: public_projection_has_drift

Sample Changed Paths

  • copy/update docs/protocols/a2a-fleet-delegation.md
  • copy/update docs/protocols/a2a-peer-pairing.md
  • copy/update docs/protocols/a2a-tmux-smoke.md
  • copy/update package.json
  • copy/update packages/tui-rs/src/app.rs
  • copy/update packages/tui-rs/src/commands/registry.rs
  • copy/update packages/tui-rs/src/commands/types.rs
  • copy/update scripts/smoke-maestro-a2a-tmux.sh
  • copy/update src/cli-tui/commands/a2a-handlers.ts
  • copy/update src/cli/commands/a2a.ts
  • copy/update src/platform/a2a-fleet.ts
  • copy/update src/platform/a2a-task-ledger.ts
  • copy/update src/safety/bash-parser.ts
  • copy/update test/cli-tui/commands/a2a-handlers.test.ts
  • copy/update test/cli/commands/a2a-fleet-delegation.test.ts
  • copy/update test/cli/commands/a2a.test.ts
  • copy/update test/platform/a2a-task-ledger.test.ts

Guidance

Let internal main generate and merge the public sync PR before relying on public main.

Drift sample

  • copy/update docs/protocols/a2a-fleet-delegation.md
  • copy/update docs/protocols/a2a-peer-pairing.md
  • copy/update docs/protocols/a2a-tmux-smoke.md
  • copy/update package.json
  • copy/update packages/tui-rs/src/app.rs
  • copy/update packages/tui-rs/src/commands/registry.rs
  • copy/update packages/tui-rs/src/commands/types.rs
  • copy/update scripts/smoke-maestro-a2a-tmux.sh
  • copy/update src/cli-tui/commands/a2a-handlers.ts
  • copy/update src/cli/commands/a2a.ts
  • copy/update src/platform/a2a-fleet.ts
  • copy/update src/platform/a2a-task-ledger.ts
  • copy/update src/safety/bash-parser.ts
  • copy/update test/cli-tui/commands/a2a-handlers.test.ts
  • copy/update test/cli/commands/a2a-fleet-delegation.test.ts
  • copy/update test/cli/commands/a2a.test.ts
  • copy/update test/platform/a2a-task-ledger.test.ts

Public-only commits since last generated sync

Validation

  • generated by the sync-public-release-mirror workflow in public-tree mode

Test Plan

  • generated by the sync-public-release-mirror workflow in public-tree mode
  • public-source-provenance require-internal-pr check confirms internal source PR lineage
  • CI, integration, rust-hosted-conformance, coverage, Socket, and Cursor checks must pass before merge

Staged Rollout

  • Staging is unnecessary for this generated mirror PR: it does not independently promote user-visible behavior. It mirrors already-reviewed internal source from evalops/maestro-internal@070671068b00bf119ebcf83bffb8a0149447aa7d, including existing hidden/evaluation surfaces, and keeps public package parity behind the established public-source-provenance gate.

Supersedes

@cursor
Copy link
Copy Markdown

cursor Bot commented May 16, 2026

PR Summary

Medium Risk
Adds new A2A delegation/task-ledger persistence and fleet inspection logic plus new CLI/TUI commands, which touches networking, local file writes, and command parsing. Main risk is incorrect task/peer state reporting or ledger corruption/regressions in existing maestro a2a flows.

Overview
Adds an A2A fleet/delegation layer: new maestro a2a fleet, maestro a2a delegate, and maestro a2a tasks commands that join live peer Agent Card health with a new durable task ledger and support refreshing task state.

Introduces src/platform/a2a-task-ledger.ts for storing per-peer task records + transcripts (with MAESTRO_A2A_TASKS_FILE and CODEX_A2A_TASKS_FILE), and src/platform/a2a-fleet.ts to summarize registry peers, capabilities, and each peer’s latest ledger task without printing bearer tokens.

Updates TypeScript TUI /a2a handler and Rust TUI command parsing/help to recognize fleet/tasks/delegate (mostly as guidance messages), expands the tmux smoke harness to exercise delegation + fleet/task views, adds an npm smoke:a2a-tmux script, and adds focused unit/e2e tests for the new commands and ledger behavior. Separately tweaks bash-parser’s optional tree-sitter typing/imports to avoid relying on external type shapes.

Reviewed by Cursor Bugbot for commit 648caf2. Bugbot is set up for automated code reviews on this repo. Configure here.

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Refresh fetches all tasks including permanently terminal ones
    • refreshA2ATaskLedger now skips ledger entries already in terminal A2A states before resolving peers or fetching remote task state.

You can send follow-ups to the cloud agent here.

Comment thread src/cli/commands/a2a.ts
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c00af884ea

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/platform/a2a-task-ledger.ts
@haasonsaas haasonsaas enabled auto-merge (squash) May 16, 2026 05:45
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d6ac24b662

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/platform/a2a-task-ledger.ts Outdated
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Fail-open catch swallows AbortError without rethrowing
    • persistA2ALedgerBestEffort now rethrows aborts before logging fail-open ledger warnings.
  • ✅ Fixed: Fleet peer inspect catch swallows AbortError silently
    • inspectPeer now rethrows AbortError instead of converting cancellation into an unreachable-peer fallback.
Preview (98fbf2e179)
diff --git a/docs/protocols/a2a-fleet-delegation.md b/docs/protocols/a2a-fleet-delegation.md
new file mode 100644
--- /dev/null
+++ b/docs/protocols/a2a-fleet-delegation.md
@@ -1,0 +1,66 @@
+# A2A Fleet And Delegation
+
+Native A2A pairing makes peers discoverable. The fleet layer turns those peers
+into a small, durable operator network: inspect who is available, delegate work,
+poll task state, and keep a local transcript of what was asked and what came
+back.
+
+## Commands
+
+```sh
+maestro a2a fleet [--json] [--registry <path>] [--tasks <path>]
+maestro a2a delegate <peer> <text> [--role <role>] [--cwd <path>] [--wait]
+maestro a2a tasks [peer] [--json] [--refresh]
+maestro a2a wait <peer> <task-id>
+```
+
+`fleet` reads the native peer registry, fetches each peer Agent Card when
+reachable, and joins the result with the local task ledger. It never prints
+bearer token values. Peers that cannot be reached are still shown with their
+registry URL and a bounded error.
+
+`delegate` sends a normal A2A `message:send` request with Maestro delegation
+metadata: origin, peer name, role, and working directory. The resulting task is
+recorded in the local ledger before optional waiting begins.
+
+`tasks` reads the durable ledger and can refresh known task IDs from their
+registered peers. This gives the operator a single place to see outstanding work
+across the Mac mini, dev desktop, and local Maestro instances.
+
+## Files
+
+The peer registry remains:
+
+```text
+~/.maestro/a2a/peers.json
+```
+
+The task ledger defaults to:
+
+```text
+~/.maestro/a2a/tasks.json
+```
+
+`MAESTRO_A2A_TASKS_FILE` overrides the ledger path. `CODEX_A2A_TASKS_FILE` is
+accepted as a migration alias.
+
+## Acceptance Tests
+
+Before this feature, the following tests fail:
+
+```sh
+npm run test:fast -- test/cli/commands/a2a-fleet-delegation.test.ts test/cli-tui/commands/a2a-handlers.test.ts
+cargo test -p maestro-tui commands::registry::tests::a2a_command_parses_peer_actions
+```
+
+After implementation, they must pass and prove:
+
+- `maestro a2a delegate <peer> <text> --wait` sends real HTTP+JSON A2A traffic,
+  records the task, updates the final state, and stores a transcript.
+- `maestro a2a fleet --json` shows peer health, Agent Card capabilities, and the
+  peer's most recent ledger task without leaking token values.
+- `maestro a2a tasks --json` reads the ledger and can be used as a fleet task
+  view.
+- TypeScript and Rust TUIs both recognize `/a2a fleet`, `/a2a tasks`, and
+  `/a2a delegate`.
+

diff --git a/docs/protocols/a2a-peer-pairing.md b/docs/protocols/a2a-peer-pairing.md
--- a/docs/protocols/a2a-peer-pairing.md
+++ b/docs/protocols/a2a-peer-pairing.md
@@ -41,9 +41,10 @@

The smoke launches two local Maestro peers in tmux, exchanges native pairing
-codes, accepts each peer into isolated registries, then verifies send --wait
-and explicit wait. See A2A tmux smoke for the harness
-contract and troubleshooting knobs.
+codes, accepts each peer into isolated registries, delegates work into a durable
+task ledger, then verifies fleet, tasks, send, and explicit wait. See
+A2A tmux smoke for the harness contract and
+troubleshooting knobs.

TUI Surface

diff --git a/docs/protocols/a2a-tmux-smoke.md b/docs/protocols/a2a-tmux-smoke.md
--- a/docs/protocols/a2a-tmux-smoke.md
+++ b/docs/protocols/a2a-tmux-smoke.md
@@ -1,10 +1,10 @@

A2A tmux smoke

scripts/smoke-maestro-a2a-tmux.sh is the local end-to-end smoke for native
-Maestro A2A pairing. It launches two local Maestro control-plane peers in tmux,
-uses the TypeScript maestro a2a CLI to exchange pairing codes, stores each
-peer in an isolated registry, then verifies both send --wait and explicit
-wait.
+Maestro A2A pairing and delegation. It launches two local Maestro control-plane
+peers in tmux, uses the TypeScript maestro a2a CLI to exchange pairing codes,
+stores each peer in an isolated registry, delegates work into a durable task
+ledger, then verifies fleet health, task listing, send, and explicit wait.

Run it from the repo root:

@@ -19,6 +19,9 @@
bun run a2a -- offer ...
bun run a2a -- accept ...
bun run a2a -- peers
+bun run a2a -- delegate ...
+bun run a2a -- fleet ...
+bun run a2a -- tasks ...
bun run a2a -- send ...
bun run a2a -- wait ...

@@ -29,7 +32,10 @@
- Pairing codes are generated from each peer's Agent Card.
- Each side accepts the other side into an isolated
  `MAESTRO_A2A_PEERS_FILE` registry.
-- Peer A can send to peer B and block with bounded `send --wait`.
+- Peer A can delegate work to peer B and block with bounded `delegate --wait`.
+- Fleet output joins live Agent Card health with the durable local task ledger.
+- Task output reads the recorded delegated task without resolving or printing
+  bearer token values.
- Peer B can send to peer A, parse the returned task id, and complete a bounded
  explicit `wait`.

@@ -47,7 +53,8 @@

By default the script kills the tmux session on exit. Set
`MAESTRO_A2A_TMUX_KEEP_SESSION=1` to inspect the peer panes after a failure.
-Logs and temporary peer registries are written under `tmp/a2a-tmux-smoke/`.
+Logs, temporary peer registries, and task ledgers are written under
+`tmp/a2a-tmux-smoke/`.

## Expected output


diff --git a/package.json b/package.json
--- a/package.json
+++ b/package.json
@@ -108,6 +108,7 @@
    "smoke:codex-app-server-live": "node scripts/smoke-codex-app-server-live.mjs",
    "smoke:event-bus": "tsx scripts/smoke-maestro-event-bus.ts",
    "smoke:a2a-local": "tsx scripts/smoke-maestro-a2a-local.ts",
+    "smoke:a2a-tmux": "bash scripts/smoke-maestro-a2a-tmux.sh",
    "a2a": "tsx src/cli.ts a2a",
    "a2a:codex-bridge": "python3 scripts/codex-a2a-bridge.py",
    "a2a:peer": "tsx src/cli.ts a2a",

diff --git a/packages/tui-rs/src/app.rs b/packages/tui-rs/src/app.rs
--- a/packages/tui-rs/src/app.rs
+++ b/packages/tui-rs/src/app.rs
@@ -3371,27 +3371,48 @@
                    [
                        "## A2A peer pairing",
                        "",
+                        "/a2a fleet",
                        "/a2a peers",
+                        "/a2a tasks [peer]",
                        "/a2a accept <pairing-code>",
+                        "/a2a delegate <peer> <text>",
                        "/a2a send <peer> <text>",
                        "",
-                        "Native pairing codes are shared with the TypeScript CLI/TUI. Use `maestro a2a offer` to create a code from a running peer.",
+                        "Native pairing codes, fleet views, and delegation ledgers are shared with the TypeScript CLI/TUI.",
                    ]
                    .join("\n"),
                );
            }
+            A2aAction::Fleet => {
+                self.state.add_system_message(
+                    "A2A fleet inspection uses the shared Maestro peer registry. Run `maestro a2a fleet` for live health and task summaries until the Rust fleet reader is wired into this view."
+                        .to_string(),
+                );
+            }
            A2aAction::Peers => {
                self.state.add_system_message(
                    "A2A peer listing uses the shared Maestro peer registry. Run `maestro a2a peers` for the current registry until the Rust registry reader is wired into this view."
                        .to_string(),
                );
            }
+            A2aAction::Tasks { peer } => {
+                let scope = peer.as_deref().unwrap_or("all peers");
+                self.state.add_system_message(format!(
+                    "A2A task ledger requested for {scope}. Run `maestro a2a tasks` for the current durable ledger until the Rust task reader is wired into this view."
+                ));
+            }
            A2aAction::Accept { code } => {
                self.state.add_system_message(format!(
                    "A2A pairing code captured ({} chars). Run `maestro a2a accept <code>` or use the TypeScript TUI `/a2a accept <code>` to persist it in the shared registry.",
                    code.len()
                ));
            }
+            A2aAction::Delegate { peer, text } => {
+                self.state.add_system_message(format!(
+                    "A2A delegation prepared for `{peer}` ({} chars). Run `maestro a2a delegate {peer} <text> --wait` while the Rust delegation controller is connected to the shared A2A client.",
+                    text.len()
+                ));
+            }
            A2aAction::Send { peer, text } => {
                self.state.add_system_message(format!(
                    "A2A send request prepared for `{peer}` ({} chars). Run `maestro a2a send {peer} <text> --wait` while the Rust send controller is connected to the shared A2A client.",

diff --git a/packages/tui-rs/src/commands/registry.rs b/packages/tui-rs/src/commands/registry.rs
--- a/packages/tui-rs/src/commands/registry.rs
+++ b/packages/tui-rs/src/commands/registry.rs
@@ -511,13 +511,30 @@
        .unwrap_or_default();
    match subcommand.as_str() {
        "" | "help" => Ok(A2aAction::Help),
+        "fleet" => Ok(A2aAction::Fleet),
        "peers" | "list" => Ok(A2aAction::Peers),
+        "tasks" => Ok(A2aAction::Tasks {
+            peer: tokens.get(1).cloned(),
+        }),
        "accept" => {
            let code = tokens
                .get(1)
                .ok_or_else(|| CommandError::new("Usage: /a2a accept <pairing-code>"))?;
            Ok(A2aAction::Accept { code: code.clone() })
        }
+        "delegate" => {
+            let peer = tokens
+                .get(1)
+                .ok_or_else(|| CommandError::new("Usage: /a2a delegate <peer> <text>"))?;
+            let text = tokens.get(2..).unwrap_or(&[]).join(" ");
+            if text.trim().is_empty() {
+                return Err(CommandError::new("Usage: /a2a delegate <peer> <text>"));
+            }
+            Ok(A2aAction::Delegate {
+                peer: peer.clone(),
+                text,
+            })
+        }
        "send" => {
            let peer = tokens
                .get(1)
@@ -533,7 +550,7 @@
        }
        _ => Err(
            CommandError::new(format!("Unknown A2A subcommand: {subcommand}"))
-                .with_hint("Usage: /a2a [peers|accept <code>|send <peer> <text>]"),
+                .with_hint("Usage: /a2a [fleet|peers|tasks|accept <code>|delegate <peer> <text>|send <peer> <text>]"),
        ),
    }
}
@@ -754,7 +771,7 @@
    registry.register(
        Command::new(
            "a2a",
-            "Pair and inspect A2A peer agents",
+            "Pair, inspect, and delegate to A2A peer agents",
            CommandCategory::Tools,
            Box::new(|ctx| {
                Ok(CommandOutput::Action(CommandAction::A2a(parse_a2a_action(
@@ -762,7 +779,7 @@
                )?)))
            }),
        )
-        .usage("/a2a [peers|accept <code>|send <peer> <text>]"),
+        .usage("/a2a [fleet|peers|tasks|accept <code>|delegate <peer> <text>|send <peer> <text>]"),
    );

    // Queue command
@@ -1959,6 +1976,17 @@
    fn a2a_command_parses_peer_actions() {
        let registry = build_command_registry();

+        assert!(registry.execute("/a2a fleet", "/tmp", None, None).is_ok());
+        assert!(registry.execute("/a2a tasks", "/tmp", None, None).is_ok());
+        assert!(registry
+            .execute(
+                "/a2a delegate mac-mini run workspace smoke",
+                "/tmp",
+                None,
+                None,
+            )
+            .is_ok());
+
        match registry
            .execute("/a2a peers", "/tmp", None, None)
            .expect("a2a peers should parse")

diff --git a/packages/tui-rs/src/commands/types.rs b/packages/tui-rs/src/commands/types.rs
--- a/packages/tui-rs/src/commands/types.rs
+++ b/packages/tui-rs/src/commands/types.rs
@@ -357,10 +357,16 @@
pub enum A2aAction {
    /// Show native A2A pairing help.
    Help,
+    /// Show A2A fleet health.
+    Fleet,
    /// List paired peers.
    Peers,
+    /// List delegated A2A tasks.
+    Tasks { peer: Option<String> },
    /// Accept a pairing code.
    Accept { code: String },
+    /// Delegate work to a peer.
+    Delegate { peer: String, text: String },
    /// Send a message to a peer.
    Send { peer: String, text: String },
}

diff --git a/scripts/smoke-maestro-a2a-tmux.sh b/scripts/smoke-maestro-a2a-tmux.sh
--- a/scripts/smoke-maestro-a2a-tmux.sh
+++ b/scripts/smoke-maestro-a2a-tmux.sh
@@ -7,6 +7,8 @@
LOG_DIR="$WORK_DIR/logs"
REGISTRY_A="$WORK_DIR/peer-a-registry.json"
REGISTRY_B="$WORK_DIR/peer-b-registry.json"
+TASKS_A="$WORK_DIR/peer-a-tasks.json"
+TASKS_B="$WORK_DIR/peer-b-tasks.json"
READY_TIMEOUT_SECONDS="${MAESTRO_A2A_TMUX_READY_TIMEOUT_SECONDS:-120}"
KEEP_SESSION="${MAESTRO_A2A_TMUX_KEEP_SESSION:-0}"

@@ -74,7 +76,7 @@

cd "$ROOT_DIR"
mkdir -p "$LOG_DIR"
-rm -f "$REGISTRY_A" "$REGISTRY_B"
+rm -f "$REGISTRY_A" "$REGISTRY_B" "$TASKS_A" "$TASKS_B"

if tmux has-session -t "$SESSION_NAME" >/dev/null 2>&1; then
	echo "tmux session already exists: $SESSION_NAME" >&2
@@ -108,14 +110,32 @@
a2a_cli "$REGISTRY_A" peers
a2a_cli "$REGISTRY_B" peers

-echo "sending peer-a -> peer-b with bounded wait"
-SEND_A_TO_B="$(a2a_cli "$REGISTRY_A" send peer-b "hello from tmux peer A" --wait --max-wait-ms 30000 --interval-ms 250 --timeout-ms 3000)"
-echo "$SEND_A_TO_B"
-if ! grep -q "tmux peer B received the A2A message" <<<"$SEND_A_TO_B"; then
-	echo "peer-a -> peer-b response did not include expected peer B text" >&2
+echo "delegating peer-a -> peer-b with bounded wait and durable ledger"
+DELEGATE_A_TO_B="$(a2a_cli "$REGISTRY_A" delegate peer-b "run the tmux A2A smoke from peer A" --role background-worker --cwd "$ROOT_DIR" --wait --tasks "$TASKS_A" --max-wait-ms 30000 --interval-ms 250 --timeout-ms 3000)"
+echo "$DELEGATE_A_TO_B"
+if ! grep -q "tmux peer B received the A2A message" <<<"$DELEGATE_A_TO_B"; then
+	echo "peer-a -> peer-b delegation response did not include expected peer B text" >&2
	exit 1
fi

+echo "checking fleet and delegated task views"
+FLEET_A="$(a2a_cli "$REGISTRY_A" fleet --json --tasks "$TASKS_A" --timeout-ms 3000)"
+echo "$FLEET_A"
+if ! grep -q '"status": "online"' <<<"$FLEET_A"; then
+	echo "fleet output did not show peer-b online" >&2
+	exit 1
+fi
+if ! grep -q '"lastTask"' <<<"$FLEET_A"; then
+	echo "fleet output did not include the delegated task summary" >&2
+	exit 1
+fi
+TASKS_A_OUT="$(a2a_cli "$REGISTRY_A" tasks --json --tasks "$TASKS_A")"
+echo "$TASKS_A_OUT"
+if ! grep -q '"state": "TASK_STATE_COMPLETED"' <<<"$TASKS_A_OUT"; then
+	echo "task ledger did not record completed delegation" >&2
+	exit 1
+fi
+
echo "sending peer-b -> peer-a, then verifying explicit wait"
SEND_B_TO_A="$(a2a_cli "$REGISTRY_B" send peer-a "hello from tmux peer B" --timeout-ms 3000)"
echo "$SEND_B_TO_A"

diff --git a/src/cli-tui/commands/a2a-handlers.ts b/src/cli-tui/commands/a2a-handlers.ts
--- a/src/cli-tui/commands/a2a-handlers.ts
+++ b/src/cli-tui/commands/a2a-handlers.ts
@@ -1,9 +1,15 @@
import { parseA2AArgs } from "../../cli/commands/a2a.js";
+import { inspectA2AFleet } from "../../platform/a2a-fleet.js";
import { decodeA2APeerPairingCode } from "../../platform/a2a-peer-pairing.js";
import {
	listA2APeers,
	upsertA2APeerFromPairingPayload,
} from "../../platform/a2a-peer-registry.js";
+import {
+	getA2ATaskLedgerPath,
+	listA2ATaskEntries,
+	loadA2ATaskLedger,
+} from "../../platform/a2a-task-ledger.js";
import type { CommandExecutionContext } from "./types.js";

export interface A2ACommandHandlerDeps {
@@ -18,7 +24,9 @@
	const parsed = parseA2AArgs(splitCommandArgs(context.argumentText));
	const subcommand = parsed.positionals.shift()?.toLowerCase() ?? "help";
	if (subcommand === "peers" || subcommand === "list") {
-		const { path, registry } = await listA2APeers();
+		const { path, registry } = await listA2APeers({
+			path: stringFlag(parsed.flags, "--registry"),
+		});
		const entries = Object.entries(registry.peers).sort(([left], [right]) =>
			left.localeCompare(right),
		);
@@ -43,6 +51,50 @@
		deps.requestRender();
		return;
	}
+	if (subcommand === "fleet") {
+		const fleet = await inspectA2AFleet({
+			registryPath: stringFlag(parsed.flags, "--registry"),
+			tasksPath: stringFlag(parsed.flags, "--tasks"),
+		});
+		deps.addContent(
+			[
+				`A2A fleet (${fleet.registryPath})`,
+				fleet.peers.length === 0
+					? "No peers registered. Use /a2a accept <pairing-code>."
+					: fleet.peers
+							.map((peer) => {
+								const last = peer.lastTask
+									? ` last=${peer.lastTask.id} ${peer.lastTask.state}`
+									: "";
+								return `${peer.status} ${peer.name} ${peer.url}${last}`;
+							})
+							.join("\n"),
+			].join("\n"),
+		);
+		deps.requestRender();
+		return;
+	}
+	if (subcommand === "tasks") {
+		const peer = parsed.positionals.shift();
+		const tasksPath = stringFlag(parsed.flags, "--tasks");
+		const ledger = await loadA2ATaskLedger({ path: tasksPath });
+		const entries = listA2ATaskEntries(ledger, { peer });
+		deps.addContent(
+			[
+				`A2A tasks (${getA2ATaskLedgerPath(tasksPath)})`,
+				entries.length === 0
+					? "No delegated tasks recorded yet."
+					: entries
+							.map(
+								(entry) =>
+									`${entry.peer} ${entry.taskId} ${entry.state} ${entry.text}`,
+							)
+							.join("\n"),
+			].join("\n"),
+		);
+		deps.requestRender();
+		return;
+	}
	if (subcommand === "accept") {
		const code = parsed.positionals.shift();
		if (!code) {
@@ -55,6 +107,7 @@
			makeDefault: parsed.flags.get("--default") === true,
			tokenEnv: stringFlag(parsed.flags, "--token-env"),
			tokenFile: stringFlag(parsed.flags, "--token-file"),
+			path: stringFlag(parsed.flags, "--registry"),
		});
		context.showInfo(`Registered A2A peer ${result.name}.`);
		deps.addContent(
@@ -76,10 +129,19 @@
		);
		return;
	}
+	if (subcommand === "delegate") {
+		context.showInfo(
+			"Use `maestro a2a delegate <peer> <text> --wait` for native A2A delegation while the TUI task panel is being wired.",
+		);
+		return;
+	}
	deps.addContent(
		[
			"/a2a accept <pairing-code> [--name <peer>] [--default] [--token-env ENV]",
+			"/a2a fleet",
			"/a2a peers",
+			"/a2a delegate <peer> <text>",
+			"/a2a tasks [peer]",
			"/a2a send <peer> <text>",
		].join("\n"),
	);

diff --git a/src/cli-tui/commands/command-catalog.ts b/src/cli-tui/commands/command-catalog.ts
--- a/src/cli-tui/commands/command-catalog.ts
+++ b/src/cli-tui/commands/command-catalog.ts
@@ -179,11 +179,14 @@
		tags: ["session", "automation"],
	}),
	withArgs("a2a", "a2a", {
-		description: "Pair and inspect A2A peer agents",
-		usage: "/a2a [accept <code>|peers|send <peer> <text>]",
+		description: "Pair, inspect, and delegate to A2A peer agents",
+		usage: "/a2a [accept <code>|fleet|peers|tasks|delegate <peer> <text>]",
		tags: ["tools", "agents"],
		examples: [
+			"/a2a fleet",
			"/a2a peers",
+			"/a2a tasks",
+			"/a2a delegate mac-mini run workspace smoke",
			"/a2a accept maestro-pair-v1.payload.checksum --name mac-mini",
		],
	}),

diff --git a/src/cli/commands/a2a.ts b/src/cli/commands/a2a.ts
--- a/src/cli/commands/a2a.ts
+++ b/src/cli/commands/a2a.ts
@@ -9,6 +9,7 @@
	getA2ATask,
	sendA2AMessage,
} from "../../platform/a2a-client.js";
+import { inspectA2AFleet } from "../../platform/a2a-fleet.js";
import {
	createA2APeerPairingPayload,
	createA2APeerPairingPayloadFromAgentCard,
@@ -21,27 +22,72 @@
	resolveA2APeer,
	upsertA2APeerFromPairingPayload,
} from "../../platform/a2a-peer-registry.js";
+import {
+	extractA2ATaskText,
+	getA2ATaskLedgerPath,
+	isTerminalA2AState,
+	listA2ATaskEntries,
+	loadA2ATaskLedger,
+	recordA2ATaskStart,
+	updateA2ATaskInLedger,
+} from "../../platform/a2a-task-ledger.js";
import { getEnvValue } from "../../platform/client.js";
+import { isAbortError } from "../../utils/abort-error.js";

const DEFAULT_WAIT_MS = 300_000;
const DEFAULT_WAIT_INTERVAL_MS = 5_000;
-const A2A_VALUE_FLAGS = new Set([
-	"--agent-card-url",
-	"--base-url",
-	"--interval-ms",
-	"--max-wait-ms",
-	"--name",
-	"--organization-id",
-	"--peer-id",
-	"--registry",
-	"--timeout-ms",
-	"--token-env",
-	"--token-file",
-	"--ttl-minutes",
-	"--url",
-	"--workspace-id",
-]);
-const A2A_BOOLEAN_FLAGS = new Set(["--default", "--wait"]);
+const A2A_VALUE_FLAGS_BY_SUBCOMMAND: Record<string, readonly string[]> = {
+	accept: [
+		"--name",
+		"--organization-id",
+		"--registry",
+		"--token-env",
+		"--token-file",
+		"--workspace-id",
+	],
+	card: ["--registry", "--timeout-ms"],
+	delegate: [
+		"--cwd",
+		"--interval-ms",
+		"--max-wait-ms",
+		"--registry",
+		"--role",
+		"--tasks",
+		"--timeout-ms",
+	],
+	fleet: ["--registry", "--tasks", "--timeout-ms"],
+	offer: [
+		"--agent-card-url",
+		"--base-url",
+		"--name",
+		"--peer-id",
+		"--ttl-minutes",
+		"--url",
+	],
+	peers: ["--registry"],
+	send: ["--interval-ms", "--max-wait-ms", "--registry", "--timeout-ms"],
+	tasks: ["--registry", "--tasks", "--timeout-ms"],
+	wait: [
+		"--interval-ms",
+		"--max-wait-ms",
+		"--registry",
+		"--tasks",
+		"--timeout-ms",
+	],
+};
+const A2A_BOOLEAN_FLAGS_BY_SUBCOMMAND: Record<string, readonly string[]> = {
+	accept: ["--default"],
+	delegate: ["--wait"],
+	fleet: ["--json"],
+	send: ["--wait"],
+	tasks: ["--json", "--refresh"],
+};
+const A2A_LEADING_VALUE_FLAGS = new Set(
+	Object.values(A2A_VALUE_FLAGS_BY_SUBCOMMAND).flat(),
+);
+const A2A_LEADING_BOOLEAN_FLAGS = new Set(
+	Object.values(A2A_BOOLEAN_FLAGS_BY_SUBCOMMAND).flat(),
+);

export interface ParsedA2AArgs {
	positionals: string[];
@@ -64,12 +110,22 @@
		case "list":
			await handleA2APeers(parsed);
			return;
+		case "fleet":
+			await handleA2AFleet(parsed);
+			return;
		case "card":
			await handleA2ACard(parsed);
			return;
		case "send":
			await handleA2ASend(parsed);
			return;
+		case "delegate":
+		case "delegation":
+			await handleA2ADelegate(parsed);
+			return;
+		case "tasks":
+			await handleA2ATasks(parsed);
+			return;
		case "wait":
			await handleA2AWait(parsed);
			return;
@@ -81,6 +137,15 @@
export function parseA2AArgs(args: string[]): ParsedA2AArgs {
	const flags = new Map<string, string | boolean>();
	const positionals: string[] = [];
+	const subcommandIndex = findA2ASubcommandIndex(args);
+	const subcommand =
+		subcommandIndex >= 0
+			? canonicalA2ASubcommand(args[subcommandIndex])
+			: "help";
+	const valueFlags = new Set(A2A_VALUE_FLAGS_BY_SUBCOMMAND[subcommand] ?? []);
+	const booleanFlags = new Set(
+		A2A_BOOLEAN_FLAGS_BY_SUBCOMMAND[subcommand] ?? [],
+	);
	for (let index = 0; index < args.length; index++) {
		const arg = args[index];
		if (!arg) continue;
@@ -93,7 +158,19 @@
			if (!flag) {
				continue;
			}
-			if (!A2A_VALUE_FLAGS.has(flag) && !A2A_BOOLEAN_FLAGS.has(flag)) {
+			if (
+				index < subcommandIndex &&
+				!valueFlags.has(flag) &&
+				!booleanFlags.has(flag) &&
+				(A2A_LEADING_VALUE_FLAGS.has(flag) ||
+					A2A_LEADING_BOOLEAN_FLAGS.has(flag))
+			) {
+				if (A2A_LEADING_VALUE_FLAGS.has(flag) && inlineValue === undefined) {
+					index++;
+				}
+				continue;
+			}
+			if (!valueFlags.has(flag) && !booleanFlags.has(flag)) {
				positionals.push(arg);
				continue;
			}
@@ -101,7 +178,7 @@
				flags.set(flag, inlineValue);
				continue;
			}
-			if (A2A_BOOLEAN_FLAGS.has(flag)) {
+			if (booleanFlags.has(flag)) {
				flags.set(flag, true);
				continue;
			}
@@ -119,6 +196,45 @@
	return { flags, positionals };
}

+function findA2ASubcommandIndex(args: readonly string[]): number {
+	for (let index = 0; index < args.length; index++) {
+		const arg = args[index];
+		if (!arg || arg === "--") {
+			break;
+		}
+		if (!arg.startsWith("--")) {
+			return index;
+		}
+		const [flag = "", inlineValue] = arg.split("=", 2);
+		if (A2A_LEADING_VALUE_FLAGS.has(flag) && inlineValue === undefined) {
+			index++;
+			continue;
+		}
+		if (
+			A2A_LEADING_VALUE_FLAGS.has(flag) ||
+			A2A_LEADING_BOOLEAN_FLAGS.has(flag)
+		) {
+			continue;
+		}
+		break;
+	}
+	return -1;
+}
+
+function canonicalA2ASubcommand(input: string | undefined): string {
+	switch (input?.toLowerCase()) {
+		case "pair":
+		case "create":
+			return "offer";
+		case "list":
+			return "peers";
+		case "delegation":
+			return "delegate";
+		default:
+			return input?.toLowerCase() ?? "help";
+	}
+}
+
async function handleA2AOffer(parsed: ParsedA2AArgs): Promise<void> {
	const baseUrl =
		stringFlag(parsed, "--url") ?? stringFlag(parsed, "--base-url");
@@ -239,6 +355,56 @@
	console.log(JSON.stringify(card, null, 2));
}

+async function handleA2AFleet(parsed: ParsedA2AArgs): Promise<void> {
+	const fleet = await inspectA2AFleet({
+		registryPath: stringFlag(parsed, "--registry"),
+		tasksPath: stringFlag(parsed, "--tasks"),
+		timeoutMs: numberFlag(parsed, "--timeout-ms"),
+	});
+	if (booleanFlag(parsed, "--json")) {
+		console.log(JSON.stringify(fleet, null, 2));
+		return;
+	}
+	console.log(`A2A fleet (${fleet.registryPath})`);
+	if (fleet.peers.length === 0) {
+		console.log(
+			chalk.dim("  No peers registered. Run maestro a2a accept <code>."),
+		);
+		return;
+	}
+	for (const peer of fleet.peers) {
+		const status =
+			peer.status === "online" ? chalk.green("online") : chalk.yellow("down");
+		const label = peer.displayName
+			? `${peer.name} (${peer.displayName})`
+			: peer.name;
+		console.log(`${status} ${chalk.bold(label)} ${chalk.dim(peer.url)}`);
+		if (peer.model || peer.cwd || peer.auth) {
+			console.log(
+				chalk.dim(
+					`  ${[
+						peer.model ? `model=${peer.model}` : undefined,
+						peer.cwd ? `cwd=${peer.cwd}` : undefined,
+						peer.auth ? `auth=${peer.auth}` : undefined,
+					]
+						.filter(Boolean)
+						.join(" ")}`,
+				),
+			);
+		}
+		if (peer.lastTask) {
+			console.log(
+				chalk.dim(
+					`  last=${peer.lastTask.id} ${peer.lastTask.state} ${peer.lastTask.text}`,
+				),
+			);
+		}
+		if (peer.error) {
+			console.log(chalk.dim(`  error=${peer.error}`));
+		}
+	}
+}
+
async function handleA2ASend(parsed: ParsedA2AArgs): Promise<void> {
	const peerName =
		parsed.positionals.shift() ?? fail("Usage: maestro a2a send <peer> <text>");
@@ -269,6 +435,122 @@
	printTask(task);
}

+async function handleA2ADelegate(parsed: ParsedA2AArgs): Promise<void> {
+	const peerName =
+		parsed.positionals.shift() ??
+		fail("Usage: maestro a2a delegate <peer> <text>");
+	const text = parsed.positionals.join(" ").trim();
+	if (!text) {
+		fail("Usage: maestro a2a delegate <peer> <text>");
+	}
+	const peer = await resolveA2APeer(peerName, {
+		path: stringFlag(parsed, "--registry"),
+		timeoutMs: numberFlag(parsed, "--timeout-ms"),
+	});
+	const wait = booleanFlag(parsed, "--wait");
+	const role = stringFlag(parsed, "--role");
+	const cwd = stringFlag(parsed, "--cwd") ?? process.cwd();
+	const messageId = `maestro-a2a-message-${randomUUID()}`;
+	const contextId = `maestro-a2a-context-${randomUUID()}`;
+	const sent = await sendA2AMessage(peer.config, {
+		message: buildA2AUserMessage({
+			messageId,
+			contextId,
+			text,
+			metadata: {
+				requestKind: "maestro-peer-delegation",
... diff truncated: showing 800 of 2486 lines

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit d6ac24b. Configure here.

Comment thread src/cli/commands/a2a.ts
Comment thread src/platform/a2a-fleet.ts
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2ec212c32b

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/platform/a2a-fleet.ts Outdated
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 31d975ea5b

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/cli-tui/commands/a2a-handlers.ts
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 98fbf2e179

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/platform/a2a-fleet.ts Outdated
@haasonsaas haasonsaas force-pushed the sync/public-release-mirror branch from adcba00 to 648caf2 Compare May 16, 2026 06:31
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 648caf287c

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/cli/commands/a2a.ts
Comment on lines +586 to +590
const peer = await resolveA2APeer(entry.peer, {
path: stringFlag(parsed, "--registry"),
timeoutMs: numberFlag(parsed, "--timeout-ms"),
});
const task = await getA2ATask(peer.config, entry.taskId);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Continue task refresh when a single peer fetch fails

a2a tasks --refresh currently aborts on the first unresolved/offline peer because resolveA2APeer/getA2ATask errors are not handled inside the loop. In mixed fleets (e.g., one host down or removed from the registry), this prevents refreshing and printing the rest of the ledger, even though other tasks are still retrievable. Refresh should be best-effort per entry (warn and continue) so one failing peer does not block the full task view.

Useful? React with 👍 / 👎.

@haasonsaas haasonsaas merged commit dd0e0be into main May 16, 2026
13 of 14 checks passed
@haasonsaas haasonsaas deleted the sync/public-release-mirror branch May 16, 2026 06:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant