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