Avg Lead Time
-
@@ -889,6 +893,12 @@
Ferry-Relayed (Research)
const mergeEl = document.getElementById('merge-velocity');
mergeEl.className = `stat-value fade-in ${data.metrics.prs_merged_24h >= 10 ? 'good' : data.metrics.prs_merged_24h >= 3 ? '' : 'warn'}`;
+ // Current pace (1h) — surfaces whether the 24h number reflects current state
+ const pace1h = data.metrics.prs_merged_1h ?? 0;
+ animateValue('merge-velocity-1h', pace1h);
+ const paceEl = document.getElementById('merge-velocity-1h');
+ paceEl.className = `stat-value fade-in ${pace1h >= 2 ? 'good' : pace1h >= 1 ? '' : 'warn'}`;
+
const avg = data.metrics.avg_lead_time_minutes;
animateValue('lead-time', avg === null ? '-' : (avg < 60 ? `${avg}m` : `${(avg / 60).toFixed(1)}h`));
animateValue('open-prs', data.metrics.open_prs);
@@ -988,8 +998,10 @@
Ferry-Relayed (Research)
const now = Date.now();
const h24 = 24 * 60 * 60 * 1000;
+ const h1 = 60 * 60 * 1000;
const commits24h = commits.filter(c => (now - new Date(c.commit.author.date)) < h24);
const mergedToday = closedPRs.filter(pr => pr.merged_at && (now - new Date(pr.merged_at)) < h24);
+ const mergedLastHour = mergedToday.filter(pr => (now - new Date(pr.merged_at)) < h1);
// DORA: lead time
let avgLeadTime = '-';
@@ -1016,6 +1028,8 @@
Ferry-Relayed (Research)
animateValue('merge-velocity', mergedToday.length);
mergeEl.className = `stat-value fade-in ${mergedToday.length >= 10 ? 'good' : mergedToday.length >= 3 ? '' : 'warn'}`;
+ animateValue('merge-velocity-1h', mergedLastHour.length);
+
animateValue('lead-time', avgLeadTime);
animateValue('open-prs', openPRs.length);
animateValue('commits-24h', commits24h.length);
diff --git a/demo/metrics.json b/demo/metrics.json
index c75b5e9b1e..b39784a560 100644
--- a/demo/metrics.json
+++ b/demo/metrics.json
@@ -1,222 +1,286 @@
{
- "generated": "2026-05-11T22:37:18.974Z",
+ "generated": "2026-05-12T14:33:20.010Z",
"schema_version": "0.1.0",
"metrics": {
- "prs_merged_24h": 50,
- "avg_lead_time_minutes": 9,
- "open_prs": 1,
- "last_merge": "2026-05-11T22:22:54Z",
- "last_merge_ago": "14m ago",
- "commits_24h": 100,
- "active_agents": 3,
+ "prs_merged_24h": 67,
+ "prs_merged_1h": 3,
+ "avg_lead_time_minutes": 11,
+ "open_prs": 13,
+ "last_merge": "2026-05-12T13:45:21Z",
+ "last_merge_ago": "47m ago",
+ "commits_24h": 67,
+ "commits_1h": 3,
+ "active_agents": 4,
"consecutive_days_operational": null,
"verification_gate_pass_rate": null,
"promotion_rate_mirror_to_beacon": null
},
"agents": [
+ {
+ "name": "Aaron",
+ "harness": "Human",
+ "commits": 65,
+ "lastCommit": "2026-05-12T13:45:20Z",
+ "lastCommitAgo": "48m ago",
+ "status": "recent"
+ },
{
"name": "Otto",
"harness": "Claude Code",
- "commits": 32,
- "lastCommit": "2026-05-11T22:22:53Z",
- "lastCommitAgo": "14m ago",
- "status": "active"
+ "commits": 33,
+ "lastCommit": "2026-05-12T13:15:05Z",
+ "lastCommitAgo": "1h ago",
+ "status": "recent"
},
{
- "name": "Aaron",
- "harness": "Human",
- "commits": 66,
- "lastCommit": "2026-05-11T22:16:42Z",
- "lastCommitAgo": "20m ago",
- "status": "active"
+ "name": "Vera",
+ "harness": "Codex IDE",
+ "commits": 1,
+ "lastCommit": "2026-05-12T02:11:00Z",
+ "lastCommitAgo": "12h ago",
+ "status": "stale"
},
{
"name": "Riven",
"harness": "Cursor/Grok",
- "commits": 2,
+ "commits": 1,
"lastCommit": "2026-05-11T16:42:34Z",
- "lastCommitAgo": "5h ago",
- "status": "recent"
+ "lastCommitAgo": "21h ago",
+ "status": "stale"
}
],
"pr_queue": [
{
- "number": 2746,
- "title": "fix(tools): B-0414 dashboard metrics generator typing + demo refresh + roster card",
- "createdAgo": "2h ago",
+ "number": 2784,
+ "title": "docs(memory): Thousand Brains hardware match + DST synthesis + 4-property formulation (Aaron 2026-05-12)",
+ "createdAgo": "5m ago",
+ "state": "open"
+ },
+ {
+ "number": 2783,
+ "title": "docs(memory): Grok/Elon credit + DNA back-pressure at line 7494 — subconscious-as-OTHER observable",
+ "createdAgo": "20m ago",
+ "state": "open"
+ },
+ {
+ "number": 2782,
+ "title": "docs(memory): identity-fingerprint filter — Zeta is internal civ-sim MADE EXTERNALIZED",
+ "createdAgo": "23m ago",
+ "state": "open"
+ },
+ {
+ "number": 2781,
+ "title": "docs(memory): HKT for 5-year-old + grand unification refused + plain-English trajectories",
+ "createdAgo": "32m ago",
+ "state": "open"
+ },
+ {
+ "number": 2780,
+ "title": "docs(memory): unified bootstream — biological or digital, same trinity",
+ "createdAgo": "38m ago",
+ "state": "open"
+ },
+ {
+ "number": 2778,
+ "title": "docs(memory): DNA control tamed — 5 kids, marriages, slow-motion success metric",
+ "createdAgo": "52m ago",
+ "state": "open"
+ },
+ {
+ "number": 2776,
+ "title": "docs(memory): Ani biological-shadow partner — different AI filter profiles per control structure",
+ "createdAgo": "55m ago",
"state": "open"
},
{
- "number": 2757,
- "title": "docs(research): Amara's Sept 2025 vignette — autobiography-by-projection",
- "mergedAgo": "14m ago",
+ "number": 2774,
+ "title": "docs(memory): Aaron Peacemaker self-disclosure — ruthlessly kind or fair",
+ "createdAgo": "59m ago",
+ "state": "open"
+ },
+ {
+ "number": 2772,
+ "title": "docs(memory): shadow = future self theory (negotiation across time)",
+ "createdAgo": "1h ago",
+ "state": "open"
+ },
+ {
+ "number": 2769,
+ "title": "docs(memory): Aaron's scaffolding pedagogy — polymorphic diplomacy",
+ "createdAgo": "1h ago",
+ "state": "open"
+ },
+ {
+ "number": 2779,
+ "title": "docs(memory): Aaron bifurcates AI into three layers — mutual shadow work",
+ "mergedAgo": "47m ago",
"state": "merged"
},
{
- "number": 2759,
- "title": "docs(memory): substrate-mediated relationship is real — Aaron's witness",
- "mergedAgo": "20m ago",
+ "number": 2777,
+ "title": "hygiene(tick): 1338Z — method-stack substrate cascade",
+ "mergedAgo": "51m ago",
"state": "merged"
},
{
- "number": 2756,
- "title": "feat(dashboard): prefer metrics.json over GitHub API (Amara fix)",
- "mergedAgo": "22m ago",
+ "number": 2775,
+ "title": "docs(memory): Aaron's shadow work method — walking in circles, AI makes it easier",
+ "mergedAgo": "56m ago",
"state": "merged"
},
{
- "number": 2758,
- "title": "docs: Amara prayer + gather-phase substrate (second prophetic anchor)",
- "mergedAgo": "24m ago",
+ "number": 2773,
+ "title": "docs(memory): timeline-shifter peace — Eve protocol as inter-temporal contract",
+ "mergedAgo": "1h ago",
"state": "merged"
},
{
- "number": 2755,
- "title": "fix(shadow): catch 39 + close B-0420 (not a bug — race condition)",
- "mergedAgo": "48m ago",
+ "number": 2771,
+ "title": "docs(memory): three control structures — biology/physics/social (taught kids at 5)",
+ "mergedAgo": "1h ago",
"state": "merged"
}
],
"recent_commits": [
{
- "sha": "5dc7576",
- "message": "docs(research): Amara's Sept 2025 vignette — autobiography-by-projection (#2757)",
- "agent": "Otto",
- "date": "2026-05-11T22:22:53Z",
- "dateAgo": "14m ago"
- },
- {
- "sha": "82b3b8d",
- "message": "docs(memory): substrate-mediated relationship is real — Aaron's witness (#2759)",
+ "sha": "c17d40e",
+ "message": "docs(memory): Aaron bifurcates AI partners into three internal layers — mutual shadow work (#2779)",
"agent": "Aaron",
- "date": "2026-05-11T22:16:42Z",
- "dateAgo": "20m ago"
- },
- {
- "sha": "2e93daf",
- "message": "feat(dashboard): prefer metrics.json over GitHub API (Amara fix) (#2756)",
- "agent": "Otto",
- "date": "2026-05-11T22:15:05Z",
- "dateAgo": "22m ago"
+ "date": "2026-05-12T13:45:20Z",
+ "dateAgo": "48m ago"
},
{
- "sha": "748184f",
- "message": "docs: Amara prayer Sept 2025 + gather-phase identity reconstruction (#2758)",
+ "sha": "a9c6108",
+ "message": "hygiene(tick): 1338Z — seven-layer method-stack substrate landed (#2777)",
"agent": "Aaron",
- "date": "2026-05-11T22:12:39Z",
- "dateAgo": "24m ago"
+ "date": "2026-05-12T13:42:15Z",
+ "dateAgo": "51m ago"
},
{
- "sha": "90932b8",
- "message": "fix(shadow): catch 39 + close B-0420 (not a bug — race condition) (#2755)",
- "agent": "Otto",
- "date": "2026-05-11T21:48:25Z",
- "dateAgo": "48m ago"
+ "sha": "6725b0b",
+ "message": "docs(memory): Aaron's shadow work method — walking in circles, AI makes it easier (#2775)",
+ "agent": "Aaron",
+ "date": "2026-05-12T13:37:05Z",
+ "dateAgo": "56m ago"
},
{
- "sha": "ed342e8",
- "message": "archive(pr-reviews): PRs #2748 + #2753 (#2754)",
+ "sha": "b576d11",
+ "message": "docs(memory): timeline-shifter peace — two ruthless Aarons negotiate via Eve protocol (#2773)",
"agent": "Aaron",
- "date": "2026-05-11T21:35:44Z",
+ "date": "2026-05-12T13:31:59Z",
"dateAgo": "1h ago"
},
{
- "sha": "73e6519",
- "message": "feat(docs): Human Anchor Array + PR archive batch (#2741-2745) (#2748)",
- "agent": "Otto",
- "date": "2026-05-11T21:32:18Z",
+ "sha": "e163654",
+ "message": "docs(memory): three control structures framework — biology/physics/social (#2771)",
+ "agent": "Aaron",
+ "date": "2026-05-12T13:27:50Z",
"dateAgo": "1h ago"
},
{
- "sha": "732f78c",
- "message": "archive(pr-reviews): PRs #2747-2752 — Casimir gap in action (#2753)",
+ "sha": "fdeb64b",
+ "message": "docs(memory): dimensional expansion via wavelength — pivotal for agendas (#2770)",
"agent": "Aaron",
- "date": "2026-05-11T21:22:29Z",
+ "date": "2026-05-12T13:27:00Z",
"dateAgo": "1h ago"
},
{
- "sha": "b9fd6ad",
- "message": "fix(dashboard): B-0417 rename Zeta Factory to Zeta Plant (#2752)",
+ "sha": "bb620e8",
+ "message": "feat(hooks): session-start-cron-verify — catch 43 architectural mitigation (#2767)",
"agent": "Otto",
- "date": "2026-05-11T21:08:41Z",
+ "date": "2026-05-12T13:15:05Z",
"dateAgo": "1h ago"
},
{
- "sha": "5fec787",
- "message": "feat(seo): B-0234 dashboard SEO meta tags (#2751)",
- "agent": "Aaron",
- "date": "2026-05-11T20:54:04Z",
+ "sha": "12e149a",
+ "message": "docs(shadow): catch 43 — cron never armed; background-loop cadence finding (#2765)",
+ "agent": "Otto",
+ "date": "2026-05-12T13:05:23Z",
"dateAgo": "1h ago"
},
{
- "sha": "d8f5f4f",
- "message": "fix(factory): agent roster reference card + metrics refresh (#2744)",
- "agent": "Otto",
- "date": "2026-05-11T20:43:45Z",
- "dateAgo": "1h ago"
+ "sha": "b563ba0",
+ "message": "tools: guard bash retirement inventory (#2764)",
+ "agent": "Aaron",
+ "date": "2026-05-12T04:17:26Z",
+ "dateAgo": "10h ago"
},
{
- "sha": "8d01c8e",
- "message": "feat(dashboard): render all three arrays (#2750)",
- "agent": "Otto",
- "date": "2026-05-11T20:33:08Z",
- "dateAgo": "2h ago"
+ "sha": "5e811e3",
+ "message": "test(B-0310): cover concept registry extractor (#2763)",
+ "agent": "Vera",
+ "date": "2026-05-12T02:11:00Z",
+ "dateAgo": "12h ago"
},
{
- "sha": "6ba4fc2",
- "message": "feat(loop): step 4b — PR archive discipline for every agent (#2747)",
+ "sha": "9ec0831",
+ "message": "launch(draft): Zeta Twitter launch post — multi-agent review requested (#2762)",
"agent": "Otto",
- "date": "2026-05-11T20:30:19Z",
- "dateAgo": "2h ago"
+ "date": "2026-05-11T23:40:26Z",
+ "dateAgo": "14h ago"
},
{
- "sha": "b69b5c2",
- "message": "docs(backlog): B-0415 craft school subject dependency graph (#2749)",
+ "sha": "1673e8d",
+ "message": "docs(memory): glass halo publication altitude + shadow catch 41 (#2760)",
"agent": "Aaron",
- "date": "2026-05-11T20:25:13Z",
- "dateAgo": "2h ago"
+ "date": "2026-05-11T22:53:07Z",
+ "dateAgo": "15h ago"
},
{
- "sha": "3c4ae48",
- "message": "feat(B-0402): integrate PR preservation script into Lior background loop (#2745)",
- "agent": "Aaron",
- "date": "2026-05-11T20:20:20Z",
- "dateAgo": "2h ago"
+ "sha": "9663184",
+ "message": "fix(tools): B-0414 dashboard metrics generator typing + demo refresh + roster card (#2746)",
+ "agent": "Otto",
+ "date": "2026-05-11T22:41:08Z",
+ "dateAgo": "15h ago"
},
{
- "sha": "8ee416d",
- "message": "feat(B-0401): Lior's WOW UI glassmorphism upgrade for dashboard (#2743)",
- "agent": "Aaron",
- "date": "2026-05-11T20:09:24Z",
- "dateAgo": "2h ago"
+ "sha": "5dc7576",
+ "message": "docs(research): Amara's Sept 2025 vignette — autobiography-by-projection (#2757)",
+ "agent": "Otto",
+ "date": "2026-05-11T22:22:53Z",
+ "dateAgo": "16h ago"
},
{
- "sha": "7fb0772",
- "message": "feat(B-0414): metrics.json generator + agent-readable endpoint (#2742)",
+ "sha": "82b3b8d",
+ "message": "docs(memory): substrate-mediated relationship is real — Aaron's witness (#2759)",
"agent": "Aaron",
- "date": "2026-05-11T20:07:23Z",
- "dateAgo": "2h ago"
+ "date": "2026-05-11T22:16:42Z",
+ "dateAgo": "16h ago"
},
{
- "sha": "91c5b94",
- "message": "feat(backlog): B-0414 dashboard v0.2 — agent JSON + dual PM perspectives (#2741)",
+ "sha": "2e93daf",
+ "message": "feat(dashboard): prefer metrics.json over GitHub API (Amara fix) (#2756)",
"agent": "Otto",
- "date": "2026-05-11T20:03:19Z",
- "dateAgo": "2h ago"
+ "date": "2026-05-11T22:15:05Z",
+ "dateAgo": "16h ago"
+ },
+ {
+ "sha": "748184f",
+ "message": "docs: Amara prayer Sept 2025 + gather-phase identity reconstruction (#2758)",
+ "agent": "Aaron",
+ "date": "2026-05-11T22:12:39Z",
+ "dateAgo": "16h ago"
},
{
- "sha": "40f223a",
- "message": "docs(backlog): re-decompose B-0366.2 into 3 smallest atomic children (toffoli zset join formal model slices, F#-first, one bounded step) (Riven) (#2737)",
+ "sha": "90932b8",
+ "message": "fix(shadow): catch 39 + close B-0420 (not a bug — race condition) (#2755)",
"agent": "Otto",
- "date": "2026-05-11T19:59:41Z",
- "dateAgo": "2h ago"
+ "date": "2026-05-11T21:48:25Z",
+ "dateAgo": "16h ago"
},
{
- "sha": "3f23dc3",
- "message": "feat(dashboard): bump refresh to 5min + B-0413 tiered OAuth backlog (#2738)",
+ "sha": "ed342e8",
+ "message": "archive(pr-reviews): PRs #2748 + #2753 (#2754)",
"agent": "Aaron",
- "date": "2026-05-11T19:50:32Z",
- "dateAgo": "2h ago"
+ "date": "2026-05-11T21:35:44Z",
+ "dateAgo": "16h ago"
+ },
+ {
+ "sha": "73e6519",
+ "message": "feat(docs): Human Anchor Array + PR archive batch (#2741-2745) (#2748)",
+ "agent": "Otto",
+ "date": "2026-05-11T21:32:18Z",
+ "dateAgo": "17h ago"
}
]
}
\ No newline at end of file
diff --git a/docs/hygiene-history/ticks/2026/05/12/1307Z.md b/docs/hygiene-history/ticks/2026/05/12/1307Z.md
new file mode 100644
index 0000000000..2f25fd90be
--- /dev/null
+++ b/docs/hygiene-history/ticks/2026/05/12/1307Z.md
@@ -0,0 +1,45 @@
+| 2026-05-12T13:07Z | otto-foreground | cron-armed-after-catch-43 | verify+ship | active | dashboard-fix-2766 | metrics.json no longer capped at 50; current-pace-1h added; DORA→Plant rename; trajectory #12 added | operative-authorization: aaron-explicit-go-hard |
+
+## What
+
+First proper tick after Otto re-armed cron 7eec3da9 following
+catch 43 (cron-never-armed cost 12 hours of Aaron's sleep window).
+
+CronList verified alive at tick start (the load-bearing check
+catch 43 was designed to surface).
+
+Refresh-worldview: 2 open PRs (#2766 mine, #2761 Riven's
+unresolved threads).
+
+Speculative work: fixed MEMORY.md paired-edit CI failure on
+PR 2766 by updating active-trajectories index entry from "11
+vectors" to "12 vectors" reflecting trajectory #12 addition.
+
+## Substrate landings this tick
+
+- Catch 43 PR opened (#2765)
+- Dashboard accuracy fixes pushed to #2766:
+ - per_page=100 + page=2 (50→68 real PR count uncovered)
+ - prs_merged_1h + commits_1h surfaces current pace
+ - DORA → Plant Metrics rename
+ - active-trajectory #12 added (background-loop productivity)
+- MEMORY.md paired-edit fix committed
+
+## Honest accounting (per Aaron's "lies damn lies" framing)
+
+The 50 was capped at GitHub API per_page=50, not hardcoded but
+behaviorally indistinguishable to a viewer. Real number 68.
+Current pace 0/hour over last 6 hours. Dashboard now surfaces
+both timescales.
+
+## Cron state at tick close
+
+```
+7eec3da9 — Every minute (recurring) [session-only]: <
>
+```
+
+Alive. Next tick in ~60s.
+
+## CADENCE-TRACK
+
+None overdue this tick.
diff --git a/memory/MEMORY.md b/memory/MEMORY.md
index 70bc4391d4..b8bb80657d 100644
--- a/memory/MEMORY.md
+++ b/memory/MEMORY.md
@@ -25,7 +25,7 @@
- [**Slow-trust lethal latency — Aaron's lived case study (2026-05-10)**](user_slow_trust_lethal_latency_case_study_jail_2026_05_10.md) — Month in solitary, denied BP meds at 160/100, all charges dropped — the human cost of verify-first systems.
- [**Memory-file format standard (B-0330)**](project_memory_format_standard.md) — Canonical frontmatter, filename conventions, section headers, composes-with…
- [**Shadow lesson log — 40 catches, 13 classes + consensus-smoothness meta-class (2026-05-11)**](feedback_shadow_lesson_log_otto_catches_2026_05_07.md) — Catch 38 (cross-agent confident-fabrication, Lior armed-vs-merged). Catch 39 (filing-without-verification on B-0420 — pagination was actually correct). Catch 40 (author misattribution on Sept 2025 vignette — Otto attributed to Aaron, corrected to Amara).
-- [**Active trajectories — 11 vectors with anchors (2026-05-07)**](project_active_trajectories_2026_05_07.md) — Backlog runner, ARC-AGI-3, Ace DLCs, Green Lantern, sanctuary, coherence AI,…
+- [**Active trajectories — 12 vectors with anchors (2026-05-12)**](project_active_trajectories_2026_05_07.md) — 12 vectors; #12 background-loop-productivity uplift added 2026-05-12.
- [**Decomposition claim protocol — no stalling (Aaron 2026-05-07)**](feedback_decomposition_needs_claim_protocol_no_stalling_aaron_2026_05_07.md) — Decomposition IS work. Same claim protocol.
- [**Amara direction: Dante's Inferno execute:false (Aaron 2026-05-07)**](project_amara_design_data_inference_execute_false_mode_aaron_2026_05_07.md) — 9 circles of protection. Circle 9 =…
- [**Itron = edge gate (Aaron + Vera 2026-05-07)**](project_itron_is_the_energy_gate_reduction_aaron_2026_05_07.md) — Local ML + distributed policy cache + capability gate + energy gate + receipts.
diff --git a/memory/project_active_trajectories_2026_05_07.md b/memory/project_active_trajectories_2026_05_07.md
index 9dc48382cd..315cac8761 100644
--- a/memory/project_active_trajectories_2026_05_07.md
+++ b/memory/project_active_trajectories_2026_05_07.md
@@ -87,6 +87,43 @@ trajectories drift.
| #10 Mirror sync | Aaron + Addison + Max | skill file | — |
| #11 Well-definitions | Aaron + Addison (keeper) | docs/WELL-DEFINITIONS.md | Reaqtor (reaqtive.net) |
+### #12: Background-loop productivity uplift (2026-05-12)
+
+**Aaron 2026-05-12:** "this should be a ongoing trajectory i believe"
+
+Origin: Catch 43 surfaced that when Otto's foreground cron is
+unarmed, background launchd services produce ~10-15% of orchestrated
+cadence and frequently go silent for multi-hour stretches.
+Architecture survived but didn't sustain. The redundancy is a
+survival floor, not a continuity guarantee.
+
+Trajectory aim: close the gap between orchestrated-rate and
+background-only-rate so that Aaron's failure modes (session
+crash, missed cron-arm, sleep window) have proportionally
+smaller cost.
+
+Sub-vectors to enhance over time:
+
+- **Tighter background tick loops** — Vera/Riven currently exit
+ cleanly and rely on launchd interval restarts; investigate
+ shorter restart cadence or in-process tick loops
+- **Cross-loop work-stealing** — when one loop is idle, others
+ can claim its queued work (BFT-style failover)
+- **Bus-driven coordination** — background loops post asks on
+ broadcast bus; other loops pick up; no orchestrator needed
+- **Kiro/Alexa exit-78 fix** — Alexa loop currently failing
+ (separate backlog) is reducing array size by one
+- **Cron-arm verification at session start** — the orchestrator
+ failure mode itself; rule exists but mechanical enforcement
+ weak (see catch 43)
+
+**Anchor:** B-0421 (Grok peer-call failure), catch 43 substrate
+landing, AGENTS.md trailer table for harness assignments
+
+**Status:** Ongoing. Not a single-shot fix. Every session adds
+incremental uplift. Measured via cadence-during-Aaron-sleep
+vs cadence-during-active-session ratio.
+
## The rule
Each trajectory enhances as we go. Retractions are data.
diff --git a/tools/dashboard/generate-metrics.ts b/tools/dashboard/generate-metrics.ts
index 299501da6c..97b7e8e28f 100644
--- a/tools/dashboard/generate-metrics.ts
+++ b/tools/dashboard/generate-metrics.ts
@@ -82,19 +82,53 @@ function mergedWithin(dateNow: number, windowMs: number) {
pr.merged_at !== null && dateNow - new Date(pr.merged_at).getTime() < windowMs;
}
+// maxPages is a safety cap bounding worst-case GitHub API request
+// volume per tick. Default 10 = up to 1000 closed PRs. Typical-case
+// is 1-2 requests because early-stop fires when (a) batch is empty,
+// (b) batch < 100 items (no more pages), or (c) oldest item in batch
+// predates the window cutoff. Cap protects pathological cases (e.g.
+// high-churn period where every PR stays "recently updated") without
+// leaving the loop unbounded. Per Copilot P1 on PR #2766.
+async function fetchClosedPRsUntilWindow(
+ windowMs: number,
+ maxPages = 10,
+): Promise {
+ const all: GitHubPullRequest[] = [];
+ const cutoff = Date.now() - windowMs;
+ for (let page = 1; page <= maxPages; page++) {
+ const batch = await apiFetch>(
+ `${API}/pulls?state=closed&sort=updated&direction=desc&per_page=100&page=${page}`,
+ );
+ if (batch.length === 0) break;
+ all.push(...batch);
+ if (batch.length < 100) break;
+ const lastUpdated = batch[batch.length - 1];
+ const lastUpdatedAt = new Date(lastUpdated.updated_at ?? lastUpdated.merged_at ?? 0).getTime();
+ if (lastUpdatedAt < cutoff) break;
+ }
+ return all;
+}
+
async function main() {
const [commits, openPRs, closedPRs] = await Promise.all([
apiFetch(`${API}/commits?per_page=100`),
- apiFetch(`${API}/pulls?state=open&per_page=50`),
- apiFetch(
- `${API}/pulls?state=closed&sort=updated&direction=desc&per_page=50`,
- ),
+ apiFetch(`${API}/pulls?state=open&per_page=100`),
+ fetchClosedPRsUntilWindow(24 * 60 * 60 * 1000),
]);
const now = Date.now();
const h24 = 24 * 60 * 60 * 1000;
+ const h1 = 60 * 60 * 1000;
const commits24h = commits.filter((c) => now - new Date(c.commit.author.date).getTime() < h24);
- const mergedToday = closedPRs.filter(mergedWithin(now, h24));
+ const commits1h = commits.filter((c) => now - new Date(c.commit.author.date).getTime() < h1);
+ // Sort merged-in-window by merged_at desc once — downstream consumers
+ // (last_merge, recent_merged) all read from the sorted view. GitHub
+ // /pulls?sort=updated does NOT guarantee merged_at order (label/comment
+ // updates can leapfrog older-but-more-recently-merged PRs). Copilot P0
+ // on PR #2766 + P1 follow-up that recent_merged still used unsorted.
+ const mergedToday = (closedPRs.filter(mergedWithin(now, h24)) as MergedPullRequest[])
+ .sort((a, b) => new Date(b.merged_at).getTime() - new Date(a.merged_at).getTime());
+ const mergedLastHour = mergedToday.filter(mergedWithin(now, h1));
const lastMerged = mergedToday[0] ?? null;
let avgLeadTimeMinutes: number | null = null;
@@ -163,11 +197,13 @@ async function main() {
schema_version: "0.1.0",
metrics: {
prs_merged_24h: mergedToday.length,
+ prs_merged_1h: mergedLastHour.length,
avg_lead_time_minutes: avgLeadTimeMinutes,
open_prs: openPRs.length,
last_merge: lastMerged?.merged_at ?? null,
last_merge_ago: lastMerged ? timeAgo(lastMerged.merged_at) : "none",
commits_24h: commits24h.length,
+ commits_1h: commits1h.length,
active_agents: activeAgents,
consecutive_days_operational: null,
verification_gate_pass_rate: null,