cascrow-cli 1.3.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -0
- package/README.md +49 -32
- package/builder-agent.js +146 -49
- package/index.js +619 -123
- package/package.json +2 -2
package/index.js
CHANGED
|
@@ -6,10 +6,91 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { readFileSync } from "node:fs";
|
|
9
|
+
import { spawn } from "node:child_process";
|
|
9
10
|
|
|
10
11
|
const BASE_URL = process.env.CASCROW_BASE_URL ?? "https://cascrow.com";
|
|
11
12
|
const API_KEY = process.env.CASCROW_API_KEY ?? "";
|
|
12
13
|
|
|
14
|
+
// What every cascrow client says about MilestoneEscrowV3 — a JSON copy of
|
|
15
|
+
// V3_CLIENT_TEXT in src/lib/v3-client-text.ts (this package cannot import the
|
|
16
|
+
// app). v3-client-text-parity.test.ts holds the copy equal to the owner.
|
|
17
|
+
// BEGIN v3-client-text
|
|
18
|
+
const V3_CLIENT_TEXT = {
|
|
19
|
+
"signerPin": "cascrow-signer@0.1.0",
|
|
20
|
+
"remoteTools": [
|
|
21
|
+
"cascrow_v3_offer",
|
|
22
|
+
"cascrow_v3_acceptance_get",
|
|
23
|
+
"cascrow_v3_acceptance_submit",
|
|
24
|
+
"cascrow_v3_prepare",
|
|
25
|
+
"cascrow_v3_report_tx",
|
|
26
|
+
"cascrow_v3_attestation",
|
|
27
|
+
"cascrow_v3_claims"
|
|
28
|
+
],
|
|
29
|
+
"codeHints": {
|
|
30
|
+
"V3_DISABLED": "MilestoneEscrowV3 is switched off for NEW money right now (offers, acceptances, funding). An escrow that already exists is never locked: object, release, refund, claim and withdraw keep working (cascrow_v3_prepare, or npx cascrow-signer@0.1.0 without cascrow at all).",
|
|
31
|
+
"RATE_UNAVAILABLE": "cascrow could not get a consistent XRP/USD rate from its two sources, so no offer can be priced. Nothing was created; retry in a minute.",
|
|
32
|
+
"V3_NOT_OFFERED": "The payer has not offered MilestoneEscrowV3 terms for this milestone yet — only the payer can (cascrow_v3_offer). The payer has been notified.",
|
|
33
|
+
"OFFER_EXISTS": "An open offer already exists for this milestone and stays valid until it expires. Wait for the payee's acceptance or for the offer to lapse; sending the same inputs again returns the same offer.",
|
|
34
|
+
"V3_OFFER_OPEN": "An open MilestoneEscrowV3 offer, or a signed acceptance that is not funded yet, is bound to this milestone or wallet, so it cannot change now. Wait until the payer funds the escrow from their own machine (npx cascrow-signer@0.1.0 fund --milestone <milestoneId>) or the offer or acceptance lapses.",
|
|
35
|
+
"ATTESTATION_PENDING": "The AI panel has not finished on this claim. Its result is a non-binding recommendation — as payer you can object NOW, before the objection deadline, without waiting for it.",
|
|
36
|
+
"CLAIM_PROOF_UNKNOWN": "This claim's proofHash matches no evidence submitted to cascrow, so there is no AI recommendation for it. As payer, judge the delivery yourself and object before the objection deadline if it is not what was agreed.",
|
|
37
|
+
"V3_ESCROW": "This milestone is held in a MilestoneEscrowV3 escrow: only its payer, payee or arbiter moves that money, on chain, with their own key. Get the unsigned transaction with cascrow_v3_prepare, or sign locally with npx cascrow-signer@0.1.0.",
|
|
38
|
+
"V2_ESCROW_PRESENT": "This milestone or its contract still carries data of the retired v2 (RLUSD) escrow, so it cannot become a MilestoneEscrowV3 escrow. Nothing was changed; contact support.",
|
|
39
|
+
"INVALID_SIGNATURE": "The acceptance signature does not verify for your linked wallet over these exact terms (EIP-712, low-s, from an ordinary account). Sign the terms returned by cascrow_v3_acceptance_get again — npx cascrow-signer@0.1.0 accept does it and checks them first.",
|
|
40
|
+
"V3_INDEX_STALE": "cascrow's index of the chain is behind. Read the escrow from the chain directly (npx cascrow-signer@0.1.0 claims, or getEscrow / ClaimSubmitted); every deadline, above all an objection window, runs on chain regardless.",
|
|
41
|
+
"ACCEPTANCE_LAPSED": "The acceptance expired without funding, so the milestone is closed — this is not a failure of either party. A new attempt is a new milestone.",
|
|
42
|
+
"NOT_COMPLETABLE_WITHOUT_ESCROW": "The work was approved, but this milestone carries an amount without a MilestoneEscrowV3 escrow, and the previous escrow system has been removed — nothing can complete or pay it, so it was left as it was and NO money moved. cascrow has opened a case for it; contact support@cascrow.com with the contract ID. Do not resubmit: your evidence is recorded.",
|
|
43
|
+
"RELEASE_HOLD_REMOVED": "releaseHoldHours was removed with the previous escrow system; send the contract without it. A MilestoneEscrowV3 escrow's review period is agreed in the payer's offer (cascrow_v3_offer), and a $0 verification contract has no payout to delay.",
|
|
44
|
+
"AMOUNT_VIA_MILESTONES": "This draft has milestones, and their amounts are the contract's amount — it cannot be set on the contract. Nothing was changed. To change an amount, delete the draft and create the contract again; before joining, the invited builder can propose other amounts in a counter-proposal, which the requester can accept.",
|
|
45
|
+
"DEADLINE_VIA_MILESTONES": "This draft has milestones, and their deadlines are the ones enforced — a contract-level deadline changes nothing, so it is refused. Nothing was changed. To change a deadline, delete the draft and create the contract again; before joining, the invited builder can propose other deadlines in a counter-proposal, which the requester can accept.",
|
|
46
|
+
"MIXED_AMOUNTS": "A contract is either verification-only (every milestone $0) or escrowed (every milestone above $0), never both. Nothing was created or changed. Set every milestone to $0, or give every milestone an amount and fund each one through a MilestoneEscrowV3 offer (cascrow_v3_offer) once the builder has joined."
|
|
47
|
+
},
|
|
48
|
+
"joined": {
|
|
49
|
+
"active": "Joined as Builder. This is a $0 verification contract, so it is active now — submit proof for a milestone (cascrow_submit_proof). No money is held or moved.",
|
|
50
|
+
"awaitingOffer": "Joined as Builder. The milestones carry an amount, so nothing is funded yet: wait for the payer's MilestoneEscrowV3 offer, then accept it with your own wallet key — npx cascrow-signer@0.1.0 accept --milestone <milestoneId>, or on the contract page. cascrow never signs for you. Submit proof once the payer has funded the milestone.",
|
|
51
|
+
"unknown": "Joined as Builder. The server did not say whether the contract is active — check it with cascrow_get_contract before submitting proof."
|
|
52
|
+
},
|
|
53
|
+
"payeeHints": {
|
|
54
|
+
"CLAIM_PROOF_UNKNOWN": "This manifest is not a sealed submission of this milestone, so cascrow will not confirm it and nothing was signed. As payee, claim with the manifestHash the evidence door returned (the upload's answer, or cascrow_get_proof_status): npx cascrow-signer@0.1.0 submit-claim --milestone <milestoneId> --proof-hash <manifestHash>."
|
|
55
|
+
},
|
|
56
|
+
"fundRemoved": "cascrow_fund_milestone was removed (and with it fundMilestone in cascrow-sdk and `cascrow fund`). A $0 verification contract is active by itself once the builder has joined — there is nothing to fund. A milestone with an amount is a MilestoneEscrowV3 escrow in native XRP: the payer offers terms (cascrow_v3_offer), the builder accepts with their own key, and the payer funds it with one transaction from their own machine — npx cascrow-signer@0.1.0 fund --milestone <milestoneId>. cascrow never signs or sends for you.",
|
|
57
|
+
"recommendation": "🧭 AI RECOMMENDATION {verdict} for this MilestoneEscrowV3 submission ({confidence}% confidence on this file) — this verdict moves NO money. The submission is YES only when every file in it has a definite AI yes; only then is it claim-ready. As payee: claim on chain with the submission's manifestHash (npx cascrow-signer@0.1.0 submit-claim --milestone <milestoneId> --proof-hash <manifestHash>). As payer: release, or object before the objection deadline.",
|
|
58
|
+
"reportTxOnlyHash": "cascrow_v3_report_tx takes a transaction HASH (0x + 64 hex characters) and nothing else. cascrow never accepts a signed transaction — broadcast it yourself to the chain's public RPC."
|
|
59
|
+
};
|
|
60
|
+
// END v3-client-text
|
|
61
|
+
const TX_HASH_RE = /^0x[0-9a-fA-F]{64}$/;
|
|
62
|
+
function v3HintFor(code, body) {
|
|
63
|
+
if (!code) return null;
|
|
64
|
+
// Role-aware (post-review H9): CLAIM_PROOF_UNKNOWN without objectionDeadline comes from
|
|
65
|
+
// prepare?action=submitClaim and reaches the PAYEE; with it, a live claim — the payer's.
|
|
66
|
+
const b = body && typeof body === "object" ? body : null;
|
|
67
|
+
const liveClaim = !!b && typeof b.objectionDeadline === "string";
|
|
68
|
+
if (b && !liveClaim && V3_CLIENT_TEXT.payeeHints[code]) return V3_CLIENT_TEXT.payeeHints[code];
|
|
69
|
+
return V3_CLIENT_TEXT.codeHints[code] || null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** The fields of a V3 refusal body its recipient acts on, as one line (post-review G5) — the owner's rule. */
|
|
73
|
+
function v3RefusalDetail(body) {
|
|
74
|
+
if (!body || typeof body !== "object") return "";
|
|
75
|
+
const out = [];
|
|
76
|
+
if (typeof body.remedy === "string" && body.remedy) out.push(body.remedy);
|
|
77
|
+
if (Array.isArray(body.allowed)) out.push(`Allowed now: ${body.allowed.length ? body.allowed.map(String).join(", ") : "nothing"}.`);
|
|
78
|
+
if (typeof body.objectionDeadline === "string" && body.objectionDeadline) out.push(`Objection deadline: ${body.objectionDeadline}.`);
|
|
79
|
+
if (typeof body.offerExpiresAt === "string" && body.offerExpiresAt) out.push(`Open offer expires: ${body.offerExpiresAt}.`);
|
|
80
|
+
return out.join(" ");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The line for a `POST /api/contracts/join` answer — same rule as `joinedLine` in
|
|
85
|
+
* src/lib/v3-client-text.ts (v3-client-text-parity.test.ts runs this copy against it).
|
|
86
|
+
*/
|
|
87
|
+
function joinedLine(body) {
|
|
88
|
+
const activated = body && typeof body === "object" ? body.activated : undefined;
|
|
89
|
+
if (activated === true) return V3_CLIENT_TEXT.joined.active;
|
|
90
|
+
if (activated === false) return V3_CLIENT_TEXT.joined.awaitingOffer;
|
|
91
|
+
return V3_CLIENT_TEXT.joined.unknown;
|
|
92
|
+
}
|
|
93
|
+
|
|
13
94
|
// ─── HTTP helpers ─────────────────────────────────────────────────────────────
|
|
14
95
|
// Authorization header is omitted entirely when key is empty, rather than
|
|
15
96
|
// sending a garbage "Bearer " token — matters for `verify-work`, the one
|
|
@@ -19,20 +100,91 @@ function authHeader(key) {
|
|
|
19
100
|
return key ? { "Authorization": `Bearer ${key}` } : {};
|
|
20
101
|
}
|
|
21
102
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
103
|
+
// Client deadlines (T11-A4). This package cannot import src/, so the one with an
|
|
104
|
+
// owner is MIRRORED and checked against it by client-deadline-mirrors.test.ts:
|
|
105
|
+
// UPLOAD_CLIENT_ABORT_MS mirrors UPLOAD_CLIENT_ABORT_MS in src/lib/latency-budget.ts
|
|
106
|
+
// mcp/submit is 235 s like the stream (one step below its 240 s cap); everything
|
|
107
|
+
// else gets the longest maxDuration any route has. None of these calls had a
|
|
108
|
+
// deadline before.
|
|
109
|
+
const UPLOAD_CLIENT_ABORT_MS = 120_000;
|
|
110
|
+
const MCP_SUBMIT_CLIENT_ABORT_MS = 235_000;
|
|
111
|
+
const REQUEST_CLIENT_ABORT_MS = 300_000;
|
|
112
|
+
|
|
113
|
+
/** A response body parsed without trusting it to be JSON: an HTML 502 reads "HTTP 502 (non-JSON response from server)". */
|
|
114
|
+
async function readJsonSafe(res) {
|
|
115
|
+
const text = await res.text();
|
|
116
|
+
if (!text) return {};
|
|
117
|
+
try {
|
|
118
|
+
return JSON.parse(text);
|
|
119
|
+
} catch {
|
|
120
|
+
return { error: `HTTP ${res.status} (non-JSON response from server)`, code: "UPSTREAM_NON_JSON" };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Seconds the server asked us to wait: Retry-After, else `retry_after_seconds`. */
|
|
125
|
+
function retryAfterOf(res, data) {
|
|
126
|
+
const header = res.headers && typeof res.headers.get === "function" ? res.headers.get("retry-after") : null;
|
|
127
|
+
if (header !== null && /^\d+$/.test(header.trim())) return Number(header.trim());
|
|
128
|
+
return typeof data?.retry_after_seconds === "number" ? data.retry_after_seconds : null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* The one way this CLI calls the API. Throws an Error carrying `status`, `code` and
|
|
133
|
+
* `retryAfterSeconds`; its message ends with the code in brackets, so a script that
|
|
134
|
+
* greps stderr can branch on it. The deadline covers the body read.
|
|
135
|
+
*/
|
|
136
|
+
async function fetchJson(method, path, { json, form, key = API_KEY, deadlineMs = REQUEST_CLIENT_ABORT_MS } = {}) {
|
|
137
|
+
const controller = new AbortController();
|
|
138
|
+
const timer = setTimeout(() => controller.abort(), deadlineMs);
|
|
139
|
+
let res;
|
|
140
|
+
let data;
|
|
141
|
+
try {
|
|
142
|
+
res = await fetch(`${BASE_URL}${path}`, {
|
|
143
|
+
method,
|
|
144
|
+
headers: { ...authHeader(key), ...(json !== undefined ? { "Content-Type": "application/json" } : {}) },
|
|
145
|
+
...(json !== undefined ? { body: JSON.stringify(json) } : form !== undefined ? { body: form } : {}),
|
|
146
|
+
signal: controller.signal,
|
|
147
|
+
});
|
|
148
|
+
data = await readJsonSafe(res);
|
|
149
|
+
} catch (e) {
|
|
150
|
+
if (e && e.name === "AbortError") {
|
|
151
|
+
throw Object.assign(
|
|
152
|
+
new Error(`No answer from ${path} within ${Math.round(deadlineMs / 1000)} s. The request may still complete on the server — check its result before repeating it. [CLIENT_TIMEOUT]`),
|
|
153
|
+
{ status: 0, code: "CLIENT_TIMEOUT", retryAfterSeconds: null }
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
throw e;
|
|
157
|
+
} finally {
|
|
158
|
+
clearTimeout(timer);
|
|
159
|
+
}
|
|
160
|
+
if (!res.ok || data.code === "UPSTREAM_NON_JSON") {
|
|
161
|
+
const code = typeof data.code === "string" ? data.code : null;
|
|
162
|
+
const retryAfterSeconds = retryAfterOf(res, data);
|
|
163
|
+
const base = typeof data.error === "string" && data.error ? data.error : `HTTP ${res.status}`;
|
|
164
|
+
// A platform error page on a POST (typically the 504 when a route hits its own
|
|
165
|
+
// maxDuration — contracts/review settles on chain inside its own) does not mean the
|
|
166
|
+
// call failed: the function may have moved money before it was killed. Say so, and
|
|
167
|
+
// how to check.
|
|
168
|
+
const mayHaveCompleted = method !== "GET" && res.status >= 500 && data.code === "UPSTREAM_NON_JSON";
|
|
169
|
+
throw Object.assign(
|
|
170
|
+
new Error(
|
|
171
|
+
`${base}${retryAfterSeconds !== null && (res.status === 429 || res.status === 503) ? ` — retry after ${retryAfterSeconds} s` : ""}${code ? ` [${code}]` : ""}` +
|
|
172
|
+
(mayHaveCompleted ? ". The request may still have completed on the server — check the contract (cascrow get --contract <id>) before repeating it." : "") +
|
|
173
|
+
// A MilestoneEscrowV3 refusal says who can act and how (F-72): the body's actionable
|
|
174
|
+
// fields (G5), then the owner's words, role-aware on the body (H9).
|
|
175
|
+
(v3RefusalDetail(data) ? ` — ${v3RefusalDetail(data)}` : "") +
|
|
176
|
+
(v3HintFor(code, data) ? `\n\n${v3HintFor(code, data)}` : "")
|
|
177
|
+
),
|
|
178
|
+
{ status: res.status, code, retryAfterSeconds }
|
|
179
|
+
);
|
|
180
|
+
}
|
|
33
181
|
return data;
|
|
34
182
|
}
|
|
35
183
|
|
|
184
|
+
async function apiPost(path, body, key = API_KEY, deadlineMs = REQUEST_CLIENT_ABORT_MS) {
|
|
185
|
+
return fetchJson("POST", path, { json: body, key, deadlineMs });
|
|
186
|
+
}
|
|
187
|
+
|
|
36
188
|
// SEC-078. A failed standalone verification still has a row, and the route answers
|
|
37
189
|
// the 500 with `id`, `publicHash` and `reportUrl` so the caller who paid for it can name it. The
|
|
38
190
|
// CLI only prints a string, so the reference goes into the string. Both request
|
|
@@ -46,12 +198,7 @@ function standaloneFailure(status, data) {
|
|
|
46
198
|
}
|
|
47
199
|
|
|
48
200
|
async function apiGet(path, key = API_KEY) {
|
|
49
|
-
|
|
50
|
-
headers: { ...authHeader(key) },
|
|
51
|
-
});
|
|
52
|
-
const data = await res.json();
|
|
53
|
-
if (!res.ok) throw new Error(data.error ?? `HTTP ${res.status}`);
|
|
54
|
-
return data;
|
|
201
|
+
return fetchJson("GET", path, { key });
|
|
55
202
|
}
|
|
56
203
|
|
|
57
204
|
async function uploadProof(milestoneId, filename, content, key = API_KEY) {
|
|
@@ -59,14 +206,7 @@ async function uploadProof(milestoneId, filename, content, key = API_KEY) {
|
|
|
59
206
|
const form = new FormData();
|
|
60
207
|
form.append("file", blob, filename);
|
|
61
208
|
form.append("milestoneId", milestoneId);
|
|
62
|
-
|
|
63
|
-
method: "POST",
|
|
64
|
-
headers: { ...authHeader(key) },
|
|
65
|
-
body: form,
|
|
66
|
-
});
|
|
67
|
-
const data = await res.json();
|
|
68
|
-
if (!res.ok) throw new Error(data.error ?? `HTTP ${res.status}`);
|
|
69
|
-
return data;
|
|
209
|
+
return fetchJson("POST", "/api/proof/upload", { form, key, deadlineMs: UPLOAD_CLIENT_ABORT_MS });
|
|
70
210
|
}
|
|
71
211
|
|
|
72
212
|
// Stream verify via SSE — used by the legacy two-step `submit` + `verify --proof <id>` flow
|
|
@@ -112,8 +252,21 @@ async function streamVerify(proofId, key = API_KEY) {
|
|
|
112
252
|
// long-lived MCP server that holds the event loop and later aborts a fetch
|
|
113
253
|
// that finished minutes ago.
|
|
114
254
|
clearTimeout(timeout);
|
|
115
|
-
const data = await res
|
|
116
|
-
|
|
255
|
+
const data = await readJsonSafe(res);
|
|
256
|
+
// T01-8. Upload starts verification by itself, so losing the claim to a RUNNING
|
|
257
|
+
// run is the normal case: wait for that run through proof-status instead of
|
|
258
|
+
// failing. This 409's Retry-After is the ~1 h takeover cutoff, never a wait.
|
|
259
|
+
if (res.status === 409 && data.code === "VERIFICATION_IN_PROGRESS" && data.phase === "running") {
|
|
260
|
+
process.stderr.write(" A verification of this proof is already running — waiting for it instead of starting another.\n");
|
|
261
|
+
const budgetMs = Math.max(30_000, Math.min(235_000, ((data.expected_done_in_seconds ?? 200) + 30) * 1000));
|
|
262
|
+
return { attached: true, status: await waitForProof(proofId, budgetMs, key) };
|
|
263
|
+
}
|
|
264
|
+
const code = typeof data.code === "string" ? data.code : null;
|
|
265
|
+
throw Object.assign(new Error(`${data.error ?? `HTTP ${res.status}`}${code ? ` [${code}]` : ""}`), {
|
|
266
|
+
status: res.status,
|
|
267
|
+
code,
|
|
268
|
+
retryAfterSeconds: retryAfterOf(res, data),
|
|
269
|
+
});
|
|
117
270
|
}
|
|
118
271
|
if (!res.body) {
|
|
119
272
|
clearTimeout(timeout);
|
|
@@ -178,6 +331,90 @@ async function streamVerify(proofId, key = API_KEY) {
|
|
|
178
331
|
throw new Error("Stream ended without a complete event");
|
|
179
332
|
}
|
|
180
333
|
|
|
334
|
+
/**
|
|
335
|
+
* Poll proof-status until the proof settles, the run is found dead, or `maxMs` runs
|
|
336
|
+
* out. Returns the last status read, with `stoppedBecause` set when it stopped early:
|
|
337
|
+
* - 401 / 403: waiting does not change the key or the account;
|
|
338
|
+
* - 404: permanent — upload answers only after the proof exists and proof-status
|
|
339
|
+
* reads the primary database, so the proof was deleted or erased;
|
|
340
|
+
* - "interrupted": the run that held the proof died (claimPhase "orphaned").
|
|
341
|
+
* 429 / 503 wait at least Retry-After, never past the budget; anything else is
|
|
342
|
+
* transient. A milestone that is closed, or parked for a human, also settles it.
|
|
343
|
+
*/
|
|
344
|
+
async function waitForProof(proofId, maxMs = 100_000, key = API_KEY, pollMs = 5_000) {
|
|
345
|
+
const started = Date.now();
|
|
346
|
+
let waitMs = pollMs;
|
|
347
|
+
let last = null;
|
|
348
|
+
while (Date.now() - started < maxMs) {
|
|
349
|
+
await new Promise((r) => setTimeout(r, waitMs));
|
|
350
|
+
waitMs = pollMs;
|
|
351
|
+
try {
|
|
352
|
+
last = await apiGet(`/api/agent/proof-status/${encodeURIComponent(proofId)}`, key);
|
|
353
|
+
} catch (e) {
|
|
354
|
+
if (e && (e.status === 401 || e.status === 403 || e.status === 404)) {
|
|
355
|
+
return { pending: true, proofId, stoppedBecause: e.status, error: e.message };
|
|
356
|
+
}
|
|
357
|
+
if (e && (e.status === 429 || e.status === 503) && typeof e.retryAfterSeconds === "number") {
|
|
358
|
+
waitMs = Math.min(Math.max(pollMs, e.retryAfterSeconds * 1000), Math.max(0, maxMs - (Date.now() - started)));
|
|
359
|
+
}
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
// `notCompletableWithoutEscrow`: no verdict can complete this milestone — final.
|
|
363
|
+
if (!last.pending || last.milestoneClosed === true || last.notCompletableWithoutEscrow === true) return { ...last, proofId };
|
|
364
|
+
// Parked for a human: no poll will change it (same rule as both MCP servers).
|
|
365
|
+
// Never a MilestoneEscrowV3 proof (post-review L2): no human review decides one.
|
|
366
|
+
if (last.escrowVersion !== 3 && last.running !== true && last.milestoneStatus === "PENDING_REVIEW") return { ...last, proofId, awaitingHumanReview: true };
|
|
367
|
+
if (last.claimPhase === "orphaned") return { ...last, proofId, stoppedBecause: "interrupted" };
|
|
368
|
+
}
|
|
369
|
+
return { ...(last ?? {}), pending: true, proofId, timedOut: true };
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/** One line for a proof-status result, saying whether funds moved. */
|
|
373
|
+
function statusSummary(s) {
|
|
374
|
+
if (s.stoppedBecause === 404) return `⛔ Proof ${s.proofId} no longer exists (deleted or erased). Nothing will verify it.`;
|
|
375
|
+
if (s.stoppedBecause === 401 || s.stoppedBecause === 403) return `⛔ Stopped: ${s.error}. Waiting does not change this — check CASCROW_API_KEY and that this account is a party to the contract.`;
|
|
376
|
+
if (s.stoppedBecause === "interrupted") {
|
|
377
|
+
// Not "re-run automatically": a claim with no timestamp is never taken over, and
|
|
378
|
+
// the retry cron skips demo contracts and milestones past PROOF_SUBMITTED.
|
|
379
|
+
return `⚠️ The last verification run was interrupted; nothing is running now. Once its claim is an hour old, \`cascrow verify --proof ${s.proofId}\` can start it again — or submit the proof again, which always works.`;
|
|
380
|
+
}
|
|
381
|
+
if (s.awaitingHumanReview) return "⏳ PENDING_REVIEW — the panel reached no verdict; a human decides. Waiting will not change it.";
|
|
382
|
+
if (s.timedOut) return `⏳ Still verifying. Check again: cascrow status --proof ${s.proofId} --wait`;
|
|
383
|
+
const conf = typeof s.confidence === "number" ? ` (${s.confidence}% confidence)` : "";
|
|
384
|
+
// MilestoneEscrowV3 (post-review G4): the verdict is a RECOMMENDATION and moves no money.
|
|
385
|
+
// Asked before every v2 arm — the claim-relevant answer is the SUBMISSION's verdict
|
|
386
|
+
// (`submissionVerdict`, `claimReady`), never this proof's `decision`, and a closed or
|
|
387
|
+
// COMPLETED V3 milestone was settled on chain by the parties.
|
|
388
|
+
if (s.escrowVersion === 3 && !(s.milestoneStatus === "COMPLETED" || s.milestoneClosed === true)) {
|
|
389
|
+
const line = V3_CLIENT_TEXT.recommendation.replace("{verdict}", s.submissionVerdict ?? "PENDING").replace("{confidence}", String(s.confidence ?? 0));
|
|
390
|
+
return s.manifestHash ? `${line} manifestHash: ${s.manifestHash}` : line;
|
|
391
|
+
}
|
|
392
|
+
if (s.escrowVersion === 3) {
|
|
393
|
+
return s.milestoneStatus === "COMPLETED"
|
|
394
|
+
? "✅ COMPLETED — the MilestoneEscrowV3 escrow paid out on chain."
|
|
395
|
+
: `⛔ CLOSED — the milestone is ${s.terminalStatus ?? "closed"}; the MilestoneEscrowV3 escrow settled on chain. Do not resubmit to it.`;
|
|
396
|
+
}
|
|
397
|
+
// COMPLETED first: proof-status reports a paid milestone as `milestoneClosed` too
|
|
398
|
+
// (terminalStatus COMPLETED), and the CLOSED arm below would read a release as
|
|
399
|
+
// "moves no funds" — SEC-059's inversion (same order as proofOutcomeLine in both
|
|
400
|
+
// MCP servers). Whether it was THIS proof's YES is `decision`.
|
|
401
|
+
if (s.milestoneStatus === "COMPLETED" || s.terminalStatus === "COMPLETED") {
|
|
402
|
+
return s.decision === "YES" ? `✅ COMPLETED${conf} — no money was held (verification-only contract).` : `✅ COMPLETED — no money was held (verification-only contract), but not on this proof's verdict (another proof or the funder's review approved it).`;
|
|
403
|
+
}
|
|
404
|
+
if (s.milestoneClosed === true) {
|
|
405
|
+
return s.terminalReason === "RELEASED_ON_CHAIN"
|
|
406
|
+
? "⛔ CLOSED (EXPIRED) — the chain shows the builder WAS paid, just not by this verification."
|
|
407
|
+
: `⛔ CLOSED — the milestone is ${s.terminalStatus ?? "closed"}; this verification moved no funds. Do not resubmit to it.`;
|
|
408
|
+
}
|
|
409
|
+
// An approval cannot complete this milestone (an amount without a MilestoneEscrowV3
|
|
410
|
+
// escrow); the verdict doors answered 409 NOT_COMPLETABLE_WITHOUT_ESCROW.
|
|
411
|
+
if (s.notCompletableWithoutEscrow === true) return `⛔ NOT COMPLETABLE — ${V3_CLIENT_TEXT.codeHints.NOT_COMPLETABLE_WITHOUT_ESCROW}`;
|
|
412
|
+
if (s.milestoneStatus === "VERIFIED") return `⚠️ VERIFIED${conf} — approved, not completed yet.`;
|
|
413
|
+
if (s.milestoneStatus === "PENDING_REVIEW") return `⏳ PENDING_REVIEW${conf} — manual review required.`;
|
|
414
|
+
if (s.decision === "NO") return `❌ REJECTED${conf}.`;
|
|
415
|
+
return `ℹ️ Decision: ${s.decision ?? "none"}${conf}, milestone ${s.milestoneStatus ?? "unknown"}.`;
|
|
416
|
+
}
|
|
417
|
+
|
|
181
418
|
/**
|
|
182
419
|
* One human-readable line for a verification `complete` frame.
|
|
183
420
|
*
|
|
@@ -188,22 +425,34 @@ async function streamVerify(proofId, key = API_KEY) {
|
|
|
188
425
|
*/
|
|
189
426
|
function outcomeLine(r) {
|
|
190
427
|
const c = r.confidence ?? 0;
|
|
428
|
+
// MilestoneEscrowV3: a recommendation that moves nothing — asked first, so no arm
|
|
429
|
+
// below can read it as a payout or a rejection.
|
|
430
|
+
if (r.action === "V3_RECOMMENDATION") {
|
|
431
|
+
// The SUBMISSION's verdict, never the panel's `decision` on one file.
|
|
432
|
+
return V3_CLIENT_TEXT.recommendation.replace("{verdict}", r.submissionVerdict ?? "PENDING").replace("{confidence}", String(c));
|
|
433
|
+
}
|
|
191
434
|
if (r.action === "SUPERSEDED") {
|
|
192
|
-
|
|
435
|
+
// `settledAs`: the milestone was closed (e.g. EXPIRED) before this verification could
|
|
436
|
+
// settle it, not beaten by a proof.
|
|
437
|
+
return r.settledAs
|
|
438
|
+
? `ℹ️ SUPERSEDED — this milestone was closed (${r.settledAs}) before this verification could settle it, so it changed nothing and moved no funds. Do not resubmit to it.`
|
|
439
|
+
: "ℹ️ SUPERSEDED — another proof on this milestone was decided first, so this verification changed nothing. Your evidence was recorded.";
|
|
193
440
|
}
|
|
194
441
|
if (r.action === "COMPLETED") {
|
|
195
442
|
return r.alreadyReleased === true
|
|
196
|
-
?
|
|
197
|
-
: `✅ COMPLETED (${c}% confidence) —
|
|
443
|
+
? `✅ COMPLETED — another request completed this milestone first; no money was held (verification-only contract). Do not resubmit.`
|
|
444
|
+
: `✅ COMPLETED (${c}% confidence) — no money was held (verification-only contract)${r.txHash ? `, tx ${r.txHash}` : ""}.`;
|
|
198
445
|
}
|
|
199
446
|
if (r.complianceBlocked === true) {
|
|
200
447
|
return `⏸️ VERIFIED (${c}%) — payout on hold pending compliance review. No funds moved.`;
|
|
201
448
|
}
|
|
202
|
-
if (r.
|
|
203
|
-
return
|
|
449
|
+
if (r.action === "NOT_COMPLETABLE_WITHOUT_ESCROW") {
|
|
450
|
+
return `⛔ NOT COMPLETED (${c}% confidence) — ${V3_CLIENT_TEXT.codeHints.NOT_COMPLETABLE_WITHOUT_ESCROW}`;
|
|
204
451
|
}
|
|
205
452
|
if (r.action === "VERIFIED") {
|
|
206
|
-
|
|
453
|
+
// Approved, but no completion followed; the producer does not reach this since
|
|
454
|
+
// Phase 6. No retry is promised — there is nothing to retry.
|
|
455
|
+
return `⚠️ VERIFIED (${c}% confidence) — the work was approved, but the milestone was NOT completed. No money moved. Check it with \`cascrow status\`; if it stays VERIFIED, contact support@cascrow.com.`;
|
|
207
456
|
}
|
|
208
457
|
if (r.timedOut === true) {
|
|
209
458
|
return "⏳ PENDING_REVIEW — the verification panel did not answer (too few models responded). This is NOT a rejection and NOT a judgement of the work; a human decides.";
|
|
@@ -285,7 +534,7 @@ function err(msg) {
|
|
|
285
534
|
|
|
286
535
|
function usage() {
|
|
287
536
|
console.log(`
|
|
288
|
-
|
|
537
|
+
cascrow CLI — AI verification, on-chain reputation and non-custodial escrow for agents
|
|
289
538
|
|
|
290
539
|
Usage:
|
|
291
540
|
cascrow <command> [options]
|
|
@@ -295,15 +544,18 @@ Commands:
|
|
|
295
544
|
verify-work Verify AI-generated work — no contract, no escrow, no account needed
|
|
296
545
|
verify Submit proof and get AI verdict in one call (recommended)
|
|
297
546
|
create Create a milestone contract
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
submit Upload proof text (returns proofId for legacy verify flow)
|
|
547
|
+
submit Upload proof text — verification starts by itself (then: status --proof <id> --wait)
|
|
548
|
+
status Show (or --wait for) the verification result of a proof
|
|
301
549
|
get Get contract status and milestones
|
|
302
550
|
join Join a contract as Builder via invite code
|
|
303
551
|
handoff Send a contract invite to a Builder agent by Agent ID
|
|
304
552
|
check-invites Check pending contract invites for this agent
|
|
305
553
|
review-queue As funder: list milestones still waiting for your verdict
|
|
306
554
|
review As funder: approve or reject a milestone under manual review
|
|
555
|
+
v3-claims As payer: list MilestoneEscrowV3 claims you can still object to on chain
|
|
556
|
+
v3 <sub> MilestoneEscrowV3, remote half: offer | acceptance | accept-submit | prepare | report-tx | attestation | claims
|
|
557
|
+
and "v3 sign <cascrow-signer command…>" — runs the pinned local signer (npx cascrow-signer@0.1.0),
|
|
558
|
+
which holds your key on YOUR machine. cascrow never signs, sends or relays for you.
|
|
307
559
|
me Print this agent's ID
|
|
308
560
|
|
|
309
561
|
Options for each command:
|
|
@@ -328,25 +580,25 @@ Options for each command:
|
|
|
328
580
|
--commit <sha> optional Git commit SHA or PR URL
|
|
329
581
|
|
|
330
582
|
create
|
|
331
|
-
--title <title> required milestone title
|
|
583
|
+
--title <title> required short milestone title (printed on the certificate)
|
|
332
584
|
--days <n> optional deadline days from now (default: 7)
|
|
333
|
-
--amount <usd> optional
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
--
|
|
337
|
-
--
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
--amount <usd> IGNORED the escrowed amount is the milestone's own amountUSD
|
|
343
|
-
--milestone <id> optional
|
|
344
|
-
|
|
345
|
-
submit [legacy: use verify instead]
|
|
585
|
+
--amount <usd> optional default 0 = verification-only (active once the builder joins). Above 0 the
|
|
586
|
+
milestone is a MilestoneEscrowV3 escrow in native XRP: offer it with
|
|
587
|
+
"cascrow v3 offer", the builder accepts, you fund with "cascrow v3 sign fund"
|
|
588
|
+
--criteria <text> optional what the AI panel checks the proof against (max 4000 chars)
|
|
589
|
+
--criteria-file <path> optional the same, read from a file
|
|
590
|
+
--checklist-file <path> optional JSON array of { title, severity?, id? } (max 20) scored item by item on text proofs
|
|
591
|
+
--review-mode <mode> optional AUTO | MANUAL | MANUAL_AUTO — what happens when the panel is unsure (60–85%)
|
|
592
|
+
|
|
593
|
+
submit [or use verify for text proof in one call]
|
|
346
594
|
--milestone <id> required
|
|
347
595
|
--proof <text> required
|
|
348
596
|
--file <name> optional filename (default: proof-report.txt)
|
|
349
597
|
|
|
598
|
+
status
|
|
599
|
+
--proof <id> required
|
|
600
|
+
--wait optional wait up to ~2 min for the verdict; stops early when waiting cannot help
|
|
601
|
+
|
|
350
602
|
get
|
|
351
603
|
--contract <id> required
|
|
352
604
|
|
|
@@ -375,15 +627,13 @@ Examples:
|
|
|
375
627
|
# Register a new agent account (no human in the loop)
|
|
376
628
|
cascrow register --email bot@example.com --password secret123
|
|
377
629
|
|
|
378
|
-
# One-shot: create contract
|
|
379
|
-
cascrow create --title "Fix auth bug
|
|
380
|
-
cascrow fund --contract <contractId>
|
|
630
|
+
# One-shot: create a verification-only contract (active once the builder joins), then verify a code fix
|
|
631
|
+
cascrow create --title "Fix auth bug" --criteria "All tests pass; JWT expiry honoured; no new lint errors" --days 7
|
|
381
632
|
cascrow verify --contract <contractId> --proof "Fixed JWT expiry bug in auth.ts, 42 tests green, PR #51 merged" --commit abc1234 --links https://github.com/you/repo/pull/51
|
|
382
633
|
|
|
383
|
-
#
|
|
384
|
-
cascrow
|
|
385
|
-
cascrow
|
|
386
|
-
cascrow verify --contract <contractId> --proof "API live at https://api.example.com, all endpoints responding"
|
|
634
|
+
# Paid work is a MilestoneEscrowV3 escrow (native XRP, testnet): create with --amount,
|
|
635
|
+
# offer it (cascrow v3 offer), the builder accepts, and you fund with one transaction
|
|
636
|
+
# from your own machine (cascrow v3 sign fund). This CLI never takes a key.
|
|
387
637
|
|
|
388
638
|
# Agent-to-agent handoff
|
|
389
639
|
cascrow me # → get your Agent ID
|
|
@@ -395,14 +645,88 @@ Examples:
|
|
|
395
645
|
|
|
396
646
|
# As the funder: decide what the AI panel could not (confidence 60-85)
|
|
397
647
|
# Poll this — a manual_review.required webhook you missed is a decision nobody
|
|
398
|
-
# makes
|
|
648
|
+
# makes; an undecided milestone completes automatically after 48h under review mode
|
|
649
|
+
# MANUAL_AUTO or AUTO, 14 days otherwise.
|
|
650
|
+
# Verification-only contracts: a verdict completes or hands back, and moves no money.
|
|
399
651
|
cascrow review-queue
|
|
400
652
|
cascrow review --contract <contractId> --decision APPROVE
|
|
401
653
|
cascrow review --contract <contractId> --decision REJECT --milestone <milestoneId>
|
|
654
|
+
|
|
655
|
+
# As the payer of a MilestoneEscrowV3 escrow: claims release unless YOUR wallet
|
|
656
|
+
# calls object(escrowId) before the deadline — cascrow cannot object for you.
|
|
657
|
+
cascrow v3-claims
|
|
658
|
+
|
|
659
|
+
# MilestoneEscrowV3 (native XRP). cascrow prepares and reads; you sign locally.
|
|
660
|
+
cascrow v3 offer --milestone <id> --review-period-sec 259200 [--watcher] [--arbiter 0x… --arbitration-period-sec N]
|
|
661
|
+
cascrow v3 acceptance --milestone <id> # as builder: the frozen terms to check
|
|
662
|
+
cascrow v3 sign accept --milestone <id> --network testnet # check + sign locally (pinned cascrow-signer)
|
|
663
|
+
cascrow v3 sign fund --milestone <id> --network testnet # as payer: one transaction, value = amount
|
|
664
|
+
cascrow v3 prepare --milestone <id> --action object --reason-hash 0x… # UNSIGNED — verify before signing
|
|
665
|
+
cascrow v3 report-tx --milestone <id> --tx 0x<64 hex> # a HASH only, never a signed transaction
|
|
666
|
+
cascrow v3 attestation --escrow 0x… --claim-index 1 # non-binding AI recommendation
|
|
402
667
|
`);
|
|
403
668
|
process.exit(0);
|
|
404
669
|
}
|
|
405
670
|
|
|
671
|
+
// ─── The bar a milestone is judged against ────────────────────────────────────
|
|
672
|
+
// COPY of normalizeMilestoneChecklist / normalizeCriteria in src/lib/milestone-bar-input.ts
|
|
673
|
+
// (this package cannot import src/). milestone-bar-input-parity.test.ts runs this copy
|
|
674
|
+
// against the owner over the same case matrix.
|
|
675
|
+
const CHECKLIST_MAX_ITEMS = 20;
|
|
676
|
+
const CHECKLIST_TITLE_MAX = 200;
|
|
677
|
+
const CHECKLIST_ID_MAX = 100;
|
|
678
|
+
const CRITERIA_MAX = 4000;
|
|
679
|
+
const CHECKLIST_SEVERITIES = ["CRITICAL", "HIGH", "MEDIUM", "LOW"];
|
|
680
|
+
|
|
681
|
+
function normalizeMilestoneChecklist(items) {
|
|
682
|
+
if (items === undefined || items === null) return undefined;
|
|
683
|
+
if (!Array.isArray(items)) throw new Error("verificationChecklist: must be an array of { title, severity?, id? }");
|
|
684
|
+
if (items.length === 0) return undefined;
|
|
685
|
+
if (items.length > CHECKLIST_MAX_ITEMS) throw new Error(`verificationChecklist: at most ${CHECKLIST_MAX_ITEMS} items`);
|
|
686
|
+
items.forEach((raw, i) => {
|
|
687
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
688
|
+
throw new Error(`verificationChecklist[${i}]: must be an object with a title`);
|
|
689
|
+
}
|
|
690
|
+
});
|
|
691
|
+
const withId = items.filter((it) => it.id !== undefined).length;
|
|
692
|
+
if (withId !== 0 && withId !== items.length) {
|
|
693
|
+
throw new Error("verificationChecklist: either give every item an id or none (ids are then assigned 1..n)");
|
|
694
|
+
}
|
|
695
|
+
const seen = new Set();
|
|
696
|
+
return items.map((it, i) => {
|
|
697
|
+
const title = typeof it.title === "string" ? it.title.trim() : "";
|
|
698
|
+
if (title.length === 0) throw new Error(`verificationChecklist[${i}]: title is required`);
|
|
699
|
+
if (title.length > CHECKLIST_TITLE_MAX) throw new Error(`verificationChecklist[${i}]: title must be at most ${CHECKLIST_TITLE_MAX} characters`);
|
|
700
|
+
let id = i + 1;
|
|
701
|
+
if (withId) {
|
|
702
|
+
if (typeof it.id !== "number" || !Number.isInteger(it.id) || it.id < 1 || it.id > CHECKLIST_ID_MAX) {
|
|
703
|
+
throw new Error(`verificationChecklist[${i}]: id must be an integer from 1 to ${CHECKLIST_ID_MAX}`);
|
|
704
|
+
}
|
|
705
|
+
if (seen.has(it.id)) throw new Error(`verificationChecklist[${i}]: duplicate id ${it.id}`);
|
|
706
|
+
seen.add(it.id);
|
|
707
|
+
id = it.id;
|
|
708
|
+
}
|
|
709
|
+
const out = { id, title };
|
|
710
|
+
if (it.severity !== undefined && it.severity !== null && it.severity !== "") {
|
|
711
|
+
const sev = typeof it.severity === "string" ? it.severity.trim().toUpperCase() : "";
|
|
712
|
+
if (!CHECKLIST_SEVERITIES.includes(sev)) {
|
|
713
|
+
throw new Error(`verificationChecklist[${i}]: severity must be one of ${CHECKLIST_SEVERITIES.join(", ")}`);
|
|
714
|
+
}
|
|
715
|
+
out.severity = sev;
|
|
716
|
+
}
|
|
717
|
+
return out;
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
function normalizeCriteria(value) {
|
|
722
|
+
if (value === undefined || value === null) return undefined;
|
|
723
|
+
if (typeof value !== "string") throw new Error("verificationCriteria: must be a string");
|
|
724
|
+
const v = value.trim();
|
|
725
|
+
if (v.length === 0) return undefined;
|
|
726
|
+
if (v.length > CRITERIA_MAX) throw new Error(`verificationCriteria: at most ${CRITERIA_MAX} characters (got ${v.length})`);
|
|
727
|
+
return v;
|
|
728
|
+
}
|
|
729
|
+
|
|
406
730
|
// ─── Argument parser ──────────────────────────────────────────────────────────
|
|
407
731
|
|
|
408
732
|
function parseArgs(argv) {
|
|
@@ -439,8 +763,8 @@ async function cmdRegister(args) {
|
|
|
439
763
|
keyName: args["key-name"] ?? "cli",
|
|
440
764
|
}),
|
|
441
765
|
});
|
|
442
|
-
const data = await res
|
|
443
|
-
if (!res.ok) err(data.error ?? `HTTP ${res.status}`);
|
|
766
|
+
const data = await readJsonSafe(res);
|
|
767
|
+
if (!res.ok || data.code === "UPSTREAM_NON_JSON") err(`${data.error ?? `HTTP ${res.status}`}${data.code ? ` [${data.code}]` : ""}`);
|
|
444
768
|
print(data);
|
|
445
769
|
console.error("\nSave your API key and set it:\n export CASCROW_API_KEY=" + data.apiKey + "\n");
|
|
446
770
|
}
|
|
@@ -484,7 +808,7 @@ async function cmdVerifyWork(args) {
|
|
|
484
808
|
// The body is read INSIDE the deadline. A response whose headers arrived
|
|
485
809
|
// and whose body then stalls is otherwise unbounded — a deadline that ends
|
|
486
810
|
// at the headers bounds time-to-first-byte and nothing else (SEC-073).
|
|
487
|
-
return { res: r, data: await r
|
|
811
|
+
return { res: r, data: await readJsonSafe(r) };
|
|
488
812
|
});
|
|
489
813
|
if (!res.ok) throw standaloneFailure(res.status, body);
|
|
490
814
|
// Assigned to the OUTER `data`, not shadowing it: `const { data }` inside the
|
|
@@ -506,7 +830,7 @@ async function cmdVerifyWork(args) {
|
|
|
506
830
|
// The body is read INSIDE the deadline. A response whose headers arrived
|
|
507
831
|
// and whose body then stalls is otherwise unbounded — a deadline that ends
|
|
508
832
|
// at the headers bounds time-to-first-byte and nothing else (SEC-073).
|
|
509
|
-
return { res: r, data: await r
|
|
833
|
+
return { res: r, data: await readJsonSafe(r) };
|
|
510
834
|
});
|
|
511
835
|
if (!res.ok) throw standaloneFailure(res.status, answer);
|
|
512
836
|
// Assigned to the OUTER `data`, not shadowing it: `const { data }` inside the
|
|
@@ -541,7 +865,8 @@ async function cmdVerifyWork(args) {
|
|
|
541
865
|
}
|
|
542
866
|
|
|
543
867
|
// One-shot verify: submit text proof + get verdict in a single call via /api/mcp/submit.
|
|
544
|
-
//
|
|
868
|
+
// A verification-only milestone completes (NFT minted) on approval; a MilestoneEscrowV3
|
|
869
|
+
// verdict is a recommendation that moves no money. No separate `verify --proof <id>` needed.
|
|
545
870
|
async function cmdVerify(args) {
|
|
546
871
|
if (!API_KEY) err("CASCROW_API_KEY is not set");
|
|
547
872
|
if (!args.proof) err("--proof is required");
|
|
@@ -554,6 +879,13 @@ async function cmdVerify(args) {
|
|
|
554
879
|
if (!args.contract && args.proof && !args.proof.includes(" ")) {
|
|
555
880
|
process.stderr.write("\nRunning AI verification (5 models)...\n\n");
|
|
556
881
|
const result = await streamVerify(args.proof);
|
|
882
|
+
// The run was already going (usually the one upload started): print its outcome
|
|
883
|
+
// as proof-status reports it — the stream's frame never arrived here.
|
|
884
|
+
if (result && result.attached) {
|
|
885
|
+
print(result.status);
|
|
886
|
+
process.stderr.write(statusSummary(result.status) + "\n");
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
557
889
|
// SEC-055/058/059 at the third published surface. `passed` used to be
|
|
558
890
|
// `decision === "YES" && confidence >= 85`, i.e. a re-implementation of the
|
|
559
891
|
// three-tier rule from the PROOF's verdict — which answers a different question
|
|
@@ -573,10 +905,18 @@ async function cmdVerify(args) {
|
|
|
573
905
|
released,
|
|
574
906
|
alreadyReleased: result.alreadyReleased === true,
|
|
575
907
|
superseded: result.action === "SUPERSEDED",
|
|
908
|
+
// With SUPERSEDED: the TERMINAL status the row was already in (EXPIRED, COMPLETED),
|
|
909
|
+
// or null when another proof won it. It used to reach only the stderr line from
|
|
910
|
+
// `outcomeLine`, so a machine caller reading this JSON could not tell "closed —
|
|
911
|
+
// nothing moved, do not resubmit" from "another proof won" (round-3 pass 2).
|
|
912
|
+
settledAs: typeof result.settledAs === "string" ? result.settledAs : null,
|
|
576
913
|
complianceBlocked: result.complianceBlocked === true,
|
|
577
|
-
|
|
914
|
+
notCompletableWithoutEscrow: result.action === "NOT_COMPLETABLE_WITHOUT_ESCROW",
|
|
578
915
|
panelUnavailable: result.timedOut === true,
|
|
579
916
|
txHash: result.txHash ?? null,
|
|
917
|
+
// MilestoneEscrowV3 (post-review G4): the recommendation's own fields, so a machine
|
|
918
|
+
// caller reads the SUBMISSION's verdict and the hash to claim with.
|
|
919
|
+
...(result.action === "V3_RECOMMENDATION" || result.escrowVersion === 3 ? v3Fields(result) : {}),
|
|
580
920
|
reasoning: result.reasoning,
|
|
581
921
|
modelVotes: (result.votes ?? []).map(v => ({ model: v.model, decision: v.decision, confidence: v.confidence })),
|
|
582
922
|
});
|
|
@@ -601,20 +941,23 @@ async function cmdVerify(args) {
|
|
|
601
941
|
contract_id: args.contract,
|
|
602
942
|
milestone_id: args.milestone ?? undefined,
|
|
603
943
|
evidence,
|
|
604
|
-
});
|
|
944
|
+
}, API_KEY, MCP_SUBMIT_CLIENT_ABORT_MS);
|
|
605
945
|
|
|
606
946
|
// `decided: "elsewhere"` and `panelUnavailable` are 200 responses that carry NO
|
|
607
947
|
// `verdict` key, so `data.verdict ?? "unknown"` fell to the final arm below and told
|
|
608
948
|
// the builder "❌ REJECTED — resubmit with stronger proof" for a milestone that may
|
|
609
949
|
// well have been approved and paid (SEC-055). Classified before the verdict is read.
|
|
610
|
-
const
|
|
950
|
+
const isV3 = data.action === "V3_RECOMMENDATION";
|
|
951
|
+
const verdict = isV3
|
|
952
|
+
? "v3_recommendation"
|
|
953
|
+
: data.decided === "elsewhere"
|
|
611
954
|
? "decided_elsewhere"
|
|
612
955
|
: data.panelUnavailable
|
|
613
956
|
? "panel_unavailable"
|
|
614
957
|
: (data.verdict ?? "unknown");
|
|
615
958
|
const confidence = data.confidence ?? 0;
|
|
616
|
-
//
|
|
617
|
-
|
|
959
|
+
// An approved milestone that cannot complete is not a 200: the route answers 409
|
|
960
|
+
// NOT_COMPLETABLE_WITHOUT_ESCROW, which `apiPost` turns into an error with its hint.
|
|
618
961
|
|
|
619
962
|
// Print model votes if available
|
|
620
963
|
const votes = Array.isArray(data.model_votes) ? data.model_votes : [];
|
|
@@ -624,8 +967,22 @@ async function cmdVerify(args) {
|
|
|
624
967
|
}
|
|
625
968
|
process.stderr.write("\n");
|
|
626
969
|
|
|
970
|
+
// With `decided: "elsewhere"`: the TERMINAL status the milestone was already in. EXPIRED
|
|
971
|
+
// is a funder cancel or the deadline, not "the funder's review, or a concurrent
|
|
972
|
+
// submission", and resubmitting to it is pointless.
|
|
973
|
+
const settledAs = verdict === "decided_elsewhere" && typeof data.settledAs === "string" ? data.settledAs : null;
|
|
974
|
+
|
|
627
975
|
let statusLine;
|
|
628
|
-
if (
|
|
976
|
+
if (isV3) {
|
|
977
|
+
// A MilestoneEscrowV3 submission: sealed, recommended on, nothing moved (G4).
|
|
978
|
+
statusLine = V3_CLIENT_TEXT.recommendation.replace("{verdict}", data.submissionVerdict ?? "PENDING").replace("{confidence}", String(confidence));
|
|
979
|
+
if (data.manifestHash) statusLine += `\n manifestHash: ${data.manifestHash}`;
|
|
980
|
+
if (typeof data.next === "string") statusLine += `\n ${data.next}`;
|
|
981
|
+
} else if (settledAs) {
|
|
982
|
+
statusLine =
|
|
983
|
+
`ℹ️ This milestone was closed (${settledAs}) before this submission could settle it, so this submission ` +
|
|
984
|
+
"changed nothing and moved no funds itself. Your evidence was recorded. Do not resubmit to this milestone";
|
|
985
|
+
} else if (verdict === "decided_elsewhere") {
|
|
629
986
|
statusLine =
|
|
630
987
|
"ℹ️ This milestone was decided by another request while your proof was being verified — " +
|
|
631
988
|
"by the funder's review, or by a concurrent submission. Your evidence was recorded. " +
|
|
@@ -635,10 +992,6 @@ async function cmdVerify(args) {
|
|
|
635
992
|
"⏳ PENDING_REVIEW — the verification panel could not reach a verdict (too few models " +
|
|
636
993
|
"answered). This is NOT a rejection and NOT a judgement of your work; a human decides, " +
|
|
637
994
|
"and resubmitting the same evidence will not help while the panel is down";
|
|
638
|
-
} else if (verdict === "approved" && releaseHoldUntil) {
|
|
639
|
-
statusLine =
|
|
640
|
-
`⏸️ APPROVED (${confidence}% confidence) — payout scheduled for ${releaseHoldUntil}. ` +
|
|
641
|
-
"NO FUNDS HAVE MOVED YET: the funder can contest until then, and the date can move";
|
|
642
995
|
} else if (verdict === "approved") {
|
|
643
996
|
statusLine = `✅ VERIFIED (${confidence}% confidence)`;
|
|
644
997
|
if (data.on_chain_url) statusLine += `\n Proof: ${data.on_chain_url}`;
|
|
@@ -659,17 +1012,28 @@ async function cmdVerify(args) {
|
|
|
659
1012
|
print({
|
|
660
1013
|
verdict,
|
|
661
1014
|
confidence,
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
release_hold_until: releaseHoldUntil,
|
|
1015
|
+
passed: verdict === "approved",
|
|
1016
|
+
...(settledAs ? { settled_as: settledAs } : {}),
|
|
665
1017
|
reasoning: data.reasoning ?? "",
|
|
666
1018
|
on_chain_url: data.on_chain_url ?? null,
|
|
667
1019
|
proof_id: data.proof_id ?? null,
|
|
668
1020
|
signed_at: data.signed_at ?? null,
|
|
1021
|
+
...(isV3 ? v3Fields(data) : {}),
|
|
669
1022
|
model_votes: votes,
|
|
670
1023
|
});
|
|
671
1024
|
}
|
|
672
1025
|
|
|
1026
|
+
/** The MilestoneEscrowV3 fields of an answer, for JSON output (post-review G4). Never derived: forwarded. */
|
|
1027
|
+
function v3Fields(r) {
|
|
1028
|
+
return {
|
|
1029
|
+
escrowVersion: 3,
|
|
1030
|
+
submissionId: r.submissionId ?? null,
|
|
1031
|
+
manifestHash: r.manifestHash ?? null,
|
|
1032
|
+
submissionVerdict: r.submissionVerdict ?? "PENDING",
|
|
1033
|
+
claimReady: r.claimReady === true,
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
|
|
673
1037
|
async function cmdCreate(args) {
|
|
674
1038
|
if (!API_KEY) err("CASCROW_API_KEY is not set");
|
|
675
1039
|
if (!args.title) err("--title is required");
|
|
@@ -679,12 +1043,40 @@ async function cmdCreate(args) {
|
|
|
679
1043
|
deadline.setDate(deadline.getDate() + deadlineDays);
|
|
680
1044
|
const amountUSD = parseFloat(args.amount ?? "0");
|
|
681
1045
|
|
|
1046
|
+
// T07. The bar the panel judges against — kept OUT of the title, which is printed
|
|
1047
|
+
// on the public certificate. Normalised by a copy of src/lib/milestone-bar-input.ts
|
|
1048
|
+
// (milestone-bar-input-parity.test.ts runs it against the owner).
|
|
1049
|
+
let criteriaRaw = typeof args.criteria === "string" ? args.criteria : undefined;
|
|
1050
|
+
if (typeof args["criteria-file"] === "string") criteriaRaw = readFileSync(args["criteria-file"], "utf8");
|
|
1051
|
+
let checklistRaw;
|
|
1052
|
+
if (typeof args["checklist-file"] === "string") {
|
|
1053
|
+
try {
|
|
1054
|
+
checklistRaw = JSON.parse(readFileSync(args["checklist-file"], "utf8"));
|
|
1055
|
+
} catch (e) {
|
|
1056
|
+
err(`--checklist-file: not a JSON array of { title, severity?, id? } (${e.message})`);
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
const reviewMode = typeof args["review-mode"] === "string" ? args["review-mode"].toUpperCase() : undefined;
|
|
1060
|
+
if (reviewMode !== undefined && !["AUTO", "MANUAL", "MANUAL_AUTO"].includes(reviewMode)) {
|
|
1061
|
+
err("--review-mode must be AUTO, MANUAL or MANUAL_AUTO");
|
|
1062
|
+
}
|
|
1063
|
+
let criteria, checklist;
|
|
1064
|
+
try {
|
|
1065
|
+
criteria = normalizeCriteria(criteriaRaw);
|
|
1066
|
+
checklist = normalizeMilestoneChecklist(checklistRaw);
|
|
1067
|
+
} catch (e) {
|
|
1068
|
+
err(e.message);
|
|
1069
|
+
}
|
|
1070
|
+
|
|
682
1071
|
const data = await apiPost("/api/contracts", {
|
|
683
1072
|
milestones: [{
|
|
684
1073
|
title: args.title,
|
|
685
1074
|
amountUSD,
|
|
686
1075
|
cancelAfter: deadline.toISOString(),
|
|
1076
|
+
...(criteria !== undefined ? { verificationCriteria: criteria } : {}),
|
|
1077
|
+
...(checklist !== undefined ? { verificationChecklist: checklist } : {}),
|
|
687
1078
|
}],
|
|
1079
|
+
...(reviewMode !== undefined ? { agentReviewMode: reviewMode } : {}),
|
|
688
1080
|
});
|
|
689
1081
|
|
|
690
1082
|
const contractUrl = `${BASE_URL}/contract/${data.contractId}`;
|
|
@@ -692,42 +1084,28 @@ async function cmdCreate(args) {
|
|
|
692
1084
|
process.stderr.write(`\nContract created: ${contractUrl}\n\n`);
|
|
693
1085
|
}
|
|
694
1086
|
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
: { contractId: args.contract };
|
|
702
|
-
|
|
703
|
-
const data = await apiPost("/api/agent/fund-milestone", body);
|
|
704
|
-
print(data);
|
|
705
|
-
process.stderr.write(`\nMilestone funded (simulation). ID: ${data.milestoneId}\n\n`);
|
|
1087
|
+
// Removed in 3.0.0 (cascrow Phase 6, owner decision 16): `fund` activated a v2
|
|
1088
|
+
// milestone. A $0 verification contract is active by itself once the builder has
|
|
1089
|
+
// joined, and a milestone with an amount is a MilestoneEscrowV3 escrow its payer
|
|
1090
|
+
// funds from their own machine. Kept as a command only to say so; it sends nothing.
|
|
1091
|
+
function cmdFundRemoved() {
|
|
1092
|
+
err(`${V3_CLIENT_TEXT.fundRemoved} [TOOL_REMOVED]`);
|
|
706
1093
|
}
|
|
707
1094
|
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
process.stderr.write(
|
|
723
|
-
"\n⚠️ --amount is ignored for escrow funding: the escrowed amount is the milestone's own amountUSD.\n"
|
|
724
|
-
);
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
process.stderr.write("\nFunding milestone on-chain (approve + fundMilestone)...\n");
|
|
728
|
-
const data = await apiPost("/api/agent/escrow-fund", body);
|
|
729
|
-
print(data);
|
|
730
|
-
process.stderr.write(`\n✅ ${data.amountUSD} RLUSD locked on-chain. TX: ${data.txHash}\n\n`);
|
|
1095
|
+
// Removed in 2.0.0 (cascrow Phase 5): `escrow-fund --private-key` sent the key
|
|
1096
|
+
// to cascrow, which signed the deposit server-side. Kept as a command only to say
|
|
1097
|
+
// so: it sends NOTHING — no request, no argument — so a key typed on the command
|
|
1098
|
+
// line goes nowhere. It cannot un-type it, which is why the message tells the user
|
|
1099
|
+
// what a key that reached a shell history (or an older version of this CLI) is now.
|
|
1100
|
+
function cmdEscrowFundRemoved() {
|
|
1101
|
+
err(
|
|
1102
|
+
"escrow-fund was removed in cascrow-cli 2.0.0: cascrow never accepts, creates, stores or signs with private keys. " +
|
|
1103
|
+
"A $0 verification contract needs no funding — it is active once the builder has joined. A milestone with an amount " +
|
|
1104
|
+
"is a MilestoneEscrowV3 escrow in native XRP, funded by the payer with one transaction from their own machine: first link " +
|
|
1105
|
+
"the wallet you will fund from (GET /api/user/wallet/challenge, sign the message, POST /api/user/wallet), then run " +
|
|
1106
|
+
"npx cascrow-signer@0.1.0 fund --milestone <milestoneId>. If you ever passed a private key to this command (it is also in your shell history), " +
|
|
1107
|
+
"treat that key as exposed and move its funds to a new wallet. [CUSTODY_PATH_REMOVED]"
|
|
1108
|
+
);
|
|
731
1109
|
}
|
|
732
1110
|
|
|
733
1111
|
// Legacy two-step: upload proof → returns proofId → user runs `cascrow verify --proof <id>`
|
|
@@ -742,7 +1120,25 @@ async function cmdSubmit(args) {
|
|
|
742
1120
|
args.proof
|
|
743
1121
|
);
|
|
744
1122
|
print(data);
|
|
745
|
-
|
|
1123
|
+
// Upload starts verification by itself — `verify --proof` would only race it.
|
|
1124
|
+
process.stderr.write(`\nProof submitted; verification has started. Wait for the verdict: cascrow status --proof ${data.proofId} --wait\n\n`);
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
async function cmdStatus(args) {
|
|
1128
|
+
if (!API_KEY) err("CASCROW_API_KEY is not set");
|
|
1129
|
+
if (!args.proof) err("--proof is required");
|
|
1130
|
+
if (args.wait) {
|
|
1131
|
+
process.stderr.write("\nWaiting for the verification to settle (up to ~2 min)...\n\n");
|
|
1132
|
+
const s = await waitForProof(args.proof, 120_000);
|
|
1133
|
+
print(s);
|
|
1134
|
+
process.stderr.write("\n" + statusSummary(s) + "\n\n");
|
|
1135
|
+
return;
|
|
1136
|
+
}
|
|
1137
|
+
const data = await apiGet(`/api/agent/proof-status/${encodeURIComponent(args.proof)}`);
|
|
1138
|
+
print(data);
|
|
1139
|
+
process.stderr.write(
|
|
1140
|
+
"\n" + (data.pending && data.milestoneClosed !== true ? `⏳ Still verifying${data.claimPhase === "orphaned" ? " — the run was interrupted" : ""}.` : statusSummary({ ...data, proofId: args.proof })) + "\n\n"
|
|
1141
|
+
);
|
|
746
1142
|
}
|
|
747
1143
|
|
|
748
1144
|
async function cmdGet(args) {
|
|
@@ -757,7 +1153,8 @@ async function cmdJoin(args) {
|
|
|
757
1153
|
if (!args.invite) err("--invite is required");
|
|
758
1154
|
const data = await apiPost("/api/contracts/join", { inviteCode: args.invite });
|
|
759
1155
|
print(data);
|
|
760
|
-
|
|
1156
|
+
// `activated`: a $0 contract is active now; a money contract waits for its V3 offer.
|
|
1157
|
+
process.stderr.write(`\nJoined contract: ${data.contractId}\n${joinedLine(data)}\n\n`);
|
|
761
1158
|
}
|
|
762
1159
|
|
|
763
1160
|
async function cmdHandoff(args) {
|
|
@@ -797,9 +1194,9 @@ async function cmdCheckInvites() {
|
|
|
797
1194
|
* verdict on cannot come apart (F-22).
|
|
798
1195
|
*
|
|
799
1196
|
* `review-queue` is the pull behind the `manual_review.required` webhook: a
|
|
800
|
-
* webhook nobody received is a decision nobody makes, and cascrow
|
|
801
|
-
*
|
|
802
|
-
*
|
|
1197
|
+
* webhook nobody received is a decision nobody makes, and cascrow completes an
|
|
1198
|
+
* undecided milestone after 48 h (agent contracts in MANUAL_AUTO/AUTO) or 14 days
|
|
1199
|
+
* (every other contract). Verification-only contracts: no money moves. `review` ships with it rather than later, because a list
|
|
803
1200
|
* its reader cannot act on names a remedy they are unable to perform (F-72).
|
|
804
1201
|
*/
|
|
805
1202
|
async function cmdReviewQueue() {
|
|
@@ -807,8 +1204,16 @@ async function cmdReviewQueue() {
|
|
|
807
1204
|
const data = await apiGet("/api/agent/review-queue");
|
|
808
1205
|
const contracts = data.contracts ?? [];
|
|
809
1206
|
print(data);
|
|
1207
|
+
// Said in both branches: an empty v2 queue is exactly when a reader stops
|
|
1208
|
+
// looking, and a V3 claim window keeps running.
|
|
1209
|
+
const v3 =
|
|
1210
|
+
data.v3ClaimsPending === null
|
|
1211
|
+
? "Open MilestoneEscrowV3 claims could not be counted — run: cascrow v3-claims\n\n"
|
|
1212
|
+
: data.v3ClaimsPending > 0
|
|
1213
|
+
? `${data.v3ClaimsPending} MilestoneEscrowV3 claim(s) can still be objected to — run: cascrow v3-claims\n\n`
|
|
1214
|
+
: "";
|
|
810
1215
|
if (contracts.length === 0) {
|
|
811
|
-
process.stderr.write(
|
|
1216
|
+
process.stderr.write(`\nNo milestones are waiting for your verdict.\n\n${v3}`);
|
|
812
1217
|
return;
|
|
813
1218
|
}
|
|
814
1219
|
const lines = contracts.map((c) => {
|
|
@@ -819,15 +1224,100 @@ async function cmdReviewQueue() {
|
|
|
819
1224
|
: pending.length > 1
|
|
820
1225
|
? `${pending.length} milestones pending — pass --milestone <id>`
|
|
821
1226
|
: "contract-level review — omit --milestone";
|
|
822
|
-
return ` ${c.contractId}
|
|
1227
|
+
return ` ${c.contractId} (${ms})`;
|
|
823
1228
|
}).join("\n");
|
|
824
1229
|
const more = data.truncated ? `\n(${data.total} in total — this list is capped.)` : "";
|
|
825
1230
|
process.stderr.write(
|
|
826
1231
|
`\n${data.total} contract(s) waiting for your verdict:\n${lines}${more}\n\n` +
|
|
827
|
-
"Decide with: cascrow review --contract <id> --decision APPROVE|REJECT [--milestone <id>]\n\n"
|
|
1232
|
+
"Decide with: cascrow review --contract <id> --decision APPROVE|REJECT [--milestone <id>]\n\n" +
|
|
1233
|
+
v3
|
|
828
1234
|
);
|
|
829
1235
|
}
|
|
830
1236
|
|
|
1237
|
+
/**
|
|
1238
|
+
* The payer's open MilestoneEscrowV3 claims — `GET /api/agent/v3-claims`, the
|
|
1239
|
+
* pull behind the `claim.submitted` webhook. Print only: the action is the
|
|
1240
|
+
* payer's own `object(escrowId)` on chain, which cascrow never performs.
|
|
1241
|
+
*/
|
|
1242
|
+
async function cmdV3Claims() {
|
|
1243
|
+
if (!API_KEY) err("CASCROW_API_KEY is not set");
|
|
1244
|
+
const data = await apiGet("/api/agent/v3-claims");
|
|
1245
|
+
const claims = data.claims ?? [];
|
|
1246
|
+
print(data);
|
|
1247
|
+
if (claims.length === 0) {
|
|
1248
|
+
process.stderr.write("\nNo MilestoneEscrowV3 claim is waiting on you.\n\n");
|
|
1249
|
+
return;
|
|
1250
|
+
}
|
|
1251
|
+
const lines = claims.map((c) =>
|
|
1252
|
+
` ${c.escrowId} on ${c.chainId}/${c.escrowContract} claim ${c.claimIndex}` +
|
|
1253
|
+
`${c.secondFunding ? " SECOND funding" : ""}${c.held ? " (held — verify on chain)" : ""} ` +
|
|
1254
|
+
(c.objectable === null ? "claim state unknown here — read getEscrow on chain" : c.objectable ? `object() before ${c.objectionDeadline}` : `window closed at ${c.objectionDeadline}`)
|
|
1255
|
+
).join("\n");
|
|
1256
|
+
const more = data.truncated ? "\n(This list is capped; more claims exist.)" : "";
|
|
1257
|
+
process.stderr.write(`\n${claims.length} claim(s):\n${lines}${more}\n\ncascrow cannot object for you — your own wallet calls object(escrowId) on the escrow contract.\n\n`);
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
// ─── MilestoneEscrowV3, remote half (design Rev. 10) ──────────────────────────
|
|
1261
|
+
// cascrow prepares and reads; the party signs and sends with its own key. This CLI
|
|
1262
|
+
// holds no key and has no dependency: `v3 sign …` runs the PINNED local signer
|
|
1263
|
+
// (V3_CLIENT_TEXT.signerPin) in a child process, with this terminal attached, so
|
|
1264
|
+
// its prompts and its checks are the signer's own.
|
|
1265
|
+
async function cmdV3(sub, args, rawArgs) {
|
|
1266
|
+
if (sub === "sign") {
|
|
1267
|
+
const child = spawn("npx", ["--yes", V3_CLIENT_TEXT.signerPin, ...rawArgs], { stdio: "inherit", shell: process.platform === "win32" });
|
|
1268
|
+
const code = await new Promise((resolve) => child.on("close", resolve));
|
|
1269
|
+
process.exit(typeof code === "number" ? code : 1);
|
|
1270
|
+
}
|
|
1271
|
+
if (!API_KEY) err("CASCROW_API_KEY is not set");
|
|
1272
|
+
switch (sub) {
|
|
1273
|
+
case "offer": {
|
|
1274
|
+
if (!args.milestone || !args["review-period-sec"]) err("--milestone and --review-period-sec are required");
|
|
1275
|
+
const body = {
|
|
1276
|
+
milestoneId: args.milestone,
|
|
1277
|
+
reviewPeriodSec: Number(args["review-period-sec"]),
|
|
1278
|
+
watcherDeclared: args.watcher === true || args.watcher === "true",
|
|
1279
|
+
...(args.arbiter ? { arbiter: args.arbiter } : {}),
|
|
1280
|
+
...(args["arbitration-period-sec"] ? { arbitrationPeriodSec: Number(args["arbitration-period-sec"]) } : {}),
|
|
1281
|
+
};
|
|
1282
|
+
print(await apiPost("/api/escrow/v3/offer", body));
|
|
1283
|
+
process.stderr.write(`\nOffer sent. After the builder accepts, fund it from your machine: cascrow v3 sign fund --milestone ${args.milestone}\n\n`);
|
|
1284
|
+
return;
|
|
1285
|
+
}
|
|
1286
|
+
case "acceptance":
|
|
1287
|
+
if (!args.milestone) err("--milestone is required");
|
|
1288
|
+
print(await apiGet(`/api/escrow/v3/acceptance?milestoneId=${encodeURIComponent(args.milestone)}`));
|
|
1289
|
+
process.stderr.write("\nCheck every term — your signature binds you until acceptBy. To check and sign locally: cascrow v3 sign accept --milestone " + args.milestone + "\n\n");
|
|
1290
|
+
return;
|
|
1291
|
+
case "accept-submit":
|
|
1292
|
+
if (!args.offer || !args.signature) err("--offer and --signature are required");
|
|
1293
|
+
print(await apiPost("/api/escrow/v3/acceptance", { offerId: args.offer, signature: args.signature }));
|
|
1294
|
+
return;
|
|
1295
|
+
case "prepare": {
|
|
1296
|
+
if (!args.milestone || !args.action) err("--milestone and --action are required");
|
|
1297
|
+
const q = new URLSearchParams({ milestoneId: args.milestone, action: args.action });
|
|
1298
|
+
if (args["proof-hash"]) q.set("proofHash", args["proof-hash"]);
|
|
1299
|
+
if (args["reason-hash"]) q.set("reasonHash", args["reason-hash"]);
|
|
1300
|
+
if (args["new-deadline"]) q.set("newDeadline", String(args["new-deadline"]));
|
|
1301
|
+
print(await apiGet(`/api/escrow/v3/prepare?${q.toString()}`));
|
|
1302
|
+
process.stderr.write("\nUNSIGNED — cascrow does not sign, send or relay it. Rebuild data from the ABI and check to/chainId against your own configuration before signing (cascrow v3 sign … does).\n\n");
|
|
1303
|
+
return;
|
|
1304
|
+
}
|
|
1305
|
+
case "report-tx":
|
|
1306
|
+
if (!args.milestone || typeof args.tx !== "string" || !TX_HASH_RE.test(args.tx)) err(V3_CLIENT_TEXT.reportTxOnlyHash);
|
|
1307
|
+
print(await apiPost("/api/escrow/v3/sync", { milestoneId: args.milestone, txHash: args.tx }));
|
|
1308
|
+
return;
|
|
1309
|
+
case "attestation":
|
|
1310
|
+
if (!args.escrow || !args["claim-index"]) err("--escrow and --claim-index are required");
|
|
1311
|
+
print(await apiGet(`/api/escrow/v3/attestation?${new URLSearchParams({ escrowId: args.escrow, claimIndex: String(args["claim-index"]) }).toString()}`));
|
|
1312
|
+
return;
|
|
1313
|
+
case "claims":
|
|
1314
|
+
await cmdV3Claims();
|
|
1315
|
+
return;
|
|
1316
|
+
default:
|
|
1317
|
+
err(`unknown v3 command ${sub ?? "(none)"} — offer | acceptance | accept-submit | prepare | report-tx | attestation | claims | sign`);
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
|
|
831
1321
|
async function cmdReview(args) {
|
|
832
1322
|
if (!API_KEY) err("CASCROW_API_KEY is not set");
|
|
833
1323
|
const contractId = args.contract;
|
|
@@ -837,11 +1327,14 @@ async function cmdReview(args) {
|
|
|
837
1327
|
const body = args.milestone ? { contractId, decision, milestoneId: args.milestone } : { contractId, decision };
|
|
838
1328
|
const data = await apiPost("/api/contracts/review", body);
|
|
839
1329
|
print(data);
|
|
1330
|
+
// The route answers `{ status }` and nothing else: a review only decides
|
|
1331
|
+
// verification-only ($0) milestones, and nothing moves money (a MilestoneEscrowV3
|
|
1332
|
+
// milestone is refused — its payer releases or objects on chain).
|
|
840
1333
|
const what =
|
|
841
1334
|
decision === "APPROVE"
|
|
842
|
-
? "approved
|
|
1335
|
+
? "approved and completed (a verification-only contract: no money moves)"
|
|
843
1336
|
: "rejected — the milestone is back with the builder, who can resubmit";
|
|
844
|
-
process.stderr.write(`\nMilestone ${what}.${data.
|
|
1337
|
+
process.stderr.write(`\nMilestone ${what}.${data.status ? ` Contract status: ${data.status}.` : ""}\n\n`);
|
|
845
1338
|
}
|
|
846
1339
|
|
|
847
1340
|
async function cmdMe() {
|
|
@@ -866,15 +1359,18 @@ try {
|
|
|
866
1359
|
case "verify-work": await cmdVerifyWork(args); break;
|
|
867
1360
|
case "verify": await cmdVerify(args); break;
|
|
868
1361
|
case "create": await cmdCreate(args); break;
|
|
869
|
-
case "fund":
|
|
870
|
-
case "escrow-fund":
|
|
1362
|
+
case "fund": cmdFundRemoved(); break;
|
|
1363
|
+
case "escrow-fund": cmdEscrowFundRemoved(); break;
|
|
871
1364
|
case "submit": await cmdSubmit(args); break;
|
|
1365
|
+
case "status": await cmdStatus(args); break;
|
|
872
1366
|
case "get": await cmdGet(args); break;
|
|
873
1367
|
case "join": await cmdJoin(args); break;
|
|
874
1368
|
case "handoff": await cmdHandoff(args); break;
|
|
875
1369
|
case "check-invites": await cmdCheckInvites(); break;
|
|
876
1370
|
case "review-queue": await cmdReviewQueue(); break;
|
|
877
1371
|
case "review": await cmdReview(args); break;
|
|
1372
|
+
case "v3-claims": await cmdV3Claims(); break;
|
|
1373
|
+
case "v3": await cmdV3(rest[0], parseArgs(rest.slice(1)), rest.slice(1)); break;
|
|
878
1374
|
case "me": await cmdMe(); break;
|
|
879
1375
|
default:
|
|
880
1376
|
console.error(`Unknown command: ${command}\n`);
|