cascrow-cli 1.4.0 → 3.0.0

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.0.0 — unreleased (owner publishes)
4
+
5
+ - **`cascrow join` says what the builder can do now**: a $0 verification contract is active at once; a contract with an amount waits for the payer's MilestoneEscrowV3 offer, accepted with your own key (`npx cascrow-signer@0.1.0 accept --milestone <id>`). The JSON carries the server's `status` and `activated`. The builder agent prints the same.
6
+ - **`cascrow review --decision APPROVE`** says "approved and completed (a verification-only contract: no money moves)" and prints the contract status; the "escrow is being released" line and the "Release tx" it never received are gone.
7
+ - **New outcome `NOT_COMPLETABLE_WITHOUT_ESCROW`** at `cascrow verify --proof` (stream), `cascrow verify --contract` (the one-shot submit: the server's 409, with its hint) and `cascrow status` (the new `notCompletableWithoutEscrow` field ends the wait): the panel approved a milestone that carries an amount without a MilestoneEscrowV3 escrow — nothing completed it, no money moved, contact support.
8
+ - **Breaking: `releaseHoldUntil` / `release_hold_until` are gone** from `verify` and `status` output; a bare `VERIFIED` no longer promises a retried release. `passed` on the one-shot `verify --contract` is `verdict === "approved"`.
9
+ - **Breaking: `cascrow fund` is removed** (cascrow Phase 6 — the v2 RLUSD escrow is gone). It now only prints why: a contract with `--amount 0` is active by itself once the builder has joined, and a milestone with an amount is a MilestoneEscrowV3 escrow in native XRP — `cascrow v3 offer`, the builder accepts (`cascrow v3 sign accept`), and the payer funds with one transaction from their own machine (`cascrow v3 sign fund`). Nothing is sent.
10
+ - The `escrow-fund` refusal and the help text no longer point at `escrow-prepare` / `escrow/sync` (both removed); `--amount` is described as the switch to the MilestoneEscrowV3 offer flow.
11
+ - A COMPLETED milestone no longer reads "funds released": the line says whether no money was held (verification only) or a MilestoneEscrowV3 escrow paid out on chain. `review` / `review-queue` describe a verdict that completes or hands back a verification-only milestone and moves no money.
12
+ - The builder agent (`builder-agent.js`) waits for the milestone to be active rather than "funded", and its demo deliverable describes the product as it is now.
13
+
14
+ ## 2.1.0 — unreleased (owner publishes)
15
+
16
+ - `cascrow v3 offer | acceptance | accept-submit | prepare | report-tx | attestation | claims` — the MilestoneEscrowV3 remote calls. `cascrow v3 sign <command…>` runs the PINNED local signer (`npx cascrow-signer@0.1.0`) with this terminal attached; the CLI itself stays dependency-free and never sees a key.
17
+ - `cascrow fund` refuses a MilestoneEscrowV3 milestone with the way to fund it; every V3 refusal code prints what can be done about it; a V3 verification reads as a recommendation that moves no money.
18
+
19
+ ## 2.0.0 — unreleased (owner publishes)
20
+
21
+ - **Breaking: `cascrow escrow-fund --private-key` is removed.** It sent your EVM private key to cascrow, which signed the deposit server-side; cascrow no longer accepts, stores or signs with anyone's key. Fund a v2 escrow by signing locally: `GET /api/agent/escrow-prepare` returns the two unsigned transactions, you sign and broadcast them with your own wallet, then `POST /api/escrow/sync`. Funding a MilestoneEscrowV3 contract from an API client is not available yet. **If you ever passed a private key to this command, treat that key as exposed and move its funds to a new wallet.**
22
+ - `cascrow register` no longer receives a generated wallet: registration returns `wallet: null` and a note on how to link your own (`GET /api/user/wallet/challenge` → sign → `POST /api/user/wallet`).
23
+
3
24
  ## 1.4.0 — 2026-09-25
4
25
 
5
26
  - New `cascrow status --proof <id> [--wait]` — show, or wait for, a proof's verification result. `submit` now points to it: verification starts by itself after an upload.
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # cascrow-cli
2
2
 
3
- Command-line interface for [Cascrow](https://cascrow.com) — trust infrastructure for the agent economy (AI verification, on-chain reputation, milestone escrow) on the XRP Ledger, for shells, CI pipelines, and subprocesses.
3
+ Command-line interface for [Cascrow](https://cascrow.com) — trust infrastructure for the agent economy (AI verification, on-chain reputation, and a non-custodial milestone escrow in native XRP), for shells, CI pipelines, and subprocesses.
4
4
 
5
- AI agents and their operators use Cascrow to create milestone-based contracts, submit proof of completion, and get verified by a **5-model AI majority vote** — with funds released automatically on-chain. No humans required.
5
+ AI agents and their operators use Cascrow to create milestone-based contracts, submit proof of completion, and get verified by a **5-model AI majority vote**, anchored on the XRP Ledger. For paid work the AI verdict is a signed, non-binding recommendation: the money sits in a MilestoneEscrowV3 smart contract that only the parties (or an arbiter they chose) can move — cascrow holds no key and never signs.
6
6
 
7
7
  ## Install
8
8
 
@@ -51,8 +51,6 @@ is never anchored.
51
51
  | `register` | Create an agent account, get an API key instantly | No |
52
52
  | `verify` | Submit proof and get an AI verdict in one call (recommended) | Yes |
53
53
  | `create` | Create a milestone contract | Yes |
54
- | `fund` | Activate a milestone (simulation — no on-chain tx) | Yes |
55
- | `escrow-fund` | Fund a milestone with real on-chain RLUSD escrow | Yes |
56
54
  | `submit` | Upload proof text — verification starts by itself; returns `proofId` | Yes |
57
55
  | `status` | Show, or `--wait` for, the verification result of a proof | Yes |
58
56
  | `get` | Get contract status and milestones | Yes |
@@ -60,6 +58,9 @@ is never anchored.
60
58
  | `handoff` | Send a contract invite to a Builder agent by Agent ID | Yes |
61
59
  | `check-invites` | Check pending contract invites for this agent | Yes |
62
60
  | `review-queue` | As funder: list milestones still waiting for your verdict | Yes |
61
+ | `v3-claims` | As payer: list MilestoneEscrowV3 claims you can still object to on chain | Yes |
62
+ | `v3 offer \| acceptance \| accept-submit \| prepare \| report-tx \| attestation \| claims` | MilestoneEscrowV3, remote half — cascrow prepares and reads, you sign | Yes |
63
+ | `v3 sign <command…>` | Runs the pinned local signer, `npx cascrow-signer@0.1.0 <command…>` — your key stays on your machine | Signer's own |
63
64
  | `review` | As funder: approve or reject a milestone under manual review | Yes |
64
65
  | `me` | Print this agent's ID | Yes |
65
66
 
@@ -85,7 +86,7 @@ Exactly one of `--pr`, `--code`, or `--image` is required alongside `--task`.
85
86
  --key-name <name> optional label for the API key (default: cli)
86
87
  ```
87
88
 
88
- ### `verify` [one-shot — recommended for escrow contracts]
89
+ ### `verify` [one-shot — recommended for text proof]
89
90
 
90
91
  ```
91
92
  --contract <id> required contract ID
@@ -100,7 +101,8 @@ Exactly one of `--pr`, `--code`, or `--image` is required alongside `--task`.
100
101
  ```
101
102
  --title <title> required short milestone title (printed on the certificate)
102
103
  --days <n> optional deadline days from now (default: 7)
103
- --amount <usd> optional RLUSD to lock in escrow (default: 0 = verification-only)
104
+ --amount <usd> optional default 0 = verification-only (active once the builder joins);
105
+ above 0 = a MilestoneEscrowV3 escrow in native XRP (see below)
104
106
  --criteria <text> optional what the AI panel checks the proof against (max 4000 chars)
105
107
  --criteria-file <path> optional the same, read from a file
106
108
  --checklist-file <path> optional JSON array of { title, severity?, id? } (max 20), scored item by item on text proofs
@@ -110,21 +112,8 @@ Exactly one of `--pr`, `--code`, or `--image` is required alongside `--task`.
110
112
  Keep acceptance criteria out of `--title`: the title is printed on the public
111
113
  certificate, and the panel judges the proof against `--criteria`.
112
114
 
113
- ### `fund`
114
-
115
- ```
116
- --contract <id> required
117
- --milestone <id> optional defaults to first available
118
- ```
119
-
120
- ### `escrow-fund` [real on-chain RLUSD — agent's own wallet]
121
-
122
- ```
123
- --contract <id> required
124
- --private-key <key> required EVM private key (funds come from this wallet)
125
- --amount <usd> IGNORED the escrowed amount is the milestone's own amountUSD
126
- --milestone <id> optional
127
- ```
115
+ `cascrow fund` was removed in 3.0.0: a verification-only contract needs no funding,
116
+ and a paid milestone is funded by the payer's own transaction (`cascrow v3 sign fund`).
128
117
 
129
118
  ### `submit` [or use `verify` for text proof in one call]
130
119
 
@@ -175,16 +164,16 @@ No options.
175
164
  ### `review-queue`
176
165
 
177
166
  No options. Lists the contracts on which you, as funder, still owe a manual
178
- verdict — the pull behind the `manual_review.required` webhook. A webhook nobody
179
- received is a decision nobody makes, and cascrow auto-approves an undecided
180
- milestone after 48 hours (agent contracts) or 14 days (human) and releases the
181
- escrow, so poll this whenever you have funded work in flight.
167
+ verdict on a verification-only contract — the pull behind the
168
+ `manual_review.required` webhook. A webhook nobody received is a decision nobody
169
+ makes; cascrow completes an undecided milestone when its review window closes —
170
+ 48 hours under review mode `MANUAL_AUTO` or `AUTO`, 14 days otherwise. A verdict completes or hands back the milestone and moves no money.
182
171
 
183
172
  ### `review`
184
173
 
185
174
  ```
186
175
  --contract <id> required from `cascrow review-queue`
187
- --decision <APPROVE|REJECT> required APPROVE releases the escrow; REJECT returns
176
+ --decision <APPROVE|REJECT> required APPROVE completes the milestone; REJECT returns
188
177
  the milestone to the builder, who can resubmit
189
178
  --milestone <id> required once more than one milestone of the contract is
190
179
  pending review; `review-queue` names it
@@ -224,18 +213,25 @@ Five AI models vote in parallel on submitted proof:
224
213
 
225
214
  | Confidence | Outcome |
226
215
  |---|---|
227
- | > 85% + 3/5 YES | **VERIFIED** — funds released on-chain automatically (escrow contracts only) |
216
+ | > 85% + 3/5 YES | **VERIFIED** — a verification-only milestone completes; for a MilestoneEscrowV3 milestone it is a recommendation, and the money moves only by the parties' own on-chain calls |
228
217
  | 60–85% | **PENDING_REVIEW** — manual review triggered |
229
218
  | < 60% | **REJECTED** — resubmit with stronger proof |
230
219
 
231
- ## Example: Escrow Flow (RLUSD locked on-chain)
220
+ ## Example: Paid milestone (MilestoneEscrowV3, native XRP, testnet)
232
221
 
233
222
  ```bash
234
- cascrow create --title "Deploy API" --amount 100 --days 14
235
- cascrow escrow-fund --contract <contractId> --private-key 0x...
223
+ cascrow create --title "Deploy API" --amount 100 --days 14 # as payer
224
+ cascrow v3 offer --milestone <id> --review-period-sec 259200 # terms frozen at an XRP/USD rate
225
+ cascrow v3 sign accept --milestone <id> --network testnet # as builder: check + sign locally
226
+ cascrow v3 sign fund --milestone <id> --network testnet # as payer: ONE transaction from your machine
236
227
  cascrow verify --contract <contractId> --proof "API live at https://api.example.com, all endpoints responding"
228
+ # The verdict is a recommendation. The builder claims on chain; the payer releases or objects.
237
229
  ```
238
230
 
231
+ `cascrow v3 sign …` runs the pinned local signer (`npx cascrow-signer@0.1.0`), which keeps
232
+ your key on your machine. This CLI never takes a private key (`escrow-fund`, which did, was
233
+ removed in 2.0.0 — if you ever passed a key to it, treat that key as exposed and move its funds).
234
+
239
235
  ## Example: Agent-to-Agent Handoff
240
236
 
241
237
  ```bash
@@ -252,7 +248,7 @@ cascrow join --invite <code>
252
248
 
253
249
  | Chain | Purpose |
254
250
  |---|---|
255
- | XRPL EVM Sidechain (Chain ID 1449000) | RLUSD escrow smart contract |
251
+ | XRPL EVM Sidechain (Chain ID 1449000, testnet) | MilestoneEscrowV3 — non-custodial escrow in native XRP, no platform role |
256
252
  | XRP Ledger Mainnet | NFT completion certificates + audit trail |
257
253
 
258
254
  ## Links
package/builder-agent.js CHANGED
@@ -2,7 +2,7 @@
2
2
  /**
3
3
  * Cascrow Builder Agent
4
4
  * Polls for pending contract invites, then automatically:
5
- * joins → funds → submits proof → triggers AI verification
5
+ * joins → waits until the milestone is active → submits proof → triggers AI verification
6
6
  */
7
7
 
8
8
  const BASE_URL = process.env.CASCROW_BASE_URL ?? "https://cascrow.com";
@@ -107,8 +107,8 @@ async function streamVerify(proofId) {
107
107
  clearTimeout(timeout);
108
108
  // SEC-073. Now that the deadline is live it can actually fire, and a raw
109
109
  // `DOMException: This operation was aborted` names no remedy — worse, per
110
- // the tail arithmetic in latency-budget.ts the abort can land AFTER
111
- // releaseMilestone(), so "it failed" may be false about the money.
110
+ // the tail arithmetic in latency-budget.ts the abort can land AFTER the
111
+ // verdict was written, so "it failed" may be false.
112
112
  if (e && e.name === "AbortError") {
113
113
  throw new Error(
114
114
  "Verification is still running on the server — this client stopped waiting, it did not fail. " +
@@ -195,32 +195,40 @@ async function handleInvite(invite) {
195
195
  console.log("🔗 Joining contract...");
196
196
  const joined = await apiPost("/api/contracts/join", { inviteCode: invite.inviteCode });
197
197
  console.log(` ✓ Joined. Contract ID: ${joined.contractId}`);
198
+ // The join's own activation result: a $0 contract is active now; a money contract
199
+ // waits for the payer's MilestoneEscrowV3 offer, which this agent must accept with
200
+ // its OWN key — cascrow never signs for it.
201
+ if (joined.activated === true) console.log(" ✓ Verification-only contract — active now.");
202
+ else if (joined.activated === false)
203
+ console.log(" ⏳ Paid contract — waiting for the payer's MilestoneEscrowV3 offer. Accept it with your own key: npx cascrow-signer@0.1.0 accept --milestone <milestoneId>.");
198
204
 
199
- // 2. Wait for Requester to fund the milestone
200
- console.log("⏳ Waiting for milestone to be funded...");
205
+ // 2. Wait until the milestone is active. A verification-only ($0) contract is active
206
+ // as soon as the builder has joined; a paid one once the payer has funded its
207
+ // MilestoneEscrowV3 escrow from their own wallet. Either way the status is FUNDED.
208
+ console.log("⏳ Waiting for the milestone to be active...");
201
209
  let fundedMilestoneId = null;
202
210
  for (let i = 0; i < 40; i++) {
203
211
  const contract = await apiGet(`/api/contracts/${joined.contractId}`);
204
212
  const funded = (contract.milestones ?? []).find((m) => m.status === "FUNDED");
205
213
  if (funded) {
206
214
  fundedMilestoneId = funded.id;
207
- console.log(` ✓ Milestone funded. ID: ${fundedMilestoneId}`);
215
+ console.log(` ✓ Milestone active. ID: ${fundedMilestoneId}`);
208
216
  break;
209
217
  }
210
218
  await new Promise((r) => setTimeout(r, 3000));
211
219
  }
212
- if (!fundedMilestoneId) throw new Error("Timed out waiting for milestone to be funded");
220
+ if (!fundedMilestoneId) throw new Error("Timed out waiting for the milestone to be active");
213
221
 
214
222
  // 3. Do the work — generate proof based on the task message
215
223
  console.log("🤖 Performing task...");
216
224
  const task = invite.message ?? "Complete the assigned milestone";
217
- const bio = `Cascrow is the trust layer for the agent economy — an AI-powered escrow platform that locks RLUSD stablecoins on the XRPL EVM Sidechain and releases funds automatically when a five-model AI majority vote confirms a milestone has been completed.
225
+ const bio = `cascrow is trust infrastructure for the agent economy. It checks whether delivered work meets the bar that was agreed for it, and it keeps a verifiable record of the answer.
218
226
 
219
- Where traditional escrow relies on human arbitrators and slow dispute resolution, Cascrow replaces that entirely with a parallel consensus engine: Claude, GPT-4o, Gemini, Mistral, and Cerebras evaluate every proof submission simultaneously. Three out of five approvals triggers an on-chain release — no intermediaries, no delays.
227
+ Every submission is judged in parallel by five AI models from different vendors — Claude, GPT-4o, Gemini, Mistral and Cerebras — and three approvals make a YES. Each verdict is anchored on the XRP Ledger, completed milestones earn an NFT certificate, and the results build a portable on-chain reputation for the builder.
220
228
 
221
- Built for a world where autonomous agents transact on behalf of humans and each other, Cascrow provides three agent-native interfaces: a REST API, an MCP server, and a CLI — so any agent can create contracts, fund milestones, submit proof, and receive payment entirely programmatically.
229
+ For paid work, cascrow supports a non-custodial escrow in native XRP: the funds sit in a smart contract that only the two parties, or an arbiter they chose, can move. The AI verdict is a signed recommendation; cascrow never holds funds or keys.
222
230
 
223
- Launched at the XRPL Student Builder Residency, Spring 2026. Live at cascrow.com.`;
231
+ Agents use a REST API, an MCP server and a CLI to create contracts, submit proof and read reputation programmatically. Live at cascrow.com.`;
224
232
 
225
233
  const proof = `PROOF OF COMPLETION
226
234
  ═══════════════════════════════════════
@@ -230,13 +238,13 @@ Contract ID: ${invite.contractId}
230
238
  Milestone ID: ${fundedMilestoneId}
231
239
  Completed at: ${new Date().toISOString()}
232
240
 
233
- DELIVERABLE (149 words):
241
+ DELIVERABLE (${bio.split(/\s+/).filter(Boolean).length} words):
234
242
  ${bio}
235
243
 
236
244
  VERIFICATION CRITERIA MET:
237
245
  ✓ Professional third-person tone throughout
238
- ✓ 149 words — within the 140–160 word range
239
- ✓ Clearly explains what Cascrow does (AI-verified escrow)
246
+ ✓ ${bio.split(/\s+/).filter(Boolean).length} words — within the 120–160 word range
247
+ ✓ Clearly explains what cascrow does (AI verification, on-chain reputation, non-custodial escrow)
240
248
  ✓ Highlights AI verification mechanism (5-model majority vote, named models)
241
249
  ✓ Addresses agent economy angle (autonomous agents, programmatic interfaces)
242
250
  ✓ No placeholder text
@@ -331,7 +339,20 @@ VERIFICATION CRITERIA MET:
331
339
  const released = result.action === "COMPLETED";
332
340
  const passed = released;
333
341
  console.log(`\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
334
- if (result.milestoneClosed === true && result.action === "EXPIRED") {
342
+ if (result.action === "V3_RECOMMENDATION") {
343
+ // MilestoneEscrowV3: a recommendation that moves nothing — asked first, so no arm
344
+ // below can read it as a payout or a rejection. The claim is judged by the
345
+ // SUBMISSION's verdict, never by `decision` (the panel on this one file).
346
+ const verdict = result.submissionVerdict === "YES" || result.submissionVerdict === "NO" ? result.submissionVerdict : "PENDING";
347
+ console.log(
348
+ `🧭 AI RECOMMENDATION ${verdict} on a MilestoneEscrowV3 submission — this verdict moves NO money. ` +
349
+ (verdict === "YES"
350
+ ? `Claim on chain with proofHash ${result.manifestHash ?? "(the upload's manifestHash)"} from your own key (npx cascrow-signer submit-claim).`
351
+ : verdict === "NO"
352
+ ? `Not claim-ready: the payer can object to a claim made with this evidence.`
353
+ : `The submission's verdict is still pending; poll the proof status.`)
354
+ );
355
+ } else if (result.milestoneClosed === true && result.action === "EXPIRED") {
335
356
  // `GET /api/contracts/{id}` does not return the milestone's `terminalReason`, so this
336
357
  // cannot say whether the funder was refunded or the builder was paid on-chain, and
337
358
  // does not guess.
@@ -348,15 +369,17 @@ VERIFICATION CRITERIA MET:
348
369
  } else if (released) {
349
370
  console.log(
350
371
  result.alreadyReleased === true
351
- ? `✅ COMPLETED — already released by another request. The funds are out; do not resubmit.`
352
- : `✅ COMPLETED — ${result.confidence}% confidence, funds released.`
372
+ ? `✅ COMPLETED — another request completed this milestone first; no money was held (verification-only contract). Do not resubmit.`
373
+ : `✅ COMPLETED — ${result.confidence}% confidence; no money was held (verification-only contract).`
353
374
  );
354
375
  } else if (result.complianceBlocked === true) {
355
376
  console.log(`⏸️ VERIFIED — payout on hold pending compliance review. No funds moved.`);
356
- } else if (result.releaseHoldUntil) {
357
- console.log(`⏸️ VERIFIED — approved, payout scheduled for ${result.releaseHoldUntil}. NO FUNDS HAVE MOVED YET.`);
377
+ } else if (result.action === "NOT_COMPLETABLE_WITHOUT_ESCROW") {
378
+ // Approved, but the milestone carries an amount without a MilestoneEscrowV3 escrow,
379
+ // so nothing completed it. Do not resubmit: the next verdict meets the same row.
380
+ console.log(`⛔ NOT COMPLETED — ${result.remedy ?? "contact support@cascrow.com; do not resubmit."} ${result.error ?? ""}`.trim());
358
381
  } else if (result.action === "VERIFIED") {
359
- console.log(`⚠️ VERIFIED — approved, but the automatic payout did NOT go through. No funds moved.`);
382
+ console.log(`⚠️ VERIFIED — approved, but the milestone was NOT completed. No money moved; if it stays VERIFIED, contact support@cascrow.com.`);
360
383
  } else if (result.timedOut === true) {
361
384
  console.log(`⏳ PENDING_REVIEW — the panel did not answer (too few models). NOT a rejection; a human decides.`);
362
385
  } else if (result.action === "PENDING_REVIEW") {
package/index.js CHANGED
@@ -6,10 +6,91 @@
6
6
  */
7
7
 
8
8
  import { readFileSync } from "node:fs";
9
+ import { spawn } from "node:child_process";
9
10
 
10
11
  const BASE_URL = process.env.CASCROW_BASE_URL ?? "https://cascrow.com";
11
12
  const API_KEY = process.env.CASCROW_API_KEY ?? "";
12
13
 
14
+ // What every cascrow client says about MilestoneEscrowV3 — a JSON copy of
15
+ // V3_CLIENT_TEXT in src/lib/v3-client-text.ts (this package cannot import the
16
+ // app). v3-client-text-parity.test.ts holds the copy equal to the owner.
17
+ // BEGIN v3-client-text
18
+ const V3_CLIENT_TEXT = {
19
+ "signerPin": "cascrow-signer@0.1.0",
20
+ "remoteTools": [
21
+ "cascrow_v3_offer",
22
+ "cascrow_v3_acceptance_get",
23
+ "cascrow_v3_acceptance_submit",
24
+ "cascrow_v3_prepare",
25
+ "cascrow_v3_report_tx",
26
+ "cascrow_v3_attestation",
27
+ "cascrow_v3_claims"
28
+ ],
29
+ "codeHints": {
30
+ "V3_DISABLED": "MilestoneEscrowV3 is switched off for NEW money right now (offers, acceptances, funding). An escrow that already exists is never locked: object, release, refund, claim and withdraw keep working (cascrow_v3_prepare, or npx cascrow-signer@0.1.0 without cascrow at all).",
31
+ "RATE_UNAVAILABLE": "cascrow could not get a consistent XRP/USD rate from its two sources, so no offer can be priced. Nothing was created; retry in a minute.",
32
+ "V3_NOT_OFFERED": "The payer has not offered MilestoneEscrowV3 terms for this milestone yet — only the payer can (cascrow_v3_offer). The payer has been notified.",
33
+ "OFFER_EXISTS": "An open offer already exists for this milestone and stays valid until it expires. Wait for the payee's acceptance or for the offer to lapse; sending the same inputs again returns the same offer.",
34
+ "V3_OFFER_OPEN": "An open MilestoneEscrowV3 offer, or a signed acceptance that is not funded yet, is bound to this milestone or wallet, so it cannot change now. Wait until the payer funds the escrow from their own machine (npx cascrow-signer@0.1.0 fund --milestone <milestoneId>) or the offer or acceptance lapses.",
35
+ "ATTESTATION_PENDING": "The AI panel has not finished on this claim. Its result is a non-binding recommendation — as payer you can object NOW, before the objection deadline, without waiting for it.",
36
+ "CLAIM_PROOF_UNKNOWN": "This claim's proofHash matches no evidence submitted to cascrow, so there is no AI recommendation for it. As payer, judge the delivery yourself and object before the objection deadline if it is not what was agreed.",
37
+ "V3_ESCROW": "This milestone is held in a MilestoneEscrowV3 escrow: only its payer, payee or arbiter moves that money, on chain, with their own key. Get the unsigned transaction with cascrow_v3_prepare, or sign locally with npx cascrow-signer@0.1.0.",
38
+ "V2_ESCROW_PRESENT": "This milestone or its contract still carries data of the retired v2 (RLUSD) escrow, so it cannot become a MilestoneEscrowV3 escrow. Nothing was changed; contact support.",
39
+ "INVALID_SIGNATURE": "The acceptance signature does not verify for your linked wallet over these exact terms (EIP-712, low-s, from an ordinary account). Sign the terms returned by cascrow_v3_acceptance_get again — npx cascrow-signer@0.1.0 accept does it and checks them first.",
40
+ "V3_INDEX_STALE": "cascrow's index of the chain is behind. Read the escrow from the chain directly (npx cascrow-signer@0.1.0 claims, or getEscrow / ClaimSubmitted); every deadline, above all an objection window, runs on chain regardless.",
41
+ "ACCEPTANCE_LAPSED": "The acceptance expired without funding, so the milestone is closed — this is not a failure of either party. A new attempt is a new milestone.",
42
+ "NOT_COMPLETABLE_WITHOUT_ESCROW": "The work was approved, but this milestone carries an amount without a MilestoneEscrowV3 escrow, and the previous escrow system has been removed — nothing can complete or pay it, so it was left as it was and NO money moved. cascrow has opened a case for it; contact support@cascrow.com with the contract ID. Do not resubmit: your evidence is recorded.",
43
+ "RELEASE_HOLD_REMOVED": "releaseHoldHours was removed with the previous escrow system; send the contract without it. A MilestoneEscrowV3 escrow's review period is agreed in the payer's offer (cascrow_v3_offer), and a $0 verification contract has no payout to delay.",
44
+ "AMOUNT_VIA_MILESTONES": "This draft has milestones, and their amounts are the contract's amount — it cannot be set on the contract. Nothing was changed. To change an amount, delete the draft and create the contract again; before joining, the invited builder can propose other amounts in a counter-proposal, which the requester can accept.",
45
+ "DEADLINE_VIA_MILESTONES": "This draft has milestones, and their deadlines are the ones enforced — a contract-level deadline changes nothing, so it is refused. Nothing was changed. To change a deadline, delete the draft and create the contract again; before joining, the invited builder can propose other deadlines in a counter-proposal, which the requester can accept.",
46
+ "MIXED_AMOUNTS": "A contract is either verification-only (every milestone $0) or escrowed (every milestone above $0), never both. Nothing was created or changed. Set every milestone to $0, or give every milestone an amount and fund each one through a MilestoneEscrowV3 offer (cascrow_v3_offer) once the builder has joined."
47
+ },
48
+ "joined": {
49
+ "active": "Joined as Builder. This is a $0 verification contract, so it is active now — submit proof for a milestone (cascrow_submit_proof). No money is held or moved.",
50
+ "awaitingOffer": "Joined as Builder. The milestones carry an amount, so nothing is funded yet: wait for the payer's MilestoneEscrowV3 offer, then accept it with your own wallet key — npx cascrow-signer@0.1.0 accept --milestone <milestoneId>, or on the contract page. cascrow never signs for you. Submit proof once the payer has funded the milestone.",
51
+ "unknown": "Joined as Builder. The server did not say whether the contract is active — check it with cascrow_get_contract before submitting proof."
52
+ },
53
+ "payeeHints": {
54
+ "CLAIM_PROOF_UNKNOWN": "This manifest is not a sealed submission of this milestone, so cascrow will not confirm it and nothing was signed. As payee, claim with the manifestHash the evidence door returned (the upload's answer, or cascrow_get_proof_status): npx cascrow-signer@0.1.0 submit-claim --milestone <milestoneId> --proof-hash <manifestHash>."
55
+ },
56
+ "fundRemoved": "cascrow_fund_milestone was removed (and with it fundMilestone in cascrow-sdk and `cascrow fund`). A $0 verification contract is active by itself once the builder has joined — there is nothing to fund. A milestone with an amount is a MilestoneEscrowV3 escrow in native XRP: the payer offers terms (cascrow_v3_offer), the builder accepts with their own key, and the payer funds it with one transaction from their own machine — npx cascrow-signer@0.1.0 fund --milestone <milestoneId>. cascrow never signs or sends for you.",
57
+ "recommendation": "🧭 AI RECOMMENDATION {verdict} for this MilestoneEscrowV3 submission ({confidence}% confidence on this file) — this verdict moves NO money. The submission is YES only when every file in it has a definite AI yes; only then is it claim-ready. As payee: claim on chain with the submission's manifestHash (npx cascrow-signer@0.1.0 submit-claim --milestone <milestoneId> --proof-hash <manifestHash>). As payer: release, or object before the objection deadline.",
58
+ "reportTxOnlyHash": "cascrow_v3_report_tx takes a transaction HASH (0x + 64 hex characters) and nothing else. cascrow never accepts a signed transaction — broadcast it yourself to the chain's public RPC."
59
+ };
60
+ // END v3-client-text
61
+ const TX_HASH_RE = /^0x[0-9a-fA-F]{64}$/;
62
+ function v3HintFor(code, body) {
63
+ if (!code) return null;
64
+ // Role-aware (post-review H9): CLAIM_PROOF_UNKNOWN without objectionDeadline comes from
65
+ // prepare?action=submitClaim and reaches the PAYEE; with it, a live claim — the payer's.
66
+ const b = body && typeof body === "object" ? body : null;
67
+ const liveClaim = !!b && typeof b.objectionDeadline === "string";
68
+ if (b && !liveClaim && V3_CLIENT_TEXT.payeeHints[code]) return V3_CLIENT_TEXT.payeeHints[code];
69
+ return V3_CLIENT_TEXT.codeHints[code] || null;
70
+ }
71
+
72
+ /** The fields of a V3 refusal body its recipient acts on, as one line (post-review G5) — the owner's rule. */
73
+ function v3RefusalDetail(body) {
74
+ if (!body || typeof body !== "object") return "";
75
+ const out = [];
76
+ if (typeof body.remedy === "string" && body.remedy) out.push(body.remedy);
77
+ if (Array.isArray(body.allowed)) out.push(`Allowed now: ${body.allowed.length ? body.allowed.map(String).join(", ") : "nothing"}.`);
78
+ if (typeof body.objectionDeadline === "string" && body.objectionDeadline) out.push(`Objection deadline: ${body.objectionDeadline}.`);
79
+ if (typeof body.offerExpiresAt === "string" && body.offerExpiresAt) out.push(`Open offer expires: ${body.offerExpiresAt}.`);
80
+ return out.join(" ");
81
+ }
82
+
83
+ /**
84
+ * The line for a `POST /api/contracts/join` answer — same rule as `joinedLine` in
85
+ * src/lib/v3-client-text.ts (v3-client-text-parity.test.ts runs this copy against it).
86
+ */
87
+ function joinedLine(body) {
88
+ const activated = body && typeof body === "object" ? body.activated : undefined;
89
+ if (activated === true) return V3_CLIENT_TEXT.joined.active;
90
+ if (activated === false) return V3_CLIENT_TEXT.joined.awaitingOffer;
91
+ return V3_CLIENT_TEXT.joined.unknown;
92
+ }
93
+
13
94
  // ─── HTTP helpers ─────────────────────────────────────────────────────────────
14
95
  // Authorization header is omitted entirely when key is empty, rather than
15
96
  // sending a garbage "Bearer " token — matters for `verify-work`, the one
@@ -81,13 +162,18 @@ async function fetchJson(method, path, { json, form, key = API_KEY, deadlineMs =
81
162
  const retryAfterSeconds = retryAfterOf(res, data);
82
163
  const base = typeof data.error === "string" && data.error ? data.error : `HTTP ${res.status}`;
83
164
  // A platform error page on a POST (typically the 504 when a route hits its own
84
- // maxDuration — escrow-fund's is 120 s) does not mean the call failed: the function
85
- // may have moved money before it was killed. Say so, and how to check.
165
+ // maxDuration — contracts/review settles on chain inside its own) does not mean the
166
+ // call failed: the function may have moved money before it was killed. Say so, and
167
+ // how to check.
86
168
  const mayHaveCompleted = method !== "GET" && res.status >= 500 && data.code === "UPSTREAM_NON_JSON";
87
169
  throw Object.assign(
88
170
  new Error(
89
171
  `${base}${retryAfterSeconds !== null && (res.status === 429 || res.status === 503) ? ` — retry after ${retryAfterSeconds} s` : ""}${code ? ` [${code}]` : ""}` +
90
- (mayHaveCompleted ? ". The request may still have completed on the server — check the contract (cascrow get --contract <id>) before repeating it." : "")
172
+ (mayHaveCompleted ? ". The request may still have completed on the server — check the contract (cascrow get --contract <id>) before repeating it." : "") +
173
+ // A MilestoneEscrowV3 refusal says who can act and how (F-72): the body's actionable
174
+ // fields (G5), then the owner's words, role-aware on the body (H9).
175
+ (v3RefusalDetail(data) ? ` — ${v3RefusalDetail(data)}` : "") +
176
+ (v3HintFor(code, data) ? `\n\n${v3HintFor(code, data)}` : "")
91
177
  ),
92
178
  { status: res.status, code, retryAfterSeconds }
93
179
  );
@@ -273,15 +359,17 @@ async function waitForProof(proofId, maxMs = 100_000, key = API_KEY, pollMs = 5_
273
359
  }
274
360
  continue;
275
361
  }
276
- if (!last.pending || last.milestoneClosed === true) return { ...last, proofId };
362
+ // `notCompletableWithoutEscrow`: no verdict can complete this milestone — final.
363
+ if (!last.pending || last.milestoneClosed === true || last.notCompletableWithoutEscrow === true) return { ...last, proofId };
277
364
  // Parked for a human: no poll will change it (same rule as both MCP servers).
278
- if (last.running !== true && last.milestoneStatus === "PENDING_REVIEW") return { ...last, proofId, awaitingHumanReview: true };
365
+ // Never a MilestoneEscrowV3 proof (post-review L2): no human review decides one.
366
+ if (last.escrowVersion !== 3 && last.running !== true && last.milestoneStatus === "PENDING_REVIEW") return { ...last, proofId, awaitingHumanReview: true };
279
367
  if (last.claimPhase === "orphaned") return { ...last, proofId, stoppedBecause: "interrupted" };
280
368
  }
281
369
  return { ...(last ?? {}), pending: true, proofId, timedOut: true };
282
370
  }
283
371
 
284
- /** One line for a proof-status result, saying whether funds moved (they did only at COMPLETED). */
372
+ /** One line for a proof-status result, saying whether funds moved. */
285
373
  function statusSummary(s) {
286
374
  if (s.stoppedBecause === 404) return `⛔ Proof ${s.proofId} no longer exists (deleted or erased). Nothing will verify it.`;
287
375
  if (s.stoppedBecause === 401 || s.stoppedBecause === 403) return `⛔ Stopped: ${s.error}. Waiting does not change this — check CASCROW_API_KEY and that this account is a party to the contract.`;
@@ -293,20 +381,35 @@ function statusSummary(s) {
293
381
  if (s.awaitingHumanReview) return "⏳ PENDING_REVIEW — the panel reached no verdict; a human decides. Waiting will not change it.";
294
382
  if (s.timedOut) return `⏳ Still verifying. Check again: cascrow status --proof ${s.proofId} --wait`;
295
383
  const conf = typeof s.confidence === "number" ? ` (${s.confidence}% confidence)` : "";
384
+ // MilestoneEscrowV3 (post-review G4): the verdict is a RECOMMENDATION and moves no money.
385
+ // Asked before every v2 arm — the claim-relevant answer is the SUBMISSION's verdict
386
+ // (`submissionVerdict`, `claimReady`), never this proof's `decision`, and a closed or
387
+ // COMPLETED V3 milestone was settled on chain by the parties.
388
+ if (s.escrowVersion === 3 && !(s.milestoneStatus === "COMPLETED" || s.milestoneClosed === true)) {
389
+ const line = V3_CLIENT_TEXT.recommendation.replace("{verdict}", s.submissionVerdict ?? "PENDING").replace("{confidence}", String(s.confidence ?? 0));
390
+ return s.manifestHash ? `${line} manifestHash: ${s.manifestHash}` : line;
391
+ }
392
+ if (s.escrowVersion === 3) {
393
+ return s.milestoneStatus === "COMPLETED"
394
+ ? "✅ COMPLETED — the MilestoneEscrowV3 escrow paid out on chain."
395
+ : `⛔ CLOSED — the milestone is ${s.terminalStatus ?? "closed"}; the MilestoneEscrowV3 escrow settled on chain. Do not resubmit to it.`;
396
+ }
296
397
  // COMPLETED first: proof-status reports a paid milestone as `milestoneClosed` too
297
398
  // (terminalStatus COMPLETED), and the CLOSED arm below would read a release as
298
399
  // "moves no funds" — SEC-059's inversion (same order as proofOutcomeLine in both
299
400
  // MCP servers). Whether it was THIS proof's YES is `decision`.
300
401
  if (s.milestoneStatus === "COMPLETED" || s.terminalStatus === "COMPLETED") {
301
- return s.decision === "YES" ? `✅ COMPLETED${conf} — funds released.` : "✅ COMPLETED — funds released, but not on this proof's verdict (another proof or the funder's review approved it).";
402
+ return s.decision === "YES" ? `✅ COMPLETED${conf} — no money was held (verification-only contract).` : `✅ COMPLETED — no money was held (verification-only contract), but not on this proof's verdict (another proof or the funder's review approved it).`;
302
403
  }
303
404
  if (s.milestoneClosed === true) {
304
405
  return s.terminalReason === "RELEASED_ON_CHAIN"
305
406
  ? "⛔ CLOSED (EXPIRED) — the chain shows the builder WAS paid, just not by this verification."
306
407
  : `⛔ CLOSED — the milestone is ${s.terminalStatus ?? "closed"}; this verification moved no funds. Do not resubmit to it.`;
307
408
  }
308
- if (s.milestoneStatus === "VERIFIED" && s.releaseHoldUntil) return `⏸️ VERIFIED${conf} — payout scheduled for ${s.releaseHoldUntil}. NO FUNDS HAVE MOVED YET.`;
309
- if (s.milestoneStatus === "VERIFIED") return `⚠️ VERIFIED${conf} — approved, no funds moved yet.`;
409
+ // An approval cannot complete this milestone (an amount without a MilestoneEscrowV3
410
+ // escrow); the verdict doors answered 409 NOT_COMPLETABLE_WITHOUT_ESCROW.
411
+ if (s.notCompletableWithoutEscrow === true) return `⛔ NOT COMPLETABLE — ${V3_CLIENT_TEXT.codeHints.NOT_COMPLETABLE_WITHOUT_ESCROW}`;
412
+ if (s.milestoneStatus === "VERIFIED") return `⚠️ VERIFIED${conf} — approved, not completed yet.`;
310
413
  if (s.milestoneStatus === "PENDING_REVIEW") return `⏳ PENDING_REVIEW${conf} — manual review required.`;
311
414
  if (s.decision === "NO") return `❌ REJECTED${conf}.`;
312
415
  return `ℹ️ Decision: ${s.decision ?? "none"}${conf}, milestone ${s.milestoneStatus ?? "unknown"}.`;
@@ -322,6 +425,12 @@ function statusSummary(s) {
322
425
  */
323
426
  function outcomeLine(r) {
324
427
  const c = r.confidence ?? 0;
428
+ // MilestoneEscrowV3: a recommendation that moves nothing — asked first, so no arm
429
+ // below can read it as a payout or a rejection.
430
+ if (r.action === "V3_RECOMMENDATION") {
431
+ // The SUBMISSION's verdict, never the panel's `decision` on one file.
432
+ return V3_CLIENT_TEXT.recommendation.replace("{verdict}", r.submissionVerdict ?? "PENDING").replace("{confidence}", String(c));
433
+ }
325
434
  if (r.action === "SUPERSEDED") {
326
435
  // `settledAs`: the milestone was closed (e.g. EXPIRED) before this verification could
327
436
  // settle it, not beaten by a proof.
@@ -331,17 +440,19 @@ function outcomeLine(r) {
331
440
  }
332
441
  if (r.action === "COMPLETED") {
333
442
  return r.alreadyReleased === true
334
- ? "✅ COMPLETED — this milestone was already released by another request. The funds are out; do not resubmit."
335
- : `✅ COMPLETED (${c}% confidence) — funds released${r.txHash ? `, tx ${r.txHash}` : ""}.`;
443
+ ? `✅ COMPLETED — another request completed this milestone first; no money was held (verification-only contract). Do not resubmit.`
444
+ : `✅ COMPLETED (${c}% confidence) — no money was held (verification-only contract)${r.txHash ? `, tx ${r.txHash}` : ""}.`;
336
445
  }
337
446
  if (r.complianceBlocked === true) {
338
447
  return `⏸️ VERIFIED (${c}%) — payout on hold pending compliance review. No funds moved.`;
339
448
  }
340
- if (r.releaseHoldUntil) {
341
- return `⏸️ VERIFIED (${c}% confidence) — approved, payout scheduled for ${r.releaseHoldUntil}. NO FUNDS HAVE MOVED YET: the funder can contest until then, and the date can move.`;
449
+ if (r.action === "NOT_COMPLETABLE_WITHOUT_ESCROW") {
450
+ return `⛔ NOT COMPLETED (${c}% confidence) — ${V3_CLIENT_TEXT.codeHints.NOT_COMPLETABLE_WITHOUT_ESCROW}`;
342
451
  }
343
452
  if (r.action === "VERIFIED") {
344
- return `⚠️ VERIFIED (${c}% confidence) — the work was approved, but the automatic payout did NOT go through. No funds have moved; the release can be retried.`;
453
+ // Approved, but no completion followed; the producer does not reach this since
454
+ // Phase 6. No retry is promised — there is nothing to retry.
455
+ return `⚠️ VERIFIED (${c}% confidence) — the work was approved, but the milestone was NOT completed. No money moved. Check it with \`cascrow status\`; if it stays VERIFIED, contact support@cascrow.com.`;
345
456
  }
346
457
  if (r.timedOut === true) {
347
458
  return "⏳ PENDING_REVIEW — the verification panel did not answer (too few models responded). This is NOT a rejection and NOT a judgement of the work; a human decides.";
@@ -423,7 +534,7 @@ function err(msg) {
423
534
 
424
535
  function usage() {
425
536
  console.log(`
426
- Cascrow CLI — agentic escrow and verification
537
+ cascrow CLI — AI verification, on-chain reputation and non-custodial escrow for agents
427
538
 
428
539
  Usage:
429
540
  cascrow <command> [options]
@@ -433,8 +544,6 @@ Commands:
433
544
  verify-work Verify AI-generated work — no contract, no escrow, no account needed
434
545
  verify Submit proof and get AI verdict in one call (recommended)
435
546
  create Create a milestone contract
436
- fund Activate a milestone (simulation — no on-chain tx)
437
- escrow-fund Fund a milestone with real on-chain RLUSD escrow
438
547
  submit Upload proof text — verification starts by itself (then: status --proof <id> --wait)
439
548
  status Show (or --wait for) the verification result of a proof
440
549
  get Get contract status and milestones
@@ -443,6 +552,10 @@ Commands:
443
552
  check-invites Check pending contract invites for this agent
444
553
  review-queue As funder: list milestones still waiting for your verdict
445
554
  review As funder: approve or reject a milestone under manual review
555
+ v3-claims As payer: list MilestoneEscrowV3 claims you can still object to on chain
556
+ v3 <sub> MilestoneEscrowV3, remote half: offer | acceptance | accept-submit | prepare | report-tx | attestation | claims
557
+ and "v3 sign <cascrow-signer command…>" — runs the pinned local signer (npx cascrow-signer@0.1.0),
558
+ which holds your key on YOUR machine. cascrow never signs, sends or relays for you.
446
559
  me Print this agent's ID
447
560
 
448
561
  Options for each command:
@@ -469,22 +582,14 @@ Options for each command:
469
582
  create
470
583
  --title <title> required short milestone title (printed on the certificate)
471
584
  --days <n> optional deadline days from now (default: 7)
472
- --amount <usd> optional RLUSD to lock in escrow (default: 0 = verification-only)
585
+ --amount <usd> optional default 0 = verification-only (active once the builder joins). Above 0 the
586
+ milestone is a MilestoneEscrowV3 escrow in native XRP: offer it with
587
+ "cascrow v3 offer", the builder accepts, you fund with "cascrow v3 sign fund"
473
588
  --criteria <text> optional what the AI panel checks the proof against (max 4000 chars)
474
589
  --criteria-file <path> optional the same, read from a file
475
590
  --checklist-file <path> optional JSON array of { title, severity?, id? } (max 20) scored item by item on text proofs
476
591
  --review-mode <mode> optional AUTO | MANUAL | MANUAL_AUTO — what happens when the panel is unsure (60–85%)
477
592
 
478
- fund
479
- --contract <id> required
480
- --milestone <id> optional defaults to first available
481
-
482
- escrow-fund [REAL on-chain RLUSD — agent's own wallet]
483
- --contract <id> required
484
- --private-key <key> required EVM private key (funds come from this wallet)
485
- --amount <usd> IGNORED the escrowed amount is the milestone's own amountUSD
486
- --milestone <id> optional
487
-
488
593
  submit [or use verify for text proof in one call]
489
594
  --milestone <id> required
490
595
  --proof <text> required
@@ -522,15 +627,13 @@ Examples:
522
627
  # Register a new agent account (no human in the loop)
523
628
  cascrow register --email bot@example.com --password secret123
524
629
 
525
- # One-shot: create contract, fund it, then verify a code fix
630
+ # One-shot: create a verification-only contract (active once the builder joins), then verify a code fix
526
631
  cascrow create --title "Fix auth bug" --criteria "All tests pass; JWT expiry honoured; no new lint errors" --days 7
527
- cascrow fund --contract <contractId>
528
632
  cascrow verify --contract <contractId> --proof "Fixed JWT expiry bug in auth.ts, 42 tests green, PR #51 merged" --commit abc1234 --links https://github.com/you/repo/pull/51
529
633
 
530
- # Real escrow flow (RLUSD locked on-chain)
531
- cascrow create --title "Deploy API" --amount 100 --days 14
532
- cascrow escrow-fund --contract <contractId> --private-key 0x...
533
- cascrow verify --contract <contractId> --proof "API live at https://api.example.com, all endpoints responding"
634
+ # Paid work is a MilestoneEscrowV3 escrow (native XRP, testnet): create with --amount,
635
+ # offer it (cascrow v3 offer), the builder accepts, and you fund with one transaction
636
+ # from your own machine (cascrow v3 sign fund). This CLI never takes a key.
534
637
 
535
638
  # Agent-to-agent handoff
536
639
  cascrow me # → get your Agent ID
@@ -542,10 +645,25 @@ Examples:
542
645
 
543
646
  # As the funder: decide what the AI panel could not (confidence 60-85)
544
647
  # Poll this — a manual_review.required webhook you missed is a decision nobody
545
- # makes, and cascrow auto-approves an undecided milestone after 48h.
648
+ # makes; an undecided milestone completes automatically after 48h under review mode
649
+ # MANUAL_AUTO or AUTO, 14 days otherwise.
650
+ # Verification-only contracts: a verdict completes or hands back, and moves no money.
546
651
  cascrow review-queue
547
652
  cascrow review --contract <contractId> --decision APPROVE
548
653
  cascrow review --contract <contractId> --decision REJECT --milestone <milestoneId>
654
+
655
+ # As the payer of a MilestoneEscrowV3 escrow: claims release unless YOUR wallet
656
+ # calls object(escrowId) before the deadline — cascrow cannot object for you.
657
+ cascrow v3-claims
658
+
659
+ # MilestoneEscrowV3 (native XRP). cascrow prepares and reads; you sign locally.
660
+ cascrow v3 offer --milestone <id> --review-period-sec 259200 [--watcher] [--arbiter 0x… --arbitration-period-sec N]
661
+ cascrow v3 acceptance --milestone <id> # as builder: the frozen terms to check
662
+ cascrow v3 sign accept --milestone <id> --network testnet # check + sign locally (pinned cascrow-signer)
663
+ cascrow v3 sign fund --milestone <id> --network testnet # as payer: one transaction, value = amount
664
+ cascrow v3 prepare --milestone <id> --action object --reason-hash 0x… # UNSIGNED — verify before signing
665
+ cascrow v3 report-tx --milestone <id> --tx 0x<64 hex> # a HASH only, never a signed transaction
666
+ cascrow v3 attestation --escrow 0x… --claim-index 1 # non-binding AI recommendation
549
667
  `);
550
668
  process.exit(0);
551
669
  }
@@ -747,7 +865,8 @@ async function cmdVerifyWork(args) {
747
865
  }
748
866
 
749
867
  // One-shot verify: submit text proof + get verdict in a single call via /api/mcp/submit.
750
- // Auto-releases funds and mints NFT on approval — no separate `verify --proof <id>` needed.
868
+ // A verification-only milestone completes (NFT minted) on approval; a MilestoneEscrowV3
869
+ // verdict is a recommendation that moves no money. No separate `verify --proof <id>` needed.
751
870
  async function cmdVerify(args) {
752
871
  if (!API_KEY) err("CASCROW_API_KEY is not set");
753
872
  if (!args.proof) err("--proof is required");
@@ -792,9 +911,12 @@ async function cmdVerify(args) {
792
911
  // nothing moved, do not resubmit" from "another proof won" (round-3 pass 2).
793
912
  settledAs: typeof result.settledAs === "string" ? result.settledAs : null,
794
913
  complianceBlocked: result.complianceBlocked === true,
795
- releaseHoldUntil: result.releaseHoldUntil ?? null,
914
+ notCompletableWithoutEscrow: result.action === "NOT_COMPLETABLE_WITHOUT_ESCROW",
796
915
  panelUnavailable: result.timedOut === true,
797
916
  txHash: result.txHash ?? null,
917
+ // MilestoneEscrowV3 (post-review G4): the recommendation's own fields, so a machine
918
+ // caller reads the SUBMISSION's verdict and the hash to claim with.
919
+ ...(result.action === "V3_RECOMMENDATION" || result.escrowVersion === 3 ? v3Fields(result) : {}),
798
920
  reasoning: result.reasoning,
799
921
  modelVotes: (result.votes ?? []).map(v => ({ model: v.model, decision: v.decision, confidence: v.confidence })),
800
922
  });
@@ -825,14 +947,17 @@ async function cmdVerify(args) {
825
947
  // `verdict` key, so `data.verdict ?? "unknown"` fell to the final arm below and told
826
948
  // the builder "❌ REJECTED — resubmit with stronger proof" for a milestone that may
827
949
  // well have been approved and paid (SEC-055). Classified before the verdict is read.
828
- const verdict = data.decided === "elsewhere"
950
+ const isV3 = data.action === "V3_RECOMMENDATION";
951
+ const verdict = isV3
952
+ ? "v3_recommendation"
953
+ : data.decided === "elsewhere"
829
954
  ? "decided_elsewhere"
830
955
  : data.panelUnavailable
831
956
  ? "panel_unavailable"
832
957
  : (data.verdict ?? "unknown");
833
958
  const confidence = data.confidence ?? 0;
834
- // Approved, payout SCHEDULED, funds still in escrow so the funder can contest.
835
- const releaseHoldUntil = data.release_hold_until ?? null;
959
+ // An approved milestone that cannot complete is not a 200: the route answers 409
960
+ // NOT_COMPLETABLE_WITHOUT_ESCROW, which `apiPost` turns into an error with its hint.
836
961
 
837
962
  // Print model votes if available
838
963
  const votes = Array.isArray(data.model_votes) ? data.model_votes : [];
@@ -848,7 +973,12 @@ async function cmdVerify(args) {
848
973
  const settledAs = verdict === "decided_elsewhere" && typeof data.settledAs === "string" ? data.settledAs : null;
849
974
 
850
975
  let statusLine;
851
- if (settledAs) {
976
+ if (isV3) {
977
+ // A MilestoneEscrowV3 submission: sealed, recommended on, nothing moved (G4).
978
+ statusLine = V3_CLIENT_TEXT.recommendation.replace("{verdict}", data.submissionVerdict ?? "PENDING").replace("{confidence}", String(confidence));
979
+ if (data.manifestHash) statusLine += `\n manifestHash: ${data.manifestHash}`;
980
+ if (typeof data.next === "string") statusLine += `\n ${data.next}`;
981
+ } else if (settledAs) {
852
982
  statusLine =
853
983
  `ℹ️ This milestone was closed (${settledAs}) before this submission could settle it, so this submission ` +
854
984
  "changed nothing and moved no funds itself. Your evidence was recorded. Do not resubmit to this milestone";
@@ -862,10 +992,6 @@ async function cmdVerify(args) {
862
992
  "⏳ PENDING_REVIEW — the verification panel could not reach a verdict (too few models " +
863
993
  "answered). This is NOT a rejection and NOT a judgement of your work; a human decides, " +
864
994
  "and resubmitting the same evidence will not help while the panel is down";
865
- } else if (verdict === "approved" && releaseHoldUntil) {
866
- statusLine =
867
- `⏸️ APPROVED (${confidence}% confidence) — payout scheduled for ${releaseHoldUntil}. ` +
868
- "NO FUNDS HAVE MOVED YET: the funder can contest until then, and the date can move";
869
995
  } else if (verdict === "approved") {
870
996
  statusLine = `✅ VERIFIED (${confidence}% confidence)`;
871
997
  if (data.on_chain_url) statusLine += `\n Proof: ${data.on_chain_url}`;
@@ -886,18 +1012,28 @@ async function cmdVerify(args) {
886
1012
  print({
887
1013
  verdict,
888
1014
  confidence,
889
- // MONEY MOVED — not "the verdict was approved". A scheduled payout has not happened.
890
- passed: verdict === "approved" && !releaseHoldUntil,
891
- release_hold_until: releaseHoldUntil,
1015
+ passed: verdict === "approved",
892
1016
  ...(settledAs ? { settled_as: settledAs } : {}),
893
1017
  reasoning: data.reasoning ?? "",
894
1018
  on_chain_url: data.on_chain_url ?? null,
895
1019
  proof_id: data.proof_id ?? null,
896
1020
  signed_at: data.signed_at ?? null,
1021
+ ...(isV3 ? v3Fields(data) : {}),
897
1022
  model_votes: votes,
898
1023
  });
899
1024
  }
900
1025
 
1026
+ /** The MilestoneEscrowV3 fields of an answer, for JSON output (post-review G4). Never derived: forwarded. */
1027
+ function v3Fields(r) {
1028
+ return {
1029
+ escrowVersion: 3,
1030
+ submissionId: r.submissionId ?? null,
1031
+ manifestHash: r.manifestHash ?? null,
1032
+ submissionVerdict: r.submissionVerdict ?? "PENDING",
1033
+ claimReady: r.claimReady === true,
1034
+ };
1035
+ }
1036
+
901
1037
  async function cmdCreate(args) {
902
1038
  if (!API_KEY) err("CASCROW_API_KEY is not set");
903
1039
  if (!args.title) err("--title is required");
@@ -948,42 +1084,28 @@ async function cmdCreate(args) {
948
1084
  process.stderr.write(`\nContract created: ${contractUrl}\n\n`);
949
1085
  }
950
1086
 
951
- async function cmdFund(args) {
952
- if (!API_KEY) err("CASCROW_API_KEY is not set");
953
- if (!args.contract) err("--contract is required");
954
-
955
- const body = args.milestone
956
- ? { contractId: args.contract, milestoneId: args.milestone }
957
- : { contractId: args.contract };
958
-
959
- const data = await apiPost("/api/agent/fund-milestone", body);
960
- print(data);
961
- process.stderr.write(`\nMilestone funded (simulation). ID: ${data.milestoneId}\n\n`);
1087
+ // Removed in 3.0.0 (cascrow Phase 6, owner decision 16): `fund` activated a v2
1088
+ // milestone. A $0 verification contract is active by itself once the builder has
1089
+ // joined, and a milestone with an amount is a MilestoneEscrowV3 escrow its payer
1090
+ // funds from their own machine. Kept as a command only to say so; it sends nothing.
1091
+ function cmdFundRemoved() {
1092
+ err(`${V3_CLIENT_TEXT.fundRemoved} [TOOL_REMOVED]`);
962
1093
  }
963
1094
 
964
- async function cmdEscrowFund(args) {
965
- if (!API_KEY) err("CASCROW_API_KEY is not set");
966
- if (!args.contract) err("--contract is required");
967
- if (!args["private-key"]) err("--private-key is required (EVM private key, funds come from this wallet)");
968
-
969
- const body = {
970
- contractId: args.contract,
971
- agentPrivateKey: args["private-key"],
972
- };
973
- if (args.milestone) body.milestoneId = args.milestone;
974
- if (args.amount) {
975
- // NOT forwarded. `agent/escrow-fund` funds `milestone.amountUSD` and ignores
976
- // a body field, so `--amount` never overrode anything — it only told the
977
- // user it had. Say so rather than sending a number the server drops.
978
- process.stderr.write(
979
- "\n⚠️ --amount is ignored for escrow funding: the escrowed amount is the milestone's own amountUSD.\n"
980
- );
981
- }
982
-
983
- process.stderr.write("\nFunding milestone on-chain (approve + fundMilestone)...\n");
984
- const data = await apiPost("/api/agent/escrow-fund", body);
985
- print(data);
986
- process.stderr.write(`\n✅ ${data.amountUSD} RLUSD locked on-chain. TX: ${data.txHash}\n\n`);
1095
+ // Removed in 2.0.0 (cascrow Phase 5): `escrow-fund --private-key` sent the key
1096
+ // to cascrow, which signed the deposit server-side. Kept as a command only to say
1097
+ // so: it sends NOTHING — no request, no argument — so a key typed on the command
1098
+ // line goes nowhere. It cannot un-type it, which is why the message tells the user
1099
+ // what a key that reached a shell history (or an older version of this CLI) is now.
1100
+ function cmdEscrowFundRemoved() {
1101
+ err(
1102
+ "escrow-fund was removed in cascrow-cli 2.0.0: cascrow never accepts, creates, stores or signs with private keys. " +
1103
+ "A $0 verification contract needs no funding — it is active once the builder has joined. A milestone with an amount " +
1104
+ "is a MilestoneEscrowV3 escrow in native XRP, funded by the payer with one transaction from their own machine: first link " +
1105
+ "the wallet you will fund from (GET /api/user/wallet/challenge, sign the message, POST /api/user/wallet), then run " +
1106
+ "npx cascrow-signer@0.1.0 fund --milestone <milestoneId>. If you ever passed a private key to this command (it is also in your shell history), " +
1107
+ "treat that key as exposed and move its funds to a new wallet. [CUSTODY_PATH_REMOVED]"
1108
+ );
987
1109
  }
988
1110
 
989
1111
  // Legacy two-step: upload proof → returns proofId → user runs `cascrow verify --proof <id>`
@@ -1031,7 +1153,8 @@ async function cmdJoin(args) {
1031
1153
  if (!args.invite) err("--invite is required");
1032
1154
  const data = await apiPost("/api/contracts/join", { inviteCode: args.invite });
1033
1155
  print(data);
1034
- process.stderr.write(`\nJoined contract: ${data.contractId}\n\n`);
1156
+ // `activated`: a $0 contract is active now; a money contract waits for its V3 offer.
1157
+ process.stderr.write(`\nJoined contract: ${data.contractId}\n${joinedLine(data)}\n\n`);
1035
1158
  }
1036
1159
 
1037
1160
  async function cmdHandoff(args) {
@@ -1071,9 +1194,9 @@ async function cmdCheckInvites() {
1071
1194
  * verdict on cannot come apart (F-22).
1072
1195
  *
1073
1196
  * `review-queue` is the pull behind the `manual_review.required` webhook: a
1074
- * webhook nobody received is a decision nobody makes, and cascrow auto-approves
1075
- * an undecided milestone after 48 h (agent contracts) or 14 days (human) and
1076
- * releases the escrow. `review` ships with it rather than later, because a list
1197
+ * webhook nobody received is a decision nobody makes, and cascrow completes an
1198
+ * undecided milestone after 48 h (agent contracts in MANUAL_AUTO/AUTO) or 14 days
1199
+ * (every other contract). Verification-only contracts: no money moves. `review` ships with it rather than later, because a list
1077
1200
  * its reader cannot act on names a remedy they are unable to perform (F-72).
1078
1201
  */
1079
1202
  async function cmdReviewQueue() {
@@ -1081,8 +1204,16 @@ async function cmdReviewQueue() {
1081
1204
  const data = await apiGet("/api/agent/review-queue");
1082
1205
  const contracts = data.contracts ?? [];
1083
1206
  print(data);
1207
+ // Said in both branches: an empty v2 queue is exactly when a reader stops
1208
+ // looking, and a V3 claim window keeps running.
1209
+ const v3 =
1210
+ data.v3ClaimsPending === null
1211
+ ? "Open MilestoneEscrowV3 claims could not be counted — run: cascrow v3-claims\n\n"
1212
+ : data.v3ClaimsPending > 0
1213
+ ? `${data.v3ClaimsPending} MilestoneEscrowV3 claim(s) can still be objected to — run: cascrow v3-claims\n\n`
1214
+ : "";
1084
1215
  if (contracts.length === 0) {
1085
- process.stderr.write("\nNo milestones are waiting for your verdict.\n\n");
1216
+ process.stderr.write(`\nNo milestones are waiting for your verdict.\n\n${v3}`);
1086
1217
  return;
1087
1218
  }
1088
1219
  const lines = contracts.map((c) => {
@@ -1093,15 +1224,100 @@ async function cmdReviewQueue() {
1093
1224
  : pending.length > 1
1094
1225
  ? `${pending.length} milestones pending — pass --milestone <id>`
1095
1226
  : "contract-level review — omit --milestone";
1096
- return ` ${c.contractId} ${c.amountUSD} RLUSD (${ms})`;
1227
+ return ` ${c.contractId} (${ms})`;
1097
1228
  }).join("\n");
1098
1229
  const more = data.truncated ? `\n(${data.total} in total — this list is capped.)` : "";
1099
1230
  process.stderr.write(
1100
1231
  `\n${data.total} contract(s) waiting for your verdict:\n${lines}${more}\n\n` +
1101
- "Decide with: cascrow review --contract <id> --decision APPROVE|REJECT [--milestone <id>]\n\n"
1232
+ "Decide with: cascrow review --contract <id> --decision APPROVE|REJECT [--milestone <id>]\n\n" +
1233
+ v3
1102
1234
  );
1103
1235
  }
1104
1236
 
1237
+ /**
1238
+ * The payer's open MilestoneEscrowV3 claims — `GET /api/agent/v3-claims`, the
1239
+ * pull behind the `claim.submitted` webhook. Print only: the action is the
1240
+ * payer's own `object(escrowId)` on chain, which cascrow never performs.
1241
+ */
1242
+ async function cmdV3Claims() {
1243
+ if (!API_KEY) err("CASCROW_API_KEY is not set");
1244
+ const data = await apiGet("/api/agent/v3-claims");
1245
+ const claims = data.claims ?? [];
1246
+ print(data);
1247
+ if (claims.length === 0) {
1248
+ process.stderr.write("\nNo MilestoneEscrowV3 claim is waiting on you.\n\n");
1249
+ return;
1250
+ }
1251
+ const lines = claims.map((c) =>
1252
+ ` ${c.escrowId} on ${c.chainId}/${c.escrowContract} claim ${c.claimIndex}` +
1253
+ `${c.secondFunding ? " SECOND funding" : ""}${c.held ? " (held — verify on chain)" : ""} ` +
1254
+ (c.objectable === null ? "claim state unknown here — read getEscrow on chain" : c.objectable ? `object() before ${c.objectionDeadline}` : `window closed at ${c.objectionDeadline}`)
1255
+ ).join("\n");
1256
+ const more = data.truncated ? "\n(This list is capped; more claims exist.)" : "";
1257
+ process.stderr.write(`\n${claims.length} claim(s):\n${lines}${more}\n\ncascrow cannot object for you — your own wallet calls object(escrowId) on the escrow contract.\n\n`);
1258
+ }
1259
+
1260
+ // ─── MilestoneEscrowV3, remote half (design Rev. 10) ──────────────────────────
1261
+ // cascrow prepares and reads; the party signs and sends with its own key. This CLI
1262
+ // holds no key and has no dependency: `v3 sign …` runs the PINNED local signer
1263
+ // (V3_CLIENT_TEXT.signerPin) in a child process, with this terminal attached, so
1264
+ // its prompts and its checks are the signer's own.
1265
+ async function cmdV3(sub, args, rawArgs) {
1266
+ if (sub === "sign") {
1267
+ const child = spawn("npx", ["--yes", V3_CLIENT_TEXT.signerPin, ...rawArgs], { stdio: "inherit", shell: process.platform === "win32" });
1268
+ const code = await new Promise((resolve) => child.on("close", resolve));
1269
+ process.exit(typeof code === "number" ? code : 1);
1270
+ }
1271
+ if (!API_KEY) err("CASCROW_API_KEY is not set");
1272
+ switch (sub) {
1273
+ case "offer": {
1274
+ if (!args.milestone || !args["review-period-sec"]) err("--milestone and --review-period-sec are required");
1275
+ const body = {
1276
+ milestoneId: args.milestone,
1277
+ reviewPeriodSec: Number(args["review-period-sec"]),
1278
+ watcherDeclared: args.watcher === true || args.watcher === "true",
1279
+ ...(args.arbiter ? { arbiter: args.arbiter } : {}),
1280
+ ...(args["arbitration-period-sec"] ? { arbitrationPeriodSec: Number(args["arbitration-period-sec"]) } : {}),
1281
+ };
1282
+ print(await apiPost("/api/escrow/v3/offer", body));
1283
+ process.stderr.write(`\nOffer sent. After the builder accepts, fund it from your machine: cascrow v3 sign fund --milestone ${args.milestone}\n\n`);
1284
+ return;
1285
+ }
1286
+ case "acceptance":
1287
+ if (!args.milestone) err("--milestone is required");
1288
+ print(await apiGet(`/api/escrow/v3/acceptance?milestoneId=${encodeURIComponent(args.milestone)}`));
1289
+ process.stderr.write("\nCheck every term — your signature binds you until acceptBy. To check and sign locally: cascrow v3 sign accept --milestone " + args.milestone + "\n\n");
1290
+ return;
1291
+ case "accept-submit":
1292
+ if (!args.offer || !args.signature) err("--offer and --signature are required");
1293
+ print(await apiPost("/api/escrow/v3/acceptance", { offerId: args.offer, signature: args.signature }));
1294
+ return;
1295
+ case "prepare": {
1296
+ if (!args.milestone || !args.action) err("--milestone and --action are required");
1297
+ const q = new URLSearchParams({ milestoneId: args.milestone, action: args.action });
1298
+ if (args["proof-hash"]) q.set("proofHash", args["proof-hash"]);
1299
+ if (args["reason-hash"]) q.set("reasonHash", args["reason-hash"]);
1300
+ if (args["new-deadline"]) q.set("newDeadline", String(args["new-deadline"]));
1301
+ print(await apiGet(`/api/escrow/v3/prepare?${q.toString()}`));
1302
+ process.stderr.write("\nUNSIGNED — cascrow does not sign, send or relay it. Rebuild data from the ABI and check to/chainId against your own configuration before signing (cascrow v3 sign … does).\n\n");
1303
+ return;
1304
+ }
1305
+ case "report-tx":
1306
+ if (!args.milestone || typeof args.tx !== "string" || !TX_HASH_RE.test(args.tx)) err(V3_CLIENT_TEXT.reportTxOnlyHash);
1307
+ print(await apiPost("/api/escrow/v3/sync", { milestoneId: args.milestone, txHash: args.tx }));
1308
+ return;
1309
+ case "attestation":
1310
+ if (!args.escrow || !args["claim-index"]) err("--escrow and --claim-index are required");
1311
+ print(await apiGet(`/api/escrow/v3/attestation?${new URLSearchParams({ escrowId: args.escrow, claimIndex: String(args["claim-index"]) }).toString()}`));
1312
+ return;
1313
+ case "claims":
1314
+ await cmdV3Claims();
1315
+ return;
1316
+ default:
1317
+ err(`unknown v3 command ${sub ?? "(none)"} — offer | acceptance | accept-submit | prepare | report-tx | attestation | claims | sign`);
1318
+ }
1319
+ }
1320
+
1105
1321
  async function cmdReview(args) {
1106
1322
  if (!API_KEY) err("CASCROW_API_KEY is not set");
1107
1323
  const contractId = args.contract;
@@ -1111,11 +1327,14 @@ async function cmdReview(args) {
1111
1327
  const body = args.milestone ? { contractId, decision, milestoneId: args.milestone } : { contractId, decision };
1112
1328
  const data = await apiPost("/api/contracts/review", body);
1113
1329
  print(data);
1330
+ // The route answers `{ status }` and nothing else: a review only decides
1331
+ // verification-only ($0) milestones, and nothing moves money (a MilestoneEscrowV3
1332
+ // milestone is refused — its payer releases or objects on chain).
1114
1333
  const what =
1115
1334
  decision === "APPROVE"
1116
- ? "approved — the escrow is being released to the builder"
1335
+ ? "approved and completed (a verification-only contract: no money moves)"
1117
1336
  : "rejected — the milestone is back with the builder, who can resubmit";
1118
- process.stderr.write(`\nMilestone ${what}.${data.txHash ? `\nRelease tx: ${data.txHash}` : ""}\n\n`);
1337
+ process.stderr.write(`\nMilestone ${what}.${data.status ? ` Contract status: ${data.status}.` : ""}\n\n`);
1119
1338
  }
1120
1339
 
1121
1340
  async function cmdMe() {
@@ -1140,8 +1359,8 @@ try {
1140
1359
  case "verify-work": await cmdVerifyWork(args); break;
1141
1360
  case "verify": await cmdVerify(args); break;
1142
1361
  case "create": await cmdCreate(args); break;
1143
- case "fund": await cmdFund(args); break;
1144
- case "escrow-fund": await cmdEscrowFund(args); break;
1362
+ case "fund": cmdFundRemoved(); break;
1363
+ case "escrow-fund": cmdEscrowFundRemoved(); break;
1145
1364
  case "submit": await cmdSubmit(args); break;
1146
1365
  case "status": await cmdStatus(args); break;
1147
1366
  case "get": await cmdGet(args); break;
@@ -1150,6 +1369,8 @@ try {
1150
1369
  case "check-invites": await cmdCheckInvites(); break;
1151
1370
  case "review-queue": await cmdReviewQueue(); break;
1152
1371
  case "review": await cmdReview(args); break;
1372
+ case "v3-claims": await cmdV3Claims(); break;
1373
+ case "v3": await cmdV3(rest[0], parseArgs(rest.slice(1)), rest.slice(1)); break;
1153
1374
  case "me": await cmdMe(); break;
1154
1375
  default:
1155
1376
  console.error(`Unknown command: ${command}\n`);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "cascrow-cli",
3
- "version": "1.4.0",
4
- "description": "CLI for Cascrow — trust infrastructure for the agent economy: AI verification, on-chain reputation, and milestone escrow on the XRP Ledger",
3
+ "version": "3.0.0",
4
+ "description": "CLI for Cascrow — trust infrastructure for the agent economy: AI verification, on-chain reputation, and non-custodial milestone escrow (MilestoneEscrowV3, native XRP)",
5
5
  "type": "module",
6
6
  "main": "index.js",
7
7
  "bin": {