Registro público
Informe de salud del softwareesquema 0.27.0 · métricas 1.13.0 · 2026-07-24 08:08 UTC

jose-pr / duho

A declarative CLI framework for Python

PythonMIT★ 0 estrellas⑂ 0 forksdesde jul 2026Ver en GitHub ↗

jose-pr/duho tiene un índice de salud de 51 sobre 100, lo que lo sitúa en la banda Moderado. Su puntuación más alta es Vitality (69/100) y la más baja, Community & Adoption (33/100). Se actualizó por última vez hoy. Una sola persona concentra la mayor parte del trabajo reciente.

51
global / 100
Moderado

Índice de salud del software

Las métricas se agrupan en categorías ponderadas sobre una escala de 1 a 100. El resultado global parte de su media; cuando la evidencia pública activa la Política de Jurisdicciones de Alto Riesgo, la calificación se ajusta y recibe el límite 49 (En riesgo). Preparación para IA queda fuera.

51
Excelente85-100Ejemplar; cumple prácticamente todos los criterios evaluados
Bueno70-84Saludable; carencias menores
Moderado50-69Aceptable con carencias notables; se recomienda revisión
En riesgo30-49Debilidades significativas; su adopción exige cautela
Crítico1-29Problemas graves (proyecto abandonado, un solo mantenedor, sin higiene)
VitalidadComunidad yAdopciónSostenibilidady GobernanzaCalidad deIngenieríaSeguridadPreparaciónpara IA

Perfil de puntuación

Cada eje es una categoría. La forma importa más que la media: un proyecto sano llena toda la figura, mientras que un perfil de picos y cráteres indica que la fortaleza en una dimensión enmascara el riesgo en otra.

Titularidad

Jose ACuenta personal
0 seguidores43 repositorios públicosdesde oct 2014

Este repositorio pertenece a una cuenta personal. Un proyecto con un único propietario conlleva más riesgo de continuidad que uno respaldado por una organización.

Ecosistemas de paquetes

RegistroPaqueteVersiónDescargas / mesVersionesÚltima publicación
PyPIduho0.5.0-10hace 0 días

Métricas por categoría

Vitalidad

¿Está vivo el proyecto: se escribe código y se publican versiones?

69Moderado · 22% del índice global
Cómo se puntúa
36/36Recencia de push — último push hace 0 días
1.4/36Cadencia de commits — 2/52 semanas con commits
18/18Volumen de commits — 110 commits en el último año
0/10OpenSSF Scorecard: Maintained — project was created within the last 90 days. Please review its contents carefully
Datos de entrada utilizados
commits_last_year110
human_commit_share1
days_since_last_push0
active_weeks_last_year2
Cómo se puntúa
27/27Publica versiones — 9 versiones publicadas
36/36Recencia de las versiones — última versión hace 0 días
27/27Cadencia de publicación — una versión cada ~1,2 días
0/10OpenSSF Scorecard: Signed-Releases — Project has not signed or included provenance with any releases.
Datos de entrada utilizados
releases_count9
latest_release_tagv0.5.0
releases_from_tagsno
days_since_latest_release0
mean_days_between_releases1,2

Comunidad y Adopción

¿Tiene el proyecto usuarios, descargas, atención y unas condiciones acogedoras para quienes contribuyen?

33En riesgo · 18% del índice global
Cómo se puntúa
0/60Estrellas — 0 estrellas
0/25Forks — 0 forks
0/15Observadores — 0 observadores
Datos de entrada utilizados
forks0
stars0
watchers0
growth_stateunverified
growth_factor_pct100
growth_unverified_reasonno_history
Cómo se puntúa
22.5/22.5README
22.5/22.5Licencia — licencia reconocida (MIT)
18/18Guía CONTRIBUTING
0/13.5Código de conducta
0/7.2Plantilla de issues
0/6.3Plantilla de PR
Datos de entrada utilizados
has_readme
has_license
has_contributing
has_issue_templateno
has_code_of_conductno
has_pull_request_templateno

Sostenibilidad y Gobernanza

¿Sobrevivirá el proyecto a sus personas: factor bus, capacidad de respuesta, quién lo respalda y mantenimiento del paquete?

43En riesgo · 24% del índice global
Cómo se puntúa
9/54Factor bus — la mitad de los commits recae en 1 contribuyente(s)
0/22.5Distribución de commits — el principal contribuyente firma el 100% de los commits
1.4/13.5Amplitud de contribuyentes — 1 contribuyentes
0/10OpenSSF Scorecard: Contributors — project has 0 contributing companies or organizations -- score normalized to 0
Datos de entrada utilizados
bus_factor1
contributors_sampled1
top_contributor_share1
Cómo se puntúa
0/46.8Resolución de issues — 0% de issues cerradas
38.2/38.3Aceptación de PR — 1/1 PR decididos fusionados
0/15OpenSSF Scorecard: Code-Review — Found 0/30 approved changesets -- score normalized to 0
Datos de entrada utilizados
merged_prs1
open_issues1
closed_issues0
issue_closed_ratio0
closed_unmerged_prs0
Cómo se puntúa
10/30Respaldo de la propiedad — cuenta personal (usuario)
0/20Dominio verificado — no aplicable a cuentas de usuario
0/25Alcance del propietario — 0 seguidores de jose-pr
24/25Trayectoria — 43 repos públicos, cuenta de ~11 años
Datos de entrada utilizados
followers0
owner_typeUser
is_verified
owner_loginjose-pr
public_repos43
account_age_days4299
Excluidos de la puntuación (sin datos o no aplicable): Dominio verificado. Los pesos restantes se han renormalizado.
Cómo se puntúa
25/25Publicado y resoluble — 1 paquete(s) en pypi
35/35Recencia de publicación — última publicación hace 0 días
20/20Historial de versiones — 10 versiones en el registro
20/20No obsoleto — activo, ni obsoleto ni retirado
Datos de entrada utilizados
packagesduho
ecosystemspypi
any_deprecatedno
min_days_since_publish0

Calidad de Ingeniería

¿Existen unas prácticas mínimas de ingeniería y documentación?

66Moderado · 20% del índice global
Cómo se puntúa
24/24Flujos de trabajo de CI — 3 flujo(s) de trabajo
24/24Pruebas presentes
0/16Configuración de linter
0/9.6Hooks de pre-commit
0/6.4.editorconfig
0/20OpenSSF Scorecard: CI-Tests — sin datos
Datos de entrada utilizados
has_ci
has_tests
has_editorconfigno
has_linter_configno
has_precommit_configno
Excluidos de la puntuación (sin datos o no aplicable): OpenSSF Scorecard: CI-Tests. Los pesos restantes se han renormalizado.
Cómo se puntúa
30/30README
25/25Directorio de documentación
0/15Sitio de documentación / página del proyecto
10/10Descripción del repositorio
0/10Topics
10/10Wiki
Datos de entrada utilizados
topics
has_wiki
homepage
has_readme
has_docs_dir
has_description

Seguridad

¿Son sólidas las prácticas visibles de seguridad y de cadena de suministro, sin exposición jurisdiccional de alto riesgo sin resolver?

39En riesgo · 16% del índice global
Cómo se puntúa
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 — sin datos
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.5Licencia — 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
Datos de entrada utilizados
sourceopenssf_scorecard
checks_evaluated17
scorecard_versionv5.5.0
checks_inconclusive1
scorecard_aggregate3,9
Excluidos de la puntuación (sin datos o no aplicable): ci_tests. Los pesos restantes se han renormalizado.

Preparación para IA

¿Hasta qué punto está el repositorio preparado para desarrollarse y mantenerse con agentes de codificación de IA? Es una insignia independiente y experimental — peso 0,0, de modo que se presenta por separado y no afecta a la puntuación de salud global.

60Moderado · 0% del índice global
Cómo se puntúa
45/45Instrucciones para agentes — src/duho/AGENTS.md
0/15Documentación legible por máquinas (llms.txt)
40/40Historial de commits legible — 100 de 100 commits humanos declaran su intención (asunto estructurado o cuerpo explicativo)
Datos de entrada utilizados
has_llms_txtno
legible_history_share1
agent_instruction_filessrc/duho/AGENTS.md
agent_instruction_max_bytes14.238
Cómo se puntúa
0/18Arranque con un solo comando
22/22Pruebas automatizadas
0/11Configuración de lint / formato
11/11Verificación estática de tipos — src/duho/py.typed
0/10Entorno reproducible
0/10Práctica demostrada con agentes — ningún commit con autoría de agente entre los últimos 100
0/8Mantenimiento automatizado — no se observan actualizaciones automáticas de dependencias
0/10OpenSSF Scorecard: Pinned-Dependencies — dependency not pinned by hash detected -- score normalized to 0
Datos de entrada utilizados
has_nixno
has_tests
lockfiles
has_dockerfileno
typed_languageno
bootstrap_files
has_devcontainerno
has_linter_configno
typecheck_configssrc/duho/py.typed
agent_commit_share0
toolchain_manifests
dependency_bot_commit_share0
Cómo se puntúa
27/45Código verificable por tipos — Python con configuración de verificación de tipos (src/duho/py.typed)
54.4/55Tamaños de archivo manejables — 1/92 archivos fuente de más de 60 KB
Datos de entrada utilizados
primary_languagePython
largest_source_bytes102.306
source_files_sampled92
oversized_source_files1
Cómo se puntúa
0/40Esquema de API (OpenAPI/GraphQL/proto)
20/20Servidor MCP
40/40Ejemplos ejecutables — examples
Datos de entrada utilizados
example_dirsexamples
has_mcp_signal
api_schema_files

Datos clave

0estrellas de GitHub
1contribuidores
110commits en los últimos 12 meses
0días desde el último push
9versiones publicadas
1factor bus
1issues abiertas
PyPIecosistemas de paquetes

Advertencias de recopilación de datos

  • 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

Más detalle

OpenSSF Scorecard 3.9 / 10
3.9agregado

Evaluación de seguridad independiente y agnóstica en cuanto a herramientas, procedente del proyecto de código abierto OpenSSF Scorecard. Cada comprobación premia una práctica de seguridad, no la herramienta de un proveedor concreto. Las comprobaciones que Scorecard no pudo determinar se marcan como n/d y se excluyen de la puntuación de seguridad (nunca se cuentan como cero).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
n/dCI-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
Todas las dependencias no recopilado

No fue posible recopilar el conjunto de dependencias resuelto para este informe: GitHub dependency-graph SBOM unavailable (404); the dependency graph may be disabled for this repository

Informe JSON sin procesar legible por máquina
{
  "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"
}

Las puntuaciones son señales, no garantías. Reflejan prácticas públicamente visibles en GitHub; no son una auditoría de código ni una garantía de seguridad.

Los datos ausentes se excluyen y los pesos se renormalizan; nunca se puntúan como cero. La metodología es versionada y abierta: métricas v1.13.0, esquema v0.27.0 — metodología completa · wiki de métricas.

Cómo se sitúa un resultado dentro del registro general: estadísticas agregadasPyPI.