chore: sync public mirror from internal#420
Conversation
PR SummaryMedium Risk Overview Introduces Updates TypeScript TUI Reviewed by Cursor Bugbot for commit 648caf2. Bugbot is set up for automated code reviews on this repo. Configure here. |
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
There was a problem hiding this comment.
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
refreshA2ATaskLedgernow 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.
There was a problem hiding this comment.
💡 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".
There was a problem hiding this comment.
💡 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".
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Bugbot Autofix prepared fixes for both issues found in the latest run.
- ✅ Fixed: Fail-open catch swallows AbortError without rethrowing
persistA2ALedgerBestEffortnow rethrows aborts before logging fail-open ledger warnings.
- ✅ Fixed: Fleet peer inspect catch swallows AbortError silently
inspectPeernow rethrowsAbortErrorinstead 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.
There was a problem hiding this comment.
💡 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".
There was a problem hiding this comment.
💡 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".
There was a problem hiding this comment.
💡 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".
adcba00 to
648caf2
Compare
There was a problem hiding this comment.
💡 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".
| const peer = await resolveA2APeer(entry.peer, { | ||
| path: stringFlag(parsed, "--registry"), | ||
| timeoutMs: numberFlag(parsed, "--timeout-ms"), | ||
| }); | ||
| const task = await getA2ATask(peer.config, entry.taskId); |
There was a problem hiding this comment.
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 👍 / 👎.

Summary
evalops/maestro-internalevalops/maestroas a generated public mirror of the private source of truth070671068b00bf119ebcf83bffb8a0149447aa7db0bc8c532b6e802adb61715775a9e1d27a9a4aca17file(s) to copy/update and0stale file(s) to delete4Source-of-truth status
Public Mirror Drift Audit
@evalops/maestrohttps://github.com/evalops/maestro-internal@main (070671068b00)https://github.com/evalops/maestro@main (39c0c810c5a5)170public_projection_has_driftSample Changed Paths
Guidance
Let internal main generate and merge the public sync PR before relying on public main.
Drift sample
Public-only commits since last generated sync
Validation
sync-public-release-mirrorworkflow inpublic-treemodeTest Plan
sync-public-release-mirrorworkflow inpublic-treemoderequire-internal-prcheck confirms internal source PR lineageStaged Rollout
evalops/maestro-internal@070671068b00bf119ebcf83bffb8a0149447aa7d, including existing hidden/evaluation surfaces, and keeps public package parity behind the established public-source-provenance gate.Supersedes
070671068b00bf119ebcf83bffb8a0149447aa7d