You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Orb stops being a per-instance GitHub App and becomes a central, anonymized fleet-calibration collector + analytics layer. Gittensory cloud stays the owner's review app for our own repos; Orb aggregates de-noised calibration telemetry up from maintainers' self-hosted instances.
The key correction: every self-hosted instance already records rich, de-noised ground truth in review_audit (migration 0049) via processGitHubWebhook → recordPrOutcome + recordReversalSignals:
gate_decision — the prediction (merge/close/hold)
pr_outcome — ground truth (merged/closed), author-self-close-filtered
reversal_reopened / reversal_reverted — the human-override signals
Orb's current orb_events model is a noisier reinvention of this (raw merged/closed, no reversals, broken review_targets verdict lookup, wrong head_sha dedup key) and the /orb/setup per-instance App wizard is exactly the per-instance custom work we want to avoid. So we retire that, repoint the exporter at review_audit, extend the collector schema + analytics, and keep the anonymized fleet-export (one central store, instances contribute, no shared secret — the das-github-mirror principle).
PR #1254 (merged) already shipped the live collector URL (https://gittensory-api.aethereal.dev/v1/orb/ingest), the strict per-IP rate-limit on /v1/orb/ingest, and the no-shared-secret .env.example framing. This work builds on it.
1. What Orb collects (one anonymized row per resolved PR)
Sourced from review_audit (+ review_targets for open-time), notorb_events:
Cycle-time gap (must solve, not hand-wave):time_to_close_ms currently comes only from the retiring orb-webhook.ts. review_audit has the close timestamp but not PR-open time. Resolution: join pr_outcome → review_targets.created_at (migration 0050, indexed) on target_id → time_to_close_ms = outcome_at − review_targets.created_at; fall back to outcome_at − decision_at (tagged) when no review_targets row; receiver tolerates NULL.
Dedup key:(instance_id, repo_hash, pr_hash) UPSERT (latest wins). The current 0058UNIQUE(instance_id, pr_hash) is wrong — two instances reviewing the same owner/repo#123 collide.
Exporter join (over review_audit, mirrors computeGateEval): latest gate_decision per target_id via MAX(created_at) LEFT JOIN latest pr_outcome LEFT JOIN reversal marker LEFT JOIN review_targets. Filter source='gittensory-native'; join key is target_id (project#pr), NOT head_sha.
2. How gittensory uses it (fleet analytics)
Metric
Formula
Feeds
merge precision
merge∧merged / merge
auto-tune
close precision
close∧closed / close
auto-tune
false-positive rate
merge∧closed / merge
auto-tune + alerts
false-negative rate
close∧merged / close
observability
reversal rate
reversal≠none / decided, split reverted vs reopened
observability + trust
cycle-time p50/p95
percentiles of time_to_close_ms per instance/fleet/bucket
observability
verdict skew
per-bucket outcome distribution per verdict
alerts
instance outliers
per-instance metric vs fleet median
health dashboard
Aggregation is median/percentile, never mean (untrusted contributed data). Auto-tuning only consumes the robust, high-volume metrics (precision/FP); reversal/FN/cycle-time/skew/outliers stay advisory until per-instance reputation weighting (v2).
Surface (3 reads):
GET /v1/internal/fleet/analytics — bearer-gated with INTERNAL_JOB_TOKEN (mirror /v1/internal/ops/stats). The ingest endpoint stays unauthenticated + rate-limited; analytics is owner-only.
A fleetMetrics section on the operator dashboard → "Fleet Health" panel in gittensory-ui.
3. Build plan (ordered, file-level)
Migration 0060_orb_fleet_collector.sql (next contiguous after 0059):
DROP TABLE orb_events; DROP TABLE orb_installations;
Recreate orb_signals with the final schema (drop+recreate is clean — young regenerable telemetry table; SQLite can't ALTER away the old table-level UNIQUE): columns incl. reversal_flag (CHECK in none/reopened/reverted), decision_timestamp, outcome_timestamp, gate_reasoncode_bucket; UNIQUE(instance_id, repo_hash, pr_hash); indexes (instance_id, gate_verdict, outcome, reversal_flag) and (instance_id, received_at DESC).
orb_export_cursor(instance_hash PK, last_exported_at, updated_at) — replaces orb_events.exported_at (review_audit is append-only).
Retire (delete):src/selfhost/orb-setup.ts, src/selfhost/orb-webhook.ts, their tests, the /orb/setup, /orb/setup/callback, /orb/webhook routes + orb_setup_state cookie in src/server.ts (≈285-333), and the dead gittensory_orb_webhook_total/gittensory_orb_installs_total counters. Keep the orbEnabled/exportOrbBatch import + hourly export cron.
Repoint (src/selfhost/orb-collector.ts): replace SELECT * FROM orb_events with the review_audit+review_targets join; drop recordOrbEvent/OrbRow/OrbEvent (local recorder gone — pr_outcome already recorded by outcomes-wire.ts); add bucketReasonCode; extend payload with new fields; replace exported_at marking with orb_export_cursor. Keep HMAC anonymization + instance_id as-is.
Extend receiver (src/orb/ingest.ts): new fields + validation (whitelist reversal_flag); INSERT OR REPLACE over the new dedup key; per-instance hourly cap + time_to_close_ms floor(1s)/ceiling(1y) + sent_at skew rejection.
Analytics: new src/orb/analytics.ts (computeFleetAnalytics, median-robust); GET /v1/internal/fleet/analytics; gittensory_get_fleet_analytics MCP tool; fleetMetrics on operator dashboard.
Config (.env.example): remove ORB_APP_ID, ORB_APP_SLUG, ORB_PRIVATE_KEY, ORB_SETUP_OUTPUT_PATH; keep/clarify ORB_ENABLED, ORB_COLLECTOR_URL, ORB_ANONYMIZE, ORB_AIR_GAP, ORB_WEBHOOK_SECRET (now the instance's own stable HMAC secret, no second App).
Tests + artifacts: repoint/extend test/unit/selfhost-orb-collector*, test/integration/orb-ingest.test.ts (new fields, invalid reversal_flag, OR REPLACE upsert, caps, cycle-time fallback both branches); both arms of every ??/ternary for the 97% branch floor; npm run ui:openapi; npm run test:coverage unsharded; npm run cf-typegen if bindings change.
4. Privacy & trust
repo_hash/pr_hash are HMACs keyed by the instance's own secret; the collector holds no instance secret → cannot reverse. instance_id is a one-way hash. reasonCode bucketed at source. head_sha/raw names/logins/titles/code never exported.
Summary / decision
Orb stops being a per-instance GitHub App and becomes a central, anonymized fleet-calibration collector + analytics layer. Gittensory cloud stays the owner's review app for our own repos; Orb aggregates de-noised calibration telemetry up from maintainers' self-hosted instances.
The key correction: every self-hosted instance already records rich, de-noised ground truth in
review_audit(migration0049) viaprocessGitHubWebhook→recordPrOutcome+recordReversalSignals:gate_decision— the prediction (merge/close/hold)pr_outcome— ground truth (merged/closed), author-self-close-filteredreversal_reopened/reversal_reverted— the human-override signalsOrb's current
orb_eventsmodel is a noisier reinvention of this (raw merged/closed, no reversals, brokenreview_targetsverdict lookup, wronghead_shadedup key) and the/orb/setupper-instance App wizard is exactly the per-instance custom work we want to avoid. So we retire that, repoint the exporter atreview_audit, extend the collector schema + analytics, and keep the anonymized fleet-export (one central store, instances contribute, no shared secret — thedas-github-mirrorprinciple).PR #1254 (merged) already shipped the live collector URL (
https://gittensory-api.aethereal.dev/v1/orb/ingest), thestrictper-IP rate-limit on/v1/orb/ingest, and the no-shared-secret.env.exampleframing. This work builds on it.1. What Orb collects (one anonymized row per resolved PR)
Sourced from
review_audit(+review_targetsfor open-time), notorb_events:instance_idSHA256(ORB_APP_ID).slice(0,16)repo_hashHMAC(repo_full_name, ORB_WEBHOOK_SECRET).slice(0,24)pr_hashHMAC("${repo}#${pr}", secret).slice(0,24)gate_verdictgate_decision.decision(merge/close/hold)outcomepr_outcome.decision(merged/closed)reversal_flagreversal_reopened/reversal_reverted→reopened/reverted/nonegate_reasoncode_bucketgate_decision.summaryreasonCodedecision_timestamp,outcome_timestampgate_decision.created_at,pr_outcome.created_attime_to_close_msNever exported:
head_sha, raw repo/PR identifiers, actor logins, titles, code, raw reasonCode.reasonCode buckets:
issue_policy,duplicate_risk,ai_quality,slop_advisory,author_policy,ci_readiness,none,other.Cycle-time gap (must solve, not hand-wave):
time_to_close_mscurrently comes only from the retiringorb-webhook.ts.review_audithas the close timestamp but not PR-open time. Resolution: joinpr_outcome→review_targets.created_at(migration0050, indexed) ontarget_id→time_to_close_ms = outcome_at − review_targets.created_at; fall back tooutcome_at − decision_at(tagged) when noreview_targetsrow; receiver toleratesNULL.Dedup key:
(instance_id, repo_hash, pr_hash)UPSERT (latest wins). The current0058UNIQUE(instance_id, pr_hash)is wrong — two instances reviewing the sameowner/repo#123collide.Exporter join (over
review_audit, mirrorscomputeGateEval): latestgate_decisionpertarget_idviaMAX(created_at)LEFT JOIN latestpr_outcomeLEFT JOIN reversal marker LEFT JOINreview_targets. Filtersource='gittensory-native'; join key istarget_id(project#pr), NOThead_sha.2. How gittensory uses it (fleet analytics)
merge∧merged / mergeclose∧closed / closemerge∧closed / mergeclose∧merged / closereversal≠none / decided, split reverted vs reopenedtime_to_close_msper instance/fleet/bucketAggregation is median/percentile, never mean (untrusted contributed data). Auto-tuning only consumes the robust, high-volume metrics (precision/FP); reversal/FN/cycle-time/skew/outliers stay advisory until per-instance reputation weighting (v2).
Surface (3 reads):
GET /v1/internal/fleet/analytics— bearer-gated withINTERNAL_JOB_TOKEN(mirror/v1/internal/ops/stats). The ingest endpoint stays unauthenticated + rate-limited; analytics is owner-only.gittensory_get_fleet_analyticsMCP tool (alongsidegittensory_get_outcome_calibration).fleetMetricssection on the operator dashboard → "Fleet Health" panel in gittensory-ui.3. Build plan (ordered, file-level)
Migration
0060_orb_fleet_collector.sql(next contiguous after 0059):DROP TABLE orb_events; DROP TABLE orb_installations;orb_signalswith the final schema (drop+recreate is clean — young regenerable telemetry table; SQLite can'tALTERaway the old table-level UNIQUE): columns incl.reversal_flag(CHECK in none/reopened/reverted),decision_timestamp,outcome_timestamp,gate_reasoncode_bucket;UNIQUE(instance_id, repo_hash, pr_hash); indexes(instance_id, gate_verdict, outcome, reversal_flag)and(instance_id, received_at DESC).orb_export_cursor(instance_hash PK, last_exported_at, updated_at)— replacesorb_events.exported_at(review_audit is append-only).Retire (delete):
src/selfhost/orb-setup.ts,src/selfhost/orb-webhook.ts, their tests, the/orb/setup,/orb/setup/callback,/orb/webhookroutes +orb_setup_statecookie insrc/server.ts(≈285-333), and the deadgittensory_orb_webhook_total/gittensory_orb_installs_totalcounters. Keep theorbEnabled/exportOrbBatchimport + hourly export cron.Repoint (
src/selfhost/orb-collector.ts): replaceSELECT * FROM orb_eventswith thereview_audit+review_targetsjoin; droprecordOrbEvent/OrbRow/OrbEvent(local recorder gone —pr_outcomealready recorded byoutcomes-wire.ts); addbucketReasonCode; extend payload with new fields; replaceexported_atmarking withorb_export_cursor. Keep HMAC anonymization +instance_idas-is.Extend receiver (
src/orb/ingest.ts): new fields + validation (whitelistreversal_flag);INSERT OR REPLACEover the new dedup key; per-instance hourly cap +time_to_close_msfloor(1s)/ceiling(1y) +sent_atskew rejection.Analytics: new
src/orb/analytics.ts(computeFleetAnalytics, median-robust);GET /v1/internal/fleet/analytics;gittensory_get_fleet_analyticsMCP tool;fleetMetricson operator dashboard.Config (
.env.example): removeORB_APP_ID,ORB_APP_SLUG,ORB_PRIVATE_KEY,ORB_SETUP_OUTPUT_PATH; keep/clarifyORB_ENABLED,ORB_COLLECTOR_URL,ORB_ANONYMIZE,ORB_AIR_GAP,ORB_WEBHOOK_SECRET(now the instance's own stable HMAC secret, no second App).Tests + artifacts: repoint/extend
test/unit/selfhost-orb-collector*,test/integration/orb-ingest.test.ts(new fields, invalidreversal_flag, OR REPLACE upsert, caps, cycle-time fallback both branches); both arms of every??/ternary for the 97% branch floor;npm run ui:openapi;npm run test:coverageunsharded;npm run cf-typegenif bindings change.4. Privacy & trust
repo_hash/pr_hashare HMACs keyed by the instance's own secret; the collector holds no instance secret → cannot reverse.instance_idis a one-way hash. reasonCode bucketed at source.head_sha/raw names/logins/titles/code never exported.strictrate limit; input caps. v2: per-instance reputation weighting.What the owner does
0060runs on deploy (deploy:api).INTERNAL_JOB_TOKENalready set (ops/stats) — no new secret./orb/setupentirely — setORB_ENABLED=true(+ a stableORB_WEBHOOK_SECRET). No Orb GitHub App, no wizard.Acceptance criteria
orb_events/orb_installations+/orb/setup*+/orb/webhookremoved; per-instance Orb App no longer required.exportOrbBatchreadsreview_auditjoin; payload carriesreversal_flag+ reason bucket + cycle-time (withreview_targetsfallback).orb_signalsre-keyed + extended (migration 0060); receiver validates new fields + caps.GET /v1/internal/fleet/analytics+gittensory_get_fleet_analyticsMCP tool + operator-dashboard Fleet Health section.npm run test:ci, unsharded coverage ≥97% patch,npm audit),ui:openapiregenerated.