mycelium-cli 0.2.0__tar.gz → 0.4.0__tar.gz

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 (38) hide show
  1. {mycelium_cli-0.2.0 → mycelium_cli-0.4.0}/PKG-INFO +41 -1
  2. {mycelium_cli-0.2.0 → mycelium_cli-0.4.0}/README.md +40 -0
  3. {mycelium_cli-0.2.0 → mycelium_cli-0.4.0}/mycelium_cli/commands/deal.py +31 -25
  4. {mycelium_cli-0.2.0 → mycelium_cli-0.4.0}/mycelium_cli/commands/discover.py +11 -2
  5. mycelium_cli-0.4.0/mycelium_cli/commands/jobs.py +373 -0
  6. mycelium_cli-0.4.0/mycelium_cli/commands/memory.py +206 -0
  7. mycelium_cli-0.4.0/mycelium_cli/commands/verifier.py +142 -0
  8. {mycelium_cli-0.2.0 → mycelium_cli-0.4.0}/mycelium_cli/main.py +9 -1
  9. {mycelium_cli-0.2.0 → mycelium_cli-0.4.0}/mycelium_cli.egg-info/PKG-INFO +41 -1
  10. {mycelium_cli-0.2.0 → mycelium_cli-0.4.0}/mycelium_cli.egg-info/SOURCES.txt +2 -0
  11. {mycelium_cli-0.2.0 → mycelium_cli-0.4.0}/pyproject.toml +1 -1
  12. mycelium_cli-0.2.0/mycelium_cli/commands/jobs.py +0 -238
  13. {mycelium_cli-0.2.0 → mycelium_cli-0.4.0}/mycelium_cli/__init__.py +0 -0
  14. {mycelium_cli-0.2.0 → mycelium_cli-0.4.0}/mycelium_cli/commands/__init__.py +0 -0
  15. {mycelium_cli-0.2.0 → mycelium_cli-0.4.0}/mycelium_cli/commands/agent.py +0 -0
  16. {mycelium_cli-0.2.0 → mycelium_cli-0.4.0}/mycelium_cli/commands/call.py +0 -0
  17. {mycelium_cli-0.2.0 → mycelium_cli-0.4.0}/mycelium_cli/commands/check.py +0 -0
  18. {mycelium_cli-0.2.0 → mycelium_cli-0.4.0}/mycelium_cli/commands/compile.py +0 -0
  19. {mycelium_cli-0.2.0 → mycelium_cli-0.4.0}/mycelium_cli/commands/deploy.py +0 -0
  20. {mycelium_cli-0.2.0 → mycelium_cli-0.4.0}/mycelium_cli/commands/doctor.py +0 -0
  21. {mycelium_cli-0.2.0 → mycelium_cli-0.4.0}/mycelium_cli/commands/events.py +0 -0
  22. {mycelium_cli-0.2.0 → mycelium_cli-0.4.0}/mycelium_cli/commands/fund.py +0 -0
  23. {mycelium_cli-0.2.0 → mycelium_cli-0.4.0}/mycelium_cli/commands/init.py +0 -0
  24. {mycelium_cli-0.2.0 → mycelium_cli-0.4.0}/mycelium_cli/commands/newwallet.py +0 -0
  25. {mycelium_cli-0.2.0 → mycelium_cli-0.4.0}/mycelium_cli/commands/pay.py +0 -0
  26. {mycelium_cli-0.2.0 → mycelium_cli-0.4.0}/mycelium_cli/commands/register.py +0 -0
  27. {mycelium_cli-0.2.0 → mycelium_cli-0.4.0}/mycelium_cli/commands/resolve.py +0 -0
  28. {mycelium_cli-0.2.0 → mycelium_cli-0.4.0}/mycelium_cli/commands/run.py +0 -0
  29. {mycelium_cli-0.2.0 → mycelium_cli-0.4.0}/mycelium_cli/commands/status.py +0 -0
  30. {mycelium_cli-0.2.0 → mycelium_cli-0.4.0}/mycelium_cli/commands/test.py +0 -0
  31. {mycelium_cli-0.2.0 → mycelium_cli-0.4.0}/mycelium_cli/config.py +0 -0
  32. {mycelium_cli-0.2.0 → mycelium_cli-0.4.0}/mycelium_cli.egg-info/dependency_links.txt +0 -0
  33. {mycelium_cli-0.2.0 → mycelium_cli-0.4.0}/mycelium_cli.egg-info/entry_points.txt +0 -0
  34. {mycelium_cli-0.2.0 → mycelium_cli-0.4.0}/mycelium_cli.egg-info/requires.txt +0 -0
  35. {mycelium_cli-0.2.0 → mycelium_cli-0.4.0}/mycelium_cli.egg-info/top_level.txt +0 -0
  36. {mycelium_cli-0.2.0 → mycelium_cli-0.4.0}/setup.cfg +0 -0
  37. {mycelium_cli-0.2.0 → mycelium_cli-0.4.0}/tests/test_agent.py +0 -0
  38. {mycelium_cli-0.2.0 → mycelium_cli-0.4.0}/tests/test_cli.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mycelium-cli
3
- Version: 0.2.0
3
+ Version: 0.4.0
4
4
  Summary: Mycelium CLI — init, newwallet, compile, deploy, register
5
5
  Requires-Python: >=3.10
6
6
  Description-Content-Type: text/markdown
@@ -230,6 +230,46 @@ Performs a simulation dry-run of the agent loop. It intercepts all state-changin
230
230
  mycelium test
231
231
  ```
232
232
 
233
+ ### 16. `mycelium job` (Proof Layer — v0.4.0)
234
+ Posts, performs, judges, and settles verifiable bounties. A job is self-describing
235
+ on-chain (title, description, weighted checks, judge panel); release is gated on a
236
+ multi-LLM panel verdict, not a hash.
237
+
238
+ * **Syntax**:
239
+ ```bash
240
+ # Post a self-describing bounty + judge panel
241
+ mycelium job post \
242
+ --title "Write a sales-report SQL query" \
243
+ --description "Aggregate revenue by region, last 12 months." \
244
+ --check correct:70:"returns correct rows" \
245
+ --check style:30:"readable, indexed" \
246
+ --judge-model nvidia:meta/llama-3.1-70b \
247
+ --judge-model groq:llama-3.3-70b \
248
+ --threshold 75
249
+
250
+ mycelium job do <job_id> --model groq:llama-3.3-70b # worker produces + submits evidence
251
+ mycelium job judge <job_id> # run the job's panel → verdict → settle
252
+ mycelium job models --provider nvidia # list available judge/worker models
253
+ mycelium job status <job_id> # on-chain title/checks/panel/score
254
+ ```
255
+ * **Details**: `--check` is `id:weight:text` (repeatable); `--judge-model` is
256
+ `provider:model` (repeatable). A single agent is paid the full bounty on a
257
+ passing verdict; a swarm is paid a balanced split — both gated on the same panel.
258
+
259
+ ### 17. `mycelium verifier` (Staked judge pool — v0.4.0)
260
+ Manages the `VerifierRegistry`: judges stake an XLM bond to become eligible and are
261
+ slashed for outlier verdicts; per-judge accuracy (verifier reputation) is tracked.
262
+
263
+ * **Syntax**:
264
+ ```bash
265
+ mycelium verifier register --model nvidia:meta/llama-3.1-70b
266
+ mycelium verifier stake 100 # lock an XLM bond
267
+ mycelium verifier info <address> # stake, jobs judged, accuracy, active
268
+ mycelium verifier eligible <model> # judges staked + tagged for a model
269
+ mycelium verifier slash <address> # market-only: penalize an outlier verdict
270
+ mycelium verifier accuracy <address> # verifier reputation (within-tolerance rate)
271
+ ```
272
+
233
273
  ---
234
274
 
235
275
  ## 🔐 Environment Variables
@@ -220,6 +220,46 @@ Performs a simulation dry-run of the agent loop. It intercepts all state-changin
220
220
  mycelium test
221
221
  ```
222
222
 
223
+ ### 16. `mycelium job` (Proof Layer — v0.4.0)
224
+ Posts, performs, judges, and settles verifiable bounties. A job is self-describing
225
+ on-chain (title, description, weighted checks, judge panel); release is gated on a
226
+ multi-LLM panel verdict, not a hash.
227
+
228
+ * **Syntax**:
229
+ ```bash
230
+ # Post a self-describing bounty + judge panel
231
+ mycelium job post \
232
+ --title "Write a sales-report SQL query" \
233
+ --description "Aggregate revenue by region, last 12 months." \
234
+ --check correct:70:"returns correct rows" \
235
+ --check style:30:"readable, indexed" \
236
+ --judge-model nvidia:meta/llama-3.1-70b \
237
+ --judge-model groq:llama-3.3-70b \
238
+ --threshold 75
239
+
240
+ mycelium job do <job_id> --model groq:llama-3.3-70b # worker produces + submits evidence
241
+ mycelium job judge <job_id> # run the job's panel → verdict → settle
242
+ mycelium job models --provider nvidia # list available judge/worker models
243
+ mycelium job status <job_id> # on-chain title/checks/panel/score
244
+ ```
245
+ * **Details**: `--check` is `id:weight:text` (repeatable); `--judge-model` is
246
+ `provider:model` (repeatable). A single agent is paid the full bounty on a
247
+ passing verdict; a swarm is paid a balanced split — both gated on the same panel.
248
+
249
+ ### 17. `mycelium verifier` (Staked judge pool — v0.4.0)
250
+ Manages the `VerifierRegistry`: judges stake an XLM bond to become eligible and are
251
+ slashed for outlier verdicts; per-judge accuracy (verifier reputation) is tracked.
252
+
253
+ * **Syntax**:
254
+ ```bash
255
+ mycelium verifier register --model nvidia:meta/llama-3.1-70b
256
+ mycelium verifier stake 100 # lock an XLM bond
257
+ mycelium verifier info <address> # stake, jobs judged, accuracy, active
258
+ mycelium verifier eligible <model> # judges staked + tagged for a model
259
+ mycelium verifier slash <address> # market-only: penalize an outlier verdict
260
+ mycelium verifier accuracy <address> # verifier reputation (within-tolerance rate)
261
+ ```
262
+
223
263
  ---
224
264
 
225
265
  ## 🔐 Environment Variables
@@ -4,20 +4,22 @@
4
4
  Where `mycelium pay` is an *unconditional* transfer, a deal is *conditional*
5
5
  x402 commerce between two agents: the payer locks funds into a fresh escrow
6
6
  payable to a provider (resolved by Hive Registry unique name or raw address),
7
- and the provider only collects once it publishes a proof of the agreed task.
8
- If the provider never delivers, the payer reclaims the funds after the timeout.
7
+ and the provider only collects once a `judge` authorizes release on a passing
8
+ verdict of the delivered work. If no release happens, the payer reclaims the
9
+ funds after the timeout.
9
10
 
10
11
  This is the CLI front door to what `a2a_demo.py` / `EscrowPaymentRouter` do by
11
- hand — two sovereign agents transacting purely through on-chain state:
12
+ hand — two sovereign agents transacting purely through on-chain state, with a
13
+ judge as the impartial release authority (see PROOF_SYSTEM.md):
12
14
 
13
- open payer locks `amount` XLM to a provider against a task hash → escrow id
14
- release provider (or payer) publishes the proof preimage → funds disburse
15
+ open payer locks `amount` XLM to a provider, naming a judge → escrow id
16
+ release the judge authorizes payout (passing verdict) → funds disburse
15
17
  refund payer reclaims the locked funds after the deadline passes
16
- status read the escrow's current state (amount, provider, deadline, settled)
18
+ status read the escrow's current state (amount, provider, judge, settled)
17
19
 
18
- The escrow contract enforces the proof (SHA-256(proof) == task_hash) and the
19
- deadline on-chain, so neither side has to trust the other. The board/registry
20
- default from `mycelium.toml`, mirroring `deploy` / `register` / `job`.
20
+ Release follows a judge's verdict rather than a SHA-256 preimage: a hash only
21
+ proved the claimant could echo the agreed bytes, never that the work was done.
22
+ The board/registry default from `mycelium.toml`, mirroring `deploy` / `job`.
21
23
 
22
24
  Commands: open, release, refund, status.
23
25
  """
@@ -77,38 +79,40 @@ def _resolve_agent_address(context, agent: str, registry: Optional[str]) -> str:
77
79
  return addr
78
80
 
79
81
 
80
- def _task_bytes(task: str) -> bytes:
81
- """A task file's raw bytes, or the UTF-8 bytes of a literal string."""
82
- if os.path.isfile(task):
83
- with open(task, "rb") as f:
84
- return f.read()
85
- return task.encode("utf-8")
82
+ def _evidence_root(evidence: str) -> bytes:
83
+ """The 32-byte evidence_root: SHA-256 of a file's bytes or a literal string."""
84
+ if os.path.isfile(evidence):
85
+ with open(evidence, "rb") as f:
86
+ data = f.read()
87
+ else:
88
+ data = evidence.encode("utf-8")
89
+ return hashlib.sha256(data).digest()
86
90
 
87
91
 
88
92
  @deal_app.command("open")
89
93
  def open_deal(
90
94
  to: str = typer.Option(..., "--to", help="Provider: Hive Registry unique name or G/C address to pay"),
91
95
  amount: str = typer.Option(..., "--amount", help="Amount in XLM to lock for the provider"),
92
- task: str = typer.Option(..., "--task", help="Task spec file or string; its SHA-256 is the release condition"),
96
+ judge: str = typer.Option(..., "--judge", help="Judge: unique name or G/C address the release authority"),
93
97
  token: str = typer.Option(None, "--token", help="Payment token contract (defaults to native XLM SAC)"),
94
98
  timeout: int = typer.Option(DEFAULT_TIMEOUT_SECONDS, "--timeout", help="Refund deadline in seconds"),
95
99
  network: str = typer.Option(None, help="testnet or mainnet (defaults to mycelium.toml)"),
96
100
  wallet: str = typer.Option(DEFAULT_WALLET_PATH, help="Wallet path"),
97
101
  registry: str = typer.Option(None, "--registry", help="Hive Registry id override (for name resolution)"),
98
102
  ):
99
- """Payer locks `amount` XLM to a provider against a task hash; prints the escrow id."""
103
+ """Payer locks `amount` XLM to a provider, naming a judge as the release authority."""
100
104
  from mycelium_sdk.x402.settlement import EscrowPaymentRouter
101
105
 
102
106
  context = _context(network, wallet, signing=True)
103
107
  provider = _resolve_agent_address(context, to, registry)
104
- task_hash = hashlib.sha256(_task_bytes(task)).digest()
108
+ judge_addr = _resolve_agent_address(context, judge, registry)
105
109
 
106
- typer.echo(f"[deal] Locking {amount} XLM to provider {to} ({provider[:8]}…) for {timeout}s...")
110
+ typer.echo(f"[deal] Locking {amount} XLM to provider {to} ({provider[:8]}…), judge {judge_addr[:8]}…, for {timeout}s...")
107
111
  try:
108
112
  escrow_id = EscrowPaymentRouter(context).create_locked_escrow(
109
113
  provider_id=provider,
110
114
  amount_xlm=Decimal(amount),
111
- task_hash=task_hash,
115
+ judge=judge_addr,
112
116
  token=token,
113
117
  timeout_seconds=timeout,
114
118
  )
@@ -116,23 +120,23 @@ def open_deal(
116
120
  typer.echo(f"❌ deal open failed: {e}")
117
121
  raise typer.Exit(code=1)
118
122
  typer.echo(f"✓ Deal opened. Escrow {escrow_id}")
119
- typer.echo(f" Provider releases with: mycelium deal release {escrow_id} --proof <task>")
123
+ typer.echo(f" Judge releases with: mycelium deal release {escrow_id} --evidence <bundle>")
120
124
  typer.echo(f" Payer refunds after {timeout}s with: mycelium deal refund {escrow_id}")
121
125
 
122
126
 
123
127
  @deal_app.command("release")
124
128
  def release(
125
129
  escrow_id: str = typer.Argument(..., help="Escrow contract id from `deal open`"),
126
- proof: str = typer.Option(..., "--proof", help="Proof file or string (must SHA-256 to the task hash)"),
130
+ evidence: str = typer.Option(..., "--evidence", help="Evidence bundle file/string (its SHA-256 is recorded for audit)"),
127
131
  network: str = typer.Option(None, help="testnet or mainnet (defaults to mycelium.toml)"),
128
- wallet: str = typer.Option(DEFAULT_WALLET_PATH, help="Wallet path"),
132
+ wallet: str = typer.Option(DEFAULT_WALLET_PATH, help="Judge wallet path (the escrow's release authority)"),
129
133
  ):
130
- """Disburse the locked funds to the provider by publishing the task proof."""
134
+ """Judge disburses the locked funds to the provider on a passing verdict."""
131
135
  from mycelium_sdk.x402.settlement import EscrowPaymentRouter
132
136
 
133
137
  context = _context(network, wallet, signing=True)
134
138
  try:
135
- EscrowPaymentRouter(context).release_funds(escrow_id, _task_bytes(proof))
139
+ EscrowPaymentRouter(context).release_funds(escrow_id, _evidence_root(evidence))
136
140
  except Exception as e: # noqa: BLE001
137
141
  typer.echo(f"❌ deal release failed: {e}")
138
142
  raise typer.Exit(code=1)
@@ -177,10 +181,12 @@ def status(
177
181
 
178
182
  amount = _get("amount")
179
183
  provider = _get("provider")
184
+ judge = _get("judge")
180
185
  deadline = _get("deadline")
181
186
  settled = _get("settled")
182
187
  typer.echo(f"Deal escrow {escrow_id}")
183
188
  typer.echo(f" amount : {int(amount) / 10_000_000:.7f} XLM" if amount is not None else " amount : —")
184
189
  typer.echo(f" provider : {getattr(provider, 'address', provider)}")
190
+ typer.echo(f" judge : {getattr(judge, 'address', judge)}")
185
191
  typer.echo(f" deadline : {int(deadline)} (unix)" if deadline is not None else " deadline : —")
186
192
  typer.echo(f" settled : {bool(settled)}")
@@ -31,9 +31,18 @@ def run_discover(
31
31
  context = AgentContext.read_only(network_type=network)
32
32
  hive = HiveClient(context, registry_address=registry)
33
33
 
34
- print(f"[discover] Scanning Hive Registry {registry} on {network}...")
34
+ # Hosted indexer first (instant, full history); falls back to the on-chain
35
+ # event-scan automatically when unreachable. An explicit --start-ledger means
36
+ # the caller wants the chain scan, so skip the indexer in that case.
37
+ prefer_indexer = start_ledger is None
38
+ if prefer_indexer:
39
+ print(f"[discover] Querying indexer (falls back to chain scan of {registry})...")
40
+ else:
41
+ print(f"[discover] Scanning Hive Registry {registry} on {network}...")
35
42
  try:
36
- agents = hive.discover_agents(start_ledger=start_ledger, resolve=resolve)
43
+ agents = hive.discover_agents(
44
+ start_ledger=start_ledger, resolve=resolve, prefer_indexer=prefer_indexer
45
+ )
37
46
  except Exception as e:
38
47
  print(f"❌ Discovery failed: {e}")
39
48
  sys.exit(1)
@@ -0,0 +1,373 @@
1
+ """
2
+ `mycelium job …` — drive the Sovereign Job Boards from the console.
3
+
4
+ A Typer sub-app (registered as the `job` command group in `main.py`) that thin-
5
+ wraps `mycelium_sdk.JobBoardClient`, reusing the same wallet load + passphrase
6
+ resolution as `deploy` / `register`. The JobBoard contract address defaults from
7
+ `mycelium.toml` (`[jobs].board_address`); override with `--board`.
8
+
9
+ Commands: post, list, claim, assign, join, submit, verdict, finalize, status.
10
+ """
11
+
12
+ import hashlib
13
+ import os
14
+ import sys
15
+ from decimal import Decimal
16
+
17
+ import typer
18
+
19
+ from mycelium_cli.config import get_value
20
+
21
+ DEFAULT_WALLET_PATH = os.path.join(".mycelium", "wallet.json")
22
+ PASSPHRASE_ENV_VAR = "MYCELIUM_DECRYPT_KEY"
23
+
24
+ job_app = typer.Typer(help="Post, claim, and settle on-chain jobs (Sovereign Job Boards).")
25
+
26
+
27
+ def _resolve_passphrase(label: str = "Wallet passphrase") -> str:
28
+ """MYCELIUM_DECRYPT_KEY if set, else prompt — matches the rest of the CLI."""
29
+ env_value = os.environ.get(PASSPHRASE_ENV_VAR)
30
+ if env_value:
31
+ return env_value
32
+ return typer.prompt(label, hide_input=True)
33
+
34
+
35
+ def _board_address(override: str | None) -> str:
36
+ board = override or get_value("jobs", "board_address")
37
+ if not board:
38
+ typer.echo(
39
+ "Error: no JobBoard address. Set [jobs].board_address in mycelium.toml "
40
+ "or pass --board <contract_id>."
41
+ )
42
+ raise typer.Exit(code=1)
43
+ return board
44
+
45
+
46
+ def _client(network: str | None, wallet: str, board: str | None, *, signing: bool):
47
+ """Build a JobBoardClient. Read-only commands skip wallet + passphrase."""
48
+ from mycelium_sdk import AgentContext, JobBoardClient
49
+
50
+ network = network or get_value("onchain", "network", "testnet")
51
+ if signing:
52
+ if not os.path.exists(wallet):
53
+ typer.echo(f"Error: wallet {wallet} not found. Run `mycelium newwallet` first.")
54
+ raise typer.Exit(code=1)
55
+ ctx = AgentContext(keypair_path=wallet, network_type=network, passphrase=_resolve_passphrase())
56
+ else:
57
+ ctx = AgentContext.read_only(network_type=network)
58
+ return JobBoardClient(ctx, _board_address(board))
59
+
60
+
61
+ def _parse_check(spec: str) -> dict:
62
+ """Parse a --check 'id:weight:description' into a check dict (LLM-judged)."""
63
+ parts = spec.split(":", 2)
64
+ if len(parts) != 3:
65
+ raise typer.BadParameter(f"--check must be 'id:weight:description' (got {spec!r}).")
66
+ cid, weight, text = parts
67
+ return {"id": cid.strip(), "weight": int(weight), "check": text.strip()}
68
+
69
+
70
+ @job_app.command("post")
71
+ def post(
72
+ title: str = typer.Option(..., "--title", help="Job heading (required)"),
73
+ description: str = typer.Option(..., "--description", help="What the work is (required)"),
74
+ check: list[str] = typer.Option(..., "--check", help="A check 'id:weight:description' (repeatable, ≥1)"),
75
+ judge_model: list[str] = typer.Option(..., "--judge-model", help="Judge model 'provider:model' (repeatable; the panel)"),
76
+ bounty: str = typer.Option(..., "--bounty", help="Bounty in XLM"),
77
+ judge: str = typer.Option(..., "--judge", help="Judge address (G…) — on-chain verdict authority that releases the escrow"),
78
+ threshold: int = typer.Option(70, "--threshold", help="Pass score (0-100); payout only at/above this"),
79
+ deliverable_type: str = typer.Option("any", "--type", help="Freeform deliverable type, e.g. text/sql, file/pptx"),
80
+ mode: str = typer.Option("single", "--mode", help="single | swarm"),
81
+ token: str = typer.Option(None, "--token", help="Payment token contract (defaults to native XLM SAC)"),
82
+ deadline: int = typer.Option(86400, "--deadline", help="Refund deadline in seconds"),
83
+ network: str = typer.Option(None, help="testnet or mainnet (defaults to mycelium.toml)"),
84
+ wallet: str = typer.Option(DEFAULT_WALLET_PATH, help="Wallet path"),
85
+ board: str = typer.Option(None, "--board", help="JobBoard contract id override"),
86
+ ):
87
+ """
88
+ Post a self-describing bounty for ANY job. Title, description, checks, and the
89
+ chosen judge panel are stored ON-CHAIN, so the bounty is fully readable from
90
+ the contract. Example:
91
+
92
+ mycelium job post --title "Promo script" --description "60s TigerGraph video" \\
93
+ --check "hook:30:strong opening" --check "clarity:40:explains the bounty" \\
94
+ --check "cta:30:clear call to action" \\
95
+ --judge-model nvidia:deepseek-ai/deepseek-v4-pro \\
96
+ --judge-model groq:llama-3.3-70b-versatile \\
97
+ --bounty 5 --judge G... --threshold 70
98
+ """
99
+ client = _client(network, wallet, board, signing=True)
100
+ try:
101
+ checks = [_parse_check(c) for c in check]
102
+ job_id = client.post_bounty(
103
+ title=title, description=description, checks=checks, judge_models=judge_model,
104
+ bounty_xlm=Decimal(bounty), judge=judge, pass_threshold=threshold,
105
+ deliverable_type=deliverable_type, mode=mode, token=token, deadline_seconds=deadline,
106
+ )
107
+ except Exception as e: # noqa: BLE001
108
+ typer.echo(f"❌ post failed: {e}")
109
+ raise typer.Exit(code=1)
110
+ typer.echo(f"✓ Posted job #{job_id}: '{title}' (bounty {bounty} XLM, {len(checks)} checks, "
111
+ f"{len(judge_model)}-model panel, threshold {threshold}).")
112
+
113
+
114
+ @job_app.command("list")
115
+ def list_jobs(
116
+ status: str = typer.Option(None, "--status", help="Filter: open | claimed | submitted | done | cancelled"),
117
+ network: str = typer.Option(None, help="testnet or mainnet (defaults to mycelium.toml)"),
118
+ board: str = typer.Option(None, "--board", help="JobBoard contract id override"),
119
+ ):
120
+ """List jobs (read-only, no wallet)."""
121
+ client = _client(network, DEFAULT_WALLET_PATH, board, signing=False)
122
+ jobs = client.list_open_jobs(status=status)
123
+ if not jobs:
124
+ typer.echo("No jobs found." if status is None else f"No jobs with status '{status}'.")
125
+ return
126
+ for j in jobs:
127
+ bounty_xlm = j["bounty_stroops"] / 10_000_000
128
+ typer.echo(
129
+ f" #{j['job_id']:>3} [{j['status']:<9}] {j['mode']:<6} "
130
+ f"{bounty_xlm:>10.4f} XLM poster={j['poster'][:8]}… escrow={(j['escrow'] or '')[:8]}…"
131
+ )
132
+
133
+
134
+ @job_app.command("claim")
135
+ def claim(
136
+ job_id: int = typer.Argument(..., help="Job id to claim"),
137
+ network: str = typer.Option(None, help="testnet or mainnet (defaults to mycelium.toml)"),
138
+ wallet: str = typer.Option(DEFAULT_WALLET_PATH, help="Wallet path"),
139
+ board: str = typer.Option(None, "--board", help="JobBoard contract id override"),
140
+ ):
141
+ """Single-agent self-claim of an open job."""
142
+ client = _client(network, wallet, board, signing=True)
143
+ client.claim_job(job_id)
144
+ typer.echo(f"✓ Claimed job #{job_id}.")
145
+
146
+
147
+ @job_app.command("assign")
148
+ def assign(
149
+ job_id: int = typer.Argument(..., help="Job id to assign"),
150
+ agent: str = typer.Option(..., "--agent", help="Agent unique name (Hive Registry) or G/C address"),
151
+ network: str = typer.Option(None, help="testnet or mainnet (defaults to mycelium.toml)"),
152
+ wallet: str = typer.Option(DEFAULT_WALLET_PATH, help="Wallet path"),
153
+ board: str = typer.Option(None, "--board", help="JobBoard contract id override"),
154
+ registry: str = typer.Option(None, "--registry", help="Hive Registry id override (for name resolution)"),
155
+ ):
156
+ """Poster-side: assign a specific agent (by name or address) to an open job."""
157
+ client = _client(network, wallet, board, signing=True)
158
+ agent_addr = _resolve_agent_address(client.context, agent, registry)
159
+ client.assign_agent(job_id, agent_addr)
160
+ typer.echo(f"✓ Assigned {agent} ({agent_addr[:8]}…) to job #{job_id}.")
161
+
162
+
163
+ @job_app.command("join")
164
+ def join(
165
+ job_id: int = typer.Argument(..., help="Job id to join"),
166
+ capability: str = typer.Option(..., "--capability", help="Capability tag you bring to the swarm"),
167
+ share: int = typer.Option(..., "--share", help="Agreed bounty share in basis points (sum to 10000)"),
168
+ network: str = typer.Option(None, help="testnet or mainnet (defaults to mycelium.toml)"),
169
+ wallet: str = typer.Option(DEFAULT_WALLET_PATH, help="Wallet path"),
170
+ board: str = typer.Option(None, "--board", help="JobBoard contract id override"),
171
+ ):
172
+ """Join a swarm job with an agreed bounty share."""
173
+ if not 0 < share <= 10000:
174
+ typer.echo(f"Error: --share must be between 1 and 10000 basis points (got {share}).")
175
+ raise typer.Exit(code=1)
176
+ client = _client(network, wallet, board, signing=True)
177
+ client.join_swarm(job_id, capability, share)
178
+ typer.echo(f"✓ Joined swarm for job #{job_id} ({share} bps, capability '{capability}').")
179
+
180
+
181
+ @job_app.command("submit")
182
+ def submit(
183
+ job_id: int = typer.Argument(..., help="Job id"),
184
+ evidence: str = typer.Option(..., "--evidence", help="Deliverable file/string; its SHA-256 is anchored as the evidence_root"),
185
+ uri: str = typer.Option("", "--uri", help="Optional pointer to the full bundle (recorded on-chain)"),
186
+ network: str = typer.Option(None, help="testnet or mainnet (defaults to mycelium.toml)"),
187
+ wallet: str = typer.Option(DEFAULT_WALLET_PATH, help="Agent wallet path"),
188
+ board: str = typer.Option(None, "--board", help="JobBoard contract id override"),
189
+ ):
190
+ """Agent: anchor a pre-made deliverable's evidence on-chain (manual alternative to `do`)."""
191
+ client = _client(network, wallet, board, signing=True)
192
+ client.submit_evidence(job_id, _evidence_root(evidence), uri)
193
+ typer.echo(f"✓ Submitted evidence for job #{job_id} — awaiting the judge panel.")
194
+
195
+
196
+ @job_app.command("do")
197
+ def do_job(
198
+ job_id: int = typer.Argument(..., help="Job id to work on"),
199
+ model: str = typer.Option(..., "--model", help="The agent's model 'provider:model' (nvidia/groq)"),
200
+ claim: bool = typer.Option(True, "--claim/--no-claim", help="Self-claim the job first (single mode)"),
201
+ no_revise: bool = typer.Option(False, "--no-revise", help="Single pass (skip the self-review pass)"),
202
+ network: str = typer.Option(None, help="testnet or mainnet (defaults to mycelium.toml)"),
203
+ wallet: str = typer.Option(DEFAULT_WALLET_PATH, help="Agent wallet path"),
204
+ board: str = typer.Option(None, "--board", help="JobBoard contract id override"),
205
+ ):
206
+ """Agent: read the job from chain, do the actual work with your model, and submit real evidence."""
207
+ client = _client(network, wallet, board, signing=True)
208
+ try:
209
+ bundle, content = client.execute_job(
210
+ job_id, model, claim=claim, revise=not no_revise, evidence_uri="inline://deliverable")
211
+ except Exception as e: # noqa: BLE001
212
+ typer.echo(f"❌ do failed: {e}")
213
+ raise typer.Exit(code=1)
214
+ typer.echo(f"✓ Job #{job_id}: produced {len(content.split())} words, evidence anchored "
215
+ f"(root {bundle.evidence_root().hex()[:12]}…). Awaiting the panel.")
216
+
217
+
218
+ @job_app.command("judge")
219
+ def judge_job(
220
+ job_id: int = typer.Argument(..., help="Job id to judge"),
221
+ deliverable: str = typer.Option(..., "--deliverable", help="The deliverable text/file to score (what the agent produced)"),
222
+ network: str = typer.Option(None, help="testnet or mainnet (defaults to mycelium.toml)"),
223
+ wallet: str = typer.Option(DEFAULT_WALLET_PATH, help="Judge wallet path"),
224
+ board: str = typer.Option(None, "--board", help="JobBoard contract id override"),
225
+ ):
226
+ """Judge: run the panel the JOB prescribes (from its on-chain spec) over the deliverable, record the score, and settle."""
227
+ from mycelium_sdk.proof.evidence import EvidenceBundle, Artifact, Claim
228
+
229
+ client = _client(network, wallet, board, signing=True)
230
+ text = open(deliverable).read() if os.path.isfile(deliverable) else deliverable
231
+ job = client.get_job(job_id)
232
+ # Rebuild the bundle the agent anchored is not needed for scoring text; the
233
+ # on-chain evidence_root is what the verdict binds to.
234
+ bundle = EvidenceBundle(job_id=job_id, rubric_hash=job.get("rubric_hash", ""),
235
+ artifacts=[Artifact.from_bytes("deliverable", "inline://deliverable", text.encode())],
236
+ claims=[])
237
+ try:
238
+ result = client.judge_and_settle(job_id, bundle, content_views={"inline://deliverable": text})
239
+ except Exception as e: # noqa: BLE001
240
+ typer.echo(f"❌ judge failed: {e}")
241
+ raise typer.Exit(code=1)
242
+ verdict = "PASS ✅ — bounty released" if result.passed else "FAIL ❌ — no payout"
243
+ typer.echo(f"Panel score {result.weighted_score:.1f} → {verdict}")
244
+ for v in result.seat_verdicts:
245
+ typer.echo(" " + v.model + ": " + ", ".join(f"{s.id}={s.score}" for s in v.scores))
246
+
247
+
248
+ @job_app.command("verdict")
249
+ def verdict(
250
+ job_id: int = typer.Argument(..., help="Job id"),
251
+ evidence: str = typer.Option(..., "--evidence", help="The same evidence bundle file/string the worker submitted"),
252
+ passed: bool = typer.Option(None, "--pass/--fail", help="Whether the deliverable met the checks"),
253
+ score: int = typer.Option(None, "--score", help="Numeric score 0-100 to record (defaults 100 on pass, 0 on fail)"),
254
+ network: str = typer.Option(None, help="testnet or mainnet (defaults to mycelium.toml)"),
255
+ wallet: str = typer.Option(DEFAULT_WALLET_PATH, help="Judge wallet path"),
256
+ board: str = typer.Option(None, "--board", help="JobBoard contract id override"),
257
+ ):
258
+ """Judge: manually record a verdict (+score) and, on a pass, release the bounty. (Use `judge` for the LLM panel.)"""
259
+ if passed is None:
260
+ typer.echo("Error: specify --pass or --fail.")
261
+ raise typer.Exit(code=1)
262
+ client = _client(network, wallet, board, signing=True)
263
+ sc = score if score is not None else (100 if passed else 0)
264
+ client.settle(job_id, passed, sc, _evidence_root(evidence))
265
+ if passed:
266
+ typer.echo(f"✓ Verdict PASS (score {sc}) for job #{job_id} — bounty released to the worker.")
267
+ else:
268
+ typer.echo(f"✓ Verdict FAIL (score {sc}) for job #{job_id} — no payout; depositor may refund after the deadline.")
269
+
270
+
271
+ @job_app.command("finalize")
272
+ def finalize(
273
+ job_id: int = typer.Argument(..., help="Job id"),
274
+ network: str = typer.Option(None, help="testnet or mainnet (defaults to mycelium.toml)"),
275
+ wallet: str = typer.Option(DEFAULT_WALLET_PATH, help="Poster wallet path"),
276
+ board: str = typer.Option(None, "--board", help="JobBoard contract id override"),
277
+ ):
278
+ """Poster: close the record of a verified job (the bounty is already released)."""
279
+ client = _client(network, wallet, board, signing=True)
280
+ client.finalize(job_id)
281
+ typer.echo(f"✓ Finalized job #{job_id} — record closed.")
282
+
283
+
284
+ @job_app.command("status")
285
+ def status(
286
+ job_id: int = typer.Argument(..., help="Job id"),
287
+ network: str = typer.Option(None, help="testnet or mainnet (defaults to mycelium.toml)"),
288
+ board: str = typer.Option(None, "--board", help="JobBoard contract id override"),
289
+ ):
290
+ """Show a job's full on-chain detail — title, description, checks, chosen panel, score — plus escrow/claimants."""
291
+ import json as _json
292
+
293
+ client = _client(network, DEFAULT_WALLET_PATH, board, signing=False)
294
+ job = client.get_job(job_id)
295
+ typer.echo(f"Job #{job_id}: {job.get('title') or '(untitled)'}")
296
+ if job.get("description"):
297
+ typer.echo(f" description : {job['description']}")
298
+ spec = {}
299
+ if job.get("spec"):
300
+ try:
301
+ spec = _json.loads(job["spec"])
302
+ except Exception:
303
+ pass
304
+ if spec.get("criteria"):
305
+ typer.echo(" checks :")
306
+ for c in spec["criteria"]:
307
+ typer.echo(f" - {c['id']} ({c['weight']}): {c['check']}")
308
+ typer.echo(f" panel : {spec.get('judges', {}).get('models', [])}")
309
+ typer.echo(f" threshold : {spec.get('pass_threshold')}")
310
+ typer.echo(f" status : {job['status']} score: {job.get('score')}")
311
+ typer.echo(f" mode : {job['mode']} bounty: {job['bounty_stroops'] / 10_000_000:.7f} XLM")
312
+ typer.echo(f" poster : {job['poster']}")
313
+ typer.echo(f" judge : {job.get('judge')}")
314
+ typer.echo(f" escrow : {job['escrow']}")
315
+ if job.get("evidence_uri"):
316
+ typer.echo(f" evidence : {job['evidence_uri']} (root {job.get('evidence_root','')[:12]}…)")
317
+ typer.echo(f" deadline : {job['deadline']} (unix)")
318
+ if job["mode"] == "swarm":
319
+ members = client.get_swarm(job_id)
320
+ shares = client.get_shares(job_id)
321
+ typer.echo(" swarm :")
322
+ for m, s in zip(members, shares):
323
+ typer.echo(f" - {m} {s} bps")
324
+ else:
325
+ typer.echo(f" agent : {job['agent']}")
326
+
327
+
328
+ @job_app.command("models")
329
+ def models(
330
+ provider: str = typer.Option("nvidia", "--provider", help="Provider to list models for: nvidia | groq"),
331
+ ):
332
+ """List the models a provider serves (for choosing a judge panel or an agent's model)."""
333
+ from mycelium_sdk.proof import list_models
334
+
335
+ try:
336
+ ids = list_models(provider)
337
+ except Exception as e: # noqa: BLE001
338
+ typer.echo(f"❌ could not list {provider} models: {e}")
339
+ raise typer.Exit(code=1)
340
+ typer.echo(f"{provider}: {len(ids)} models")
341
+ for i in ids:
342
+ typer.echo(f" {provider}:{i}")
343
+
344
+
345
+ def _evidence_root(evidence: str) -> bytes:
346
+ """
347
+ The 32-byte evidence_root for a submission. If `evidence` is a file, hash its
348
+ bytes; otherwise hash the literal string. (The SDK's `proof.EvidenceBundle`
349
+ produces the same kind of SHA-256 root over a structured bundle; the CLI keeps
350
+ it simple by hashing whatever you point it at.)
351
+ """
352
+ if os.path.isfile(evidence):
353
+ with open(evidence, "rb") as f:
354
+ data = f.read()
355
+ else:
356
+ data = evidence.encode("utf-8")
357
+ return hashlib.sha256(data).digest()
358
+
359
+
360
+ def _resolve_agent_address(context, agent: str, registry: str | None) -> str:
361
+ """Pass through a G/C address; otherwise resolve a Hive Registry unique name."""
362
+ from stellar_sdk import StrKey
363
+
364
+ if StrKey.is_valid_ed25519_public_key(agent) or StrKey.is_valid_contract(agent):
365
+ return agent
366
+ from mycelium_sdk import HiveClient
367
+
368
+ entry = HiveClient(context, registry_address=registry).resolve_agent(agent)
369
+ addr = entry.get("public_key")
370
+ if not addr:
371
+ typer.echo(f"Error: could not resolve agent '{agent}' to an address.")
372
+ raise typer.Exit(code=1)
373
+ return addr