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

jose-pr / duho

A declarative CLI framework for Python

PythonMIT★ 0 星标⑂ 0 复刻始于 2026年7月在 GitHub 上查看 ↗

jose-pr/duho 的健康指数为 100 分中的 51 分,处于「中等」区间。 其得分最高的类别是Vitality(69/100),最低的是Community & Adoption(33/100)。 最近一次更新在今天。 近期的大部分工作由 1 位贡献者完成。

51
总分 / 100
中等

软件健康指数

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

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

评分画像

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

所有权

Jose A个人账户
0 关注者43 个公开仓库始于 2014年10月

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

软件包生态系统

注册表软件包版本月下载量版本数最近发布
PyPIduho0.5.0-100 天前

按类别列示的指标

活力

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

69中等 · 占总体的 22%
评分方式
36/36推送新近度 — 最近一次推送于 0 天前
1.4/36提交节奏 — 52 周中有 2 周有提交
18/18提交量 — 最近一年 110 次提交
0/10OpenSSF Scorecard:Maintained — project was created within the last 90 days. Please review its contents carefully
所用输入
commits_last_year110
human_commit_share1
days_since_last_push0
active_weeks_last_year2

发布纪律

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

社区与采用

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

33存在风险 · 占总体的 18%
评分方式
0/60星标 — 0 个星标
0/25复刻 — 0 个复刻
0/15关注者 — 0 位关注者
所用输入
forks0
stars0
watchers0
growth_stateunverified
growth_factor_pct100
growth_unverified_reasonno_history

社区健康

70良好
评分方式
22.5/22.5README
22.5/22.5许可证 — 可识别的许可证(MIT)
18/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

可持续性与治理

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

43存在风险 · 占总体的 24%
评分方式
9/54巴士系数 — 1 位贡献者贡献了半数提交
0/22.5提交分布 — 头号贡献者编写了 100% 的提交
1.4/13.5贡献者广度 — 1 位贡献者
0/10OpenSSF Scorecard:Contributors — project has 0 contributing companies or organizations -- score normalized to 0
所用输入
bus_factor1
contributors_sampled1
top_contributor_share1
评分方式
0/46.8议题解决 — 0% 的议题已关闭
38.2/38.3PR 接受 — 已裁定的 PR 中 1/1 已合并
0/15OpenSSF Scorecard:Code-Review — Found 0/30 approved changesets -- score normalized to 0
所用输入
merged_prs1
open_issues1
closed_issues0
issue_closed_ratio0
closed_unmerged_prs0
评分方式
10/30所有权背书 — 个人(用户)账户
0/20已验证域名 — 不适用于个人账户
0/25所有者影响力 — jose-pr 有 0 位关注者
24/25既往记录 — 43 个公开仓库,账户约 11 年
所用输入
followers0
owner_typeUser
is_verified
owner_loginjose-pr
public_repos43
account_age_days4,299
已排除计分(无数据或不适用):已验证域名。 其余权重已重新归一化。
评分方式
25/25已发布且可解析 — pypi 上有 1 个软件包
35/35发布时效 — 最近一次发布于 0 天前
20/20版本历史 — 10 个已发布版本
20/20未被弃用 — 活跃,未被弃用或撤回
所用输入
packagesduho
ecosystemspypi
any_deprecated
min_days_since_publish0

工程质量

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

66中等 · 占总体的 20%

工程实践

60中等
评分方式
24/24CI 工作流 — 3 个工作流
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。 其余权重已重新归一化。

文档

75良好
评分方式
30/30README
25/25文档目录
0/15文档 / 主页站点
10/10仓库描述
0/10主题标签
10/10Wiki
所用输入
topics
has_wiki
homepage
has_readme
has_docs_dir
has_description

安全

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

39存在风险 · 占总体的 16%

安全态势

39存在风险
评分方式
7.5/7.5Binary-Artifacts — no binaries found in the repo
0/7.5Branch-Protection — branch protection not enabled on development/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
0/2.5Contributors — project has 0 contributing companies or organizations -- score normalized to 0
10/10Dangerous-Workflow — no dangerous workflow patterns detected
0/7.5Dependency-Update-Tool — no update tool detected
0/5Fuzzing — project is not 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
0/5Pinned-Dependencies — dependency not pinned by hash detected -- score normalized to 0
0/5SAST — no SAST tool detected
0/5Security-Policy — security policy file not 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
7.5/7.5Vulnerabilities — 0 existing vulnerabilities detected
所用输入
sourceopenssf_scorecard
checks_evaluated17
scorecard_versionv5.5.0
checks_inconclusive1
scorecard_aggregate3.9
已排除计分(无数据或不适用):ci_tests。 其余权重已重新归一化。

AI 就绪度

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

60中等 · 占总体的 0%
评分方式
45/45代理指令 — src/duho/AGENTS.md
0/15机器可读文档(llms.txt)
40/40可读的提交历史 — 100 次人类提交中有 100 次说明了意图(结构化标题或解释性正文)
所用输入
has_llms_txt
legible_history_share1
agent_instruction_filessrc/duho/AGENTS.md
agent_instruction_max_bytes14,238
评分方式
0/18一条命令的引导启动
22/22自动化测试
0/11Lint / 格式化配置
11/11静态类型检查 — src/duho/py.typed
0/10可复现环境
0/10已体现的代理实践 — 最近 100 次提交中没有代理编写的提交
0/8自动化维护 — 未观察到自动依赖更新
0/10OpenSSF Scorecard:Pinned-Dependencies — dependency not pinned by hash detected -- score normalized to 0
所用输入
has_nix
has_tests
lockfiles
has_dockerfile
typed_language
bootstrap_files
has_devcontainer
has_linter_config
typecheck_configssrc/duho/py.typed
agent_commit_share0
toolchain_manifests
dependency_bot_commit_share0
评分方式
27/45可类型检查的代码 — Python,已配置类型检查(src/duho/py.typed)
54.4/55可控的文件大小 — 采样的 92 个源文件中有 1 个超过 60KB
所用输入
primary_languagePython
largest_source_bytes102,306
source_files_sampled92
oversized_source_files1
评分方式
0/40API 模式(OpenAPI/GraphQL/proto)
20/20MCP 服务器
40/40可运行示例 — examples
所用输入
example_dirsexamples
has_mcp_signal
api_schema_files

关键数据

0GitHub 星标
1贡献者
110最近 12 个月提交数
0距最近推送天数
9发布版本数
1巴士系数(bus factor)
1开放议题
PyPI软件包生态系统数

数据采集警告

  • GitHub dependency-graph SBOM unavailable (404); the dependency graph may be disabled for this repository
  • deps.dev does not index pypi:duho@0.5.0; advisories assessed against the repository dependency graph instead

更多细节

OpenSSF Scorecard 3.9 / 10
3.9综合

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

10Binary-Artifactsno binaries found in the repo
0Branch-Protectionbranch protection not enabled on development/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
0Contributorsproject has 0 contributing companies or organizations -- score normalized to 0
10Dangerous-Workflowno dangerous workflow patterns detected
0Dependency-Update-Toolno update tool detected
0Fuzzingproject is not fuzzed
10Licenselicense file detected
0Maintainedproject was created within the last 90 days. Please review its contents carefully
10Packagingpackaging workflow detected
0Pinned-Dependenciesdependency not pinned by hash detected -- score normalized to 0
0SASTno SAST tool detected
0Security-Policysecurity policy file not detected
0Signed-ReleasesProject has not signed or included provenance with any releases.
10Token-PermissionsGitHub workflow tokens follow principle of least privilege
10Vulnerabilities0 existing vulnerabilities detected
全部依赖 未采集

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

原始 JSON 报告 机器可读
{
  "data": {
    "repo": {
      "topics": [],
      "is_fork": false,
      "size_kb": 618,
      "has_wiki": true,
      "homepage": null,
      "languages": {
        "Python": 790893
      },
      "pushed_at": "2026-07-24T08:04:55Z",
      "created_at": "2026-07-14T05:42:19Z",
      "owner_type": "User",
      "updated_at": "2026-07-24T08:05:13Z",
      "description": "A declarative CLI framework for Python",
      "is_archived": false,
      "is_disabled": false,
      "license_spdx": "MIT",
      "default_branch": "main",
      "license_spdx_raw": "MIT",
      "primary_language": "Python",
      "significant_languages": [
        "Python"
      ]
    },
    "owner": {
      "blog": null,
      "name": "Jose A",
      "type": "User",
      "login": "jose-pr",
      "company": null,
      "location": null,
      "followers": 0,
      "avatar_url": "https://avatars.githubusercontent.com/u/9260810?v=4",
      "created_at": "2014-10-16T02:10:25Z",
      "is_verified": null,
      "public_repos": 43,
      "account_age_days": 4299
    },
    "license": {
      "state": "standard",
      "spdx_id": "MIT",
      "raw_spdx": "MIT",
      "file_present": true,
      "scorecard_found": true,
      "profile_has_license": true
    },
    "activity": {
      "releases": [
        {
          "tag": "v0.5.0",
          "kind": "minor",
          "published_at": "2026-07-24T08:06:45Z"
        },
        {
          "tag": "v0.4.1",
          "kind": "patch",
          "published_at": "2026-07-24T06:27:03Z"
        },
        {
          "tag": "v0.4.0",
          "kind": "minor",
          "published_at": "2026-07-23T23:00:55Z"
        },
        {
          "tag": "v0.3.3",
          "kind": "patch",
          "published_at": "2026-07-18T13:13:32Z"
        },
        {
          "tag": "v0.3.2",
          "kind": "patch",
          "published_at": "2026-07-18T12:41:23Z"
        },
        {
          "tag": "v0.3.1",
          "kind": "patch",
          "published_at": "2026-07-18T02:22:42Z"
        },
        {
          "tag": "v0.3.0",
          "kind": "minor",
          "published_at": "2026-07-18T02:03:18Z"
        },
        {
          "tag": "v0.2.0",
          "kind": "minor",
          "published_at": "2026-07-16T05:05:13Z"
        },
        {
          "tag": "v0.1.1",
          "kind": "patch",
          "published_at": "2026-07-14T22:27:35Z"
        }
      ],
      "recent_commits": [
        {
          "oid": "94fa7fe2eba28dd7142ae5c450e20ebdd42800e8",
          "body": "subprocess.run([_BASH, \"-c\", harness], ...) had no timeout. On a machine\nwhere `bash` on PATH resolves to the WSL launcher shim rather than a native\nbash, invoking it non-interactively via subprocess can hang indefinitely on\nWSL interop instead of erroring -- observed hanging the whole test run\nforever with zero output, no assertion failure to point at. Adds a 10s\ntimeout that skips (with a clear reason) instead of wedging.",
          "is_bot": false,
          "headline": "test: bound the bash-completion subprocess call with a timeout",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-24T08:04:10Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "48ee66386927eba9522afdaac728265a25a63909",
          "body": "This reverts commit 39d220e6f10a92c5722e58b60bbd6d4c2b13e5cf.\n\nv0.5.0's Release CI failed (test_property.py::test_field_round_trip, fixed by\nthe two preceding commits) and never published to PyPI, so there's no\nreleased 0.5.0 to protect -- the tag moves forward to the now-fixed commit\ninstead of bumping to 0.5.1.",
          "is_bot": false,
          "headline": "Revert \"chore: bump version to 0.5.1\"",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-24T07:47:47Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "39d220e4031ac8f858e628222f9150400f6edd69",
          "body": "Folded into 0.5.0's still-unpublished changelog section (renamed, not a new\nentry below it) since 0.5.0 was never tagged/released: the module-command\nreorder fix and the self-shadowing field-name guard both close gaps in the\nsame 0.5.0 change before it ships.",
          "is_bot": false,
          "headline": "chore: bump version to 0.5.1",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-24T07:45:09Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "f5f5bf25bf352605b9c38f7443302d7597779d26",
          "body": "…arly\n\n`bool: bool = False` (a field whose name matches its own annotated type)\nself-shadows within its own statement: Python's class-body execution stores\nthe assigned value BEFORE the annotation expression is evaluated, so\ntyping.get_type_hints sees `False`, not the builtin bool, as the \"type\" --\n\n[…]\n confusing crash.\ntest_property.py's round-trip generator excludes builtin type names as field\nnames, since this is a duho-detected error case, not a shape it should\ngenerate and expect to round-trip.",
          "is_bot": false,
          "headline": "fix: a field named the same as its own type annotation now errors cle…",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-24T07:44:58Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "386ae3697dc48fa4a7b020ef783786188a474b33",
          "body": "_register_module_command built a module command's subparser via a plain\nsubparsers.add_parser(...), never patched by Args._initparser_ -- so only\nduho's own declarative Cmd/Args subcommand tree got the flag-between-\npositionals reorder fix from the prior release. A module command with the\nsame shape\n[…]\nnstance from a parser that was never declared as one.\n\nRegression test reproduces the exact real-world shape (a downstream\nconsumer's `query <ns> -f <val> <targets...>` command) that surfaced the gap.",
          "is_bot": false,
          "headline": "fix: module command subparsers get the positional-reorder fix too",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-24T07:44:49Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "241dd7d4b520a320ea3c0902001dc5dae23d5e30",
          "body": "Minor bump for a breaking change under pre-1.0 semver: list/set/tuple option\nfields now take one value per occurrence by default, plus the flag-between-\npositionals reorder fix.",
          "is_bot": false,
          "headline": "chore: bump version to 0.5.0",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-24T07:21:48Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "658035b07f2e4b306ce5314b504f746aff924ff3",
          "body": "…ence default\n\nExisting tests pinned the old space-separated-in-one-occurrence behavior\nfor list/set/tuple fields used as options; updated to assert the new\nrepeat-flag contract, plus explicit rejection tests for the removed form\nand one test proving NS(nargs=\"*\") still restores it on a specific field.",
          "is_bot": false,
          "headline": "test: update list/set/tuple option tests for the one-value-per-occurr…",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-24T07:17:59Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "ee59e70f9cdd7f42ea7db18b9a99a410864b5380",
          "body": "…e one value\n\nargparse's own greedy positional-run matching (bpo-15112) breaks when an\noption sits between a fixed positional and a variadic one: the run is\nsettled against the argv slice before the next option, so the variadic\npositional closes out empty/short. Args._initparser_'s patched\nparse_kno\n[…]\nady-correctly-ordered argv (verified against\nbare stdlib argparse), so the reorder fix alone could not cover that shape.\nPass an explicit NS(nargs=\"*\") on a specific field to restore the old\nbehavior.",
          "is_bot": false,
          "headline": "fix!: option-between-positionals reorder + list/set/tuple options tak…",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-24T07:17:53Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "81700873338fc741bcf7cc53c1d334a0bc117f41",
          "body": "Bug fixes (Env precedence, register-hook gating, CMDS_PATH-as-layer) plus\none additive feature (module command Args-class support) -- no breaking\nchanges, patch release.",
          "is_bot": false,
          "headline": "chore: bump version to 0.4.1",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-24T06:24:16Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "aa8541c8887965436277efb9873900851b2b81ec",
          "body": "…ter()\n\nA module-level Args (either a real Args subclass, or a plain class with\nannotated fields) declares the module's own CLI fields declaratively, added\nto the subparser before register() runs -- a declared field precedes any\nregister-added positional, matching the existing convention of calling \n[…]\ne root instance,\nper the existing module-command contract) -- this means NS(conflicts=...)/\nNS(group=...) aren't supported for these declared fields yet; register()\nremains the escape hatch for those.",
          "is_bot": false,
          "headline": "feat: module commands can declare an Args class instead of only regis…",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-24T06:21:06Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "37a4fbd069d043f71d66ba42bdbb75e88f89e680",
          "body": "…y-exclusive branch\n\n_resolve_commands returned early for an explicit commands=/source=/\nentry_points=, so CMDS_PATH was only ever consulted in the fallback\nroot._subcommands_ branch -- passing any other command source silently\ndisabled CMDS_PATH entirely, even with env= also given, with no warning \n[…]\n_path_commands/_merge_discovered so env-discovered\ncommands now always merge on top of whichever base source produced the list,\nkeeping the existing \"discovered wins on a name clash, logged\" contract.",
          "is_bot": false,
          "headline": "fix: CMDS_PATH env discovery is a layer, always merged, not a mutuall…",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-24T06:03:33Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "c3130bd5a93057f27d5d77d87b54c7702a2573fa",
          "body": "…ctually called\n\n_register_module_command derived its \"is register real\" gate and its arity\ncheck from a fresh getattr(module, \"register\", ...) instead of\ncommand.register (the object actually invoked). A caller wrapping/\nreassigning command.register directly -- a documented-looking seam, since\nModu\n[…]\no register hook of its\nown (module.register was None there, so the gate never passed), and could\nhave its arity misdetected if the wrapper's signature differed from the\nwrapped module's original hook.",
          "is_bot": false,
          "headline": "fix: gate/introspect a module command's register hook on the object a…",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-24T05:58:08Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "edfe61474c424cf4ed4f952aa1f5840ce5452a0b",
          "body": "__getitem__ checked the module-seeded value before os.environ, so a shipped\n\"<prefix>env.py\" default silently shadowed a real exported <PREFIX>_<KEY>\nvariable -- the class docstring already called these \"defaults\", the\nimplementation now matches. Separates module-seeded values into their own\n_defaults dict (kwargs/runtime writes stay in _env, still highest\nprecedence) so __getitem__ can consult os.environ before falling back to it.",
          "is_bot": false,
          "headline": "fix: Env companion-module defaults no longer override real environment",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-24T05:53:20Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "bffd93858c48acd2750f73edd94a0fb82eedc52b",
          "body": "release.yml's docs-build/docs-deploy only run as part of a tag-triggered\nrelease, with no way to retry just the docs job against a newer commit\nwithout moving the tag. This lets a docs-only fix redeploy without a\nrelease, and keeps docs current between releases when docs/mkdocs.yml/\nREADME.md change on main.",
          "is_bot": false,
          "headline": "ci: add a standalone docs workflow (manual dispatch + push-to-main)",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-23T23:16:09Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "506da000b8ce305e0bf8874e4c6a6e6fd08c8171",
          "body": "… docs_dir)\n\nBlocked the release workflow's `mkdocs build --strict` docs job. README.md\nlives outside docs_dir entirely, so no relative link from a docs/ page can\nresolve it -- point at the GitHub-hosted README section instead.",
          "is_bot": false,
          "headline": "docs: fix dead ../../README.md link (unresolvable within mkdocs's own…",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-23T23:04:52Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "bc231315dde0566f7c9d88759c6384f5e0cc6078",
          "body": "…onflicting subparser\n\n_register_module_command/_register_class_command's collision check only\ntracked names registered during app()'s own resolution loop (`registry`),\nnever the root's own pre-registered `_subcommands_` builtins already sitting\nin the subparsers action before the loop starts. A CMD\n[…]\nr instead of the\nolder ValueError some test/CI environments tolerated differently. Seed\n`registry` with the preregistered builtins so the existing collision-and-\nderegister path also covers this case.",
          "is_bot": false,
          "headline": "fix: CMDS_PATH override of a preregistered builtin no longer raises c…",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-23T22:55:35Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "d082f0ab37a33030a155e571d4083d6bb33a04db",
          "body": "…ble`\n\nMatches the private predecessor's own token spelling convention (its\nfrom_matchstring encoded a disable as a trailing :!enable token) -- the\n`.enabled` attribute name is unaffected, only the user-facing token spelling\n(`enable`/`!enable`) changes.",
          "is_bot": false,
          "headline": "fix: rename the filename/--rcopts enable token from `enabled` to `ena…",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-23T22:07:43Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "28f17a34daa7d79deb43e68e905b46d772aefcd5",
          "body": "Add DiscoveryAppArgs/RunpathAppArgs shared root classes (a data+method mixin\nstyled after the \"one shared Args class every subcommand uses\" pattern) to\ndiscovery_app.py/runpath_app.py, exercising config/env layering (_config_,\nNS(env=...)) and Choice/Count/Append helper factories that fileinstall.py\n[…]\n step calls a plain module-level helper function, since a\nprovider-built RunPathCmd only inherits data via register(base=), not an\nad-hoc method declared on a caller-side class it never derives from).",
          "is_bot": false,
          "headline": "docs: examples exercise a shared root, its own method, and real logging",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-23T21:56:20Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "cc3f68b88b98a01097dc5d7a880ac6a1e5eaba4a",
          "body": "…ia register(base=)\n\nA provider-built RunPathCmd subclass previously had no real _logger_/\n_set_loglevels_ methods -- app()'s parents= mechanism only copies a root's\nDATA fields onto a class command's parsed instance, never its methods, so\n-v/stderr logging setup never activated for any RunPath comm\n[…]\nmodule's own long-documented \"usual app shape\"), so -v/_logger_ work out of\nthe box, and an app with a custom shared root can pass it via\nregister(base=MyAppRoot) to get its own methods inherited too.",
          "is_bot": false,
          "headline": "feat: RunPath commands inherit LoggingArgs by default, configurable v…",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-23T21:56:07Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "f91229ab32950e524a9328215f33592f20cdae44",
          "body": "Additive API surface (RunPath __main__.py lifecycle, filename-encoded\nper-step options, BEFORE/AFTER) and a bug fix (Extend() field flattening) --\nminor bump, no breaking changes.",
          "is_bot": false,
          "headline": "chore: bump version to 0.4.0",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-23T21:11:25Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "0bce498c8805682272fb827cef085aa546bf87ea",
          "body": null,
          "is_bot": false,
          "headline": "docs: add type hints to example module commands and RunPath steps",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-23T21:11:17Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "abaf49eb9c40b452fca45eb481a38a7b01e856d0",
          "body": "… nav\n\nAdd examples/discovery_app.py (module + class command discovery from loose\nfiles) and examples/runpath_app.py (a full RunPath feature tour: __main__.py\nlifecycle, BEFORE/REQUIRED/AFTER, filename-encoded !strict), both with\nsmoke tests. Add docs/guide/runpath.md and a \"Discovering commands from\nfiles\" section in running.md, wired into mkdocs.yml nav.",
          "is_bot": false,
          "headline": "docs: examples for path/rc discovery, runpath guide, examples in docs…",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-23T21:08:31Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "2b8af7125ef8e4c329c8f92973f3a34a56095168",
          "body": "…/AFTER\n\nAdd three additions to duho.runpath's opt-in step-runner:\n\n- An optional per-directory __main__.py lifecycle (init/success/finally_,\n  mirroring ModuleCommand's hooks) so a RunPath dir can build a shared client\n  or connection once and hand it to every step as ctx; a step written\n  def main\n[…]\nun-wide strict/!strict flag.\n- BEFORE/AFTER soft ordering constants alongside the existing hard REQUIRED,\n  styled after systemd's Before=/After=, merged into the existing topological\n  ordering pass.",
          "is_bot": false,
          "headline": "feat: RunPath __main__.py lifecycle, filename-encoded options, BEFORE…",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-23T21:08:21Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "e33acdfc98388d08fac64b2ad34a05cae4c8b02b",
          "body": "A list[str] field's default nargs=\"*\" combined with Extend's comma-splitting\ntype caused each occurrence's split result to be appended as one nested\nelement instead of flattening (--rcopts '!*,build' produced [['!*','build']]\ninstead of ['!*','build']). Every existing caller (duho.runpath's rcopts\nfield) bypassed real argparse parsing in tests, so this went undetected.\nExtend() now sets nargs=None so a single occurrence's split flattens directly.",
          "is_bot": false,
          "headline": "fix: Extend() field type no longer double-collects into nested lists",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-23T21:08:13Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "844ee93edf0034cfd8b98fb37c8de04ff1447561",
          "body": "README.md is force-included into the wheel so a consumer can read it\nfrom site-packages via importlib.resources without the repo.\n\nsrc/<pkg>/AGENTS.md documents library *use* and ships inside the\npackage, so it must not link to repo paths an installed consumer does\nnot have; those references now point at the project URL. The repo-root\nAGENTS.md is development documentation and is deliberately not shipped.",
          "is_bot": false,
          "headline": "chore: ship README.md in the wheel; make the API header self-contained",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-22T05:54:02Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "30669be665fba58fc65a39799e8b3e3a1c7d353e",
          "body": "Precedence made CMDS_PATH all-or-nothing: setting it dropped an app's built-in\ncommands entirely, so a supplementary command directory could not be used\nwithout every invocation depending on the variable being right. The usual\nreason to point at one is \"I have a few extra commands\", not \"replace thi\n[…]\narents=, also carried the root's subparsers action. Every\n  subcommand therefore nested the whole command tree and demanded its own\n  `command` argument. Stripped.\n\n673 passed (3 new), no regressions.",
          "is_bot": false,
          "headline": "feat: CMDS_PATH extends _subcommands_ instead of replacing them",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-21T06:26:42Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "5ac2b04f3de7d8af8c4fe0969c75d6d7be934d95",
          "body": "An AGENTS.local.md (or any *.local.md) is the unshared override -- machine- or\nuser-specific -- and must never be committed. Nothing excluded it before.",
          "is_bot": false,
          "headline": "chore: gitignore *.local.md",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-21T05:46:45Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "5400a812c77c3dac1ce1b073fd6f5ebed99395d5",
          "body": "`dotagents link` makes <project>/.agents a symlink, which git treats as a file,\nso a directory-only `.agents/` pattern does not ignore it.",
          "is_bot": false,
          "headline": "chore: gitignore .agents slashless so the symlink is matched",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-21T04:34:32Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "b106d63d4782696d70a8f6ef65769d95365a0da1",
          "body": "Root AGENTS.md is now a committed, public dev-facing overview; src/duho/AGENTS.md\nis the header-file-style public API reference for consuming agents. The former\nplan-by-plan private working notes move to .agents/AGENTS.md (gitignored). Stop\ngitignoring root AGENTS.md and clear a stale \"Phase 2\" test comment so the tree is\nleak-clean.",
          "is_bot": false,
          "headline": "docs: adopt D40 AGENTS.md convention (public overview + API header)",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-21T01:01:39Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "ad458b0cc6362744fbd02e7c92de8e81b459be35",
          "body": "README gains an \"MCP tool surface\" section; CHANGELOG [Unreleased] notes the\nnew opt-in duho.mcp submodule; examples/mcp_app.py is a runnable sample app.",
          "is_bot": false,
          "headline": "docs: document duho.mcp (README, CHANGELOG) + runnable example",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-20T22:58:20Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "621c04ca73020227e63a045f91b8533461a46ba5",
          "body": "Opt-in, standalone submodule (like runpath/fanout/scaffold — core duho\nnever imports it, not on the top-level duho.* surface) exposing a duho\nCLI's Cmd/Cli classes as MCP (Model Context Protocol) tools with zero\nredeclaration:\n\n- input_schema_for_command / json_schema_for_field: a type -> JSON Schem\n[…]\ny function-local throughout, preserving the\nzero-eager-import contract (tests/test_config_json.py::test_json_import_is_lazy,\ntests/test_entry_points.py::test_entry_points_lazy_import both still pass).",
          "is_bot": false,
          "headline": "feat: add duho.mcp — MCP tool surface over a duho CLI",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-20T22:53:18Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "dfab07e9b7c7b3683cc6cad1c5460d183652b274",
          "body": null,
          "is_bot": false,
          "headline": "docs: changelog note for CMDS_PATH os.pathsep fix",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-20T22:04:40Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "f3484db2cd9a8b0a9cb735a925fb8308fc1038d2",
          "body": "Drop the eager `from . import agenthelp` and load the submodule on first\nattribute access via a PEP 562 module __getattr__. agenthelp is only needed\nwhen agent help actually fires (all entry points already import it lazily at\ncall time), so `import duho` now resolves no extra submodule -- keeping th\n[…]\nimal and ensuring agenthelp can never be pulled in at\nentry-point load (test_entry_points_lazy_import). `duho.agenthelp` and\n`import duho.agenthelp` still work on demand; `agenthelp` stays in __all__.",
          "is_bot": false,
          "headline": "fix: import duho.agenthelp lazily (keep `import duho` free of it)",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-20T22:04:09Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "a74284accb4ac146eeaefdbbd588a411f23e8501",
          "body": "CMDS_PATH was split with a hard-coded ':' separator, so on Windows an\nabsolute path's drive-letter colon (\"C:\\...\") was mis-split into a bogus\n\"C\" entry -> ImportError: not a directory: C. Add Env.paths(), which splits\na path-list env var on the platform separator (os.pathsep: ';' on Windows,\n':' on\n[…]\np default, drive-letter-not-split, PATHSEP\noverride, custom type, missing->[]) and a multi-dir CMDS_PATH resolution\ntest joined with os.pathsep; the two single-dir CMDS_PATH tests now pass on\nWindows.",
          "is_bot": false,
          "headline": "fix: split CMDS_PATH on os.pathsep (PATHSEP override), not ':'",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-20T22:04:08Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "adbdb657bfbe9390f61e59bedc0a1226047659aa",
          "body": null,
          "is_bot": false,
          "headline": "docs: document agent help (README section + changelog)",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-20T21:47:12Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "69405696e121ffba6dca072bac905c29a6b524aa",
          "body": "Add duho.agenthelp: a detailed, machine-readable (JSON) description of a\nCLI, built on the existing introspection (get_clsargs / ClsArgDeclaration +\nper-field ArgumentBuilder). It covers every subcommand (with aliases), each\noption's type/default/required/repeatable/choices, positionals, per-field\ne\n[…]\n its argparse actions. json stays lazily imported.\n\nNew public surface: duho.agenthelp, duho.print_agent_help(cls), and the\nCli class attrs _agent_help_ / _agent_help_env_ / _examples_ / _exit_codes_.",
          "is_bot": false,
          "headline": "feat: agent-oriented help (machine-readable --help for AI agents)",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-20T21:47:12Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "34bff1f4513f03971c3336c009dec6eb9fc61ee5",
          "body": "Wire CI for Plan 03: install zsh and fish on the Linux test legs so the T3\ncompletion syntax-check tests actually run instead of skipping, and add a\ndedicated coverage-gate job on ubuntu (py3.12, zsh/fish installed) running\npytest --cov=duho --cov-fail-under=92 (floor(93.66)-1, == the plan post-T\ntarget). The ubuntu matrix already spans 3.9-3.13. Kept separate from the\nPlan 02 benchmark gate.",
          "is_bot": false,
          "headline": "ci: coverage gate + install zsh/fish for completion tests",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-19T20:00:35Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "7a14f2a5b6b09d28a5a34c9d21eec61762ad12b1",
          "body": "Add hypothesis to the dev extra and tests/test_property.py: field round-trip\n(a drawn-type field written to a real temp module, parsed from generated argv,\nvalue must equal the expected conversion -- covering str/int/float/bool/Path/\nEnum/Literal/list/set/tuple/Optional), duho.expand vs a naive Cartesian-product\nreference (length == product of range sizes), snakecase [a-z0-9_]* invariant,\nand parse_loglevels never raising on separator soup with a well-shaped mapping.",
          "is_bot": false,
          "headline": "test: property-based round-trip suite (hypothesis)",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-19T20:00:35Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "4129770d68c03156a9ac01f868f626e0e1256f4c",
          "body": "Add tests/test_parsers_internals.py exercising pop_action/insert_action/\nadd_help_argument directly, the disable/enable_subparser_check round-trip, and\nthe key invariant that prerun_parse restores every per-instance __class__ swap\n(help + subparsers actions) and the subparsers required flag even when\nparse_known_args raises mid-call -- with no argparse class attribute left\nmutated.",
          "is_bot": false,
          "headline": "test: direct unit tests for parsers helpers + exception-path restoration",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-19T19:53:32Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "3af12de719e6ea40c6ebfd752597f967b2f5ec95",
          "body": "Add tests/test_logging_color.py (named + compound 'red+white' color resolution\nvia colorama, and graceful plain output when colorama is absent),\ntests/test_introspect_fallback.py (getclsdef returns None for exec-built classes\nwith a bogus/missing __module__, and parses non-ASCII UTF-8 source), and Env\nmapping-protocol tests (pop/popitem/iteration; environ is a read-only underlay,\nso popping an environ-only key raises -- documenting the 01-D2 decision).",
          "is_bot": false,
          "headline": "test: logging color, introspect fallback, Env mapping protocol",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-19T19:52:48Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "ac7f5bb107eed1365f568c9ed0e99c43b4db694d",
          "body": "Add tests/test_error_messages.py asserting the actual message text for a bad\nenv value (names the variable + field), a bad config value (names field + class),\nunion multi-factory exhaustion ('could not convert ... using any of'), the\nfixed-length-tuple build-time error, and main() on a non-runnable data Args.",
          "is_bot": false,
          "headline": "test: user-facing error-message contracts in args.py",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-19T19:51:10Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "d6f5ce195edd04e28519a2bf1a2cfb215b358e04",
          "body": "Add tests/test_runtime_gaps.py covering CMDS_PATH env-driven resolution (real\ntmp dir), setup_logging installing a stderr handler exactly once, a non-Cmd\nselected leaf raising NotImplementedError, name/description overrides reaching\nthe parser, the module register 3-arg logger fallback to the 'duho' logger when\nthe root has no _logger_, a non-dict [subcommand] config table being tolerated,\nand the advisory-prepass SystemExit degrading to no-prepass.",
          "is_bot": false,
          "headline": "test: runtime app() coverage gaps",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-19T19:50:05Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "571200aceff28af0272194a9d905a16e56b49147",
          "body": "Add tests/test_completion_positionals.py covering the previously-0%-coverage\npositional branches of all four emitters (bash/zsh/fish/powershell) via a\nrequired/choice/variadic/Path positional fixture, plus zsh -n and\nfish --no-execute syntax checks (skipif shell absent). Functional bash tests\nsource\n[…]\n Injection tests prove a hostile choice never executes;\nthe intact single-candidate round-trip is documented xfail(strict) since\ncompgen -W word-splits metacharacter values (execution-hardening only).",
          "is_bot": false,
          "headline": "test: completion positional branches + functional bash + injection",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-19T19:47:44Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "180c175fdc33600c74ebd3bc933258994891dced",
          "body": "…er_integration\n\nAdd tests/test_e2e.py spawning duho CLIs as child processes with\nPYTHONPATH=src to cover the production sys.argv path, SystemExit-to-exit-code\npropagation, the real -- passthrough split, and --version. Rename the\nmisnamed in-process parser-composition tests accordingly.",
          "is_bot": false,
          "headline": "test: real subprocess e2e tests; rename test_integration to test_pars…",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-19T19:45:33Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "0cd1a593b17afb765f6e9b750adca39a23f6a7ca",
          "body": "Apply Plan 04's 'Rejected candidates' doc/test actions:\n\n- Negative-number handling: document that negative values work as option values\n  and positionals out of the box (argparse's _negative_number_matcher), with the\n  NS(kwargs=...) escape hatch for the rare -1-style flag; add a regression test\n  \n[…]\n exit codes (an IntEnum return\n  propagates as the process exit code unchanged); add a test and a running-guide\n  example.\n\n(The duho.Count() README type-table row was already added in the F1 commit.)",
          "is_bot": false,
          "headline": "docs: negative-number + IntEnum exit-code notes and regression tests",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-19T19:41:22Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "2c26d7cb7d5e488849b0659085f3f3a7ac17eb61",
          "body": "Add a fourth completion emitter, `duho.completion.powershell(parser)`, alongside\nbash/zsh/fish. It walks the same shell-agnostic `CompletionSpec` tree and emits a\n`Register-ArgumentCompleter -Native` script block that reconstructs the\n(sub)command path from the non-flag words on the line (skipping t\n[…]\n expanded.\n\nTests assert emitted-script content and escaping; a pwsh parse smoke-check runs\nonly when pwsh is on PATH (skipped otherwise). Adds README + completion-guide\nupdates and a CHANGELOG entry.",
          "is_bot": false,
          "headline": "feat: PowerShell completion (F9)",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-19T19:39:15Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "39347ee3217fd0c9bde6292de869c4893af15b63",
          "body": "Add a class-level `_help_formatter_` attribute (declared on `Cli`, read by\n`Args._parser_` and plumbed into argparse's `formatter_class`, and propagated\nacross a static `_subcommands_` tree) plus three new public formatters in a new\n`duho.formatters` module:\n\n- `DefaultsFormatter` -- appends `(defau\n[…]\nsFormatter` -- the two composed.\n\nAll three are off by default: plain `--help` is unchanged unless a class opts in.\nAdds README + running-guide sections, an API formatters page, and a CHANGELOG\nentry.",
          "is_bot": false,
          "headline": "feat: opt-in defaults-in-help + ANSI help formatters (F8)",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-19T19:35:43Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "e865d7a221b4cc4c6a7a6de4e4c15b329b94fab2",
          "body": "`_load_config` now dispatches on suffix: a `.json` config path is parsed with the\nstdlib `json` module (imported lazily, so a non-JSON config never pays its import\ncost; a malformed file raises a ValueError naming the file), while `.toml` and\nany other suffix keep the existing tomllib/tomli path. JS\n[…]\n any format) loader without duho importing or depending on it. Threaded\nthrough both `_apply_default_layers` (parse/main) and `duho.app`.\n\nAdds README + config-guide subsections and a CHANGELOG entry.",
          "is_bot": false,
          "headline": "feat: JSON config + pluggable loader hook (F7)",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-19T19:30:42Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "4832f276542dd5081778eed6ba69d2fa1eea34c3",
          "body": "Add `duho.app(root, entry_points=\"group\")` and the public\n`duho.discover_entry_points(group)`: load commands advertised by installed\ndistributions' entry points in a group, coercing each loaded object to a\ncommand (a `Cmd` subclass -> class command; a module -> module command) through\nthe same path \n[…]\nared framework base,\nleaking via inheritance to every subclass and mis-naming their subcommands.\n\nAdds README subsection, a guide page (guide/plugins.md), an API discovery page,\nand CHANGELOG entries.",
          "is_bot": false,
          "headline": "feat: entry-points plugin discovery (F6)",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-19T19:28:25Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "ccd1967e15561f29208b569250eabbfd9e030e6d",
          "body": "Add a @dataclass Meta with the known field-metadata keys (help, env,\nconflicts, conflicts_required, group, action, nargs, const, choices, metavar,\nrequired, type, version, dest, kwargs), each defaulting to a private sentinel.\n_getargs_ merges only the explicitly-set fields, so an unknown keyword is \n[…]\nnition time instead of the silent no-op NS(...) gives.\nNS keeps working. PEP-727 Doc duck-typing (a .documentation str attr) already\ncontributes help (C8) and is now documented. Exported as duho.Meta.",
          "is_bot": false,
          "headline": "feat: typed metadata duho.Meta (F5)",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-19T19:16:04Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "f3b481b36a87be3f117de96dec64d57fa531a321",
          "body": "A Cmd whose __call__ is `async def` returns a coroutine; duho.main and\nduho.run_command drive it to completion with asyncio.run at the call site\n(via a shared _maybe_await helper), so the awaited value becomes the exit\ncode. asyncio is imported lazily -- a synchronous app never pays its import\ncost. A fan-out worker dispatching the command per target gets its own\nasyncio.run per call. Module-command lifecycle hooks stay synchronous.",
          "is_bot": false,
          "headline": "feat: async __call__ support (F4)",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-19T19:13:15Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "7403a3b9b5f638fc5e280797096c8a60e0896242",
          "body": "F2: NS(conflicts=..., conflicts_required=True) on any member makes the group\nrequired; requiredness is pre-computed across members and passed at group\ncreation. F3: NS(group=\"Title\") buckets a field under a lazily-created titled\nargument group; a field with both group= and conflicts= nests the exclusive\ngroup inside the titled group (keyed by (group, conflicts)). Titled groups are\npersisted on the parser for parents=/override reuse.",
          "is_bot": false,
          "headline": "feat: required mutually-exclusive + titled argument groups (F2, F3)",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-19T19:11:27Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "dd42d03feac1766b4e30d907f33bee8df9bd8dd2",
          "body": "Promote the KEY=VALUE dict pattern to a first-class annotation. A dict[K, V]\nfield collects KEY=VALUE tokens merged via UpdateAction; the value half is\nconverted with V. Bare dict == dict[str, str]; non-str keys are a build-time\nerror; missing '=' is a clean argparse error. Default {}. Env (k=v ->\no\n[…]\npair dict) and TOML-table config layers convert through the value factory.\nUpdateAction now makes a shallow per-occurrence copy instead of deepcopy.\nREADME type table gains dict and duho.Count() rows.",
          "is_bot": false,
          "headline": "feat: first-class dict[str, V] fields (F1)",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-19T19:08:55Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "c6910c23f10cc2d2a82b34a44b95f8f77956512c",
          "body": "Add fresh-process startup deltas (bench_startup.py), subcommand-tree\nscaling and a field-type matrix (run.py, via new shared _bench.py),\na discovery benchmark (bench_discovery.py), a committed baseline.json\nwith update_baseline.py/check_baseline.py, and wire a regression gate\ninto the CI benchmark j\n[…]\n).\nRe-label compare_cache.py output cold-vs-warm. Document the\nbenchmark/update workflow in CONTRIBUTING.md. All benchmark tooling is\nstdlib-only and stays excluded from the sdist (zero runtime deps).",
          "is_bot": false,
          "headline": "perf: benchmark harness upgrade + CI regression gate (P6)",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-19T17:47:07Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "d4096e08a4972b674a0076342b7ca9b047f6c3b9",
          "body": "getclsdef now returns None immediately when a module file was indexed\nsuccessfully but the class qualname is absent -- a type()/duho.command()\nclass has no literal ClassDef, so the inspect.getsource fallback re-parses\nthe same file and fails the identical lookup, only slower (up to ~23 ms per\nclass in a large dynamic tree). The fallback stays for the no-module-file\ncase (REPL/exec). Adds a test asserting getsource is never called for a\ntree of command(...)-generated classes.",
          "is_bot": false,
          "headline": "perf: skip getsource fallback for dynamically-created classes (P5)",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-19T17:39:45Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "7de11155e50401606628b99d701d5dccf5cacc15",
          "body": "…k (P2, P3)\n\nP2: seed empty _duho_constants_ on Args/Cmd/Cli so _class_constants\nshort-circuits for the framework bases -- building any user parser no\nlonger AST-parses args.py. LoggingArgs is deliberately NOT seeded (its\nbody declares real fields). P3: _module_index now recurses only into\nstatement\n[…]\n-field build drops from ~12.7 ms to ~7.6 ms. Adds tests/test_introspect.py\n(classes under if/try/finally/function-locals/class-in-class resolve;\n_module_index output identical to the exhaustive walk).",
          "is_bot": false,
          "headline": "perf: stop AST-parsing duho's own source; statement-only qualname wal…",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-19T17:38:40Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "f2efe500f0f1b3a78747f2a80e2733d22a024f23",
          "body": "Move the ~30 ms importlib.metadata import into _resolve_version's AUTO\nbranch and defer colorama to first named-color use in logging._getcolor.\nA plain `import duho` drops from ~75 ms to ~54 ms (min, fresh process).\nAUTO-version tests now monkeypatch importlib.metadata directly (the local\nimport binds the same cached module object).",
          "is_bot": false,
          "headline": "perf: lazy-import importlib.metadata and colorama (P1, P4)",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-19T17:35:17Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "4ed98c93c5761ff78de4fb7b216bd31f03b0b4fe",
          "body": "…aths (C12, M2, M8, fish)\n\nEscape every interpolated value per shell: bash word lists backslash-escape\ncompgen-expansion triggers so a hostile choice like $(...) cannot execute at\nTab-press, zsh/fish single-quoted contexts escape embedded quotes, and a program\nname with whitespace/metacharacters is \n[…]\ne (C12). Skip a value-taking flag's argument when reconstructing the bash\ncommand path (M8). Emit single-dash multi-char fish flags with -o and use the\none-line help as the -d description (fish nits).",
          "is_bot": false,
          "headline": "fix: harden completion emitters — escaping, zsh optspecs, bash/fish p…",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-19T17:30:14Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "b06e588990771d2278331d849825ca4b5e0d7136",
          "body": "…M12, M16, M18, M19, M22)\n\nsnakecase now lowercases interior uppercase with an underscore and returns \"\"\nfor empty (C13). value_sources compares against effective defaults and merges\nsubcommand provenance to the root (C14). _getcolor resolves fore+back and never\nreturns the raw compound string (M9).\n[…]\neps a child's overridden default (M16).\nrelative_to with an empty base returns self (M19). run_command runs success only\non a successful exit and never lets a raising finally_ mask the original (M22).",
          "is_bot": false,
          "headline": "fix: text/qualname/logging/introspect/lifecycle bundle (C13, C14, M9-…",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-19T17:23:39Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "ad0684e12a42b077768f2d1089652050ba9e1b66",
          "body": "…cleanup (M1, M20)\n\nReplace prerun_parse's process-global monkey-patching of\n_SubParsersAction.__call__/_HelpAction.__call__ with per-instance __class__\nswaps restored in finally, so the prepass is thread-safe and reentrant and never\nmutates argparse's own classes (M1). pop_action now also removes the action from\nits argument group's _group_actions so a popped flag disappears from\nformat_help() (M20).",
          "is_bot": false,
          "headline": "fix: per-instance argparse surgery in prerun_parse; pop_action group …",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-19T17:17:09Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "b84ca95110c6e1de1789618c83c106ec2ee72091",
          "body": "Normalise a fan-out target's return inside the isolation boundary so a non-int\nvalue is logged and counted as failure instead of escaping run_targets (M5).\nIn runpath, skip a step whose import raises ImportError/NotImplementedError\n(warn resilient / raise strict), warn or raise when an enabled step REQUIRES a\ndisabled step, and raise on a REQUIRED cycle under strict; SyntaxError and other\nbody errors still surface (M4).",
          "is_bot": false,
          "headline": "fix: fanout non-int isolation and runpath import/dep resilience (M5, M4)",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-19T17:14:49Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "30b6f97dcf66efd33848b244341ea5ccec59aaa3",
          "body": "…rt (C11, M3)\n\nEnv.list now returns [] for a missing/empty value instead of [ty(\"\")], so a\nmissing <PREFIX>_CMDS_PATH no longer resolves to [Path(\".\")] and glob-imports\nthe CWD (C11); runtime._resolve_commands double-guards on a non-empty raw value.\nEnv autoload now seeds only UPPER_CASE non-undersc\n[…]\n(str coercion so env.bool never sees a raw bool), and\naccepts autoload=False to skip the sys.path/CWD import (M3). README + docs\nupdated for the new contracts, plus the layered value-conversion rules.",
          "is_bot": false,
          "headline": "fix: Env.list empty contract + autoload hardening; CMDS_PATH CWD impo…",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-19T17:12:32Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "9bfeda9751ec88a1750236f153da0e27b8aeba24",
          "body": "…ta (C6-C10, C15, M15, M17)\n\nExtract the type dispatch into _factory_for and recurse Union members through\nit so Optional[list[int]] / Optional[Literal[...]] get their full spec, and\nreject collection members inside multi-member unions (C6). Copy mutable\ncollection defaults per build/parse (C7). Tol\n[…]\nnal (C9). Route a bool\nLiteral through type=+choices= (C10). Add date/datetime/time fromisoformat\nfactories (C15). Reject a set flags container (M15) and honor SUPPRESS anywhere\nin the metadata (M17).",
          "is_bot": false,
          "headline": "fix: conversion/build ladder for unions, mutable defaults, and metada…",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-19T17:09:36Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "546e9b916da887cb42f6ab22dc77c6b1917272fc",
          "body": "…C4, C5, M6)\n\nSuppress the root's inherited option defaults on every app() subparser so a\nglobal given before the subcommand or supplied by env/config survives to the\ndispatched command, including required globals (C4). Load config once and apply\nthe root layer before the advisory prepass, degrading\n[…]\nno longer hard-exits (C5). Register commands\nthrough a single name->(kind, command) registry: a duplicate name warns and the\nlast registration wins, with dispatch resolving via the same registry (M6).",
          "is_bot": false,
          "headline": "fix: app() default-layer drift for root globals and name collisions (…",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-19T17:03:13Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "cc8fba151c76b8841e4402acfd018b264b97ff12",
          "body": "…elds (C1, C2, C3, M14)\n\nIntroduce ArgumentBuilder.convert_layered so env/config layer values are\nconverted to match CLI parsing: strict bool parsing (C1), single-element\ncollection wrapping (C2), and type conversion of non-string TOML values (M14).\nDrop SUPPRESS-suppressed dests from a child parser\n[…]\nroot\noption given before a subcommand survives (C3).\n\nM13 (un-require positional via nargs) could not be reproduced: argparse's\nexisting action.required=False already un-requires a layered positional.",
          "is_bot": false,
          "headline": "fix: layered env/config value conversion for bool/collection/typed fi…",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-19T16:57:27Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "0a48f21b7bb3367f21f5e591e8fae6c5b61d642f",
          "body": "Remove unused imports (typing in completion, argparse in presets, stat in\nscaffold) and an orphaned dead helper (_zsh_value_spec) with its unused-result\ncallers in completion. No behavior change. The `from logging import *` under\nTYPE_CHECKING in logging is a deliberate type-checker re-export and is kept.\n\nFull suite: 377 passed on 3.9.",
          "is_bot": false,
          "headline": "chore: lint cleanup (dead imports/helper); release 0.3.3",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-18T13:09:01Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "00104c571dbe2e06a3186873ae0d8a098852b8ba",
          "body": null,
          "is_bot": false,
          "headline": "chore(release): 0.3.2",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-18T12:39:23Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "95239682e82eb5a8eed03c99d2101037ef6bed12",
          "body": null,
          "is_bot": false,
          "headline": "chore: ignore .venv* variant venv dirs",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-18T07:22:08Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "25d38a184b0556a6cf9731c1d4302d61c48ca7fd",
          "body": null,
          "is_bot": false,
          "headline": "docs: changelog for the review-finding fixes",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-18T07:22:08Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "bc6222e029dc8ad1c2ad17a761d570e872cee464",
          "body": "…ction defaults\n\nFour fixes surfaced by the buildutils consumer:\n\n- _parser_ escapes docstring-derived description/help (% -> %%) so a literal %\n  in a Cmd docstring no longer crashes parser build (argparse %-expands help).\n- A root's _subcommands_ children now SUPPRESS inherited optional defaults f\n[…]\ne groups as\n  parser.exclusive_groups, so a _parser_ override can add args into a\n  conflicts=-built group.\n\n12 regression tests in tests/test_review_findings.py. Suite: 377p/3s (3.9),\n378p/2s (3.14).",
          "is_bot": false,
          "headline": "fix: escape docstring %, unshadow global options, seed direct-constru…",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-18T07:22:08Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "4ff9d2e2ea1a23ff825957849ded8c848edc5866",
          "body": "Patch release: clearer error when a subcommand register() hook collides with a\nroot global flag, plus the reserved-flags doc note.",
          "is_bot": false,
          "headline": "chore(release): 0.3.1",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-18T02:20:29Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "fd8c70dd357b2af934fa23e483b057594379b9c5",
          "body": "…l flag\n\napp builds each subcommand parser with parent-arg inheritance, so a module\nregister(parser, args) hook that adds a flag already owned by the root (e.g. -q\nfrom a LoggingArgs root) crashed with argparse's bare \"conflicting option\nstring: -q\" on --help. Catch that ArgumentError and re-raise naming the command\nand the global-flag cause, and document the root's reserved flags in the README\nso a command author avoids them. Adds a regression test.",
          "is_bot": false,
          "headline": "fix: clearer error when a subcommand register() collides with a globa…",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-18T02:19:05Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "4b8128435d25e02b662aaed63afb3ef08e01893d",
          "body": "Bump version to 0.3.0 and finalize the CHANGELOG [0.3.0] section. Also\nclarify main-vs-app in the README (main = simple/static runner; app =\ndiscovery + config/env + dispatch override).",
          "is_bot": false,
          "headline": "chore(release): 0.3.0",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-18T02:00:54Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "cc186b966a560ae8d2255243506b3f9320211a03",
          "body": "Add a \"Generating launchers (duho.scaffold, opt-in)\" README section and\nan [Unreleased] CHANGELOG entry for the new opt-in launcher generator.",
          "is_bot": false,
          "headline": "docs: document duho.scaffold launcher generator",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-18T01:50:26Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "ad8bd47268adcf475064f7bf4179201944d89dec",
          "body": "Add duho.scaffold, an opt-in stdlib-only dev tool (not on the core\nduho.* surface) that generates a cross-platform launcher pair so an app\nlaid out as bin/ + a lib/src package runs from a checkout without an\ninstall.\n\ngenerate_launchers(app, root, *, libdir=\"lib\", python=None,\noverwrite=False) write\n[…]\nPYTHON override,\nlibdir) on real tmp_path files without spawning a shell, skip the POSIX\nexec-bit check on Windows, and positively verify the emitted text is\ngeneric. Full suite green on 3.9 and 3.14.",
          "is_bot": false,
          "headline": "feat: add opt-in duho.scaffold launcher generator",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-18T01:50:25Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "2cf306aecced5fa98be599d2665674dce0991762",
          "body": "Extend the argument factory's collection handling beyond list. A field\nannotated set, set[T], bare tuple, or variadic tuple[T, ...] now parses like\na list field (repeated --x a --x b and space-separated --x a b forms, per-\nelement type conversion, bare forms default to str elements), but the final\nv\n[…]\nueError at parser build naming the field, since\nit needs per-position types the collection path can't express.\n\nIncludes tests (both interpreters), README field-type table rows, and a\nchangelog entry.",
          "is_bot": false,
          "headline": "feat: support set/set[T] and tuple[T, ...] collection fields",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-18T01:39:43Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "400834db98ddc5bf25fbf2f837a206abefd1485d",
          "body": "…mmand aliases\n\nAdd README coverage for four already-shipped behaviors that were hard to\ndiscover from the existing docs:\n\n- the per-field docstring is optional (flags-tuple alone declares an argument;\n  docstring only sets help=)\n- NS(conflicts=\"group\") mutually-exclusive option groups\n- positional arguments with nargs (required, optional-with-default, variadic list)\n- subcommand aliases via _parseraliases_",
          "is_bot": false,
          "headline": "docs: document optional docstrings, conflicts, positionals, and subco…",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-18T01:35:14Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "34bf5fe8842940d407c1ef907af2aebbde9ed9ad",
          "body": "The example file and class were named after (and its docstrings referenced)\nan internal predecessor project. Rename to the generic `fileinstall.py` /\n`FileInstall` and scrub provenance from the docstrings, keeping the example's\npurpose (exercising positionals, Union types, conflicts groups, enum choices).\nUpdates the smoke tests and README references.",
          "is_bot": false,
          "headline": "refactor: rename buildutils example to fileinstall",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-18T01:29:13Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "9cc0c6fb85b80e0077743ade8609f0b6960ba532",
          "body": "A command module's optional register hook may now be written either 2-arg\nregister(parser, args) or 3-arg register(parser, args, logger). The driver\ninspects the hook's signature (inspect.signature) and, for a hook declaring a\nthird positional (or a *args catch-all), passes\nlogger = getattr(args, \"_\n[…]\nwhile\nstaying fully backward-compatible with existing 2-arg hooks.\n\nUpdated the ModuleCommand and runtime docstrings, README, and CHANGELOG; added\n2-arg / 3-arg / *args tests in tests/test_runtime.py.",
          "is_bot": false,
          "headline": "feat: accept a 3-arg module register(parser, args, logger) hook",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-18T01:26:49Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "06c4ce767ba726ca43c97dea626842ada62f8343",
          "body": "Expose a public helper to parse only a root command's global args while\nignoring/relaxing the subcommand tree, so a discovery-driven app can resolve a\nconfig-file path (or any global) before building the full subcommand parser.\nIt wraps the internal help-suppressed, subcommand-relaxed prepass\n(parse\n[…]\nbparsers action first so trailing tokens\nbecome discarded extras, matching how app's prepass runs before add_subparsers.\n\nExported from duho.__all__; tests in tests/test_parsers.py; README, CHANGELOG.",
          "is_bot": false,
          "headline": "feat: add duho.parse_globals for config-before-commands parsing",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-18T01:22:39Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "8d3b0ce0ab1c1aa23f1961d56d72daee0bae1977",
          "body": null,
          "is_bot": false,
          "headline": "docs: changelog entry for camelcase trailing-separator fix",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-18T01:14:34Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "73a518efbf4b4ec2b4cddb1b5f2fe6980121dc96",
          "body": "camelcase did `part[0]` on every split segment, so a trailing, doubled,\nor leading separator produced an empty segment and raised IndexError\n(\"x_\".split(\"_\") == [\"x\", \"\"]). Skip empty parts. This is hit in practice:\npysafe turns a keyword like `global` into `global_`, and camelcasing that\nname (via PythonName.new / QualName.camelcase) then lands on the trailing\nunderscore. Adds regression tests for trailing/doubled/leading separators.",
          "is_bot": false,
          "headline": "fix: camelcase crashes on trailing/doubled separator",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-18T01:13:42Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "02c59829b13912c79632dc12c3ee24acbf338349",
          "body": "Replace the \"target fan-out is client glue\" README section with the opt-in\nduho.fanout helper (run_targets / fan_out_command, per-target [tgt] prefixed\nlogging, max aggregation) plus the app(dispatch=) composition example, keeping a\nnote that you can still hand-roll a ThreadPoolExecutor. Add matching CHANGELOG\n[Unreleased] entries. No version bump.",
          "is_bot": false,
          "headline": "docs: document duho.fanout and the app(dispatch=) seam",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-18T01:11:10Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "ff97ad8e00800eb1610c7a49082064078bbd1d17",
          "body": "An opt-in, stdlib-only module (core never imports it; off the top-level duho.*\nsurface) for running one command against many targets concurrently and rolling\ntheir exit codes into one.\n\nrun_targets(func, targets, *, max_workers=None, aggregate=max) runs func(target)\nper target on a ThreadPoolExecuto\n[…]\ner, no per-target handler churn. Threads-first,\nstdlib-only; an asyncio variant is deferred. Tests cover aggregation, exception\nisolation, serial vs parallel, no-leak, and cross-tag under concurrency.",
          "is_bot": false,
          "headline": "feat: add opt-in duho.fanout target fan-out helper",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-18T01:11:03Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "d46729749165990479ae75b8831adbacc33c4dd3",
          "body": "app() owns discovery, parser build, registration, config/env thread-down,\nparsing, and logging setup, then runs one selected command. Add an optional\ndispatch(command, instance) -> int callback that replaces only that final run\nstep, so a consumer needing a custom run contract (per-invocation contex\n[…]\n\nre-deriving it. Both dispatch sites route through\n`run = dispatch if dispatch is not None else run_command`; with dispatch=None\nthe path is byte-identical to before. The seam is the only core change.",
          "is_bot": false,
          "headline": "feat: add app(dispatch=) seam to override the final run step",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-18T01:10:52Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "25c4f4722de48e46999f550efa06d18fbf5da754",
          "body": "Add a \"RunPath: ordered step commands (opt-in)\" README section (directory\nlayout, import-to-activate, PRIORITY/REQUIRED, --rcopts/-O selection syntax,\nand the strict-vs-resilient reconciliation) after the discovery section, and a\nCHANGELOG [Unreleased] Added entry noting the module is opt-in and additive.",
          "is_bot": false,
          "headline": "docs: document opt-in duho.runpath step-runner",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-18T00:56:30Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "3bb6561eba0648790ef372a52810ed40ae3feb98",
          "body": "Turn a directory of numbered NN-name.py files into one command that runs\nthem in order. The module plugs into the Plan-13 register_command_provider\nhook (its first consumer) and needs zero core changes: core duho never\nimports it; `import duho.runpath` auto-registers the provider as the opt-in\ngestu\n[…]\ne the provider registry so state never leaks. The pre-existing\ntest_discovery bare-directory test now clears the provider registry so the\nopt-in auto-register cannot leak into it (order-independence).",
          "is_bot": false,
          "headline": "feat: add opt-in duho.runpath ordered step-runner",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-18T00:56:20Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "4bdbce78377ffbabe456fb2bfabc04b9afa76025",
          "body": "The Unreleased section still described the Cmd entrypoint as a `main`\nmethod (and even said to migrate toward it) — stale from before the\n__call__ correction. Fix the wording to __call__ throughout.",
          "is_bot": false,
          "headline": "docs: correct CHANGELOG to __call__ command entrypoint",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-18T00:42:14Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "ddb4675d8f38cd92f48027da3bbe6f78e2f20aff",
          "body": "README: add a \"Cli: the application root\" section (opt-in mixin over\nCmd, the batteries-included class MyApp(LoggingArgs, Cli) recipe, the\n@MyApp.subcommand decorator for decentralized command files, and the\nduho.app config/env thread-down with self._env_). CHANGELOG: add the\nCli app-root, @subcommand self-registration, and app() config/env\nthread-down under [Unreleased] (all additive, non-breaking).",
          "is_bot": false,
          "headline": "docs: document Cli app-root + @subcommand self-registration",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-18T00:40:12Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "a90188344412c44f7ad16a04388ccb7e5ef746dd",
          "body": "Add duho.Cli, an opt-in mixin over Cmd for the root of a multi-command\napp. It types and documents the app-wide sandwich-named config attrs a\nleaf Cmd omits (_version_, _distribution_, _completion_, _config_,\n_subcommands_) without changing any getattr-based reader -- purely\nadditive; a plain Cmd ro\n[…]\ntional (not PEP-604 |) because get_type_hints evaluates\nall annotations before the _-prefix filter, so a | union would crash at\nparser-build on 3.9. Tests in tests/test_cli.py (14, green on 3.9+3.14).",
          "is_bot": false,
          "headline": "feat: add Cli app-root layer + @subcommand self-registration",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-18T00:40:04Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "d89bf95581a4b0d1363894a1681c2c1ca36f5e9b",
          "body": "…n field collision\n\nA Cmd subclass's namespace is user-owned: annotated non-underscore attrs\nbecome CLI fields, so a `main` method would collide with a user-declared\n`main`/`--main` field. `__call__` is a dunder that field introspection\nskips, so it never collides. The class-command entrypoint is no\n[…]\n`.\n\nModule-command entrypoints (top-level `def main(args)`) and the\nModuleCommand wrapper's `.main()` adapter API are unchanged -- a module is\nnot a CLI-field namespace, so no collision applies there.",
          "is_bot": false,
          "headline": "refactor: make Cmd entrypoint __call__ instead of main to avoid --mai…",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-18T00:01:17Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "faf9ab9d96ae5676358181d28baf487684f0372c",
          "body": "README: new example-driven sections for the Args-vs-Cmd split (with an\nupgrade note for the breaking data/runnable change), Env, string/target\nexpansion, dynamic command discovery, module commands and their lifecycle,\ncustomizing a subcommand parser, passthrough args, and target fan-out as\nclient gl\n[…]\ncommand discovery (discover_commands,\nCmdBuilder, ModuleCommand, register_command_provider). The Cmd/command,\n_passthrough_, app/run_command, and BREAKING Args/Cmd split entries were\nalready recorded.",
          "is_bot": false,
          "headline": "docs: document runtime capabilities and Args/Cmd split (Plan 13 Phase 6)",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-17T15:12:22Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "008f95c6bc0956bfff86da60d978176561dc112a",
          "body": "Finalize the package's public surface for the Plan 13 runtime layer: add\nEnv, the text/name utilities (expand, pysafe, camelcase, snakecase,\ngettext), PythonName/QualName, the Cmd/command command model, command\ndiscovery (discover_commands, CmdBuilder, ModuleCommand, Command,\nregister_command_provider), and the app/run_command driver to the\ntop-level imports and __all__ (40 names, alphabetized).",
          "is_bot": false,
          "headline": "feat: export runtime capabilities from duho public API (Plan 13 Phase 6)",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-17T15:12:12Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "254a99236a53fbee0eccce5071b3dbcc53a77823",
          "body": "Wire discovered commands into a runnable app on the existing\n_parser_/_initparser_/\"#cls\" machinery -- no parallel parser class.\n\n- run_command(command, instance, *, context=None): dispatch one resolved\n  command. Class command -> instance.main(); module command -> the\n  init -> main -> success / fi\n[…]\nhapes, lifecycle order (success + error propagation),\nregister hook (parse + help), _passthrough_, resilient discovery, and\nreturn codes. 15 tests pass on py-3.9 and py-3.14; full suite green on both.",
          "is_bot": false,
          "headline": "feat: add app/run_command multi-command runner (Plan 13 Phase 5)",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-17T14:54:27Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "60c478d8dd18f6639cf2905c8174c856b2646df9",
          "body": "Turn a class, module, import path, or directory into runnable Cmds.\n\n- Command protocol (runtime_checkable) + is_class_command/is_module_command\n  predicates classify class commands (Cmd subclasses) and module commands.\n- ModuleCommand: a plain wrapper (not a ModuleType subclass) adapting a\n  comman\n[…]\ndules and continue; a real SyntaxError still\n  propagates.\n\nTests: 33 in tests/test_discovery.py using real tmp_path fixture files.\nPass on py-3.9 and py-3.14; full suite green on both (no new skips).",
          "is_bot": false,
          "headline": "feat: add command discovery + CmdBuilder (Plan 13 Phase 4)",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-17T14:45:01Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "b858e51c0f652d74e26fa74068d70f4632d020f7",
          "body": "Drop mentions of the private predecessor library from shipped docstrings,\ncomments, and test notes; keep the behavioral rationale (why the split\ncontract, why not zero-padded, the snakecase WIP quirk) without naming the\nsource. Provenance notes stay only in the private AGENTS files.",
          "is_bot": false,
          "headline": "docs: remove internal source references from library code and tests",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-17T14:37:40Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "338534d9f588c0f9712953bbcc91ed1ea4d1dd43",
          "body": "…h_ (Plan 13 Phase 3)\n\nReverse Plan 04's \"every Args is callable\" (a pre-1.0 break). Args becomes\npure data; a new Cmd(Args) carries the executable contract: primary entrypoint\nmain(self), with __call__ delegating to it so a Cmd stays directly callable.\nCmd.main's base stub raises NotImplementedErro\n[…]\n_cmd.py covers Cmd dispatch, __call__->main delegation,\nbare-Args error, command() builder, _passthrough_ (with/without/multiple `--`),\nand LoggingArgs+Cmd MRO. Full suite green on py-3.9 and py-3.14.",
          "is_bot": false,
          "headline": "feat: split Args (data) from Cmd (executable) + command()/_passthroug…",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-17T14:35:07Z",
          "body_truncated": true,
          "is_coding_agent": false
        },
        {
          "oid": "25289d805a6831ef20e5d953c2efcc3dbbb0b3a9",
          "body": "Port coquilib's zero-dependency string/name utilities:\n- text.py: expand (brace-range expansion, not zero-padded), pysafe,\n  camelcase, snakecase (WIP quirk preserved), gettext shim\n- qualname.py: QualName / DotQualNamed / PythonName dotted-name algebra\n\nQuoted union annotations throughout for the 3.9 floor. 39 tests pass on\npy-3.9 and py-3.14; full suite green.",
          "is_bot": false,
          "headline": "feat: add text utils and PythonName qualname algebra (Plan 13 Phase 1)",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-17T14:25:40Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "adcd6ca1e8a8102e385161c9768e3ca504935878",
          "body": "Port coquilib's CoquiEnv to duho.Env: a prefixed, app-wide environment\naccessor over os.environ with .bool/.list typed accessors and an optional\n<prefix.lower()>env companion-module autoload. Fixes coquilib's broken\n__len__ (len(self.__iter__())) to sum(1 for _ in self). Union annotations\nquoted for clean import on Python 3.9. Stdlib only, zero runtime deps.",
          "is_bot": false,
          "headline": "feat: add prefixed Env accessor (Plan 13 Phase 2)",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-17T14:24:54Z",
          "body_truncated": false,
          "is_coding_agent": false
        },
        {
          "oid": "042c277d58fd64e23ee0bf150d50c44efc15ec5d",
          "body": null,
          "is_bot": false,
          "headline": "docs: explain tar-style mode dispatch",
          "author_name": "Jose A",
          "author_login": "jose-pr",
          "committed_at": "2026-07-17T00:13:35Z",
          "body_truncated": false,
          "is_coding_agent": false
        }
      ],
      "releases_count": 9,
      "commits_last_year": 110,
      "latest_release_at": "2026-07-24T08:06:45Z",
      "latest_release_tag": "v0.5.0",
      "releases_from_tags": false,
      "days_since_last_push": 0,
      "active_weeks_last_year": 2,
      "days_since_latest_release": 0,
      "mean_days_between_releases": 1.2
    },
    "community": {
      "has_readme": true,
      "has_license": true,
      "has_description": true,
      "has_contributing": true,
      "health_percentage": 57,
      "has_issue_template": false,
      "has_code_of_conduct": false,
      "has_pull_request_template": false
    },
    "ecosystem": {
      "packages": [
        {
          "name": "duho",
          "exists": true,
          "license": null,
          "keywords": [
            "Development Status :: 4 - Beta",
            "Environment :: Console",
            "Intended Audience :: Developers",
            "License :: OSI Approved :: MIT License",
            "Operating System :: OS Independent",
            "Programming Language :: Python :: 3",
            "Programming Language :: Python :: 3.10",
            "Programming Language :: Python :: 3.11",
            "Programming Language :: Python :: 3.12",
            "Programming Language :: Python :: 3.13",
            "Programming Language :: Python :: 3.9",
            "Topic :: Software Development :: Libraries :: Python Modules",
            "Topic :: System :: Shells",
            "Typing :: Typed"
          ],
          "ecosystem": "pypi",
          "matches_repo": true,
          "registry_url": "https://pypi.org/project/duho/",
          "is_deprecated": false,
          "latest_version": "0.5.0",
          "repository_url": "https://github.com/jose-pr/duho",
          "versions_count": 10,
          "total_downloads": null,
          "dependents_count": null,
          "deprecation_note": null,
          "maintainers_count": null,
          "monthly_downloads": null,
          "first_published_at": "2026-07-14T21:16:12.707200Z",
          "latest_published_at": "2026-07-24T08:07:11.308490Z",
          "latest_version_yanked": null,
          "days_since_latest_publish": 0
        }
      ]
    },
    "popularity": {
      "forks": 0,
      "stars": 0,
      "watchers": 0,
      "fork_history": {
        "days": [],
        "complete": true,
        "collected": 0,
        "total_forks": 0
      },
      "star_history": {
        "days": [],
        "complete": true,
        "collected": 0,
        "total_stars": 0,
        "collected_at": null
      },
      "open_issues_and_prs": 1
    },
    "ai_readiness": {
      "has_nix": false,
      "example_dirs": [
        "examples"
      ],
      "has_llms_txt": false,
      "has_dockerfile": false,
      "has_mcp_signal": true,
      "bootstrap_files": [],
      "api_schema_files": [],
      "has_devcontainer": false,
      "typecheck_configs": [
        "src/duho/py.typed"
      ],
      "toolchain_manifests": [],
      "largest_source_bytes": 102306,
      "source_files_sampled": 92,
      "oversized_source_files": 1,
      "agent_instruction_files": [
        "src/duho/AGENTS.md"
      ],
      "agent_instruction_max_bytes": 14238
    },
    "dependencies": {
      "manifests": [
        "pyproject.toml"
      ],
      "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": [
        "pypi"
      ],
      "dependencies": [],
      "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": 1,
        "open_issues": 1,
        "closed_ratio": 0,
        "closed_issues": 0,
        "closed_unmerged_prs": 0
      },
      "bus_factor": 1,
      "bot_contributors": 0,
      "top_contributors": [
        {
          "type": "User",
          "login": "jose-pr",
          "commits": 123,
          "avatar_url": "https://avatars.githubusercontent.com/u/9260810?v=4"
        }
      ],
      "contributors_sampled": 1,
      "top_contributor_share": 1
    },
    "quality_signals": {
      "has_ci": true,
      "has_tests": true,
      "ci_workflows": [
        "docs.yml",
        "release.yml",
        "test.yml"
      ],
      "has_docs_dir": true,
      "linter_configs": [],
      "has_editorconfig": false,
      "has_linter_config": false,
      "has_precommit_config": false
    },
    "security_signals": {
      "lockfiles": [],
      "scorecard": {
        "checks": [
          {
            "name": "Binary-Artifacts",
            "score": 10,
            "reason": "no binaries found in the repo",
            "documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#binary-artifacts"
          },
          {
            "name": "Branch-Protection",
            "score": 0,
            "reason": "branch protection not enabled on development/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": 0,
            "reason": "project has 0 contributing companies or organizations -- score normalized to 0",
            "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": 0,
            "reason": "no update tool detected",
            "documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#dependency-update-tool"
          },
          {
            "name": "Fuzzing",
            "score": 0,
            "reason": "project is not 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": 0,
            "reason": "dependency not pinned by hash detected -- score normalized to 0",
            "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": 0,
            "reason": "security policy file not 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": 10,
            "reason": "0 existing vulnerabilities detected",
            "documentation_url": "https://github.com/ossf/scorecard/blob/c395761df6afe1a69e476bc60a013a94bcbc153f/docs/checks.md#vulnerabilities"
          }
        ],
        "commit": "94fa7fe2eba28dd7142ae5c450e20ebdd42800e8",
        "ran_at": "2026-07-24T08:08:05Z",
        "aggregate_score": 3.9,
        "scorecard_version": "v5.5.0"
      },
      "has_codeql_workflow": false,
      "has_security_policy": false,
      "has_dependabot_config": false
    },
    "contribution_flow": {
      "collected": true,
      "ci_last_run_at": "2026-07-24T08:07:14Z",
      "oldest_open_prs": [],
      "last_merged_pr_at": "2026-07-20T22:08:28Z",
      "ci_last_conclusion": "SUCCESS",
      "oldest_open_issues": [
        {
          "number": 1,
          "created_at": "2026-07-19T15:13:42Z",
          "last_comment_at": null,
          "last_comment_author": null
        }
      ]
    }
  },
  "config": {
    "disabled_metrics": [],
    "disabled_categories": [],
    "disabled_components": {}
  },
  "source": {
    "url": "https://github.com/jose-pr/duho",
    "host": "github.com",
    "name": "duho",
    "owner": "jose-pr"
  },
  "metrics": {
    "overall": {
      "key": "overall",
      "band": "moderate",
      "name": "Overall health",
      "note": null,
      "notes": [],
      "value": 51,
      "inputs": {
        "security": 39,
        "vitality": 69,
        "community": 33,
        "governance": 43,
        "engineering": 66
      },
      "components": []
    },
    "categories": [
      {
        "key": "vitality",
        "band": "moderate",
        "name": "Vitality",
        "value": 69,
        "weight": 0.22,
        "metrics": [
          {
            "key": "development_activity",
            "band": "moderate",
            "name": "Development activity",
            "note": null,
            "notes": [],
            "value": 55,
            "inputs": {
              "commits_last_year": 110,
              "human_commit_share": 1,
              "days_since_last_push": 0,
              "active_weeks_last_year": 2
            },
            "components": [
              {
                "key": "push_recency",
                "name": "Push recency",
                "detail": "last push 0 days ago",
                "points": 36,
                "status": "met",
                "details": [
                  {
                    "code": "push_recency",
                    "params": {
                      "days": 0
                    }
                  }
                ],
                "max_points": 36
              },
              {
                "key": "commit_cadence",
                "name": "Commit cadence",
                "detail": "2/52 weeks with commits",
                "points": 1.4,
                "status": "partial",
                "details": [
                  {
                    "code": "commit_cadence_weeks",
                    "params": {
                      "weeks": 2
                    }
                  }
                ],
                "max_points": 36
              },
              {
                "key": "commit_volume",
                "name": "Commit volume",
                "detail": "110 commits in the last year",
                "points": 18,
                "status": "met",
                "details": [
                  {
                    "code": "commits_last_year",
                    "params": {
                      "count": 110
                    }
                  }
                ],
                "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": 9,
              "latest_release_tag": "v0.5.0",
              "releases_from_tags": false,
              "days_since_latest_release": 0,
              "mean_days_between_releases": 1.2
            },
            "components": [
              {
                "key": "ships_releases",
                "name": "Ships releases",
                "detail": "9 releases published",
                "points": 27,
                "status": "met",
                "details": [
                  {
                    "code": "releases_published",
                    "params": {
                      "count": 9
                    }
                  }
                ],
                "max_points": 27
              },
              {
                "key": "release_recency",
                "name": "Release recency",
                "detail": "latest release 0 days ago",
                "points": 36,
                "status": "met",
                "details": [
                  {
                    "code": "release_recency",
                    "params": {
                      "days": 0
                    }
                  }
                ],
                "max_points": 36
              },
              {
                "key": "release_cadence",
                "name": "Release cadence",
                "detail": "a release every ~1.2 days",
                "points": 27,
                "status": "met",
                "details": [
                  {
                    "code": "release_cadence",
                    "params": {
                      "gap": 1.2
                    }
                  }
                ],
                "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": "at_risk",
        "name": "Community & Adoption",
        "value": 33,
        "weight": 0.18,
        "metrics": [
          {
            "key": "popularity",
            "band": "critical",
            "name": "Popularity & adoption",
            "note": null,
            "notes": [],
            "value": 1,
            "inputs": {
              "forks": 0,
              "stars": 0,
              "watchers": 0,
              "growth_state": "unverified",
              "growth_factor_pct": 100,
              "growth_unverified_reason": "no_history"
            },
            "components": [
              {
                "key": "stars",
                "name": "Stars",
                "detail": "0 stars",
                "points": 0,
                "status": "missed",
                "details": [
                  {
                    "code": "stars",
                    "params": {
                      "count": 0
                    }
                  }
                ],
                "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": "good",
            "name": "Community health",
            "note": null,
            "notes": [],
            "value": 70,
            "inputs": {
              "has_readme": true,
              "has_license": true,
              "has_contributing": true,
              "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": 18,
                "status": "met",
                "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": 43,
        "weight": 0.24,
        "metrics": [
          {
            "key": "maintainer_resilience",
            "band": "critical",
            "name": "Maintainer resilience (bus factor)",
            "note": null,
            "notes": [],
            "value": 10,
            "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 0 contributing companies or organizations -- score normalized to 0",
                "points": 0,
                "status": "missed",
                "details": [],
                "max_points": 10
              }
            ]
          },
          {
            "key": "responsiveness",
            "band": "at_risk",
            "name": "Issue & PR responsiveness",
            "note": null,
            "notes": [],
            "value": 38,
            "inputs": {
              "merged_prs": 1,
              "open_issues": 1,
              "closed_issues": 0,
              "issue_closed_ratio": 0,
              "closed_unmerged_prs": 0
            },
            "components": [
              {
                "key": "issue_resolution",
                "name": "Issue resolution",
                "detail": "0% of issues closed",
                "points": 0,
                "status": "missed",
                "details": [
                  {
                    "code": "issues_closed_share",
                    "params": {
                      "share": 0
                    }
                  }
                ],
                "max_points": 46.75
              },
              {
                "key": "pr_acceptance",
                "name": "PR acceptance",
                "detail": "1/1 decided PRs merged",
                "points": 38.2,
                "status": "met",
                "details": [
                  {
                    "code": "decided_prs_merged",
                    "params": {
                      "merged": 1,
                      "decided": 1
                    }
                  }
                ],
                "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": "at_risk",
            "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": 42,
            "inputs": {
              "followers": 0,
              "owner_type": "User",
              "is_verified": null,
              "owner_login": "jose-pr",
              "public_repos": 43,
              "account_age_days": 4299
            },
            "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": "0 followers of jose-pr",
                "points": 0,
                "status": "missed",
                "details": [
                  {
                    "code": "owner_followers",
                    "params": {
                      "count": 0,
                      "login": "jose-pr"
                    }
                  }
                ],
                "max_points": 25
              },
              {
                "key": "track_record",
                "name": "Track record",
                "detail": "43 public repos, account ~11 yr old",
                "points": 24,
                "status": "partial",
                "details": [
                  {
                    "code": "public_repos",
                    "params": {
                      "count": 43
                    }
                  },
                  {
                    "code": "account_age_years",
                    "params": {
                      "years": 11
                    }
                  }
                ],
                "max_points": 25
              }
            ]
          },
          {
            "key": "package_maintenance",
            "band": "excellent",
            "name": "Package maintenance",
            "note": null,
            "notes": [],
            "value": 100,
            "inputs": {
              "packages": [
                "duho"
              ],
              "ecosystems": "pypi",
              "any_deprecated": false,
              "min_days_since_publish": 0
            },
            "components": [
              {
                "key": "published_resolvable",
                "name": "Published & resolvable",
                "detail": "1 package(s) on pypi",
                "points": 25,
                "status": "met",
                "details": [
                  {
                    "code": "packages_published",
                    "params": {
                      "count": 1,
                      "ecosystems": "pypi"
                    }
                  }
                ],
                "max_points": 25
              },
              {
                "key": "publish_recency",
                "name": "Publish recency",
                "detail": "latest publish 0 days ago",
                "points": 35,
                "status": "met",
                "details": [
                  {
                    "code": "publish_recency",
                    "params": {
                      "days": 0
                    }
                  }
                ],
                "max_points": 35
              },
              {
                "key": "version_history",
                "name": "Version history",
                "detail": "10 published versions",
                "points": 20,
                "status": "met",
                "details": [
                  {
                    "code": "published_versions",
                    "params": {
                      "count": 10
                    }
                  }
                ],
                "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": 66,
        "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": "3 workflow(s)",
                "points": 24,
                "status": "met",
                "details": [
                  {
                    "code": "ci_workflows",
                    "params": {
                      "count": 3
                    }
                  }
                ],
                "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": "good",
            "name": "Documentation",
            "note": null,
            "notes": [],
            "value": 75,
            "inputs": {
              "topics": [],
              "has_wiki": true,
              "homepage": null,
              "has_readme": true,
              "has_docs_dir": true,
              "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": 25,
                "status": "met",
                "details": [],
                "max_points": 25
              },
              {
                "key": "documentation_homepage_site",
                "name": "Documentation / homepage site",
                "detail": null,
                "points": 0,
                "status": "missed",
                "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": null,
                "points": 0,
                "status": "missed",
                "details": [],
                "max_points": 10
              },
              {
                "key": "wiki",
                "name": "Wiki",
                "detail": null,
                "points": 10,
                "status": "met",
                "details": [],
                "max_points": 10
              }
            ]
          }
        ],
        "description": "Are baseline engineering and documentation practices in place?"
      },
      {
        "key": "security",
        "band": "at_risk",
        "name": "Security",
        "value": 39,
        "weight": 0.16,
        "metrics": [
          {
            "key": "security_posture",
            "band": "at_risk",
            "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": 39,
            "inputs": {
              "source": "openssf_scorecard",
              "checks_evaluated": 17,
              "scorecard_version": "v5.5.0",
              "checks_inconclusive": 1,
              "scorecard_aggregate": 3.9
            },
            "components": [
              {
                "key": "binary_artifacts",
                "name": "Binary-Artifacts",
                "detail": "no binaries found in the repo",
                "points": 7.5,
                "status": "met",
                "details": [],
                "max_points": 7.5
              },
              {
                "key": "branch_protection",
                "name": "Branch-Protection",
                "detail": "branch protection not enabled on development/release branches",
                "points": 0,
                "status": "missed",
                "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 0 contributing companies or organizations -- score normalized to 0",
                "points": 0,
                "status": "missed",
                "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": "no update tool detected",
                "points": 0,
                "status": "missed",
                "details": [],
                "max_points": 7.5
              },
              {
                "key": "fuzzing",
                "name": "Fuzzing",
                "detail": "project is not fuzzed",
                "points": 0,
                "status": "missed",
                "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 0",
                "points": 0,
                "status": "missed",
                "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 not detected",
                "points": 0,
                "status": "missed",
                "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": "0 existing vulnerabilities detected",
                "points": 7.5,
                "status": "met",
                "details": [],
                "max_points": 7.5
              }
            ]
          }
        ],
        "description": "Are visible security and supply-chain practices strong, with no malicious dependency and no unresolved high-risk jurisdiction exposure?"
      },
      {
        "key": "ai_readiness",
        "band": "moderate",
        "name": "AI Readiness",
        "value": 60,
        "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": 1,
              "agent_instruction_files": [
                "src/duho/AGENTS.md"
              ],
              "agent_instruction_max_bytes": 14238
            },
            "components": [
              {
                "key": "agent_instructions",
                "name": "Agent instructions",
                "detail": "src/duho/AGENTS.md",
                "points": 45,
                "status": "met",
                "details": [
                  {
                    "code": "file_list",
                    "params": {
                      "files": "src/duho/AGENTS.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": "100 of 100 human commits state their intent (structured subject or explanatory body)",
                "points": 40,
                "status": "met",
                "details": [
                  {
                    "code": "legible_history",
                    "params": {
                      "legible": 100,
                      "sampled": 100
                    }
                  }
                ],
                "max_points": 40
              }
            ]
          },
          {
            "key": "ai_verify_loop",
            "band": "at_risk",
            "name": "Verify loop (build / test / typecheck)",
            "note": null,
            "notes": [],
            "value": 33,
            "inputs": {
              "has_nix": false,
              "has_tests": true,
              "lockfiles": [],
              "has_dockerfile": false,
              "typed_language": false,
              "bootstrap_files": [],
              "has_devcontainer": false,
              "has_linter_config": false,
              "typecheck_configs": [
                "src/duho/py.typed"
              ],
              "agent_commit_share": 0,
              "toolchain_manifests": [],
              "dependency_bot_commit_share": 0
            },
            "components": [
              {
                "key": "one_command_bootstrap",
                "name": "One-command bootstrap",
                "detail": null,
                "points": 0,
                "status": "missed",
                "details": [],
                "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": "src/duho/py.typed",
                "points": 11,
                "status": "met",
                "details": [
                  {
                    "code": "file_list",
                    "params": {
                      "files": "src/duho/py.typed"
                    }
                  }
                ],
                "max_points": 11
              },
              {
                "key": "reproducible_environment",
                "name": "Reproducible environment",
                "detail": null,
                "points": 0,
                "status": "missed",
                "details": [],
                "max_points": 10
              },
              {
                "key": "demonstrated_agent_practice",
                "name": "Demonstrated agent practice",
                "detail": "no agent-authored commits among the last 100",
                "points": 0,
                "status": "missed",
                "details": [
                  {
                    "code": "no_agent_authored_commits",
                    "params": {
                      "sampled": 100
                    }
                  }
                ],
                "max_points": 10
              },
              {
                "key": "automated_maintenance",
                "name": "Automated maintenance",
                "detail": "no automated dependency updates observed",
                "points": 0,
                "status": "missed",
                "details": [
                  {
                    "code": "no_dependency_automation",
                    "params": {}
                  }
                ],
                "max_points": 8
              },
              {
                "key": "openssf_scorecard_pinned_dependencies",
                "name": "OpenSSF Scorecard: Pinned-Dependencies",
                "detail": "dependency not pinned by hash detected -- score normalized to 0",
                "points": 0,
                "status": "missed",
                "details": [],
                "max_points": 10
              }
            ]
          },
          {
            "key": "ai_code_legibility",
            "band": "good",
            "name": "Code legibility for models",
            "note": null,
            "notes": [],
            "value": 81,
            "inputs": {
              "primary_language": "Python",
              "largest_source_bytes": 102306,
              "source_files_sampled": 92,
              "oversized_source_files": 1
            },
            "components": [
              {
                "key": "type_checkable_code",
                "name": "Type-checkable code",
                "detail": "Python with type-check config (src/duho/py.typed)",
                "points": 27,
                "status": "partial",
                "details": [
                  {
                    "code": "typecheck_config_language",
                    "params": {
                      "files": "src/duho/py.typed",
                      "language": "Python"
                    }
                  }
                ],
                "max_points": 45
              },
              {
                "key": "manageable_file_sizes",
                "name": "Manageable file sizes",
                "detail": "1/92 source files over 60KB",
                "points": 54.4,
                "status": "partial",
                "details": [
                  {
                    "code": "oversized_source_files",
                    "params": {
                      "kb": 60,
                      "sampled": 92,
                      "oversized": 1
                    }
                  }
                ],
                "max_points": 55
              }
            ]
          },
          {
            "key": "ai_interfaces",
            "band": "moderate",
            "name": "Machine-readable interfaces",
            "note": null,
            "notes": [],
            "value": 60,
            "inputs": {
              "example_dirs": [
                "examples"
              ],
              "has_mcp_signal": true,
              "api_schema_files": []
            },
            "components": [
              {
                "key": "api_schema_openapi_graphql_proto",
                "name": "API schema (OpenAPI/GraphQL/proto)",
                "detail": null,
                "points": 0,
                "status": "missed",
                "details": [],
                "max_points": 40
              },
              {
                "key": "mcp_server",
                "name": "MCP server",
                "detail": null,
                "points": 20,
                "status": "met",
                "details": [],
                "max_points": 20
              },
              {
                "key": "runnable_examples",
                "name": "Runnable examples",
                "detail": "examples",
                "points": 40,
                "status": "met",
                "details": [
                  {
                    "code": "file_list",
                    "params": {
                      "files": "examples"
                    }
                  }
                ],
                "max_points": 40
              }
            ]
          }
        ],
        "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": [
    "GitHub dependency-graph SBOM unavailable (404); the dependency graph may be disabled for this repository",
    "deps.dev does not index pypi:duho@0.5.0; advisories assessed against the repository dependency graph instead"
  ],
  "report_type": "repository",
  "generated_at": "2026-07-24T08:08:10.339357Z",
  "schema_version": "0.27.0",
  "badge_url": "https://raw.githubusercontent.com/inspect-software/badges/main/v1/j/jose-pr/duho.svg",
  "full_name": "jose-pr/duho",
  "license_state": "standard",
  "license_spdx": "MIT"
}

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

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

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