Conformance status: a2a-cli against SPEC.md v0.2 — all tiers
Audited commit: b741dc3 (main, 2026-08-28) · Audit date: 2026-09-01
Specification: SPEC.md v0.2, rev 2026-08-17 (Review — pre-Proposed)
Registry: COMPLIANCE.md
Status snapshot, not a conformance claim. Graded against main only, excluding open PRs #23 and #24. No TCK-conformant third-party agent was exercised, so per the Conformance model nothing here may be published as a tier claim. §5 shows what #23 already changes.
1. Goals
- Establish a baseline. Give a per-requirement, evidence-linked map of where the implementation stands against every tier of v0.2 — not just Tier 1.
- Separate "not built" from "built differently." A missing feature and a feature that diverges from its contract need different fixes; conflating them hides both.
- Flag registry drift. Where
COMPLIANCE.md describes a surface SPEC.md has since changed, record it — a stale row makes a conformant tool read as failing.
- Produce a re-runnable artifact. Machine-readable YAML keyed by requirement ID, so this can be regenerated as work lands instead of re-argued.
This document measures; it does not plan. Remediation ordering, effort estimates and issue-filing live outside it.
2. Scope
In scope: all three capability tiers of SPEC.md v0.2 — Tier 1 Core, Tier 2 Standard, Tier 3 Advanced.
Explicitly out of scope for this report:
| Excluded |
Requirement IDs |
Count |
Why |
| Authentication |
all A2ACLI_AUTH_* (Tier 1: 001–004 · Tier 2: 005–007 · Tier 3: 008–011) |
11 |
Handled separately |
| Agent Skills |
A2ACLI_SKILL_001, A2ACLI_SKILL_002 |
2 |
Tracked in #4 |
Exclusion rule: by requirement area, not by subject matter. A2ACLI_CARD_GET_003 (authenticated extended card) stays in scope because its area is CARD_GET; its auth dependency is noted in its row.
Total in scope: 53 of 66 requirements.
One in-scope item already has an issue and is reported here only for completeness: CARD_GET_005 (catalog resolution) → #19.
Disclosure. This audit was produced by the author of SPEC.md, measuring an implementation written largely by another maintainer. The specification is pre-Proposed (§15.1) and still moving — PR #23 amends it while this is being written — so a gap is at least as likely to mean the spec moved as the code is behind. §4.4 records three cases where the fault is on the specification side. Read the numbers as a drift map between two halves of the same project, not as a grade.
3. Summary
Graded fail-closed per Appendix A.3: only pass satisfies; partial and fail both block a tier.
| Tier |
In scope |
✅ Pass |
◐ Partial |
❌ Fail |
Pass rate |
Satisfied? |
| Tier 1 — Core |
33 |
16 |
10 |
7 |
48% |
No |
| Tier 2 — Standard |
12 |
6 |
1 |
5 |
50% |
No |
| Tier 3 — Advanced |
8 |
2 |
1 |
5 |
25% |
No |
| Total |
53 |
24 |
12 |
17 |
45% |
No |
The shape of the gap. Every A2A operation in Appendix A has a command behind it, three transports are wired, and go build ./... / go test ./... are green. What is missing is the layer the specification exists to standardise — the machine contract: deterministic exit codes, the JSON error envelope, blocking-wait-by-default, and task get --wait. Those four account for most of Tier 1's shortfall and are what let a script, a CI job, or a coding harness drive the tool unattended (§2.3, §6.1, §6.6).
Counting commands gives ~80% done. Counting requirements gives 43%. The difference is entirely the output and error contract — and for a CLI whose stated audience is programs, that contract is the product.
4. Details
Companion documents:
| Document |
Contents |
report-partial-features.md |
The 12 partial items, with the specific gap in each |
compliance-partial.yaml |
Same 12, machine-readable, keyed by requirement ID |
report-failed-features.md |
The 18 missing/failed items, split into unbuilt vs built-but-non-conformant |
compliance-failed.yaml |
Same 18, machine-readable |
4.1 Passing (24)
Tier 1 (16). CARD_GET_001 · SEND_001 · SEND_003 · SEND_005 · TASK_GET_001 · TASK_CANCEL_001 · INTERACT_001 · INTERACT_003 · INTERACT_005 · OUT_001 · OUT_006 · TX_001 · TX_002 · TX_003 · CONFIG_001 · CLI_001
Tier 2 (6). TASK_LIST_001 · TASK_SUBSCRIBE_001 · TX_004 · CONFIG_002 · CLI_002 · PUSH_001
Tier 3 (2). TX_005 (gRPC) · DEMO_SERVER_001 (shipped as server --echo|--exec|--proxy, exceeding the requirement)
Notable strengths: card resolution handles all four reference forms; v0.3↔v1.0 compatibility is wired on both client and server; message parts are repeatable and order-preserving with correct --media-type binding; configuration precedence is complete and tested. TX_003 passes by delegation — the SDK's tenantTransportDecorator (a2aclient/factory.go:167, v2.5.0) applies the selected interface's declared tenant when the request carries none, and omits it when the interface declares none. The CLI's non-spec --tenant global can override a declared value, which is worth watching but is not a §13.1 breach on its own.
Caveat — four of these have no automated test. task cancel, task list, task subscribe, and config show output are covered by no test case, so "pass" rests on a code read (plus, for config show, a manual run). TASK_CANCEL_001's idempotency requirement in particular is unverified. Strictly these are closer to not_measured than pass; they are recorded as pass because the code path is unambiguous, and flagged here rather than silently upgraded.
4.2 Partial (12) — built, but diverging from contract
| Tier |
ID |
One-line gap |
| 1 |
SEND_002 |
--async works; the blocking-wait default it opts out of does not exist |
| 1 |
SEND_004 |
Nothing dropped, but a file part shows [file: url], not name + media type + size |
| 1 |
SEND_006 |
Ordering correct; --data-part - treats - as a literal, not stdin |
| 1 |
INTERACT_002 |
Rejection surfaces correctly, but cannot "point at --debug" — no such flag |
| 1 |
INTERACT_004 |
Ids printed; no resume command, no stop-and-report on interruption |
| 1 |
TASK_POLL_003 |
Stops on INPUT_REQUIRED but not AUTH_REQUIRED |
| 1 |
TASK_POLL_004 |
Falls back only on unsupported-capability, not on mid-stream failure |
| 1 |
OUT_002 |
One document ✅, but a bare Task instead of the SendMessageResponse wrapper |
| 1 |
OUT_005 |
Labels stable, no ANSI; artifacts are indented lines, not §11.2 blocks |
| 1 |
EXIT_002 |
Exit 0 correct; no stderr warning on a FAILED/REJECTED outcome |
| 2 |
VER_003 |
Capability checked for streaming; not for push or extended card |
| 3 |
CARD_GET_003 |
--extended works; the public card's declared schemes are never consulted first |
4.3 Missing or failed (17)
Built but non-conformant (4) — code exists and does the wrong thing:
| Tier |
ID |
Defect |
| 1 |
EXIT_001 |
Exit codes collapse to 0/1; 2 required, 3/4/5 reserved |
| 1 |
OUT_003 |
-o json --stream pretty-prints instead of emitting JSONL |
| 1 |
OUT_004 |
Failures print Error: %v; no envelope, zero A2ACLI_ERR_* codes in the tree |
| 1 |
DEFAULT_001 |
4 of 6 baseline defaults shipped; blocked by SEND_002 + VER_001 |
Not built (13) — feature absent:
| Tier |
ID |
Missing |
| 1 |
TASK_POLL_001 |
task get --wait |
| 1 |
TASK_POLL_002 |
--poll-interval / wait --timeout, bounded backoff |
| 1 |
VER_001 |
--a2a-version; A2A-Version never signalled → servers assume legacy 0.3 |
| 2 |
CARD_GET_002 |
card get --validate |
| 2 |
TASK_SUBSCRIBE_002 |
Stream resumption after disconnect |
| 2 |
DOWNLOAD_001 |
task download — a file artifact can be named but never retrieved |
| 2 |
OUT_007 |
--debug with raw protocol logging |
| 2 |
CONFORM_001 |
conformance TCK smoke-check |
| 3 |
CARD_GET_004 |
Agent Card signature verification |
| 3 |
CARD_GET_005 |
Catalog / registry resolution (tracked in #19) |
| 3 |
PUSH_002 |
Local webhook receiver — push-config registers a URL the CLI cannot host |
| 3 |
CHAT_001 |
Interactive chat |
| 3 |
VER_002 |
Declare server-required protocol extensions |
4.4 Registry drift — COMPLIANCE.md lags SPEC.md
Three requirement rows describe a command surface SPEC.md has since changed. SPEC.md is authoritative for behavior (COMPLIANCE.md, About this document), and in each case the implementation already tracks it — so these are stale registry entries, not code defects and not genuine contradictions.
They matter for one practical reason: a row that names a flag which no longer exists reads as a failure the tool does not have. Anyone grading against the registry verbatim would mark SEND_006 failed for the wrong reason.
| # |
Registry row |
Says |
SPEC.md says |
Code follows |
| 1 |
A2ACLI_SEND_006 (L143) |
--text / --file / --data |
§10.2: --text-part / --file-part / --data-part |
SPEC ✅ |
| 2 |
A2ACLI_CLI_001 (L173) |
"-v/--version prints the tool version" |
§7.2: -v, --verbose; --version takes no short form |
SPEC ✅ |
| 3 |
A2ACLI_DEMO_SERVER_001 (L224) |
demo-server |
§7.1 table: server |
SPEC ✅ |
Fix: resync the three rows to SPEC.md. Renumbering and rewording are still permitted while the specification is pre-Proposed (COMPLIANCE.md, Requirement identifiers); from the first Proposed version onward they freeze, so this is worth clearing before then. Row 3 keeps its DEMO_SERVER identifier regardless — identifiers are never reused, and tier membership is not encoded in them.
One stale sentence inside SPEC.md itself. §7.1's Naming paragraph still lists demo-server among the bare-verb commands while its own table two lines above says server. That is the source the registry row was written from, so fix it in the same pass.
One genuine specification gap (not drift — neither document is stale, the question is simply unanswered): §6.5 mandates wait-by-default, but A2A permits a direct Message reply that creates no task. SPEC.md should state that the wait is a no-op in that case, so implementers do not poll a nonexistent taskId.
Keeping it from recurring. All three rows drifted because SPEC.md was edited without a matching registry pass. Resyncing COMPLIANCE.md in the same change that touches SPEC.md — and, once the tool matures, generating the report from the registry rather than by hand — would close the loop. Specific concerns about in-flight spec edits belong on the PR making them, not here.
5. Effect of open PR #23
PR #23 feat: cli spec reconciliation part 2 (branch yarolegovich/cli-spec-reconcile-2) is unmerged but already improves five in-scope rows — three of them to a full pass, verified from its diff:
| ID |
main |
With #23 |
Change |
OUT_003 |
❌ |
✅ |
Compact encoder per event + --pretty opt-out |
OUT_002 |
◐ |
✅ |
PrintSendResult wraps in StreamResponse |
INTERACT_004 |
◐ |
✅ |
formatResumeHint() appended |
VER_001 |
❌ |
◐ |
--a2a-version added, gates 0.3 vs 1.0 transports |
DEFAULT_001 |
❌ |
❌ |
Version row fixed; wait-by-default still missing |
It also fixes two §7.2 flag divergences (--polling-interval → --poll-interval at 2s; --svc-param accepts :) — note the rename ships no alias, so existing scripts using --polling-interval will break.
It amends SPEC.md too: adds --pretty to §7.2 and §11.3, and removes three normative clauses from §13.2 — the 1.0 negotiation floor, the "versions earlier than 1.0 MUST require an explicit opt-in" rule, and the independent MUST to "surface a version-unsupported error clearly rather than silently downgrading". Raised on the PR itself rather than argued here.
Projected after merge: 27 ✅ / 11 ◐ / 15 ❌ — 51% pass. Nothing in #23 touches exit codes, the error envelope, the wait engine, or polling.go, so every remaining item in §4.2 and §4.3 survives it.
6. Method and reproduction
Verified by execution: go build ./... (exit 0) and go test ./... — 43 test functions and 127 subtests, 170 passing test cases, 0 failing, across 2,630 lines of test code. Plus live runs of the built binary against the CLI's own a2a server --echo: exit codes, error shape, JSONL framing, --async, and config show redaction were observed, not inferred.
Test coverage is narrower than "all green" suggests. The module has 9 packages; 4 carry no test file at all — internal/output, internal/utils, internal/testutil, and the root main. internal/output is the notable one: it implements the entire OUT_* surface, including two requirements graded pass here, with no test of its own.
Verified by code reading only: requirements needing an agent this repo cannot produce — INPUT_REQUIRED/AUTH_REQUIRED interruption, mid-stream failure, a taskless Message reply, a signed card.
git clone https://github.com/a2aproject/a2a-cli && cd a2a-cli
git checkout b741dc33b0506008e438e11dbd3c602e3abe1a92
go build ./... && go test ./...
go build -o /tmp/a2a .
/tmp/a2a server --echo --port 18923 --quiet &
$ /tmp/a2a send --bogus-flag x >/dev/null 2>&1; echo $?
1 # §11.6 requires 2
$ /tmp/a2a card get https://nonexistent.invalid.example -o json
Error: failed to resolve agent card: … # §11.4 requires the Appendix B envelope
$ /tmp/a2a send -a 127.0.0.1:18923 -o json --stream "hello"
{
"task": { # §11.3 requires one object per physical line
…
A green test suite is not evidence of conformance here. TestSendStreaming decodes with json.NewDecoder, which accepts multi-line JSON — so the OUT_003 violation passes CI today. Recommend a spec_compliance_test.go keyed by requirement ID so COMPLIANCE.md rows map 1:1 to test names and this report becomes generated rather than hand-graded.
Appendix A — Machine-readable detail
Per-requirement evidence, file:line references and remediation notes. Schema: COMPLIANCE.md Appendix A.4; status vocabulary per A.2.
compliance-partial.yaml — the 12 partial rows
# a2a-cli — PARTIAL requirements against SPEC.md v0.2
#
# Schema: COMPLIANCE.md Appendix A.4. Status vocabulary (A.2):
# pass | partial | fail | not_measured | na | withdrawn
# Every status other than `pass` carries a note (A.2).
#
# Scope: EXCLUDES Authentication (A2ACLI_AUTH_*) and Agent Skills (A2ACLI_SKILL_*).
# Exclusion is by requirement AREA, so A2ACLI_CARD_GET_003 remains in scope.
#
# This file lists ONLY the 12 `partial` rows. Companion: compliance-failed.yaml.
# Grading is a status snapshot from a code read plus live runs against the CLI's
# own echo server -- NOT a conformance claim (no TCK-conformant agent exercised).
report:
tool: a2a-cli
tool_version: dev
language: Go
repository: https://github.com/a2aproject/a2a-cli
commit: b741dc33b0506008e438e11dbd3c602e3abe1a92
report_date: 2026-09-01
spec_version: "0.2"
spec_revision: 2026-08-17
a2a_versions: ["1.0"]
agent_tested: "local `a2a server --echo` only; no third-party agent"
agent_tck_conformant: "no"
transports_covered: [HTTP+JSON]
tier_claimed: none
excluded_areas: [AUTH, SKILL]
subset: partial
summary:
in_scope_total: 53
tier_satisfied: {tier_1: false, tier_2: false, tier_3: false}
partial_total: 12
by_tier:
tier_1: 10
tier_2: 1
tier_3: 1
requirements:
A2ACLI_SEND_002:
tier: 1
area: SEND
spec: "§10.2, §6.5"
requirement: "Blocking by default; --async returns identifiers immediately"
status: partial
note: >-
--async is implemented and sets Config.ReturnImmediately. The blocking-wait
default it opts out of does not exist: the non-stream path issues a single
SendMessage with a nil Config and never polls to a terminal or interrupted
state. No --wait, and no --return-immediately / --no-wait aliases.
evidence: "internal/cli/send.go:95-105 (one-shot path), :144-152 (buildRequest)"
remediation: "Route the default through the shared waiter added for TASK_POLL_001."
A2ACLI_SEND_004:
tier: 1
area: SEND
spec: "§10.2"
requirement: "Renders produced artifacts; never silently discards a part"
status: partial
note: >-
partsText renders text, data, file and raw parts, so nothing is silently
dropped. But a file part is rendered as "[file: <url>]" or
"[binary N bytes]" rather than reporting name, media type and size.
evidence: "internal/output/output.go:293-324 partsText; :241-250 artifact loop"
remediation: "Emit Filename, MediaType and byte length for file parts."
A2ACLI_SEND_006:
tier: 1
area: SEND
spec: "§10.2"
requirement: "Part flags repeatable and order-preserving; --data-part - reads stdin"
status: partial
note: >-
Ordering and repeatability are correct and covered by TestPartsParse.
'--data-part -' does not read stdin: RawOrInline falls through to treating
a non-file value as an inline literal, so '-' becomes the string "-".
evidence: "internal/flagparse/parts.go:45-77 (ordering); :148-156 RawOrInline"
remediation: "Special-case '-' to io.ReadAll(os.Stdin) before the file probe."
A2ACLI_INTERACT_002:
tier: 1
area: INTERACT
spec: "§8.1"
requirement: >-
Continue a task via --task-id, with or without --context-id; a rejected
identifier fails non-zero, creates no new task, and points at --debug
status: partial
note: >-
The identifier is sent against the task and a server rejection surfaces as
a non-zero exit with no phantom task created. The tool cannot satisfy the
final clause -- it has no --debug flag to point the caller at.
evidence: "internal/cli/send.go:194-196"
remediation: "Blocked on OUT_007 (--debug); then add the pointer to the error hint."
A2ACLI_INTERACT_004:
tier: 1
area: INTERACT
spec: "§8.2"
requirement: >-
Reports taskId / contextId / state on completion and interruption, in
copy-pasteable form, and prints the exact command to resume
status: partial
note: >-
Identifiers and state are printed in a stable, copy-pasteable form. No
resume command is emitted, and there is no stop-and-report behaviour on an
interrupted state.
evidence: "internal/output/output.go:226-239 formatTask"
remediation: "Resolved by PR #23, which adds formatResumeHint()."
fixed_in_pr: 23
A2ACLI_TASK_POLL_003:
tier: 1
area: TASK_POLL
spec: "§9.1, §9.3"
requirement: >-
Stops immediately on interrupted states (INPUT_REQUIRED / AUTH_REQUIRED);
treats TASK_STATE_UNSPECIFIED as neither terminal nor interrupted
status: partial
note: >-
The loop tests `Terminal() || InputRequired` in both the guard and the
event branch. AUTH_REQUIRED is missing, so a task pausing for
authentication is polled until the deadline instead of returning to the
caller. UNSPECIFIED handling is incidentally correct but untested.
evidence: "internal/polling/polling.go:62 (loop guard), :82 (event branch)"
remediation: "Add TaskStateAuthRequired to both conditions; add the sibling test."
A2ACLI_TASK_POLL_004:
tier: 1
area: TASK_POLL
spec: "§9.3"
requirement: "When a wait prefers streaming, falls back to polling on stream failure"
status: partial
note: >-
Fallback triggers only on a2a.ErrUnsupportedOperation (capability absent).
A mid-stream transport failure aborts the run instead of degrading to
polling, which §9.3 requires. Also inherits the AUTH_REQUIRED gap above.
evidence: "internal/cli/send.go:73-93 (fallback), :80-82 (error class check)"
remediation: "Broaden the fallback predicate to any stream-establishment or mid-stream error."
A2ACLI_OUT_002:
tier: 1
area: OUT
spec: "§11.3, App. B"
requirement: >-
-o json without --stream emits exactly one document, the terminal protocol
object, never implicitly switched to its streamed JSONL form
status: partial
note: >-
Cardinality is correct -- exactly one document. The shape is not:
PrintSendResult prints the oneof MEMBER (a bare Task) rather than the
SendMessageResponse wrapper Appendix B specifies. The same command under
--stream DOES wrap, so one command emits two incompatible top-level shapes
and a consumer must sniff for `id` vs `messageId`.
evidence: "internal/output/output.go:117-131 PrintSendResult"
remediation: "Resolved by PR #23, which wraps in a2a.StreamResponse{Event: result}."
fixed_in_pr: 23
A2ACLI_OUT_005:
tier: 1
area: OUT
spec: "§11.2, §6.1"
requirement: >-
text floor -- one Label: value per line, stable labels, no control
sequences; block content under its own Label: line closed by a blank line
status: partial
note: >-
Labels are stable across invocations and no terminal control sequences are
emitted. Block content is not implemented: rendered artifacts and history
appear as indented lines beneath a heading rather than as §11.2 blocks
closed by a blank line, so a reader cannot skip a block whole.
evidence: "internal/output/output.go:241-261 (artifact and history loops)"
remediation: "Emit each artifact under its own label line, terminated by a blank line."
A2ACLI_EXIT_002:
tier: 1
area: EXIT
spec: "§6.6, §11.6"
requirement: >-
Tool execution and agent outcome stay decoupled; a non-success or paused
outcome SHOULD be named in a stderr warning
status: partial
note: >-
The decoupling holds -- a conducted and reported turn exits 0 regardless of
the agent's verdict, verified against the local echo server. The stderr
warning naming a FAILED / REJECTED / INPUT_REQUIRED outcome is absent; a
tier claim promotes that SHOULD to required.
evidence: "internal/cli/root.go:71-82; internal/cli/send.go:102-105"
remediation: "Warn on stderr when the terminal state is not COMPLETED."
A2ACLI_VER_003:
tier: 2
area: VER
spec: "§13.3"
requirement: >-
Verifies a capability on the Agent Card before invoking a capability-gated
operation (streaming, push notifications, extended card)
status: partial
note: >-
Done for streaming: handleStreaming checks card.Capabilities.Streaming and
returns ErrUnsupportedOperation. No equivalent guard before push-config
operations or before fetching the extended card.
evidence: "internal/cli/send.go:157-159"
remediation: "Apply the same guard in task_push_config_*.go and card_get.go."
A2ACLI_CARD_GET_003:
tier: 3
area: CARD_GET
spec: "§10.1, §12.4"
requirement: >-
Authenticated extended Agent Card, fetched only via a security scheme
advertised on the public card
status: partial
note: >-
--extended fetches the extended card via client.GetExtendedAgentCard and
renders it. The qualifier is unmet: the public card's declared
securitySchemes are never consulted before a credential is presented.
Retained in scope because its area is CARD_GET, though the unmet half is
authentication-related.
evidence: "internal/cli/card_get.go:64 (flag), :68-81 getExtendedAgentCard"
remediation: "Read securitySchemes from the public card and gate the fetch on a match."
compliance-failed.yaml — the 17 failed rows
# a2a-cli — FAILED / MISSING requirements against SPEC.md v0.2
#
# Schema: COMPLIANCE.md Appendix A.4. Status vocabulary (A.2):
# pass | partial | fail | not_measured | na | withdrawn
# Every status other than `pass` carries a note (A.2).
#
# Scope: EXCLUDES Authentication (A2ACLI_AUTH_*) and Agent Skills (A2ACLI_SKILL_*).
# Exclusion is by requirement AREA.
#
# This file lists ONLY the 18 `fail` rows. Companion: compliance-partial.yaml.
# `kind` is a local annotation, not part of the COMPLIANCE.md schema:
# non_conformant -> code exists and behaves against the requirement
# not_built -> the feature is absent
report:
tool: a2a-cli
tool_version: dev
language: Go
repository: https://github.com/a2aproject/a2a-cli
commit: b741dc33b0506008e438e11dbd3c602e3abe1a92
report_date: 2026-09-01
spec_version: "0.2"
spec_revision: 2026-08-17
a2a_versions: ["1.0"]
agent_tested: "local `a2a server --echo` only; no third-party agent"
agent_tck_conformant: "no"
transports_covered: [HTTP+JSON]
tier_claimed: none
excluded_areas: [AUTH, SKILL]
subset: failed
summary:
in_scope_total: 53
tier_satisfied: {tier_1: false, tier_2: false, tier_3: false}
failed_total: 17
by_kind:
non_conformant: 4
not_built: 13
by_tier:
tier_1: 7
tier_2: 5
tier_3: 5
requirements:
# ─────────── Built but non-conformant (5) ───────────
A2ACLI_EXIT_001:
tier: 1
area: EXIT
spec: "§11.6, App. D"
kind: non_conformant
requirement: "Implements exit statuses 0, 1 and 2; reserved statuses carry documented meanings"
status: fail
note: >-
Execute() returns only 0 or 1. An unknown flag exits 1 where §11.6 requires
2; an unresolvable card exits 1 where 3 is reserved. Verified by running the
built binary. A caller cannot distinguish failure classes without parsing
stderr prose -- the exact outcome §11.6 exists to prevent.
evidence: "internal/cli/root.go:71-82"
remediation: "Return a typed error carrying an exit status; map per App. D."
A2ACLI_OUT_004:
tier: 1
area: OUT
spec: "§11.4, App. B, App. D"
kind: non_conformant
requirement: >-
Errors are machine-readable and consistent across transports; protocol
failures carry the A2A error name, CLI-local failures an A2ACLI_ERR_* code
status: fail
note: >-
Every failure renders as "Error: %v" on stderr in both -o text and -o json.
No error envelope, no hint field, and the string A2ACLI_ERR_ appears zero
times in the source tree.
evidence: "internal/cli/root.go:77-80"
remediation: >-
Add internal/clierr with the eight App. D codes; classify at call sites;
render {error:{code,message,hint,a2aCode}} on stdout under -o json.
A2ACLI_OUT_003:
tier: 1
area: OUT
spec: "§11.3, §11.4, App. B"
kind: non_conformant
requirement: >-
-o json --stream emits JSONL: one complete JSON object per line, flushed as
produced, final line carrying the terminal object
status: fail
note: >-
PrintJSON calls enc.SetIndent("", " ") unconditionally, so each streamed
event spans roughly 20 physical lines. Verified by running the binary
against the local echo server. A stream-terminating error is also not
emitted as a final error line.
evidence: "internal/output/output.go:60-65 PrintJSON, via PrintEvent :86"
remediation: "Resolved by PR #23: compact encoder per event plus a --pretty opt-out."
fixed_in_pr: 23
A2ACLI_DEFAULT_001:
tier: 1
area: DEFAULT
spec: "§6.5"
kind: non_conformant
requirement: "Ships the baseline defaults, each overridable, and exposes them via --help"
status: fail
note: >-
Four of six rows ship: transport server-preference order, text output,
concise detail, TLS verification on (gRPC only). Missing: wait-on-completion
(SEND_002) and protocol version (VER_001). App. A.3 rule 3 forbids a pass
while any breakdown row is not shipped-and-overridable.
evidence: "internal/cli/root.go:114-124"
remediation: "Unblocked by SEND_002 and VER_001."
# ─────────── Not built (13) ───────────
A2ACLI_TASK_POLL_001:
tier: 1
area: TASK_POLL
spec: "§9.3"
kind: not_built
requirement: "Polling path available -- one-shot task get, plus task get --wait"
status: fail
note: >-
task get registers only --history. §7.1 states polling is not a separate
command but is task get --wait. The polling engine exists yet is reachable
only as a send --stream fallback, so a job started with --async cannot be
waited on at all.
evidence: "internal/cli/task_get.go:64; internal/polling/polling.go:32 (used only at send.go:86)"
remediation: "Expose the shared waiter as task get --wait."
A2ACLI_TASK_POLL_002:
tier: 1
area: TASK_POLL
spec: "§9.3"
kind: not_built
requirement: "--poll-interval and --timeout honored; bounded backoff; no busy-loop; interruptible"
status: fail
note: >-
No --poll-interval: the flag is --polling-interval, send-local, defaulting
to 5s against the RECOMMENDED 2s. The loop uses a fixed time.After with no
backoff. The global --timeout is a per-request deadline, not a wait
deadline, and no A2ACLI_ERR_TIMEOUT is reported on expiry.
evidence: "internal/cli/send.go:116; internal/polling/polling.go:63-68"
remediation: "Add both flags to the shared waiter with exponential backoff and a wait deadline."
A2ACLI_VER_001:
tier: 1
area: VER
spec: "§13.2"
kind: not_built
requirement: >-
A2A-Version signaled explicitly on every request; negotiates only within
1.x, never below 1.0; no silent downgrade
status: fail
note: >-
No --a2a-version flag and no version service parameter set by the CLI. A2A
§3.6.2 makes an empty value mean 0.3, so every request is silently treated
as legacy by a conformant server. The only occurrence of the name in the
tree is an env-mapping test for a flag that does not exist.
evidence: "internal/clicfg/binder_test.go:34 (only occurrence)"
remediation: >-
Partly resolved by PR #23, which adds --a2a-version and gates 0.3 vs 1.0
transport registration; confirm the header is emitted on every request.
fixed_in_pr: 23
A2ACLI_CARD_GET_002:
tier: 2
area: CARD_GET
spec: "§10.1"
kind: not_built
requirement: "card get --validate -- validate the Agent Card against the A2A schema"
status: fail
note: "Flag absent; card get registers only --extended."
evidence: "internal/cli/card_get.go:64"
A2ACLI_TASK_SUBSCRIBE_002:
tier: 2
area: TASK_SUBSCRIBE
spec: "§9.4, §9.2"
kind: not_built
requirement: "Stream resumption after disconnect; the first event re-delivers the full Task"
status: fail
note: >-
No reconnect loop. WithInactivityTimeout cancels the context on silence
rather than resubscribing, so a dropped connection ends the run.
evidence: "internal/cli/task_subscribe.go:26-58; internal/utils/timeout.go:28-60"
A2ACLI_DOWNLOAD_001:
tier: 2
area: DOWNLOAD
spec: "§7.1"
kind: not_built
requirement: "task download -- save task artifacts to disk"
status: fail
note: >-
Not implemented. Combined with SEND_004, a file artifact can be named but
never retrieved, which makes file-producing agents unusable end to end.
A2ACLI_OUT_007:
tier: 2
area: OUT
spec: "§7.2"
kind: not_built
requirement: "--debug enables diagnostic logging to stderr, including raw protocol messages"
status: fail
note: >-
No --debug flag. --verbose emits a handful of '#'-prefixed lines, not wire
traffic. Also blocks INTERACT_002, which must point callers at --debug.
evidence: "internal/cli/root.go:64-68 (logf), :122 (--verbose)"
A2ACLI_CONFORM_001:
tier: 2
area: CONFORM
spec: "§7.1"
kind: not_built
requirement: "conformance -- smoke-check a live agent against the A2A TCK"
status: fail
note: "Not implemented; no conformance command in the tree."
A2ACLI_CARD_GET_004:
tier: 3
area: CARD_GET
spec: "§10.1"
kind: not_built
requirement: >-
Agent Card signature verification per A2A §8.4.3; reports verified /
unverifiable / absent and never presents an unverified card as verified
status: fail
note: "Not implemented. No JWS or RFC 8785 canonicalization path exists."
A2ACLI_CARD_GET_005:
tier: 3
area: CARD_GET
spec: "§10.1"
kind: not_built
requirement: "Catalog / registry resolution wherever --agent-card takes a reference"
status: fail
note: "Not implemented. ALREADY TRACKED in issue #19 -- do not re-file."
tracked_in_issue: 19
A2ACLI_PUSH_002:
tier: 3
area: PUSH
spec: "§9.2"
kind: not_built
requirement: "Local webhook receiver able to accept push notifications"
status: fail
note: >-
Not implemented. task push-config can register a callback URL the CLI
itself cannot host, so the loop cannot be closed locally.
A2ACLI_CHAT_001:
tier: 3
area: CHAT
spec: "§8.1, §7.1"
kind: not_built
requirement: "Interactive chat carrying contextId and active taskId across turns"
status: fail
note: >-
Not implemented. Note §6.1 and §11.2 require any such mode to be gated by
terminal detection and to auto-degrade to text when stdout is not a TTY.
A2ACLI_VER_002:
tier: 3
area: VER
spec: "§13.3, A2A §4.6"
kind: not_built
requirement: "Declares server-required protocol extensions the tool supports"
status: fail
note: "Not implemented; the CLI cannot talk to an agent that requires an extension."
# Required by COMPLIANCE.md A.3 rule 3: DEFAULT_001 cannot pass
# unless every row below is shipped: yes AND overridable: yes.
default_001_breakdown:
transport_server_preference_order: {shipped: yes, overridable: yes}
task_completion_wait_by_default: {shipped: no, overridable: no, note: "SEND_002"}
output_human_readable_text: {shipped: yes, overridable: yes}
detail_level_concise: {shipped: yes, overridable: yes}
protocol_version_highest_mutual: {shipped: no, overridable: no, note: "VER_001"}
transport_security_tls_on: {shipped: yes, overridable: yes, note: "--insecure covers gRPC only"}
Conformance status:
a2a-cliagainstSPEC.mdv0.2 — all tiersAudited commit:
b741dc3(main, 2026-08-28) · Audit date: 2026-09-01Specification:
SPEC.mdv0.2, rev 2026-08-17 (Review — pre-Proposed)Registry:
COMPLIANCE.md1. Goals
COMPLIANCE.mddescribes a surfaceSPEC.mdhas since changed, record it — a stale row makes a conformant tool read as failing.This document measures; it does not plan. Remediation ordering, effort estimates and issue-filing live outside it.
2. Scope
In scope: all three capability tiers of
SPEC.mdv0.2 — Tier 1 Core, Tier 2 Standard, Tier 3 Advanced.Explicitly out of scope for this report:
A2ACLI_AUTH_*(Tier 1: 001–004 · Tier 2: 005–007 · Tier 3: 008–011)A2ACLI_SKILL_001,A2ACLI_SKILL_002Exclusion rule: by requirement area, not by subject matter.
A2ACLI_CARD_GET_003(authenticated extended card) stays in scope because its area isCARD_GET; its auth dependency is noted in its row.Total in scope: 53 of 66 requirements.
One in-scope item already has an issue and is reported here only for completeness:
CARD_GET_005(catalog resolution) → #19.3. Summary
Graded fail-closed per Appendix A.3: only
passsatisfies;partialandfailboth block a tier.The shape of the gap. Every A2A operation in Appendix A has a command behind it, three transports are wired, and
go build ./.../go test ./...are green. What is missing is the layer the specification exists to standardise — the machine contract: deterministic exit codes, the JSON error envelope, blocking-wait-by-default, andtask get --wait. Those four account for most of Tier 1's shortfall and are what let a script, a CI job, or a coding harness drive the tool unattended (§2.3, §6.1, §6.6).Counting commands gives ~80% done. Counting requirements gives 43%. The difference is entirely the output and error contract — and for a CLI whose stated audience is programs, that contract is the product.
4. Details
Companion documents:
report-partial-features.mdcompliance-partial.yamlreport-failed-features.mdcompliance-failed.yaml4.1 Passing (24)
Tier 1 (16).
CARD_GET_001·SEND_001·SEND_003·SEND_005·TASK_GET_001·TASK_CANCEL_001·INTERACT_001·INTERACT_003·INTERACT_005·OUT_001·OUT_006·TX_001·TX_002·TX_003·CONFIG_001·CLI_001Tier 2 (6).
TASK_LIST_001·TASK_SUBSCRIBE_001·TX_004·CONFIG_002·CLI_002·PUSH_001Tier 3 (2).
TX_005(gRPC) ·DEMO_SERVER_001(shipped asserver --echo|--exec|--proxy, exceeding the requirement)Notable strengths: card resolution handles all four reference forms; v0.3↔v1.0 compatibility is wired on both client and server; message parts are repeatable and order-preserving with correct
--media-typebinding; configuration precedence is complete and tested.TX_003passes by delegation — the SDK'stenantTransportDecorator(a2aclient/factory.go:167, v2.5.0) applies the selected interface's declaredtenantwhen the request carries none, and omits it when the interface declares none. The CLI's non-spec--tenantglobal can override a declared value, which is worth watching but is not a §13.1 breach on its own.Caveat — four of these have no automated test.
task cancel,task list,task subscribe, andconfig showoutput are covered by no test case, so "pass" rests on a code read (plus, forconfig show, a manual run).TASK_CANCEL_001's idempotency requirement in particular is unverified. Strictly these are closer tonot_measuredthanpass; they are recorded aspassbecause the code path is unambiguous, and flagged here rather than silently upgraded.4.2 Partial (12) — built, but diverging from contract
SEND_002--asyncworks; the blocking-wait default it opts out of does not existSEND_004[file: url], not name + media type + sizeSEND_006--data-part -treats-as a literal, not stdinINTERACT_002--debug" — no such flagINTERACT_004TASK_POLL_003INPUT_REQUIREDbut notAUTH_REQUIREDTASK_POLL_004OUT_002Taskinstead of theSendMessageResponsewrapperOUT_005EXIT_0020correct; no stderr warning on aFAILED/REJECTEDoutcomeVER_003CARD_GET_003--extendedworks; the public card's declared schemes are never consulted first4.3 Missing or failed (17)
Built but non-conformant (4) — code exists and does the wrong thing:
EXIT_0010/1;2required,3/4/5reservedOUT_003-o json --streampretty-prints instead of emitting JSONLOUT_004Error: %v; no envelope, zeroA2ACLI_ERR_*codes in the treeDEFAULT_001SEND_002+VER_001Not built (13) — feature absent:
TASK_POLL_001task get --waitTASK_POLL_002--poll-interval/ wait--timeout, bounded backoffVER_001--a2a-version;A2A-Versionnever signalled → servers assume legacy 0.3CARD_GET_002card get --validateTASK_SUBSCRIBE_002DOWNLOAD_001task download— a file artifact can be named but never retrievedOUT_007--debugwith raw protocol loggingCONFORM_001conformanceTCK smoke-checkCARD_GET_004CARD_GET_005PUSH_002push-configregisters a URL the CLI cannot hostCHAT_001chatVER_0024.4 Registry drift —
COMPLIANCE.mdlagsSPEC.mdThree requirement rows describe a command surface
SPEC.mdhas since changed.SPEC.mdis authoritative for behavior (COMPLIANCE.md, About this document), and in each case the implementation already tracks it — so these are stale registry entries, not code defects and not genuine contradictions.They matter for one practical reason: a row that names a flag which no longer exists reads as a failure the tool does not have. Anyone grading against the registry verbatim would mark
SEND_006failed for the wrong reason.SPEC.mdsaysA2ACLI_SEND_006(L143)--text/--file/--data--text-part/--file-part/--data-partA2ACLI_CLI_001(L173)-v/--versionprints the tool version"-v, --verbose;--versiontakes no short formA2ACLI_DEMO_SERVER_001(L224)demo-serverserverFix: resync the three rows to
SPEC.md. Renumbering and rewording are still permitted while the specification is pre-Proposed (COMPLIANCE.md, Requirement identifiers); from the first Proposed version onward they freeze, so this is worth clearing before then. Row 3 keeps itsDEMO_SERVERidentifier regardless — identifiers are never reused, and tier membership is not encoded in them.One stale sentence inside
SPEC.mditself. §7.1's Naming paragraph still listsdemo-serveramong the bare-verb commands while its own table two lines above saysserver. That is the source the registry row was written from, so fix it in the same pass.One genuine specification gap (not drift — neither document is stale, the question is simply unanswered): §6.5 mandates wait-by-default, but A2A permits a direct
Messagereply that creates no task.SPEC.mdshould state that the wait is a no-op in that case, so implementers do not poll a nonexistenttaskId.Keeping it from recurring. All three rows drifted because
SPEC.mdwas edited without a matching registry pass. ResyncingCOMPLIANCE.mdin the same change that touchesSPEC.md— and, once the tool matures, generating the report from the registry rather than by hand — would close the loop. Specific concerns about in-flight spec edits belong on the PR making them, not here.5. Effect of open PR #23
PR #23
feat: cli spec reconciliation part 2(branchyarolegovich/cli-spec-reconcile-2) is unmerged but already improves five in-scope rows — three of them to a full pass, verified from its diff:mainOUT_003--prettyopt-outOUT_002PrintSendResultwraps inStreamResponseINTERACT_004formatResumeHint()appendedVER_001--a2a-versionadded, gates 0.3 vs 1.0 transportsDEFAULT_001It also fixes two §7.2 flag divergences (
--polling-interval→--poll-intervalat 2s;--svc-paramaccepts:) — note the rename ships no alias, so existing scripts using--polling-intervalwill break.It amends
SPEC.mdtoo: adds--prettyto §7.2 and §11.3, and removes three normative clauses from §13.2 — the 1.0 negotiation floor, the "versions earlier than 1.0 MUST require an explicit opt-in" rule, and the independent MUST to "surface a version-unsupported error clearly rather than silently downgrading". Raised on the PR itself rather than argued here.Projected after merge: 27 ✅ / 11 ◐ / 15 ❌ — 51% pass. Nothing in #23 touches exit codes, the error envelope, the wait engine, or
polling.go, so every remaining item in §4.2 and §4.3 survives it.6. Method and reproduction
Verified by execution:
go build ./...(exit 0) andgo test ./...— 43 test functions and 127 subtests, 170 passing test cases, 0 failing, across 2,630 lines of test code. Plus live runs of the built binary against the CLI's owna2a server --echo: exit codes, error shape, JSONL framing,--async, andconfig showredaction were observed, not inferred.Test coverage is narrower than "all green" suggests. The module has 9 packages; 4 carry no test file at all —
internal/output,internal/utils,internal/testutil, and the rootmain.internal/outputis the notable one: it implements the entireOUT_*surface, including two requirements gradedpasshere, with no test of its own.Verified by code reading only: requirements needing an agent this repo cannot produce —
INPUT_REQUIRED/AUTH_REQUIREDinterruption, mid-stream failure, a tasklessMessagereply, a signed card.A green test suite is not evidence of conformance here.
TestSendStreamingdecodes withjson.NewDecoder, which accepts multi-line JSON — so theOUT_003violation passes CI today. Recommend aspec_compliance_test.gokeyed by requirement ID soCOMPLIANCE.mdrows map 1:1 to test names and this report becomes generated rather than hand-graded.Appendix A — Machine-readable detail
Per-requirement evidence,
file:linereferences and remediation notes. Schema:COMPLIANCE.mdAppendix A.4; status vocabulary per A.2.compliance-partial.yaml— the 12 partial rowscompliance-failed.yaml— the 17 failed rows