公开记录
软件健康报告模式 0.27.0 · 指标 1.13.0 · 2026-07-28 07:52 UTC

AndrewDryga / onlytty

Share access to your shell or specific command and control it from a browser or a phone. End-to-end encrypted.

Elixir · Go · JavaScriptMIT★ 2 星标⑂ 0 复刻始于 2026年6月在 GitHub 上查看 ↗

AndrewDryga/onlytty 的健康指数为 100 分中的 52 分,处于「中等」区间。 其得分最高的类别是AI Readiness(75/100),最低的是Community & Adoption(24/100)。 最近一次更新在 18 天前。 近期的大部分工作由 1 位贡献者完成。

52
总分 / 100
中等

软件健康指数

指标归入加权类别,统一采用 1–100 量表。总体分先取类别加权平均;当公开证据触发高风险司法辖区政策时,评级会按政策调整,并设置 49(有风险)的上限。AI 就绪度不计入总体分。

52
优秀85-100堪称典范;基本满足所有检验标准
良好70-84健康;仅有轻微不足
中等50-69可接受,但存在明显不足;建议进行审查
存在风险30-49存在重大薄弱环节;采用时应保持审慎
危急1-29问题严重(项目被弃置、仅有单一维护者、缺乏基本工程规范)
活力社区与采用可持续性与治理工程质量安全AI 就绪度

评分画像

每条轴代表一个类别。形状比平均值更重要——健康的对象会填满整个图形,而“一峰一谷”式画像意味着某一维度的优势正掩盖另一维度的风险。

所有权

Andrew Dryga个人账户
213 关注者89 个公开仓库始于 2012年6月Hire me!

该仓库由个人账户拥有。相较于组织支持的项目,单一所有者项目的延续性风险更高。

软件包生态系统

注册表软件包版本月下载量版本数最近发布
Gogithub.com/AndrewDryga/onlyttyv0.4.11-2018 天前

按类别列示的指标

活力

项目是否仍有生命——是否仍在编写代码,是否仍在发布版本?

66中等 · 占总体的 22%
评分方式
28.8/36推送新近度 — 最近一次推送于 18 天前
2.8/36提交节奏 — 52 周中有 4 周有提交
18/18提交量 — 最近一年 271 次提交
0/10OpenSSF Scorecard:Maintained — project was created within the last 90 days. Please review its contents carefully
所用输入
commits_last_year271
human_commit_share0.98
days_since_last_push18
active_weeks_last_year4

发布纪律

90优秀
评分方式
27/27有发布版本 — 已发布 17 个发布版本
36/36发布时效 — 最近一次发布版本于 18 天前
27/27发布节奏 — 约每 1.7 天发布一次
0/10OpenSSF Scorecard:Signed-Releases — Project has not signed or included provenance with any releases.
所用输入
releases_count17
latest_release_tagv0.4.10
releases_from_tags
days_since_latest_release18
mean_days_between_releases1.7

社区与采用

项目是否拥有用户、下载量与关注度,并具备欢迎贡献者参与的配置?

24危急 · 占总体的 18%
评分方式
0/60星标 — 2 个星标
0/25复刻 — 0 个复刻
0/15关注者 — 0 位关注者
所用输入
forks0
stars2
watchers0
growth_stateunverified
growth_factor_pct100
growth_unverified_reasonno_history

社区健康

50中等
评分方式
22.5/22.5README
22.5/22.5许可证 — 可识别的许可证(MIT)
0/18CONTRIBUTING 指南
0/13.5行为准则
0/7.2议题模板
0/6.3PR 模板
所用输入
has_readme
has_license
has_contributing
has_issue_template
has_code_of_conduct
has_pull_request_template

可持续性与治理

项目能否在其成员之外延续——巴士系数、响应能力、由谁支持,以及软件包的维护状况?

46存在风险 · 占总体的 24%
评分方式
9/54巴士系数 — 1 位贡献者贡献了半数提交
0/22.5提交分布 — 头号贡献者编写了 100% 的提交
1.4/13.5贡献者广度 — 1 位贡献者
10/10OpenSSF Scorecard:Contributors — project has 3 contributing companies or organizations -- score normalized to 10
所用输入
bus_factor1
contributors_sampled1
top_contributor_share1
评分方式
0/46.8议题解决 — 没有议题或无数据
7.7/38.3PR 接受 — 已裁定的 PR 中 2/10 已合并
0/15OpenSSF Scorecard:Code-Review — Found 0/30 approved changesets -- score normalized to 0
所用输入
merged_prs2
open_issues0
closed_issues0
issue_closed_ratio
closed_unmerged_prs8
已排除计分(无数据或不适用):议题解决。 其余权重已重新归一化。
评分方式
10/30所有权背书 — 个人(用户)账户
0/20已验证域名 — 不适用于个人账户
16.8/25所有者影响力 — AndrewDryga 有 213 位关注者
25/25既往记录 — 89 个公开仓库,账户约 14 年
所用输入
followers213
owner_typeUser
is_verified
owner_loginAndrewDryga
public_repos89
account_age_days5,149
已排除计分(无数据或不适用):已验证域名。 其余权重已重新归一化。
评分方式
25/25已发布且可解析 — go 上有 1 个软件包
35/35发布时效 — 最近一次发布于 18 天前
20/20版本历史 — 20 个已发布版本
20/20未被弃用 — 活跃,未被弃用或撤回
所用输入
packagesgithub.com/AndrewDryga/onlytty
ecosystemsgo
any_deprecated
min_days_since_publish18

工程质量

基础的工程与文档实践是否到位?

62中等 · 占总体的 20%

工程实践

60中等
评分方式
24/24CI 工作流 — 5 个工作流
24/24存在测试
0/16Linter 配置
0/9.6Pre-commit 钩子
0/6.4.editorconfig
0/20OpenSSF Scorecard:CI-Tests — 无数据
所用输入
has_ci
has_tests
has_editorconfig
has_linter_config
has_precommit_config
已排除计分(无数据或不适用):OpenSSF Scorecard:CI-Tests。 其余权重已重新归一化。

文档

65中等
评分方式
30/30README
0/25文档目录
15/15文档 / 主页站点 — https://onlytty.com
10/10仓库描述
10/10主题标签 — 10 个主题标签
0/10Wiki
所用输入
topicselixir, go, shell, terminal, terraform, tty, remote-access, remote-control, remote-execution, self-hosted
has_wiki
homepagehttps://onlytty.com
has_readme
has_docs_dir
has_description

安全

可见的安全与供应链实践是否稳固,且不存在未解决的高风险司法辖区暴露?

61中等 · 占总体的 16%

安全态势

61中等
评分方式
6.8/7.5Binary-Artifacts — binaries present in source code
2.2/7.5Branch-Protection — branch protection is not maximal on development and all release branches
0/2.5CI-Tests — 无数据
0/2.5CII-Best-Practices — no effort to earn an OpenSSF best practices badge detected
0/7.5Code-Review — Found 0/30 approved changesets -- score normalized to 0
2.5/2.5Contributors — project has 3 contributing companies or organizations -- score normalized to 10
10/10Dangerous-Workflow — no dangerous workflow patterns detected
7.5/7.5Dependency-Update-Tool — update tool detected
5/5Fuzzing — project is fuzzed
2.5/2.5许可证 — license file detected
0/7.5Maintained — project was created within the last 90 days. Please review its contents carefully
5/5Packaging — packaging workflow detected
3.5/5Pinned-Dependencies — dependency not pinned by hash detected -- score normalized to 7
0/5SAST — no SAST tool detected
5/5Security-Policy — security policy file detected
0/7.5Signed-Releases — Project has not signed or included provenance with any releases.
7.5/7.5Token-Permissions — GitHub workflow tokens follow principle of least privilege
5.2/7.5Vulnerabilities — 3 existing vulnerabilities detected
所用输入
sourceopenssf_scorecard
checks_evaluated17
scorecard_versionv5.5.0
checks_inconclusive1
scorecard_aggregate6.1
已排除计分(无数据或不适用):ci_tests。 其余权重已重新归一化。

AI 就绪度

该仓库在多大程度上具备与 AI 编码代理协同开发与维护的条件?这是一枚独立的实验性徽章——权重为 0.0,因此单独呈现,不影响总体健康评分。

75良好 · 占总体的 0%
评分方式
45/45代理指令 — AGENTS.md, CLAUDE.md, GEMINI.md
0/15机器可读文档(llms.txt)
40/40可读的提交历史 — 98 次人类提交中有 95 次说明了意图(结构化标题或解释性正文)
所用输入
has_llms_txt
legible_history_share0.969
agent_instruction_filesAGENTS.md, CLAUDE.md, GEMINI.md
agent_instruction_max_bytes5,586
评分方式
18/18一条命令的引导启动 — Makefile
22/22自动化测试
0/11Lint / 格式化配置
0/11静态类型检查
10/10可复现环境 — Dockerfile, lockfile
10/10已体现的代理实践 — 最近 100 次提交中有 82 次由代理编写或署名代理
8/8自动化维护 — 最近 100 次提交中有 2 次为自动依赖更新
7/10OpenSSF Scorecard:Pinned-Dependencies — dependency not pinned by hash detected -- score normalized to 7
所用输入
has_nix
has_tests
lockfilesgo.sum, mix.lock, package-lock.json
has_dockerfile
typed_language
bootstrap_filesMakefile
has_devcontainer
has_linter_config
typecheck_configs
agent_commit_share0.82
toolchain_manifestsportal/mix.exs, runner/go.mod
dependency_bot_commit_share0.02
评分方式
0/45可类型检查的代码 — Elixir,未配置类型检查
55/55可控的文件大小 — 采样的 80 个源文件中有 0 个超过 60KB
所用输入
primary_languageElixir
largest_source_bytes55,861
source_files_sampled80
oversized_source_files0

关键数据

2GitHub 星标
1贡献者
271最近 12 个月提交数
18距最近推送天数
17发布版本数
1巴士系数(bus factor)
0开放议题
Go, Hex, npm软件包生态系统数

数据采集警告

  • Star history unavailable: GitHub GraphQL error: Resource not accessible by personal access token
  • Could not fetch hex package 'onlytty' from its registry
  • GitHub dependency-graph SBOM unavailable (404); the dependency graph may be disabled for this repository

更多细节

OpenSSF Scorecard 6.1 / 10
6.1综合

来自开源项目 OpenSSF Scorecard 的独立、工具无关的安全评估。每项检查奖励的是安全实践本身,而非特定供应商的工具。Scorecard 无法判定的检查项标记为 不适用,并从安全评分中剔除(绝不按零分计)。Scorecard v5.5.0 · 2026-07-28 07:52 UTC

9Binary-Artifactsbinaries present in source code
3Branch-Protectionbranch protection is not maximal on development and all release branches
不适用CI-Testsno pull request found
0CII-Best-Practicesno effort to earn an OpenSSF best practices badge detected
0Code-ReviewFound 0/30 approved changesets -- score normalized to 0
10Contributorsproject has 3 contributing companies or organizations -- score normalized to 10
10Dangerous-Workflowno dangerous workflow patterns detected
10Dependency-Update-Toolupdate tool detected
10Fuzzingproject is fuzzed
10Licenselicense file detected
0Maintainedproject was created within the last 90 days. Please review its contents carefully
10Packagingpackaging workflow detected
7Pinned-Dependenciesdependency not pinned by hash detected -- score normalized to 7
0SASTno SAST tool detected
10Security-Policysecurity policy file detected
0Signed-ReleasesProject has not signed or included provenance with any releases.
10Token-PermissionsGitHub workflow tokens follow principle of least privilege
7Vulnerabilities3 existing vulnerabilities detected
直接依赖 13
注册表软件包版本约束清单文件
Hexphoenix~> 1.8portal/mix.exs
Hexwebsock_adapter~> 0.6portal/mix.exs
Hextelemetry_metrics~> 1.0portal/mix.exs
Hextelemetry_poller~> 1.0portal/mix.exs
Hexjason~> 1.2portal/mix.exs
Hexlibcluster~> 3.5portal/mix.exs
Hexbandit~> 1.5portal/mix.exs
Hexsentry~> 13.2portal/mix.exs
Hexhackney~> 4.4portal/mix.exs
Gogithub.com/coder/websocketv1.8.15runner/go.mod
Gogithub.com/creack/ptyv1.1.24runner/go.mod
Gogolang.org/x/termv0.44.0runner/go.mod
Gorsc.io/qrv0.2.0runner/go.mod
全部依赖 未采集

本报告未能采集到解析后的依赖集合:GitHub dependency-graph SBOM unavailable (404); the dependency graph may be disabled for this repository

原始 JSON 报告 机器可读
{
  "data": {
    "repo": {
      "topics": [
        "elixir",
        "go",
        "shell",
        "terminal",
        "terraform",
        "tty",
        "remote-access",
        "remote-control",
        "remote-execution",
        "self-hosted"
      ],
      "is_fork": false,
      "size_kb": 12488,
      "has_wiki": false,
      "homepage": "https://onlytty.com",
      "languages": {
        "Go": 131144,
        "CSS": 29780,
        "HCL": 24743,
        "HTML": 13495,
        "Shell": 32609,
        "Elixir": 279582,
        "Makefile": 4269,
        "Batchfile": 47,
        "Dockerfile": 3445,
        "JavaScript": 107754
      },
      "pushed_at": "2026-07-10T04:59:54Z",
      "created_at": "2026-06-20T09:07:20Z",
      "owner_type": "User",
      "updated_at": "2026-07-21T19:27:43Z",
      "description": "Share access to your shell or specific command and control it from a browser or a phone. End-to-end encrypted.",
      "is_archived": false,
      "is_disabled": false,
      "license_spdx": "MIT",
      "default_branch": "main",
      "license_spdx_raw": "MIT",
      "primary_language": "Elixir",
      "significant_languages": [
        "Elixir",
        "Go",
        "JavaScript"
      ]
    },
    "owner": {
      "blog": "http://dryga.com",
      "name": "Andrew Dryga",
      "type": "User",
      "login": "AndrewDryga",
      "company": "Hire me!",
      "location": "Ukraine / Mexico / USA",
      "followers": 213,
      "avatar_url": "https://avatars.githubusercontent.com/u/1877644?v=4",
      "created_at": "2012-06-21T16:37:55Z",
      "is_verified": null,
      "public_repos": 89,
      "account_age_days": 5149
    },
    "license": {
      "state": "standard",
      "spdx_id": "MIT",
      "raw_spdx": "MIT",
      "file_present": true,
      "scorecard_found": true,
      "profile_has_license": true
    },
    "activity": {
      "releases": [
        {
          "tag": "v0.4.10",
          "kind": "patch",
          "published_at": "2026-07-10T02:14:10Z"
        },
        {
          "tag": "v0.4.9",
          "kind": "patch",
          "published_at": "2026-07-09T19:35:36Z"
        },
        {
          "tag": "v0.4.7",
          "kind": "patch",
          "published_at": "2026-07-07T18:08:30Z"
        },
        {
          "tag": "v0.4.5",
          "kind": "patch",
          "published_at": "2026-07-02T23:04:48Z"
        },
        {
          "tag": "v0.4.4",
          "kind": "patch",
          "published_at": "2026-07-02T22:01:11Z"
        },
        {
          "tag": "v0.4.3",
          "kind": "patch",
          "published_at": "2026-07-02T05:52:13Z"
        },
        {
          "tag": "v0.4.2",
          "kind": "patch",
          "published_at": "2026-07-02T05:52:07Z"
        },
        {
          "tag": "v0.4.1",
          "kind": "patch",
          "published_at": "2026-07-01T18:32:13Z"
        },
        {
          "tag": "v0.4.0",
          "kind": "minor",
          "published_at": "2026-07-01T17:51:44Z"
        },
        {
          "tag": "v0.3.0",
          "kind": "minor",
          "published_at": "2026-06-24T17:08:24Z"
        },
        {
          "tag": "v0.2.5",
          "kind": "patch",
          "published_at": "2026-06-23T18:22:14Z"
        },
        {
          "tag": "v0.2.4",
          "kind": "patch",
          "published_at": "2026-06-22T23:08:45Z"
        },
        {
          "tag": "v0.2.3",
          "kind": "patch",
          "published_at": "2026-06-22T09:20:14Z"
        },
        {
          "tag": "v0.2.2",
          "kind": "patch",
          "published_at": "2026-06-22T08:32:13Z"
        },
        {
          "tag": "v0.2.1",
          "kind": "patch",
          "published_at": "2026-06-22T03:38:43Z"
        },
        {
          "tag": "v0.2.0",
          "kind": "minor",
          "published_at": "2026-06-22T01:31:31Z"
        },
        {
          "tag": "v0.1.0",
          "kind": "minor",
          "published_at": "2026-06-21T22:19:27Z"
        }
      ],
      "recent_commits": [
        {
          "oid": "f27b76b8d27761c812e25db45c3fb8de48cc9a3f",
          "body": null,
          "is_bot": false,
          "headline": "chore(release): prepare v0.4.11",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-07-10T04:59:37Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "986913c10418feabab8fcc9c68c6a3182b031405",
          "body": "On mobile the most common cause of a relay `busy` is the viewer's OWN\nhalf-dead prior socket: a wifi→LTE hop or a frozen backgrounded tab leaves\nthe old TCP dead client-side with no FIN, while the relay-side socket\nsurvives for minutes (Bandit's process-message timeout + the runner's\nkeepalive HELLO\n[…]\nnged.\n\nAdds two browser tests: a post-connect busy retries and recovers; a\nnever-connected busy stays fatal with no second socket.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
          "is_bot": false,
          "headline": "fix(viewer): make a relay busy retryable after a network switch",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-07-10T04:44:44Z",
          "body_truncated": true,
          "is_coding_agent": true
        },
        {
          "oid": "d9a97361aa600be4578ac2e588d9ef44377119dd",
          "body": "make selfhost-check does a full real-HTTPS e2e of the exact shipped\nselfhost/compose.yml + Caddyfile, but ran in no workflow — a compose\nenv-var rename or a Caddyfile change could ship broken and be found by a\nself-hoster instead of CI.\n\nAdd an advisory workflow that runs `make selfhost-check` on ub\n[…]\not a job in deploy-check.yml)\nso the heavy bundle build only fires on bundle/image changes and stays off\nthe four required checks.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
          "is_bot": false,
          "headline": "ci: cover the selfhost bundle (compose + Caddyfile) end to end",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-07-10T04:31:00Z",
          "body_truncated": true,
          "is_coding_agent": true
        },
        {
          "oid": "db9aab3490f6b6b533f80a0f9004cb20b683f523",
          "body": "A hostile relay owns the socket and can force a runner reconnect, so it\ncould replay a controlling viewer's captured, still-valid CtrlReq+INPUT\nframes — re-granting control to the ghost id and re-running its keystrokes\non the host in the default `auto` control mode. That broke the \"relay\ncannot repl\n[…]\nstReplayRejectedAfterFloorResetPaths (verified red pre-fix) and\ndocuments the retention guarantee in PROTOCOL.md's Replay section.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
          "is_bot": false,
          "headline": "fix(runner): retain viewer replay floors across reconnect and leave",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-07-10T04:27:47Z",
          "body_truncated": true,
          "is_coding_agent": true
        },
        {
          "oid": "544e54d4f5ff1e6aeecfd570766e1f8783cd199b",
          "body": "…tifacts\n\nThe dispatch path took a \"tag to release\" input but never used it for\nlabeling: VERSION derived from GITHUB_REF_NAME, action-gh-release defaulted\ntag_name to github.ref_name, and docker/metadata derived semver from the run\nref — all the dispatch ref, not the input. A dispatch from main wit\n[…]\nthe run ref (gh workflow run release.yml\n--ref vX.Y.Z), so version, release tag, and image tag all agree. Tag-push path\nunchanged.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
          "is_bot": false,
          "headline": "fix(release): drop the workflow_dispatch ref input that mislabeled ar…",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-07-10T04:17:14Z",
          "body_truncated": true,
          "is_coding_agent": true
        },
        {
          "oid": "0cc0ae6ec160995c9bb6cc5a153917d73eb8c060",
          "body": "One create call could make a multi_viewer session and script unbounded\nviewer connects to /ws/viewer/:id: join_viewer's unlocked branch admitted\nevery socket with a Process.monitor and no cap, and no per-IP throttle\nguarded the upgrade — so one client could drain the shared connection\nbudget and loc\n[…]\n).\n\nTests: cap rejects the (max+1)th viewer; a second upgrade from one IP is\n429. make check (128 server) and make e2e (20) green.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
          "is_bot": false,
          "headline": "fix: cap viewers per session and throttle WS upgrades on the relay",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-07-10T04:13:51Z",
          "body_truncated": true,
          "is_coding_agent": true
        },
        {
          "oid": "d8bf791048db25c93f8b75682d12615e2af4a83e",
          "body": "ONLYTTY_TRUSTED_PROXY_HOPS read the client IP at a fixed offset from the\nRIGHT of X-Forwarded-For, which is only spoof-resistant for the LB shape\n`<client>, <proxy>` (hops=1). A single edge proxy — the bundled Caddy,\nnginx's $proxy_add_x_forwarded_for — appends the CLIENT as the LAST entry,\nso no nu\n[…]\ne/N modes and an nginx recipe added.\nTests: client_ip edge key-on-last / spoof / distinctness / fallback,\nenv edge parse + reject.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
          "is_bot": false,
          "headline": "fix: per-client rate limiting behind the bundled Caddy (edge XFF mode)",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-07-10T04:04:33Z",
          "body_truncated": true,
          "is_coding_agent": true
        },
        {
          "oid": "5ca97641aa486e509fb90147c433d9c18779382f",
          "body": "…e green\n\n`make check` includes install-check, but ci.yml ran runner/web/server-check in\nseparate jobs and never install-check — an installer regression (e.g. the\n`sh -s -- claude` run-after-install path) merged green on all four required checks\nand first surfaced at the release gate. Add `make inst\n[…]\njob names stay stable for branch protection). Add the `## CI` section to\nREADME that AGENTS.md's \"README → CI\" pointer referenced.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
          "is_bot": false,
          "headline": "fix: run the installer test in CI so installer regressions can't merg…",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-07-10T02:13:00Z",
          "body_truncated": true,
          "is_coding_agent": true
        },
        {
          "oid": "27db76c9906818c888f22a42d75b8eafff762a35",
          "body": "On a phone, tapping the terminal focuses xterm's hidden helper textarea,\nwhich raises the soft keyboard — but every input path (typed keys, the touch\nkey bar, pinned shortcuts, Paste) funnels into sendInput(), whose first line\nsilently dropped everything until the user separately tapped Take control\n[…]\ncontrol view-only host, typing surfaces the existing \"not granted\" status,\nthe button never goes live, and the keystroke never reaches the host.\n\nCo-Authored-By: Claude Fable 5 <noreply@anthropic.com>",
          "is_bot": false,
          "headline": "fix: typing in a read-only viewer requests control instead of vanishing",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-07-10T02:08:06Z",
          "body_truncated": true,
          "is_coding_agent": true
        },
        {
          "oid": "8d9765044c49fab03d1795d7c585798c2a0d1a53",
          "body": null,
          "is_bot": false,
          "headline": "fix: bump Go toolchain for release audit",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-07-09T19:30:15Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "674e30019c145119d40bd4b85c5ffd32803339ef",
          "body": "…e knob\n\nThe POST /api/sessions rate limit keys on the client IP, but the production\ncloud-init never set ONLYTTY_TRUSTED_PROXY_HOPS, so it defaulted to 0 and\nkeyed on conn.remote_ip — behind the Google HTTPS LB that is a GFE address,\ncollapsing the \"30/min per IP\" limit into a fleet-wide shared buc\n[…]\n.\n\nDocs/infra only; cloud-init YAML validated. The selfhost ClientIP enhancement\nthat would close the single-edge-proxy gap is filed separately.\n\nCo-Authored-By: Claude Fable 5 <noreply@anthropic.com>",
          "is_bot": false,
          "headline": "fix: enable per-client rate limiting on the hosted relay; document th…",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-07-09T19:28:35Z",
          "body_truncated": true,
          "is_coding_agent": true
        },
        {
          "oid": "92cd562d27e5047c6e63d4f6efa32df9ac3107b4",
          "body": "The accidental-paste guard tested data.slice(0, -1) for a newline, exempting\nthe final character — so a single-line paste ending in a newline (\"cmd\\n\",\nor \"cmd\\r\" after xterm's paste normalization) saw no newline, skipped the\nconfirm, and executed immediately. That is exactly the classic\npaste-a-com\n[…]\nt pastes a newline-terminated command and asserts the\nconfirm fires and a dismissed paste never reaches the host; verified against\na live relay.\n\nCo-Authored-By: Claude Fable 5 <noreply@anthropic.com>",
          "is_bot": false,
          "headline": "fix: close the paste-guard bypass for a newline-terminated paste",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-07-09T19:23:19Z",
          "body_truncated": true,
          "is_coding_agent": true
        },
        {
          "oid": "0826d249493e2abaee1ebf68ce7034a3b5f8c442",
          "body": "Run() read o.sess.ExitCode() after wg.Wait(), but wg only waits for\npumpOutput — not the exit-watcher goroutine, which is inside cmd.Wait()\nwriting cmd.ProcessState. On a SIGTERM/SIGHUP teardown the two race: Run\ncould return a torn value or -1 (ProcessState still nil) → os.Exit sees 255.\n`go test -\n[…]\nffort by design.\n\nAlso switched runner-check to `go test -race` so this class fails the gate;\nverified it flags the old read and passes the fix.\n\nCo-Authored-By: Claude Fable 5 <noreply@anthropic.com>",
          "is_bot": false,
          "headline": "fix: record child exit code via an atomic to end the exit-code data race",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-07-09T19:16:59Z",
          "body_truncated": true,
          "is_coding_agent": true
        },
        {
          "oid": "8123d1ae035a603d2fd212a9aef3a837122d2509",
          "body": "When the runner's send queue overflows under heavy child output, the sender\nfires a broadcast repaint (viewerID \"\") to resync every viewer. sendRepaint\nunconditionally appended a CONTROL frame, and controlState(\"\") resolves to\nTaken whenever anyone owns control — so the broadcast told the *current\no\n[…]\nemits no control frame (and a\ntargeted one still does); it fails on the old always-append behavior.\nrunner-check green (go vet + go test ./...).\n\nCo-Authored-By: Claude Fable 5 <noreply@anthropic.com>",
          "is_bot": false,
          "headline": "fix: don't broadcast a control frame in the drop-recovery repaint",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-07-09T19:14:19Z",
          "body_truncated": true,
          "is_coding_agent": true
        },
        {
          "oid": "372ff66cba98553eff3e6d0e60b24e2ec29a4226",
          "body": "…he QR\n\nNew Claude Code boots into the alternate screen (ESC[?1049h ESC[2J) ~0.4s\nafter launch, hiding the QR/link banner for the whole session: the alt\nscreen has no scrollback and nothing re-prints the link, so the host had\n~0.4s to scan it. Command-mode runs now hold the launch after the banner\nw\n[…]\nter in both the stdin-TTY and /dev/tty flows and claude enters the\nalt screen only after; piped runs are unchanged. make check + make e2e\ngreen.\n\nCo-Authored-By: Claude Fable 5 <noreply@anthropic.com>",
          "is_bot": false,
          "headline": "fix: hold command launch until Enter so full-screen apps can't wipe t…",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-07-09T18:27:53Z",
          "body_truncated": true,
          "is_coding_agent": true
        },
        {
          "oid": "636b701969d7a0c96e1bfc5dba85fcc6323b7029",
          "body": null,
          "is_bot": false,
          "headline": "fix: bump Phoenix and hpax for release audit",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-07-07T18:03:46Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "1d377ac1cb8bdcfe265e8c7eb96753420afedefb",
          "body": null,
          "is_bot": false,
          "headline": "Let installer launch a command after install",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-07-07T17:53:00Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "41d71ff2791f6b2ff2591796ee254598d15648bc",
          "body": null,
          "is_bot": false,
          "headline": "fix: clarify onlytty host session state",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-07-02T22:48:52Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "482b85d40e5b77310fb2e55e5f4c7f46cf9f91e8",
          "body": null,
          "is_bot": false,
          "headline": "chore: drop pre-v1 legacy compatibility shims",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-07-02T21:55:05Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "c789ae342af1e4a268c9d86b6d7a3ea3fbcb2117",
          "body": "web-check ran only the node unit tests, which import the pure modules\n(crypto/wire/keys) but never app.js — so a syntax error in the largest\nclient file passed `make check` and only surfaced as e2e timeouts\n(this happened once; see .agent history). Add a parse-only\n`node --check` loop over portal/priv/static/assets/*.js (vendor/\nexcluded by the glob) before the tests. Proven red on a planted\n`function broken( {` in app.js, then reverted.\n\nCo-Authored-By: Claude Fable 5 <noreply@anthropic.com>",
          "is_bot": false,
          "headline": "gate(web): syntax-check first-party viewer JS with node --check",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-07-02T11:54:41Z",
          "body_truncated": false,
          "is_coding_agent": true
        },
        {
          "oid": "a1c3db792a0bcfbbd27fa8a5316a8a13eb0a2f17",
          "body": "…'t end a live session\n\nThe viewer compared the relay's absolute expires_at against the device's\nown Date.now(); a fast client clock made the remaining time go negative\nbefore the relay TTL fired, and tickTtl self-disconnected with \"session\nexpired\" even though the relay — the real authority — still\n[…]\n waited out) — the viewer must stay\nconnected showing \"expiring…\", and an injected relay-style\n{\"t\":\"bye\",\"reason\":\"expired\"} must still end it.\n\nCo-Authored-By: Claude Fable 5 <noreply@anthropic.com>",
          "is_bot": false,
          "headline": "fix(viewer): make the expiry countdown display-only so clock skew can…",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-07-02T11:50:46Z",
          "body_truncated": true,
          "is_coding_agent": true
        },
        {
          "oid": "383141b660473146ea175048d9c830f2b1270913",
          "body": "…copy\n\n62 near-identical /control/:slug pages differed only by two sentences, and\nthe :why copy repeated one bathroom-break gag across dozens of tools — the\nclassic doorway/thin-content pattern, and off-tone for a security product.\n\nDirection C (hybrid): rewrite all 62 :why lines to concrete, distin\n[…]\nindex_slugs\nMapSet + Tools.indexable?/1. Demoted pages still render, keep a self\ncanonical, and stay linked from /tools, so nothing is orphaned.\n\nCo-Authored-By: Claude Fable 5 <noreply@anthropic.com>",
          "is_bot": false,
          "headline": "seo(site): thin the indexable tool-page set and rewrite the per-tool …",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-07-02T11:43:41Z",
          "body_truncated": true,
          "is_coding_agent": true
        },
        {
          "oid": "17a41bd6a1f9dfae32688f8657c3d8443a0ea2de",
          "body": "…icit\n\nThe relay speaks plain HTTP and force_ssl trusts X-Forwarded-Proto to decide a\nrequest already arrived over HTTPS. Safe behind a TLS proxy, but a client\nreaching :4000 directly could forge the header and bypass the http->https\nredirect, serving the secret-bearing viewer page in cleartext. Not\n[…]\n bare docker run example binds 127.0.0.1:4000:4000 to\nmatch the nginx proxy_pass above it. No code change; rewrite_on left intact.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
          "is_bot": false,
          "headline": "docs(selfhost): make force_ssl X-Forwarded-Proto trust invariant expl…",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-07-02T10:44:36Z",
          "body_truncated": true,
          "is_coding_agent": true
        },
        {
          "oid": "f227ec1c2c1d81f4a3174b0b3c3cb08e9b83a76b",
          "body": "…es_at\n\nPROTOCOL.md's relay control-plane section had drifted from the code in three\nplaces. Cross-checked each against the implementation and added them:\n\n- going_away: relay sends {\"t\":\"going_away\"} on drain (session.ex:254) to both\n  runner and viewer — a reconnect nudge, not a close; traffic kee\n[…]\nhat\n  viewer_protocol appears on the runner hello only.\n\nSkimmed to_viewer / runner_ready while here; both still match. Docs only.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
          "is_bot": false,
          "headline": "docs(protocol): document going_away, replaced bye reason, hello expir…",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-07-02T10:41:13Z",
          "body_truncated": true,
          "is_coding_agent": true
        },
        {
          "oid": "96dd43d6ea13bb399e93f6d1a647a98495008c1d",
          "body": "\"ask\" implied the host is prompted before a viewer takes control; it\nisn't — control is auto-granted to any link-holder. Rename the default\nto \"auto\" (enum ControlAsk->ControlAuto repo-wide) and keep \"ask\" as a\ndeprecated but accepted alias so scripts don't break. Docs and error\ntext updated; wire protocol and browser unchanged.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
          "is_bot": false,
          "headline": "refactor(runner): rename default --control ask mode to auto",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-07-02T10:38:54Z",
          "body_truncated": false,
          "is_coding_agent": true
        },
        {
          "oid": "0efdfdbd5059840926a42a5cf0321a9199f542a0",
          "body": "The docs said the URL #fragment \"never leaves the browser,\" which reads as\n\"leaves no trace.\" Browsers never *send* a URL fragment to a server, but the\nsecret-bearing link persists client-side: browser history, address bar, and\ncross-device browser sync (Chrome/Safari account sync). It can't be stri\n[…]\nd, and the site \"Scan it\" step to \"never sent to\na server (but stays in history/URL — treat like a password).\" No behavior change.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
          "is_bot": false,
          "headline": "docs(security): note the link fragment secret leaves local traces",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-07-02T10:35:29Z",
          "body_truncated": true,
          "is_coding_agent": true
        },
        {
          "oid": "2b00eafe5b7b9fa7860025b519bffcbf5428736b",
          "body": "The relay compared secrets three ways for the same job: two hand-rolled\n:crypto.hash_equals + ArgumentError rescues (session, socket_controller)\nand one direct Plug.Crypto.secure_compare/2 (metrics_access).\n\nAdd OnlyTTY.SecureCompare.equal?/2 delegating to Plug.Crypto.secure_compare/2\n(constant-time\n[…]\n copies.\n\nAdd secure_compare_test.exs covering equal, unequal same-length, unequal\ndifferent-length, empty, and non-binary inputs.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
          "is_bot": false,
          "headline": "refactor(relay): centralize constant-time secret compare",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-07-02T10:32:54Z",
          "body_truncated": true,
          "is_coding_agent": true
        },
        {
          "oid": "2efc195911d391bbbbdb122fbd657c321da7a085",
          "body": "POST /api/sessions throttled on the full client address. For IPv6 a\nsingle client is handed a whole /64, so an attacker could rotate source\naddresses within their prefix to sidestep \"30 creates/min per IP\" and\ninflate the limiter's ETS table with one {key, bucket} per address.\n\nAdd mask/1 in ClientI\n[…]\ndirect\npaths mask. Scoped to the returned key only — conn.remote_ip is never\ntouched, so the /metrics loopback gate is unaffected.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
          "is_bot": false,
          "headline": "fix(relay): bucket IPv6 rate-limit keys to /64",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-07-02T06:35:18Z",
          "body_truncated": true,
          "is_coding_agent": true
        },
        {
          "oid": "0c8dc93c294b36b5631e509855639e2a9717b396",
          "body": "The runner keyed control ownership and per-viewer replay floors on the\nrelay-assigned viewer_id. A hostile relay could assign a second link-holder\nthe SAME id as the current control owner and promote it to control,\nweakening the `once` \"first taker only\" guarantee.\n\nEach viewer now generates its own\n[…]\nted as a fresh\nviewer (new self id -> repaint); the legacy empty-id single-viewer path is\nunchanged. PROTOCOL.md updated to match.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
          "is_bot": false,
          "headline": "feat(security): bind control ownership to a viewer-chosen id",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-07-02T06:32:13Z",
          "body_truncated": true,
          "is_coding_agent": true
        },
        {
          "oid": "ad0531645e93289e187f14658588cb34d5bd80af",
          "body": "The ring drops oldest bytes at an arbitrary byte offset, so a viewer\njoin/reconnect repaint could begin partway through a multi-byte rune,\nshowing garbage glyphs at the top of the first screen (worse on\nreconnect-heavy mobile use).\n\nSkip leading UTF-8 continuation bytes (b&0xC0 == 0x80) in Ring.Snap\n[…]\nentionally omitted:\nsendRepaint already prepends \\x1bc (RIS full reset), which neutralizes a\ndangling partial escape at the front.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
          "is_bot": false,
          "headline": "fix(runner): align repaint snapshot to a UTF-8 rune boundary",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-07-02T06:16:45Z",
          "body_truncated": true,
          "is_coding_agent": true
        },
        {
          "oid": "ef9b35166e2291c31f38f6d9b7ce6de1bc68b686",
          "body": "A viewer that stops draining its socket applied no backpressure to the\nrunner: the relay's fan-out uses async send/2, so runner output piled\ninto the stalled viewer's process mailbox unbounded — one connection\ncould push a relay node toward OOM.\n\nAdd a per-viewer backlog watermark on the viewer rece\n[…]\ntuffed mailbox\n(real TCP backpressure isn't reproducible in-suite). Gates: make check\ngreen (112 portal tests) and make e2e green.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
          "is_bot": false,
          "headline": "feat(relay): evict slow viewers to bound fan-out backlog",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-07-02T06:13:56Z",
          "body_truncated": true,
          "is_coding_agent": true
        },
        {
          "oid": "43aa247a42d2ba93d25247d73969b268edf64d9d",
          "body": null,
          "is_bot": false,
          "headline": "fix: make mobile hero first paint static",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-07-02T05:46:55Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "fb7f4a8f22c2f5e8d5bb41d8c8933df18b8bd227",
          "body": null,
          "is_bot": false,
          "headline": "feat: support multi-viewer control handoff",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-07-02T01:35:00Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "21047427d3087351ef4588afa353c61743dcf4fa",
          "body": null,
          "is_bot": false,
          "headline": "ci: make image promotion manual",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-07-02T01:34:48Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "baf422fab4348f285b1044a568e41bfc31048495",
          "body": null,
          "is_bot": false,
          "headline": "Add instance_redistribution_type NONE to MIG update policy",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-07-01T20:02:21Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "a8fbd0f6b169d51b4df887aab8dde671ee7a148d",
          "body": "Prevent allocation failures and boot-time races:\n\n- Set distribution_policy_target_shape = \"BALANCED\" so GCE can\n  avoid zones with insufficient small-VM capacity during rollouts\n- Add explicit depends_on edges for NAT, firewall rules, and IAM\n  grants so instances never start before their runtime prerequisites\n  are fully converged",
          "is_bot": false,
          "headline": "Add BALANCED distribution policy and explicit MIG dependencies",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-07-01T19:34:20Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "2a9e12ba68290d55e79d052718d61acb0c2224aa",
          "body": null,
          "is_bot": false,
          "headline": "chore(release): v0.4.1",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-07-01T18:27:28Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "5278b6011c38a54213184c7bd53a1fc8c1888926",
          "body": "Bump the portal version to 0.4.0 and clear release audit blockers by updating Sentry/Hackney/Plug and removing the test-only gun/cowlib dependency.\n\nReplace the gun-based WebSocket test helper with a small raw TCP WebSocket client so relay tests still exercise Bandit/WebSock over real sockets withou\n[…]\nnerable test-only deps.\n\nLocal gates: make check; PATH=\"$(go env GOPATH)/bin:$PATH\" make audit; MIX_ENV=prod PHX_SERVER=true PHX_HOST=localhost PORT=4001 ONLYTTY_SERVER=http://127.0.0.1:4001 make e2e.",
          "is_bot": false,
          "headline": "chore(release): v0.4.0",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-07-01T17:47:27Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "ead501358ad5365b971a91819a7a9dd614b3b5ca",
          "body": "Extract all `ONLYTTY_*` env parsing from `runtime.exs` into\n`Onlytty.Env.runtime_overrides/1`, which accepts an injectable\n`get_env` function. `runtime.exs` now calls it once and applies\nthe resulting keyword list.\n\nAdd `Onlytty.Test.RuntimeEnv.with_runtime_env/2` so tests drive\nthe same parse/validate path that deployments use, instead of\nsetting app env directly. Tests that formerly mutated app env\nnow use this helper, which saves and restores both process env\nand app env around each case.",
          "is_bot": false,
          "headline": "Centralize runtime env parsing and fix test isolation",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-07-01T17:36:40Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "b5f6230a160ee671b84308982493ba4cebae1b04",
          "body": "The bottom touch key bar scrolls horizontally with a hidden scrollbar, so\nat phone width ^C/^D and pinned shortcuts scrolled off-screen with no\naffordance — the interrupt flow could be undiscoverable. Fade whichever\nedge still has keys off-screen (of-l/of-r toggled from scroll position;\nmask-image is paint-only, so no layout shift and taps still land).\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
          "is_bot": false,
          "headline": "feat(viewer): fade the mobile keys bar's overflowing edges",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-07-01T17:21:45Z",
          "body_truncated": false,
          "is_coding_agent": true
        },
        {
          "oid": "407e9ae833bb8ecb51e0dd9745ef890d0270ad60",
          "body": "The browser viewer reconnects forever once it has connected, so it survives\ndeploys and relay-node loss while the runner re-claims the session. But the\ngive-up path (failCount >= 3) was gated on !everConnected, so a no-expiry\nsession whose relay node hard-crashed — and whose runner will never reclai\n[…]\nut a transient blip to reconnect. The\nnever-connected fast give-up is unchanged. make check (106) and make e2e (15,\nwas 13) green.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
          "is_bot": false,
          "headline": "fix(viewer): cap reconnects after a session goes missing for good",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-07-01T16:54:41Z",
          "body_truncated": true,
          "is_coding_agent": true
        },
        {
          "oid": "f76e3cab2e9737c2b78526fc2db7c07d1d8501a0",
          "body": "…proxy\n\nBehind the hosted Google HTTPS LB, conn.remote_ip is the LB/GFE, not the client,\nso \"N creates/min per IP\" collapsed into one global bucket — one client could\n429 everyone, and normal growth could false-positive.\n\nAdd OnlyttyWeb.ClientIP.resolve/1, used by RateLimitGuard for the throttle key\n[…]\nhat hops=1 buckets per real\nclient while hops=0 can't be multiplied by a spoofed header. make check (106)\nand make e2e (13) green.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
          "is_bot": false,
          "headline": "feat(relay): key create rate limiting on the real client IP behind a …",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-07-01T16:33:21Z",
          "body_truncated": true,
          "is_coding_agent": true
        },
        {
          "oid": "27d629686d7c077934bafd2fcf19bfbf0424f836",
          "body": "The 404 re-claim path (relay node drained/replaced, or a full-cluster loss)\nre-created the session with the original `--ttl`. For a TTL session that meant a\nresurrected session got a fresh full lifetime and outlived the user's intended\ndeadline — a violation of the TTL contract, even if only in the \n[…]\n-driven\nTestReclaimTTL covers remaining / below-min / sub-second / at- and\npast-deadline. make check (95) and make e2e (13) green.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
          "is_bot": false,
          "headline": "fix(runner): preserve the TTL deadline when re-claiming a lost session",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-07-01T16:26:05Z",
          "body_truncated": true,
          "is_coding_agent": true
        },
        {
          "oid": "5bf0d63952236b87d95cc6d6721bfd1e52468695",
          "body": "The latent `locked: false` branch overwrote the single `state.viewer` slot on a\nsecond viewer join, leaking the first viewer's monitor and leaving it wired to\nthe runner — so it could never actually support multiple viewers.\n\nModel viewers as a set instead:\n\n- `Onlytty.Session` state: `viewer`/`view\n[…]\noves\nbyte-level broadcast to two viewers and that each viewer's input reaches the\nrunner. make check (95) and make e2e (13) green.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
          "is_bot": false,
          "headline": "refactor(relay): model viewers as a set before unlocked sessions",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-07-01T16:21:21Z",
          "body_truncated": true,
          "is_coding_agent": true
        },
        {
          "oid": "ccd9d9ebbb3a291d2975d69edd58821a5e221a48",
          "body": "…sh Copilot copy\n\nThree parked low-risk nits:\n\n- Viewer buttons: the viewer has no <form>, so a default type=\"submit\" is\n  harmless today but brittle. Set type=\"button\" on every non-submit button in\n  viewer.html and the ones app.js builds dynamically.\n- Home page: removed the id=\"tools\" anchor — no\n[…]\ninst GitHub docs), so `cmd` was already correct; refreshed the\n  stale explain/suggest description to its current agentic framing.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
          "is_bot": false,
          "headline": "chore(cleanup): explicit button types, drop dead #tools anchor, refre…",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-07-01T16:09:35Z",
          "body_truncated": true,
          "is_coding_agent": true
        },
        {
          "oid": "694da80f781bb67cc213ec60ce86cc2f41e195a0",
          "body": null,
          "is_bot": false,
          "headline": "Convert skills to adopt coop v3",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-07-01T15:54:24Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "4fac7c0bd02e1f9bafab844a0547a09718140180",
          "body": null,
          "is_bot": false,
          "headline": "fix(site): make mobile hero paint reliably",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-07-01T15:53:42Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "db76e96ceedba611a30f6e87ecd71430f2c89133",
          "body": "Turnkey self-hosting + the first multi-arch relay image.\n\n- selfhost/ bundle (Docker Compose + Caddy automatic HTTPS) + SELF_HOSTING.md,\n  verified end-to-end via `make selfhost-check`\n- relay image now built for linux/amd64 + linux/arm64\n- /self-hosting marketing page, linked from the nav, FAQ, and\n[…]\nter\n- fix: button text stays readable on hover (was light-on-light)\n- docs: corrected portal/README TTL defaults to match the code\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
          "is_bot": false,
          "headline": "chore(release): v0.3.0",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-06-24T17:04:15Z",
          "body_truncated": true,
          "is_coding_agent": true
        },
        {
          "oid": "ca75208c3fe9fa376ab2d594b9506367bd1213f6",
          "body": "The .btn-* are <a> tags, so the global `a:hover { color: #b7f2fb }` (specificity\n0,1,1) beat `.btn-primary { color: var(--ink) }` (0,1,0) on hover — flipping the\ndark ink text to light cyan on the bright cyan fill, i.e. unreadable. Affected\nevery primary button site-wide (nav \"Get started\", \"Get the\n[…]\n,27) (--ink), not rgb(183,242,251). site.css is content-hashed\ninto the asset cache-buster, so returning visitors pick up the fix.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
          "is_bot": false,
          "headline": "fix(site): keep button text readable on hover (was light-on-light)",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-06-24T16:53:28Z",
          "body_truncated": true,
          "is_coding_agent": true
        },
        {
          "oid": "c81975531d29dd21e7f4edcb62bf0fe9d0fa5f3c",
          "body": "The steps jumped straight to `cp .env.example .env` without saying where the\nbundle comes from. Add a \"Get the bundle\" step (git clone … && cd onlytty/selfhost,\nmatching SELF_HOSTING.md) and fix the now-inaccurate \"Copy two files\" heading\nto \"Clone the bundle, set two values, bring it up\".\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
          "is_bot": false,
          "headline": "fix(site): self-hosting quick-start was missing the \"get the files\" step",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-06-24T16:41:55Z",
          "body_truncated": false,
          "is_coding_agent": true
        },
        {
          "oid": "36503e5f2261a42fb2ce845a0183bfce4fe8e2e9",
          "body": "It was only in the footer + FAQ, but /self-hosting is a top-level page just like\n/tools, which is in the nav — so it belongs there too for wayfinding and as an\nopen-source trust signal. No mobile cost: .nav-links is display:none below 600px,\nso the desktop links never touch the small-phone nav (brand + GitHub + Get started).\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
          "is_bot": false,
          "headline": "feat(site): add Self-hosting to the primary nav",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-06-24T16:29:23Z",
          "body_truncated": false,
          "is_coding_agent": true
        },
        {
          "oid": "7b13e168f9160f6618f0d013cfc72962d5520039",
          "body": "Add a server-rendered, indexable /self-hosting page (PageController :self_hosting\n-> Page.self_hosting/0) that answers the \"self-host terminal sharing\" intent: the\none-command Compose+Caddy path, why-self-host (own domain / limits / network /\ncode-delivery trust), what-you-run (stateless, no DB, beh\n[…]\nlists it next\nto Protocol / Security model. site_test covers the new page + footer link, and\nthe sitemap count bumps to Tools + 6.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
          "is_bot": false,
          "headline": "feat(site): self-hosting landing page, linked from the FAQ and footer",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-06-24T16:26:16Z",
          "body_truncated": true,
          "is_coding_agent": true
        },
        {
          "oid": "d2c6257facc6c9f23695545be34a6c8d25716d38",
          "body": "The portal-image job built amd64 only, so self-hosters on arm64 (Graviton,\nAmpere, Apple Silicon) who pull ghcr.io/.../onlytty hit a prim_tty NIF crash\nunder QEMU emulation. Add docker/setup-qemu-action + setup-buildx-action and\nbuild `platforms: linux/amd64,linux/arm64`. The Dockerfile already uses\n[…]\n\n\nTakes effect on the next tagged release (the arm64 leg compiles the Elixir\nrelease under emulation, so release runs are slower).\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
          "is_bot": false,
          "headline": "ci(release): publish a multi-arch (amd64+arm64) relay image",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-06-24T16:01:14Z",
          "body_truncated": true,
          "is_coding_agent": true
        },
        {
          "oid": "6afd21b244da0aa13505a8e743bf8f536e2772ec",
          "body": "Add a copy-paste Docker Compose + Caddy bundle (selfhost/) that brings the\nrelay up with automatic HTTPS in one `docker compose up -d`, plus a\nSELF_HOSTING.md guide: the breeze path, bring-your-own-proxy\n(nginx/Cloudflare/Fly), the full config reference, operating notes, and the\ncode-delivery trust \n[…]\nv table out of the\nroot README (it now points to the guide). gitignore selfhost/.env so a real\nSECRET_KEY_BASE is never committed.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
          "is_bot": false,
          "headline": "feat(selfhost): one-command self-hosting bundle + tested guide",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-06-24T16:01:05Z",
          "body_truncated": true,
          "is_coding_agent": true
        },
        {
          "oid": "b88f7ef14897a75e57df56529f9c559f6f530353",
          "body": "…ive downgrade)\n\nMixpanel's official snippet injects the library with a protocol-relative URL\n(//cdn.mxpnl.com/...), which resolves to http:// on a plaintext origin. The\nmarketing CSP allows script-src https://cdn.mxpnl.com only, so the e2e CSP guard\n(dev/test/browser/site.test.js, served over http:\n[…]\ny origin scheme.\n\nVerified by replaying the e2e guard over http with the exact marketing CSP: zero\nsecuritypolicyviolation events.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
          "is_bot": false,
          "headline": "fix(site): load the Mixpanel library over https (was a protocol-relat…",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-06-23T18:18:08Z",
          "body_truncated": true,
          "is_coding_agent": true
        },
        {
          "oid": "357a762f38f1a7802b6419f583c47ebab38c8a87",
          "body": "Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
          "is_bot": false,
          "headline": "chore(release): v0.2.5",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-06-23T18:09:06Z",
          "body_truncated": false,
          "is_coding_agent": true
        },
        {
          "oid": "8a1ce97d4e698aa4fe9201c43abe3b9c1a89a1d7",
          "body": "…ed\")\n\nThe custom loader fetched mixpanel-2-latest.min.js directly, but the modern\nlibrary requires Mixpanel's stub snippet to be present first — loading it bare\nlogged '\"mixpanel\" object not initialized' and left window.mixpanel undefined\n(seen live on onlytty.com). Replace the loader with Mixpanel\n[…]\nl.__loaded === true,\nget_distinct_id present) with no error — with api.mixpanel.com blocked so no test\nevents reached the project.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
          "is_bot": false,
          "headline": "fix(site): use Mixpanel's official loader snippet (was \"not initializ…",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-06-23T17:53:28Z",
          "body_truncated": true,
          "is_coding_agent": true
        },
        {
          "oid": "534dd58172691824db75eab8b4008c23e8b6f3e1",
          "body": "Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
          "is_bot": false,
          "headline": "chore(release): v0.2.4",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-06-22T23:04:37Z",
          "body_truncated": false,
          "is_coding_agent": true
        },
        {
          "oid": "588ee2e72c8722b2c4858a9a6cf5e58be299ea9a",
          "body": "Adds product analytics (page views) to the marketing site — deliberately NOT to\nthe terminal viewer, where a tracker could leak the link's #fragment secret.\n\n- /assets/mixpanel.js: a first-party loader (keeps script-src 'self', no inline),\n  honors Do Not Track, persists in localStorage (not cookies\n[…]\n assert the CSP split (analytics on /, strict on /s/:id), the loader is\n  served + unreferenced by the viewer, and the disclosure.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
          "is_bot": false,
          "headline": "feat(site): Mixpanel analytics on the public marketing pages only",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-06-22T23:01:10Z",
          "body_truncated": true,
          "is_coding_agent": true
        },
        {
          "oid": "3bccf47d38b5cc84be6a3bd3f438c4089f6d8702",
          "body": "Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
          "is_bot": false,
          "headline": "chore(release): v0.2.3",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-06-22T09:16:13Z",
          "body_truncated": false,
          "is_coding_agent": true
        },
        {
          "oid": "aaf34746aae9974f5fe2cd132fc1001284138b61",
          "body": "Move the phone viewer from centered to the terminal's bottom-right corner (like\nthe desktop overlay), so the QR and banner stay fully visible on the left.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
          "is_bot": false,
          "headline": "fix(site): anchor the mobile hero phone to the terminal's bottom-right",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-06-22T09:15:02Z",
          "body_truncated": false,
          "is_coding_agent": true
        },
        {
          "oid": "da98c13099efe0fc4c5bac99a6fbac804bd54c28",
          "body": "Slightly larger phone viewer over the terminal background; proportions kept via\naspect-ratio. Still smaller than the terminal, which remains the backdrop.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
          "is_bot": false,
          "headline": "fix(site): enlarge the mobile hero phone ~30% (130->169px)",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-06-22T09:12:54Z",
          "body_truncated": false,
          "is_coding_agent": true
        },
        {
          "oid": "988b00a8abd3d706fd8730b3d3b5ca513c90bc8c",
          "body": "Phone width 260->130px (proportions preserved by aspect-ratio). Now that the\nphone is smaller than the terminal, the terminal sets the stage height again (in\nflow, as the background) and the small phone is centered absolutely on top of it.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
          "is_bot": false,
          "headline": "fix(site): make the mobile hero phone 2x smaller over the terminal",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-06-22T08:51:07Z",
          "body_truncated": false,
          "is_coding_agent": true
        },
        {
          "oid": "c153c21e5e4a69278ce3b104000c01db9c4ae7d2",
          "body": "…al behind)\n\nInstead of stacking, the phone viewer (kept large) now sits on top of the\nterminal window, which recedes to the background. The phone is in flow (it's the\ntaller element, so it sets the stage height); the terminal is centered absolutely\nbehind it. Mobile only — desktop keeps the corner overlay.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
          "is_bot": false,
          "headline": "fix(site): on mobile, layer the large phone over the terminal (termin…",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-06-22T08:46:37Z",
          "body_truncated": false,
          "is_coding_agent": true
        },
        {
          "oid": "4ffbde2705104f5482c0125776d11261ad09faa9",
          "body": "The 132px corner overlay cramped the viewer's text and crumbled its key bar, and\na larger phone can't overlap the narrow mobile terminal cleanly. On phones, stack\nthe terminal (with its QR) above a larger centered phone (up to 260px) so the\nsession log and the Esc/Tab/Ctrl/^C bar read cleanly. Desktop unchanged.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
          "is_bot": false,
          "headline": "fix(site): bigger, centered phone viewer in the mobile hero",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-06-22T08:40:23Z",
          "body_truncated": false,
          "is_coding_agent": true
        },
        {
          "oid": "7fbfff2ecfdec7f5f299967d8c6eb6784323f85c",
          "body": "Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
          "is_bot": false,
          "headline": "chore(release): v0.2.2",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-06-22T08:27:54Z",
          "body_truncated": false,
          "is_coding_agent": true
        },
        {
          "oid": "8231f1744d3d58583cb67baa8cad65ac072f75ae",
          "body": "Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
          "is_bot": false,
          "headline": "content(site): nudge the start-section CTA (\"try it, under a minute\")",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-06-22T08:27:22Z",
          "body_truncated": false,
          "is_coding_agent": true
        },
        {
          "oid": "c200cabbd9e0c79201178b250ab14b6b37c11191",
          "body": "Restore the desktop overlay look on phones — the phone viewer sits over the\nterminal's bottom-right corner (sized down to 132px so it doesn't bury the QR)\ninstead of stacking below it.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
          "is_bot": false,
          "headline": "fix(site): keep the phone overlapping the terminal on mobile",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-06-22T08:22:12Z",
          "body_truncated": false,
          "is_coding_agent": true
        },
        {
          "oid": "feed218ef658cc1ccd8e9106efb8e945806ce3bf",
          "body": "The previous change hid the desktop terminal on phones, which also hid the\nQR/link banner. Instead, at <=640px stack the terminal (with its QR) and the\nphone viewer un-overlapped so both stay visible. Desktop keeps the overlapping\nlayout.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
          "is_bot": false,
          "headline": "fix(site): on mobile show both the terminal (QR) and the phone, stacked",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-06-22T08:19:42Z",
          "body_truncated": false,
          "is_coding_agent": true
        },
        {
          "oid": "6d0ccb0b7b6b9511fd222c34804b3adf12e2fec2",
          "body": "…ro on mobile\n\nOn phones the hero showed the desktop terminal window, which reads wrong on a\nphone. Below 640px, hide .term and show the .phone viewer mock standalone\n(centered, static) — the product on the device you're actually holding. Desktop\nkeeps the terminal window with the phone overlay. Applies to tool heroes too\n(shared term_demo).\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
          "is_bot": false,
          "headline": "fix(site): show the phone viewer, not the desktop terminal, in the he…",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-06-22T08:13:09Z",
          "body_truncated": false,
          "is_coding_agent": true
        },
        {
          "oid": "369782103ae37a7e9dc8ba13abd44880cd96ddfd",
          "body": "Drop the .stage::before radial glow and the accent-glow layer in the .term\nbox-shadow. The terminal keeps its window drop shadow but no longer sits in a\ncyan halo — cleaner against the dark background. (Applies on tool pages too,\nwhich share .stage.)\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
          "is_bot": false,
          "headline": "fix(site): remove the cyan glow behind the hero terminal",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-06-22T08:08:53Z",
          "body_truncated": false,
          "is_coding_agent": true
        },
        {
          "oid": "8fc19e99225612358a7a3e512e9d2e69dd7478a1",
          "body": "The global display h1 (up to 4.6rem) crowded the hero's split column and read as\noverloaded. Scope a smaller clamp to .hero h1 (max 3.5rem, slightly looser\ntracking); the headline now wraps to three lines with breathing room. Rendered\ndesktop + mobile.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
          "is_bot": false,
          "headline": "fix(site): calmer hero headline so it doesn't feel overloaded",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-06-22T07:58:23Z",
          "body_truncated": false,
          "is_coding_agent": true
        },
        {
          "oid": "d1c7646f413d1689740f33838aa814ad99520158",
          "body": "…shadows\n\nRedesign feedback:\n- Hero: back to the split layout (copy left, the real onlytty terminal right; on\n  mobile it drops below). The custom wide hero terminal is replaced by the\n  product-accurate term_demo — the banner reads top-to-bottom (command, header,\n  scannable QR, then Link/Expires b\n[…]\n shadow (it's a window).\n- Remove the now-dead hero_term + ht-*/deck/stage CSS.\n\nmake check green (85); rendered desktop + mobile.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
          "is_bot": false,
          "headline": "fix(site): split hero back, product-accurate terminal, flatten block …",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-06-22T07:49:18Z",
          "body_truncated": true,
          "is_coding_agent": true
        },
        {
          "oid": "5269742974757aaad49a43ef1dec8716909420e1",
          "body": "Agency-style redesign via the creative-director method (brief → territories →\nchosen direction → rendered passes). The site now reads as the onlytty program's\nown output instead of generic SaaS furniture, retiring the split-hero,\ncentered-3-card, and glow-backdrop clichés.\n\n- Hero: a left-aligned ed\n[…]\n reviewed at 1440/1024/390 (desktop + mobile): no overflow, no JS\nerrors. make check green, with a test for the new proof section.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
          "is_bot": false,
          "headline": "redesign(site): \"standard output\" — a terminal-first editorial home",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-06-22T05:05:29Z",
          "body_truncated": true,
          "is_coding_agent": true
        },
        {
          "oid": "53d53276a3d98ef6d830faec1503861a07971343",
          "body": "Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
          "is_bot": false,
          "headline": "chore(release): v0.2.1",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-06-22T03:35:03Z",
          "body_truncated": false,
          "is_coding_agent": true
        },
        {
          "oid": "604c225d4b678faea4daeab8a074a1d62e540b76",
          "body": "…ttons\n\nOn touch, .btn-sm (nav GitHub / Get started) and .copy computed to ~29–34px,\nunder the WCAG 2.5.5 44px minimum and on the primary mobile conversion path.\nAdd a (pointer: coarse) min-height: 44px.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
          "is_bot": false,
          "headline": "fix(site): meet the 44px tap-target floor for the small nav + copy bu…",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-06-22T03:08:51Z",
          "body_truncated": false,
          "is_coding_agent": true
        },
        {
          "oid": "1905f23138feb07c196e160bcfb7e52e6f03b477",
          "body": "The hero/tool demos labeled the default session \"read-only\", but the default\n(--control ask) auto-grants control to any viewer on request — the opposite of\nwhat the demo implied (and what the rest of the site correctly states). Drop the\ntag from the terminal banner; the phone now shows \"view-only · \n[…]\nl landing state) and \"single viewer\" (the single-viewer lock),\nand the demo's alt text no longer claims you're already in control.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
          "is_bot": false,
          "headline": "content(site): fix the demo's \"read-only\" overclaim of the default",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-06-22T03:08:51Z",
          "body_truncated": true,
          "is_coding_agent": true
        },
        {
          "oid": "45d532ba198fad399cdafa72f2312e77c8f6d373",
          "body": "Review of the product's live surface found several gaps:\n- The session-fingerprint chip shipped `hidden` and was never revealed — the\n  trust signal only flashed in the verify dialog. Reveal it once keys derive,\n  style it, and make it tap/Enter to re-open verify.\n- \"They don't match\" only warned, t\n[…]\nrendering the viewer: fingerprint visible, overlay traps focus and\ninerts the background, no JS errors, bar layout holds at 390px.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
          "is_bot": false,
          "headline": "fix(viewer): harden the trust surface + a11y of the mobile viewer",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-06-22T03:05:40Z",
          "body_truncated": true,
          "is_coding_agent": true
        },
        {
          "oid": "47c9c5bf784158f578d265daeae02739f2590091",
          "body": "…t 500\n\nTwo ways an unauthenticated POST /api/sessions could become a 500 (+ crash-log\nspam) on the public relay, both found in review:\n\n- An absurd ttl_seconds (multi-millennium) made ttl*1000 overflow the BEAM timer\n  → ArgumentError in Session.init → unmatched start_child error → 500. Add a\n  har\n[…]\nroc exit\n  surfaced as 500. Catch it → {:error, :unavailable} → 503, which the runner\n  retries (the :global name clears shortly).\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
          "is_bot": false,
          "headline": "fix(portal): bound ttl + handle the create race so /api/sessions can'…",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-06-22T02:56:41Z",
          "body_truncated": true,
          "is_coding_agent": true
        },
        {
          "oid": "531bad622facd63b270393b8c12bbe5fe14e19e6",
          "body": "…claimer\n\nOnlyTTY is in production; the draft-stage hedge appended to every legal page\n(Terms/Privacy/Acceptable Use) no longer belongs. The pages keep their actual\nterms; only the meta-disclaimer is removed.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
          "is_bot": false,
          "headline": "content(site): drop the \"not legal advice / review before launch\" dis…",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-06-22T02:42:02Z",
          "body_truncated": false,
          "is_coding_agent": true
        },
        {
          "oid": "ab5ca6d79524190c8cbb682aae274e9150b096cc",
          "body": "The OG card showed `$ relay -- claude` (the old binary name) on every social\nshare. The hand-export had no scripted source, so replace it with a reproducible\ngenerator: dev/brand/og.html (built from the live design tokens + the app icon)\nrendered by dev/brand/og-render.mjs (Playwright 2× → sips 1200\n[…]\nal layout; now shows `$ onlytty -- claude`, ~67KB.\nBrand README documents the pipeline; banner.png noted as the superseded source.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
          "is_bot": false,
          "headline": "brand(og): regenerate the social card with the current `onlytty` command",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-06-22T02:00:42Z",
          "body_truncated": true,
          "is_coding_agent": true
        },
        {
          "oid": "eb6f7e5394855dd5970d46d43d480ae8dddc7b6b",
          "body": "`go install github.com/AndrewDryga/onlytty/runner@latest` installed a binary\nnamed `runner` (Go names it after the import path's last element), so the\ndocumented \"go install … then run onlytty\" path was broken. Move package main\nfrom the module root into runner/cmd/onlytty/ — now\n`go install …/runne\n[…]\nthe new path (caveat dropped); the marketing site\nstays curl|sh-only. No CLI behavior change; internal packages and tests unmoved.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
          "is_bot": false,
          "headline": "build(runner): move main to cmd/onlytty so `go install` yields `onlytty`",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-06-22T01:53:10Z",
          "body_truncated": true,
          "is_coding_agent": true
        },
        {
          "oid": "e8add83f3518e98b2e22c374c2e9bd1862082395",
          "body": "The sticky header (brand + GitHub + Get started) had a ~388px min-content floor,\nso on narrow phones (320–375px) the page was 13–28px wider than the viewport and\n\"Get started\" clipped. On ≤400px make the GitHub button icon-only (its new\naria-label keeps the accessible name) and tighten the brand↔cta gap. Verified\ndocument width == viewport at 320/360/375/390; desktop nav unchanged.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
          "is_bot": false,
          "headline": "fix(site): keep the nav from overflowing on ≤388px phones",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-06-22T01:49:26Z",
          "body_truncated": false,
          "is_coding_agent": true
        },
        {
          "oid": "fb43318d150c7e146dad6f9901028f390398e538",
          "body": "The v0.2.0 release run warned these three still target Node 20 (forced onto 24\nnow; will hard-fail when GitHub drops it). Pin to current node24 releases —\ninputs verified unchanged against each action.yml (files / registry+username+\npassword / context+push+tags+labels):\n  softprops/action-gh-release\n[…]\nr release warning (\"xref: [exclude:]\" deprecated) is from deps\n(phoenix/plug/sentry/castore), not our mix.exs — left for upstream.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
          "is_bot": false,
          "headline": "ci(release): bump the Node 20 actions to current node24 versions",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-06-22T01:45:07Z",
          "body_truncated": true,
          "is_coding_agent": true
        },
        {
          "oid": "66f5e7bcf0b4e605a2cc74d0046fe36d6fdf92ca",
          "body": "The endpoint plugged Plug.Session with a hardcoded signing_salt (\"nPKh7BWO\")\nand the rename-residue key \"_relay_key\", but nothing reads the session: the\nrelay has no accounts, and there's no fetch_session/protect_from_forgery\nanywhere (the router is :api/:raw only). Remove the dead plug + @session_options\nso a hardcoded secret isn't shipped in a public repo.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
          "is_bot": false,
          "headline": "security(portal): drop the dead cookie session + hardcoded signing salt",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-06-22T01:41:30Z",
          "body_truncated": false,
          "is_coding_agent": true
        },
        {
          "oid": "c8545491500cff7eaa87d03dfb5bbe34065debdc",
          "body": "Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
          "is_bot": false,
          "headline": "chore(release): v0.2.0",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-06-22T01:27:00Z",
          "body_truncated": false,
          "is_coding_agent": true
        },
        {
          "oid": "656828971beda5b03f3b5e6eb8e15b0c90afae0d",
          "body": "- Install: `curl -fsSL https://onlytty.com/install.sh | sh` is now the\n  consistent install across the hero, the start section, and every tool page\n  (the inner pages showed `go install …/runner@latest`, which installs the\n  binary as `runner`, not `onlytty`). README switched to the branded\n  onlytt\n[…]\nhone status (no expiry). Legal/FAQ already stated this correctly.\n- Tests assert the installer renders on the home and tool pages.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
          "is_bot": false,
          "headline": "docs(site): lead with the install.sh one-liner + freshen copy",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-06-22T01:26:42Z",
          "body_truncated": true,
          "is_coding_agent": true
        },
        {
          "oid": "716fda2e8ae62fef0ba79d6d4c84b694c45f347d",
          "body": "The hero grid's single mobile column (1fr) couldn't shrink below the install\nsnippet's intrinsic width, so the long `curl … | sh` line floored the column at\n~509px and the whole hero was clipped at the right edge on phones.\n\nLet both columns shrink (.hero-copy, .hero-demo { min-width: 0 }) and let t\n[…]\nnippet's code scroll horizontally instead of flooring the layout\n(.snippet code { min-width: 0 }). Verified rendered at 320–430px.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
          "is_bot": false,
          "headline": "fix(site): stop the mobile hero from overflowing the viewport",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-06-22T01:26:34Z",
          "body_truncated": true,
          "is_coding_agent": true
        },
        {
          "oid": "dea554a8646cab6bbe69888a0a7514c59b91caeb",
          "body": "`onlytty -- <cmd>` now works with zero config: when neither --server nor\nONLYTTY_SERVER is set, the runner connects to the hosted relay at\nhttps://onlytty.com (self-hosters still override via --server/ONLYTTY_SERVER).\nThe relay only ever sees ciphertext, so a shared default is safe.\n\nAdds envOr() an\n[…]\nat the default passes the same https/host validation\nas a user-supplied --server, so a fat-fingered constant can't break the flow.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
          "is_bot": false,
          "headline": "feat(runner): default the relay to onlytty.com when unset",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-06-22T01:26:27Z",
          "body_truncated": true,
          "is_coding_agent": true
        },
        {
          "oid": "9dd86765e0376a83093816d5a8ca1430e83e1b3c",
          "body": "… check\n\nProduction served 503 — the global LB marked every backend UNHEALTHY though the\nrelay answered /healthz=200 on the box. Two stacked causes, both blocking the\nLB's plain-HTTP probe to the instance's :4000:\n\n- Container-Optimized OS ships `iptables -P INPUT DROP` (only lo/icmp/\n  ESTABLISHED/\n[…]\nfied live as hotfixes first (gcloud HC --host + iptables on the VMs):\nbackends HEALTHY, https /healthz + / = 200, nodes clustered.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
          "is_bot": false,
          "headline": "fix(infra): open COS host firewall + Host:localhost for the LB health…",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-06-22T00:09:18Z",
          "body_truncated": true,
          "is_coding_agent": true
        },
        {
          "oid": "135373da4656d9d37e6961cee590568d14ab2932",
          "body": "Add wait_for_instances + wait_for_instances_status=UPDATED to the regional MIG so\napply waits for the new version to fully roll out and every instance to be stable.\nCombined with the auto_healing /healthz check, an instance that never goes healthy\nkeeps the group unstable and fails the apply (bounde\n[…]\n exist\nfirst — README setup reordered with a -target secret-bootstrap step and the\ncloudresourcemanager/serviceusage prerequisite.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
          "is_bot": false,
          "headline": "infra: block terraform apply until the MIG rolls out healthy",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-06-21T23:21:26Z",
          "body_truncated": true,
          "is_coding_agent": true
        },
        {
          "oid": "693b0a66caa1e753795d71f58e2a9c4a4ce39b00",
          "body": "systemd failed at EXEC spawning /var/lib/onlytty/start.sh with \"Permission\ndenied\": Container-Optimized OS mounts /var noexec, so it can't execve the\nscript even though it's 0755. Run it through bash (ExecStart=/bin/bash ...) —\nreading the script from /var is allowed and bash is on the exec rootfs.\nVerified on the instance: /var is rw,nosuid,nodev,noexec and /bin/bash exists.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
          "is_bot": false,
          "headline": "infra: run the relay start script via bash (COS mounts /var noexec)",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-06-21T23:11:08Z",
          "body_truncated": false,
          "is_coding_agent": true
        },
        {
          "oid": "1448ed596a67c97038e5ca19337dd665fb1624e5",
          "body": "Delegating nameservers to the Cloud DNS zone makes it authoritative, so the\npre-existing email/verification records must live there or mail + Google domain\nverification break at cutover. Add the Google Workspace MX, the apex SPF +\nsite-verification TXT, the _spfm SPF include, and the _dmarc DMARC re\n[…]\nr records from the old zone too (esp. a Workspace DKIM\nTXT at google._domainkey, and www/subdomains) before switching nameservers.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
          "is_bot": false,
          "headline": "infra: replicate email + verification DNS into the Cloud DNS zone",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-06-21T23:02:11Z",
          "body_truncated": true,
          "is_coding_agent": true
        },
        {
          "oid": "51efc8eebad0711754676298192daa36dd470fe1",
          "body": "Move the Authorization header off curl's command line into a 0600 curl --config\nfile, so the token can't leak via a process listing or xtrace. Guard the secret\nwrite with `{ set +x; }` so it's never echoed even if tracing is later enabled.\ncurl stays -sS (no -v) and the token is never echoed. GitHub already masks the\nsecret in logs — this is defense in depth.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
          "is_bot": false,
          "headline": "deploy.yml: keep TF_API_TOKEN out of argv (no-leak hardening)",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-06-21T22:37:55Z",
          "body_truncated": false,
          "is_coding_agent": true
        },
        {
          "oid": "e1a9660b09e17bb9fff294b3605f884228be25da",
          "body": "Queuing the run failed with only \"curl: (22) 404\" because `curl -f` swallowed the\nresponse body. Wrap the TFC calls in a tf() helper that prints the actual error\nbody, with a hint that a 404 on POST /runs means the token can't queue runs (use\na user/team token with Apply access — an org token can write vars but not create\nruns). Also document the GITHUB_TOKEN release-trigger limitation. Happy path\nunchanged.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
          "is_bot": false,
          "headline": "deploy.yml: surface TFC API errors instead of a bare exit code",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-06-21T22:30:58Z",
          "body_truncated": false,
          "is_coding_agent": true
        },
        {
          "oid": "ba30e8e10752195e162002bca9350ff832ea0bba",
          "body": "Bumps [docker/metadata-action](https://github.com/docker/metadata-action) from 5.10.0 to 6.1.0.\n- [Release notes](https://github.com/docker/metadata-action/releases)\n- [Commits](https://github.com/docker/metadata-action/compare/c299e40c65443455700f0fdfc63efafe5b349051...80c7e94dd9b9319bd5eb7a0e0fe92\n[…]\nemver-major\n...\n\nSigned-off-by: dependabot[bot] <support@github.com>\nCo-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>\nCo-authored-by: Andrew Dryga <andrew@dryga.com>",
          "is_bot": true,
          "headline": "build(deps): bump docker/metadata-action from 5.10.0 to 6.1.0 (#2)",
          "author_name": "dependabot[bot]",
          "author_login": "dependabot[bot]",
          "committed_at": "2026-06-21T22:07:05Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "a0ab89da6fc852b3b779f918f8a0915aae234509",
          "body": "… (#4)\n\nBumps [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance) from 2.4.0 to 4.1.0.\n- [Release notes](https://github.com/actions/attest-build-provenance/releases)\n- [Changelog](https://github.com/actions/attest-build-provenance/blob/main/RELEASE.md)\n- [Commits](h\n[…]\nemver-major\n...\n\nSigned-off-by: dependabot[bot] <support@github.com>\nCo-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>\nCo-authored-by: Andrew Dryga <andrew@dryga.com>",
          "is_bot": true,
          "headline": "build(deps): bump actions/attest-build-provenance from 2.4.0 to 4.1.0…",
          "author_name": "dependabot[bot]",
          "author_login": "dependabot[bot]",
          "committed_at": "2026-06-21T22:07:01Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "a062b1e1dfe3138b63e8947770a313372e6c0990",
          "body": "POST /api/sessions now requires a runner-supplied id + runner_token, so the\nempty-body creates in deploy-check.sh (smoke) and load.sh (load test) 400'd —\nthe Deploy-check workflow had been red since the claim-based change. Generate an\nid+token for the smoke check; in load.sh generate a fresh pair per request so\neach POST creates a new session. Follow-up to 6ca05d4.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
          "is_bot": false,
          "headline": "Fix deploy-check + load scripts for claim-based create",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-06-21T21:57:21Z",
          "body_truncated": false,
          "is_coding_agent": true
        },
        {
          "oid": "f6c25fa3cea8d78fba28eccc832f1038cd0d64cb",
          "body": "Replace the out-of-band `gcloud rolling-action replace` in deploy.yml with a\nTFC-driven deploy: on workflow_dispatch(version) or a published release, set the\n`container_image` workspace variable to ghcr.io/<owner>/onlytty:<version> via the\nTFC API and queue a run; a human approves it in TFC (\"Confir\n[…]\nlice B).\nRemoves the GCP WIF auth path. No-ops without TF_API_TOKEN. README updated.\n\nSlice C of the A/B/C deploy-resilience goal.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
          "is_bot": false,
          "headline": "Deploy via Terraform Cloud, approved with Confirm & Apply",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-06-21T21:52:53Z",
          "body_truncated": true,
          "is_coding_agent": true
        },
        {
          "oid": "84ed857752549f4e5debc54a9aa6a78f4e89a323",
          "body": "On SIGTERM (a deploy draining the instance) the relay now flips /healthz to 503\n(LB stops new connections), tells connected runners/viewers the node is going\naway (they reconnect elsewhere — claim-based resume re-establishes the session),\nwaits a grace, then stops — instead of hard-cutting sockets.\n\n[…]\n\n\nmake check green (portal 81 incl. drain_test); terraform validate clean. e2e in\nCI. Slice B of the A/B/C deploy-resilience goal.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>",
          "is_bot": false,
          "headline": "Graceful drain on SIGTERM: seamless deploys",
          "author_name": "Andrew Dryga",
          "author_login": "AndrewDryga",
          "committed_at": "2026-06-21T21:48:39Z",
          "body_truncated": true,
          "is_coding_agent": true
        }
      ],
      "releases_count": 17,
      "commits_last_year": 271,
      "latest_release_at": "2026-07-10T02:14:10Z",
      "latest_release_tag": "v0.4.10",
      "releases_from_tags": false,
      "days_since_last_push": 18,
      "active_weeks_last_year": 4,
      "days_since_latest_release": 18,
      "mean_days_between_releases": 1.7
    },
    "community": {
      "has_readme": true,
      "has_license": true,
      "has_description": true,
      "has_contributing": false,
      "health_percentage": 57,
      "has_issue_template": false,
      "has_code_of_conduct": false,
      "has_pull_request_template": false
    },
    "ecosystem": {
      "packages": [
        {
          "name": "github.com/AndrewDryga/onlytty",
          "exists": true,
          "license": null,
          "keywords": [],
          "ecosystem": "go",
          "matches_repo": true,
          "registry_url": "https://pkg.go.dev/github.com/AndrewDryga/onlytty",
          "is_deprecated": false,
          "latest_version": "v0.4.11",
          "repository_url": "https://github.com/AndrewDryga/onlytty",
          "versions_count": 20,
          "total_downloads": null,
          "dependents_count": null,
          "deprecation_note": null,
          "maintainers_count": null,
          "monthly_downloads": null,
          "first_published_at": null,
          "latest_published_at": "2026-07-10T04:59:37Z",
          "latest_version_yanked": null,
          "days_since_latest_publish": 18
        }
      ]
    },
    "popularity": {
      "forks": 0,
      "stars": 2,
      "watchers": 0,
      "fork_history": {
        "days": [],
        "complete": true,
        "collected": 0,
        "total_forks": 0
      },
      "star_history": null,
      "open_issues_and_prs": 0
    },
    "ai_readiness": {
      "has_nix": false,
      "example_dirs": [],
      "has_llms_txt": false,
      "has_dockerfile": true,
      "has_mcp_signal": false,
      "bootstrap_files": [
        "Makefile"
      ],
      "api_schema_files": [],
      "has_devcontainer": false,
      "typecheck_configs": [],
      "toolchain_manifests": [
        "portal/mix.exs",
        "runner/go.mod"
      ],
      "largest_source_bytes": 55861,
      "source_files_sampled": 80,
      "oversized_source_files": 0,
      "agent_instruction_files": [
        "AGENTS.md",
        "CLAUDE.md",
        "GEMINI.md"
      ],
      "agent_instruction_max_bytes": 5586
    },
    "dependencies": {
      "manifests": [
        "package.json",
        "portal/mix.exs",
        "runner/go.mod"
      ],
      "advisories": {
        "error": null,
        "scope": null,
        "source": null,
        "findings": [],
        "collected": false,
        "malicious": [],
        "truncated": false,
        "by_severity": {},
        "advisory_count": 0,
        "affected_count": 0,
        "assessed_count": 0,
        "malicious_count": 0,
        "assessed_package": null,
        "unassessed_count": 0,
        "direct_affected_count": 0
      },
      "ecosystems": [
        "go",
        "hex",
        "npm"
      ],
      "dependencies": [
        {
          "name": "phoenix",
          "manifest": "portal/mix.exs",
          "ecosystem": "hex",
          "version_constraint": "~> 1.8"
        },
        {
          "name": "websock_adapter",
          "manifest": "portal/mix.exs",
          "ecosystem": "hex",
          "version_constraint": "~> 0.6"
        },
        {
          "name": "telemetry_metrics",
          "manifest": "portal/mix.exs",
          "ecosystem": "hex",
          "version_constraint": "~> 1.0"
        },
        {
          "name": "telemetry_poller",
          "manifest": "portal/mix.exs",
          "ecosystem": "hex",
          "version_constraint": "~> 1.0"
        },
        {
          "name": "jason",
          "manifest": "portal/mix.exs",
          "ecosystem": "hex",
          "version_constraint": "~> 1.2"
        },
        {
          "name": "libcluster",
          "manifest": "portal/mix.exs",
          "ecosystem": "hex",
          "version_constraint": "~> 3.5"
        },
        {
          "name": "bandit",
          "manifest": "portal/mix.exs",
          "ecosystem": "hex",
          "version_constraint": "~> 1.5"
        },
        {
          "name": "sentry",
          "manifest": "portal/mix.exs",
          "ecosystem": "hex",
          "version_constraint": "~> 13.2"
        },
        {
          "name": "hackney",
          "manifest": "portal/mix.exs",
          "ecosystem": "hex",
          "version_constraint": "~> 4.4"
        },
        {
          "name": "github.com/coder/websocket",
          "manifest": "runner/go.mod",
          "ecosystem": "go",
          "version_constraint": "v1.8.15"
        },
        {
          "name": "github.com/creack/pty",
          "manifest": "runner/go.mod",
          "ecosystem": "go",
          "version_constraint": "v1.1.24"
        },
        {
          "name": "golang.org/x/term",
          "manifest": "runner/go.mod",
          "ecosystem": "go",
          "version_constraint": "v0.44.0"
        },
        {
          "name": "rsc.io/qr",
          "manifest": "runner/go.mod",
          "ecosystem": "go",
          "version_constraint": "v0.2.0"
        }
      ],
      "all_dependencies": {
        "error": "GitHub dependency-graph SBOM unavailable (404); the dependency graph may be disabled for this repository",
        "source": null,
        "packages": [],
        "collected": false,
        "truncated": false,
        "total_count": null,
        "direct_count": null,
        "indirect_count": null
      }
    },
    "maintainership": {
      "issues": {
        "open_prs": 0,
        "merged_prs": 2,
        "open_issues": 0,
        "closed_ratio": null,
        "closed_issues": 0,
        "closed_unmerged_prs": 8
      },
      "bus_factor": 1,
      "bot_contributors": 1,
      "top_contributors": [
        {
          "type": "User",
          "login": "AndrewDryga",
          "commits": 269,
          "avatar_url": "https://avatars.githubusercontent.com/u/1877644?v=4"
        }
      ],
      "contributors_sampled": 1,
      "top_contributor_share": 1
    },
    "quality_signals": {
      "has_ci": true,
      "has_tests": true,
      "ci_workflows": [
        "ci.yml",
        "deploy-check.yml",
        "promote-image.yml",
        "release.yml",
        "selfhost-check.yml"
      ],
      "has_docs_dir": false,
      "linter_configs": [],
      "has_editorconfig": false,
      "has_linter_config": false,
      "has_precommit_config": false
    },
    "security_signals": {
      "lockfiles": [
        "go.sum",
        "mix.lock",
        "package-lock.json"
      ],
      "scorecard": {
        "checks": [
          {
            "name": "Binary-Artifacts",
            "score": 9,
            "reason": "binaries present in source code",
            "documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#binary-artifacts"
          },
          {
            "name": "Branch-Protection",
            "score": 3,
            "reason": "branch protection is not maximal on development and all release branches",
            "documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#branch-protection"
          },
          {
            "name": "CI-Tests",
            "score": null,
            "reason": "no pull request found",
            "documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#ci-tests"
          },
          {
            "name": "CII-Best-Practices",
            "score": 0,
            "reason": "no effort to earn an OpenSSF best practices badge detected",
            "documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#cii-best-practices"
          },
          {
            "name": "Code-Review",
            "score": 0,
            "reason": "Found 0/30 approved changesets -- score normalized to 0",
            "documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#code-review"
          },
          {
            "name": "Contributors",
            "score": 10,
            "reason": "project has 3 contributing companies or organizations -- score normalized to 10",
            "documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#contributors"
          },
          {
            "name": "Dangerous-Workflow",
            "score": 10,
            "reason": "no dangerous workflow patterns detected",
            "documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#dangerous-workflow"
          },
          {
            "name": "Dependency-Update-Tool",
            "score": 10,
            "reason": "update tool detected",
            "documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#dependency-update-tool"
          },
          {
            "name": "Fuzzing",
            "score": 10,
            "reason": "project is fuzzed",
            "documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#fuzzing"
          },
          {
            "name": "License",
            "score": 10,
            "reason": "license file detected",
            "documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#license"
          },
          {
            "name": "Maintained",
            "score": 0,
            "reason": "project was created within the last 90 days. Please review its contents carefully",
            "documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#maintained"
          },
          {
            "name": "Packaging",
            "score": 10,
            "reason": "packaging workflow detected",
            "documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#packaging"
          },
          {
            "name": "Pinned-Dependencies",
            "score": 7,
            "reason": "dependency not pinned by hash detected -- score normalized to 7",
            "documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#pinned-dependencies"
          },
          {
            "name": "SAST",
            "score": 0,
            "reason": "no SAST tool detected",
            "documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#sast"
          },
          {
            "name": "Security-Policy",
            "score": 10,
            "reason": "security policy file detected",
            "documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#security-policy"
          },
          {
            "name": "Signed-Releases",
            "score": 0,
            "reason": "Project has not signed or included provenance with any releases.",
            "documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#signed-releases"
          },
          {
            "name": "Token-Permissions",
            "score": 10,
            "reason": "GitHub workflow tokens follow principle of least privilege",
            "documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#token-permissions"
          },
          {
            "name": "Vulnerabilities",
            "score": 7,
            "reason": "3 existing vulnerabilities detected",
            "documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#vulnerabilities"
          }
        ],
        "commit": "f27b76b8d27761c812e25db45c3fb8de48cc9a3f",
        "ran_at": "2026-07-28T07:52:02Z",
        "aggregate_score": 6.1,
        "scorecard_version": "v5.5.0"
      },
      "has_codeql_workflow": false,
      "has_security_policy": true,
      "has_dependabot_config": true
    },
    "contribution_flow": {
      "collected": true,
      "ci_last_run_at": "2026-07-10T05:07:18Z",
      "oldest_open_prs": [],
      "last_merged_pr_at": "2026-06-21T22:07:05Z",
      "ci_last_conclusion": "FAILURE",
      "oldest_open_issues": []
    }
  },
  "config": {
    "disabled_metrics": [],
    "disabled_categories": [],
    "disabled_components": {}
  },
  "source": {
    "url": "https://github.com/AndrewDryga/onlytty",
    "host": "github.com",
    "name": "onlytty",
    "owner": "AndrewDryga"
  },
  "metrics": {
    "overall": {
      "key": "overall",
      "band": "moderate",
      "name": "Overall health",
      "note": null,
      "notes": [],
      "value": 52,
      "inputs": {
        "security": 61,
        "vitality": 66,
        "community": 24,
        "governance": 46,
        "engineering": 62
      },
      "components": []
    },
    "categories": [
      {
        "key": "vitality",
        "band": "moderate",
        "name": "Vitality",
        "value": 66,
        "weight": 0.22,
        "metrics": [
          {
            "key": "development_activity",
            "band": "moderate",
            "name": "Development activity",
            "note": null,
            "notes": [],
            "value": 50,
            "inputs": {
              "commits_last_year": 271,
              "human_commit_share": 0.98,
              "days_since_last_push": 18,
              "active_weeks_last_year": 4
            },
            "components": [
              {
                "key": "push_recency",
                "name": "Push recency",
                "detail": "last push 18 days ago",
                "points": 28.8,
                "status": "partial",
                "details": [
                  {
                    "code": "push_recency",
                    "params": {
                      "days": 18
                    }
                  }
                ],
                "max_points": 36
              },
              {
                "key": "commit_cadence",
                "name": "Commit cadence",
                "detail": "4/52 weeks with commits",
                "points": 2.8,
                "status": "partial",
                "details": [
                  {
                    "code": "commit_cadence_weeks",
                    "params": {
                      "weeks": 4
                    }
                  }
                ],
                "max_points": 36
              },
              {
                "key": "commit_volume",
                "name": "Commit volume",
                "detail": "271 commits in the last year",
                "points": 18,
                "status": "met",
                "details": [
                  {
                    "code": "commits_last_year",
                    "params": {
                      "count": 271
                    }
                  }
                ],
                "max_points": 18
              },
              {
                "key": "openssf_scorecard_maintained",
                "name": "OpenSSF Scorecard: Maintained",
                "detail": "project was created within the last 90 days. Please review its contents carefully",
                "points": 0,
                "status": "missed",
                "details": [],
                "max_points": 10
              }
            ]
          },
          {
            "key": "release_discipline",
            "band": "excellent",
            "name": "Release discipline",
            "note": null,
            "notes": [],
            "value": 90,
            "inputs": {
              "releases_count": 17,
              "latest_release_tag": "v0.4.10",
              "releases_from_tags": false,
              "days_since_latest_release": 18,
              "mean_days_between_releases": 1.7
            },
            "components": [
              {
                "key": "ships_releases",
                "name": "Ships releases",
                "detail": "17 releases published",
                "points": 27,
                "status": "met",
                "details": [
                  {
                    "code": "releases_published",
                    "params": {
                      "count": 17
                    }
                  }
                ],
                "max_points": 27
              },
              {
                "key": "release_recency",
                "name": "Release recency",
                "detail": "latest release 18 days ago",
                "points": 36,
                "status": "met",
                "details": [
                  {
                    "code": "release_recency",
                    "params": {
                      "days": 18
                    }
                  }
                ],
                "max_points": 36
              },
              {
                "key": "release_cadence",
                "name": "Release cadence",
                "detail": "a release every ~1.7 days",
                "points": 27,
                "status": "met",
                "details": [
                  {
                    "code": "release_cadence",
                    "params": {
                      "gap": 1.7
                    }
                  }
                ],
                "max_points": 27
              },
              {
                "key": "openssf_scorecard_signed_releases",
                "name": "OpenSSF Scorecard: Signed-Releases",
                "detail": "Project has not signed or included provenance with any releases.",
                "points": 0,
                "status": "missed",
                "details": [],
                "max_points": 10
              }
            ]
          },
          {
            "key": "abandonment",
            "band": "excellent",
            "name": "Abandonment",
            "note": null,
            "notes": [],
            "value": 100,
            "inputs": {
              "cap": null,
              "state": "unverified",
              "guards": [],
              "signals": [],
              "red_flag": false,
              "multiplier_pct": 100,
              "declared_reason": null,
              "unverified_reason": "repository_too_young",
              "unanswered_open_prs": null,
              "unanswered_open_issues": null,
              "days_since_last_merged_pr": null,
              "days_since_last_human_commit": null,
              "days_since_last_human_commit_is_floor": false
            },
            "components": [
              {
                "key": "project_is_still_maintained",
                "name": "Project is still maintained",
                "detail": "maintenance record not established from the collected data",
                "points": 100,
                "status": "met",
                "details": [
                  {
                    "code": "abandonment_unverified",
                    "params": {}
                  }
                ],
                "max_points": 100
              }
            ]
          }
        ],
        "description": "Is the project alive — is code being written and are releases shipping?"
      },
      {
        "key": "community",
        "band": "critical",
        "name": "Community & Adoption",
        "value": 24,
        "weight": 0.18,
        "metrics": [
          {
            "key": "popularity",
            "band": "critical",
            "name": "Popularity & adoption",
            "note": null,
            "notes": [],
            "value": 1,
            "inputs": {
              "forks": 0,
              "stars": 2,
              "watchers": 0,
              "growth_state": "unverified",
              "growth_factor_pct": 100,
              "growth_unverified_reason": "no_history"
            },
            "components": [
              {
                "key": "stars",
                "name": "Stars",
                "detail": "2 stars",
                "points": 0,
                "status": "missed",
                "details": [
                  {
                    "code": "stars",
                    "params": {
                      "count": 2
                    }
                  }
                ],
                "max_points": 60
              },
              {
                "key": "forks",
                "name": "Forks",
                "detail": "0 forks",
                "points": 0,
                "status": "missed",
                "details": [
                  {
                    "code": "forks",
                    "params": {
                      "count": 0
                    }
                  }
                ],
                "max_points": 25
              },
              {
                "key": "watchers",
                "name": "Watchers",
                "detail": "0 watchers",
                "points": 0,
                "status": "missed",
                "details": [
                  {
                    "code": "watchers",
                    "params": {
                      "count": 0
                    }
                  }
                ],
                "max_points": 15
              }
            ]
          },
          {
            "key": "community_health",
            "band": "moderate",
            "name": "Community health",
            "note": null,
            "notes": [],
            "value": 50,
            "inputs": {
              "has_readme": true,
              "has_license": true,
              "has_contributing": false,
              "has_issue_template": false,
              "has_code_of_conduct": false,
              "has_pull_request_template": false
            },
            "components": [
              {
                "key": "readme",
                "name": "README",
                "detail": null,
                "points": 22.5,
                "status": "met",
                "details": [],
                "max_points": 22.5
              },
              {
                "key": "license",
                "name": "License",
                "detail": "recognized license (MIT)",
                "points": 22.5,
                "status": "met",
                "details": [
                  {
                    "code": "license_standard",
                    "params": {}
                  },
                  {
                    "code": "license_spdx",
                    "params": {
                      "spdx": "MIT"
                    }
                  }
                ],
                "max_points": 22.5
              },
              {
                "key": "contributing_guide",
                "name": "CONTRIBUTING guide",
                "detail": null,
                "points": 0,
                "status": "missed",
                "details": [],
                "max_points": 18
              },
              {
                "key": "code_of_conduct",
                "name": "Code of conduct",
                "detail": null,
                "points": 0,
                "status": "missed",
                "details": [],
                "max_points": 13.5
              },
              {
                "key": "issue_template",
                "name": "Issue template",
                "detail": null,
                "points": 0,
                "status": "missed",
                "details": [],
                "max_points": 7.2
              },
              {
                "key": "pr_template",
                "name": "PR template",
                "detail": null,
                "points": 0,
                "status": "missed",
                "details": [],
                "max_points": 6.3
              }
            ]
          }
        ],
        "description": "Does the project have users, downloads, attention, and a welcoming setup for contributors?"
      },
      {
        "key": "governance",
        "band": "at_risk",
        "name": "Sustainability & Governance",
        "value": 46,
        "weight": 0.24,
        "metrics": [
          {
            "key": "maintainer_resilience",
            "band": "critical",
            "name": "Maintainer resilience (bus factor)",
            "note": null,
            "notes": [],
            "value": 20,
            "inputs": {
              "bus_factor": 1,
              "contributors_sampled": 1,
              "top_contributor_share": 1
            },
            "components": [
              {
                "key": "bus_factor",
                "name": "Bus factor",
                "detail": "1 contributor(s) cover half of all commits",
                "points": 9,
                "status": "partial",
                "details": [
                  {
                    "code": "bus_factor",
                    "params": {
                      "count": 1
                    }
                  }
                ],
                "max_points": 54
              },
              {
                "key": "commit_distribution",
                "name": "Commit distribution",
                "detail": "top contributor authored 100% of commits",
                "points": 0,
                "status": "missed",
                "details": [
                  {
                    "code": "top_contributor_share",
                    "params": {
                      "share": 100
                    }
                  }
                ],
                "max_points": 22.5
              },
              {
                "key": "contributor_breadth",
                "name": "Contributor breadth",
                "detail": "1 contributors",
                "points": 1.4,
                "status": "partial",
                "details": [
                  {
                    "code": "contributors_sampled",
                    "params": {
                      "count": 1
                    }
                  }
                ],
                "max_points": 13.5
              },
              {
                "key": "openssf_scorecard_contributors",
                "name": "OpenSSF Scorecard: Contributors",
                "detail": "project has 3 contributing companies or organizations -- score normalized to 10",
                "points": 10,
                "status": "met",
                "details": [],
                "max_points": 10
              }
            ]
          },
          {
            "key": "responsiveness",
            "band": "critical",
            "name": "Issue & PR responsiveness",
            "note": "Excluded from scoring (no data or not applicable): Issue resolution. Remaining weights renormalized.",
            "notes": [
              {
                "code": "excluded_no_data",
                "params": {
                  "components": [
                    "issue_resolution"
                  ]
                }
              },
              {
                "code": "weights_renormalized",
                "params": {}
              }
            ],
            "value": 14,
            "inputs": {
              "merged_prs": 2,
              "open_issues": 0,
              "closed_issues": 0,
              "issue_closed_ratio": null,
              "closed_unmerged_prs": 8
            },
            "components": [
              {
                "key": "issue_resolution",
                "name": "Issue resolution",
                "detail": "no issues or no data",
                "points": 0,
                "status": "excluded",
                "details": [
                  {
                    "code": "no_issues_or_data",
                    "params": {}
                  }
                ],
                "max_points": 46.75
              },
              {
                "key": "pr_acceptance",
                "name": "PR acceptance",
                "detail": "2/10 decided PRs merged",
                "points": 7.7,
                "status": "partial",
                "details": [
                  {
                    "code": "decided_prs_merged",
                    "params": {
                      "merged": 2,
                      "decided": 10
                    }
                  }
                ],
                "max_points": 38.25
              },
              {
                "key": "openssf_scorecard_code_review",
                "name": "OpenSSF Scorecard: Code-Review",
                "detail": "Found 0/30 approved changesets -- score normalized to 0",
                "points": 0,
                "status": "missed",
                "details": [],
                "max_points": 15
              }
            ]
          },
          {
            "key": "stewardship",
            "band": "moderate",
            "name": "Ownership & stewardship",
            "note": "Excluded from scoring (no data or not applicable): Verified domain. Remaining weights renormalized.",
            "notes": [
              {
                "code": "excluded_no_data",
                "params": {
                  "components": [
                    "verified_domain"
                  ]
                }
              },
              {
                "code": "weights_renormalized",
                "params": {}
              }
            ],
            "value": 65,
            "inputs": {
              "followers": 213,
              "owner_type": "User",
              "is_verified": null,
              "owner_login": "AndrewDryga",
              "public_repos": 89,
              "account_age_days": 5149
            },
            "components": [
              {
                "key": "ownership_backing",
                "name": "Ownership backing",
                "detail": "personal (user) account",
                "points": 10,
                "status": "partial",
                "details": [
                  {
                    "code": "owner_personal",
                    "params": {}
                  }
                ],
                "max_points": 30
              },
              {
                "key": "verified_domain",
                "name": "Verified domain",
                "detail": "not applicable to user accounts",
                "points": 0,
                "status": "excluded",
                "details": [
                  {
                    "code": "not_applicable_to_user_accounts",
                    "params": {}
                  }
                ],
                "max_points": 20
              },
              {
                "key": "owner_reach",
                "name": "Owner reach",
                "detail": "213 followers of AndrewDryga",
                "points": 16.8,
                "status": "partial",
                "details": [
                  {
                    "code": "owner_followers",
                    "params": {
                      "count": 213,
                      "login": "AndrewDryga"
                    }
                  }
                ],
                "max_points": 25
              },
              {
                "key": "track_record",
                "name": "Track record",
                "detail": "89 public repos, account ~14 yr old",
                "points": 25,
                "status": "met",
                "details": [
                  {
                    "code": "public_repos",
                    "params": {
                      "count": 89
                    }
                  },
                  {
                    "code": "account_age_years",
                    "params": {
                      "years": 14
                    }
                  }
                ],
                "max_points": 25
              }
            ]
          },
          {
            "key": "package_maintenance",
            "band": "excellent",
            "name": "Package maintenance",
            "note": null,
            "notes": [],
            "value": 100,
            "inputs": {
              "packages": [
                "github.com/AndrewDryga/onlytty"
              ],
              "ecosystems": "go",
              "any_deprecated": false,
              "min_days_since_publish": 18
            },
            "components": [
              {
                "key": "published_resolvable",
                "name": "Published & resolvable",
                "detail": "1 package(s) on go",
                "points": 25,
                "status": "met",
                "details": [
                  {
                    "code": "packages_published",
                    "params": {
                      "count": 1,
                      "ecosystems": "go"
                    }
                  }
                ],
                "max_points": 25
              },
              {
                "key": "publish_recency",
                "name": "Publish recency",
                "detail": "latest publish 18 days ago",
                "points": 35,
                "status": "met",
                "details": [
                  {
                    "code": "publish_recency",
                    "params": {
                      "days": 18
                    }
                  }
                ],
                "max_points": 35
              },
              {
                "key": "version_history",
                "name": "Version history",
                "detail": "20 published versions",
                "points": 20,
                "status": "met",
                "details": [
                  {
                    "code": "published_versions",
                    "params": {
                      "count": 20
                    }
                  }
                ],
                "max_points": 20
              },
              {
                "key": "not_deprecated",
                "name": "Not deprecated",
                "detail": "active, not deprecated or yanked",
                "points": 20,
                "status": "met",
                "details": [
                  {
                    "code": "package_not_deprecated",
                    "params": {}
                  }
                ],
                "max_points": 20
              }
            ]
          }
        ],
        "description": "Will the project survive its people — bus factor, responsiveness, who backs it, and package upkeep?"
      },
      {
        "key": "engineering",
        "band": "moderate",
        "name": "Engineering Quality",
        "value": 62,
        "weight": 0.2,
        "metrics": [
          {
            "key": "engineering_practices",
            "band": "moderate",
            "name": "Engineering practices",
            "note": "Excluded from scoring (no data or not applicable): OpenSSF Scorecard: CI-Tests. Remaining weights renormalized.",
            "notes": [
              {
                "code": "excluded_no_data",
                "params": {
                  "components": [
                    "openssf_scorecard_ci_tests"
                  ]
                }
              },
              {
                "code": "weights_renormalized",
                "params": {}
              }
            ],
            "value": 60,
            "inputs": {
              "has_ci": true,
              "has_tests": true,
              "has_editorconfig": false,
              "has_linter_config": false,
              "has_precommit_config": false
            },
            "components": [
              {
                "key": "ci_workflows",
                "name": "CI workflows",
                "detail": "5 workflow(s)",
                "points": 24,
                "status": "met",
                "details": [
                  {
                    "code": "ci_workflows",
                    "params": {
                      "count": 5
                    }
                  }
                ],
                "max_points": 24
              },
              {
                "key": "tests_present",
                "name": "Tests present",
                "detail": null,
                "points": 24,
                "status": "met",
                "details": [],
                "max_points": 24
              },
              {
                "key": "linter_config",
                "name": "Linter config",
                "detail": null,
                "points": 0,
                "status": "missed",
                "details": [],
                "max_points": 16
              },
              {
                "key": "pre_commit_hooks",
                "name": "Pre-commit hooks",
                "detail": null,
                "points": 0,
                "status": "missed",
                "details": [],
                "max_points": 9.6
              },
              {
                "key": "editorconfig",
                "name": ".editorconfig",
                "detail": null,
                "points": 0,
                "status": "missed",
                "details": [],
                "max_points": 6.4
              },
              {
                "key": "openssf_scorecard_ci_tests",
                "name": "OpenSSF Scorecard: CI-Tests",
                "detail": "no pull request found",
                "points": 0,
                "status": "excluded",
                "details": [
                  {
                    "code": "no_data",
                    "params": {}
                  }
                ],
                "max_points": 20
              }
            ]
          },
          {
            "key": "documentation",
            "band": "moderate",
            "name": "Documentation",
            "note": null,
            "notes": [],
            "value": 65,
            "inputs": {
              "topics": [
                "elixir",
                "go",
                "shell",
                "terminal",
                "terraform",
                "tty",
                "remote-access",
                "remote-control",
                "remote-execution",
                "self-hosted"
              ],
              "has_wiki": false,
              "homepage": "https://onlytty.com",
              "has_readme": true,
              "has_docs_dir": false,
              "has_description": true
            },
            "components": [
              {
                "key": "readme",
                "name": "README",
                "detail": null,
                "points": 30,
                "status": "met",
                "details": [],
                "max_points": 30
              },
              {
                "key": "documentation_directory",
                "name": "Documentation directory",
                "detail": null,
                "points": 0,
                "status": "missed",
                "details": [],
                "max_points": 25
              },
              {
                "key": "documentation_homepage_site",
                "name": "Documentation / homepage site",
                "detail": "https://onlytty.com",
                "points": 15,
                "status": "met",
                "details": [],
                "max_points": 15
              },
              {
                "key": "repository_description",
                "name": "Repository description",
                "detail": null,
                "points": 10,
                "status": "met",
                "details": [],
                "max_points": 10
              },
              {
                "key": "topics",
                "name": "Topics",
                "detail": "10 topics",
                "points": 10,
                "status": "met",
                "details": [
                  {
                    "code": "topics_count",
                    "params": {
                      "count": 10
                    }
                  }
                ],
                "max_points": 10
              },
              {
                "key": "wiki",
                "name": "Wiki",
                "detail": null,
                "points": 0,
                "status": "missed",
                "details": [],
                "max_points": 10
              }
            ]
          }
        ],
        "description": "Are baseline engineering and documentation practices in place?"
      },
      {
        "key": "security",
        "band": "moderate",
        "name": "Security",
        "value": 61,
        "weight": 0.16,
        "metrics": [
          {
            "key": "security_posture",
            "band": "moderate",
            "name": "Security posture",
            "note": "Excluded from scoring (no data or not applicable): CI-Tests. Remaining weights renormalized.",
            "notes": [
              {
                "code": "excluded_no_data",
                "params": {
                  "components": [
                    "ci_tests"
                  ]
                }
              },
              {
                "code": "weights_renormalized",
                "params": {}
              }
            ],
            "value": 61,
            "inputs": {
              "source": "openssf_scorecard",
              "checks_evaluated": 17,
              "scorecard_version": "v5.5.0",
              "checks_inconclusive": 1,
              "scorecard_aggregate": 6.1
            },
            "components": [
              {
                "key": "binary_artifacts",
                "name": "Binary-Artifacts",
                "detail": "binaries present in source code",
                "points": 6.8,
                "status": "partial",
                "details": [],
                "max_points": 7.5
              },
              {
                "key": "branch_protection",
                "name": "Branch-Protection",
                "detail": "branch protection is not maximal on development and all release branches",
                "points": 2.2,
                "status": "partial",
                "details": [],
                "max_points": 7.5
              },
              {
                "key": "ci_tests",
                "name": "CI-Tests",
                "detail": "no pull request found",
                "points": 0,
                "status": "excluded",
                "details": [
                  {
                    "code": "no_data",
                    "params": {}
                  }
                ],
                "max_points": 2.5
              },
              {
                "key": "cii_best_practices",
                "name": "CII-Best-Practices",
                "detail": "no effort to earn an OpenSSF best practices badge detected",
                "points": 0,
                "status": "missed",
                "details": [],
                "max_points": 2.5
              },
              {
                "key": "code_review",
                "name": "Code-Review",
                "detail": "Found 0/30 approved changesets -- score normalized to 0",
                "points": 0,
                "status": "missed",
                "details": [],
                "max_points": 7.5
              },
              {
                "key": "contributors",
                "name": "Contributors",
                "detail": "project has 3 contributing companies or organizations -- score normalized to 10",
                "points": 2.5,
                "status": "met",
                "details": [],
                "max_points": 2.5
              },
              {
                "key": "dangerous_workflow",
                "name": "Dangerous-Workflow",
                "detail": "no dangerous workflow patterns detected",
                "points": 10,
                "status": "met",
                "details": [],
                "max_points": 10
              },
              {
                "key": "dependency_update_tool",
                "name": "Dependency-Update-Tool",
                "detail": "update tool detected",
                "points": 7.5,
                "status": "met",
                "details": [],
                "max_points": 7.5
              },
              {
                "key": "fuzzing",
                "name": "Fuzzing",
                "detail": "project is fuzzed",
                "points": 5,
                "status": "met",
                "details": [],
                "max_points": 5
              },
              {
                "key": "license",
                "name": "License",
                "detail": "license file detected",
                "points": 2.5,
                "status": "met",
                "details": [],
                "max_points": 2.5
              },
              {
                "key": "maintained",
                "name": "Maintained",
                "detail": "project was created within the last 90 days. Please review its contents carefully",
                "points": 0,
                "status": "missed",
                "details": [],
                "max_points": 7.5
              },
              {
                "key": "packaging",
                "name": "Packaging",
                "detail": "packaging workflow detected",
                "points": 5,
                "status": "met",
                "details": [],
                "max_points": 5
              },
              {
                "key": "pinned_dependencies",
                "name": "Pinned-Dependencies",
                "detail": "dependency not pinned by hash detected -- score normalized to 7",
                "points": 3.5,
                "status": "partial",
                "details": [],
                "max_points": 5
              },
              {
                "key": "sast",
                "name": "SAST",
                "detail": "no SAST tool detected",
                "points": 0,
                "status": "missed",
                "details": [],
                "max_points": 5
              },
              {
                "key": "security_policy",
                "name": "Security-Policy",
                "detail": "security policy file detected",
                "points": 5,
                "status": "met",
                "details": [],
                "max_points": 5
              },
              {
                "key": "signed_releases",
                "name": "Signed-Releases",
                "detail": "Project has not signed or included provenance with any releases.",
                "points": 0,
                "status": "missed",
                "details": [],
                "max_points": 7.5
              },
              {
                "key": "token_permissions",
                "name": "Token-Permissions",
                "detail": "GitHub workflow tokens follow principle of least privilege",
                "points": 7.5,
                "status": "met",
                "details": [],
                "max_points": 7.5
              },
              {
                "key": "vulnerabilities",
                "name": "Vulnerabilities",
                "detail": "3 existing vulnerabilities detected",
                "points": 5.2,
                "status": "partial",
                "details": [],
                "max_points": 7.5
              }
            ]
          },
          {
            "key": "high_risk_jurisdiction_exposure",
            "band": "excellent",
            "name": "High-Risk Jurisdiction Exposure",
            "note": "Only high-confidence self-published location evidence affects this multiplier. Ambiguous matches are review-only; country evidence is not proof of nationality, citizenship, legal registration, malicious intent, or sanctions status.",
            "notes": [
              {
                "code": "jurisdiction_evidence_limits",
                "params": {}
              }
            ],
            "value": 100,
            "inputs": {
              "meaning": "self-published location evidence; not nationality or citizenship",
              "red_flag": false,
              "exposures": [],
              "policy_countries": [
                "Russia",
                "Iran",
                "North Korea"
              ],
              "review_only_matches": 0,
              "assessed_self_published_locations": 3
            },
            "components": [
              {
                "key": "policy_exposure_multiplier",
                "name": "Policy exposure multiplier",
                "detail": "no confirmed policy-scope location match",
                "points": 100,
                "status": "met",
                "details": [
                  {
                    "code": "jurisdiction_no_match",
                    "params": {}
                  }
                ],
                "max_points": 100
              }
            ]
          }
        ],
        "description": "Are visible security and supply-chain practices strong, with no malicious dependency and no unresolved high-risk jurisdiction exposure?"
      },
      {
        "key": "ai_readiness",
        "band": "good",
        "name": "AI Readiness",
        "value": 75,
        "weight": 0,
        "metrics": [
          {
            "key": "ai_agent_context",
            "band": "excellent",
            "name": "Agent context & guidance",
            "note": null,
            "notes": [],
            "value": 85,
            "inputs": {
              "has_llms_txt": false,
              "legible_history_share": 0.969,
              "agent_instruction_files": [
                "AGENTS.md",
                "CLAUDE.md",
                "GEMINI.md"
              ],
              "agent_instruction_max_bytes": 5586
            },
            "components": [
              {
                "key": "agent_instructions",
                "name": "Agent instructions",
                "detail": "AGENTS.md, CLAUDE.md, GEMINI.md",
                "points": 45,
                "status": "met",
                "details": [
                  {
                    "code": "file_list",
                    "params": {
                      "files": "AGENTS.md, CLAUDE.md, GEMINI.md"
                    }
                  }
                ],
                "max_points": 45
              },
              {
                "key": "machine_readable_docs_llms_txt",
                "name": "Machine-readable docs (llms.txt)",
                "detail": null,
                "points": 0,
                "status": "missed",
                "details": [],
                "max_points": 15
              },
              {
                "key": "legible_commit_history",
                "name": "Legible commit history",
                "detail": "95 of 98 human commits state their intent (structured subject or explanatory body)",
                "points": 40,
                "status": "met",
                "details": [
                  {
                    "code": "legible_history",
                    "params": {
                      "legible": 95,
                      "sampled": 98
                    }
                  }
                ],
                "max_points": 40
              }
            ]
          },
          {
            "key": "ai_verify_loop",
            "band": "good",
            "name": "Verify loop (build / test / typecheck)",
            "note": null,
            "notes": [],
            "value": 75,
            "inputs": {
              "has_nix": false,
              "has_tests": true,
              "lockfiles": [
                "go.sum",
                "mix.lock",
                "package-lock.json"
              ],
              "has_dockerfile": true,
              "typed_language": false,
              "bootstrap_files": [
                "Makefile"
              ],
              "has_devcontainer": false,
              "has_linter_config": false,
              "typecheck_configs": [],
              "agent_commit_share": 0.82,
              "toolchain_manifests": [
                "portal/mix.exs",
                "runner/go.mod"
              ],
              "dependency_bot_commit_share": 0.02
            },
            "components": [
              {
                "key": "one_command_bootstrap",
                "name": "One-command bootstrap",
                "detail": "Makefile",
                "points": 18,
                "status": "met",
                "details": [
                  {
                    "code": "file_list",
                    "params": {
                      "files": "Makefile"
                    }
                  }
                ],
                "max_points": 18
              },
              {
                "key": "automated_tests",
                "name": "Automated tests",
                "detail": null,
                "points": 22,
                "status": "met",
                "details": [],
                "max_points": 22
              },
              {
                "key": "lint_format_config",
                "name": "Lint / format config",
                "detail": null,
                "points": 0,
                "status": "missed",
                "details": [],
                "max_points": 11
              },
              {
                "key": "static_type_checking",
                "name": "Static type checking",
                "detail": null,
                "points": 0,
                "status": "missed",
                "details": [],
                "max_points": 11
              },
              {
                "key": "reproducible_environment",
                "name": "Reproducible environment",
                "detail": "Dockerfile, lockfile",
                "points": 10,
                "status": "met",
                "details": [
                  {
                    "code": "file_list",
                    "params": {
                      "files": "Dockerfile, lockfile"
                    }
                  }
                ],
                "max_points": 10
              },
              {
                "key": "demonstrated_agent_practice",
                "name": "Demonstrated agent practice",
                "detail": "82 of the last 100 commits agent-authored or agent-credited",
                "points": 10,
                "status": "met",
                "details": [
                  {
                    "code": "agent_authored_commits",
                    "params": {
                      "count": 82,
                      "sampled": 100
                    }
                  }
                ],
                "max_points": 10
              },
              {
                "key": "automated_maintenance",
                "name": "Automated maintenance",
                "detail": "2 of the last 100 commits are automated dependency updates",
                "points": 8,
                "status": "met",
                "details": [
                  {
                    "code": "dependency_bot_commits",
                    "params": {
                      "count": 2,
                      "sampled": 100
                    }
                  }
                ],
                "max_points": 8
              },
              {
                "key": "openssf_scorecard_pinned_dependencies",
                "name": "OpenSSF Scorecard: Pinned-Dependencies",
                "detail": "dependency not pinned by hash detected -- score normalized to 7",
                "points": 7,
                "status": "partial",
                "details": [],
                "max_points": 10
              }
            ]
          },
          {
            "key": "ai_code_legibility",
            "band": "moderate",
            "name": "Code legibility for models",
            "note": null,
            "notes": [],
            "value": 55,
            "inputs": {
              "primary_language": "Elixir",
              "largest_source_bytes": 55861,
              "source_files_sampled": 80,
              "oversized_source_files": 0
            },
            "components": [
              {
                "key": "type_checkable_code",
                "name": "Type-checkable code",
                "detail": "Elixir without a type-check config",
                "points": 0,
                "status": "missed",
                "details": [
                  {
                    "code": "no_typecheck_config_language",
                    "params": {
                      "language": "Elixir"
                    }
                  }
                ],
                "max_points": 45
              },
              {
                "key": "manageable_file_sizes",
                "name": "Manageable file sizes",
                "detail": "0/80 source files over 60KB",
                "points": 55,
                "status": "met",
                "details": [
                  {
                    "code": "oversized_source_files",
                    "params": {
                      "kb": 60,
                      "sampled": 80,
                      "oversized": 0
                    }
                  }
                ],
                "max_points": 55
              }
            ]
          }
        ],
        "description": "How well is the repo equipped to be developed and maintained with AI coding agents? An independent, experimental badge — weight 0.0, so it is surfaced on its own and does not affect the overall health score."
      }
    ],
    "metrics_version": "1.13.0"
  },
  "warnings": [
    "Star history unavailable: GitHub GraphQL error: Resource not accessible by personal access token",
    "Could not fetch hex package 'onlytty' from its registry",
    "GitHub dependency-graph SBOM unavailable (404); the dependency graph may be disabled for this repository"
  ],
  "report_type": "repository",
  "generated_at": "2026-07-28T07:52:08.485520Z",
  "schema_version": "0.27.0",
  "badge_url": "https://raw.githubusercontent.com/inspect-software/badges/main/v1/a/AndrewDryga/onlytty.svg",
  "full_name": "AndrewDryga/onlytty",
  "license_state": "standard",
  "license_spdx": "MIT"
}

评分是信号,而非担保。 评分反映的是 GitHub 上公开可见的实践——不是代码审计,也不是安全保证。

缺失数据将被剔除并重新归一化权重,绝不按零分计。方法论已版本化并公开:指标 v1.13.0、模式 v0.27.0—— 完整方法论 · 指标知识库.

单项结果在整体记录中的位置: 汇总统计Go.