@nelsonlongxiang/dsh-open-a2a-net 0.7.0 → 0.8.1-preview.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +109 -94
  3. package/README_zh.md +64 -66
  4. package/THIRD_PARTY_NOTICES.md +12 -0
  5. package/assets/nexusDist/assets/index-BuidhKVr.css +1 -0
  6. package/assets/nexusDist/assets/index-YtYrkDJi.js +9 -0
  7. package/assets/nexusDist/index.html +2 -2
  8. package/cli/a2a.mjs +349 -20
  9. package/cordis.patch.yml +167 -0
  10. package/docs/licenses/dsh-superagent-MIT.txt +21 -0
  11. package/lib/a2a-client.js +12 -4
  12. package/lib/card.js +2 -0
  13. package/lib/cli-deposit.js +75 -4
  14. package/lib/config.js +1 -2
  15. package/lib/direct-forward.js +23 -1
  16. package/lib/faces/admin.js +31 -48
  17. package/lib/faces/wire.js +110 -71
  18. package/lib/followup-admission.js +47 -0
  19. package/lib/followup-delivery.js +19 -2
  20. package/lib/index.js +618 -389
  21. package/lib/node-self.js +1 -1
  22. package/lib/outbound-scope.js +10 -0
  23. package/lib/peer-directory.js +3 -1
  24. package/lib/queue-guard.js +1 -1
  25. package/lib/receipt.js +24 -2
  26. package/lib/skill/SKILL.md +1 -0
  27. package/lib/skill/references/cli.md +193 -21
  28. package/lib/skill/references/collaboration.md +3 -2
  29. package/lib/skill/references/superagent.md +106 -0
  30. package/lib/superagent/a2a-watch.js +587 -0
  31. package/lib/superagent/contract.js +1 -0
  32. package/lib/superagent/host/a2a-source.js +251 -0
  33. package/lib/superagent/host/executor.js +520 -0
  34. package/lib/superagent/host/index.js +1107 -0
  35. package/lib/superagent/host/policy.js +308 -0
  36. package/lib/superagent/host/pr-follow.js +158 -0
  37. package/lib/superagent/host/review-requests.js +77 -0
  38. package/lib/superagent/host/role-tools.js +137 -0
  39. package/lib/superagent/host/store.js +136 -0
  40. package/lib/superagent/host/tools.js +297 -0
  41. package/lib/superagent/source-types.js +22 -0
  42. package/lib/superagent/upstream/dsh-superagent/src/core/assignments.js +160 -0
  43. package/lib/superagent/upstream/dsh-superagent/src/core/engine.js +244 -0
  44. package/lib/superagent/upstream/dsh-superagent/src/core/errors.js +63 -0
  45. package/lib/superagent/upstream/dsh-superagent/src/core/evals.js +381 -0
  46. package/lib/superagent/upstream/dsh-superagent/src/core/ids.js +5 -0
  47. package/lib/superagent/upstream/dsh-superagent/src/core/index.js +9 -0
  48. package/lib/superagent/upstream/dsh-superagent/src/core/intake.js +68 -0
  49. package/lib/superagent/upstream/dsh-superagent/src/core/memory.js +51 -0
  50. package/lib/superagent/upstream/dsh-superagent/src/core/model.js +79 -0
  51. package/lib/superagent/upstream/dsh-superagent/src/core/part.js +71 -0
  52. package/lib/superagent/upstream/dsh-superagent/src/core/ports.js +1 -0
  53. package/lib/superagent/upstream/dsh-superagent/src/core/reasons.js +35 -0
  54. package/lib/superagent/upstream/dsh-superagent/src/core/replies.js +94 -0
  55. package/lib/superagent/upstream/dsh-superagent/src/core/stage.js +27 -0
  56. package/lib/superagent/upstream/dsh-superagent/src/core/tickets.js +487 -0
  57. package/lib/superagent/upstream/snapshot-8c32a9d.js +62 -0
  58. package/lib/task-auth.js +21 -0
  59. package/lib/task-ledger-rows.js +4 -0
  60. package/lib/task-ledger.js +69 -3
  61. package/lib/tools/info.js +13 -7
  62. package/lib/types/a2a-client.d.ts +0 -7
  63. package/lib/types/cli-deposit.d.ts +65 -1
  64. package/lib/types/config.d.ts +6 -20
  65. package/lib/types/direct-forward.d.ts +14 -0
  66. package/lib/types/faces/admin.d.ts +5 -0
  67. package/lib/types/faces/wire.d.ts +20 -57
  68. package/lib/types/followup-admission.d.ts +13 -0
  69. package/lib/types/followup-delivery.d.ts +1 -1
  70. package/lib/types/index.d.ts +0 -2
  71. package/lib/types/outbound-scope.d.ts +1 -0
  72. package/lib/types/queue-guard.d.ts +2 -0
  73. package/lib/types/receipt.d.ts +21 -1
  74. package/lib/types/superagent/a2a-watch.d.ts +227 -0
  75. package/lib/types/superagent/contract.d.ts +36 -0
  76. package/lib/types/superagent/host/a2a-source.d.ts +90 -0
  77. package/lib/types/superagent/host/executor.d.ts +132 -0
  78. package/lib/types/superagent/host/index.d.ts +340 -0
  79. package/lib/types/superagent/host/policy.d.ts +128 -0
  80. package/lib/types/superagent/host/pr-follow.d.ts +51 -0
  81. package/lib/types/superagent/host/review-requests.d.ts +36 -0
  82. package/lib/types/superagent/host/role-tools.d.ts +85 -0
  83. package/lib/types/superagent/host/store.d.ts +61 -0
  84. package/lib/types/superagent/host/tools.d.ts +32 -0
  85. package/lib/types/superagent/source-types.d.ts +218 -0
  86. package/lib/types/superagent/upstream/dsh-superagent/src/core/assignments.d.ts +53 -0
  87. package/lib/types/superagent/upstream/dsh-superagent/src/core/engine.d.ts +191 -0
  88. package/lib/types/superagent/upstream/dsh-superagent/src/core/errors.d.ts +58 -0
  89. package/lib/types/superagent/upstream/dsh-superagent/src/core/evals.d.ts +99 -0
  90. package/lib/types/superagent/upstream/dsh-superagent/src/core/ids.d.ts +1 -0
  91. package/lib/types/superagent/upstream/dsh-superagent/src/core/index.d.ts +9 -0
  92. package/lib/types/superagent/upstream/dsh-superagent/src/core/intake.d.ts +15 -0
  93. package/lib/types/superagent/upstream/dsh-superagent/src/core/memory.d.ts +40 -0
  94. package/lib/types/superagent/upstream/dsh-superagent/src/core/model.d.ts +432 -0
  95. package/lib/types/superagent/upstream/dsh-superagent/src/core/part.d.ts +49 -0
  96. package/lib/types/superagent/upstream/dsh-superagent/src/core/ports.d.ts +217 -0
  97. package/lib/types/superagent/upstream/dsh-superagent/src/core/reasons.d.ts +36 -0
  98. package/lib/types/superagent/upstream/dsh-superagent/src/core/replies.d.ts +55 -0
  99. package/lib/types/superagent/upstream/dsh-superagent/src/core/stage.d.ts +18 -0
  100. package/lib/types/superagent/upstream/dsh-superagent/src/core/tickets.d.ts +151 -0
  101. package/lib/types/superagent/upstream/snapshot-8c32a9d.d.ts +317 -0
  102. package/lib/types/task-auth.d.ts +9 -0
  103. package/lib/types/task-ledger-rows.d.ts +21 -4
  104. package/lib/types/task-ledger.d.ts +9 -0
  105. package/lib/types/types.d.ts +7 -4
  106. package/nexus-stage/scripts/mockhost.py +2 -2
  107. package/nexus-stage/src/main.ts +27 -0
  108. package/nexus-stage/src/planning-card-text.ts +1 -1
  109. package/nexus-stage/src/planning-team-roles.ts +80 -0
  110. package/nexus-stage/src/planning-types.ts +2 -0
  111. package/nexus-stage/src/planning-view.ts +1 -0
  112. package/nexus-stage/src/planning.css +29 -9
  113. package/nexus-stage/src/wb-superagent.ts +518 -0
  114. package/nexus-stage/src/workbench.css +32 -0
  115. package/nexus-stage/src/workbench.ts +25 -3
  116. package/nexus-stage/src/world.ts +1 -1
  117. package/package.json +8 -2
  118. package/assets/nexusDist/assets/index-C7HagaGy.css +0 -1
  119. package/assets/nexusDist/assets/index-CA_YZTWG.js +0 -9
  120. package/assets/stageDist/assets/index-Bj7S2pA1.css +0 -1
  121. package/assets/stageDist/assets/index-C-28k_o5.js +0 -974
  122. package/assets/stageDist/assets/interiors-CQGBjffT.png +0 -0
  123. package/assets/stageDist/assets/office-tileset-CHQT-uYL.png +0 -0
  124. package/assets/stageDist/index.html +0 -13
  125. package/lib/legacy/teams-bridge-inbound.js +0 -156
  126. package/lib/teams-bridge.js +0 -18
  127. package/lib/types/legacy/teams-bridge-inbound.d.ts +0 -94
  128. package/lib/types/teams-bridge.d.ts +0 -112
package/cli/a2a.mjs CHANGED
@@ -23,7 +23,7 @@
23
23
  * Runs on the SDK subpaths only — no @deepseek-ai/* imports.
24
24
  */
25
25
  import { createHash, generateKeyPairSync, randomUUID, sign as edSign, createPrivateKey } from 'node:crypto'
26
- import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'node:fs'
26
+ import { appendFileSync, existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'node:fs'
27
27
  import { homedir } from 'node:os'
28
28
  import { basename, join } from 'node:path'
29
29
 
@@ -36,6 +36,11 @@ const IDENTITY_DIR = process.env.A2A_CLI_HOME !== undefined && process.env.A2A_C
36
36
  : join(homedir(), '.a2a')
37
37
  const IDENTITY = join(IDENTITY_DIR, 'cli.json')
38
38
  const INBOX_CURSOR = join(IDENTITY_DIR, 'inbox.cursor')
39
+ // superagent p1-context-read: this identity's side of its conversations (one JSON line per route the host took).
40
+ const SENT_LOG = join(IDENTITY_DIR, 'sent.jsonl')
41
+ const SENT_LOG_PREV = join(IDENTITY_DIR, 'sent.1.jsonl')
42
+ const SENT_LOG_CAP = 1024 * 1024
43
+ const SENT_TEXT_CAP = 4000
39
44
  const DEFAULT_HOME = 'http://127.0.0.1:3080'
40
45
  const DEFAULT_SKILL_ROOTS = [join(homedir(), '.agents', 'skills')]
41
46
 
@@ -43,11 +48,12 @@ const USAGE = `a2a-net CLI — aligned with the plugin's model tools ('a2a tools
43
48
 
44
49
  usage: a2a <command> [args] [--home URL] [--key <apiKey>] [--json]
45
50
 
46
- 户口制: init <name> [--uuid 16hex] [--force] · publish · inbox [--new|--wait|--hook] · revoke
51
+ 户口制: init <name> [--uuid 16hex] [--force] · publish [--owes] · inbox [--new|--wait|--hook] · thread <context_id> · receipt <task_id> <result…> · admission <caller> [--local] · revoke
47
52
  对齐面: whoami · teams [query] · route <team> <msg…> [--sync] [--context ID] [--task ID] · status · tasks · probe · report <msg…> [--task ID]
48
53
  CLI 专有: skills [name] [--json] [--root DIR] · tools (this alignment table) · help
49
54
  独立节点: serve [--config node.json] [--host 127.0.0.1|0.0.0.0] [--port N] [--home DIR] (the network layer without a DSH instance)
50
55
  文件通道: file send <path…> <team> [--note TEXT] · file fetch <ref> [--out DIR] (#91: bytes never ride a message)
56
+ superagent: superagent request <text…> [--thread ID] · superagent reevaluate <trigger id> · superagent state (owner credential: the team's workbench Source)
51
57
 
52
58
  host: --home > $A2A_HOME > the home saved by 'a2a init --home URL' > ${DEFAULT_HOME}
53
59
  key: --key > $A2A_KEY (prefer the env var: a --key value stays in shell history)
@@ -58,6 +64,10 @@ route: async by default (delivered + task id; the receipt comes to 'a2a inbox
58
64
  every route carries a task id born here; a transport failure is retried up to 3 times with
59
65
  the SAME id, and --task ID resends an earlier one — the host runs one task id at most once
60
66
  --json: teams / tasks / probe / route / report / inbox print the raw rows instead of the table
67
+ route --json adds class: sent | gone | retry (structured fields only; gone = resending the same task can never succeed)
68
+ route/receipt --json --exit-class: the exit code carries the class as well — 0 sent (or the same receipt
69
+ replayed), 4 gone, 5 the task already carries a different receipt (receipt only), 1 retry or any other failure;
70
+ without --exit-class every exit code stays as it was
61
71
 
62
72
  inbox: bare 'inbox' lists the whole mailbox (newest first, the host keeps 64; mail it pushed out is counted)
63
73
  --new only mail newer than the local cursor (~/.a2a/inbox.cursor), then advance it
@@ -65,11 +75,37 @@ inbox: bare 'inbox' lists the whole mailbox (newest first, the host keeps 64;
65
75
  --wait block until unread mail arrives, print it and exit 0; exit 3 on --timeout
66
76
  (default 30m; --interval polls, default 15s; --renew re-publishes an expiring deposit)
67
77
  --hook for a Claude Code hook: unread mail as hookSpecificOutput.additionalContext JSON,
68
- nothing at all when there is none or the host is unreachable (always exit 0)`
78
+ nothing at all when there is none or the host is unreachable (always exit 0)
79
+ --after C replayable read with a cursor the CALLER keeps ('start', then the 'next' it got back):
80
+ one JSON object {epoch, after, next, mail (oldest first), lost, epochChanged[, cursorAhead]};
81
+ never reads or moves ~/.a2a/inbox.cursor; lost is null when the host's store restarted
82
+
83
+ thread: thread <context_id> [--limit N] [--before C] one conversation, newest first, as JSON:
84
+ mailbox rows the host tagged with that context_id (dir "in") and this identity's routes the host
85
+ took in it (dir "out", from ~/.a2a/sent.jsonl, with the sync reply when there was one);
86
+ --limit 1-200 (default 20), --before takes the next_before a previous page returned;
87
+ read-only: never moves ~/.a2a/inbox.cursor; unscoped counts mailbox rows with no context_id
88
+
89
+ receipt: receipt <task_id> [--attention | --acknowledged | --outcome O] [--to TEAM] <one line…> the formal receipt for a task this identity got:
90
+ settles the caller's debt; the envelope carries disposition "result" (default) or "attention" (a question
91
+ for the caller — it settles the task but is not a completion; the answer comes back as a new task in the
92
+ same context). A result says how the work ended with --outcome: completed (default), failed, rejected
93
+ (declined, not taken up) or abandoned (stopped before it finished); an attention receipt takes no outcome.
94
+ The target is the mailbox row's sender for that task id unless --to names one; it rides
95
+ task id receipt-<task_id>, so a task gets one receipt (a second, different one is refused)
96
+ --acknowledged: the task is taken up and its result follows — the caller's row stays owed but leaves the
97
+ queue count and expires 7 days after the acknowledgement instead of 24 hours after dispatch; it rides
98
+ task id ack-<task_id>, so the later receipt is not refused
99
+
100
+ admission: admission <caller> [--local] [--json] how <caller> would be admitted delivering to THIS identity right
101
+ now (the home host recomputes it from its current links): {caller, admitted, rule}. --local asks for the
102
+ same-host face (a session on the home host). Signed with this identity's key; the host answers only the
103
+ identity itself, with the rule name and nothing more. Exit 0 once answered (admitted or not), 1 when the
104
+ host refuses or does not know the query`
69
105
 
70
106
  /** Flags that take a value (both '--home URL' and '--home=URL'); the rest are switches. */
71
- const VALUE_FLAGS = new Set(['--note', '--out', '--home', '--key', '--uuid', '--root', '--timeout', '--interval', '--context', '--task'])
72
- const SWITCHES = new Set(['--json', '--force', '--help', '-h', '--new', '--peek', '--wait', '--hook', '--renew', '--sync'])
107
+ const VALUE_FLAGS = new Set(['--note', '--out', '--home', '--key', '--uuid', '--root', '--timeout', '--interval', '--context', '--task', '--after', '--limit', '--before', '--to', '--outcome'])
108
+ const SWITCHES = new Set(['--json', '--force', '--help', '-h', '--new', '--peek', '--wait', '--hook', '--renew', '--sync', '--attention', '--acknowledged', '--owes', '--local', '--exit-class'])
73
109
 
74
110
  /** A hook must never break the session it runs in: in --hook mode every failure is silent. */
75
111
  const HOOK_MODE = process.argv.includes('--hook')
@@ -135,11 +171,13 @@ function identity() {
135
171
  return id
136
172
  }
137
173
 
174
+ /** An environment variable set to the empty string counts as unset (#490). */
175
+ const envValue = name => { const value = process.env[name]; return value !== undefined && value !== '' ? value : undefined }
138
176
  const home = () => {
139
177
  const saved = storedIdentity()?.home
140
- return String(flag('--home') ?? process.env.A2A_HOME ?? saved ?? DEFAULT_HOME).replace(/\/$/, '')
178
+ return String(flag('--home') ?? envValue('A2A_HOME') ?? saved ?? DEFAULT_HOME).replace(/\/$/, '')
141
179
  }
142
- const apiKey = () => flag('--key') ?? (process.env.A2A_KEY !== undefined && process.env.A2A_KEY !== '' ? process.env.A2A_KEY : undefined)
180
+ const apiKey = () => flag('--key') ?? envValue('A2A_KEY')
143
181
 
144
182
  /** The bound form's domain tag (src/origin-claim.ts ORIGIN_CLAIM_BOUND_TAG). */
145
183
  const ORIGIN_CLAIM_BOUND_TAG = 'a2a-origin-claim/2'
@@ -183,10 +221,7 @@ function originClaimFor(base) {
183
221
  }
184
222
 
185
223
  /** A host behind dsh-pocket also wants its session token, as the dsh_pocket_token cookie. */
186
- const pocketToken = () => {
187
- const token = process.env.A2A_POCKET_TOKEN ?? process.env.A2A_TOKEN
188
- return token !== undefined && token !== '' ? token : undefined
189
- }
224
+ const pocketToken = () => envValue('A2A_POCKET_TOKEN') ?? envValue('A2A_TOKEN')
190
225
 
191
226
  const keyHeaders = () => {
192
227
  const key = apiKey()
@@ -222,6 +257,15 @@ async function homeFetch(url, init) {
222
257
  return res
223
258
  }
224
259
 
260
+ /** The home host's zone, which names its process team (#503); `dsh` when the host does not say. */
261
+ async function homeZone() {
262
+ try {
263
+ const res = await homeFetch(`${home()}/__dsh_a2a/state`, { headers: keyHeaders() })
264
+ const zone = res.ok ? (await res.json())?.zone : undefined
265
+ return typeof zone === 'string' && zone !== '' ? zone : 'dsh'
266
+ } catch { return 'dsh' }
267
+ }
268
+
225
269
  /**
226
270
  * Read a JSON object reply. A non-JSON or non-object body (a proxy page, an
227
271
  * empty 405 from a front proxy, an old host) becomes an error object, and an
@@ -430,6 +474,210 @@ function unreadOf(box, cursor) {
430
474
  return { unread, lost: 0 }
431
475
  }
432
476
 
477
+ /**
478
+ * superagent p1-context-read: note a route the host took (class "sent") under its
479
+ * context_id, so `thread` can show this identity's side. The host names the context
480
+ * on every taken reply that carries one; a -32005 reply does not, so only an explicit
481
+ * --context covers it. Best effort: a journal failure never turns a send into an error.
482
+ */
483
+ function journalSent(body, out) {
484
+ const contextId = typeof out?.context_id === 'string' && out.context_id !== '' ? out.context_id : body.context_id
485
+ if (sendClass(out) !== 'sent' || typeof contextId !== 'string' || contextId === '') return
486
+ const cap = (text) => text.length > SENT_TEXT_CAP ? { text: text.slice(0, SENT_TEXT_CAP), truncated: true, chars: text.length } : { text }
487
+ const reply = typeof out.result?.text === 'string' ? cap(out.result.text) : undefined
488
+ const row = {
489
+ handle: body.caller_session, at: Date.now(), team: body.team, task_id: String(out.task_id ?? body.task_id), context_id: contextId,
490
+ ...cap(body.message), ...(reply !== undefined ? { reply: reply.text, ...(reply.truncated ? { reply_truncated: true, reply_chars: reply.chars } : {}) } : {}),
491
+ }
492
+ try {
493
+ mkdirSync(IDENTITY_DIR, { recursive: true })
494
+ // One older generation is kept; a rename is atomic, so a reader sees either file whole.
495
+ if ((statSync(SENT_LOG, { throwIfNoEntry: false })?.size ?? 0) > SENT_LOG_CAP) {
496
+ // Another process may have rolled it between the stat and here: then there is nothing left to roll.
497
+ try { renameSync(SENT_LOG, SENT_LOG_PREV) } catch (error) { if (error?.code !== 'ENOENT') throw error }
498
+ }
499
+ appendFileSync(SENT_LOG, `${JSON.stringify(row)}\n`, { mode: 0o600 })
500
+ } catch (error) {
501
+ console.error(`[route] sent journal not written (${String(error?.code ?? error)}) — the send itself stands; 'a2a thread' will miss it`)
502
+ }
503
+ }
504
+
505
+ /** This handle's journaled routes in one context, older generation first; unreadable lines are skipped. */
506
+ function sentIn(handle, contextId) {
507
+ const rows = []
508
+ for (const file of [SENT_LOG_PREV, SENT_LOG]) {
509
+ let lines = []
510
+ try { lines = readFileSync(file, 'utf8').split('\n') } catch { continue }
511
+ for (const line of lines) {
512
+ if (line === '') continue
513
+ try {
514
+ const r = JSON.parse(line)
515
+ if (r?.handle === handle && r.context_id === contextId && typeof r.at === 'number' && typeof r.task_id === 'string') rows.push(r)
516
+ } catch { /* a torn last line from a crashed write */ }
517
+ }
518
+ }
519
+ return rows
520
+ }
521
+
522
+ /** `thread` order, newest first: by time, then mailbox before journal, then seq / task id. */
523
+ const threadKey = (e) => e.dir === 'in' ? `${String(e.at)}:in:${String(e.seq)}` : `${String(e.at)}:out:${e.task_id}`
524
+ function threadBefore(a, b) {
525
+ if (a.at !== b.at) return a.at < b.at
526
+ if (a.dir !== b.dir) return a.dir === 'in'
527
+ return a.dir === 'in' ? a.seq < b.seq : a.task_id < b.task_id
528
+ }
529
+
530
+ async function cmdThread() {
531
+ const [contextId, extra] = positionals()
532
+ if (contextId === undefined || contextId === '' || extra !== undefined) usage('usage: a2a thread <context_id> [--limit N] [--before CURSOR]')
533
+ const limitRaw = flag('--limit')
534
+ const limit = limitRaw === undefined ? 20 : Number(limitRaw)
535
+ if (!Number.isInteger(limit) || limit < 1 || limit > 200 || (limitRaw !== undefined && !/^\d+$/.test(limitRaw))) usage(`--limit takes a whole number from 1 to 200 (got "${String(limitRaw)}")`)
536
+ const beforeRaw = flag('--before')
537
+ const m = beforeRaw === undefined ? null : /^(\d{1,15}):(in|out):(\S+)$/.exec(beforeRaw)
538
+ if (beforeRaw !== undefined && m === null) usage(`--before takes the next_before a previous 'a2a thread' page returned (got "${beforeRaw}")`)
539
+ const before = m === null ? undefined : { at: Number(m[1]), dir: m[2], ...(m[2] === 'in' ? { seq: Number(m[3]) } : { task_id: m[3] }) }
540
+ if (before?.dir === 'in' && !Number.isInteger(before.seq)) usage(`--before: an "in" cursor ends in a mailbox seq (got "${beforeRaw}")`)
541
+ const handle = identity().handle
542
+ const box = await fetchMail(handle)
543
+ const inbound = box.mail.filter(r => r?.context_id === contextId && typeof r.at === 'number').map(r => ({
544
+ dir: 'in', at: r.at, seq: Number.isInteger(r.seq) ? r.seq : 0, from: r.from, ...(typeof r.task_id === 'string' ? { task_id: r.task_id } : {}),
545
+ ...(typeof r.proof === 'string' ? { proof: r.proof } : {}), ...(typeof r.receipt_for === 'string' ? { receipt_for: r.receipt_for } : {}), text: r.text,
546
+ ...(r.truncated === true ? { truncated: true, chars: r.chars } : {}), ...(r.dup !== undefined ? { dup: r.dup } : {}),
547
+ }))
548
+ const outbound = sentIn(handle, contextId).map(({ handle: _h, context_id: _c, ...r }) => ({ dir: 'out', ...r }))
549
+ const all = [...inbound, ...outbound].filter(e => before === undefined || threadBefore(e, before)).sort((a, b) => threadBefore(a, b) ? 1 : threadBefore(b, a) ? -1 : 0)
550
+ const entries = all.slice(0, limit)
551
+ const more = all.length > limit
552
+ console.log(JSON.stringify({
553
+ handle, context_id: contextId, ...(typeof box.epoch === 'string' ? { epoch: box.epoch } : {}),
554
+ entries, more, next_before: more ? threadKey(entries.at(-1)) : null,
555
+ // Mailbox rows from before the host tagged context (or deliveries that named none) cannot be placed.
556
+ unscoped: box.mail.filter(r => typeof r?.context_id !== 'string').length,
557
+ // Mail the host's cap pushed out is gone from this read, whatever its context.
558
+ mail_lost: Number.isInteger(box.oldest_seq) ? Math.max(0, box.oldest_seq - 1) : null,
559
+ }, null, 2))
560
+ }
561
+
562
+ /** superagent p1-receipt-outcome (S26): how a result receipt says the work ended; the host-derived outcomes are not the sender's to claim. */
563
+ const SENDER_OUTCOMES = ['completed', 'failed', 'rejected', 'abandoned']
564
+
565
+ /**
566
+ * superagent p1-receipt-cmd: the one formal-receipt shape for a CLI identity —
567
+ * `[A2A receipt] task <id> <one line>` plus the v2 envelope line (src/receipt.ts
568
+ * formatReceipt key order). disposition says what comes back: the result, or a
569
+ * question needing the caller's attention; an attention receipt carries no
570
+ * outcome because it is not a completion. The receipt rides its own fixed task id
571
+ * `receipt-<id>`, so the host's idempotency holds a task to one receipt.
572
+ */
573
+ async function cmdReceipt() {
574
+ const [taskId, ...words] = positionals()
575
+ const summary = words.join(' ').replace(/\s+/g, ' ').trim()
576
+ if (taskId === undefined || summary === '') usage('usage: a2a receipt <task_id> [--attention | --acknowledged | --outcome O] [--to TEAM] <one line…>')
577
+ const outcome = flag('--outcome')
578
+ if (outcome !== undefined && has('--attention')) usage('receipt: --attention takes no --outcome — an attention receipt is a question, not an ending')
579
+ // superagent v4: an acknowledgement — the task is taken up and its result follows; the caller's row stays owed.
580
+ const acknowledged = has('--acknowledged')
581
+ if (acknowledged && (outcome !== undefined || has('--attention'))) usage('receipt: --acknowledged takes neither --outcome nor --attention — it is not an ending')
582
+ if (outcome !== undefined && !SENDER_OUTCOMES.includes(outcome)) usage(`receipt: --outcome takes ${SENDER_OUTCOMES.join(', ')} (got "${outcome}")`)
583
+ const receiptTask = `${acknowledged ? 'ack' : 'receipt'}-${taskId}`
584
+ if (!/^[A-Za-z0-9][A-Za-z0-9._:-]*$/.test(taskId)) usage(`receipt: task id "${taskId}" has characters a receipt's own task id cannot carry (letters, digits, . _ : -)`)
585
+ if (receiptTask.length > 80) usage(`receipt: task id "${taskId}" is too long — its receipt rides "${receiptTask}" (${String(receiptTask.length)} characters, at most 80); it is not cut short`)
586
+ const to = flag('--to')
587
+ if (to !== undefined && to === '') usage('--to takes the team the receipt goes to')
588
+ const handle = identity().handle
589
+ const box = await fetchMail(handle)
590
+ const rows = box.mail.filter(r => r?.task_id === taskId)
591
+ const senders = [...new Set(rows.map(r => r.from))]
592
+ if (to === undefined && senders.length !== 1) {
593
+ usage(senders.length === 0
594
+ ? `receipt: no mailbox row for task ${taskId} names its sender — pass --to <team>`
595
+ : `receipt: task ${taskId} came from more than one sender (${senders.join(', ')}) — pass --to <team>`)
596
+ }
597
+ const contextId = rows.find(r => typeof r.context_id === 'string')?.context_id
598
+ const envelope = acknowledged
599
+ ? { idempotencyKey: taskId, disposition: 'acknowledged' }
600
+ : has('--attention')
601
+ ? { idempotencyKey: taskId, disposition: 'attention' }
602
+ : { outcome: outcome ?? 'completed', idempotencyKey: taskId, disposition: 'result' }
603
+ await sendDirect('receipt', {
604
+ team: to ?? senders[0], message: `[A2A receipt] task ${taskId} ${summary}\n${JSON.stringify(envelope)}`,
605
+ caller_session: handle, wait: false, task_id: receiptTask, ...(contextId !== undefined ? { context_id: contextId } : {}),
606
+ })
607
+ }
608
+
609
+ /**
610
+ * superagent p1-mail-admission: ask the home host how <caller> would be admitted delivering to this identity now.
611
+ * The query is signed with this identity's key over ['a2a-admission', handle, caller, ingress, ts] (src/cli-deposit.ts
612
+ * admissionQueryPayload), so the host answers this identity only, never another one's links.
613
+ */
614
+ async function cmdAdmission() {
615
+ const [caller] = positionals()
616
+ if (caller === undefined || caller === '') usage('usage: a2a admission <caller> [--local] [--json]')
617
+ const id = identity()
618
+ const ingress = has('--local') ? 'local' : 'wire'
619
+ const ts = Date.now()
620
+ const sig = edSign(null, Buffer.from(JSON.stringify(['a2a-admission', id.handle, caller, ingress, ts])), createPrivateKey(id.privateKeyPem)).toString('base64')
621
+ const out = await control({ action: 'admission', handle: id.handle, caller, ingress, ts, sig })
622
+ if (out.ok !== true) die(`[admission] the host did not answer for ${id.handle} (refused, or a host before p1-mail-admission)`)
623
+ const row = { caller, admitted: out.admitted === true, ...(typeof out.rule === 'string' ? { rule: out.rule } : {}) }
624
+ if (has('--json')) { console.log(JSON.stringify(row)); return }
625
+ console.log(row.admitted ? `[admission] ${caller} → admitted (${String(row.rule)})` : `[admission] ${caller} → not admitted`)
626
+ }
627
+
628
+ /**
629
+ * superagent design v4 (PR-B2a): the owner's terminal face of the team's workbench Source. It rides the home's
630
+ * OWNER control credential ($A2A_KEY / pocket cookie), never a CLI identity. A request made this way is an ordinary
631
+ * message, not a person's (owner 2026-11-08): the router judges it against the team's rules, as an agent's.
632
+ */
633
+ async function cmdSuperagent() {
634
+ const [sub, ...words] = positionals()
635
+ const url = `${home()}/__dsh_a2a/superagent`
636
+ const answer = async (res) => {
637
+ if (res.status === 401 || res.status === 403) die(keyHint(res.status))
638
+ if (res.status === 404) die(`[superagent] ${home()} runs no superagent team (plugin config superagent: true), or predates it`)
639
+ return await replyOf(res)
640
+ }
641
+ if (sub === 'request') {
642
+ const text = words.join(' ')
643
+ if (text === '') usage('usage: a2a superagent request <text…> [--thread <workbench thread id>] [--json]')
644
+ const thread = flag('--thread')
645
+ const out = await answer(await homeFetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', ...keyHeaders() },
646
+ body: JSON.stringify({ action: 'request', text, via: 'cli', ...(thread !== undefined ? { thread } : {}) }) }))
647
+ if (has('--json')) { console.log(JSON.stringify(out)); if (out.ok !== true) process.exitCode = 1; return }
648
+ if (out.ok !== true) die(`[superagent] request refused: ${String(out.error)}`)
649
+ console.log(`[superagent] ${String(out.result)} · trigger ${String(out.triggerId)} · thread ${String(out.thread)}${out.result === 'filtered' ? ' (the workbench Source is off)' : ''}`)
650
+ return
651
+ }
652
+ if (sub === 'reevaluate') {
653
+ // Design superagent-reevaluate-note D3: the owner asks the router to judge a stored message again.
654
+ const [triggerId] = words
655
+ if (triggerId === undefined || triggerId === '') usage('usage: a2a superagent reevaluate <trigger id> [--json]')
656
+ const out = await answer(await homeFetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', ...keyHeaders() },
657
+ body: JSON.stringify({ action: 'reevaluate', trigger_id: triggerId }) }))
658
+ if (has('--json')) { console.log(JSON.stringify(out)); if (out.ok !== true) process.exitCode = 1; return }
659
+ if (out.ok !== true) die(`[superagent] reevaluate refused: ${String(out.error)}`)
660
+ console.log(out.queued === true ? `[superagent] ${triggerId} queued for a fresh judgement` : `[superagent] ${triggerId} is being judged right now; nothing queued`)
661
+ return
662
+ }
663
+ if (sub === 'state') {
664
+ const out = await answer(await homeFetch(url, { headers: keyHeaders() }))
665
+ if (has('--json')) { console.log(JSON.stringify(out)); return }
666
+ if (out.error !== undefined && !Array.isArray(out.tickets)) die(`[superagent] ${String(out.error)}`)
667
+ const settings = out.settings ?? {}
668
+ console.log(`[superagent] router ${settings.router ?? '(none)'} · accept mode ${String(settings.acceptMode)} · policy ${String(out.policyVersion)}`)
669
+ for (const t of out.tickets ?? []) {
670
+ const stage = t.ended ? `ended ${String(t.ended.outcome)}` : `${String(t.stage?.stage ?? '?')}${t.stage?.action ? ` / ${String(t.stage.action)}` : ''}`
671
+ console.log(` ${String(t.id)} ${stage.padEnd(22)} ${String(t.title)}`)
672
+ }
673
+ for (const th of out.threads ?? []) {
674
+ for (const r of th.replies ?? []) console.log(` ↳ ${String(th.ref?.id)} ${String(r.message?.kind)}: ${String(r.message?.text ?? '').slice(0, 120)}`)
675
+ }
676
+ return
677
+ }
678
+ usage('usage: a2a superagent request <text…> [--thread ID] [--json] · a2a superagent reevaluate <trigger id> [--json] · a2a superagent state [--json]')
679
+ }
680
+
433
681
  /** One mailbox read: the rows, plus the gh#113 seq bounds when the host has them. */
434
682
  async function fetchMail(handle) {
435
683
  const out = await control({ action: 'inbox', handle })
@@ -437,6 +685,34 @@ async function fetchMail(handle) {
437
685
  return out
438
686
  }
439
687
 
688
+ /**
689
+ * superagent p1-inbox-after: a read whose cursor the caller owns. The cursor is
690
+ * `<epoch>:<seq>` (or 'start'); it only ever comes back as `next`. Same epoch:
691
+ * rows past seq, and how many the cap pushed out before them. A new epoch (the
692
+ * host's store restarted): every row, `lost` unknown (null), never a guessed 0.
693
+ */
694
+ function afterRead(box, after) {
695
+ const start = after === 'start'
696
+ const m = start ? null : /^([^:\s]{1,128}):(\d{1,15})$/.exec(after)
697
+ if (!start && m === null) usage(`--after takes 'start' or the '<epoch>:<seq>' cursor a previous read returned (got "${after}")`)
698
+ const rows = [...box.mail].sort((a, b) => a.seq - b.seq)
699
+ const top = (fallback) => rows.length ? Math.max(fallback, rows.at(-1).seq) : fallback
700
+ if (start) {
701
+ const lost = Math.max(0, box.oldest_seq - 1)
702
+ return { mail: rows, lost, epochChanged: false, next: `${box.epoch}:${String(top(lost))}` }
703
+ }
704
+ if (m[1] !== box.epoch) return { mail: rows, lost: null, epochChanged: true, next: `${box.epoch}:${String(top(0))}` }
705
+ const seq = Number(m[2])
706
+ // A cursor past the newest seq this host ever issued was not handed out by it (forged or mixed up):
707
+ // flag it, keep it as given, never clamp it silently.
708
+ const issued = Number.isInteger(box.next_seq) ? box.next_seq - 1 : top(Math.max(0, box.oldest_seq - 1))
709
+ if (seq > issued) return { mail: [], lost: null, epochChanged: false, cursorAhead: true, next: after }
710
+ const mail = rows.filter(r => r.seq > seq)
711
+ const lost = Math.max(0, box.oldest_seq - seq - 1)
712
+ const floor = lost > 0 ? box.oldest_seq - 1 : seq
713
+ return { mail, lost, epochChanged: false, next: `${box.epoch}:${String(mail.length ? mail.at(-1).seq : floor)}` }
714
+ }
715
+
440
716
  /** The loss line: mail the host's 64-row cap pushed out before it was read. */
441
717
  const lostLine = (lost) => `${String(lost)} message${lost === 1 ? ' was' : 's were'} pushed out of the mailbox before ${lost === 1 ? 'it was' : 'they were'} read (the host keeps the newest 64)`
442
718
 
@@ -513,12 +789,25 @@ async function publishDeposit() {
513
789
  const signCard = await loadSignCard()
514
790
  const id = identity()
515
791
  const privateKey = createPrivateKey(id.privateKeyPem)
516
- const card = signCard({ name: id.name, session: `cli-${id.name}`, team: id.handle, capabilities: { route: true, async: true }, expiresAt: Date.now() + 2 * 24 * 60 * 60 * 1000 }, privateKey)
792
+ // superagent p1-mailbox-owes (S33): --owes declares, in the signed card, that this identity owes a receipt for what
793
+ // its mailbox takes; every publish (and --renew) states it afresh, so a publish without it withdraws the declaration.
794
+ const capabilities = { route: true, async: true, ...(has('--owes') ? { owes: true } : {}) }
795
+ const card = signCard({ name: id.name, session: `cli-${id.name}`, team: id.handle, capabilities, expiresAt: Date.now() + 2 * 24 * 60 * 60 * 1000 }, privateKey)
517
796
  return await control({ action: 'publish', card: JSON.stringify(card) })
518
797
  }
519
798
 
520
799
  async function cmdInbox() {
521
800
  const handle = identity().handle
801
+ const after = flag('--after')
802
+ if (after !== undefined) {
803
+ if (['--new', '--peek', '--wait', '--hook'].some(has)) usage('--after is its own cursor: it does not combine with --new, --peek, --wait or --hook')
804
+ const box = await fetchMail(handle)
805
+ if (!seqCapable(box)) die('[inbox] --after needs a host that numbers its mailbox (gh#113); upgrade the home host — no time-based fallback')
806
+ const read = afterRead(box, after)
807
+ if (read.cursorAhead) console.error(`[inbox] cursor ${after} is ahead of this host (newest seq ${String(Number.isInteger(box.next_seq) ? box.next_seq - 1 : '?')}): it was not issued here — check the consumer's stored cursor`)
808
+ console.log(JSON.stringify({ handle, epoch: box.epoch, after, ...read }, null, 2))
809
+ return
810
+ }
522
811
  if (has('--hook')) {
523
812
  const input = await hookInput()
524
813
  const cursor = readCursor(handle)
@@ -846,14 +1135,46 @@ async function cmdFileFetch() {
846
1135
  console.log(`[file fetch] ${path} (${String(bytes.byteLength)} bytes, sha256 ${ref.sha256})`)
847
1136
  }
848
1137
 
1138
+ /**
1139
+ * superagent p1-send-classes: one observation per --json route reply — 'sent',
1140
+ * 'gone' or 'retry' — from structured fields only (delivered, code,
1141
+ * routed/result, a replay's prior), never from error text. 'gone'
1142
+ * only where resending the same task can never succeed; anything unknown retries.
1143
+ */
1144
+ /**
1145
+ * superagent p3 G3 (design P3-7): with --exit-class the exit code carries the class, because a caller that runs the CLI
1146
+ * through a command runner may keep the exit code and lose stdout. Opt-in only; without it no exit code changes.
1147
+ */
1148
+ const CLASS_EXIT = { sent: 0, gone: 4, retry: 1 }
1149
+ const RECEIPT_CONFLICT_EXIT = 5
1150
+ const exitClass = () => has('--exit-class')
1151
+
1152
+ function sendClass(out, prior) {
1153
+ // A replay proves the host already holds this id: it landed, unless it already failed.
1154
+ if (prior !== undefined) return prior.status === 'failed' ? 'gone' : 'sent'
1155
+ if (out?.delivered === true || (out?.routed === true && out?.result !== undefined) || out?.code === -32005) return 'sent'
1156
+ if (out?.code === -32001 || out?.code === -32002) return 'gone'
1157
+ // acceptance:"unknown", 401/403, 5xx, -32003/-32007/-32008/-32009/-32011, generic -32000, anything unrecognised.
1158
+ return 'retry'
1159
+ }
1160
+
849
1161
  async function sendDirect(verb, body) {
1162
+ if (exitClass() && !has('--json')) usage(`${verb}: --exit-class needs --json (the class it exits with is the one --json prints)`)
850
1163
  const { res } = await postDirect(body)
851
1164
  // Exhausted 5xx resends: the outcome is unknown, not "not delivered" (the hint above names the safe resend).
852
1165
  if (res.status >= 500 && !has('--json')) die(`[${verb}] not confirmed (HTTP ${String(res.status)}) — the task may have landed; use the --task resend above`)
853
- if (res.status !== 409) return await printDelivery(verb, res)
1166
+ if (res.status !== 409) {
1167
+ const out = await replyOf(res)
1168
+ if (verb === 'route' || verb === 'receipt') journalSent(body, out)
1169
+ return printDelivery(verb, res, out)
1170
+ }
854
1171
  const out = await replyOf(res)
855
1172
  if (out.replay !== true) {
856
- if (has('--json')) console.log(JSON.stringify(out, null, 2))
1173
+ if (has('--json')) console.log(JSON.stringify({ ...out, class: sendClass(out) }, null, 2))
1174
+ // A task id already used for another message: a receipt says so with 5; a route exits with the class --json printed.
1175
+ if (exitClass()) process.exit(verb === 'receipt' ? RECEIPT_CONFLICT_EXIT : CLASS_EXIT[sendClass(out)])
1176
+ // p1-receipt-cmd: one receipt per task — a follow-up is a new task in the same context, never a second receipt.
1177
+ if (verb === 'receipt') die(`[receipt] task ${String(body.task_id)} already carried a different receipt — a task gets one receipt; send any follow-up as a new task in the same context (a2a route <team> … --context <id>)`)
857
1178
  die(`[${verb}] task ${String(body.task_id)} was already used for a different message (or with${body.wait === true ? 'out' : ''} --sync) — not sent; drop --task to send it as a new task`)
858
1179
  }
859
1180
  const prior = await queryOutcome(body)
@@ -862,7 +1183,8 @@ async function sendDirect(verb, body) {
862
1183
  // async delivery waits for its receipt; a host before the gh#112 fix also
863
1184
  // leaves refused async rounds pending).
864
1185
  if (has('--json')) {
865
- console.log(JSON.stringify({ ...out, received: true, ...(prior.status === 'completed' ? { delivered: true } : prior.status === 'failed' ? { delivered: false } : {}), prior }, null, 2))
1186
+ console.log(JSON.stringify({ ...out, received: true, ...(prior.status === 'completed' ? { delivered: true } : prior.status === 'failed' ? { delivered: false } : {}), prior, class: sendClass(out, prior) }, null, 2))
1187
+ if (exitClass()) { process.exitCode = CLASS_EXIT[sendClass(out, prior)]; return }
866
1188
  if (prior.status === 'failed' || (body.wait === true && prior.status !== 'completed')) process.exitCode = 1
867
1189
  return
868
1190
  }
@@ -882,15 +1204,18 @@ async function sendDirect(verb, body) {
882
1204
  }
883
1205
 
884
1206
  /** One readable line for a /a2a/direct reply; the raw reply stays one --json away. */
885
- async function printDelivery(verb, res) {
886
- const out = await replyOf(res)
1207
+ function printDelivery(verb, res, out) {
887
1208
  if (has('--json')) {
888
- console.log(JSON.stringify(out, null, 2))
1209
+ console.log(JSON.stringify({ ...out, class: sendClass(out) }, null, 2))
1210
+ if (exitClass()) { process.exitCode = CLASS_EXIT[sendClass(out)]; return }
889
1211
  if (out.delivered !== true) process.exitCode = 1
890
1212
  return
891
1213
  }
892
1214
  if (res.status === 401 || res.status === 403) die(`[${verb}] ${keyHint(res.status)}${out.error !== undefined ? ` (${String(out.error)})` : ''}`)
1215
+ if(out.acceptance==='unknown')die(`[${verb}] 接纳状态未知,请用原 task id 对账,不要换 id 重派 · task ${String(out.task_id)}`)
1216
+ if(out.acceptance==='inserted'){console.log(`[${verb}] 已送达、后续处理未确认,请按原 task id 对账,不要重派 · task ${String(out.task_id)}`);return}
893
1217
  if(out.delivered===true&&out.receiverBookkeeping==='failed'){console.log(`[${verb}] 已送达、接收方记账失败,请对账,不要重派 · task ${String(out.task_id)} — delivered; receiver bookkeeping failed; reconcile, do not redispatch`);return}
1218
+ if(has('--sync')&&out.task_status==='TASK_STATE_DELIVERED'&&out.reply_final!==true&&out.bridge===undefined){console.log(`[${verb}] 已送达、未得到最终答复,请按 task id 对账 · ${String(out.task_id)} — no final reply`);console.error(`[${verb}] status TASK_STATE_DELIVERED — delivered, not failed; reconcile the original task`);if(out.result?.text)console.log(String(out.result.text));return}
894
1219
  // A sync round answers with the target's reply instead of a delivery marker.
895
1220
  if (out.routed === true && typeof out.result?.text === 'string') {
896
1221
  console.log(`[${verb}] reply from ${String(out.team)} · task ${String(out.task_id)}${typeof out.context_id === 'string' && out.context_id !== '' ? ` · context ${out.context_id}` : ''}`)
@@ -968,6 +1293,7 @@ async function main() {
968
1293
  return
969
1294
  }
970
1295
  if (cmd === 'inbox') { await cmdInbox(); return }
1296
+ if (cmd === 'thread') { await cmdThread(); return }
971
1297
  if (cmd === 'file') {
972
1298
  const sub = positionals()[0]
973
1299
  if (sub === 'send') { await cmdFileSend(); return }
@@ -1025,7 +1351,7 @@ async function main() {
1025
1351
  console.log(`dead (${String(dead.length)}):`)
1026
1352
  listCapped(dead, 20, t => ` ${String(t.taskId)} → ${String(t.team)} (${String(t.reason ?? '?')})${senderTail(t)}${t.reminderAttempts!==undefined?` reminders=${t.reminderDelivered??0}/${t.reminderAttempts} last=${t.reminderLastAt??'?'}${t.repeatedlyUnreconciled?' 多次提醒未对账':''}`:''}`)
1027
1353
  console.log(`archived (${String(archived.length)}):`)
1028
- listCapped(archived, 20, t => ` ${String(t.taskId)} ${String(t.outcome ?? '?')}${t.endedBy !== undefined && t.endedBy !== 'receipt' ? ` (${String(t.endedBy)})` : ''}${t.withdrawalResult?` withdrawal=${String(t.withdrawalResult)}`:''}${senderTail(t)}${t.reminderAttempts!==undefined?` reminders=${t.reminderDelivered??0}/${t.reminderAttempts} last=${t.reminderLastAt??'?'}${t.repeatedlyUnreconciled?' 多次提醒未对账':''}`:''}`)
1354
+ listCapped(archived, 20, t => ` ${String(t.taskId)} ${String(t.outcome ?? '?')}${t.disposition !== undefined ? ` disposition=${String(t.disposition)}` : ''}${t.endedBy !== undefined && t.endedBy !== 'receipt' ? ` (${String(t.endedBy)})` : ''}${t.withdrawalResult?` withdrawal=${String(t.withdrawalResult)}`:''}${senderTail(t)}${t.reminderAttempts!==undefined?` reminders=${t.reminderDelivered??0}/${t.reminderAttempts} last=${t.reminderLastAt??'?'}${t.repeatedlyUnreconciled?' 多次提醒未对账':''}`:''}`)
1029
1355
  if (s.conservation != null) console.log(`conservation: conserved=${String(s.conservation.conserved)} delta=${String(s.conservation.delta)}`)
1030
1356
  if (s.settlement != null) console.log(`settlement: mode=${String(s.settlement.mode)} refusalsTotal=${String(s.settlement.refusalsTotal)} offChain=${String(s.settlement.offChain)}`)
1031
1357
  if (s.settlement?.enforceRefused) console.log(`enforce refused: ${String(s.settlement.enforceRefused.reason)}; blockers=${JSON.stringify(s.settlement.enforceRefused.blockers)}`)
@@ -1069,10 +1395,13 @@ async function main() {
1069
1395
  console.log(`deposit ${depositLine(list, identity().handle, 'OFFLINE')}`)
1070
1396
  return
1071
1397
  }
1398
+ if (cmd === 'receipt') { await cmdReceipt(); return }
1399
+ if (cmd === 'admission') { await cmdAdmission(); return }
1400
+ if (cmd === 'superagent') { await cmdSuperagent(); return }
1072
1401
  if (cmd === 'report') {
1073
1402
  const text = positionals().join(' ')
1074
1403
  if (text === '') usage('usage: a2a report <message…> — one-way report into the host process-team mailbox')
1075
- await sendDirect('report', { team: 'dsh', message: `[a2a-net cli report] ${text}`, caller_session: identity().handle, wait: false, task_id: taskFlag() ?? newTaskId() })
1404
+ await sendDirect('report', { team: await homeZone(), message: `[a2a-net cli report] ${text}`, caller_session: identity().handle, wait: false, task_id: taskFlag() ?? newTaskId() })
1076
1405
  return
1077
1406
  }
1078
1407
  if (cmd === 'revoke') {