terminalhire 0.40.12 → 0.41.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.
@@ -26129,7 +26129,6 @@ var TERMINALHIRE_DIR5 = process.env.TERMINALHIRE_DIR || join7(homedir5(), ".term
26129
26129
  var CLAIM_PUSH_AUTO_MARKER = join7(TERMINALHIRE_DIR5, "claim-push-auto.json");
26130
26130
  var CLAIM_PUSH_TOKEN_FILE = join7(TERMINALHIRE_DIR5, "claim-push-token.enc");
26131
26131
  var CLAIM_PUSH_MANUAL_MARKER = join7(TERMINALHIRE_DIR5, "claim-push.json");
26132
- var CLAIM_SYNC_BASE = "https://terminalhire.com";
26133
26132
  var AUTO_CONSENT_VERSION = 2;
26134
26133
  var AUTO_PUSH_THROTTLE_MS = 24 * 60 * 60 * 1e3;
26135
26134
  async function writePushTokenEnc(rawToken) {
@@ -26174,28 +26173,6 @@ function clearAutoMarker() {
26174
26173
  function computeSnapshotHash(pushed) {
26175
26174
  return createHash3("sha256").update(JSON.stringify(pushed)).digest("hex");
26176
26175
  }
26177
- function backgroundPushGate(params) {
26178
- const {
26179
- autoMarkerExists,
26180
- tokenFileExists,
26181
- lastPushedAt,
26182
- now,
26183
- throttleMs,
26184
- currentHash,
26185
- lastSnapshotHash
26186
- } = params;
26187
- if (!autoMarkerExists || !tokenFileExists) {
26188
- return { push: false, reason: "not-opted-in" };
26189
- }
26190
- const last = lastPushedAt ? Date.parse(lastPushedAt) : NaN;
26191
- if (!Number.isNaN(last) && now - last < throttleMs) {
26192
- return { push: false, reason: "throttled" };
26193
- }
26194
- if (lastSnapshotHash && lastSnapshotHash === currentHash) {
26195
- return { push: false, reason: "unchanged" };
26196
- }
26197
- return { push: true, reason: "ok" };
26198
- }
26199
26176
  function unpushedNudgeGate(params) {
26200
26177
  const {
26201
26178
  autoMarkerExists,
@@ -26233,50 +26210,107 @@ async function shouldNudgeUnpushed() {
26233
26210
  return false;
26234
26211
  }
26235
26212
  }
26236
- async function runBackgroundClaimPush({ now = Date.now() } = {}) {
26213
+
26214
+ // bin/founder-verdict-sync.js
26215
+ var CLAIM_SYNC_BASE = "https://terminalhire.com";
26216
+ var TERMINAL = /* @__PURE__ */ new Set(["merged", "abandoned"]);
26217
+ function verdictState(verdict) {
26218
+ return verdict === "rejected" ? "abandoned" : "merged";
26219
+ }
26220
+ async function fetchFounderVerdicts(pushToken, fetchImpl = fetch) {
26221
+ if (typeof pushToken !== "string" || pushToken.length === 0) return null;
26237
26222
  try {
26238
- if (!existsSync5(CLAIM_PUSH_AUTO_MARKER) || !existsSync5(CLAIM_PUSH_TOKEN_FILE)) {
26239
- return { pushed: false, reason: "not-opted-in" };
26240
- }
26241
- const marker = readAutoMarker();
26242
- if (!marker || !marker.autoConsentedAt) return { pushed: false, reason: "not-opted-in" };
26243
- const { listClaims: listClaims2, toPushedClaim: toPushedClaim2, PUSHED_CLAIM_FIELDS: PUSHED_CLAIM_FIELDS2 } = await Promise.resolve().then(() => (init_claims(), claims_exports));
26244
- const pushed = listClaims2().map((c) => toPushedClaim2(c));
26245
- const currentHash = computeSnapshotHash(pushed);
26246
- const gate = backgroundPushGate({
26247
- autoMarkerExists: true,
26248
- tokenFileExists: true,
26249
- lastPushedAt: marker.lastPushedAt ?? null,
26250
- now,
26251
- throttleMs: AUTO_PUSH_THROTTLE_MS,
26252
- currentHash,
26253
- lastSnapshotHash: marker.lastSnapshotHash ?? null
26254
- });
26255
- if (!gate.push) return { pushed: false, reason: gate.reason };
26256
- const token = await readPushTokenEnc();
26257
- if (!token) return { pushed: false, reason: "unreadable-token" };
26258
- const consentReceipt = {
26259
- consentedAt: marker.autoConsentedAt,
26260
- version: AUTO_CONSENT_VERSION,
26261
- fields: PUSHED_CLAIM_FIELDS2
26262
- };
26263
- const res = await fetch(`${CLAIM_SYNC_BASE}/api/claim-sync`, {
26223
+ const res = await fetchImpl(`${CLAIM_SYNC_BASE}/api/claim/verdicts`, {
26264
26224
  method: "POST",
26265
26225
  headers: { "Content-Type": "application/json" },
26266
- body: JSON.stringify({ consentToken: consentReceipt, claims: pushed, pushToken: token }),
26267
- signal: AbortSignal.timeout(1e4)
26226
+ body: JSON.stringify({ pushToken }),
26227
+ signal: AbortSignal.timeout(15e3)
26268
26228
  });
26269
- if (!res.ok) {
26270
- return { pushed: false, reason: `server-${res.status}` };
26271
- }
26272
- writeAutoMarker({
26273
- ...marker,
26274
- lastPushedAt: new Date(now).toISOString(),
26275
- lastSnapshotHash: currentHash
26229
+ if (!res?.ok) return null;
26230
+ const body = await res.json();
26231
+ if (!body || !Array.isArray(body.verdicts)) return null;
26232
+ const verdicts = body.verdicts.filter(
26233
+ (v) => v && typeof v.claimId === "string" && v.claimId !== "" && (v.verdict === "accepted" || v.verdict === "rejected")
26234
+ );
26235
+ return { verdicts, latestAt: typeof body.latestAt === "string" ? body.latestAt : null };
26236
+ } catch {
26237
+ return null;
26238
+ }
26239
+ }
26240
+ function planVerdictTransitions(claims, verdicts, nextPolledState2) {
26241
+ if (!Array.isArray(claims) || !Array.isArray(verdicts)) return [];
26242
+ const byClaimId = /* @__PURE__ */ new Map();
26243
+ for (const v of verdicts) {
26244
+ if (v && typeof v.claimId === "string" && v.claimId !== "") byClaimId.set(v.claimId, v);
26245
+ }
26246
+ const plan = [];
26247
+ for (const c of claims) {
26248
+ const claimId = c?.approval?.claimId;
26249
+ if (!claimId) continue;
26250
+ const v = byClaimId.get(claimId);
26251
+ if (!v) continue;
26252
+ if (TERMINAL.has(c.state)) continue;
26253
+ const to = verdictState(v.verdict);
26254
+ const next = nextPolledState2(c.state, to);
26255
+ if (next === c.state) continue;
26256
+ plan.push({
26257
+ id: c.id,
26258
+ claimId,
26259
+ from: c.state,
26260
+ to: next,
26261
+ verdict: v.verdict,
26262
+ settled: v.settled === true,
26263
+ amountUSD: typeof c.amountUSD === "number" ? c.amountUSD : null,
26264
+ title: typeof c.title === "string" ? c.title : ""
26276
26265
  });
26277
- return { pushed: true, reason: "ok" };
26266
+ }
26267
+ return plan;
26268
+ }
26269
+ function buildVerdictNotice(t) {
26270
+ if (!t || typeof t.to !== "string") return null;
26271
+ const amount = typeof t.amountUSD === "number" && t.amountUSD > 0 ? `$${t.amountUSD}` : null;
26272
+ if (t.verdict === "rejected") {
26273
+ return ` \u2717 founder rejected${amount ? ` \u2014 ${amount}` : ""} \xB7 claim moved to ${t.to}`;
26274
+ }
26275
+ const paid = t.settled ? " \xB7 paid" : "";
26276
+ return ` \u2713 founder accepted${amount ? ` \u2014 ${amount}` : ""}${paid} \xB7 claim moved to ${t.to}`;
26277
+ }
26278
+ async function syncFounderVerdicts({
26279
+ claimsModule,
26280
+ targets,
26281
+ readPushTokenEnc: readPushTokenEnc2,
26282
+ fetchImpl = fetch,
26283
+ log = console.log
26284
+ } = {}) {
26285
+ const quiet = { checked: false, unavailable: false, applied: [] };
26286
+ try {
26287
+ const founderTargets = (targets ?? []).filter(
26288
+ (c) => Boolean(c?.approval) && !TERMINAL.has(c.state)
26289
+ );
26290
+ if (founderTargets.length === 0) return quiet;
26291
+ let pushToken = null;
26292
+ try {
26293
+ pushToken = await readPushTokenEnc2();
26294
+ } catch {
26295
+ pushToken = null;
26296
+ }
26297
+ if (!pushToken) return quiet;
26298
+ const res = await fetchFounderVerdicts(pushToken, fetchImpl);
26299
+ if (!res) return { checked: false, unavailable: true, applied: [] };
26300
+ const plan = planVerdictTransitions(founderTargets, res.verdicts, claimsModule.nextPolledState);
26301
+ const applied = [];
26302
+ for (const t of plan) {
26303
+ try {
26304
+ claimsModule.updateClaim(t.id, { state: t.to });
26305
+ applied.push(t);
26306
+ const line = buildVerdictNotice(t);
26307
+ if (line) log(line);
26308
+ } catch {
26309
+ }
26310
+ }
26311
+ return { checked: true, unavailable: false, applied };
26278
26312
  } catch {
26279
- return { pushed: false, reason: "failed" };
26313
+ return quiet;
26280
26314
  }
26281
26315
  }
26282
26316
 
@@ -27218,15 +27252,35 @@ async function localLoginForPaidBrowserClaim() {
27218
27252
  if (timer) clearTimeout(timer);
27219
27253
  }
27220
27254
  }
27255
+ var PUSH_TOKEN_REFUSAL = Object.freeze({
27256
+ /** The credential is unknown or revoked — dead. Clear it. */
27257
+ INVALID: "invalid-push-token",
27258
+ /** The credential is LIVE, just not a registration credential. NEVER clear it. */
27259
+ INSUFFICIENT: "insufficient-push-token",
27260
+ /**
27261
+ * TERM-325. Minted before tokens were bound to a GitHub account id, so it can no
27262
+ * longer prove who holds it. Unrevoked and real, and it fails closed at every scope
27263
+ * — the slice route answers `invalid-push-token` for the same row — so the ACTION is
27264
+ * the revoked one: clear it and verify in the browser.
27265
+ *
27266
+ * The WORDING is not. The server is explicit that nothing was revoked and the
27267
+ * requirement changed on our side, so telling this developer they revoked something
27268
+ * is false. Sharing a branch must not mean sharing a sentence.
27269
+ */
27270
+ LEGACY: "legacy-push-token"
27271
+ });
27272
+ var SYNC_BACKGROUND_PUSH_ACTIVE_FIELD = "backgroundPushActive";
27221
27273
  async function registerFounderClaim(b) {
27222
27274
  const postingId = b.bountyId.replace(/^bounty:founder:/, "");
27275
+ let clearedLocalCredential = false;
27276
+ let refusedForPurpose = false;
27223
27277
  const refuse = (reason) => {
27224
27278
  console.error(
27225
27279
  `
27226
27280
  terminalhire claim: refusing to record \u2014 ${reason}
27227
- Nothing was recorded: a founder-posting claim registers with terminalhire BEFORE
27281
+ No CLAIM was recorded: a founder-posting claim registers with terminalhire BEFORE
27228
27282
  it is recorded locally (fail-closed), so a posting that is gone, taken, or
27229
- unverifiable is never claimed on stale cache data.`
27283
+ unverifiable is never claimed on stale cache data.` + (clearedLocalCredential ? "\n One local change was kept: the stored push token was deleted from this\n machine (the server no longer accepts it, so keeping it would fail every\n later claim)." : "")
27230
27284
  );
27231
27285
  process.exit(1);
27232
27286
  };
@@ -27241,7 +27295,7 @@ terminalhire claim: refusing to record \u2014 ${reason}
27241
27295
  } catch {
27242
27296
  }
27243
27297
  let expectLogin;
27244
- if (!auth) {
27298
+ const acquireProofAuth = async () => {
27245
27299
  try {
27246
27300
  expectLogin = await localLoginForPaidBrowserClaim();
27247
27301
  } catch (err) {
@@ -27275,7 +27329,8 @@ terminalhire claim: refusing to record \u2014 ${reason}
27275
27329
  );
27276
27330
  }
27277
27331
  auth = { proofToken };
27278
- }
27332
+ };
27333
+ if (!auth) await acquireProofAuth();
27279
27334
  console.log("\n Registering this claim with terminalhire (founder posting)...");
27280
27335
  const sendRegistration = async (includeExpectation) => fetch(`${CLAIM_SYNC_BASE2}/api/claim/register`, {
27281
27336
  method: "POST",
@@ -27287,6 +27342,14 @@ terminalhire claim: refusing to record \u2014 ${reason}
27287
27342
  }),
27288
27343
  signal: AbortSignal.timeout(1e4)
27289
27344
  });
27345
+ const readRefusal = async (r) => {
27346
+ if (r.ok) return null;
27347
+ try {
27348
+ return await r.json();
27349
+ } catch {
27350
+ return null;
27351
+ }
27352
+ };
27290
27353
  let res;
27291
27354
  try {
27292
27355
  res = await sendRegistration(true);
@@ -27295,23 +27358,54 @@ terminalhire claim: refusing to record \u2014 ${reason}
27295
27358
  `terminalhire is unreachable (${err instanceof Error ? err.message : String(err)}), so the posting could not be revalidated.`
27296
27359
  );
27297
27360
  }
27298
- let refusalBody = null;
27299
- if (!res.ok) {
27361
+ let refusalBody = await readRefusal(res);
27362
+ const pushTokenRefusal = storedPushToken && res.status === 403 && (refusalBody?.error === PUSH_TOKEN_REFUSAL.INVALID || refusalBody?.error === PUSH_TOKEN_REFUSAL.LEGACY || refusalBody?.error === PUSH_TOKEN_REFUSAL.INSUFFICIENT) ? refusalBody.error : null;
27363
+ if (pushTokenRefusal) {
27364
+ if (pushTokenRefusal !== PUSH_TOKEN_REFUSAL.INSUFFICIENT) {
27365
+ if (pushTokenRefusal === PUSH_TOKEN_REFUSAL.LEGACY) {
27366
+ console.log("\n The push token on this machine was issued before terminalhire tied");
27367
+ console.log(" tokens to a GitHub account, so it can no longer prove who holds it.");
27368
+ console.log(" Nothing was revoked \u2014 the requirement changed on our side.");
27369
+ console.log(" Registering this claim needs only a ONE-TIME identity check, so");
27370
+ console.log(" falling back to browser verification.");
27371
+ } else {
27372
+ console.log("\n The push token stored on this machine is no longer valid.");
27373
+ console.log(" Registering this claim needs only a ONE-TIME identity check, so");
27374
+ console.log(" falling back to browser verification. (Re-enrolling background");
27375
+ console.log(" push is a separate thing you need only for repeatable reads \u2014");
27376
+ console.log(" fetching your granted slice and polling CI.)");
27377
+ }
27378
+ storedPushToken = null;
27379
+ const hadAutoMarker = Boolean(readAutoMarker());
27380
+ clearPushTokenEnc();
27381
+ clearedLocalCredential = true;
27382
+ if (hadAutoMarker) {
27383
+ clearAutoMarker();
27384
+ console.log("\n Background dashboard updates are now OFF \u2014 that token was the");
27385
+ console.log(" credential they ran on. Turn them back on any time:");
27386
+ console.log(" terminalhire claim --push --keep-updated");
27387
+ }
27388
+ } else {
27389
+ refusedForPurpose = true;
27390
+ console.log("\n Registering a claim takes a one-time browser check, which the");
27391
+ console.log(" credential stored on this machine is not for. Falling back to it now.");
27392
+ console.log(" It was not revoked or changed \u2014 this refusal was about which");
27393
+ console.log(" action the credential is for, not whether it is still good.");
27394
+ }
27395
+ await acquireProofAuth();
27300
27396
  try {
27301
- refusalBody = await res.json();
27302
- } catch {
27397
+ res = await sendRegistration(true);
27398
+ } catch (err) {
27399
+ refuse(
27400
+ `terminalhire is unreachable (${err instanceof Error ? err.message : String(err)}), so the posting could not be revalidated.`
27401
+ );
27303
27402
  }
27403
+ refusalBody = await readRefusal(res);
27304
27404
  }
27305
27405
  if (expectLogin && res.status === 422 && refusalBody?.error === "unknown field(s): expectLogin") {
27306
27406
  try {
27307
27407
  res = await sendRegistration(false);
27308
- refusalBody = null;
27309
- if (!res.ok) {
27310
- try {
27311
- refusalBody = await res.json();
27312
- } catch {
27313
- }
27314
- }
27408
+ refusalBody = await readRefusal(res);
27315
27409
  } catch (err) {
27316
27410
  refuse(
27317
27411
  `terminalhire is unreachable (${err instanceof Error ? err.message : String(err)}), so the posting could not be revalidated.`
@@ -27333,43 +27427,61 @@ terminalhire claim: refusing to record \u2014 ${reason}
27333
27427
  if (!body || body.ok !== true) {
27334
27428
  refuse("malformed registration response from the server.");
27335
27429
  }
27430
+ const mintedToken = typeof body.pushToken === "string" && body.pushToken.length > 0 ? body.pushToken : null;
27431
+ if (refusedForPurpose && mintedToken) {
27432
+ try {
27433
+ await writePushTokenEnc(mintedToken);
27434
+ console.log("\n Your stored credential was refreshed \u2014 registering this claim");
27435
+ console.log(" rotated it, so the previous one is no longer valid.");
27436
+ } catch (err) {
27437
+ console.log("\n \u26A0 Registered, but the refreshed credential could not be stored here.");
27438
+ const reason = err instanceof Error ? err.message : String(err);
27439
+ for (const line of reason.split("\n")) console.log(` ${line}`);
27440
+ console.log(" Re-enrol before fetching a slice: terminalhire claim --push");
27441
+ }
27442
+ }
27336
27443
  return {
27337
27444
  claimId: typeof body.claimId === "string" ? body.claimId : null,
27338
27445
  claimantLogin: typeof body.claimantLogin === "string" ? body.claimantLogin : null,
27339
- // Existing-token auth deliberately gets no replacement from the server. Reuse
27340
- // the encrypted value we just authenticated with; proof auth receives a newly
27341
- // minted token exactly once in this private/no-store response.
27342
- pushToken: storedPushToken ?? (typeof body.pushToken === "string" && body.pushToken.length > 0 ? body.pushToken : null)
27446
+ // Existing-token auth deliberately gets no replacement from the server: reuse the
27447
+ // encrypted value we just authenticated with. The one exception is the rotation
27448
+ // above there the stored value is the superseded one, so it must not win.
27449
+ pushToken: refusedForPurpose && mintedToken ? mintedToken : storedPushToken ?? mintedToken
27343
27450
  };
27344
27451
  }
27452
+ function readCredentialDisposition({
27453
+ markerExists,
27454
+ enrolledTokenExists,
27455
+ enrollmentRevoked = false
27456
+ }) {
27457
+ if (!markerExists) return "store";
27458
+ if (enrollmentRevoked) return "store-and-clear-stale-marker";
27459
+ if (enrolledTokenExists) return "keep-enrolled";
27460
+ return "store-and-clear-stale-marker";
27461
+ }
27345
27462
  async function bootstrapFounderClaimEnrollment(claim, registration) {
27346
- if (!(claim.amountUSD > 0)) return { enrolled: false, reason: "free" };
27463
+ if (!(claim.amountUSD > 0)) return { stored: false, reason: "free" };
27347
27464
  if (!registration.pushToken || !registration.claimantLogin) {
27348
- return { enrolled: false, reason: "token-unavailable" };
27465
+ return { stored: false, reason: "token-unavailable" };
27466
+ }
27467
+ const disposition = readCredentialDisposition({
27468
+ markerExists: Boolean(readAutoMarker()),
27469
+ enrolledTokenExists: Boolean(await readPushTokenEnc().catch(() => null))
27470
+ });
27471
+ if (disposition === "keep-enrolled") {
27472
+ return { stored: false, reason: "kept-enrolled" };
27349
27473
  }
27350
27474
  try {
27351
27475
  await writePushTokenEnc(registration.pushToken);
27352
- const prior = readAutoMarker();
27353
- const now = (/* @__PURE__ */ new Date()).toISOString();
27354
- writeAutoMarker({
27355
- ...prior ?? {},
27356
- autoConsentedAt: prior?.autoConsentedAt ?? now,
27357
- version: AUTO_CONSENT_VERSION,
27358
- login: registration.claimantLogin
27359
- // Do not invent lastPushedAt/lastSnapshotHash here. If this is a fresh
27360
- // marker the immediate existing background path must push the newly
27361
- // recorded local claim; if it is an existing marker those fields remain
27362
- // truthful and the changed snapshot makes the normal gate fire.
27363
- });
27476
+ if (disposition === "store-and-clear-stale-marker") clearAutoMarker();
27364
27477
  } catch (err) {
27365
27478
  return {
27366
- enrolled: false,
27479
+ stored: false,
27367
27480
  reason: "local-write-failed",
27368
27481
  detail: err instanceof Error ? err.message : String(err)
27369
27482
  };
27370
27483
  }
27371
- const pushed = await runBackgroundClaimPush();
27372
- return pushed?.pushed ? { enrolled: true, reason: "ok" } : { enrolled: true, reason: pushed?.reason ?? "sync-failed" };
27484
+ return { stored: true, reason: "ok" };
27373
27485
  }
27374
27486
  async function cmdRecord(arg, flags = {}) {
27375
27487
  const claims = await Promise.resolve().then(() => (init_claims(), claims_exports));
@@ -27580,21 +27692,19 @@ terminalhire claim: refusing to record \u2014 read ${b.repoFullName}'s contribut
27580
27692
  ` registered with terminalhire${claim.approval.claimId ? ` (server claim ${claim.approval.claimId})` : ""}`
27581
27693
  );
27582
27694
  if (claim.amountUSD > 0) {
27583
- if (enrollment?.enrolled && enrollment.reason === "ok") {
27584
- console.log(" \u2713 claim updates enabled; push-only token stored encrypted on this machine");
27585
- console.log(" \u2713 score-free claim ledger synced for your founder-facing work record");
27586
- console.log(" Revoke any time: terminalhire claim --push --revoke");
27587
- } else if (enrollment?.enrolled) {
27588
- console.log(" \u2713 claim updates enabled; push-only token stored encrypted on this machine");
27589
- console.log(` \u26A0 initial claim-ledger sync did not finish (${enrollment.reason}).`);
27590
- console.log(" Retry: terminalhire claim --push --keep-updated");
27591
- console.log(" Revoke any time: terminalhire claim --push --revoke");
27695
+ if (enrollment?.stored) {
27696
+ console.log(" \u2713 credential for your granted slice + CI results stored encrypted here");
27697
+ console.log(" Background dashboard updates are NOT on (claiming does not enable them).");
27698
+ console.log(" Want them? terminalhire claim --push --keep-updated");
27699
+ console.log(" Remove the stored credential any time: terminalhire claim --push --revoke");
27700
+ } else if (enrollment?.reason === "kept-enrolled") {
27701
+ console.log(" \u2713 your existing enrolled credential is kept and covers this claim");
27592
27702
  } else {
27593
- console.log(" \u26A0 Claim recorded, but automatic claim updates were not fully enrolled.");
27703
+ console.log(" \u26A0 Claim recorded, but the credential could not be stored on this machine.");
27594
27704
  if (enrollment?.detail) {
27595
27705
  for (const line of String(enrollment.detail).split("\n")) console.log(` ${line}`);
27596
27706
  }
27597
- console.log(" Finish setup: terminalhire claim --push --keep-updated");
27707
+ console.log(" Store it before fetching your slice: terminalhire claim --push");
27598
27708
  }
27599
27709
  }
27600
27710
  if (claim.approval.state === "pending") {
@@ -27726,6 +27836,17 @@ async function cmdList(active) {
27726
27836
  }
27727
27837
  await syncFounderApprovals(claims, list);
27728
27838
  list = claims.listClaims({ active });
27839
+ await syncFounderVerdicts({
27840
+ claimsModule: claims,
27841
+ targets: list,
27842
+ readPushTokenEnc
27843
+ });
27844
+ list = claims.listClaims({ active });
27845
+ if (list.length === 0) {
27846
+ console.log(active ? "\nNo active claims." : "\nNo claims yet.");
27847
+ printMetric(claims.acceptedPRRate());
27848
+ return;
27849
+ }
27729
27850
  console.log(`
27730
27851
  ${list.length} ${active ? "active " : ""}claim${list.length === 1 ? "" : "s"}:
27731
27852
  `);
@@ -27874,6 +27995,12 @@ async function cmdStatus(id) {
27874
27995
  approvalsChecked = syncRes.approvalsChecked;
27875
27996
  approvalsUnavailable = syncRes.approvalsUnavailable;
27876
27997
  targets = id ? [claims.findClaim(id)].filter(Boolean) : claims.listClaims();
27998
+ await syncFounderVerdicts({
27999
+ claimsModule: claims,
28000
+ targets,
28001
+ readPushTokenEnc
28002
+ });
28003
+ targets = id ? [claims.findClaim(id)].filter(Boolean) : claims.listClaims();
27877
28004
  console.log("\n Founder claims:");
27878
28005
  for (const c of targets.filter((claim) => Boolean(claim.approval))) {
27879
28006
  console.log(` ${founderClaimStanding(c, approvalsChecked)} \u2014 ${c.title}`);
@@ -28577,6 +28704,26 @@ function requireFounderLoopClaim(claims, id, verb) {
28577
28704
  }
28578
28705
  return claim;
28579
28706
  }
28707
+ function retireRevokedReadCredential(status, body, what) {
28708
+ if (status !== 403 || body?.error !== PUSH_TOKEN_REFUSAL.INVALID) return false;
28709
+ const hadMarker = Boolean(readAutoMarker());
28710
+ clearPushTokenEnc();
28711
+ if (hadMarker) clearAutoMarker();
28712
+ console.error(
28713
+ `
28714
+ terminalhire claim: ${what} needs the stored credential, and the server says it has been revoked.`
28715
+ );
28716
+ console.error(" It has been removed from this machine, so nothing keeps retrying a");
28717
+ console.error(" credential that cannot work.");
28718
+ if (hadMarker) {
28719
+ console.error(" Background dashboard updates are now OFF \u2014 they ran on that credential.");
28720
+ }
28721
+ console.error(" Get a working one (browser verification): terminalhire claim --push");
28722
+ if (hadMarker) {
28723
+ console.error(" Add --keep-updated to that command to turn background updates back on.");
28724
+ }
28725
+ return true;
28726
+ }
28580
28727
  async function requireReadPushToken(what) {
28581
28728
  let stored = null;
28582
28729
  try {
@@ -28620,7 +28767,9 @@ async function cmdSlice(id, flags = {}) {
28620
28767
  } catch {
28621
28768
  }
28622
28769
  if (!res.ok) {
28623
- console.error(`terminalhire claim: ${renderServerRefusal(res.status, body)}`);
28770
+ if (!retireRevokedReadCredential(res.status, body, "fetching your granted slice")) {
28771
+ console.error(`terminalhire claim: ${renderServerRefusal(res.status, body)}`);
28772
+ }
28624
28773
  process.exit(1);
28625
28774
  }
28626
28775
  if (!body || body.ok !== true || !Array.isArray(body.files)) {
@@ -28748,7 +28897,9 @@ async function cmdRuns(id, flags = {}) {
28748
28897
  process.exit(1);
28749
28898
  }
28750
28899
  if (r.kind === "refusal") {
28751
- console.error(`terminalhire claim: ${renderServerRefusal(r.status, r.body)}`);
28900
+ if (!retireRevokedReadCredential(r.status, r.body, "reading CI results")) {
28901
+ console.error(`terminalhire claim: ${renderServerRefusal(r.status, r.body)}`);
28902
+ }
28752
28903
  process.exit(1);
28753
28904
  }
28754
28905
  console.log(`
@@ -29527,10 +29678,11 @@ async function cmdPush({ keepUpdated = false } = {}) {
29527
29678
  }
29528
29679
  let deleteToken = null;
29529
29680
  let pushToken = null;
29681
+ let syncBody = null;
29530
29682
  try {
29531
- const body = await res.json();
29532
- deleteToken = body?.deleteToken || null;
29533
- pushToken = body?.pushToken || null;
29683
+ syncBody = await res.json();
29684
+ deleteToken = syncBody?.deleteToken || null;
29685
+ pushToken = syncBody?.pushToken || null;
29534
29686
  } catch {
29535
29687
  }
29536
29688
  writeClaimPushMarker({
@@ -29540,23 +29692,55 @@ async function cmdPush({ keepUpdated = false } = {}) {
29540
29692
  lastPushedAt: consentedAt,
29541
29693
  lastSnapshotHash: computeSnapshotHash(pushed)
29542
29694
  });
29543
- if (autoConsent && pushToken) {
29695
+ if (pushToken) {
29544
29696
  try {
29545
- await writePushTokenEnc(pushToken);
29546
- writeAutoMarker({
29547
- autoConsentedAt: consentedAt,
29548
- version: AUTO_CONSENT_VERSION,
29549
- login,
29550
- lastPushedAt: consentedAt,
29551
- lastSnapshotHash: computeSnapshotHash(pushed)
29552
- });
29553
- console.log("\n \u2713 Background updates enabled \u2014 your dashboard will stay current");
29554
- console.log(" (at most once/day). Stop any time: terminalhire claim --push --revoke");
29697
+ if (autoConsent) {
29698
+ await writePushTokenEnc(pushToken);
29699
+ writeAutoMarker({
29700
+ autoConsentedAt: consentedAt,
29701
+ version: AUTO_CONSENT_VERSION,
29702
+ login,
29703
+ lastPushedAt: consentedAt,
29704
+ lastSnapshotHash: computeSnapshotHash(pushed)
29705
+ });
29706
+ console.log("\n \u2713 Background updates enabled \u2014 your dashboard will stay current");
29707
+ console.log(" (at most once/day). Stop any time: terminalhire claim --push --revoke");
29708
+ } else {
29709
+ const backgroundPushRevoked = syncBody?.[SYNC_BACKGROUND_PUSH_ACTIVE_FIELD] === false;
29710
+ const disposition = readCredentialDisposition({
29711
+ markerExists: Boolean(readAutoMarker()),
29712
+ enrolledTokenExists: Boolean(await readPushTokenEnc()),
29713
+ enrollmentRevoked: backgroundPushRevoked
29714
+ });
29715
+ if (disposition === "store") {
29716
+ await writePushTokenEnc(pushToken);
29717
+ console.log(
29718
+ "\n \u2713 Stored the credential for fetching your granted slice and CI results."
29719
+ );
29720
+ } else if (disposition === "keep-enrolled") {
29721
+ console.log("\n \u2713 Background updates are already active; keeping the credential");
29722
+ console.log(" they run on. Nothing about them changed.");
29723
+ } else {
29724
+ await writePushTokenEnc(pushToken);
29725
+ clearAutoMarker();
29726
+ console.log(
29727
+ "\n \u2713 Stored the credential for fetching your granted slice and CI results."
29728
+ );
29729
+ console.log(
29730
+ backgroundPushRevoked ? " Background dashboard updates are OFF \u2014 that enrolment was stopped or revoked." : " Background dashboard updates are OFF (their credential was gone)."
29731
+ );
29732
+ console.log(" Turn them back on any time: terminalhire claim --push --keep-updated");
29733
+ }
29734
+ }
29555
29735
  } catch (err) {
29556
- console.log("\n \u2713 Pushed, but could not enable background updates on this machine.");
29736
+ console.log(
29737
+ autoConsent ? "\n \u2713 Pushed, but could not enable background updates on this machine." : "\n \u2713 Pushed, but could not store the credential for slice fetch / CI polling."
29738
+ );
29557
29739
  const reason = err instanceof Error ? err.message : String(err);
29558
29740
  for (const line of reason.split("\n")) console.log(` ${line}`);
29559
- console.log(" Re-run `terminalhire claim --push --keep-updated` to retry.");
29741
+ console.log(
29742
+ autoConsent ? " Re-run `terminalhire claim --push --keep-updated` to retry." : " Re-run `terminalhire claim --push` to retry."
29743
+ );
29560
29744
  }
29561
29745
  } else if (backgroundEnableFailed(autoConsent, pushToken)) {
29562
29746
  console.log(
@@ -29844,8 +30028,10 @@ async function run() {
29844
30028
  export {
29845
30029
  AI_DISCLOSURE_NOTE,
29846
30030
  CLAIM_CONSENT_VERSION,
30031
+ PUSH_TOKEN_REFUSAL,
29847
30032
  REVISE_RECOVERY_STATES,
29848
30033
  SUBMIT_ACCEPTS,
30034
+ SYNC_BACKGROUND_PUSH_ACTIVE_FIELD,
29849
30035
  backgroundEnableFailed,
29850
30036
  buildAssignmentComment,
29851
30037
  buildPatchSubmission,
@@ -29853,6 +30039,7 @@ export {
29853
30039
  buildStandDownComment,
29854
30040
  buildSubmitBody,
29855
30041
  claimUpdatePatch,
30042
+ cmdPush,
29856
30043
  cmdRecord,
29857
30044
  cmdRuns,
29858
30045
  cmdSlice,
@@ -29880,6 +30067,7 @@ export {
29880
30067
  pickBodySource,
29881
30068
  pickExistingPr,
29882
30069
  printNextSteps,
30070
+ readCredentialDisposition,
29883
30071
  renderClaimHistory,
29884
30072
  renderRunView,
29885
30073
  renderServerRefusal,