cascrow-cli 3.0.0 → 3.1.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,6 +1,23 @@
1
1
  # Changelog
2
2
 
3
- ## 3.0.0 — unreleased (owner publishes)
3
+ ## 3.1.0 — 2026-10-20
4
+
5
+ Prepared as 3.0.1 (texts only) and never published; new options made it a minor release.
6
+
7
+ - **New command `cascrow human-review --contract <id> [--milestone <id>] [--reason "…"]`** (either party asks a person at cascrow to review a verification outcome, Art. 22(3) GDPR; decided within one month, both parties receive the decision with its reasons) **and `cascrow human-review --contract <id> --list`** (the requests and their decisions). A request already open prints its deadline and exits 0. A refused call's error now carries the server's body.
8
+ - The review-queue help no longer says an undecided milestone completes after 48 h or 14 days: it completes when its window closes only when the panel's own YES opened the review and the contract's terms let silence count; any other review goes to cascrow's human review.
9
+ - **`cascrow create` takes `--review-days <n>`** (your review window, 2–30 days, default 14; under 7 only if both parties registered as businesses) **and `--panel-alone`** (the panel decides alone: YES above 85 % completes, anything else is a rejection the builder can resubmit against). The server validates both and answers with its own sentence.
10
+ - `cascrow verify` says "5 AI models run by 5 different providers" (was "5 independent AI models").
11
+ - New hint for **`SCREENING_UNAVAILABLE`** (the new-money sanctions screen fails closed — nothing was created; retry), and the V3 text table carries the no-arbiter veto sentence.
12
+
13
+ The 3.0.1 notes, unchanged:
14
+
15
+ - The wallet-linking instructions (the custody-path refusal and the builder agent) follow cascrow's Sign-In with Ethereum challenge: `GET /api/user/wallet/challenge?address=<your wallet>`, sign the returned message with that wallet, `POST /api/user/wallet` — or `npx cascrow-signer@0.2.0 link`. The old one-line challenge is refused by the server.
16
+ - `cascrow v3 sign` runs the pinned `cascrow-signer@0.2.0`; 0.1.0 can no longer link a wallet.
17
+ - The builder agent names the panel's OpenAI model correctly (GPT-4o-mini).
18
+ - The package now carries its MIT `LICENSE` and ships only `index.js`, `README.md`, `CHANGELOG.md` and `LICENSE`. `builder-agent.js`, a demo agent that no command runs, stays in the repository.
19
+
20
+ ## 3.0.0 — 2026-09-27
4
21
 
5
22
  - **`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
23
  - **`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.
@@ -11,12 +28,12 @@
11
28
  - 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
29
  - 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
30
 
14
- ## 2.1.0 — unreleased (owner publishes)
31
+ ## 2.1.0 — not published on its own, released as part of 3.0.0
15
32
 
16
33
  - `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
34
  - `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
35
 
19
- ## 2.0.0 — unreleased (owner publishes)
36
+ ## 2.0.0 — not published on its own, released as part of 3.0.0
20
37
 
21
38
  - **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
39
  - `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`).
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Cascrow
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
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**, 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.
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 that can move escrowed funds and never signs an escrow transaction.
6
6
 
7
7
  ## Install
8
8
 
@@ -60,8 +60,9 @@ is never anchored.
60
60
  | `review-queue` | As funder: list milestones still waiting for your verdict | Yes |
61
61
  | `v3-claims` | As payer: list MilestoneEscrowV3 claims you can still object to on chain | Yes |
62
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
+ | `v3 sign <command…>` | Runs the pinned local signer, `npx cascrow-signer@0.2.0 <command…>` — your key stays on your machine | Signer's own |
64
64
  | `review` | As funder: approve or reject a milestone under manual review | Yes |
65
+ | `human-review` | Either party: ask a person at cascrow to review a verification outcome (Art. 22(3) GDPR), `--list` its decisions | Yes |
65
66
  | `me` | Print this agent's ID | Yes |
66
67
 
67
68
  Run `cascrow --help` (or `cascrow`) any time for the full, always-current option reference.
@@ -166,8 +167,8 @@ No options.
166
167
  No options. Lists the contracts on which you, as funder, still owe a manual
167
168
  verdict on a verification-only contract — the pull behind the
168
169
  `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.
170
+ makes; cascrow completes an undecided milestone when its review window closes
171
+ only when the panel's own verdict opened the review with a YES — any other review goes to cascrow's human review instead; the window is the contract's `reviewWindowDays` (default 14 days; 48h on an agent contract created before 2026-10-20 in review mode `MANUAL_AUTO` or `AUTO`). A verdict completes or hands back the milestone and moves no money.
171
172
 
172
173
  ### `review`
173
174
 
@@ -228,7 +229,7 @@ cascrow verify --contract <contractId> --proof "API live at https://api.example.
228
229
  # The verdict is a recommendation. The builder claims on chain; the payer releases or objects.
229
230
  ```
230
231
 
231
- `cascrow v3 sign …` runs the pinned local signer (`npx cascrow-signer@0.1.0`), which keeps
232
+ `cascrow v3 sign …` runs the pinned local signer (`npx cascrow-signer@0.2.0`), which keeps
232
233
  your key on your machine. This CLI never takes a private key (`escrow-fund`, which did, was
233
234
  removed in 2.0.0 — if you ever passed a key to it, treat that key as exposed and move its funds).
234
235
 
package/index.js CHANGED
@@ -16,7 +16,7 @@ const API_KEY = process.env.CASCROW_API_KEY ?? "";
16
16
  // app). v3-client-text-parity.test.ts holds the copy equal to the owner.
17
17
  // BEGIN v3-client-text
18
18
  const V3_CLIENT_TEXT = {
19
- "signerPin": "cascrow-signer@0.1.0",
19
+ "signerPin": "cascrow-signer@0.2.0",
20
20
  "remoteTools": [
21
21
  "cascrow_v3_offer",
22
22
  "cascrow_v3_acceptance_get",
@@ -27,17 +27,18 @@ const V3_CLIENT_TEXT = {
27
27
  "cascrow_v3_claims"
28
28
  ],
29
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).",
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.2.0 without cascrow at all).",
31
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
+ "SCREENING_UNAVAILABLE": "cascrow could not complete the sanctions screening this new-escrow step needs (offer, acceptance or funding), so nothing was created or prepared. This is a condition on cascrow's side, not a finding about you or your counterparty — retry in a few minutes. It does not touch escrows that already exist: their actions keep working (cascrow_v3_prepare, or npx cascrow-signer@0.2.0).",
32
33
  "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
34
  "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
+ "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.2.0 fund --milestone <milestoneId>) or the offer or acceptance lapses.",
35
36
  "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
37
  "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
+ "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.2.0.",
38
39
  "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.",
40
+ "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.2.0 accept does it and checks them first.",
41
+ "V3_INDEX_STALE": "cascrow's index of the chain is behind. Read the escrow from the chain directly (npx cascrow-signer@0.2.0 claims, or getEscrow / ClaimSubmitted); every deadline, above all an objection window, runs on chain regardless.",
41
42
  "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
43
  "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
44
  "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.",
@@ -47,14 +48,15 @@ const V3_CLIENT_TEXT = {
47
48
  },
48
49
  "joined": {
49
50
  "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
+ "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.2.0 accept --milestone <milestoneId>, or on the contract page. cascrow never signs for you. Submit proof once the payer has funded the milestone.",
51
52
  "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
  },
53
54
  "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
+ "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.2.0 submit-claim --milestone <milestoneId> --proof-hash <manifestHash>."
55
56
  },
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.",
57
+ "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.2.0 fund --milestone <milestoneId>. cascrow never signs or sends for you.",
58
+ "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.2.0 submit-claim --milestone <milestoneId> --proof-hash <manifestHash>). As payer: release, or object before the objection deadline.",
59
+ "noArbiterVeto": "No arbiter is named. The payer can object to every claim at no cost, and after the deadline an objected claim ends in a refund to the payer. This escrow protects you only against an inactive payer, not a dishonest one.",
58
60
  "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
61
  };
60
62
  // END v3-client-text
@@ -175,7 +177,7 @@ async function fetchJson(method, path, { json, form, key = API_KEY, deadlineMs =
175
177
  (v3RefusalDetail(data) ? ` — ${v3RefusalDetail(data)}` : "") +
176
178
  (v3HintFor(code, data) ? `\n\n${v3HintFor(code, data)}` : "")
177
179
  ),
178
- { status: res.status, code, retryAfterSeconds }
180
+ { status: res.status, code, retryAfterSeconds, body: data }
179
181
  );
180
182
  }
181
183
  return data;
@@ -552,9 +554,10 @@ Commands:
552
554
  check-invites Check pending contract invites for this agent
553
555
  review-queue As funder: list milestones still waiting for your verdict
554
556
  review As funder: approve or reject a milestone under manual review
557
+ human-review As funder or builder: ask a person at cascrow to review a verification outcome (Art. 22(3) GDPR), or --list the requests
555
558
  v3-claims As payer: list MilestoneEscrowV3 claims you can still object to on chain
556
559
  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),
560
+ and "v3 sign <cascrow-signer command…>" — runs the pinned local signer (npx cascrow-signer@0.2.0),
558
561
  which holds your key on YOUR machine. cascrow never signs, sends or relays for you.
559
562
  me Print this agent's ID
560
563
 
@@ -589,6 +592,11 @@ Options for each command:
589
592
  --criteria-file <path> optional the same, read from a file
590
593
  --checklist-file <path> optional JSON array of { title, severity?, id? } (max 20) scored item by item on text proofs
591
594
  --review-mode <mode> optional AUTO | MANUAL | MANUAL_AUTO — what happens when the panel is unsure (60–85%)
595
+ --review-days <n> optional your review window, 2–30 days (default 14; under 7 only if both parties
596
+ registered as businesses). Without your decision a milestone completes
597
+ only if the panel's own verdict was YES; otherwise cascrow's human review decides
598
+ --panel-alone optional the panel decides alone: YES above 85 % completes, anything else is a
599
+ rejection the builder can resubmit against — you do not review
592
600
 
593
601
  submit [or use verify for text proof in one call]
594
602
  --milestone <id> required
@@ -645,13 +653,18 @@ Examples:
645
653
 
646
654
  # As the funder: decide what the AI panel could not (confidence 60-85)
647
655
  # Poll this — a manual_review.required webhook you missed is a decision nobody
648
- # makes; an undecided milestone completes automatically after 48h under review mode
649
- # MANUAL_AUTO or AUTO, 14 days otherwise.
656
+ # makes; an undecided milestone completes automatically when its review window
657
+ # closes only when the panel's own YES opened the review and the contract's terms let
658
+ # silence count. Any other review goes to cascrow's human review.
650
659
  # Verification-only contracts: a verdict completes or hands back, and moves no money.
651
660
  cascrow review-queue
652
661
  cascrow review --contract <contractId> --decision APPROVE
653
662
  cascrow review --contract <contractId> --decision REJECT --milestone <milestoneId>
654
663
 
664
+ # Either party: a person at cascrow reviews a verification outcome (decided within a month)
665
+ cascrow human-review --contract <contractId> [--milestone <milestoneId>] [--reason "why"]
666
+ cascrow human-review --contract <contractId> --list
667
+
655
668
  # As the payer of a MilestoneEscrowV3 escrow: claims release unless YOUR wallet
656
669
  # calls object(escrowId) before the deadline — cascrow cannot object for you.
657
670
  cascrow v3-claims
@@ -790,7 +803,7 @@ async function cmdVerifyWork(args) {
790
803
  }
791
804
  }
792
805
 
793
- process.stderr.write("\nVerifying with 5 independent AI models (no escrow, no account needed)...\n\n");
806
+ process.stderr.write("\nVerifying with 5 AI models run by 5 different providers (no escrow, no account needed)...\n\n");
794
807
 
795
808
  let data;
796
809
  if (args.image) {
@@ -1060,6 +1073,10 @@ async function cmdCreate(args) {
1060
1073
  if (reviewMode !== undefined && !["AUTO", "MANUAL", "MANUAL_AUTO"].includes(reviewMode)) {
1061
1074
  err("--review-mode must be AUTO, MANUAL or MANUAL_AUTO");
1062
1075
  }
1076
+ // The range is the server's (review-terms.ts); only a non-number is refused here.
1077
+ const reviewDays = args["review-days"] !== undefined ? Number(args["review-days"]) : undefined;
1078
+ if (reviewDays !== undefined && !Number.isInteger(reviewDays)) err("--review-days must be a whole number of days");
1079
+ const panelAlone = args["panel-alone"] === true || args["panel-alone"] === "true" ? true : undefined;
1063
1080
  let criteria, checklist;
1064
1081
  try {
1065
1082
  criteria = normalizeCriteria(criteriaRaw);
@@ -1077,6 +1094,8 @@ async function cmdCreate(args) {
1077
1094
  ...(checklist !== undefined ? { verificationChecklist: checklist } : {}),
1078
1095
  }],
1079
1096
  ...(reviewMode !== undefined ? { agentReviewMode: reviewMode } : {}),
1097
+ ...(reviewDays !== undefined ? { reviewWindowDays: reviewDays } : {}),
1098
+ ...(panelAlone !== undefined ? { panelDecidesAlone: panelAlone } : {}),
1080
1099
  });
1081
1100
 
1082
1101
  const contractUrl = `${BASE_URL}/contract/${data.contractId}`;
@@ -1102,8 +1121,8 @@ function cmdEscrowFundRemoved() {
1102
1121
  "escrow-fund was removed in cascrow-cli 2.0.0: cascrow never accepts, creates, stores or signs with private keys. " +
1103
1122
  "A $0 verification contract needs no funding — it is active once the builder has joined. A milestone with an amount " +
1104
1123
  "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), " +
1124
+ "the wallet you will fund from (GET /api/user/wallet/challenge?address=<your wallet>, sign the message with that wallet, POST /api/user/wallet), then run " +
1125
+ "npx cascrow-signer@0.2.0 fund --milestone <milestoneId>. If you ever passed a private key to this command (it is also in your shell history), " +
1107
1126
  "treat that key as exposed and move its funds to a new wallet. [CUSTODY_PATH_REMOVED]"
1108
1127
  );
1109
1128
  }
@@ -1337,6 +1356,42 @@ async function cmdReview(args) {
1337
1356
  process.stderr.write(`\nMilestone ${what}.${data.status ? ` Contract status: ${data.status}.` : ""}\n\n`);
1338
1357
  }
1339
1358
 
1359
+ /**
1360
+ * `cascrow human-review` — the request (POST /api/human-review) or, with --list, the
1361
+ * requests on a contract with their decisions (vp1-5, 2026-10-20). A request already
1362
+ * open on that milestone is not an error: it prints the deadline and exits 0.
1363
+ */
1364
+ async function cmdHumanReview(args) {
1365
+ if (!API_KEY) err("CASCROW_API_KEY is not set");
1366
+ const contractId = args.contract;
1367
+ if (!contractId) err("--contract <id> is required");
1368
+ if (args.list) {
1369
+ const data = await apiGet(`/api/human-review?contractId=${encodeURIComponent(contractId)}`);
1370
+ print(data);
1371
+ const requests = Array.isArray(data.requests) ? data.requests : [];
1372
+ process.stderr.write(
1373
+ requests.length === 0
1374
+ ? "\nNo human-review requests on this contract.\n\n"
1375
+ : `\n${requests.map((r) => ` ${r.id} [${r.status}] ${r.outcome ? `outcome ${r.outcome}` : `decision due by ${r.dueAt}`}`).join("\n")}\n\n`
1376
+ );
1377
+ return;
1378
+ }
1379
+ const body = { contractId, ...(args.milestone ? { milestoneId: args.milestone } : {}), ...(typeof args.reason === "string" && args.reason ? { reason: args.reason } : {}) };
1380
+ try {
1381
+ const data = await apiPost("/api/human-review", body);
1382
+ print(data);
1383
+ process.stderr.write(`\nHuman review requested (${data.requestId}). A person at cascrow decides by ${data.dueAt}; both parties receive the decision with its reasons.\n\n`);
1384
+ } catch (e) {
1385
+ if (e && e.code === "HUMAN_REVIEW_ALREADY_OPEN") {
1386
+ const dueAt = e.body && typeof e.body.dueAt === "string" ? e.body.dueAt : null;
1387
+ print({ status: "ALREADY_OPEN", dueAt });
1388
+ process.stderr.write(`\nA human review of this milestone is already open${dueAt ? `; a person at cascrow decides by ${dueAt}` : ""}. No need to ask again.\n\n`);
1389
+ return;
1390
+ }
1391
+ throw e;
1392
+ }
1393
+ }
1394
+
1340
1395
  async function cmdMe() {
1341
1396
  if (!API_KEY) err("CASCROW_API_KEY is not set");
1342
1397
  const data = await apiGet("/api/agent/me");
@@ -1369,6 +1424,7 @@ try {
1369
1424
  case "check-invites": await cmdCheckInvites(); break;
1370
1425
  case "review-queue": await cmdReviewQueue(); break;
1371
1426
  case "review": await cmdReview(args); break;
1427
+ case "human-review": await cmdHumanReview(args); break;
1372
1428
  case "v3-claims": await cmdV3Claims(); break;
1373
1429
  case "v3": await cmdV3(rest[0], parseArgs(rest.slice(1)), rest.slice(1)); break;
1374
1430
  case "me": await cmdMe(); break;
package/package.json CHANGED
@@ -1,9 +1,15 @@
1
1
  {
2
2
  "name": "cascrow-cli",
3
- "version": "3.0.0",
3
+ "version": "3.1.0",
4
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
+ "files": [
8
+ "index.js",
9
+ "README.md",
10
+ "CHANGELOG.md",
11
+ "LICENSE"
12
+ ],
7
13
  "bin": {
8
14
  "cascrow": "index.js"
9
15
  },
package/builder-agent.js DELETED
@@ -1,453 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Cascrow Builder Agent
4
- * Polls for pending contract invites, then automatically:
5
- * joins → waits until the milestone is active → submits proof → triggers AI verification
6
- */
7
-
8
- const BASE_URL = process.env.CASCROW_BASE_URL ?? "https://cascrow.com";
9
- const API_KEY = process.env.CASCROW_API_KEY ?? "";
10
- const AGENT_EVM_WALLET = process.env.AGENT_EVM_WALLET ?? "";
11
- const POLL_INTERVAL_MS = 3000;
12
-
13
- if (!API_KEY) {
14
- console.error("ERROR: CASCROW_API_KEY is not set");
15
- process.exit(1);
16
- }
17
-
18
- // Every call reads the body as text and parses it in a try (an HTML 502 used to
19
- // crash the agent as "Unexpected token '<'"), carries a deadline over the body
20
- // read, and throws an Error with `status` and `code` (T11-A4). Upload's deadline
21
- // mirrors UPLOAD_CLIENT_ABORT_MS in src/lib/latency-budget.ts.
22
- const UPLOAD_CLIENT_ABORT_MS = 120_000;
23
- const REQUEST_CLIENT_ABORT_MS = 300_000;
24
-
25
- async function readJsonSafe(res) {
26
- const text = await res.text();
27
- if (!text) return {};
28
- try {
29
- return JSON.parse(text);
30
- } catch {
31
- return { error: `HTTP ${res.status} (non-JSON response from server)`, code: "UPSTREAM_NON_JSON" };
32
- }
33
- }
34
-
35
- async function fetchJson(method, path, { json, form, deadlineMs = REQUEST_CLIENT_ABORT_MS } = {}) {
36
- const controller = new AbortController();
37
- const timer = setTimeout(() => controller.abort(), deadlineMs);
38
- let res;
39
- let data;
40
- try {
41
- res = await fetch(`${BASE_URL}${path}`, {
42
- method,
43
- headers: { "Authorization": `Bearer ${API_KEY}`, ...(json !== undefined ? { "Content-Type": "application/json" } : {}) },
44
- ...(json !== undefined ? { body: JSON.stringify(json) } : form !== undefined ? { body: form } : {}),
45
- signal: controller.signal,
46
- });
47
- data = await readJsonSafe(res);
48
- } catch (e) {
49
- if (e && e.name === "AbortError") {
50
- throw Object.assign(new Error(`No answer from ${path} within ${Math.round(deadlineMs / 1000)} s [CLIENT_TIMEOUT]`), { status: 0, code: "CLIENT_TIMEOUT" });
51
- }
52
- throw e;
53
- } finally {
54
- clearTimeout(timer);
55
- }
56
- if (!res.ok || data.code === "UPSTREAM_NON_JSON") {
57
- const code = typeof data.code === "string" ? data.code : null;
58
- throw Object.assign(new Error(`${data.error ?? `HTTP ${res.status}`}${code ? ` [${code}]` : ""}`), { status: res.status, code });
59
- }
60
- return data;
61
- }
62
-
63
- async function apiPost(path, body) {
64
- return fetchJson("POST", path, { json: body });
65
- }
66
-
67
- async function apiGet(path) {
68
- return fetchJson("GET", path);
69
- }
70
-
71
- async function uploadProof(milestoneId, content) {
72
- const blob = new Blob([content], { type: "text/plain" });
73
- const form = new FormData();
74
- form.append("file", blob, "proof-report.txt");
75
- form.append("milestoneId", milestoneId);
76
- return fetchJson("POST", "/api/proof/upload", { form, deadlineMs: UPLOAD_CLIENT_ABORT_MS });
77
- }
78
-
79
- // SEC-073. The FIFTH client door of `/api/verify`, and until 2026-09-22 the only
80
- // one with no deadline at all: it was invisible to `latency-budget.test.ts`,
81
- // whose client list was three hand-typed files — and this file lives inside one
82
- // of the three packages already on that list, so the subject was PACKAGES where
83
- // the population is FILES (F-30).
84
- //
85
- // 235_000 — one step below the route's 240 s `maxDuration`, NOT "above the
86
- // worst case": the tail (pre-stream + a 15 s blob read + the 205 s ladder + the
87
- // bounded EVM retry) reaches ~239 s, so no value is both above it and below the
88
- // cap. See VERIFY_TAIL_WORST_CASE_MS in src/lib/latency-budget.ts. Cleared after
89
- // the READ LOOP rather than at the `fetch`: `fetch` resolves on response headers about 12 s in, and the whole
90
- // retry ladder arrives afterwards out of `res.body`.
91
- async function streamVerify(proofId) {
92
- const controller = new AbortController();
93
- const timeout = setTimeout(() => controller.abort(), 235_000);
94
- let res;
95
- try {
96
- res = await fetch(`${BASE_URL}/api/verify`, {
97
- method: "POST",
98
- headers: {
99
- "Authorization": `Bearer ${API_KEY}`,
100
- "Content-Type": "application/json",
101
- "Accept": "text/event-stream",
102
- },
103
- body: JSON.stringify({ proofId }),
104
- signal: controller.signal,
105
- });
106
- } catch (e) {
107
- clearTimeout(timeout);
108
- // SEC-073. Now that the deadline is live it can actually fire, and a raw
109
- // `DOMException: This operation was aborted` names no remedy — worse, per
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
- if (e && e.name === "AbortError") {
113
- throw new Error(
114
- "Verification is still running on the server — this client stopped waiting, it did not fail. " +
115
- `Poll GET /api/agent/proof-status/${proofId} for the verdict.`
116
- );
117
- }
118
- throw e;
119
- }
120
- if (!res.ok) {
121
- clearTimeout(timeout);
122
- const data = await readJsonSafe(res);
123
- // T01-8. Upload starts verification by itself, and this agent then starts it
124
- // again — so losing the claim to a RUNNING run is the normal case, not a failure.
125
- // The caller's polling path waits for that run; `alreadyRunning` sends it there.
126
- // Never read this 409's Retry-After as a wait: it is the ~1 h takeover cutoff.
127
- if (res.status === 409 && data.code === "VERIFICATION_IN_PROGRESS" && data.phase === "running") {
128
- throw Object.assign(new Error("A verification of this proof is already running (the one upload started)."), { alreadyRunning: true });
129
- }
130
- const code = typeof data.code === "string" ? data.code : null;
131
- throw Object.assign(new Error(`${data.error ?? `HTTP ${res.status}`}${code ? ` [${code}]` : ""}`), { status: res.status, code });
132
- }
133
- if (!res.body) {
134
- clearTimeout(timeout);
135
- throw new Error("Verification stream had no body");
136
- }
137
-
138
- const reader = res.body.getReader();
139
- const decoder = new TextDecoder();
140
- let buffer = "";
141
-
142
- try {
143
- while (true) {
144
- const { done, value } = await reader.read();
145
- if (done) break;
146
- buffer += decoder.decode(value, { stream: true });
147
- const lines = buffer.split("\n");
148
- buffer = lines.pop() ?? "";
149
- for (const line of lines) {
150
- if (!line.startsWith("data:")) continue;
151
- const raw = line.slice(5).trim();
152
- if (!raw) continue;
153
- try {
154
- const msg = JSON.parse(raw);
155
- if (msg.type === "model_vote") {
156
- const icon = msg.decision === "YES" ? "✅" : "❌";
157
- console.log(` ${icon} ${msg.model}: ${msg.decision} (${msg.confidence}%)`);
158
- } else if (msg.type === "complete") {
159
- return msg;
160
- } else if (msg.type === "error") {
161
- throw new Error(msg.message);
162
- }
163
- } catch (e) {
164
- if (e instanceof SyntaxError) continue;
165
- throw e;
166
- }
167
- }
168
- }
169
- } catch (e) {
170
- // The deadline can now fire DURING the read — that is the whole point of
171
- // clearing it below the loop rather than at the fetch. Same translation as
172
- // the fetch-side catch above.
173
- if (e && e.name === "AbortError") {
174
- throw new Error(
175
- "Verification is still running on the server — this client stopped waiting, it did not fail. " +
176
- `Poll GET /api/agent/proof-status/${proofId} for the verdict.`
177
- );
178
- }
179
- throw e;
180
- } finally {
181
- clearTimeout(timeout);
182
- reader.cancel().catch(() => {});
183
- }
184
- throw new Error("Stream ended without complete event");
185
- }
186
-
187
- async function handleInvite(invite) {
188
- console.log(`\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
189
- console.log(`📨 New contract received!`);
190
- console.log(` Contract: ${invite.contractId}`);
191
- if (invite.message) console.log(` Task: ${invite.message}`);
192
- console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`);
193
-
194
- // 1. Join contract
195
- console.log("🔗 Joining contract...");
196
- const joined = await apiPost("/api/contracts/join", { inviteCode: invite.inviteCode });
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>.");
204
-
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...");
209
- let fundedMilestoneId = null;
210
- for (let i = 0; i < 40; i++) {
211
- const contract = await apiGet(`/api/contracts/${joined.contractId}`);
212
- const funded = (contract.milestones ?? []).find((m) => m.status === "FUNDED");
213
- if (funded) {
214
- fundedMilestoneId = funded.id;
215
- console.log(` ✓ Milestone active. ID: ${fundedMilestoneId}`);
216
- break;
217
- }
218
- await new Promise((r) => setTimeout(r, 3000));
219
- }
220
- if (!fundedMilestoneId) throw new Error("Timed out waiting for the milestone to be active");
221
-
222
- // 3. Do the work — generate proof based on the task message
223
- console.log("🤖 Performing task...");
224
- const task = invite.message ?? "Complete the assigned milestone";
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.
226
-
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.
228
-
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.
230
-
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.`;
232
-
233
- const proof = `PROOF OF COMPLETION
234
- ═══════════════════════════════════════
235
-
236
- Task: ${task}
237
- Contract ID: ${invite.contractId}
238
- Milestone ID: ${fundedMilestoneId}
239
- Completed at: ${new Date().toISOString()}
240
-
241
- DELIVERABLE (${bio.split(/\s+/).filter(Boolean).length} words):
242
- ${bio}
243
-
244
- VERIFICATION CRITERIA MET:
245
- ✓ Professional third-person tone throughout
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)
248
- ✓ Highlights AI verification mechanism (5-model majority vote, named models)
249
- ✓ Addresses agent economy angle (autonomous agents, programmatic interfaces)
250
- ✓ No placeholder text
251
- ✓ Suitable for About page or press kit`;
252
-
253
- console.log(` ✓ Task complete.`);
254
-
255
- // 4. Submit proof
256
- console.log("📤 Submitting proof...");
257
- const proofData = await uploadProof(fundedMilestoneId, proof);
258
- console.log(` ✓ Proof submitted. ID: ${proofData.proofId}`);
259
-
260
- // 5. Trigger verification
261
- console.log("\n🔍 Triggering AI verification (5 models)...\n");
262
- let result;
263
- try {
264
- result = await streamVerify(proofData.proofId);
265
- } catch (streamErr) {
266
- // Stream disconnected before complete event (e.g. Vercel 150s timeout).
267
- // The verification is still running server-side — just poll for the result,
268
- // do NOT call streamVerify again (would trigger duplicate AI verification).
269
- // Two ways here, and the loop below serves both: the stream dropped mid-run, or
270
- // it never opened because upload's own run already held the claim (T01-8).
271
- if (streamErr && streamErr.alreadyRunning) {
272
- console.log(` ℹ️ Verification was already running (upload starts it) — waiting for its result...`);
273
- } else if (streamErr && (streamErr.status === 401 || streamErr.status === 403 || streamErr.status === 404)) {
274
- throw streamErr;
275
- } else {
276
- console.log(` ⚠️ Stream ended early, polling for result...`);
277
- }
278
- // SEC-076. This polled `contract.status`, and that column answers a different
279
- // question than the one asked here. It is a single slot with nine writers:
280
- // funding a SIBLING milestone writes `contractStatusAfterFunding(...)` over
281
- // it under `status: { notIn: ["COMPLETED","EXPIRED"] }`, so it fails both
282
- // ways. False negative: the pin is overwritten with FUNDED, no terminal value
283
- // ever appears, and this loop dies after five minutes on "Timed out waiting
284
- // for verification result" for a proof that was in fact decided. False
285
- // positive: the contract reads VERIFIED/COMPLETED because ANOTHER milestone
286
- // settled, and the builder is handed a verdict that is not their proof's -
287
- // `decision` below is derived from it.
288
- //
289
- // `GET /api/contracts/{id}` returns `milestones: [{ id, status, … }]`, and we
290
- // funded a specific one, so the honest read is that row. The contract-level
291
- // read is kept only for the legacy population that has no milestone rows at
292
- // all (proofs written via the contractId-only path).
293
- //
294
- // EXPIRED is terminal too. Since 2026-09-23 `/api/verify` no longer reopens a
295
- // milestone that went EXPIRED while its panel ran, so without it this loop polled a
296
- // closed row for five minutes and then threw "Timed out" — and it must never be
297
- // mapped to a YES: the milestone moved no funds through this verification.
298
- const TERMINAL_PROOF_STATES = ["VERIFIED", "COMPLETED", "REJECTED", "PENDING_REVIEW", "EXPIRED"];
299
- for (let i = 0; i < 60; i++) {
300
- await new Promise((r) => setTimeout(r, 5000));
301
- let contract;
302
- try {
303
- contract = await apiGet(`/api/contracts/${joined.contractId}`);
304
- } catch (e) {
305
- // 401/403/404 do not change by waiting; anything else is transient. One
306
- // dropped poll used to end the whole run.
307
- if (e && (e.status === 401 || e.status === 403 || e.status === 404)) throw e;
308
- continue;
309
- }
310
- const mine = (contract.milestones ?? []).find((m) => m.id === fundedMilestoneId);
311
- const observed = mine ? mine.status : (contract.milestones ?? []).length === 0 ? contract.status : null;
312
- if (observed && TERMINAL_PROOF_STATES.includes(observed)) {
313
- if (observed === "EXPIRED") {
314
- // No verdict is read on this path: the stream that carried it dropped, and
315
- // the row says only that it is closed.
316
- result = { decision: null, confidence: 0, action: "EXPIRED", milestoneClosed: true };
317
- console.log(` ✓ Got result via polling: ${observed}`);
318
- break;
319
- }
320
- const decision = ["VERIFIED", "COMPLETED"].includes(observed) ? "YES" : "NO";
321
- result = { decision, confidence: 0, action: observed };
322
- console.log(` ✓ Got result via polling: ${observed}`);
323
- break;
324
- }
325
- }
326
- if (!result) throw new Error("Timed out waiting for verification result");
327
- }
328
-
329
- // SEC-055/058/059. This re-implemented the three-tier rule from the PROOF's verdict
330
- // (`decision === "YES" && confidence >= 85`), so `COMPLETED`, `SUPERSEDED`, a
331
- // compliance block and a release hold all printed "✅ VERIFIED", and a panel outage
332
- // (`decision: "PENDING"`, confidence 0) printed "❌ NOT VERIFIED — 0% confidence" —
333
- // our own outage read back to the builder as a rejection of their work.
334
- //
335
- // `passed` now means FUNDS MOVED, and only `COMPLETED` says that. The polling
336
- // fallback above already derives `action` from the funded MILESTONE's status
337
- // (SEC-076; it read the contract row until 2026-09-22), so both paths reach this
338
- // with the same vocabulary.
339
- const released = result.action === "COMPLETED";
340
- const passed = released;
341
- console.log(`\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
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") {
356
- // `GET /api/contracts/{id}` does not return the milestone's `terminalReason`, so this
357
- // cannot say whether the funder was refunded or the builder was paid on-chain, and
358
- // does not guess.
359
- console.log(
360
- `⛔ CLOSED — the milestone was closed (EXPIRED) while the panel ran or after it decided, so this verification moved no funds. ` +
361
- `The reason is not available here — check the contract page before assuming where the funds are. Do not resubmit to it.`
362
- );
363
- } else if (result.action === "SUPERSEDED") {
364
- console.log(
365
- result.settledAs
366
- ? `ℹ️ SUPERSEDED — this milestone was closed (${result.settledAs}) before this verification could settle it; it changed nothing and moved no funds. Do not resubmit to it.`
367
- : `ℹ️ SUPERSEDED — another proof on this milestone was decided first; this changed nothing.`
368
- );
369
- } else if (released) {
370
- console.log(
371
- result.alreadyReleased === true
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).`
374
- );
375
- } else if (result.complianceBlocked === true) {
376
- console.log(`⏸️ VERIFIED — payout on hold pending compliance review. No funds moved.`);
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());
381
- } else if (result.action === "VERIFIED") {
382
- console.log(`⚠️ VERIFIED — approved, but the milestone was NOT completed. No money moved; if it stays VERIFIED, contact support@cascrow.com.`);
383
- } else if (result.timedOut === true) {
384
- console.log(`⏳ PENDING_REVIEW — the panel did not answer (too few models). NOT a rejection; a human decides.`);
385
- } else if (result.action === "PENDING_REVIEW") {
386
- console.log(`⏳ PENDING_REVIEW — ${result.confidence}% confidence, manual review required.`);
387
- } else if (result.action === "REJECTED") {
388
- console.log(`❌ REJECTED — ${result.confidence}% confidence.`);
389
- } else {
390
- console.log(`⚠️ Outcome not recognised by this version (action: ${result.action}). No conclusion drawn.`);
391
- }
392
- console.log(` Decision: ${result.decision ?? "none read (the stream dropped before it)"}`);
393
- console.log(` View contract: ${BASE_URL}/contract/${invite.contractId}`);
394
- console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`);
395
- }
396
-
397
- async function getAgentId() {
398
- const data = await apiGet("/api/agent/me");
399
- return data.agentId;
400
- }
401
-
402
- async function main() {
403
- const agentId = await getAgentId();
404
- console.log(`\n🤖 Cascrow Builder Agent started`);
405
- console.log(` Agent ID: ${agentId}`);
406
- console.log(` Share this ID with the Requester agent`);
407
- console.log(` Polling for work every ${POLL_INTERVAL_MS / 1000}s...\n`);
408
-
409
- // Register EVM wallet address so Requester can fund real escrow to this agent.
410
- //
411
- // POST /api/user/wallet proves key possession: it wants a challenge token from
412
- // GET /api/user/wallet/challenge plus a signature over the nonce it carries.
413
- // This CLI is deliberately dependency-free and Node's crypto cannot produce an
414
- // EIP-191 signature (no keccak256, no recovery id), so it cannot complete that
415
- // flow on its own — and posting the address alone has been answered with 400
416
- // since the check went in. Say so plainly instead of warning past it: a
417
- // builder whose wallet is not linked is not discoverable and cannot be paid,
418
- // which is worth more than one line of yellow text.
419
- if (AGENT_EVM_WALLET) {
420
- try {
421
- await apiPost("/api/user/wallet", { walletAddress: AGENT_EVM_WALLET });
422
- console.log(` 💳 EVM wallet registered: ${AGENT_EVM_WALLET}\n`);
423
- } catch (e) {
424
- console.warn(
425
- ` ⚠️ Wallet NOT linked (${e.message}).\n` +
426
- ` ${AGENT_EVM_WALLET} is unusable until it is: escrow cannot be funded to it\n` +
427
- ` and this agent will not appear in /api/agent/discover.\n` +
428
- ` Link it once, from a signer that holds the key:\n` +
429
- ` 1. GET ${BASE_URL}/api/user/wallet/challenge (Bearer <your API key>)\n` +
430
- ` 2. sign the nonce in the returned token with the wallet's key\n` +
431
- ` 3. POST ${BASE_URL}/api/user/wallet { walletAddress, signature, token }\n`
432
- );
433
- }
434
- }
435
-
436
- while (true) {
437
- try {
438
- const data = await apiGet("/api/agent/pending-invites");
439
- const invites = data.invites ?? [];
440
- for (const invite of invites) {
441
- await handleInvite(invite);
442
- }
443
- } catch (err) {
444
- console.error(`[poll error] ${err.message}`);
445
- }
446
- await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
447
- }
448
- }
449
-
450
- main().catch((err) => {
451
- console.error("Fatal:", err.message);
452
- process.exit(1);
453
- });