terminalhire 0.29.0 → 0.30.1

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.
@@ -1706,7 +1706,152 @@ async function fetchPRScoringFacts(prUrl, token, signal, governor) {
1706
1706
  fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
1707
1707
  };
1708
1708
  }
1709
- var TRACTION_TOP_N, MAINTAINER_ENRICH_MAX, CANDIDATE_PR_PAGE, MAX_ENRICH_PRS, OPEN_PR_PAGE, TRANSIENT_META_ERROR, RESUME_DECAY_HALF_LIFE_MS, RESUME_MIN_SCORE, RECEPTIVITY_RECENCY_DAYS, RECEPTIVITY_RECENCY_FLOOR, GITHUB_GRAPHQL_URL;
1709
+ function isLifecycleBot(actor) {
1710
+ if (!actor) return false;
1711
+ if (actor.type === "Bot") return true;
1712
+ const login = String(actor.login ?? "");
1713
+ if (/\[bot\]$/i.test(login)) return true;
1714
+ return LIFECYCLE_BOT_LOGINS.has(login.toLowerCase());
1715
+ }
1716
+ async function fetchPRLifecycle(prUrl, token, signal, governor) {
1717
+ const ref = parseGitHubRef(prUrl);
1718
+ if (!ref || ref.kind !== "pull") return null;
1719
+ const { owner, repo, number } = ref;
1720
+ const sig = signal ?? AbortSignal.timeout(1e4);
1721
+ const gov = makeScoringGovernor(governor);
1722
+ if (gov.tripped() || gov.budgetExhausted()) return null;
1723
+ let pr;
1724
+ try {
1725
+ pr = await ghFetch(`/repos/${owner}/${repo}/pulls/${number}`, token, sig);
1726
+ } catch {
1727
+ return null;
1728
+ }
1729
+ const authorId = pr.user?.id ?? null;
1730
+ const events = [];
1731
+ const complete = { reviews: false, comments: false, commits: false };
1732
+ if (pr.created_at) {
1733
+ events.push({
1734
+ event_type: "pr_opened",
1735
+ source_id: `pr-${number}`,
1736
+ actor_id: authorId ?? 0,
1737
+ actor_assoc: "",
1738
+ is_author: true,
1739
+ is_bot: false,
1740
+ occurred_at: pr.created_at
1741
+ });
1742
+ }
1743
+ if (!gov.tripped() && !gov.budgetExhausted()) {
1744
+ try {
1745
+ const reviews = await ghFetch(
1746
+ `/repos/${owner}/${repo}/pulls/${number}/reviews?per_page=100`,
1747
+ token,
1748
+ sig
1749
+ );
1750
+ for (const r of reviews) {
1751
+ if (r.state === "PENDING" || !r.submitted_at) continue;
1752
+ const aid = r.user?.id ?? 0;
1753
+ events.push({
1754
+ event_type: "review_submitted",
1755
+ source_id: `review-${r.id}`,
1756
+ actor_id: aid,
1757
+ actor_assoc: r.author_association ?? "",
1758
+ is_author: authorId != null && aid === authorId,
1759
+ is_bot: isLifecycleBot(r.user),
1760
+ occurred_at: r.submitted_at
1761
+ });
1762
+ }
1763
+ complete.reviews = true;
1764
+ } catch {
1765
+ complete.reviews = false;
1766
+ }
1767
+ }
1768
+ if (!gov.tripped() && !gov.budgetExhausted()) {
1769
+ try {
1770
+ const comments = await ghFetch(
1771
+ `/repos/${owner}/${repo}/issues/${number}/comments?per_page=100`,
1772
+ token,
1773
+ sig
1774
+ );
1775
+ for (const c of comments) {
1776
+ if (!c.created_at) continue;
1777
+ const aid = c.user?.id ?? 0;
1778
+ events.push({
1779
+ event_type: "issue_comment",
1780
+ source_id: `comment-${c.id}`,
1781
+ actor_id: aid,
1782
+ actor_assoc: c.author_association ?? "",
1783
+ is_author: authorId != null && aid === authorId,
1784
+ is_bot: isLifecycleBot(c.user),
1785
+ occurred_at: c.created_at
1786
+ });
1787
+ }
1788
+ complete.comments = true;
1789
+ } catch {
1790
+ complete.comments = false;
1791
+ }
1792
+ }
1793
+ if (!gov.tripped() && !gov.budgetExhausted()) {
1794
+ try {
1795
+ const commits = await ghFetch(
1796
+ `/repos/${owner}/${repo}/pulls/${number}/commits?per_page=100`,
1797
+ token,
1798
+ sig
1799
+ );
1800
+ for (const cm of commits) {
1801
+ const when = cm.commit?.author?.date ?? null;
1802
+ if (!when) continue;
1803
+ const aid = cm.author?.id ?? authorId ?? 0;
1804
+ events.push({
1805
+ event_type: "commit_pushed",
1806
+ source_id: `commit-${cm.sha}`,
1807
+ actor_id: aid,
1808
+ actor_assoc: "",
1809
+ is_author: authorId != null && aid === authorId,
1810
+ is_bot: isLifecycleBot(cm.author),
1811
+ occurred_at: when
1812
+ });
1813
+ }
1814
+ complete.commits = true;
1815
+ } catch {
1816
+ complete.commits = false;
1817
+ }
1818
+ }
1819
+ if (pr.merged && pr.merged_at) {
1820
+ events.push({
1821
+ event_type: "pr_merged",
1822
+ source_id: `merged-${number}`,
1823
+ actor_id: pr.merged_by?.id ?? 0,
1824
+ actor_assoc: "",
1825
+ is_author: authorId != null && pr.merged_by?.id === authorId,
1826
+ // A bot merger (mergify[bot], a merge queue, etc.) must NOT count as an
1827
+ // independent human counterparty. Classify it like every other actor (§4b V4).
1828
+ is_bot: isLifecycleBot(pr.merged_by),
1829
+ occurred_at: pr.merged_at
1830
+ });
1831
+ } else if (pr.state === "closed" && pr.closed_at) {
1832
+ events.push({
1833
+ event_type: "pr_closed_unmerged",
1834
+ source_id: `closed-${number}`,
1835
+ actor_id: 0,
1836
+ actor_assoc: "",
1837
+ is_author: false,
1838
+ is_bot: false,
1839
+ occurred_at: pr.closed_at
1840
+ });
1841
+ }
1842
+ return {
1843
+ prNumber: number,
1844
+ prUrl: pr.html_url,
1845
+ openedAt: pr.created_at ?? null,
1846
+ merged: pr.merged === true,
1847
+ mergedAt: pr.merged_at ?? null,
1848
+ closedUnmergedAt: !pr.merged && pr.state === "closed" ? pr.closed_at ?? null : null,
1849
+ authorId,
1850
+ events,
1851
+ complete
1852
+ };
1853
+ }
1854
+ var TRACTION_TOP_N, MAINTAINER_ENRICH_MAX, CANDIDATE_PR_PAGE, MAX_ENRICH_PRS, OPEN_PR_PAGE, TRANSIENT_META_ERROR, RESUME_DECAY_HALF_LIFE_MS, RESUME_MIN_SCORE, RECEPTIVITY_RECENCY_DAYS, RECEPTIVITY_RECENCY_FLOOR, GITHUB_GRAPHQL_URL, LIFECYCLE_BOT_LOGINS;
1710
1855
  var init_github = __esm({
1711
1856
  "../../packages/core/src/github.ts"() {
1712
1857
  "use strict";
@@ -1726,6 +1871,20 @@ var init_github = __esm({
1726
1871
  RECEPTIVITY_RECENCY_DAYS = 180;
1727
1872
  RECEPTIVITY_RECENCY_FLOOR = 0.1;
1728
1873
  GITHUB_GRAPHQL_URL = "https://api.github.com/graphql";
1874
+ LIFECYCLE_BOT_LOGINS = /* @__PURE__ */ new Set([
1875
+ "mergify",
1876
+ "mergify[bot]",
1877
+ "bors",
1878
+ "bors[bot]",
1879
+ "kodiakhq",
1880
+ "kodiakhq[bot]",
1881
+ "dependabot",
1882
+ "dependabot[bot]",
1883
+ "renovate",
1884
+ "renovate[bot]",
1885
+ "github-actions",
1886
+ "github-actions[bot]"
1887
+ ]);
1729
1888
  }
1730
1889
  });
1731
1890
 
@@ -7807,6 +7966,7 @@ __export(src_exports, {
7807
7966
  fetchGitHubProfile: () => fetchGitHubProfile,
7808
7967
  fetchOpenExternalPRs: () => fetchOpenExternalPRs,
7809
7968
  fetchOwnedRepoTraction: () => fetchOwnedRepoTraction,
7969
+ fetchPRLifecycle: () => fetchPRLifecycle,
7810
7970
  fetchPRScoringFacts: () => fetchPRScoringFacts,
7811
7971
  fetchRepoRecency: () => fetchRepoRecency,
7812
7972
  fetchRepoReceptivity: () => fetchRepoReceptivity,
@@ -1706,7 +1706,152 @@ async function fetchPRScoringFacts(prUrl, token, signal, governor) {
1706
1706
  fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
1707
1707
  };
1708
1708
  }
1709
- var TRACTION_TOP_N, MAINTAINER_ENRICH_MAX, CANDIDATE_PR_PAGE, MAX_ENRICH_PRS, OPEN_PR_PAGE, TRANSIENT_META_ERROR, RESUME_DECAY_HALF_LIFE_MS, RESUME_MIN_SCORE, RECEPTIVITY_RECENCY_DAYS, RECEPTIVITY_RECENCY_FLOOR, GITHUB_GRAPHQL_URL;
1709
+ function isLifecycleBot(actor) {
1710
+ if (!actor) return false;
1711
+ if (actor.type === "Bot") return true;
1712
+ const login = String(actor.login ?? "");
1713
+ if (/\[bot\]$/i.test(login)) return true;
1714
+ return LIFECYCLE_BOT_LOGINS.has(login.toLowerCase());
1715
+ }
1716
+ async function fetchPRLifecycle(prUrl, token, signal, governor) {
1717
+ const ref = parseGitHubRef(prUrl);
1718
+ if (!ref || ref.kind !== "pull") return null;
1719
+ const { owner, repo, number } = ref;
1720
+ const sig = signal ?? AbortSignal.timeout(1e4);
1721
+ const gov = makeScoringGovernor(governor);
1722
+ if (gov.tripped() || gov.budgetExhausted()) return null;
1723
+ let pr;
1724
+ try {
1725
+ pr = await ghFetch(`/repos/${owner}/${repo}/pulls/${number}`, token, sig);
1726
+ } catch {
1727
+ return null;
1728
+ }
1729
+ const authorId = pr.user?.id ?? null;
1730
+ const events = [];
1731
+ const complete = { reviews: false, comments: false, commits: false };
1732
+ if (pr.created_at) {
1733
+ events.push({
1734
+ event_type: "pr_opened",
1735
+ source_id: `pr-${number}`,
1736
+ actor_id: authorId ?? 0,
1737
+ actor_assoc: "",
1738
+ is_author: true,
1739
+ is_bot: false,
1740
+ occurred_at: pr.created_at
1741
+ });
1742
+ }
1743
+ if (!gov.tripped() && !gov.budgetExhausted()) {
1744
+ try {
1745
+ const reviews = await ghFetch(
1746
+ `/repos/${owner}/${repo}/pulls/${number}/reviews?per_page=100`,
1747
+ token,
1748
+ sig
1749
+ );
1750
+ for (const r of reviews) {
1751
+ if (r.state === "PENDING" || !r.submitted_at) continue;
1752
+ const aid = r.user?.id ?? 0;
1753
+ events.push({
1754
+ event_type: "review_submitted",
1755
+ source_id: `review-${r.id}`,
1756
+ actor_id: aid,
1757
+ actor_assoc: r.author_association ?? "",
1758
+ is_author: authorId != null && aid === authorId,
1759
+ is_bot: isLifecycleBot(r.user),
1760
+ occurred_at: r.submitted_at
1761
+ });
1762
+ }
1763
+ complete.reviews = true;
1764
+ } catch {
1765
+ complete.reviews = false;
1766
+ }
1767
+ }
1768
+ if (!gov.tripped() && !gov.budgetExhausted()) {
1769
+ try {
1770
+ const comments = await ghFetch(
1771
+ `/repos/${owner}/${repo}/issues/${number}/comments?per_page=100`,
1772
+ token,
1773
+ sig
1774
+ );
1775
+ for (const c of comments) {
1776
+ if (!c.created_at) continue;
1777
+ const aid = c.user?.id ?? 0;
1778
+ events.push({
1779
+ event_type: "issue_comment",
1780
+ source_id: `comment-${c.id}`,
1781
+ actor_id: aid,
1782
+ actor_assoc: c.author_association ?? "",
1783
+ is_author: authorId != null && aid === authorId,
1784
+ is_bot: isLifecycleBot(c.user),
1785
+ occurred_at: c.created_at
1786
+ });
1787
+ }
1788
+ complete.comments = true;
1789
+ } catch {
1790
+ complete.comments = false;
1791
+ }
1792
+ }
1793
+ if (!gov.tripped() && !gov.budgetExhausted()) {
1794
+ try {
1795
+ const commits = await ghFetch(
1796
+ `/repos/${owner}/${repo}/pulls/${number}/commits?per_page=100`,
1797
+ token,
1798
+ sig
1799
+ );
1800
+ for (const cm of commits) {
1801
+ const when = cm.commit?.author?.date ?? null;
1802
+ if (!when) continue;
1803
+ const aid = cm.author?.id ?? authorId ?? 0;
1804
+ events.push({
1805
+ event_type: "commit_pushed",
1806
+ source_id: `commit-${cm.sha}`,
1807
+ actor_id: aid,
1808
+ actor_assoc: "",
1809
+ is_author: authorId != null && aid === authorId,
1810
+ is_bot: isLifecycleBot(cm.author),
1811
+ occurred_at: when
1812
+ });
1813
+ }
1814
+ complete.commits = true;
1815
+ } catch {
1816
+ complete.commits = false;
1817
+ }
1818
+ }
1819
+ if (pr.merged && pr.merged_at) {
1820
+ events.push({
1821
+ event_type: "pr_merged",
1822
+ source_id: `merged-${number}`,
1823
+ actor_id: pr.merged_by?.id ?? 0,
1824
+ actor_assoc: "",
1825
+ is_author: authorId != null && pr.merged_by?.id === authorId,
1826
+ // A bot merger (mergify[bot], a merge queue, etc.) must NOT count as an
1827
+ // independent human counterparty. Classify it like every other actor (§4b V4).
1828
+ is_bot: isLifecycleBot(pr.merged_by),
1829
+ occurred_at: pr.merged_at
1830
+ });
1831
+ } else if (pr.state === "closed" && pr.closed_at) {
1832
+ events.push({
1833
+ event_type: "pr_closed_unmerged",
1834
+ source_id: `closed-${number}`,
1835
+ actor_id: 0,
1836
+ actor_assoc: "",
1837
+ is_author: false,
1838
+ is_bot: false,
1839
+ occurred_at: pr.closed_at
1840
+ });
1841
+ }
1842
+ return {
1843
+ prNumber: number,
1844
+ prUrl: pr.html_url,
1845
+ openedAt: pr.created_at ?? null,
1846
+ merged: pr.merged === true,
1847
+ mergedAt: pr.merged_at ?? null,
1848
+ closedUnmergedAt: !pr.merged && pr.state === "closed" ? pr.closed_at ?? null : null,
1849
+ authorId,
1850
+ events,
1851
+ complete
1852
+ };
1853
+ }
1854
+ var TRACTION_TOP_N, MAINTAINER_ENRICH_MAX, CANDIDATE_PR_PAGE, MAX_ENRICH_PRS, OPEN_PR_PAGE, TRANSIENT_META_ERROR, RESUME_DECAY_HALF_LIFE_MS, RESUME_MIN_SCORE, RECEPTIVITY_RECENCY_DAYS, RECEPTIVITY_RECENCY_FLOOR, GITHUB_GRAPHQL_URL, LIFECYCLE_BOT_LOGINS;
1710
1855
  var init_github = __esm({
1711
1856
  "../../packages/core/src/github.ts"() {
1712
1857
  "use strict";
@@ -1726,6 +1871,20 @@ var init_github = __esm({
1726
1871
  RECEPTIVITY_RECENCY_DAYS = 180;
1727
1872
  RECEPTIVITY_RECENCY_FLOOR = 0.1;
1728
1873
  GITHUB_GRAPHQL_URL = "https://api.github.com/graphql";
1874
+ LIFECYCLE_BOT_LOGINS = /* @__PURE__ */ new Set([
1875
+ "mergify",
1876
+ "mergify[bot]",
1877
+ "bors",
1878
+ "bors[bot]",
1879
+ "kodiakhq",
1880
+ "kodiakhq[bot]",
1881
+ "dependabot",
1882
+ "dependabot[bot]",
1883
+ "renovate",
1884
+ "renovate[bot]",
1885
+ "github-actions",
1886
+ "github-actions[bot]"
1887
+ ]);
1729
1888
  }
1730
1889
  });
1731
1890
 
@@ -7807,6 +7966,7 @@ __export(src_exports, {
7807
7966
  fetchGitHubProfile: () => fetchGitHubProfile,
7808
7967
  fetchOpenExternalPRs: () => fetchOpenExternalPRs,
7809
7968
  fetchOwnedRepoTraction: () => fetchOwnedRepoTraction,
7969
+ fetchPRLifecycle: () => fetchPRLifecycle,
7810
7970
  fetchPRScoringFacts: () => fetchPRScoringFacts,
7811
7971
  fetchRepoRecency: () => fetchRepoRecency,
7812
7972
  fetchRepoReceptivity: () => fetchRepoReceptivity,
@@ -8523,13 +8683,153 @@ var init_profile = __esm({
8523
8683
  }
8524
8684
  });
8525
8685
 
8686
+ // src/reputation/identity.ts
8687
+ var identity_exports = {};
8688
+ __export(identity_exports, {
8689
+ resolveCallerIdentity: () => resolveCallerIdentity
8690
+ });
8691
+ import { execFile } from "child_process";
8692
+ import { promisify } from "util";
8693
+ async function resolveCallerIdentity() {
8694
+ try {
8695
+ const { stdout } = await execFileAsync("gh", ["api", "user"], { timeout: 1e4 });
8696
+ const u = JSON.parse(stdout);
8697
+ if (typeof u.id === "number" && typeof u.login === "string" && u.login.length > 0) {
8698
+ return { id: u.id, login: u.login };
8699
+ }
8700
+ return null;
8701
+ } catch {
8702
+ return null;
8703
+ }
8704
+ }
8705
+ var execFileAsync;
8706
+ var init_identity = __esm({
8707
+ "src/reputation/identity.ts"() {
8708
+ "use strict";
8709
+ execFileAsync = promisify(execFile);
8710
+ }
8711
+ });
8712
+
8713
+ // src/reputation/fetch.ts
8714
+ var fetch_exports = {};
8715
+ __export(fetch_exports, {
8716
+ gatherAudit: () => gatherAudit
8717
+ });
8718
+ async function gatherAudit(prUrl, token) {
8719
+ const signal = AbortSignal.timeout(2e4);
8720
+ const governor = makeScoringGovernor();
8721
+ const [lifecycle, facts] = await Promise.all([
8722
+ fetchPRLifecycle(prUrl, token, signal, governor),
8723
+ fetchPRScoringFacts(prUrl, token, signal, governor)
8724
+ ]);
8725
+ return { lifecycle, facts };
8726
+ }
8727
+ var init_fetch = __esm({
8728
+ "src/reputation/fetch.ts"() {
8729
+ "use strict";
8730
+ init_src();
8731
+ }
8732
+ });
8733
+
8734
+ // src/reputation/audit.ts
8735
+ var audit_exports = {};
8736
+ __export(audit_exports, {
8737
+ buildAuditView: () => buildAuditView
8738
+ });
8739
+ function roleOf(e) {
8740
+ if (e.is_bot) return "bot";
8741
+ if (e.is_author) return "you";
8742
+ const a = String(e.actor_assoc || "").toUpperCase();
8743
+ if (MAINTAINER_ASSOC.has(a)) return "maintainer";
8744
+ if (a === "CONTRIBUTOR") return "contributor";
8745
+ if (a === "NONE") return "outside";
8746
+ return "unknown";
8747
+ }
8748
+ function isMaintainerResponse(e) {
8749
+ return !e.is_author && !e.is_bot && MAINTAINER_ASSOC.has(String(e.actor_assoc || "").toUpperCase());
8750
+ }
8751
+ function byTime(a, b) {
8752
+ if (a.occurred_at < b.occurred_at) return -1;
8753
+ if (a.occurred_at > b.occurred_at) return 1;
8754
+ if (a.source_id < b.source_id) return -1;
8755
+ if (a.source_id > b.source_id) return 1;
8756
+ return 0;
8757
+ }
8758
+ function hoursBetween(a, b) {
8759
+ if (!a || !b) return null;
8760
+ const ta = Date.parse(a);
8761
+ const tb = Date.parse(b);
8762
+ if (Number.isNaN(ta) || Number.isNaN(tb)) return null;
8763
+ return Math.round((tb - ta) / 36e5 * 10) / 10;
8764
+ }
8765
+ function buildAuditView(lifecycle, facts) {
8766
+ const events = [...lifecycle.events].sort(byTime);
8767
+ const counterpartyIds = /* @__PURE__ */ new Set();
8768
+ for (const e of events) {
8769
+ if (!e.is_author && !e.is_bot && e.actor_id !== 0) counterpartyIds.add(e.actor_id);
8770
+ }
8771
+ const firstResponse = events.find(isMaintainerResponse) ?? null;
8772
+ const firstResponseAt = firstResponse?.occurred_at ?? null;
8773
+ const maintainerReviews = events.filter(
8774
+ (e) => e.event_type === "review_submitted" && isMaintainerResponse(e)
8775
+ ).length;
8776
+ const iterationsAfterFirstResponse = firstResponseAt ? events.filter(
8777
+ (e) => e.event_type === "commit_pushed" && e.is_author && e.occurred_at > firstResponseAt
8778
+ ).length : 0;
8779
+ const outcome = lifecycle.merged ? "merged" : lifecycle.closedUnmergedAt ? "closed_unmerged" : "open";
8780
+ const signals = {
8781
+ distinctCounterparties: counterpartyIds.size,
8782
+ maintainerReviews,
8783
+ iterationsAfterFirstResponse,
8784
+ hoursToFirstResponse: hoursBetween(lifecycle.openedAt, firstResponseAt),
8785
+ hoursToMerge: hoursBetween(lifecycle.openedAt, lifecycle.mergedAt)
8786
+ };
8787
+ const timeline = events.map((e) => ({
8788
+ at: e.occurred_at,
8789
+ event: e.event_type,
8790
+ role: roleOf(e)
8791
+ }));
8792
+ const notes = [];
8793
+ if (!lifecycle.complete.reviews) notes.push("Review data incomplete \u2014 review counts may undercount.");
8794
+ if (!lifecycle.complete.comments) notes.push("Comment data incomplete \u2014 response detection may be partial.");
8795
+ if (!lifecycle.complete.commits) notes.push("Commit data incomplete \u2014 iteration count may undercount.");
8796
+ if (lifecycle.authorId == null) notes.push("PR author identity unavailable \u2014 author/counterparty split may be imprecise.");
8797
+ return {
8798
+ repo: facts?.repo ?? "",
8799
+ prNumber: lifecycle.prNumber,
8800
+ prUrl: lifecycle.prUrl,
8801
+ openedAt: lifecycle.openedAt,
8802
+ outcome,
8803
+ mergedAt: lifecycle.mergedAt,
8804
+ closedUnmergedAt: lifecycle.closedUnmergedAt,
8805
+ mergedByLogin: facts?.mergedByLogin ?? null,
8806
+ signals,
8807
+ timeline,
8808
+ completeness: { ...lifecycle.complete },
8809
+ notes,
8810
+ limitations: [...AUDIT_LIMITATIONS]
8811
+ };
8812
+ }
8813
+ var MAINTAINER_ASSOC, AUDIT_LIMITATIONS;
8814
+ var init_audit = __esm({
8815
+ "src/reputation/audit.ts"() {
8816
+ "use strict";
8817
+ MAINTAINER_ASSOC = /* @__PURE__ */ new Set(["OWNER", "MEMBER", "COLLABORATOR"]);
8818
+ AUDIT_LIMITATIONS = [
8819
+ "Post-merge bug/revert rate \u2014 longitudinal; it only shows up after the merge this audit stops at.",
8820
+ "Downstream adoption / real-world impact \u2014 a repo-level, slow signal not visible in one PR timeline.",
8821
+ "Reviewer satisfaction and collaboration quality \u2014 not observable from public timeline events."
8822
+ ];
8823
+ }
8824
+ });
8825
+
8526
8826
  // bin/jpi-claim.js
8527
8827
  init_src();
8528
8828
  import { readFileSync as readFileSync7, writeFileSync as writeFileSync6, mkdirSync as mkdirSync6, existsSync as existsSync6, rmSync as rmSync3 } from "fs";
8529
8829
  import { join as join7 } from "path";
8530
8830
  import { homedir as homedir6, hostname as osHostname } from "os";
8531
- import { execFile } from "child_process";
8532
- import { promisify } from "util";
8831
+ import { execFile as execFile2 } from "child_process";
8832
+ import { promisify as promisify2 } from "util";
8533
8833
  import { createInterface } from "readline";
8534
8834
 
8535
8835
  // src/open-url.js
@@ -8722,7 +9022,7 @@ function printPolicySection(policy) {
8722
9022
  console.log(" policy: no AI-assistance policy language detected in CONTRIBUTING/PR-template/AGENTS docs");
8723
9023
  }
8724
9024
  }
8725
- var pExecFile = promisify(execFile);
9025
+ var pExecFile = promisify2(execFile2);
8726
9026
  async function sh(cmd, args, opts = {}) {
8727
9027
  const { stdout } = await pExecFile(cmd, args, { ...opts, shell: false, maxBuffer: 16 * 1024 * 1024 });
8728
9028
  return String(stdout).trim();
@@ -10253,6 +10553,100 @@ async function cmdRevoke() {
10253
10553
  console.log("\n \u2713 Pushed claims deleted and local marker cleared.\n");
10254
10554
  console.log(" Background updates (if any) have been stopped.\n");
10255
10555
  }
10556
+ function renderAudit(view) {
10557
+ const roleLabel = {
10558
+ you: "you",
10559
+ maintainer: "maintainer",
10560
+ contributor: "contributor",
10561
+ outside: "outside",
10562
+ bot: "bot",
10563
+ unknown: "\u2014"
10564
+ };
10565
+ const eventLabel = {
10566
+ pr_opened: "opened PR",
10567
+ review_submitted: "reviewed",
10568
+ issue_comment: "commented",
10569
+ commit_pushed: "pushed commit",
10570
+ pr_merged: "merged",
10571
+ pr_closed_unmerged: "closed (unmerged)"
10572
+ };
10573
+ const outcomeLabel = {
10574
+ merged: "\u2714 merged",
10575
+ closed_unmerged: "\u2715 closed without merge",
10576
+ open: "\u2026 still open"
10577
+ };
10578
+ const hrs = (h) => h == null ? "\u2014" : `${h}h`;
10579
+ console.log(`
10580
+ Audit \u2014 ${view.repo || "repo"} #${view.prNumber}`);
10581
+ console.log(` ${view.prUrl}`);
10582
+ console.log(` Outcome: ${outcomeLabel[view.outcome] ?? view.outcome}` + (view.mergedByLogin ? ` (merged by @${view.mergedByLogin})` : ""));
10583
+ console.log("\n Engagement (raw counts \u2014 not a score):");
10584
+ console.log(` Independent counterparties engaged : ${view.signals.distinctCounterparties}`);
10585
+ console.log(` Maintainer reviews : ${view.signals.maintainerReviews}`);
10586
+ console.log(` Your commits after first response : ${view.signals.iterationsAfterFirstResponse}`);
10587
+ console.log(` Time to first maintainer response : ${hrs(view.signals.hoursToFirstResponse)}`);
10588
+ console.log(` Time to merge : ${hrs(view.signals.hoursToMerge)}`);
10589
+ console.log("\n Timeline:");
10590
+ if (view.timeline.length === 0) {
10591
+ console.log(" (no events captured)");
10592
+ } else {
10593
+ for (const e of view.timeline) {
10594
+ console.log(` ${e.at} ${roleLabel[e.role] ?? e.role} ${eventLabel[e.event] ?? e.event}`);
10595
+ }
10596
+ }
10597
+ if (view.notes.length > 0) {
10598
+ console.log("\n Notes:");
10599
+ for (const n of view.notes) console.log(` \u26A0 ${n}`);
10600
+ }
10601
+ console.log("\n What this does NOT measure:");
10602
+ for (const l of view.limitations || []) console.log(` \xB7 ${l}`);
10603
+ console.log("");
10604
+ }
10605
+ async function cmdAudit(id, flags = {}) {
10606
+ if (!id) {
10607
+ console.error("terminalhire claim audit: needs a claim id. Try `terminalhire claim list`.");
10608
+ process.exit(1);
10609
+ }
10610
+ const claims = await Promise.resolve().then(() => (init_claims(), claims_exports));
10611
+ const claim = claims.findClaim(id);
10612
+ if (!claim) {
10613
+ console.error(`No claim with id '${id}'.`);
10614
+ process.exit(1);
10615
+ }
10616
+ if (!claim.prUrl) {
10617
+ console.log(`Claim '${id}' has no submitted PR yet \u2014 nothing to audit. Submit first: terminalhire claim submit ${id}`);
10618
+ return;
10619
+ }
10620
+ const { resolveCallerIdentity: resolveCallerIdentity2 } = await Promise.resolve().then(() => (init_identity(), identity_exports));
10621
+ const identity = await resolveCallerIdentity2();
10622
+ if (!identity) {
10623
+ console.error("terminalhire claim audit: could not verify your GitHub identity (`gh api user`). Run `gh auth login` and retry.");
10624
+ process.exit(1);
10625
+ }
10626
+ let token;
10627
+ try {
10628
+ token = await sh("gh", ["auth", "token"]);
10629
+ } catch {
10630
+ token = void 0;
10631
+ }
10632
+ const { gatherAudit: gatherAudit2 } = await Promise.resolve().then(() => (init_fetch(), fetch_exports));
10633
+ const { lifecycle, facts } = await gatherAudit2(claim.prUrl, token || void 0);
10634
+ if (!lifecycle) {
10635
+ console.error(`terminalhire claim audit: could not fetch the PR lifecycle for ${claim.prUrl} (private, deleted, rate-limited, or not a PR).`);
10636
+ process.exit(1);
10637
+ }
10638
+ if (lifecycle.authorId !== identity.id) {
10639
+ console.error(`terminalhire claim audit: PR ${claim.prUrl} was not authored by you (@${identity.login}); the audit only covers your own PRs.`);
10640
+ process.exit(1);
10641
+ }
10642
+ const { buildAuditView: buildAuditView2 } = await Promise.resolve().then(() => (init_audit(), audit_exports));
10643
+ const view = buildAuditView2(lifecycle, facts);
10644
+ if (flags.json) {
10645
+ console.log(JSON.stringify(view, null, 2));
10646
+ return;
10647
+ }
10648
+ renderAudit(view);
10649
+ }
10256
10650
  async function run() {
10257
10651
  const verb = process.argv[2];
10258
10652
  const rawArgs = process.argv.slice(3).map((a) => a === "-y" ? "--yes" : a);
@@ -10303,8 +10697,11 @@ async function run() {
10303
10697
  case "release":
10304
10698
  await cmdRelease(positional[0]);
10305
10699
  break;
10700
+ case "audit":
10701
+ await cmdAudit(positional[0], flags);
10702
+ break;
10306
10703
  default:
10307
- console.error(`terminalhire claim: unknown verb '${verb ?? ""}'. Expected: preview | record | start | attach | list | status | update | submit | release`);
10704
+ console.error(`terminalhire claim: unknown verb '${verb ?? ""}'. Expected: preview | record | start | attach | list | status | update | submit | audit | release`);
10308
10705
  process.exit(1);
10309
10706
  }
10310
10707
  } catch (err) {