terminalhire 0.42.2 → 0.42.4

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.
@@ -514,7 +514,7 @@ var CLAIM_PUSH_AUTO_MARKER = join5(TERMINALHIRE_DIR4, "claim-push-auto.json");
514
514
  var CLAIM_PUSH_TOKEN_FILE = join5(TERMINALHIRE_DIR4, "claim-push-token.enc");
515
515
  var CLAIM_PUSH_MANUAL_MARKER = join5(TERMINALHIRE_DIR4, "claim-push.json");
516
516
  var CLAIM_SYNC_BASE = "https://terminalhire.com";
517
- var AUTO_CONSENT_VERSION = 2;
517
+ var AUTO_CONSENT_VERSION = 3;
518
518
  var AUTO_PUSH_THROTTLE_MS = 24 * 60 * 60 * 1e3;
519
519
  async function writePushTokenEnc(rawToken) {
520
520
  ensureStateDirForSecret(TERMINALHIRE_DIR4);
@@ -617,6 +617,82 @@ async function shouldNudgeUnpushed() {
617
617
  return false;
618
618
  }
619
619
  }
620
+ var CLAIM_HEARTBEAT_FILE = join5(TERMINALHIRE_DIR4, "claim-heartbeat.json");
621
+ var HEARTBEAT_MIN_INTERVAL_MS = 3e4;
622
+ var HEARTBEAT_MIN_CONSENT_VERSION = 3;
623
+ function readHeartbeatState() {
624
+ try {
625
+ return existsSync5(CLAIM_HEARTBEAT_FILE) ? JSON.parse(readFileSync4(CLAIM_HEARTBEAT_FILE, "utf8")) : {};
626
+ } catch {
627
+ return {};
628
+ }
629
+ }
630
+ function recordHeartbeatBeat(claimId, at) {
631
+ try {
632
+ ensureStateDir(TERMINALHIRE_DIR4);
633
+ const state = readHeartbeatState();
634
+ state[claimId] = at;
635
+ writeFileSync4(CLAIM_HEARTBEAT_FILE, JSON.stringify(state, null, 2) + "\n", "utf8");
636
+ } catch {
637
+ }
638
+ }
639
+ function heartbeatGate(params) {
640
+ const {
641
+ autoMarkerExists,
642
+ tokenFileExists,
643
+ consentVersion,
644
+ bountyId,
645
+ claimId,
646
+ lastBeatAt,
647
+ now = Date.now(),
648
+ minIntervalMs = HEARTBEAT_MIN_INTERVAL_MS
649
+ } = params;
650
+ if (typeof bountyId !== "string" || bountyId.trim() === "") {
651
+ return { beat: false, reason: "no-server-ids" };
652
+ }
653
+ if (typeof claimId !== "string" || claimId.trim() === "") {
654
+ return { beat: false, reason: "no-server-ids" };
655
+ }
656
+ if (!autoMarkerExists || !tokenFileExists) {
657
+ return { beat: false, reason: "not-opted-in" };
658
+ }
659
+ if (!(Number.isInteger(consentVersion) && consentVersion >= HEARTBEAT_MIN_CONSENT_VERSION)) {
660
+ return { beat: false, reason: "consent-predates-presence" };
661
+ }
662
+ const last = lastBeatAt ? Date.parse(lastBeatAt) : NaN;
663
+ if (!Number.isNaN(last) && now - last < minIntervalMs) {
664
+ return { beat: false, reason: "throttled" };
665
+ }
666
+ return { beat: true, reason: "ok" };
667
+ }
668
+ async function postClaimHeartbeat({ bountyId, claimId, now = Date.now() } = {}) {
669
+ try {
670
+ const marker = readAutoMarker();
671
+ const gate = heartbeatGate({
672
+ autoMarkerExists: Boolean(marker && marker.autoConsentedAt),
673
+ tokenFileExists: existsSync5(CLAIM_PUSH_TOKEN_FILE),
674
+ consentVersion: marker?.version,
675
+ bountyId,
676
+ claimId,
677
+ lastBeatAt: readHeartbeatState()[claimId] ?? null,
678
+ now
679
+ });
680
+ if (!gate.beat) return { beat: false, reason: gate.reason };
681
+ const token = await readPushTokenEnc();
682
+ if (!token) return { beat: false, reason: "unreadable-token" };
683
+ const res = await fetch(`${CLAIM_SYNC_BASE}/api/claim/heartbeat`, {
684
+ method: "POST",
685
+ headers: { "Content-Type": "application/json" },
686
+ body: JSON.stringify({ bountyId, claimId, pushToken: token }),
687
+ signal: AbortSignal.timeout(5e3)
688
+ });
689
+ if (!res.ok) return { beat: false, reason: `server-${res.status}` };
690
+ recordHeartbeatBeat(claimId, new Date(now).toISOString());
691
+ return { beat: true, reason: "ok" };
692
+ } catch {
693
+ return { beat: false, reason: "failed" };
694
+ }
695
+ }
620
696
  async function runBackgroundClaimPush({ now = Date.now() } = {}) {
621
697
  try {
622
698
  if (!existsSync5(CLAIM_PUSH_AUTO_MARKER) || !existsSync5(CLAIM_PUSH_TOKEN_FILE)) {
@@ -666,15 +742,22 @@ async function runBackgroundClaimPush({ now = Date.now() } = {}) {
666
742
  export {
667
743
  AUTO_CONSENT_VERSION,
668
744
  AUTO_PUSH_THROTTLE_MS,
745
+ CLAIM_HEARTBEAT_FILE,
669
746
  CLAIM_PUSH_AUTO_MARKER,
670
747
  CLAIM_PUSH_MANUAL_MARKER,
671
748
  CLAIM_PUSH_TOKEN_FILE,
749
+ HEARTBEAT_MIN_CONSENT_VERSION,
750
+ HEARTBEAT_MIN_INTERVAL_MS,
672
751
  backgroundPushGate,
673
752
  clearAutoMarker,
674
753
  clearPushTokenEnc,
675
754
  computeSnapshotHash,
755
+ heartbeatGate,
756
+ postClaimHeartbeat,
676
757
  readAutoMarker,
758
+ readHeartbeatState,
677
759
  readPushTokenEnc,
760
+ recordHeartbeatBeat,
678
761
  runBackgroundClaimPush,
679
762
  shouldNudgeUnpushed,
680
763
  unpushedNudgeGate,
@@ -11198,7 +11198,7 @@ __export(founder_note_sync_exports, {
11198
11198
  async function fetchFounderNotes(pushToken, fetchImpl = fetch) {
11199
11199
  if (typeof pushToken !== "string" || pushToken.length === 0) return null;
11200
11200
  try {
11201
- const res = await fetchImpl(`${CLAIM_SYNC_BASE2}/api/claim/notes`, {
11201
+ const res = await fetchImpl(`${CLAIM_SYNC_BASE3}/api/claim/notes`, {
11202
11202
  method: "POST",
11203
11203
  headers: { "Content-Type": "application/json" },
11204
11204
  body: JSON.stringify({ pushToken }),
@@ -11254,11 +11254,11 @@ function formatNote(note) {
11254
11254
  return ` ${note.at.slice(0, 16).replace("T", " ")} [${label}] ${note.claimId}
11255
11255
  ${note.note.split("\n").join("\n ")}`;
11256
11256
  }
11257
- var CLAIM_SYNC_BASE2;
11257
+ var CLAIM_SYNC_BASE3;
11258
11258
  var init_founder_note_sync = __esm({
11259
11259
  "bin/founder-note-sync.js"() {
11260
11260
  "use strict";
11261
- CLAIM_SYNC_BASE2 = "https://terminalhire.com";
11261
+ CLAIM_SYNC_BASE3 = "https://terminalhire.com";
11262
11262
  }
11263
11263
  });
11264
11264
 
@@ -26238,7 +26238,8 @@ var TERMINALHIRE_DIR5 = process.env.TERMINALHIRE_DIR || join7(homedir5(), ".term
26238
26238
  var CLAIM_PUSH_AUTO_MARKER = join7(TERMINALHIRE_DIR5, "claim-push-auto.json");
26239
26239
  var CLAIM_PUSH_TOKEN_FILE = join7(TERMINALHIRE_DIR5, "claim-push-token.enc");
26240
26240
  var CLAIM_PUSH_MANUAL_MARKER = join7(TERMINALHIRE_DIR5, "claim-push.json");
26241
- var AUTO_CONSENT_VERSION = 2;
26241
+ var CLAIM_SYNC_BASE = "https://terminalhire.com";
26242
+ var AUTO_CONSENT_VERSION = 3;
26242
26243
  var AUTO_PUSH_THROTTLE_MS = 24 * 60 * 60 * 1e3;
26243
26244
  async function writePushTokenEnc(rawToken) {
26244
26245
  ensureStateDirForSecret(TERMINALHIRE_DIR5);
@@ -26319,9 +26320,85 @@ async function shouldNudgeUnpushed() {
26319
26320
  return false;
26320
26321
  }
26321
26322
  }
26323
+ var CLAIM_HEARTBEAT_FILE = join7(TERMINALHIRE_DIR5, "claim-heartbeat.json");
26324
+ var HEARTBEAT_MIN_INTERVAL_MS = 3e4;
26325
+ var HEARTBEAT_MIN_CONSENT_VERSION = 3;
26326
+ function readHeartbeatState() {
26327
+ try {
26328
+ return existsSync5(CLAIM_HEARTBEAT_FILE) ? JSON.parse(readFileSync6(CLAIM_HEARTBEAT_FILE, "utf8")) : {};
26329
+ } catch {
26330
+ return {};
26331
+ }
26332
+ }
26333
+ function recordHeartbeatBeat(claimId, at) {
26334
+ try {
26335
+ ensureStateDir(TERMINALHIRE_DIR5);
26336
+ const state = readHeartbeatState();
26337
+ state[claimId] = at;
26338
+ writeFileSync5(CLAIM_HEARTBEAT_FILE, JSON.stringify(state, null, 2) + "\n", "utf8");
26339
+ } catch {
26340
+ }
26341
+ }
26342
+ function heartbeatGate(params) {
26343
+ const {
26344
+ autoMarkerExists,
26345
+ tokenFileExists,
26346
+ consentVersion,
26347
+ bountyId,
26348
+ claimId,
26349
+ lastBeatAt,
26350
+ now = Date.now(),
26351
+ minIntervalMs = HEARTBEAT_MIN_INTERVAL_MS
26352
+ } = params;
26353
+ if (typeof bountyId !== "string" || bountyId.trim() === "") {
26354
+ return { beat: false, reason: "no-server-ids" };
26355
+ }
26356
+ if (typeof claimId !== "string" || claimId.trim() === "") {
26357
+ return { beat: false, reason: "no-server-ids" };
26358
+ }
26359
+ if (!autoMarkerExists || !tokenFileExists) {
26360
+ return { beat: false, reason: "not-opted-in" };
26361
+ }
26362
+ if (!(Number.isInteger(consentVersion) && consentVersion >= HEARTBEAT_MIN_CONSENT_VERSION)) {
26363
+ return { beat: false, reason: "consent-predates-presence" };
26364
+ }
26365
+ const last = lastBeatAt ? Date.parse(lastBeatAt) : NaN;
26366
+ if (!Number.isNaN(last) && now - last < minIntervalMs) {
26367
+ return { beat: false, reason: "throttled" };
26368
+ }
26369
+ return { beat: true, reason: "ok" };
26370
+ }
26371
+ async function postClaimHeartbeat({ bountyId, claimId, now = Date.now() } = {}) {
26372
+ try {
26373
+ const marker = readAutoMarker();
26374
+ const gate = heartbeatGate({
26375
+ autoMarkerExists: Boolean(marker && marker.autoConsentedAt),
26376
+ tokenFileExists: existsSync5(CLAIM_PUSH_TOKEN_FILE),
26377
+ consentVersion: marker?.version,
26378
+ bountyId,
26379
+ claimId,
26380
+ lastBeatAt: readHeartbeatState()[claimId] ?? null,
26381
+ now
26382
+ });
26383
+ if (!gate.beat) return { beat: false, reason: gate.reason };
26384
+ const token = await readPushTokenEnc();
26385
+ if (!token) return { beat: false, reason: "unreadable-token" };
26386
+ const res = await fetch(`${CLAIM_SYNC_BASE}/api/claim/heartbeat`, {
26387
+ method: "POST",
26388
+ headers: { "Content-Type": "application/json" },
26389
+ body: JSON.stringify({ bountyId, claimId, pushToken: token }),
26390
+ signal: AbortSignal.timeout(5e3)
26391
+ });
26392
+ if (!res.ok) return { beat: false, reason: `server-${res.status}` };
26393
+ recordHeartbeatBeat(claimId, new Date(now).toISOString());
26394
+ return { beat: true, reason: "ok" };
26395
+ } catch {
26396
+ return { beat: false, reason: "failed" };
26397
+ }
26398
+ }
26322
26399
 
26323
26400
  // bin/founder-verdict-sync.js
26324
- var CLAIM_SYNC_BASE = "https://terminalhire.com";
26401
+ var CLAIM_SYNC_BASE2 = "https://terminalhire.com";
26325
26402
  var TERMINAL = /* @__PURE__ */ new Set(["merged", "abandoned"]);
26326
26403
  function verdictState(verdict) {
26327
26404
  return verdict === "rejected" ? "abandoned" : "merged";
@@ -26329,7 +26406,7 @@ function verdictState(verdict) {
26329
26406
  async function fetchFounderVerdicts(pushToken, fetchImpl = fetch) {
26330
26407
  if (typeof pushToken !== "string" || pushToken.length === 0) return null;
26331
26408
  try {
26332
- const res = await fetchImpl(`${CLAIM_SYNC_BASE}/api/claim/verdicts`, {
26409
+ const res = await fetchImpl(`${CLAIM_SYNC_BASE2}/api/claim/verdicts`, {
26333
26410
  method: "POST",
26334
26411
  headers: { "Content-Type": "application/json" },
26335
26412
  body: JSON.stringify({ pushToken }),
@@ -26447,7 +26524,7 @@ function markClaimNudged(id) {
26447
26524
  }
26448
26525
  }
26449
26526
  var API_URL = process.env["TERMINALHIRE_API_URL"] ?? process.env["JPI_API_URL"] ?? "https://terminalhire.com";
26450
- var CLAIM_SYNC_BASE3 = "https://terminalhire.com";
26527
+ var CLAIM_SYNC_BASE4 = "https://terminalhire.com";
26451
26528
  var CLAIM_CONSENT_VERSION = 1;
26452
26529
  var CLAIM_POLL_INTERVAL_MS = 2e3;
26453
26530
  var CLAIM_POLL_TIMEOUT_MS = 10 * 60 * 1e3;
@@ -27289,7 +27366,7 @@ async function mintRegistrationProof() {
27289
27366
  console.log(" GitHub identity has to be verified once in the browser.");
27290
27367
  let begin;
27291
27368
  try {
27292
- const r = await fetch(`${CLAIM_SYNC_BASE3}/api/claim-sync/begin`, {
27369
+ const r = await fetch(`${CLAIM_SYNC_BASE4}/api/claim-sync/begin`, {
27293
27370
  method: "POST",
27294
27371
  headers: { "Content-Type": "application/json" },
27295
27372
  body: JSON.stringify({ hostname: osHostname() }),
@@ -27322,7 +27399,7 @@ async function mintRegistrationProof() {
27322
27399
  let statusRes;
27323
27400
  try {
27324
27401
  statusRes = await fetch(
27325
- `${CLAIM_SYNC_BASE3}/api/claim-sync/status?challenge=${encodeURIComponent(challenge)}`,
27402
+ `${CLAIM_SYNC_BASE4}/api/claim-sync/status?challenge=${encodeURIComponent(challenge)}`,
27326
27403
  { signal: AbortSignal.timeout(1e4) }
27327
27404
  );
27328
27405
  } catch {
@@ -27374,6 +27451,13 @@ var PUSH_TOKEN_REFUSAL = Object.freeze({
27374
27451
  INVALID: "invalid-push-token",
27375
27452
  /** The credential is LIVE, just not a registration credential. NEVER clear it. */
27376
27453
  INSUFFICIENT: "insufficient-push-token",
27454
+ /**
27455
+ * TERM-499. Live background token, but minted under a consent card that did not
27456
+ * disclose claim registration (or presence). NEVER clear it — the daily push it
27457
+ * was enrolled for still works. Fall back to a one-time browser proof for this
27458
+ * registration, and tell the developer how to re-enrol for the updated card.
27459
+ */
27460
+ CONSENT_PREDATES_REGISTER: "consent-predates-register",
27377
27461
  /**
27378
27462
  * TERM-325. Minted before tokens were bound to a GitHub account id, so it can no
27379
27463
  * longer prove who holds it. Unrevoked and real, and it fails closed at every scope
@@ -27391,6 +27475,7 @@ async function registerFounderClaim(b) {
27391
27475
  const postingId = b.bountyId.replace(/^bounty:founder:/, "");
27392
27476
  let clearedLocalCredential = false;
27393
27477
  let refusedForPurpose = false;
27478
+ let preserveBackgroundToken = false;
27394
27479
  const refuse = (reason) => {
27395
27480
  console.error(
27396
27481
  `
@@ -27449,7 +27534,7 @@ terminalhire claim: refusing to record \u2014 ${reason}
27449
27534
  };
27450
27535
  if (!auth) await acquireProofAuth();
27451
27536
  console.log("\n Registering this claim with terminalhire (founder posting)...");
27452
- const sendRegistration = async (includeExpectation) => fetch(`${CLAIM_SYNC_BASE3}/api/claim/register`, {
27537
+ const sendRegistration = async (includeExpectation) => fetch(`${CLAIM_SYNC_BASE4}/api/claim/register`, {
27453
27538
  method: "POST",
27454
27539
  headers: { "Content-Type": "application/json" },
27455
27540
  body: JSON.stringify({
@@ -27476,9 +27561,9 @@ terminalhire claim: refusing to record \u2014 ${reason}
27476
27561
  );
27477
27562
  }
27478
27563
  let refusalBody = await readRefusal(res);
27479
- 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;
27564
+ 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 === PUSH_TOKEN_REFUSAL.CONSENT_PREDATES_REGISTER) ? refusalBody.error : null;
27480
27565
  if (pushTokenRefusal) {
27481
- if (pushTokenRefusal !== PUSH_TOKEN_REFUSAL.INSUFFICIENT) {
27566
+ if (pushTokenRefusal !== PUSH_TOKEN_REFUSAL.INSUFFICIENT && pushTokenRefusal !== PUSH_TOKEN_REFUSAL.CONSENT_PREDATES_REGISTER) {
27482
27567
  if (pushTokenRefusal === PUSH_TOKEN_REFUSAL.LEGACY) {
27483
27568
  console.log("\n The push token on this machine was issued before terminalhire tied");
27484
27569
  console.log(" tokens to a GitHub account, so it can no longer prove who holds it.");
@@ -27504,8 +27589,17 @@ terminalhire claim: refusing to record \u2014 ${reason}
27504
27589
  }
27505
27590
  } else {
27506
27591
  refusedForPurpose = true;
27507
- console.log("\n Registering a claim takes a one-time browser check, which the");
27508
- console.log(" credential stored on this machine is not for. Falling back to it now.");
27592
+ if (pushTokenRefusal === PUSH_TOKEN_REFUSAL.CONSENT_PREDATES_REGISTER) {
27593
+ preserveBackgroundToken = true;
27594
+ console.log("\n Your keep-updated enrolment predates the card that discloses");
27595
+ console.log(" claim registration (and founder presence). Nothing was revoked.");
27596
+ console.log(" Registering THIS claim falls back to a one-time browser check.");
27597
+ console.log(" To enrol under the updated card:");
27598
+ console.log(" terminalhire claim --push --keep-updated");
27599
+ } else {
27600
+ console.log("\n Registering a claim takes a one-time browser check, which the");
27601
+ console.log(" credential stored on this machine is not for. Falling back to it now.");
27602
+ }
27509
27603
  console.log(" It was not revoked or changed \u2014 this refusal was about which");
27510
27604
  console.log(" action the credential is for, not whether it is still good.");
27511
27605
  }
@@ -27545,7 +27639,7 @@ terminalhire claim: refusing to record \u2014 ${reason}
27545
27639
  refuse("malformed registration response from the server.");
27546
27640
  }
27547
27641
  const mintedToken = typeof body.pushToken === "string" && body.pushToken.length > 0 ? body.pushToken : null;
27548
- if (refusedForPurpose && mintedToken) {
27642
+ if (refusedForPurpose && mintedToken && !preserveBackgroundToken) {
27549
27643
  try {
27550
27644
  await writePushTokenEnc(mintedToken);
27551
27645
  console.log("\n Your stored credential was refreshed \u2014 registering this claim");
@@ -27563,7 +27657,8 @@ terminalhire claim: refusing to record \u2014 ${reason}
27563
27657
  // Existing-token auth deliberately gets no replacement from the server: reuse the
27564
27658
  // encrypted value we just authenticated with. The one exception is the rotation
27565
27659
  // above — there the stored value is the superseded one, so it must not win.
27566
- pushToken: refusedForPurpose && mintedToken ? mintedToken : storedPushToken ?? mintedToken
27660
+ // Consent-predates also keeps the background token even if a read mint arrived.
27661
+ pushToken: refusedForPurpose && mintedToken && !preserveBackgroundToken ? mintedToken : storedPushToken ?? mintedToken
27567
27662
  };
27568
27663
  }
27569
27664
  function readCredentialDisposition({
@@ -27600,6 +27695,13 @@ async function bootstrapFounderClaimEnrollment(claim, registration) {
27600
27695
  }
27601
27696
  return { stored: true, reason: "ok" };
27602
27697
  }
27698
+ async function beatFounderPresence(claim) {
27699
+ if (!claim) return;
27700
+ await postClaimHeartbeat({
27701
+ bountyId: founderPostingIdOf(claim),
27702
+ claimId: claim.approval?.claimId ?? null
27703
+ });
27704
+ }
27603
27705
  async function cmdRecord(arg, flags = {}) {
27604
27706
  const claims = await Promise.resolve().then(() => (init_claims(), claims_exports));
27605
27707
  if (!arg) {
@@ -27833,6 +27935,7 @@ terminalhire claim: refusing to record \u2014 read ${b.repoFullName}'s contribut
27833
27935
  console.log("\n Founder postings are never forked or cloned \u2014 the work arrives as a");
27834
27936
  console.log(" read-slice through terminalhire, and your patch goes back the same way.");
27835
27937
  }
27938
+ await beatFounderPresence(claim);
27836
27939
  return;
27837
27940
  }
27838
27941
  console.log(`
@@ -28029,7 +28132,7 @@ async function resolveIssueOutcome(c, res, claims) {
28029
28132
  async function fetchFounderApprovals(pushToken, fetchImpl = fetch) {
28030
28133
  if (typeof pushToken !== "string" || pushToken.length === 0) return null;
28031
28134
  try {
28032
- const res = await fetchImpl(`${CLAIM_SYNC_BASE3}/api/claim/approvals`, {
28135
+ const res = await fetchImpl(`${CLAIM_SYNC_BASE4}/api/claim/approvals`, {
28033
28136
  method: "POST",
28034
28137
  headers: { "Content-Type": "application/json" },
28035
28138
  body: JSON.stringify({ pushToken }),
@@ -28269,6 +28372,7 @@ async function cmdUpdate(id, state, prUrl) {
28269
28372
  process.exit(1);
28270
28373
  }
28271
28374
  console.log(`Updated ${id} \u2192 ${state}${prUrl ? ` (PR: ${prUrl})` : ""}`);
28375
+ await beatFounderPresence(updated);
28272
28376
  }
28273
28377
  async function cmdRelease(id, flags = {}) {
28274
28378
  const claims = await Promise.resolve().then(() => (init_claims(), claims_exports));
@@ -28396,8 +28500,9 @@ async function cmdAttach(id, worktree, branch) {
28396
28500
  console.error(`terminalhire claim: '${worktree}' is not a git work tree.`);
28397
28501
  process.exit(1);
28398
28502
  }
28399
- claims.updateClaim(id, { worktreePath: toplevel, branch });
28503
+ const attached = claims.updateClaim(id, { worktreePath: toplevel, branch });
28400
28504
  console.log(`Attached ${id}: worktree=${toplevel} branch=${branch}`);
28505
+ await beatFounderPresence(attached);
28401
28506
  }
28402
28507
  function workDirFor(repoFullName, issueNumber) {
28403
28508
  const [owner, repo] = String(repoFullName).split("/");
@@ -28495,6 +28600,7 @@ async function cmdStart(id, flags = {}) {
28495
28600
  if (claim.branch) console.log(` branch: ${claim.branch}`);
28496
28601
  console.log(`
28497
28602
  When it's done: terminalhire claim submit ${id}`);
28603
+ await beatFounderPresence(claim);
28498
28604
  return;
28499
28605
  }
28500
28606
  }
@@ -28512,6 +28618,7 @@ ${sanitizeText(claim.title)}`);
28512
28618
  );
28513
28619
  }
28514
28620
  printNextSteps([claim]);
28621
+ await beatFounderPresence(claim);
28515
28622
  return;
28516
28623
  }
28517
28624
  if (flags.here) {
@@ -28888,7 +28995,7 @@ async function cmdSlice(id, flags = {}) {
28888
28995
  const pushToken = await requireReadPushToken("fetching your granted slice");
28889
28996
  let res;
28890
28997
  try {
28891
- res = await fetch(`${CLAIM_SYNC_BASE3}/api/claim/slice`, {
28998
+ res = await fetch(`${CLAIM_SYNC_BASE4}/api/claim/slice`, {
28892
28999
  method: "POST",
28893
29000
  headers: { "Content-Type": "application/json" },
28894
29001
  body: JSON.stringify({
@@ -28980,7 +29087,7 @@ async function cmdSlice(id, flags = {}) {
28980
29087
  );
28981
29088
  process.exit(1);
28982
29089
  }
28983
- claims.updateClaim(claim.id, { worktreePath: dest, branch, state: "working" });
29090
+ const working = claims.updateClaim(claim.id, { worktreePath: dest, branch, state: "working" });
28984
29091
  console.log(`
28985
29092
  worktree: ${dest}`);
28986
29093
  console.log(` branch: ${branch}`);
@@ -28988,6 +29095,7 @@ async function cmdSlice(id, flags = {}) {
28988
29095
  `
28989
29096
  Author your change there (commit as you go), then: terminalhire claim submit ${claim.id}`
28990
29097
  );
29098
+ await beatFounderPresence(working ?? claim);
28991
29099
  }
28992
29100
  async function cmdRuns(id, flags = {}) {
28993
29101
  const claims = await Promise.resolve().then(() => (init_claims(), claims_exports));
@@ -28996,7 +29104,7 @@ async function cmdRuns(id, flags = {}) {
28996
29104
  const fetchOnce = async () => {
28997
29105
  let res;
28998
29106
  try {
28999
- res = await fetch(`${CLAIM_SYNC_BASE3}/api/patch/runs`, {
29107
+ res = await fetch(`${CLAIM_SYNC_BASE4}/api/patch/runs`, {
29000
29108
  method: "POST",
29001
29109
  headers: { "Content-Type": "application/json" },
29002
29110
  body: JSON.stringify({
@@ -29144,7 +29252,7 @@ async function submitFounderPatch({ claims, claim, id, wt, flags }) {
29144
29252
  console.log("\n Submitting the patch through terminalhire...");
29145
29253
  let res;
29146
29254
  try {
29147
- res = await fetch(`${CLAIM_SYNC_BASE3}/api/patch`, {
29255
+ res = await fetch(`${CLAIM_SYNC_BASE4}/api/patch`, {
29148
29256
  method: "POST",
29149
29257
  headers: { "Content-Type": "application/json" },
29150
29258
  body: JSON.stringify(submission),
@@ -29171,7 +29279,7 @@ async function submitFounderPatch({ claims, claim, id, wt, flags }) {
29171
29279
  console.error("terminalhire claim: malformed patch response from the server.");
29172
29280
  process.exit(1);
29173
29281
  }
29174
- claims.updateClaim(id, { state: "submitted" });
29282
+ const submitted = claims.updateClaim(id, { state: "submitted" });
29175
29283
  console.log(`
29176
29284
  \u2713 Patch applied by terminalhire`);
29177
29285
  console.log(` branch: ${body.branch}`);
@@ -29179,6 +29287,7 @@ async function submitFounderPatch({ claims, claim, id, wt, flags }) {
29179
29287
  if (Array.isArray(body.touchedPaths)) console.log(` touched: ${body.touchedPaths.join(", ")}`);
29180
29288
  console.log(`
29181
29289
  Read the CI result: terminalhire claim runs ${id} --watch`);
29290
+ await beatFounderPresence(submitted ?? claim);
29182
29291
  }
29183
29292
  async function cmdSubmit(id, flags = {}) {
29184
29293
  const worktreeOverride = flags.worktree;
@@ -29666,9 +29775,18 @@ function renderAutoConsent() {
29666
29775
  console.log(" pushing the SAME score-free fields, at most once/day \u2014 until you run");
29667
29776
  console.log(" `terminalhire claim --push --revoke`.");
29668
29777
  console.log("");
29669
- console.log(" This stores a push-only credential on this machine (encrypted). It can");
29670
- console.log(" ONLY add/update your OWN dashboard rows \u2014 it can never read or delete.");
29671
- console.log(" Nothing new is sent: the payload is identical to the manual push above.");
29778
+ console.log(" It also lets a FOUNDER see, on their own posting, roughly when you were");
29779
+ console.log(" last at the keyboard on a claim of theirs (active / idle). Meaningful");
29780
+ console.log(" steps send a timestamp and nothing else \u2014 never a note, never progress,");
29781
+ console.log(" never a rating, and never on OSS claims, which stay entirely local.");
29782
+ console.log("");
29783
+ console.log(" This stores an encrypted credential on this machine. It can");
29784
+ console.log(" add/update your OWN dashboard claim mirror, register a new");
29785
+ console.log(" claim on a founder posting in your name, write that coarse");
29786
+ console.log(" presence on founder claims you own, and re-read the slice and");
29787
+ console.log(" CI results for those same claims. It can never delete, and it");
29788
+ console.log(" cannot touch anyone else's claims. The daily dashboard payload");
29789
+ console.log(" is otherwise identical to the manual push above.");
29672
29790
  console.log("");
29673
29791
  }
29674
29792
  function backgroundEnableFailed(autoConsent, pushToken) {
@@ -29717,7 +29835,7 @@ async function cmdPush({ keepUpdated = false } = {}) {
29717
29835
  console.log("\n Starting browser verification...");
29718
29836
  let begin;
29719
29837
  try {
29720
- const r = await fetch(`${CLAIM_SYNC_BASE3}/api/claim-sync/begin`, {
29838
+ const r = await fetch(`${CLAIM_SYNC_BASE4}/api/claim-sync/begin`, {
29721
29839
  method: "POST",
29722
29840
  headers: { "Content-Type": "application/json" },
29723
29841
  body: JSON.stringify({ hostname: osHostname() }),
@@ -29760,7 +29878,7 @@ async function cmdPush({ keepUpdated = false } = {}) {
29760
29878
  let statusRes;
29761
29879
  try {
29762
29880
  statusRes = await fetch(
29763
- `${CLAIM_SYNC_BASE3}/api/claim-sync/status?challenge=${encodeURIComponent(challenge)}`,
29881
+ `${CLAIM_SYNC_BASE4}/api/claim-sync/status?challenge=${encodeURIComponent(challenge)}`,
29764
29882
  { signal: AbortSignal.timeout(1e4) }
29765
29883
  );
29766
29884
  } catch {
@@ -29791,7 +29909,7 @@ async function cmdPush({ keepUpdated = false } = {}) {
29791
29909
  console.log("\n Verified. Sharing your claims...");
29792
29910
  let res;
29793
29911
  try {
29794
- res = await fetch(`${CLAIM_SYNC_BASE3}/api/claim-sync`, {
29912
+ res = await fetch(`${CLAIM_SYNC_BASE4}/api/claim-sync`, {
29795
29913
  method: "POST",
29796
29914
  headers: { "Content-Type": "application/json" },
29797
29915
  // autoConsent is included ONLY when the dev opted into background updates —
@@ -29918,7 +30036,7 @@ async function cmdRevoke() {
29918
30036
  console.log("\n Requesting deletion...");
29919
30037
  let res;
29920
30038
  try {
29921
- res = await fetch(`${CLAIM_SYNC_BASE3}/api/claim-sync`, {
30039
+ res = await fetch(`${CLAIM_SYNC_BASE4}/api/claim-sync`, {
29922
30040
  method: "DELETE",
29923
30041
  headers: { "Content-Type": "application/json" },
29924
30042
  body: JSON.stringify({ login, deleteToken }),
@@ -30223,6 +30341,7 @@ export {
30223
30341
  SUBMIT_ACCEPTS,
30224
30342
  SYNC_BACKGROUND_PUSH_ACTIVE_FIELD,
30225
30343
  backgroundEnableFailed,
30344
+ beatFounderPresence,
30226
30345
  buildAssignmentComment,
30227
30346
  buildPatchSubmission,
30228
30347
  buildStakeComment,
@@ -30259,6 +30378,7 @@ export {
30259
30378
  pickStartableClaim,
30260
30379
  printNextSteps,
30261
30380
  readCredentialDisposition,
30381
+ renderAutoConsent,
30262
30382
  renderClaimHistory,
30263
30383
  renderRunView,
30264
30384
  renderServerRefusal,
@@ -118,7 +118,7 @@ function parseNudgeMode(raw) {
118
118
  function printMixValues() {
119
119
  console.log(" Valid mix values (roles vs. contribution items on the ambient surface):");
120
120
  console.log(" jobs \u2014 more roles, fewer contributions (contribute 5, roles ~15)");
121
- console.log(" balanced \u2014 rebalanced default (contribute 8, roles ~12)");
121
+ console.log(" balanced \u2014 contribution-first default (contribute 10, roles ~10)");
122
122
  console.log(" credential \u2014 contribution-forward (contribute 12, roles ~8)");
123
123
  }
124
124
  async function run() {