cascrow-cli 1.4.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 +38 -0
- package/LICENSE +21 -0
- package/README.md +28 -31
- package/index.js +372 -95
- package/package.json +8 -2
- package/builder-agent.js +0 -430
package/index.js
CHANGED
|
@@ -6,10 +6,93 @@
|
|
|
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.2.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.2.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
|
+
"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).",
|
|
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.",
|
|
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.",
|
|
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.",
|
|
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.",
|
|
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.",
|
|
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.",
|
|
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.",
|
|
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.",
|
|
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.",
|
|
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.",
|
|
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.",
|
|
45
|
+
"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.",
|
|
46
|
+
"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.",
|
|
47
|
+
"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."
|
|
48
|
+
},
|
|
49
|
+
"joined": {
|
|
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.",
|
|
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.",
|
|
52
|
+
"unknown": "Joined as Builder. The server did not say whether the contract is active — check it with cascrow_get_contract before submitting proof."
|
|
53
|
+
},
|
|
54
|
+
"payeeHints": {
|
|
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>."
|
|
56
|
+
},
|
|
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.",
|
|
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."
|
|
61
|
+
};
|
|
62
|
+
// END v3-client-text
|
|
63
|
+
const TX_HASH_RE = /^0x[0-9a-fA-F]{64}$/;
|
|
64
|
+
function v3HintFor(code, body) {
|
|
65
|
+
if (!code) return null;
|
|
66
|
+
// Role-aware (post-review H9): CLAIM_PROOF_UNKNOWN without objectionDeadline comes from
|
|
67
|
+
// prepare?action=submitClaim and reaches the PAYEE; with it, a live claim — the payer's.
|
|
68
|
+
const b = body && typeof body === "object" ? body : null;
|
|
69
|
+
const liveClaim = !!b && typeof b.objectionDeadline === "string";
|
|
70
|
+
if (b && !liveClaim && V3_CLIENT_TEXT.payeeHints[code]) return V3_CLIENT_TEXT.payeeHints[code];
|
|
71
|
+
return V3_CLIENT_TEXT.codeHints[code] || null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** The fields of a V3 refusal body its recipient acts on, as one line (post-review G5) — the owner's rule. */
|
|
75
|
+
function v3RefusalDetail(body) {
|
|
76
|
+
if (!body || typeof body !== "object") return "";
|
|
77
|
+
const out = [];
|
|
78
|
+
if (typeof body.remedy === "string" && body.remedy) out.push(body.remedy);
|
|
79
|
+
if (Array.isArray(body.allowed)) out.push(`Allowed now: ${body.allowed.length ? body.allowed.map(String).join(", ") : "nothing"}.`);
|
|
80
|
+
if (typeof body.objectionDeadline === "string" && body.objectionDeadline) out.push(`Objection deadline: ${body.objectionDeadline}.`);
|
|
81
|
+
if (typeof body.offerExpiresAt === "string" && body.offerExpiresAt) out.push(`Open offer expires: ${body.offerExpiresAt}.`);
|
|
82
|
+
return out.join(" ");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The line for a `POST /api/contracts/join` answer — same rule as `joinedLine` in
|
|
87
|
+
* src/lib/v3-client-text.ts (v3-client-text-parity.test.ts runs this copy against it).
|
|
88
|
+
*/
|
|
89
|
+
function joinedLine(body) {
|
|
90
|
+
const activated = body && typeof body === "object" ? body.activated : undefined;
|
|
91
|
+
if (activated === true) return V3_CLIENT_TEXT.joined.active;
|
|
92
|
+
if (activated === false) return V3_CLIENT_TEXT.joined.awaitingOffer;
|
|
93
|
+
return V3_CLIENT_TEXT.joined.unknown;
|
|
94
|
+
}
|
|
95
|
+
|
|
13
96
|
// ─── HTTP helpers ─────────────────────────────────────────────────────────────
|
|
14
97
|
// Authorization header is omitted entirely when key is empty, rather than
|
|
15
98
|
// sending a garbage "Bearer " token — matters for `verify-work`, the one
|
|
@@ -81,15 +164,20 @@ async function fetchJson(method, path, { json, form, key = API_KEY, deadlineMs =
|
|
|
81
164
|
const retryAfterSeconds = retryAfterOf(res, data);
|
|
82
165
|
const base = typeof data.error === "string" && data.error ? data.error : `HTTP ${res.status}`;
|
|
83
166
|
// A platform error page on a POST (typically the 504 when a route hits its own
|
|
84
|
-
// maxDuration —
|
|
85
|
-
// may have moved money before it was killed. Say so, and
|
|
167
|
+
// maxDuration — contracts/review settles on chain inside its own) does not mean the
|
|
168
|
+
// call failed: the function may have moved money before it was killed. Say so, and
|
|
169
|
+
// how to check.
|
|
86
170
|
const mayHaveCompleted = method !== "GET" && res.status >= 500 && data.code === "UPSTREAM_NON_JSON";
|
|
87
171
|
throw Object.assign(
|
|
88
172
|
new Error(
|
|
89
173
|
`${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." : "")
|
|
174
|
+
(mayHaveCompleted ? ". The request may still have completed on the server — check the contract (cascrow get --contract <id>) before repeating it." : "") +
|
|
175
|
+
// A MilestoneEscrowV3 refusal says who can act and how (F-72): the body's actionable
|
|
176
|
+
// fields (G5), then the owner's words, role-aware on the body (H9).
|
|
177
|
+
(v3RefusalDetail(data) ? ` — ${v3RefusalDetail(data)}` : "") +
|
|
178
|
+
(v3HintFor(code, data) ? `\n\n${v3HintFor(code, data)}` : "")
|
|
91
179
|
),
|
|
92
|
-
{ status: res.status, code, retryAfterSeconds }
|
|
180
|
+
{ status: res.status, code, retryAfterSeconds, body: data }
|
|
93
181
|
);
|
|
94
182
|
}
|
|
95
183
|
return data;
|
|
@@ -273,15 +361,17 @@ async function waitForProof(proofId, maxMs = 100_000, key = API_KEY, pollMs = 5_
|
|
|
273
361
|
}
|
|
274
362
|
continue;
|
|
275
363
|
}
|
|
276
|
-
|
|
364
|
+
// `notCompletableWithoutEscrow`: no verdict can complete this milestone — final.
|
|
365
|
+
if (!last.pending || last.milestoneClosed === true || last.notCompletableWithoutEscrow === true) return { ...last, proofId };
|
|
277
366
|
// Parked for a human: no poll will change it (same rule as both MCP servers).
|
|
278
|
-
|
|
367
|
+
// Never a MilestoneEscrowV3 proof (post-review L2): no human review decides one.
|
|
368
|
+
if (last.escrowVersion !== 3 && last.running !== true && last.milestoneStatus === "PENDING_REVIEW") return { ...last, proofId, awaitingHumanReview: true };
|
|
279
369
|
if (last.claimPhase === "orphaned") return { ...last, proofId, stoppedBecause: "interrupted" };
|
|
280
370
|
}
|
|
281
371
|
return { ...(last ?? {}), pending: true, proofId, timedOut: true };
|
|
282
372
|
}
|
|
283
373
|
|
|
284
|
-
/** One line for a proof-status result, saying whether funds moved
|
|
374
|
+
/** One line for a proof-status result, saying whether funds moved. */
|
|
285
375
|
function statusSummary(s) {
|
|
286
376
|
if (s.stoppedBecause === 404) return `⛔ Proof ${s.proofId} no longer exists (deleted or erased). Nothing will verify it.`;
|
|
287
377
|
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 +383,35 @@ function statusSummary(s) {
|
|
|
293
383
|
if (s.awaitingHumanReview) return "⏳ PENDING_REVIEW — the panel reached no verdict; a human decides. Waiting will not change it.";
|
|
294
384
|
if (s.timedOut) return `⏳ Still verifying. Check again: cascrow status --proof ${s.proofId} --wait`;
|
|
295
385
|
const conf = typeof s.confidence === "number" ? ` (${s.confidence}% confidence)` : "";
|
|
386
|
+
// MilestoneEscrowV3 (post-review G4): the verdict is a RECOMMENDATION and moves no money.
|
|
387
|
+
// Asked before every v2 arm — the claim-relevant answer is the SUBMISSION's verdict
|
|
388
|
+
// (`submissionVerdict`, `claimReady`), never this proof's `decision`, and a closed or
|
|
389
|
+
// COMPLETED V3 milestone was settled on chain by the parties.
|
|
390
|
+
if (s.escrowVersion === 3 && !(s.milestoneStatus === "COMPLETED" || s.milestoneClosed === true)) {
|
|
391
|
+
const line = V3_CLIENT_TEXT.recommendation.replace("{verdict}", s.submissionVerdict ?? "PENDING").replace("{confidence}", String(s.confidence ?? 0));
|
|
392
|
+
return s.manifestHash ? `${line} manifestHash: ${s.manifestHash}` : line;
|
|
393
|
+
}
|
|
394
|
+
if (s.escrowVersion === 3) {
|
|
395
|
+
return s.milestoneStatus === "COMPLETED"
|
|
396
|
+
? "✅ COMPLETED — the MilestoneEscrowV3 escrow paid out on chain."
|
|
397
|
+
: `⛔ CLOSED — the milestone is ${s.terminalStatus ?? "closed"}; the MilestoneEscrowV3 escrow settled on chain. Do not resubmit to it.`;
|
|
398
|
+
}
|
|
296
399
|
// COMPLETED first: proof-status reports a paid milestone as `milestoneClosed` too
|
|
297
400
|
// (terminalStatus COMPLETED), and the CLOSED arm below would read a release as
|
|
298
401
|
// "moves no funds" — SEC-059's inversion (same order as proofOutcomeLine in both
|
|
299
402
|
// MCP servers). Whether it was THIS proof's YES is `decision`.
|
|
300
403
|
if (s.milestoneStatus === "COMPLETED" || s.terminalStatus === "COMPLETED") {
|
|
301
|
-
return s.decision === "YES" ? `✅ COMPLETED${conf} —
|
|
404
|
+
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
405
|
}
|
|
303
406
|
if (s.milestoneClosed === true) {
|
|
304
407
|
return s.terminalReason === "RELEASED_ON_CHAIN"
|
|
305
408
|
? "⛔ CLOSED (EXPIRED) — the chain shows the builder WAS paid, just not by this verification."
|
|
306
409
|
: `⛔ CLOSED — the milestone is ${s.terminalStatus ?? "closed"}; this verification moved no funds. Do not resubmit to it.`;
|
|
307
410
|
}
|
|
308
|
-
|
|
309
|
-
|
|
411
|
+
// An approval cannot complete this milestone (an amount without a MilestoneEscrowV3
|
|
412
|
+
// escrow); the verdict doors answered 409 NOT_COMPLETABLE_WITHOUT_ESCROW.
|
|
413
|
+
if (s.notCompletableWithoutEscrow === true) return `⛔ NOT COMPLETABLE — ${V3_CLIENT_TEXT.codeHints.NOT_COMPLETABLE_WITHOUT_ESCROW}`;
|
|
414
|
+
if (s.milestoneStatus === "VERIFIED") return `⚠️ VERIFIED${conf} — approved, not completed yet.`;
|
|
310
415
|
if (s.milestoneStatus === "PENDING_REVIEW") return `⏳ PENDING_REVIEW${conf} — manual review required.`;
|
|
311
416
|
if (s.decision === "NO") return `❌ REJECTED${conf}.`;
|
|
312
417
|
return `ℹ️ Decision: ${s.decision ?? "none"}${conf}, milestone ${s.milestoneStatus ?? "unknown"}.`;
|
|
@@ -322,6 +427,12 @@ function statusSummary(s) {
|
|
|
322
427
|
*/
|
|
323
428
|
function outcomeLine(r) {
|
|
324
429
|
const c = r.confidence ?? 0;
|
|
430
|
+
// MilestoneEscrowV3: a recommendation that moves nothing — asked first, so no arm
|
|
431
|
+
// below can read it as a payout or a rejection.
|
|
432
|
+
if (r.action === "V3_RECOMMENDATION") {
|
|
433
|
+
// The SUBMISSION's verdict, never the panel's `decision` on one file.
|
|
434
|
+
return V3_CLIENT_TEXT.recommendation.replace("{verdict}", r.submissionVerdict ?? "PENDING").replace("{confidence}", String(c));
|
|
435
|
+
}
|
|
325
436
|
if (r.action === "SUPERSEDED") {
|
|
326
437
|
// `settledAs`: the milestone was closed (e.g. EXPIRED) before this verification could
|
|
327
438
|
// settle it, not beaten by a proof.
|
|
@@ -331,17 +442,19 @@ function outcomeLine(r) {
|
|
|
331
442
|
}
|
|
332
443
|
if (r.action === "COMPLETED") {
|
|
333
444
|
return r.alreadyReleased === true
|
|
334
|
-
?
|
|
335
|
-
: `✅ COMPLETED (${c}% confidence) —
|
|
445
|
+
? `✅ COMPLETED — another request completed this milestone first; no money was held (verification-only contract). Do not resubmit.`
|
|
446
|
+
: `✅ COMPLETED (${c}% confidence) — no money was held (verification-only contract)${r.txHash ? `, tx ${r.txHash}` : ""}.`;
|
|
336
447
|
}
|
|
337
448
|
if (r.complianceBlocked === true) {
|
|
338
449
|
return `⏸️ VERIFIED (${c}%) — payout on hold pending compliance review. No funds moved.`;
|
|
339
450
|
}
|
|
340
|
-
if (r.
|
|
341
|
-
return
|
|
451
|
+
if (r.action === "NOT_COMPLETABLE_WITHOUT_ESCROW") {
|
|
452
|
+
return `⛔ NOT COMPLETED (${c}% confidence) — ${V3_CLIENT_TEXT.codeHints.NOT_COMPLETABLE_WITHOUT_ESCROW}`;
|
|
342
453
|
}
|
|
343
454
|
if (r.action === "VERIFIED") {
|
|
344
|
-
|
|
455
|
+
// Approved, but no completion followed; the producer does not reach this since
|
|
456
|
+
// Phase 6. No retry is promised — there is nothing to retry.
|
|
457
|
+
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
458
|
}
|
|
346
459
|
if (r.timedOut === true) {
|
|
347
460
|
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 +536,7 @@ function err(msg) {
|
|
|
423
536
|
|
|
424
537
|
function usage() {
|
|
425
538
|
console.log(`
|
|
426
|
-
|
|
539
|
+
cascrow CLI — AI verification, on-chain reputation and non-custodial escrow for agents
|
|
427
540
|
|
|
428
541
|
Usage:
|
|
429
542
|
cascrow <command> [options]
|
|
@@ -433,8 +546,6 @@ Commands:
|
|
|
433
546
|
verify-work Verify AI-generated work — no contract, no escrow, no account needed
|
|
434
547
|
verify Submit proof and get AI verdict in one call (recommended)
|
|
435
548
|
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
549
|
submit Upload proof text — verification starts by itself (then: status --proof <id> --wait)
|
|
439
550
|
status Show (or --wait for) the verification result of a proof
|
|
440
551
|
get Get contract status and milestones
|
|
@@ -443,6 +554,11 @@ Commands:
|
|
|
443
554
|
check-invites Check pending contract invites for this agent
|
|
444
555
|
review-queue As funder: list milestones still waiting for your verdict
|
|
445
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
|
|
558
|
+
v3-claims As payer: list MilestoneEscrowV3 claims you can still object to on chain
|
|
559
|
+
v3 <sub> MilestoneEscrowV3, remote half: offer | acceptance | accept-submit | prepare | report-tx | attestation | claims
|
|
560
|
+
and "v3 sign <cascrow-signer command…>" — runs the pinned local signer (npx cascrow-signer@0.2.0),
|
|
561
|
+
which holds your key on YOUR machine. cascrow never signs, sends or relays for you.
|
|
446
562
|
me Print this agent's ID
|
|
447
563
|
|
|
448
564
|
Options for each command:
|
|
@@ -469,21 +585,18 @@ Options for each command:
|
|
|
469
585
|
create
|
|
470
586
|
--title <title> required short milestone title (printed on the certificate)
|
|
471
587
|
--days <n> optional deadline days from now (default: 7)
|
|
472
|
-
--amount <usd> optional
|
|
588
|
+
--amount <usd> optional default 0 = verification-only (active once the builder joins). Above 0 the
|
|
589
|
+
milestone is a MilestoneEscrowV3 escrow in native XRP: offer it with
|
|
590
|
+
"cascrow v3 offer", the builder accepts, you fund with "cascrow v3 sign fund"
|
|
473
591
|
--criteria <text> optional what the AI panel checks the proof against (max 4000 chars)
|
|
474
592
|
--criteria-file <path> optional the same, read from a file
|
|
475
593
|
--checklist-file <path> optional JSON array of { title, severity?, id? } (max 20) scored item by item on text proofs
|
|
476
594
|
--review-mode <mode> optional AUTO | MANUAL | MANUAL_AUTO — what happens when the panel is unsure (60–85%)
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
--
|
|
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
|
|
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
|
|
487
600
|
|
|
488
601
|
submit [or use verify for text proof in one call]
|
|
489
602
|
--milestone <id> required
|
|
@@ -522,15 +635,13 @@ Examples:
|
|
|
522
635
|
# Register a new agent account (no human in the loop)
|
|
523
636
|
cascrow register --email bot@example.com --password secret123
|
|
524
637
|
|
|
525
|
-
# One-shot: create contract
|
|
638
|
+
# One-shot: create a verification-only contract (active once the builder joins), then verify a code fix
|
|
526
639
|
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
640
|
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
641
|
|
|
530
|
-
#
|
|
531
|
-
cascrow
|
|
532
|
-
cascrow
|
|
533
|
-
cascrow verify --contract <contractId> --proof "API live at https://api.example.com, all endpoints responding"
|
|
642
|
+
# Paid work is a MilestoneEscrowV3 escrow (native XRP, testnet): create with --amount,
|
|
643
|
+
# offer it (cascrow v3 offer), the builder accepts, and you fund with one transaction
|
|
644
|
+
# from your own machine (cascrow v3 sign fund). This CLI never takes a key.
|
|
534
645
|
|
|
535
646
|
# Agent-to-agent handoff
|
|
536
647
|
cascrow me # → get your Agent ID
|
|
@@ -542,10 +653,30 @@ Examples:
|
|
|
542
653
|
|
|
543
654
|
# As the funder: decide what the AI panel could not (confidence 60-85)
|
|
544
655
|
# Poll this — a manual_review.required webhook you missed is a decision nobody
|
|
545
|
-
# makes
|
|
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.
|
|
659
|
+
# Verification-only contracts: a verdict completes or hands back, and moves no money.
|
|
546
660
|
cascrow review-queue
|
|
547
661
|
cascrow review --contract <contractId> --decision APPROVE
|
|
548
662
|
cascrow review --contract <contractId> --decision REJECT --milestone <milestoneId>
|
|
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
|
+
|
|
668
|
+
# As the payer of a MilestoneEscrowV3 escrow: claims release unless YOUR wallet
|
|
669
|
+
# calls object(escrowId) before the deadline — cascrow cannot object for you.
|
|
670
|
+
cascrow v3-claims
|
|
671
|
+
|
|
672
|
+
# MilestoneEscrowV3 (native XRP). cascrow prepares and reads; you sign locally.
|
|
673
|
+
cascrow v3 offer --milestone <id> --review-period-sec 259200 [--watcher] [--arbiter 0x… --arbitration-period-sec N]
|
|
674
|
+
cascrow v3 acceptance --milestone <id> # as builder: the frozen terms to check
|
|
675
|
+
cascrow v3 sign accept --milestone <id> --network testnet # check + sign locally (pinned cascrow-signer)
|
|
676
|
+
cascrow v3 sign fund --milestone <id> --network testnet # as payer: one transaction, value = amount
|
|
677
|
+
cascrow v3 prepare --milestone <id> --action object --reason-hash 0x… # UNSIGNED — verify before signing
|
|
678
|
+
cascrow v3 report-tx --milestone <id> --tx 0x<64 hex> # a HASH only, never a signed transaction
|
|
679
|
+
cascrow v3 attestation --escrow 0x… --claim-index 1 # non-binding AI recommendation
|
|
549
680
|
`);
|
|
550
681
|
process.exit(0);
|
|
551
682
|
}
|
|
@@ -672,7 +803,7 @@ async function cmdVerifyWork(args) {
|
|
|
672
803
|
}
|
|
673
804
|
}
|
|
674
805
|
|
|
675
|
-
process.stderr.write("\nVerifying with 5
|
|
806
|
+
process.stderr.write("\nVerifying with 5 AI models run by 5 different providers (no escrow, no account needed)...\n\n");
|
|
676
807
|
|
|
677
808
|
let data;
|
|
678
809
|
if (args.image) {
|
|
@@ -747,7 +878,8 @@ async function cmdVerifyWork(args) {
|
|
|
747
878
|
}
|
|
748
879
|
|
|
749
880
|
// One-shot verify: submit text proof + get verdict in a single call via /api/mcp/submit.
|
|
750
|
-
//
|
|
881
|
+
// A verification-only milestone completes (NFT minted) on approval; a MilestoneEscrowV3
|
|
882
|
+
// verdict is a recommendation that moves no money. No separate `verify --proof <id>` needed.
|
|
751
883
|
async function cmdVerify(args) {
|
|
752
884
|
if (!API_KEY) err("CASCROW_API_KEY is not set");
|
|
753
885
|
if (!args.proof) err("--proof is required");
|
|
@@ -792,9 +924,12 @@ async function cmdVerify(args) {
|
|
|
792
924
|
// nothing moved, do not resubmit" from "another proof won" (round-3 pass 2).
|
|
793
925
|
settledAs: typeof result.settledAs === "string" ? result.settledAs : null,
|
|
794
926
|
complianceBlocked: result.complianceBlocked === true,
|
|
795
|
-
|
|
927
|
+
notCompletableWithoutEscrow: result.action === "NOT_COMPLETABLE_WITHOUT_ESCROW",
|
|
796
928
|
panelUnavailable: result.timedOut === true,
|
|
797
929
|
txHash: result.txHash ?? null,
|
|
930
|
+
// MilestoneEscrowV3 (post-review G4): the recommendation's own fields, so a machine
|
|
931
|
+
// caller reads the SUBMISSION's verdict and the hash to claim with.
|
|
932
|
+
...(result.action === "V3_RECOMMENDATION" || result.escrowVersion === 3 ? v3Fields(result) : {}),
|
|
798
933
|
reasoning: result.reasoning,
|
|
799
934
|
modelVotes: (result.votes ?? []).map(v => ({ model: v.model, decision: v.decision, confidence: v.confidence })),
|
|
800
935
|
});
|
|
@@ -825,14 +960,17 @@ async function cmdVerify(args) {
|
|
|
825
960
|
// `verdict` key, so `data.verdict ?? "unknown"` fell to the final arm below and told
|
|
826
961
|
// the builder "❌ REJECTED — resubmit with stronger proof" for a milestone that may
|
|
827
962
|
// well have been approved and paid (SEC-055). Classified before the verdict is read.
|
|
828
|
-
const
|
|
963
|
+
const isV3 = data.action === "V3_RECOMMENDATION";
|
|
964
|
+
const verdict = isV3
|
|
965
|
+
? "v3_recommendation"
|
|
966
|
+
: data.decided === "elsewhere"
|
|
829
967
|
? "decided_elsewhere"
|
|
830
968
|
: data.panelUnavailable
|
|
831
969
|
? "panel_unavailable"
|
|
832
970
|
: (data.verdict ?? "unknown");
|
|
833
971
|
const confidence = data.confidence ?? 0;
|
|
834
|
-
//
|
|
835
|
-
|
|
972
|
+
// An approved milestone that cannot complete is not a 200: the route answers 409
|
|
973
|
+
// NOT_COMPLETABLE_WITHOUT_ESCROW, which `apiPost` turns into an error with its hint.
|
|
836
974
|
|
|
837
975
|
// Print model votes if available
|
|
838
976
|
const votes = Array.isArray(data.model_votes) ? data.model_votes : [];
|
|
@@ -848,7 +986,12 @@ async function cmdVerify(args) {
|
|
|
848
986
|
const settledAs = verdict === "decided_elsewhere" && typeof data.settledAs === "string" ? data.settledAs : null;
|
|
849
987
|
|
|
850
988
|
let statusLine;
|
|
851
|
-
if (
|
|
989
|
+
if (isV3) {
|
|
990
|
+
// A MilestoneEscrowV3 submission: sealed, recommended on, nothing moved (G4).
|
|
991
|
+
statusLine = V3_CLIENT_TEXT.recommendation.replace("{verdict}", data.submissionVerdict ?? "PENDING").replace("{confidence}", String(confidence));
|
|
992
|
+
if (data.manifestHash) statusLine += `\n manifestHash: ${data.manifestHash}`;
|
|
993
|
+
if (typeof data.next === "string") statusLine += `\n ${data.next}`;
|
|
994
|
+
} else if (settledAs) {
|
|
852
995
|
statusLine =
|
|
853
996
|
`ℹ️ This milestone was closed (${settledAs}) before this submission could settle it, so this submission ` +
|
|
854
997
|
"changed nothing and moved no funds itself. Your evidence was recorded. Do not resubmit to this milestone";
|
|
@@ -862,10 +1005,6 @@ async function cmdVerify(args) {
|
|
|
862
1005
|
"⏳ PENDING_REVIEW — the verification panel could not reach a verdict (too few models " +
|
|
863
1006
|
"answered). This is NOT a rejection and NOT a judgement of your work; a human decides, " +
|
|
864
1007
|
"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
1008
|
} else if (verdict === "approved") {
|
|
870
1009
|
statusLine = `✅ VERIFIED (${confidence}% confidence)`;
|
|
871
1010
|
if (data.on_chain_url) statusLine += `\n Proof: ${data.on_chain_url}`;
|
|
@@ -886,18 +1025,28 @@ async function cmdVerify(args) {
|
|
|
886
1025
|
print({
|
|
887
1026
|
verdict,
|
|
888
1027
|
confidence,
|
|
889
|
-
|
|
890
|
-
passed: verdict === "approved" && !releaseHoldUntil,
|
|
891
|
-
release_hold_until: releaseHoldUntil,
|
|
1028
|
+
passed: verdict === "approved",
|
|
892
1029
|
...(settledAs ? { settled_as: settledAs } : {}),
|
|
893
1030
|
reasoning: data.reasoning ?? "",
|
|
894
1031
|
on_chain_url: data.on_chain_url ?? null,
|
|
895
1032
|
proof_id: data.proof_id ?? null,
|
|
896
1033
|
signed_at: data.signed_at ?? null,
|
|
1034
|
+
...(isV3 ? v3Fields(data) : {}),
|
|
897
1035
|
model_votes: votes,
|
|
898
1036
|
});
|
|
899
1037
|
}
|
|
900
1038
|
|
|
1039
|
+
/** The MilestoneEscrowV3 fields of an answer, for JSON output (post-review G4). Never derived: forwarded. */
|
|
1040
|
+
function v3Fields(r) {
|
|
1041
|
+
return {
|
|
1042
|
+
escrowVersion: 3,
|
|
1043
|
+
submissionId: r.submissionId ?? null,
|
|
1044
|
+
manifestHash: r.manifestHash ?? null,
|
|
1045
|
+
submissionVerdict: r.submissionVerdict ?? "PENDING",
|
|
1046
|
+
claimReady: r.claimReady === true,
|
|
1047
|
+
};
|
|
1048
|
+
}
|
|
1049
|
+
|
|
901
1050
|
async function cmdCreate(args) {
|
|
902
1051
|
if (!API_KEY) err("CASCROW_API_KEY is not set");
|
|
903
1052
|
if (!args.title) err("--title is required");
|
|
@@ -924,6 +1073,10 @@ async function cmdCreate(args) {
|
|
|
924
1073
|
if (reviewMode !== undefined && !["AUTO", "MANUAL", "MANUAL_AUTO"].includes(reviewMode)) {
|
|
925
1074
|
err("--review-mode must be AUTO, MANUAL or MANUAL_AUTO");
|
|
926
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;
|
|
927
1080
|
let criteria, checklist;
|
|
928
1081
|
try {
|
|
929
1082
|
criteria = normalizeCriteria(criteriaRaw);
|
|
@@ -941,6 +1094,8 @@ async function cmdCreate(args) {
|
|
|
941
1094
|
...(checklist !== undefined ? { verificationChecklist: checklist } : {}),
|
|
942
1095
|
}],
|
|
943
1096
|
...(reviewMode !== undefined ? { agentReviewMode: reviewMode } : {}),
|
|
1097
|
+
...(reviewDays !== undefined ? { reviewWindowDays: reviewDays } : {}),
|
|
1098
|
+
...(panelAlone !== undefined ? { panelDecidesAlone: panelAlone } : {}),
|
|
944
1099
|
});
|
|
945
1100
|
|
|
946
1101
|
const contractUrl = `${BASE_URL}/contract/${data.contractId}`;
|
|
@@ -948,42 +1103,28 @@ async function cmdCreate(args) {
|
|
|
948
1103
|
process.stderr.write(`\nContract created: ${contractUrl}\n\n`);
|
|
949
1104
|
}
|
|
950
1105
|
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
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`);
|
|
1106
|
+
// Removed in 3.0.0 (cascrow Phase 6, owner decision 16): `fund` activated a v2
|
|
1107
|
+
// milestone. A $0 verification contract is active by itself once the builder has
|
|
1108
|
+
// joined, and a milestone with an amount is a MilestoneEscrowV3 escrow its payer
|
|
1109
|
+
// funds from their own machine. Kept as a command only to say so; it sends nothing.
|
|
1110
|
+
function cmdFundRemoved() {
|
|
1111
|
+
err(`${V3_CLIENT_TEXT.fundRemoved} [TOOL_REMOVED]`);
|
|
962
1112
|
}
|
|
963
1113
|
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
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`);
|
|
1114
|
+
// Removed in 2.0.0 (cascrow Phase 5): `escrow-fund --private-key` sent the key
|
|
1115
|
+
// to cascrow, which signed the deposit server-side. Kept as a command only to say
|
|
1116
|
+
// so: it sends NOTHING — no request, no argument — so a key typed on the command
|
|
1117
|
+
// line goes nowhere. It cannot un-type it, which is why the message tells the user
|
|
1118
|
+
// what a key that reached a shell history (or an older version of this CLI) is now.
|
|
1119
|
+
function cmdEscrowFundRemoved() {
|
|
1120
|
+
err(
|
|
1121
|
+
"escrow-fund was removed in cascrow-cli 2.0.0: cascrow never accepts, creates, stores or signs with private keys. " +
|
|
1122
|
+
"A $0 verification contract needs no funding — it is active once the builder has joined. A milestone with an amount " +
|
|
1123
|
+
"is a MilestoneEscrowV3 escrow in native XRP, funded by the payer with one transaction from their own machine: first link " +
|
|
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), " +
|
|
1126
|
+
"treat that key as exposed and move its funds to a new wallet. [CUSTODY_PATH_REMOVED]"
|
|
1127
|
+
);
|
|
987
1128
|
}
|
|
988
1129
|
|
|
989
1130
|
// Legacy two-step: upload proof → returns proofId → user runs `cascrow verify --proof <id>`
|
|
@@ -1031,7 +1172,8 @@ async function cmdJoin(args) {
|
|
|
1031
1172
|
if (!args.invite) err("--invite is required");
|
|
1032
1173
|
const data = await apiPost("/api/contracts/join", { inviteCode: args.invite });
|
|
1033
1174
|
print(data);
|
|
1034
|
-
|
|
1175
|
+
// `activated`: a $0 contract is active now; a money contract waits for its V3 offer.
|
|
1176
|
+
process.stderr.write(`\nJoined contract: ${data.contractId}\n${joinedLine(data)}\n\n`);
|
|
1035
1177
|
}
|
|
1036
1178
|
|
|
1037
1179
|
async function cmdHandoff(args) {
|
|
@@ -1071,9 +1213,9 @@ async function cmdCheckInvites() {
|
|
|
1071
1213
|
* verdict on cannot come apart (F-22).
|
|
1072
1214
|
*
|
|
1073
1215
|
* `review-queue` is the pull behind the `manual_review.required` webhook: a
|
|
1074
|
-
* webhook nobody received is a decision nobody makes, and cascrow
|
|
1075
|
-
*
|
|
1076
|
-
*
|
|
1216
|
+
* webhook nobody received is a decision nobody makes, and cascrow completes an
|
|
1217
|
+
* undecided milestone after 48 h (agent contracts in MANUAL_AUTO/AUTO) or 14 days
|
|
1218
|
+
* (every other contract). Verification-only contracts: no money moves. `review` ships with it rather than later, because a list
|
|
1077
1219
|
* its reader cannot act on names a remedy they are unable to perform (F-72).
|
|
1078
1220
|
*/
|
|
1079
1221
|
async function cmdReviewQueue() {
|
|
@@ -1081,8 +1223,16 @@ async function cmdReviewQueue() {
|
|
|
1081
1223
|
const data = await apiGet("/api/agent/review-queue");
|
|
1082
1224
|
const contracts = data.contracts ?? [];
|
|
1083
1225
|
print(data);
|
|
1226
|
+
// Said in both branches: an empty v2 queue is exactly when a reader stops
|
|
1227
|
+
// looking, and a V3 claim window keeps running.
|
|
1228
|
+
const v3 =
|
|
1229
|
+
data.v3ClaimsPending === null
|
|
1230
|
+
? "Open MilestoneEscrowV3 claims could not be counted — run: cascrow v3-claims\n\n"
|
|
1231
|
+
: data.v3ClaimsPending > 0
|
|
1232
|
+
? `${data.v3ClaimsPending} MilestoneEscrowV3 claim(s) can still be objected to — run: cascrow v3-claims\n\n`
|
|
1233
|
+
: "";
|
|
1084
1234
|
if (contracts.length === 0) {
|
|
1085
|
-
process.stderr.write(
|
|
1235
|
+
process.stderr.write(`\nNo milestones are waiting for your verdict.\n\n${v3}`);
|
|
1086
1236
|
return;
|
|
1087
1237
|
}
|
|
1088
1238
|
const lines = contracts.map((c) => {
|
|
@@ -1093,15 +1243,100 @@ async function cmdReviewQueue() {
|
|
|
1093
1243
|
: pending.length > 1
|
|
1094
1244
|
? `${pending.length} milestones pending — pass --milestone <id>`
|
|
1095
1245
|
: "contract-level review — omit --milestone";
|
|
1096
|
-
return ` ${c.contractId}
|
|
1246
|
+
return ` ${c.contractId} (${ms})`;
|
|
1097
1247
|
}).join("\n");
|
|
1098
1248
|
const more = data.truncated ? `\n(${data.total} in total — this list is capped.)` : "";
|
|
1099
1249
|
process.stderr.write(
|
|
1100
1250
|
`\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"
|
|
1251
|
+
"Decide with: cascrow review --contract <id> --decision APPROVE|REJECT [--milestone <id>]\n\n" +
|
|
1252
|
+
v3
|
|
1102
1253
|
);
|
|
1103
1254
|
}
|
|
1104
1255
|
|
|
1256
|
+
/**
|
|
1257
|
+
* The payer's open MilestoneEscrowV3 claims — `GET /api/agent/v3-claims`, the
|
|
1258
|
+
* pull behind the `claim.submitted` webhook. Print only: the action is the
|
|
1259
|
+
* payer's own `object(escrowId)` on chain, which cascrow never performs.
|
|
1260
|
+
*/
|
|
1261
|
+
async function cmdV3Claims() {
|
|
1262
|
+
if (!API_KEY) err("CASCROW_API_KEY is not set");
|
|
1263
|
+
const data = await apiGet("/api/agent/v3-claims");
|
|
1264
|
+
const claims = data.claims ?? [];
|
|
1265
|
+
print(data);
|
|
1266
|
+
if (claims.length === 0) {
|
|
1267
|
+
process.stderr.write("\nNo MilestoneEscrowV3 claim is waiting on you.\n\n");
|
|
1268
|
+
return;
|
|
1269
|
+
}
|
|
1270
|
+
const lines = claims.map((c) =>
|
|
1271
|
+
` ${c.escrowId} on ${c.chainId}/${c.escrowContract} claim ${c.claimIndex}` +
|
|
1272
|
+
`${c.secondFunding ? " SECOND funding" : ""}${c.held ? " (held — verify on chain)" : ""} ` +
|
|
1273
|
+
(c.objectable === null ? "claim state unknown here — read getEscrow on chain" : c.objectable ? `object() before ${c.objectionDeadline}` : `window closed at ${c.objectionDeadline}`)
|
|
1274
|
+
).join("\n");
|
|
1275
|
+
const more = data.truncated ? "\n(This list is capped; more claims exist.)" : "";
|
|
1276
|
+
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`);
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
// ─── MilestoneEscrowV3, remote half (design Rev. 10) ──────────────────────────
|
|
1280
|
+
// cascrow prepares and reads; the party signs and sends with its own key. This CLI
|
|
1281
|
+
// holds no key and has no dependency: `v3 sign …` runs the PINNED local signer
|
|
1282
|
+
// (V3_CLIENT_TEXT.signerPin) in a child process, with this terminal attached, so
|
|
1283
|
+
// its prompts and its checks are the signer's own.
|
|
1284
|
+
async function cmdV3(sub, args, rawArgs) {
|
|
1285
|
+
if (sub === "sign") {
|
|
1286
|
+
const child = spawn("npx", ["--yes", V3_CLIENT_TEXT.signerPin, ...rawArgs], { stdio: "inherit", shell: process.platform === "win32" });
|
|
1287
|
+
const code = await new Promise((resolve) => child.on("close", resolve));
|
|
1288
|
+
process.exit(typeof code === "number" ? code : 1);
|
|
1289
|
+
}
|
|
1290
|
+
if (!API_KEY) err("CASCROW_API_KEY is not set");
|
|
1291
|
+
switch (sub) {
|
|
1292
|
+
case "offer": {
|
|
1293
|
+
if (!args.milestone || !args["review-period-sec"]) err("--milestone and --review-period-sec are required");
|
|
1294
|
+
const body = {
|
|
1295
|
+
milestoneId: args.milestone,
|
|
1296
|
+
reviewPeriodSec: Number(args["review-period-sec"]),
|
|
1297
|
+
watcherDeclared: args.watcher === true || args.watcher === "true",
|
|
1298
|
+
...(args.arbiter ? { arbiter: args.arbiter } : {}),
|
|
1299
|
+
...(args["arbitration-period-sec"] ? { arbitrationPeriodSec: Number(args["arbitration-period-sec"]) } : {}),
|
|
1300
|
+
};
|
|
1301
|
+
print(await apiPost("/api/escrow/v3/offer", body));
|
|
1302
|
+
process.stderr.write(`\nOffer sent. After the builder accepts, fund it from your machine: cascrow v3 sign fund --milestone ${args.milestone}\n\n`);
|
|
1303
|
+
return;
|
|
1304
|
+
}
|
|
1305
|
+
case "acceptance":
|
|
1306
|
+
if (!args.milestone) err("--milestone is required");
|
|
1307
|
+
print(await apiGet(`/api/escrow/v3/acceptance?milestoneId=${encodeURIComponent(args.milestone)}`));
|
|
1308
|
+
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");
|
|
1309
|
+
return;
|
|
1310
|
+
case "accept-submit":
|
|
1311
|
+
if (!args.offer || !args.signature) err("--offer and --signature are required");
|
|
1312
|
+
print(await apiPost("/api/escrow/v3/acceptance", { offerId: args.offer, signature: args.signature }));
|
|
1313
|
+
return;
|
|
1314
|
+
case "prepare": {
|
|
1315
|
+
if (!args.milestone || !args.action) err("--milestone and --action are required");
|
|
1316
|
+
const q = new URLSearchParams({ milestoneId: args.milestone, action: args.action });
|
|
1317
|
+
if (args["proof-hash"]) q.set("proofHash", args["proof-hash"]);
|
|
1318
|
+
if (args["reason-hash"]) q.set("reasonHash", args["reason-hash"]);
|
|
1319
|
+
if (args["new-deadline"]) q.set("newDeadline", String(args["new-deadline"]));
|
|
1320
|
+
print(await apiGet(`/api/escrow/v3/prepare?${q.toString()}`));
|
|
1321
|
+
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");
|
|
1322
|
+
return;
|
|
1323
|
+
}
|
|
1324
|
+
case "report-tx":
|
|
1325
|
+
if (!args.milestone || typeof args.tx !== "string" || !TX_HASH_RE.test(args.tx)) err(V3_CLIENT_TEXT.reportTxOnlyHash);
|
|
1326
|
+
print(await apiPost("/api/escrow/v3/sync", { milestoneId: args.milestone, txHash: args.tx }));
|
|
1327
|
+
return;
|
|
1328
|
+
case "attestation":
|
|
1329
|
+
if (!args.escrow || !args["claim-index"]) err("--escrow and --claim-index are required");
|
|
1330
|
+
print(await apiGet(`/api/escrow/v3/attestation?${new URLSearchParams({ escrowId: args.escrow, claimIndex: String(args["claim-index"]) }).toString()}`));
|
|
1331
|
+
return;
|
|
1332
|
+
case "claims":
|
|
1333
|
+
await cmdV3Claims();
|
|
1334
|
+
return;
|
|
1335
|
+
default:
|
|
1336
|
+
err(`unknown v3 command ${sub ?? "(none)"} — offer | acceptance | accept-submit | prepare | report-tx | attestation | claims | sign`);
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1105
1340
|
async function cmdReview(args) {
|
|
1106
1341
|
if (!API_KEY) err("CASCROW_API_KEY is not set");
|
|
1107
1342
|
const contractId = args.contract;
|
|
@@ -1111,11 +1346,50 @@ async function cmdReview(args) {
|
|
|
1111
1346
|
const body = args.milestone ? { contractId, decision, milestoneId: args.milestone } : { contractId, decision };
|
|
1112
1347
|
const data = await apiPost("/api/contracts/review", body);
|
|
1113
1348
|
print(data);
|
|
1349
|
+
// The route answers `{ status }` and nothing else: a review only decides
|
|
1350
|
+
// verification-only ($0) milestones, and nothing moves money (a MilestoneEscrowV3
|
|
1351
|
+
// milestone is refused — its payer releases or objects on chain).
|
|
1114
1352
|
const what =
|
|
1115
1353
|
decision === "APPROVE"
|
|
1116
|
-
? "approved
|
|
1354
|
+
? "approved and completed (a verification-only contract: no money moves)"
|
|
1117
1355
|
: "rejected — the milestone is back with the builder, who can resubmit";
|
|
1118
|
-
process.stderr.write(`\nMilestone ${what}.${data.
|
|
1356
|
+
process.stderr.write(`\nMilestone ${what}.${data.status ? ` Contract status: ${data.status}.` : ""}\n\n`);
|
|
1357
|
+
}
|
|
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
|
+
}
|
|
1119
1393
|
}
|
|
1120
1394
|
|
|
1121
1395
|
async function cmdMe() {
|
|
@@ -1140,8 +1414,8 @@ try {
|
|
|
1140
1414
|
case "verify-work": await cmdVerifyWork(args); break;
|
|
1141
1415
|
case "verify": await cmdVerify(args); break;
|
|
1142
1416
|
case "create": await cmdCreate(args); break;
|
|
1143
|
-
case "fund":
|
|
1144
|
-
case "escrow-fund":
|
|
1417
|
+
case "fund": cmdFundRemoved(); break;
|
|
1418
|
+
case "escrow-fund": cmdEscrowFundRemoved(); break;
|
|
1145
1419
|
case "submit": await cmdSubmit(args); break;
|
|
1146
1420
|
case "status": await cmdStatus(args); break;
|
|
1147
1421
|
case "get": await cmdGet(args); break;
|
|
@@ -1150,6 +1424,9 @@ try {
|
|
|
1150
1424
|
case "check-invites": await cmdCheckInvites(); break;
|
|
1151
1425
|
case "review-queue": await cmdReviewQueue(); break;
|
|
1152
1426
|
case "review": await cmdReview(args); break;
|
|
1427
|
+
case "human-review": await cmdHumanReview(args); break;
|
|
1428
|
+
case "v3-claims": await cmdV3Claims(); break;
|
|
1429
|
+
case "v3": await cmdV3(rest[0], parseArgs(rest.slice(1)), rest.slice(1)); break;
|
|
1153
1430
|
case "me": await cmdMe(); break;
|
|
1154
1431
|
default:
|
|
1155
1432
|
console.error(`Unknown command: ${command}\n`);
|