mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-10 17:47:05 +01:00
endurain_import: phone fallback skips files already imported, one summary line
RunnerUp keeps every export on the phone, so each run re-pulled the whole history into the inbox and re-triggered every inotify watcher there. pull_from_phone now skips names already in the inbox or in processed/ (processed_dir_for, shared with the importer) and logs one summary line, escalated to WARNING when any pull failed. Also: prune evidence/contracts to newest 20 (repo-size cap). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018RHwVPyiiQMPh4M3JQwQku
This commit is contained in:
parent
d792c081e4
commit
8bcdd20915
@ -1,21 +0,0 @@
|
||||
{
|
||||
"title": "Archive billsplit together with every artifact coupled to it",
|
||||
"objective": "Remove the billsplit Flutter app from testsAndMisc without leaving the repo in a state where a gate fails or a config rule points at a missing path. billsplit is the coupled case the earlier archive commit deliberately deferred: a coverage package, an app_icons registry entry, a CI workflow and two paired binary rules all name it.",
|
||||
"acceptance_criteria": [
|
||||
"billsplit/ and python_pkg/billsplit_coverage/ are both present in kuhyx/testsAndMisc-archive, confirmed via the GitHub API before the local delete.",
|
||||
"Archived commit count equals the monorepo count for those paths (8), and tracked file counts match exactly (73 and 5).",
|
||||
"python_pkg/app_icons/apps.py no longer contains a billsplit AppIcon entry, and the app_icons registry still imports cleanly.",
|
||||
"The repo-wide pytest gate still reports 100% coverage, with the test count reduced by exactly the 19 billsplit_coverage tests and nothing else.",
|
||||
"check_no_binaries.sh exits 0 with a .binary-allowlist that now carries no patterns.",
|
||||
".gitignore contains no billsplit rule; the ! overrides are removed in the same commit as the allowlist patterns they paired with, so the two halves never disagree.",
|
||||
"No file under .github/workflows/ references billsplit or billsplit_coverage.",
|
||||
"AGENTS.md no longer presents billsplit as a live app, while keeping the both-halves guidance for any future Flutter app.",
|
||||
"pre-commit passes without --no-verify."
|
||||
],
|
||||
"out_of_scope": [
|
||||
"Removing python_pkg/app_icons itself, which serves eleven other apps.",
|
||||
"Changing how check_no_binaries.sh or the coverage gate work; this commit only removes data they read.",
|
||||
"Historical planning documents that mention billsplit as past work."
|
||||
],
|
||||
"verifier": "gh api confirms both paths in the archive; git ls-files counts match 73/5; diff -r clean; python3 meta/scripts/pytest_changed_packages.py python_pkg/app_icons/apps.py reports 2095 passed at 100%; ./meta/scripts/check_no_binaries.sh exits 0; ./meta/scripts/check_directory_depth.sh --all drops 451 -> 417; git grep billsplit finds only intentional prose."
|
||||
}
|
||||
@ -1,20 +0,0 @@
|
||||
{
|
||||
"title": "Archive bucket_catch and poker-stakes to testsAndMisc-archive",
|
||||
"objective": "Remove two self-contained apps from testsAndMisc, preserving their full git history in kuhyx/testsAndMisc-archive, as the zero-risk opening step of the repo restructure. Neither unit is referenced by any other code, systemd unit, or symlink, which is why they go first.",
|
||||
"acceptance_criteria": [
|
||||
"Both directories are present in kuhyx/testsAndMisc-archive at their original repo-relative paths, confirmed through the GitHub API rather than a local clone.",
|
||||
"The archived commit count for each unit equals the number of monorepo commits touching it: bucket_catch 10, poker-stakes 2.",
|
||||
"diff -r between the archived copy and the pre-removal working tree reports no content differences for either unit.",
|
||||
"Tracked file counts match exactly: bucket_catch 64, poker-stakes 65.",
|
||||
".github/workflows/poker-stakes-ci.yml is removed in the same commit as the app it builds, so no workflow is left pointing at a missing path.",
|
||||
"No file under .github/workflows/ still references either unit.",
|
||||
"meta/scripts/report_live_state.sh output is unchanged, proving neither unit had live system state pointing at it.",
|
||||
"pre-commit passes without --no-verify."
|
||||
],
|
||||
"out_of_scope": [
|
||||
"billsplit, which is archived separately because it is coupled to python_pkg/billsplit_coverage and an app_icons registry entry that must be deleted in the same commit.",
|
||||
"Rewriting historical planning notes that mention these names; they are dated records, not live configuration.",
|
||||
"Wiring the directory-depth gate, which stays a dry run until the tree conforms."
|
||||
],
|
||||
"verifier": "gh api repos/kuhyx/testsAndMisc-archive/contents lists both units; git ls-files counts match source; diff -r is clean; ./meta/scripts/check_directory_depth.sh --all drops from 556 to 451; ./meta/scripts/report_live_state.sh still reports 27 reference lines; pre-commit run over the staged set passes."
|
||||
}
|
||||
@ -1,21 +0,0 @@
|
||||
{
|
||||
"title": "Scope the pre-push CI mirror to the push range, and make its pytest stage actually run",
|
||||
"objective": "Take `git push` from ~130s to seconds by checking what the push changed rather than the whole tree, while repairing the python test stage, which returned success without executing a single test.",
|
||||
"acceptance_criteria": [
|
||||
"ci_mirror.sh passes the changed paths to pytest_changed_packages.py, so the stage runs the suite instead of short-circuiting on empty argv.",
|
||||
"A push whose range contains a failing python test is refused with exit 1.",
|
||||
"A push whose range touches a lib/tests suite with a failing assertion is refused with exit 1, and the suite fails bare first so the gate is not the only evidence.",
|
||||
"The green-tree cache is NOT written when the gate fails, so a red run cannot poison the short-circuit.",
|
||||
"An unknown range, or a change to .pre-commit-config.yaml / meta/scripts/ / requirements.txt / .githooks/, forces a full --all-files sweep.",
|
||||
"A change under scripts/lib/ or to a suite's lib_test_core.sh / lib_test_path.sh selects every lib/tests suite, not just the enclosing one.",
|
||||
"Repo-wide hooks (file-length-cap --all, no-binaries, check-python-location) still run on every push.",
|
||||
"No test is weakened or deleted, no --no-verify is added to any script, and pre-commit's own 4.8s commit-stage run is untouched.",
|
||||
"All three scripts are shellcheck-clean, shfmt-clean and within the 250-line cap."
|
||||
],
|
||||
"out_of_scope": [
|
||||
"Moving ci-mirror off pre-push (option 4 in the handoff): the user chose to keep the guarantee that a red push cannot reach GitHub.",
|
||||
"Changing the coverage bar or the jail instrument.",
|
||||
"Touching the pre-commit hook stage, already measured at 4.8s."
|
||||
],
|
||||
"verifier": "With PRE_COMMIT_FROM_REF/PRE_COMMIT_TO_REF pinned to a probe commit: `bash meta/scripts/ci_mirror.sh` exits 0 on a green range and 1 on a range containing a deliberately broken python assertion or shell assertion; `time` on a docs-only, python and shell range reports 7.6s / 25.3s / 43.6s against a ~130s baseline; `shellcheck -x`, `shfmt -l` and `check_file_length.sh --all` are clean."
|
||||
}
|
||||
@ -1,19 +0,0 @@
|
||||
{
|
||||
"title": "endurain importer dedupe via postgres",
|
||||
"objective": "Give the importer a working answer to 'does Endurain already hold this run?', now that the HTTP route cannot provide one: API keys are upload-only and the activities read endpoint is JWT-only, so the existing check always returned None and every pass relied on the local ledger alone.",
|
||||
"acceptance_criteria": [
|
||||
"A run Endurain already holds is skipped even when the local ledger is empty, and the activity count does not grow.",
|
||||
"The same run IS imported when the fallback is disabled, proving the check is what blocks it.",
|
||||
"A dead or renamed container yields None (ledger fallback), while a user with no activities yields an empty list; the two are never conflated.",
|
||||
"The journal distinguishes which backend answered, so a DB-sourced pass is not mistaken for a ledger-only one.",
|
||||
"Verified in the real systemd unit, not only from an interactive shell.",
|
||||
"100% branch coverage on the new and changed modules, with no lint suppressions added.",
|
||||
"No test touches the real postgres container."
|
||||
],
|
||||
"out_of_scope": [
|
||||
"Removing or replacing the HTTP lookup - it stays first in the chain so the supported path resumes working by itself if Endurain widens the API-key scope allow-list.",
|
||||
"Exposing a postgres host port or adding a database driver dependency.",
|
||||
"Patching Endurain's own source or forging scopes in users_api_keys to work around the upload-only allow-list."
|
||||
],
|
||||
"verifier": "pre-commit + python_pkg/endurain_import/tests + a real systemctl --user run with journal inspection"
|
||||
}
|
||||
@ -1,22 +0,0 @@
|
||||
{
|
||||
"title": "Teach the endurain importer to skip runs already in Endurain, and pin the adb fallback to one device",
|
||||
"objective": "RunnerUp can now upload to Endurain directly as well as via the WebDAV inbox, and the two paths cannot see each other: the same run lands twice (activities 14 and 15, identical start time, distances differing only by tcx/gpx rounding). Add a third dedupe check that asks Endurain what it already holds, and fix the adb fallback, which fails on every call the moment a second device is attached.",
|
||||
"acceptance_criteria": [
|
||||
"A run already present in Endurain is moved to processed/ without being uploaded again, even when the local ledger has never seen it.",
|
||||
"A failed or unauthorised remote query returns None and falls back to the existing ledger dedupe. None is never read as 'no matching activity exists', because that would upload a duplicate on every transport hiccup.",
|
||||
"Start times are compared with a tolerance: the file for activity 15 embeds 21:51:04Z while Endurain stored 21:51:05Z, so exact equality misses the very duplicate that motivated this work.",
|
||||
"Export filenames are parsed as LOCAL wall-clock time; RunnerUp_2026-08-14-00-58-45_Running resolves to 2026-08-13 22:58:45 UTC, not 2026-08-14.",
|
||||
"With several adb devices attached and no serial pinned, the phone fallback is skipped with a warning naming the attached serials and the env var, instead of failing every call with 'more than one device/emulator'.",
|
||||
"ENDURAIN_ADB_SERIAL selects the device when set; a pinned serial that is not attached is refused rather than silently ignored.",
|
||||
"Every adb invocation carries -s <serial>; no serial is hardcoded, since this repo is public.",
|
||||
"The five package modules keep 100% branch coverage with no lint suppressions and no coverage exclusions.",
|
||||
"The phone-side files are never written, moved or deleted: screen-locker reads the same directory as its unlock gate."
|
||||
],
|
||||
"out_of_scope": [
|
||||
"Deleting duplicates that already exist. This work prevents new ones; clearing the 14/15 collision needs session credentials and is the user's action.",
|
||||
"Granting the API key the activities:read scope. Both existing keys hold activities:upload only, so the remote check is inert until that changes; the fail-closed path is what ships working today.",
|
||||
"Changing the WebDAV inbox, the dufs config, or RunnerUp's own uploader.",
|
||||
"Recording the returned activity_id in the ledger. It is null on every existing entry, which is cosmetic and unrelated."
|
||||
],
|
||||
"verifier": ".venv/bin/python -m pytest python_pkg/endurain_import/tests/ -q (expects 85 passed), plus --cov=python_pkg.endurain_import showing 100% with zero missed branches on all five modules, plus .venv/bin/python -m ruff check python_pkg/endurain_import/, plus systemctl --user start endurain-import.service with two devices attached showing the named-serials warning rather than 'more than one device/emulator'."
|
||||
}
|
||||
@ -1,20 +0,0 @@
|
||||
{
|
||||
"title": "Endurain viewer fed by RunnerUp WebDAV exports",
|
||||
"objective": "Stand up self-hosted Endurain behind the existing Caddy edge and import RunnerUp activities into it, primarily over WebDAV from the phone with an adb pull as fallback. Endurain is a VIEWER only: screen-locker's workout gate keeps reading the phone over adb and is not modified, because a WebDAV drop folder is spoofable by anything that can write to ~/data/cloud and must never become a screen-unlock authority. Success means real activities from the phone are visible in Endurain, importing twice never creates a duplicate, and screen-locker still passes its full suite.",
|
||||
"acceptance_criteria": [
|
||||
"Endurain reachable at https://endurain.kuhy.duckdns.org with a valid Let's Encrypt certificate",
|
||||
"At least 4 real RunnerUp activities imported and visible, with correct distance and duration",
|
||||
"Re-running the importer over already-imported files creates ZERO new activities",
|
||||
"The .gpx and .tcx of a single run import as ONE activity, not two",
|
||||
"The seeded admin/admin login is rejected before the service is ever published",
|
||||
"The phone-facing WebDAV credential cannot read ~/data/cloud/Keepass or gdrive_backups",
|
||||
"screen-locker's full test suite still passes at 100% coverage"
|
||||
],
|
||||
"out_of_scope": [
|
||||
"Any edit to screen-locker's _runnerup_verification.py, _runnerup_tcx.py or _runnerup_db.py",
|
||||
"Full history backfill out of RunnerUp's SQLite DB (the phone holds only 5 files)",
|
||||
"Strava and Garmin Connect integration",
|
||||
"Patching RunnerUp to add background auto-upload (it has none by design)"
|
||||
],
|
||||
"verifier": "pre-commit run --files <changed>; pytest python_pkg/endurain_import/tests/ (52 tests, 100% branch coverage); systemctl --user start endurain-import.service; psql count against endurain-postgres; screen-locker pytest suite (1090 tests)"
|
||||
}
|
||||
@ -1,20 +0,0 @@
|
||||
{
|
||||
"title": "Extract android_guardian and settle the heavy_job_lock seam",
|
||||
"objective": "Move the Magisk module to its own public repo. The directory it lived in also held heavy_job_lock.sh, a shared lock library that is NOT part of the module, so this extraction has to split the directory rather than move it wholesale. It also inherits a packaging script whose module path was already broken before the extraction.",
|
||||
"acceptance_criteria": [
|
||||
"The module and both its test suites are in the new public repo, tests passing from a clean clone, shellcheck exiting 0 with no severity filter.",
|
||||
"heavy_job_lock.sh lands where its installer is, justified by measuring which consumers read the repo copy versus the installed /usr/local/bin copy - not by the ad-hoc '>2 consumers' rule.",
|
||||
"install_pacman_wrapper.sh's relative path to heavy_job_lock.sh resolves after the move.",
|
||||
"update_android_hosts.sh points at a real location, and a missing checkout fails with an actionable message at a single funnel rather than a bare cp error deep in a helper.",
|
||||
"The module still builds end-to-end from the new checkout.",
|
||||
"shell-tests.yml names no removed suite; the coverage allowlist names no removed path.",
|
||||
"periodic_background/utils/ is fully emptied, not left as a one-file directory.",
|
||||
"pre-commit passes without --no-verify."
|
||||
],
|
||||
"out_of_scope": [
|
||||
"Moving heavy_job_lock.sh to ~/src/utils: the measurement does not support it, and every consumer but the installer already reads the installed copy.",
|
||||
"The adb push half of update_android_hosts.sh, which needs a device in a writable state.",
|
||||
"digital_wellbeing itself, which is the next and last extraction."
|
||||
],
|
||||
"verifier": "bash tests/*.sh in the new repo passes; shellcheck exits 0 there; ANDROID_GUARDIAN_DIR=/nonexistent produces the guard message; a real run builds the zip (182331 domains); shellcheck and check_shell_coverage.sh --all clean here; check_directory_depth.sh --all continues to fall."
|
||||
}
|
||||
@ -1,22 +0,0 @@
|
||||
{
|
||||
"title": "Extract digital_wellbeing, move its six test suites with it, and make the directory-depth cap enforce",
|
||||
"objective": "Clear the last 41 directory-depth violations by moving the unit to github.com/kuhyx/digital-wellbeing, then delete --warn from the depth hook so the cap is real -- the point of the whole campaign.",
|
||||
"acceptance_criteria": [
|
||||
"check_directory_depth.sh --all exits 0 with --warn removed from the hook entry.",
|
||||
"The extracted repo's history spans all three paths the unit has lived at (63 commits), plus the 24 from the separately-filtered test suites.",
|
||||
"All 80 tracked files are byte-identical to the monorepo copies; node_modules and __pycache__ are excluded.",
|
||||
"All six moved suites plus every lib/tests/run_all.sh pass from ~/src/digital-wellbeing, with no root, pacman or Arch userland -- verified under env -i.",
|
||||
"Every moved test name is deleted from BOTH hardcoded arrays in .github/workflows/shell-tests.yml; every name still listed there resolves to a file that exists.",
|
||||
"check_and_enable_services.sh runs to completion and reports ok for pacman_wrapper, makepkg_wrapper, midnight_shutdown, startup_monitor, periodic_systems and hosts.",
|
||||
"The pacman-wrapper drift manifest verifies with sha256sum -c after regeneration.",
|
||||
"report_live_state.sh --check reports ZERO stale linux_configuration/scripts/ references (down from 8).",
|
||||
"No lint suppression is added anywhere."
|
||||
],
|
||||
"out_of_scope": [
|
||||
"Fixing the unwrapped discord/signal-desktop binaries -- pre-existing live state, identical at baseline (18 passed / 1 failed / 5 skipped both before and after).",
|
||||
"The workout_locker error, which concerns the separate screen-locker repo.",
|
||||
"The Nextcloud dedup, measured and recommended against in the handoff.",
|
||||
"Reformatting vendor/common.sh, which is a verbatim copy."
|
||||
],
|
||||
"verifier": "./meta/scripts/check_directory_depth.sh --all exits 0; ~/src/digital-wellbeing/tests/run_all.sh passes all seven suites; grep for the six moved test names in shell-tests.yml returns 0; sudo check_and_enable_services.sh completes with no unbound-variable error and no missing-script error; sudo sha256sum -c --status /var/lib/pacman-wrapper/source.sha256 exits 0; report_live_state.sh --check greps 0 for linux_configuration/scripts/; shellcheck clean on all nine changed shell files."
|
||||
}
|
||||
@ -1,24 +0,0 @@
|
||||
{
|
||||
"title": "Extract the hosts blocker into its own repo and repoint its six monorepo callers",
|
||||
"objective": "Move linux_configuration/periodic_background/hosts/ to github.com/kuhyx/hosts-blocker with full history, clearing 5 of 46 directory-depth violations, without changing a single source line or disturbing the live /etc/hosts guards.",
|
||||
"acceptance_criteria": [
|
||||
"The extracted repo's history spans all four paths the unit has lived at, not just the current one — 52 commits, reaching the original 'block websites uising etc hosts'.",
|
||||
"Every extracted source file is byte-identical to its monorepo copy (diff -r reports no differences).",
|
||||
"lib/tests/run_all.sh passes from ~/src/hosts-blocker, the real clone location, with 25 assertions.",
|
||||
"All six monorepo callers resolve to /home/kuhy/src/hosts-blocker, including under sudo, where $HOME would be /root.",
|
||||
"A missing checkout fails loudly with a clone instruction and a non-zero return — never a silent skip.",
|
||||
"check_directory_depth.sh --all reports 41 violations, all of them digital_wellbeing.",
|
||||
"report_live_state.sh --check output is byte-identical before and after: no new stale references.",
|
||||
"The installed guard plugins under /usr/local/share/guard-lib-plugins/ remain byte-identical to the extracted copies.",
|
||||
"No lint suppression (shellcheck disable, noqa, per-file ignore) is added anywhere."
|
||||
],
|
||||
"out_of_scope": [
|
||||
"Extracting digital_wellbeing (41 violations) — the next, harder unit.",
|
||||
"Removing --warn from the directory-depth-cap hook, which is only valid once BOTH units are out.",
|
||||
"Reformatting guard/plugins/*.sh with shfmt; they are unformatted in the monorepo too and their installed copies must stay identical.",
|
||||
"Re-running the hosts installer or otherwise touching the live /etc/hosts, its chattr flags, or the running guards.",
|
||||
"Fixing the 8 pre-existing stale live-state references — a separate commit after both extractions.",
|
||||
"Deleting .hippo/ or the untracked extraction residue — separate commits."
|
||||
],
|
||||
"verifier": "diff -r --exclude=.git between the monorepo unit and the extracted repo reports IDENTICAL; ~/src/hosts-blocker/lib/tests/run_all.sh exits 0 with 25 passed; pre-commit run --all-files --hook-stage pre-push in the extracted repo passes with no file modified; shellcheck is clean on the helper and all six callers; sudo -n resolution prints /home/kuhy/src/hosts-blocker; HOSTS_BLOCKER_DIR=/nonexistent require_extracted_repo returns 1 with a clone instruction; check_directory_depth.sh --all reports 41; diff of report_live_state.sh --check before/after is empty."
|
||||
}
|
||||
@ -1,20 +0,0 @@
|
||||
{
|
||||
"title": "Extract mtk_root together with the libraries that live outside it",
|
||||
"objective": "Move the MediaTek rooting toolkit to its own public repo. Unlike the three app extractions, mtk_root's libraries and half its test suites live in linux_configuration/scripts/lib/, one directory up. Extracting the directory alone would orphan eight files here and leave the new repo unable to source anything.",
|
||||
"acceptance_criteria": [
|
||||
"The new repo preserves the mtk_root/ + lib/ sibling layout, so every `source \"$SCRIPT_DIR/../lib/...\"` resolves without editing a single call site.",
|
||||
"All 84 mtk tests pass from a clean clone of the new repo.",
|
||||
"shellcheck exits 0 over every script in the new repo with no severity filter, which requires carrying .shellcheckrc across.",
|
||||
"The new repo has its own lib/tests/run_all.sh, because the monorepo's is shared with a non-mtk suite and must stay.",
|
||||
"The suite remaining in the monorepo still passes: removing a shared fixture must not take test_common_datetime.sh down with it.",
|
||||
"meta/shell-coverage-allowlist.txt has no entry naming a removed path.",
|
||||
"check_shell_coverage.sh --all exits 0.",
|
||||
"pre-commit passes without --no-verify."
|
||||
],
|
||||
"out_of_scope": [
|
||||
"single_use/utils/lib/bl9000_*.sh, which is separate Android rooting tooling that merely mentions mtkclient.",
|
||||
"Renaming mtk_harness.sh, whose name is now historical but whose one consumer would churn for no behavioural gain.",
|
||||
"Verifying the toolkit against real hardware, which READY.md already records as pending."
|
||||
],
|
||||
"verifier": "./lib/tests/run_all.sh in the new repo reports 84 passed / 0 failed; shellcheck over the new repo exits 0; ./linux_configuration/scripts/lib/tests/run_all.sh here reports 55 passed / 0 failed; bash meta/scripts/check_shell_coverage.sh --all exits 0; gh confirms the repo is public on main."
|
||||
}
|
||||
@ -1,21 +0,0 @@
|
||||
{
|
||||
"title": "Extract phone_focus_mode with focus_policy into one public repo",
|
||||
"objective": "Move the GPS focus enforcer and its Python policy half out together, since focus_policy parses phone_focus_mode's config.sh and is referenced by nothing else. The repo is public and the subject matter is a home location, so proving no coordinate leaks is part of the job, not an afterthought.",
|
||||
"acceptance_criteria": [
|
||||
"Both trees land in one public repo with full history and python_pkg.focus_policy imports rewritten.",
|
||||
"The full git history contains no real home coordinate and no committed config_secrets.sh, verified BEFORE the repo is made public.",
|
||||
"The new repo has a .gitignore covering config_secrets.sh; none came across in the filter-repo pass.",
|
||||
"The Python suites pass from a clean clone: 161 tests.",
|
||||
"The shell suites pass with NO config_secrets.sh present, which means the harness must pin its own coordinates instead of inheriting real ones.",
|
||||
"config.sh can be sourced without a secrets file, so CI and a fresh clone work.",
|
||||
"Any suite that does not pass is skipped in CI with a stated reason, and its behaviour is compared against the pre-extraction baseline to establish whether the split caused it.",
|
||||
"The monorepo's remaining tests still pass and no config, hook or workflow names a removed path.",
|
||||
"pre-commit passes without --no-verify."
|
||||
],
|
||||
"out_of_scope": [
|
||||
"hosts and digital_wellbeing, deferred while the guard-lib repoint is pending.",
|
||||
"Fixing test_magisk_service.sh, which was already broken and is not a regression from this split.",
|
||||
"python-tests.yml's stale lichess_bot and tests/ path filters, absent since May 2026."
|
||||
],
|
||||
"verifier": "git log --all -p greps clean for the home coordinates; pytest reports 161 passed in the new repo; the shell suites run with the secrets file moved away; pytest linux_configuration/tests reports 178 passed here; check_shell_coverage.sh --all and check_python_location.sh over every .py both exit 0; check_directory_depth.sh --all drops 262 -> 248."
|
||||
}
|
||||
@ -1,20 +0,0 @@
|
||||
{
|
||||
"title": "Extract system-maintenance, the first of the four live subsystems",
|
||||
"objective": "Move the periodic maintenance subsystem to its own public repo. It is the isolated one -- no cross-unit source edges -- so it goes first and establishes the pattern for the three harder ones. Its Python tests live in linux_configuration/tests/ and reach it through a shared conftest, so the test half must move with it without breaking the suites that stay.",
|
||||
"acceptance_criteria": [
|
||||
"A vmbox pass first: the installer runs from an extracted-layout copy in the guest before anything is changed on the host, and the result is reported as sandbox-only.",
|
||||
"The new repo's Python tests pass from a clean clone with its own conftest; 136 tests.",
|
||||
"Both extracted shell suites pass after their hardcoded monorepo paths are fixed, and shellcheck exits 0 with no severity filter.",
|
||||
"The new repo declares BOTH CI halves, since python-tests.yml reached it by paths filter and shell-tests.yml by name list, and neither follows the code out.",
|
||||
"The suites remaining in the monorepo still pass: 202 Python tests.",
|
||||
"The cross-unit pair test test_shutdown_timer_monitor.sh keeps working, and does not silently degrade into asserting against a stale installed binary.",
|
||||
"No workflow, conftest, pyproject sys.path entry or coverage-allowlist line names a removed path.",
|
||||
"pre-commit passes without --no-verify."
|
||||
],
|
||||
"out_of_scope": [
|
||||
"Refreshing the stale /usr/local/bin/shutdown-timer-monitor.sh, which digital_wellbeing's installer owns.",
|
||||
"The other three subsystems.",
|
||||
"Moving the shared linux_configuration/tests/conftest.py, which serves several script directories that stay."
|
||||
],
|
||||
"verifier": "vm run in guest exercises the installer; python -m pytest tests -q reports 136 passed in the new repo; bash tests/*.sh and shellcheck both clean there; pytest linux_configuration/tests reports 202 passed here; bash linux_configuration/tests/test_shutdown_timer_monitor.sh passes with the checkout and SKIPs without it; check_shell_coverage.sh --all exits 0; check_directory_depth.sh --all drops 294 -> 267."
|
||||
}
|
||||
@ -1,20 +0,0 @@
|
||||
{
|
||||
"title": "Extract reverse_survivors, kcd2_dice_solver and focus_owner to their own public repos",
|
||||
"objective": "Move three self-contained applications out of testsAndMisc into their own repositories with full history, give each real CI it never had in the monorepo, and repoint every consumer the monorepo keeps - including the live systemd unit and Docker bind mount that deploy the dice solver.",
|
||||
"acceptance_criteria": [
|
||||
"Each unit is a public GitHub repo on branch main whose commit count equals the monorepo's count for that path (8, 14, 30).",
|
||||
"Each extracted repo passes its own toolchain from a clean clone: npm check+build; pnpm lint+test+build; flutter analyze --fatal-infos + test.",
|
||||
"Each extracted repo carries a CI workflow that runs those same commands with no paths filter and no working-directory.",
|
||||
"kcd2-dice-build.service starts successfully and exits 0 after being repointed - proven by running it, not by reading it.",
|
||||
"https://dice.kuhy.duckdns.org/ returns HTTP 200 and serves the bundle hash produced by that run.",
|
||||
"setup_kcd2_dice_solver.sh builds from the extracted repo, and its generated post-commit hook no longer filters on a kcd2_dice_solver/ path that can never match again.",
|
||||
"No file outside docs/ or a historical note still points at a removed directory.",
|
||||
"The repo-wide pytest gate and pre-commit both still pass, without --no-verify."
|
||||
],
|
||||
"out_of_scope": [
|
||||
"phone_focus_mode and python_pkg/focus_policy, which move together in a later step.",
|
||||
"The four system subsystems under periodic_background.",
|
||||
"Reformatting focus_owner's Dart sources: the committed tree predates the current formatter and 15 files would change, which belongs in its own commit in the new repo."
|
||||
],
|
||||
"verifier": "gh api confirms three public repos; standalone clones pass npm/pnpm/flutter checks; systemctl --user start kcd2-dice-build.service exits 0; curl https://dice.kuhy.duckdns.org/ returns 200 with a bundle hash matching dist/; ./meta/scripts/report_live_state.sh shows no unit pointing at a removed path; pre-commit passes."
|
||||
}
|
||||
@ -1,19 +0,0 @@
|
||||
{
|
||||
"title": "Flatten i3-configuration and misc/testsAndMisc-bash, and repair the references the first flatten missed",
|
||||
"objective": "Finish the flattening for the two remaining pure-taxonomy nestings, and close out the path rewrites that the previous commit's regex did not reach. The first flatten proved that the moves are easy and the references are the risk, so this change is mostly about finding references that a diff review would not surface.",
|
||||
"acceptance_criteria": [
|
||||
"i3 config no longer sits under periodic_background, where it never belonged; both resulting directories are at depth 2.",
|
||||
"misc/ no longer contains a directory named after the repo it lives in.",
|
||||
"The media-ignore rules from the moved .gitignore still apply at their new location, and no previously tracked file becomes ignored.",
|
||||
"Every $VAR/scripts/... reference is rewritten, including the orchestrators that name the still-deferred subsystems.",
|
||||
"Both i3blocks test suites pass, having been RUN rather than eyeballed.",
|
||||
"The repo-wide pytest gate still reports 100%.",
|
||||
"Every $HOME symlink broken by the move is repointed, and the i3blocks scripts are run to prove the bar works.",
|
||||
"pre-commit passes without --no-verify."
|
||||
],
|
||||
"out_of_scope": [
|
||||
"digital_wellbeing and hosts, deferred pending the guard-lib repoint; their references are updated to the new scheme but the directories stay.",
|
||||
"The ~57 remaining violations that encode real structure (a C project, Python package dirs, artgate's routes/styles, systemd unit dirs) rather than taxonomy."
|
||||
],
|
||||
"verifier": "check_directory_depth.sh --all falls 139 -> 104; both test_i3blocks_*.sh pass; pytest_changed_packages.py reports 1798 passed at 100%; find ~ -xtype l finds nothing; git check-ignore confirms the relocated media rules; check_shell_coverage.sh --all exits 0."
|
||||
}
|
||||
@ -1,21 +0,0 @@
|
||||
{
|
||||
"title": "Dissolve linux_configuration/scripts/ and single_use/ to satisfy the depth-2 cap",
|
||||
"objective": "Flatten the largest source of directory-depth violations. scripts/ and single_use/ were three levels of pure taxonomy carrying no information, and a 2-directory cap leaves no room for them. Categories move up to sit directly under linux_configuration/, which is what makes the cap satisfiable at all.",
|
||||
"acceptance_criteria": [
|
||||
"linux_configuration/scripts/ no longer exists; its children sit one level under linux_configuration/.",
|
||||
"Every relative `source` of a shared library resolves from its file's NEW location, checked by resolving each path on disk rather than by reading the diff.",
|
||||
"Every REPO_ROOT derivation that walks up a fixed number of levels is recomputed for the new depth.",
|
||||
"All lib/tests suites that passed before still pass, and any that fail are proven to fail identically in the pre-move baseline worktree.",
|
||||
"The repo-wide pytest gate still reports 100% coverage.",
|
||||
"Every symlink in $HOME pointing into the moved tree is repointed, and the i3blocks scripts are RUN to prove the status bar still works.",
|
||||
"No config, workflow, allowlist or ignore rule names a path that no longer exists.",
|
||||
"The .gitignore rules protecting untracked secrets still match after their paths change.",
|
||||
"pre-commit passes without --no-verify."
|
||||
],
|
||||
"out_of_scope": [
|
||||
"periodic_background/digital_wellbeing and hosts, deferred while the guard-lib repoint is pending; they keep their depth violations for now.",
|
||||
"Deduplicating the two Nextcloud implementations, which is a separate change.",
|
||||
"Wiring the depth gate, which lands only once the tree conforms."
|
||||
],
|
||||
"verifier": "check_directory_depth.sh --all falls 248 -> 139; every '$SCRIPT_DIR/../lib/...' source resolves on disk; 8 of 9 lib/tests run_all.sh pass with the ninth proven to need the jail at baseline too; pytest_changed_packages.py reports 1798 passed at 100%; find ~ -xtype l finds no broken link; the i3blocks scripts emit real output."
|
||||
}
|
||||
@ -1,19 +0,0 @@
|
||||
{
|
||||
"title": "Clear both hosts-guard libs and fix the done-redirect instrument artifact",
|
||||
"objective": "Bring hosts_guard_migrate.sh and hosts_guard_rollback.sh to 100% measured line coverage and remove both allowlist exemptions, extending continuation_lines() so a loop terminator carrying a redirect stops being counted as an unreachable statement.",
|
||||
"acceptance_criteria": [
|
||||
"Both libs measure 100.00% through tests/run_all.sh at --min 100 with --fail-on-case-error, exiting 0.",
|
||||
"check_shell_coverage.sh passes on both with their allowlist lines already deleted.",
|
||||
"The new _LOOP_END_OP predicate excludes ONLY lines where `done` is followed by a redirect or pipe; a bare `done` stays in the denominator so real uncovered loops are still caught.",
|
||||
"An audit printing every newly-excluded line with its text shows exactly the two lines in hosts_guard_rollback.sh and nothing else across fixes/lib.",
|
||||
"The instrument acceptance pair still measures exactly: dot_resolver_install.sh 39/39 and rpi_nc_ca.sh 73/73, through features/lib/tests/run_all.sh with its jail_args.",
|
||||
"Both libs keep their production behaviour: every new seam defaults to the real path it replaced.",
|
||||
"The suite stays green and shellcheck reports nothing, with zero suppressions."
|
||||
],
|
||||
"out_of_scope": [
|
||||
"The backslash-continued-assignment artifact that caps bt_adapter.sh at 98.55%; it is a different predicate and only affects one line in one lib.",
|
||||
"arch_sysctl_quarantined_cases.sh, which stays outside the suite.",
|
||||
"The last two untested libs, ubuntu_perf_fixes.sh and nvidia_config.sh."
|
||||
],
|
||||
"verifier": "timeout 900s bash meta/scripts/shell_coverage_jail.sh --timeout 600s --subject linux_configuration/scripts/single_use/fixes/lib/tests/run_all.sh --measure hosts_guard_migrate.sh (then hosts_guard_rollback.sh) --min 100 --fail-on-case-error -- \"\", then the acceptance pair through features/lib/tests/run_all.sh, then check_shell_coverage.sh on both libs."
|
||||
}
|
||||
@ -1,19 +0,0 @@
|
||||
{
|
||||
"title": "Make install_core_system.sh work on a fresh machine again",
|
||||
"objective": "install_core_system.sh is the documented way to set this machine up from scratch, and it could not complete: two of its seven modules pointed at paths that stopped existing when screen_locker and steam_backlog_enforcer were extracted into their own repos, nothing installed guard-lib (so the CORE midnight-shutdown module always died with 'guardctl not found on PATH'), and the hosts blocking module applied chattr +i while the file-guard instances that are supposed to watch and re-enforce /etc/hosts were only ever registered by a one-shot script filed under fixes/. A fresh machine therefore got immutability with no watcher, no canonical copy and no bind mount.",
|
||||
"acceptance_criteria": [
|
||||
"Every module in install_core_system.sh resolves to a script that exists, verified by running the installer end-to-end in a pristine vmbox guest that starts with none of the sibling repos checked out.",
|
||||
"A missing sibling repo (screen-locker, steam-backlog-enforcer, hosts-blocker, digital-wellbeing, utils/guard-lib) is cloned automatically; if that clone fails the module lands in the installer's Failed(...) summary rather than being silently skipped.",
|
||||
"guard-lib installs before the modules that depend on guardctl, so the midnight shutdown timer no longer fails on a fresh machine.",
|
||||
"hosts-blocker's install.sh registers the hosts, nsswitch and resolved file-guard instances itself, so `guardctl file-guard status hosts` shows a live watcher with a canonical copy and a bind mount with no manual step after the installer.",
|
||||
"Guard registration runs after the hosts file is written, so the canonical snapshot is of the post-write file.",
|
||||
"Re-running the installer is a no-op that still exits 0: stop_hosts_guard/restart_hosts_guard handle the guard-lib instances, not only the legacy per-file units.",
|
||||
"The security hardening suite reports 0 skipped for the screen_locker section, which had been silently skipping five checks against an extracted path."
|
||||
],
|
||||
"out_of_scope": [
|
||||
"boot-repair: its BIOS/GRUB guard landed 2026-08-22 in aec2b275.",
|
||||
"The installer's module-runner machinery (run_installer / ask_install); the defects are all in what it points at.",
|
||||
"Retiring the legacy hosts-guard pacman hooks and systemd units, which stays in the monorepo one-shot together with --rollback/--status. A fresh machine has no legacy layer to retire."
|
||||
],
|
||||
"verifier": "In a pristine vmbox guest: install_core_system.sh --all reports zero Failed entries and exits 0; a second run of the same command is a no-op that also exits 0; guardctl file-guard status hosts shows a registered instance with a bind mount; test_security_hardening.sh reports 0 skipped in the screen locker section. On the host, the same hardening suite goes from 18 passed / 1 failed / 5 skipped to 23 passed / 0 skipped before the installer runs."
|
||||
}
|
||||
@ -1,17 +0,0 @@
|
||||
{
|
||||
"title": "Make the nftables drift check run itself",
|
||||
"objective": "setup_wireguard_ssh.sh verify already gates on the two invariants whose breach caused the 2026-08-29 and 2026-08-30 container-network outages, but nothing ever invoked it, so it reported drift only when a human thought to ask. Install systemd units that run it at every boot and daily, and make a breach a failed unit visible in `systemctl --failed` rather than a log line nobody reads.",
|
||||
"acceptance_criteria": [
|
||||
"nftables-drift.service runs `setup_wireguard_ssh.sh verify` and is enabled at multi-user.target, ordered after nftables.service, docker.service and network-online.target.",
|
||||
"With /etc/nftables.conf drifted, a reboot leaves the unit in `failed` and it appears in `systemctl --failed`, naming the diff and the missing docker0 accept.",
|
||||
"With /etc/nftables.conf matching, a reboot leaves the unit active with both invariants OK and no false positive.",
|
||||
"nftables-drift.timer is armed with Persistent=true so a machine that was off still re-checks.",
|
||||
"The units are installed by `setup`, by `allow-web`/`allow-dns`, and by a new `install-verify` subcommand, so no step depends on a human remembering."
|
||||
],
|
||||
"out_of_scope": [
|
||||
"Any change to the ruleset itself or to verify_nft's two invariants.",
|
||||
"Notifications: a failed unit is the gate; notify-send from a root system unit is not.",
|
||||
"Section 1 of the handoff (signal-bot boot resilience), which shipped separately in kuhyx/signal-bot."
|
||||
],
|
||||
"verifier": "linux_configuration/features/lib/tests/test_wg_verify_units.sh under meta/scripts/shell_coverage_jail.sh; plus two real reboots in ~/src/utils/vmbox (drifted -> failed unit, clean -> active unit) and a live run on the host."
|
||||
}
|
||||
@ -1,19 +0,0 @@
|
||||
{
|
||||
"title": "Clear nvidia_config.sh and finish the fixes/lib campaign",
|
||||
"objective": "Bring nvidia_config.sh to 100% measured line coverage and remove its allowlist exemption, completing the eight-lib job the campaign started with.",
|
||||
"acceptance_criteria": [
|
||||
"The jail reports nvidia_config.sh at 100.00% through tests/run_all.sh at --min 100, exiting 0.",
|
||||
"check_shell_coverage.sh passes on it with the allowlist line already deleted.",
|
||||
"bt_audio.sh still measures 127/127 with the new test files present.",
|
||||
"All four functions are covered: backup_file for present and absent files, configure_xorg including both backup paths, configure_gcc_workaround for fresh and already-configured profiles, and install_pyroveil for auto-install, existing checkout, missing dependencies, and both interactive answers.",
|
||||
"configure_xorg is asserted to write RenderAccel 'true' and never 'false', since 'false' forces software rendering.",
|
||||
"No write reaches /etc or a real home: /etc/X11/xorg.conf.d/20-nvidia.conf keeps its 2025 mtime and ~/pyroveil does not exist.",
|
||||
"The lib stays under the 250-line cap and shellcheck reports nothing, with zero suppressions."
|
||||
],
|
||||
"out_of_scope": [
|
||||
"Verifying that pyroveil actually builds; git/cmake/ninja are stubbed and only the orchestration is asserted.",
|
||||
"The ~12 gated libs outside fixes/lib that already fail the gate for pre-existing reasons.",
|
||||
"arch_sysctl.sh, which stays quarantined and exempt."
|
||||
],
|
||||
"verifier": "timeout 900s bash meta/scripts/shell_coverage_jail.sh --timeout 600s --subject linux_configuration/scripts/single_use/fixes/lib/tests/run_all.sh --measure nvidia_config.sh --min 100 --fail-on-case-error -- \"\" (expects 76/76 = 100.00%, exit 0), then the same for bt_audio.sh as a canary, then check_shell_coverage.sh with the allowlist line removed."
|
||||
}
|
||||
@ -1,18 +0,0 @@
|
||||
{
|
||||
"title": "Grade against 3.14, not just run on it",
|
||||
"objective": "meta/pyproject.toml's ruff, mypy and pylint targets must name the same Python that requires-python and the CI pin already name (3.14), and the tree must be clean under those targets. Success is every lint, type and style gate passing at py314 with no suppressions added.",
|
||||
"acceptance_criteria": [
|
||||
"meta/pyproject.toml has target-version = \"py314\", python_version = \"3.14\" and py-version = \"3.14\"",
|
||||
"pre-commit run ruff --all-files passes",
|
||||
"pre-commit run ruff-format --all-files passes",
|
||||
"pre-commit run mypy --all-files passes",
|
||||
"pre-commit run pylint --all-files passes",
|
||||
"No noqa, type: ignore or per-file-ignores were added"
|
||||
],
|
||||
"out_of_scope": [
|
||||
"Dependency version pins (handled by the separate freshness gate and its own commit)",
|
||||
"CI workflow files, which already pin python-version 3.x",
|
||||
"app/ and web/ subtrees, which are not Python"
|
||||
],
|
||||
"verifier": "pre-commit run --all-files (ruff, ruff-format, mypy, pylint), plus the repo's CI mirror on push"
|
||||
}
|
||||
@ -1,19 +0,0 @@
|
||||
{
|
||||
"title": "Close the extensionless blind spot in the length cap and split boot-repair",
|
||||
"objective": "check_file_length.sh required a dot in the filename, so every extensionless executable was uncapped. boot-repair had grown to 756 lines in that gap - three times the largest capped shell file in the repo. Widen the predicate and bring the file it was hiding under the cap, without a sandbox pass that would mislead: the vmbox guest is BIOS/GRUB and this machine is UEFI.",
|
||||
"acceptance_criteria": [
|
||||
"is_source_file() accepts extensionless files that begin with a shebang, and still skips extensionless data files such as i3/config.",
|
||||
"Every file in the repo, including boot-repair and its new libraries, is within the 250-line cap.",
|
||||
"The 42-test fixture suite passes with exactly the same count as before the split.",
|
||||
"verify_shell_split.sh proves the functions moved verbatim, and any difference it reports is a deliberate change named in the evidence rather than an accident.",
|
||||
"shellcheck is clean on the entry script and every library, with no suppression added.",
|
||||
"No sandbox claim is made for a UEFI-critical script that a BIOS guest cannot validate.",
|
||||
"pre-commit passes without --no-verify."
|
||||
],
|
||||
"out_of_scope": [
|
||||
"live_test.sh, which needs root and did before this change too.",
|
||||
"Restructuring boot-repair's top-level control flow to shrink it further; the report block stays inline because sourcing it broke 30 tests.",
|
||||
"Other extensionless scripts, all already under the cap."
|
||||
],
|
||||
"verifier": "check_file_length.sh --all reports boot-repair before the split and 'all checked files are within 250 lines' after; tests/test_boot_repair.sh reports 42 passed both before and after; verify_shell_split.sh reports 31/32 identical with clean_shadow_files as the one named difference; shellcheck exits 0 across the entry script and all four libs."
|
||||
}
|
||||
@ -1,17 +0,0 @@
|
||||
{
|
||||
"title": "Make the fixes/lib suite fast without weakening it",
|
||||
"objective": "Cut the suite from 110s to well under a minute so pre-push stops being dominated by it, changing only how long things wait and how the PATH shim farm is built.",
|
||||
"acceptance_criteria": [
|
||||
"run_all.sh completes in under 60s and exits 0.",
|
||||
"The assertion count is unchanged at 645 OK with 0 FAIL; no test is deleted, skipped or weakened.",
|
||||
"The cases that depend on a tool being genuinely unfindable still pass, so the shim farm is still doing its job.",
|
||||
"Every touched file stays under the 250-line cap and shellcheck-clean with zero suppressions.",
|
||||
"thorium_repairs.sh's production wait is unchanged by default: THORIUM_STARTUP_WAIT defaults to the original 4."
|
||||
],
|
||||
"out_of_scope": [
|
||||
"Changing what any test asserts.",
|
||||
"The coverage jail's own runtime, which is dominated by kcov and runs two passes by design.",
|
||||
"The pacman_hook_stall harness, which predates lib_test_core.sh."
|
||||
],
|
||||
"verifier": "time bash linux_configuration/scripts/single_use/fixes/lib/tests/run_all.sh (expects exit 0 in under 60s), plus `bash run_all.sh | grep -c '^ OK'` returning 645 and `grep -c '^ FAIL'` returning 0."
|
||||
}
|
||||
@ -1,18 +0,0 @@
|
||||
{
|
||||
"title": "Clear thorium_repairs.sh and lift the coverage gate's case timeout",
|
||||
"objective": "Bring thorium_repairs.sh to 100% measured line coverage and remove its allowlist exemption, and stop the gate's 120s per-case timeout from failing libs whose coverage is actually 100%.",
|
||||
"acceptance_criteria": [
|
||||
"The jail reports thorium_repairs.sh at 100.00% through tests/run_all.sh at --min 100, exiting 0.",
|
||||
"check_shell_coverage.sh passes on thorium_repairs.sh with its allowlist line already deleted, so the pass cannot come from the exemption.",
|
||||
"check_shell_coverage.sh passes on bt_audio.sh, which failed before this change despite measuring 127/127, because the case timed out.",
|
||||
"All five profile-repair functions plus test_thorium are covered, including every dry-run arm and the sqlite3-absent path.",
|
||||
"thorium_repairs.sh itself is unmodified: it already took every path from $THORIUM_CONFIG_DIR, so no production seam was needed.",
|
||||
"The suite stays green and shellcheck reports nothing, with zero suppressions."
|
||||
],
|
||||
"out_of_scope": [
|
||||
"Un-quarantining arch_sysctl_quarantined_cases.sh: re-adding it still breaks bt_audio's measurement even at a 600s timeout, so runtime was not its cause.",
|
||||
"Changing the coverage instrument (shell_coverage_lines.py).",
|
||||
"The remaining four untested libs: hosts_guard_migrate, hosts_guard_rollback, ubuntu_perf_fixes, nvidia_config."
|
||||
],
|
||||
"verifier": "timeout 900s bash meta/scripts/shell_coverage_jail.sh --timeout 600s --subject linux_configuration/scripts/single_use/fixes/lib/tests/run_all.sh --measure thorium_repairs.sh --min 100 --fail-on-case-error -- \"\" (expects 75/75 = 100.00%, exit 0), then bash meta/scripts/check_shell_coverage.sh on thorium_repairs.sh and bt_audio.sh with the allowlist line removed."
|
||||
}
|
||||
@ -1,16 +0,0 @@
|
||||
{
|
||||
"title": "Measure tool-call batching per API message in token_audit",
|
||||
"objective": "A per-record count of tool calls reports 1.00 calls/message regardless of actual behaviour, because the harness splits one API message into one record per tool_use block. Grouping by message.id makes the real rate (1.9%) observable, and persisting it in the snapshot lets the next weekly run adjudicate whether batching improved without any hand-rolled analysis.",
|
||||
"acceptance_criteria": [
|
||||
"Batching is grouped by message.id, not by transcript record",
|
||||
"The markdown report shows batched share and calls per message",
|
||||
"weekly.json carries tool_messages, batched_messages and batched_calls",
|
||||
"A window with no tool calls does not divide by zero",
|
||||
"100% branch coverage retained and no lint suppressions introduced"
|
||||
],
|
||||
"out_of_scope": [
|
||||
"Changing reconciled totals, cost weights, or the mtime-based window selection",
|
||||
"Acting on the measurement (hooks/nudges live in ~/.claude, not this repo)"
|
||||
],
|
||||
"verifier": "python3 -m pytest python_pkg/token_audit/tests; python3 -m coverage report (100%); pre-commit run --files <changed>; python3 -m python_pkg.token_audit --days 7 --no-write"
|
||||
}
|
||||
@ -1,19 +0,0 @@
|
||||
{
|
||||
"title": "Clear ubuntu_perf_fixes.sh from the shell coverage allowlist",
|
||||
"objective": "Bring ubuntu_perf_fixes.sh to 100% measured line coverage and remove its allowlist exemption, leaving nvidia_config.sh as the only untested lib in fixes/lib.",
|
||||
"acceptance_criteria": [
|
||||
"The jail reports ubuntu_perf_fixes.sh at 100.00% through tests/run_all.sh at --min 100, exiting 0.",
|
||||
"check_shell_coverage.sh passes on it with the allowlist line already deleted.",
|
||||
"bt_audio.sh still measures 127/127 with the new test files present, since adding a file to this suite has silently broken a cleared lib before.",
|
||||
"All four functions are covered: the sysctl drop-in and its undo fallbacks, both NVIDIA branches (helper service and standalone fallback), earlyoom install/configure/backup, and fix_failed_sssd for none, all and exactly one failed unit.",
|
||||
"The add_undo lines keep their literal /etc paths, because the undo script runs on the real system.",
|
||||
"No write reaches /etc: neither /etc/sysctl.d/99-performance-tuning.conf nor /etc/default/earlyoom exists after the suite runs.",
|
||||
"The lib stays under the 250-line cap and shellcheck reports nothing, with zero suppressions."
|
||||
],
|
||||
"out_of_scope": [
|
||||
"nvidia_config.sh, the last untested lib, which needs its own harness.",
|
||||
"The ~12 gated libs outside fixes/lib that already fail the gate for reasons predating this work.",
|
||||
"arch_sysctl.sh, which stays quarantined and exempt."
|
||||
],
|
||||
"verifier": "timeout 900s bash meta/scripts/shell_coverage_jail.sh --timeout 600s --subject linux_configuration/scripts/single_use/fixes/lib/tests/run_all.sh --measure ubuntu_perf_fixes.sh --min 100 --fail-on-case-error -- \"\" (expects 81/81 = 100.00%, exit 0), then the same for bt_audio.sh as a canary, then check_shell_coverage.sh with the allowlist line removed."
|
||||
}
|
||||
@ -1,17 +0,0 @@
|
||||
{
|
||||
"title": "Deploy the personal website's post editor as a loopback-only unit",
|
||||
"objective": "The website repo now ships a browser post editor. Give it a home on the server: a systemd unit bound to 127.0.0.1, started by setup_personal_website.sh, with credentials generated once and no public route. Success is that after a setup run the editor answers on 127.0.0.1:4321/admin, survives a reboot, and is reachable from a laptop only through an SSH tunnel.",
|
||||
"acceptance_criteria": [
|
||||
"setup_personal_website.sh builds the editor and fails if either half of the build is missing.",
|
||||
"Credentials are generated once, stored as a scrypt hash in a 0600 env file, and are not rotated by a second setup run.",
|
||||
"The unit binds to loopback, runs under ProtectSystem=strict with the checkout as its only writable path, and still completes a real build.",
|
||||
"No Caddy snippet, no firewall opening, and no reference to the editor on the public domain.",
|
||||
"The setup report prints the tunnel command and the path of the password file, never the password."
|
||||
],
|
||||
"out_of_scope": [
|
||||
"Any public or Tailscale route for the editor.",
|
||||
"TLS for the editor: the SSH tunnel terminates on localhost, which browsers already treat as a secure origin.",
|
||||
"Changes to the website container, the shared Caddy edge, or the Gitea relocation."
|
||||
],
|
||||
"verifier": "pre-commit run --files <the four shell files>; meta/scripts/shell_coverage_jail.sh over linux_configuration/features/lib/tests/test_website_admin.sh with --fail-on-case-error; systemd-analyze verify and a systemd-run rehearsal of the sandbox."
|
||||
}
|
||||
@ -1,19 +0,0 @@
|
||||
{
|
||||
"title": "Wire the shell coverage ratchet into pre-commit and test the coverage tooling",
|
||||
"objective": "Make the shell ratchet enforceable at commit time rather than existing but never running, and close the gap where the three libs that build the coverage jail had no tests of their own.",
|
||||
"acceptance_criteria": [
|
||||
"A new shell library in a directory with no tests/run_all.sh is refused by pre-commit.",
|
||||
"A shell library in a directory that has a suite, or one on the allowlist, is accepted.",
|
||||
"check_shell_coverage.sh --all reports zero BLOCK entries and completes in under 5 seconds.",
|
||||
"check_shell_coverage.sh emits no 'unbound variable' error on its failure path.",
|
||||
"The new suite runs green standalone via bash meta/scripts/lib/tests/run_all.sh.",
|
||||
"No lint suppression (noqa, shellcheck disable, per-file ignore) is added anywhere.",
|
||||
"The allowlist is not grown; entries may only be removed."
|
||||
],
|
||||
"out_of_scope": [
|
||||
"Restoring the jail-measured 100% bar removed in 578c4a6c.",
|
||||
"Writing suites for the 8 allowlisted directories that still have none.",
|
||||
"Changing shell_coverage_jail.sh, which is still used by ci_mirror.sh."
|
||||
],
|
||||
"verifier": "bash meta/scripts/lib/tests/run_all.sh exits 0 with 48 assertions; pre-commit run shell-coverage-ratchet --all-files passes; creating an untested lib under mtk_root/lib and running the hook on it fails with a 'needs: .../tests/run_all.sh' message; shellcheck -x and shfmt -l are clean on every touched file."
|
||||
}
|
||||
@ -1,36 +0,0 @@
|
||||
{
|
||||
"intent": "Two sessions reading their own virtual displays at the same moment must not kill each other's dump.",
|
||||
"scope": [
|
||||
"python_pkg/android_ui/_a11y_helper.py (lock), driver.py (passes the serial), tests, README"
|
||||
],
|
||||
"changes": [
|
||||
"dump_display runs install + am instrument under a per-phone flock at ~/.cache/android_ui/a11ydump-<serial>.lock",
|
||||
"AndroidUi passes its serial so different phones do not queue behind each other"
|
||||
],
|
||||
"verification": [
|
||||
{
|
||||
"command": "live two-session run on the Pixel 6a before the fix (fake owners claude:fakeA / claude:fakeB, 300 s)",
|
||||
"result": "fail",
|
||||
"evidence": "logcat: 'stop com.kuhy.a11ydump due to start instr' then 'Crash of app com.kuhy.a11ydump running instrumentation'; android_ui: 'a11y helper returned no tree: Process crashed'"
|
||||
},
|
||||
{
|
||||
"command": "same run after the fix: /tmp/two_session/run.sh, DURATION=300",
|
||||
"result": "pass",
|
||||
"evidence": "A 219 element taps on Calculator (display 14), B 440 dp taps + dumps on Clock (display 13), 0 failures, empty stderr; 99 samples: display 0 stayed on Signal, 14 on Calculator, 13 on Clock; all five alarms still off"
|
||||
},
|
||||
{
|
||||
"command": "raw adb from a Claude Bash call while claude:fakeA held pkg:__screen__",
|
||||
"result": "pass",
|
||||
"evidence": "`adb shell input -d 0 tap` blocked after 40 s naming claude:fakeA; `input keyevent 0` without -d blocked at once; `input -d 13 tap` refused as claude_fakeB's display"
|
||||
},
|
||||
{
|
||||
"command": "pytest python_pkg/android_ui python_pkg/phone_guard --cov --cov-branch",
|
||||
"result": "pass",
|
||||
"evidence": "153 passed; every module 100% line and branch"
|
||||
}
|
||||
],
|
||||
"risks": [
|
||||
"The lock is per host: sessions on another machine sharing the phone over adb are not serialised."
|
||||
],
|
||||
"rollback": ["git revert the commit"]
|
||||
}
|
||||
@ -1,40 +0,0 @@
|
||||
{
|
||||
"intent": "Let android_ui address Flutter/Material text fields by their visible label, so a form can be filled by name instead of by screen position.",
|
||||
"scope": [
|
||||
"python_pkg/android_ui/_elements.py",
|
||||
"python_pkg/android_ui/tests/test_driver.py",
|
||||
"python_pkg/android_ui/README.md",
|
||||
"Non-goal: no change to any app under test — the gap was in this tool, not in the app"
|
||||
],
|
||||
"changes": [
|
||||
"Parse the node's `hint` attribute and carry it on UiElement (default \"\", so existing constructor calls are unaffected).",
|
||||
"Rank hint below text and content-desc but above resource-id in `label`, and add it to the `matches` haystack: Material clears the hint once content arrives, so a filled field still answers to its real text.",
|
||||
"Keep a node that has only a hint, which the previous 'must be labelled' filter dropped.",
|
||||
"Document that wrapping such a field in Semantics(label:)/MergeSemantics is a trap that empties Android's tree, and that a11y changes must be verified with uiautomator dump rather than flutter test."
|
||||
],
|
||||
"verification": [
|
||||
{
|
||||
"command": "python3 -m pytest python_pkg/android_ui/tests/ -q",
|
||||
"result": "pass",
|
||||
"evidence": "59 passed (57 before, +2 new: hint names an empty field; real text outranks the hint once typed)"
|
||||
},
|
||||
{
|
||||
"command": "pre-commit run --files python_pkg/android_ui/_elements.py python_pkg/android_ui/tests/test_driver.py python_pkg/android_ui/README.md",
|
||||
"result": "pass",
|
||||
"evidence": "no failures reported"
|
||||
},
|
||||
{
|
||||
"command": "python3 -m python_pkg.android_ui dump # workout_app manual-workout form, real device 23181JEGR08034",
|
||||
"result": "pass",
|
||||
"evidence": "Fields previously dumped as '' now read 'Start time (HH:MM)', 'End time (HH:MM)', 'Location name', 'How did you get there?', 'Cost (e.g. 40 PLN)', 'What sport/activity', 'What was done'. Nine fields then filled by name, each reporting 'typed into <label> and verified'."
|
||||
}
|
||||
],
|
||||
"risks": [
|
||||
"A screen whose hint duplicates another element's text could become ambiguous; matches() already raises AmbiguousElementError rather than acting on a coin flip.",
|
||||
"hint is only present while a Material field is empty, so resolve every target from one dump pass before typing rather than re-dumping between fields."
|
||||
],
|
||||
"rollback": [
|
||||
"git revert the commit; UiElement.hint defaults to \"\" so nothing else depends on it.",
|
||||
"After rollback, re-run the android_ui suite and expect Material fields to dump as anonymous EditTexts again."
|
||||
]
|
||||
}
|
||||
@ -1,45 +0,0 @@
|
||||
{
|
||||
"intent": "Give the new ~/src/restaurant-rater app a launcher icon from the shared family, so it stops shipping the stock Flutter beach icon.",
|
||||
"scope": [
|
||||
"python_pkg/app_icons/_glyph_art_more.py - the new SVG fragment",
|
||||
"python_pkg/app_icons/glyphs.py - register the glyph in GLYPHS",
|
||||
"python_pkg/app_icons/apps.py - register the app so --app restaurant_rater passes argument validation",
|
||||
"Non-goal: no change to the shared accent #B8862E or the charcoal field; a glyph is distinguished by silhouette only",
|
||||
"Non-goal: no change to any existing app's icon"
|
||||
],
|
||||
"changes": [
|
||||
"Added _BOWL_STAR: a filled five-pointed star (R=140 r=56 about 512,380) above an open bowl with a rim at y=566 and walls dropping to y=752. Total ink y=240..788, inside the 560 safe box.",
|
||||
"The star is filled rather than stroked, for the same reason as _QUILL_NIB and _ANVIL: at STROKE_WIDTH=72 its five interior notches close below MIN_NEGATIVE_SPACE and render as a pentagon at 48dp.",
|
||||
"Registered Glyph('bowl-star', ...) in GLYPHS and AppIcon(key='restaurant_rater', glyph='bowl-star', linux=False) in APPS."
|
||||
],
|
||||
"verification": [
|
||||
{
|
||||
"command": "PYTHONPATH=~/src/testsAndMisc python3 -m python_pkg.app_icons list",
|
||||
"result": "pass",
|
||||
"evidence": "bowl-star appears under Glyphs; 'restaurant_rater #B8862E bowl-star /home/kuhy/src/restaurant-rater' appears under Apps."
|
||||
},
|
||||
{
|
||||
"command": "PYTHONPATH=~/src/testsAndMisc python3 -m python_pkg.app_icons generate --app restaurant_rater",
|
||||
"result": "pass",
|
||||
"evidence": "wrote 6 asset files to /home/kuhy/src/restaurant-rater/assets/icon (icon/foreground/monochrome, svg+png each). The generator's safe-box and negative-space checks did not fire."
|
||||
},
|
||||
{
|
||||
"command": "visual review of the rendered 640px icon.png",
|
||||
"result": "pass",
|
||||
"evidence": "First render read as a smiling face - shallow bowl walls stopping at y=736 became a mouth with the star for a nose. Deepened to y=752 between narrower walls (348/676) and re-rendered; it now reads as a bowl with a rim. Recorded in the fragment's comment so the next reader does not undo it."
|
||||
},
|
||||
{
|
||||
"command": "pre-commit run --files python_pkg/app_icons/glyphs.py python_pkg/app_icons/apps.py python_pkg/app_icons/_glyph_art_more.py",
|
||||
"result": "pass",
|
||||
"evidence": "mypy, pylint 10.00/10, bandit, pytest with coverage enforcement, 250-line cap and codespell all pass. An earlier run failed pylint C0301 on a 111-char comment line; the comment was rewrapped rather than suppressed."
|
||||
}
|
||||
],
|
||||
"risks": [
|
||||
"The glyph is new artwork, so only a visual check can confirm it reads correctly at 48dp. Mitigated by rendering and reviewing the actual PNG rather than trusting the SVG source.",
|
||||
"_glyph_art_more.py grew to 127 lines; it and _glyph_art.py exist solely to stay under the 250-line cap, so a third file will be needed eventually."
|
||||
],
|
||||
"rollback": [
|
||||
"git revert the commit. The three edits are additive - no existing glyph, app or shared token is touched - so a revert cannot change any other app's icon.",
|
||||
"After rollback, re-run 'python3 -m python_pkg.app_icons list' and confirm the remaining 15 glyphs still resolve, then regenerate any app whose icon was rebuilt in the meantime."
|
||||
]
|
||||
}
|
||||
@ -1,43 +0,0 @@
|
||||
{
|
||||
"intent": "The new Umbrella? Flutter app (~/src/umbrella, com.kuhy.umbrella) gets a launcher icon from the shared family instead of the stock Flutter icon.",
|
||||
"scope": [
|
||||
"python_pkg/app_icons: new `umbrella` glyph and registry entry",
|
||||
"AppIcon dataclass moved to _app_icon.py so apps.py stays under the 250-line cap (re-exported, API unchanged)",
|
||||
"No change to style.py or any other app's icon"
|
||||
],
|
||||
"changes": [
|
||||
"_glyph_art_more.py: _UMBRELLA, a filled scalloped canopy over a stroked J-handle, geometry inside SAFE_BOX with a 40px hook opening (>= MIN_NEGATIVE_SPACE)",
|
||||
"glyphs.py: registers the umbrella glyph",
|
||||
"apps.py: registers the umbrella app; AppIcon now imported from _app_icon.py and listed in __all__",
|
||||
"_app_icon.py: the AppIcon record, Path under TYPE_CHECKING"
|
||||
],
|
||||
"verification": [
|
||||
{
|
||||
"command": "PYTHONPATH=. python3 -m python_pkg.app_icons generate --app umbrella --android",
|
||||
"result": "pass",
|
||||
"evidence": "umbrella: wrote 6 asset files to ~/src/umbrella/assets/icon; regenerated Android mipmaps; no safe-box warning"
|
||||
},
|
||||
{
|
||||
"command": "PYTHONPATH=. python3 -m python_pkg.app_icons preview -o /tmp/sheet.png",
|
||||
"result": "pass",
|
||||
"evidence": "umbrella renders in the family row at full size, circle mask and 48dp; reads as an umbrella"
|
||||
},
|
||||
{
|
||||
"command": "pytest python_pkg/app_icons",
|
||||
"result": "pass",
|
||||
"evidence": "44 passed; registry invariants (real glyph, shared accent, hex accent) cover the new entry"
|
||||
},
|
||||
{
|
||||
"command": "pre-commit run --files <changed-files>",
|
||||
"result": "pass",
|
||||
"evidence": "all hooks passed"
|
||||
}
|
||||
],
|
||||
"risks": [
|
||||
"Importers of AppIcon from apps.py keep working via the re-export; generate.py and test_generate.py still import it from there."
|
||||
],
|
||||
"rollback": [
|
||||
"git revert the commit; the umbrella app keeps its already-generated PNGs",
|
||||
"Re-run pytest python_pkg/app_icons"
|
||||
]
|
||||
}
|
||||
@ -1,36 +0,0 @@
|
||||
{
|
||||
"intent": "Make the CI-baseline guard's own error message tell the whole truth: the fix-forward escape hatch has to be given at commit AND at push, because the hook runs at both stages.",
|
||||
"scope": [
|
||||
"meta/scripts/check_ci_green.sh: the header comment and the failure hint",
|
||||
"Non-goal: changing when the guard fires, what it checks, or the semantics of CI_GREEN_SKIP"
|
||||
],
|
||||
"changes": [
|
||||
"Header comment now documents both invocations and why the second is needed (the pre-push CI mirror re-runs pre-commit in a clean worktree against the same still-red baseline)",
|
||||
"Failure hint now prints `CI_GREEN_SKIP=1 git push` beneath the commit form, plus one line naming the mirror as the reason"
|
||||
],
|
||||
"verification": [
|
||||
{
|
||||
"command": "shellcheck meta/scripts/check_ci_green.sh",
|
||||
"result": "pass",
|
||||
"evidence": "shellcheck OK (no output)"
|
||||
},
|
||||
{
|
||||
"command": "CI_GREEN_SKIP=1 bash meta/scripts/check_ci_green.sh",
|
||||
"result": "pass",
|
||||
"evidence": "check_ci_green.sh: CI_GREEN_SKIP is set / skipping the CI-baseline check (pre-push ci-mirror is the real gate). exit 0"
|
||||
},
|
||||
{
|
||||
"command": "bash meta/scripts/check_ci_green.sh",
|
||||
"result": "pass",
|
||||
"evidence": "silent, exit 0, against a main that is green as of 6eb70de0"
|
||||
}
|
||||
],
|
||||
"risks": [
|
||||
"The red path's exact wording is not covered by a test; there is no test file for this script in the repo, so a future edit could break the hint without failing anything.",
|
||||
"If ci_mirror.sh ever starts scrubbing the environment, the documented push form stops working and this message becomes wrong."
|
||||
],
|
||||
"rollback": [
|
||||
"git revert the commit; the change is comment and printf text only, so nothing depends on it.",
|
||||
"After rollback, confirm the guard still exits 0 on a green main and 1 on a red one."
|
||||
]
|
||||
}
|
||||
@ -1,33 +0,0 @@
|
||||
{
|
||||
"intent": "Put every dependency this repo declares on its newest stable release, and install the shared dependency-freshness gate so it stays there.",
|
||||
"scope": [
|
||||
"meta/requirements.txt, meta/requirements-test.txt, python_pkg/random_jpg/requirements.txt",
|
||||
".pre-commit-config.yaml, .github/workflows/dependency-freshness.yml, scripts/check_dependency_freshness.sh",
|
||||
"Non-goal: no source changes; this is manifests and gate wiring only"
|
||||
],
|
||||
"changes": [
|
||||
"Exact-pinned 95 previously range-specified dependencies to their newest stable release, via ~/.claude/scripts/apply_dep_pins.py driven by the gate's own --json findings.",
|
||||
"Moved the gatelock git-tag dependency from gatelock-v0.1.0 to gatelock-v0.7.2 -- six minor versions of drift that a PEP 508 direct reference hid from every previous check.",
|
||||
"Installed the shared gate's three pieces (delegate, pre-commit hook, CI workflow) from ~/src/utils/dep_freshness/templates."
|
||||
],
|
||||
"verification": [
|
||||
{
|
||||
"command": "REQUIREMENTS_FILE=meta/requirements.txt ./meta/scripts/ci_mirror.sh",
|
||||
"result": "pass",
|
||||
"evidence": "exit 0; clean venv rebuilt from the repinned meta/requirements.txt, pre-commit --all-files and the full suite green inside it"
|
||||
},
|
||||
{
|
||||
"command": "~/src/utils/scripts/check_dependency_freshness.sh --all",
|
||||
"result": "pass",
|
||||
"evidence": "exit 0 -- 97 findings before, 0 after"
|
||||
}
|
||||
],
|
||||
"risks": [
|
||||
"gatelock v0.1.0 -> v0.7.2 is a six-version jump in a shared library; the clean-venv mirror is what exercises it here.",
|
||||
"Exact pins mean CI goes red on any upstream release until the pin is bumped. That is the chosen behaviour, not a regression."
|
||||
],
|
||||
"rollback": [
|
||||
"git revert the commit; the pins and the three gate files are the whole change.",
|
||||
"After reverting, re-run ci_mirror.sh to confirm the older pins still resolve."
|
||||
]
|
||||
}
|
||||
@ -1,43 +0,0 @@
|
||||
{
|
||||
"intent": "Keep the morning workout lock running after the PC's wake alarm was deleted upstream, by removing the orchestrator's now-dangling alarm leg in the same change.",
|
||||
"scope": [
|
||||
"python_pkg/morning_routine/_orchestrator.py",
|
||||
"python_pkg/morning_routine/morning-routine.service",
|
||||
"python_pkg/morning_routine/install.sh",
|
||||
"python_pkg/morning_routine/tests/test_orchestrator.py",
|
||||
"Non-goal: any change to screen_locker or to the workout lock itself",
|
||||
"Non-goal: removing the orchestrator, which wake_alarm's TRIGGER_TARGET_UNIT still names"
|
||||
],
|
||||
"changes": [
|
||||
"Removed ALARM_MODULE, _run_alarm() and the --with-alarm flag; main() now runs only the workout lock. wake_alarm._alarm no longer exists after kuhyx/wake-alarm retired the PC's waking role.",
|
||||
"morning-routine.service drops --with-alarm; its Description and comments now name wake-alarm-trigger.timer as what starts it, instead of the deleted systemd-sleep hook. Environment=DISPLAY=:0 kept, because workout-locker.service needs it even though the comment credited it to the alarm's Tk window.",
|
||||
"install.sh header no longer promises rtcwake/python-kasa/sleep-hook prerequisites that upstream no longer installs.",
|
||||
"Bumped hypothesis 6.165.10 -> 6.167.0 in meta/requirements.txt to clear a pre-existing red dependency-freshness baseline on main, unrelated to this change but blocking the commit."
|
||||
],
|
||||
"verification": [
|
||||
{
|
||||
"command": "/usr/bin/python3 -m pytest python_pkg/morning_routine/tests/ -q",
|
||||
"result": "pass",
|
||||
"evidence": "7 passed. Alarm-leg tests replaced by test_main_runs_the_workout_lock and test_production_flag_is_still_accepted (systemd passes --production)."
|
||||
},
|
||||
{
|
||||
"command": "shellcheck python_pkg/morning_routine/install.sh",
|
||||
"result": "pass",
|
||||
"evidence": "clean"
|
||||
},
|
||||
{
|
||||
"command": "grep -rn 'with-alarm|ALARM_MODULE|_run_alarm' python_pkg/morning_routine/",
|
||||
"result": "pass",
|
||||
"evidence": "no matches; the flag is gone from source, unit file, installer and tests together"
|
||||
}
|
||||
],
|
||||
"risks": [
|
||||
"The installed ~/.config/systemd/user/morning-routine.service is a copy, not a symlink, so it keeps the old --with-alarm ExecStart until install.sh is re-run. Until then the orchestrator receives an unknown flag and argparse exits 2, so the workout lock would not run.",
|
||||
"This is the exact failure class the change exists to prevent: on 2026-05-28 a stale module reference here left the workout lock dying with ModuleNotFoundError for a month while the alarm half kept working and hid it."
|
||||
],
|
||||
"rollback": [
|
||||
"git revert the commit, then re-run bash python_pkg/morning_routine/install.sh to restore the unit file.",
|
||||
"Rollback is only safe together with reverting kuhyx/wake-alarm's deletion commit: restoring --with-alarm while wake_alarm._alarm is absent reintroduces the dangling reference.",
|
||||
"After rollback: systemctl --user cat morning-routine.service and confirm the ExecStart matches the repo copy."
|
||||
]
|
||||
}
|
||||
@ -0,0 +1,43 @@
|
||||
{
|
||||
"intent": "The endurain_import phone fallback stops re-pulling RunnerUp exports that were already imported, so each run no longer copies the whole phone history back into the WebDAV inbox, and one summary line says what happened.",
|
||||
"scope": [
|
||||
"python_pkg/endurain_import/sources.py: PROCESSED_DIRNAME, processed_dir_for(), pull_from_phone skip + summary log",
|
||||
"python_pkg/endurain_import/__main__.py: uses processed_dir_for() instead of a literal 'processed'",
|
||||
"python_pkg/endurain_import/tests/test_phone_pull.py: new tests",
|
||||
"Non-goal: the phone is still read-only; nothing on it is deleted"
|
||||
],
|
||||
"changes": [
|
||||
"pull_from_phone skips a name already in the inbox (pending) or in processed/ (imported) before any adb pull",
|
||||
"One summary line per run: on phone / pulled / skipped imported / skipped pending / failed; logged at WARNING when any pull failed, INFO otherwise",
|
||||
"processed_dir_for(inbox) is the single definition shared by the importer and the fallback"
|
||||
],
|
||||
"verification": [
|
||||
{
|
||||
"command": "python3 -m pytest -q --cov=python_pkg.endurain_import --cov-branch python_pkg/endurain_import",
|
||||
"result": "pass",
|
||||
"evidence": "103 passed; endurain_import 100% line+branch (sources.py was 97%, lines 174-175 uncovered, before the new tests)"
|
||||
},
|
||||
{
|
||||
"command": "python3 -m python_pkg.endurain_import with stub adb on PATH, stub Endurain on 127.0.0.1:18999, temp inbox (1 file in processed/, 1 in inbox, 1 new, 1 failing pull)",
|
||||
"result": "pass",
|
||||
"evidence": "WARNING phone fallback: 4 on phone, 1 pulled, 1 skipped as already imported, 1 skipped as already in the inbox, 1 failed; adb pull called only for the new and the failing file; second run: 0 pulled, 3 skipped as already imported, 0 uploads"
|
||||
},
|
||||
{
|
||||
"command": "live run on the host (reported by the caller session, 17:56)",
|
||||
"result": "pass",
|
||||
"evidence": "27 on phone, 0 pulled, 27 skipped as already imported"
|
||||
},
|
||||
{
|
||||
"command": "pre-commit run --files sources.py __main__.py tests/test_phone_pull.py",
|
||||
"result": "pass",
|
||||
"evidence": "all hooks passed"
|
||||
}
|
||||
],
|
||||
"risks": [
|
||||
"A file deleted from processed/ by hand would be pulled again (then deduped by the ledger), which is the intended recovery path."
|
||||
],
|
||||
"rollback": [
|
||||
"git revert this commit",
|
||||
"After rollback: the next timer run re-pulls the phone history into the inbox; the ledger still prevents duplicate uploads"
|
||||
]
|
||||
}
|
||||
@ -1,48 +0,0 @@
|
||||
{
|
||||
"intent": "Turn the `Pre-commit checks` workflow green again on main, and register the new punchme app in the shared launcher-icon family.",
|
||||
"scope": [
|
||||
".github/workflows/pre-commit.yml",
|
||||
"python_pkg/app_icons/{apps,glyphs,_glyph_art_more}.py",
|
||||
"Non-goal: changing any gate's rules, or restyling the existing icon family"
|
||||
],
|
||||
"changes": [
|
||||
"pre-commit.yml now checks out kuhyx/utils into .utils/ and points UTILS_ROOT at it. The md-naming and file-length hooks are thin shims that exec the real gate out of that repo; a runner has no ~/src/utils, so both failed with 'shared gate not found at /home/runner/utils/...' and the workflow has been red since 2026-08-24. The dedicated md-naming.yml already did this checkout — this copies the same approach rather than inventing one.",
|
||||
"Registered app_icons entry 'punchme' (shared family accent #B8862E, glyph punch-card, linux=False since the app is Android-only).",
|
||||
"Added the _PUNCH_CARD glyph: a time card with a clipped corner, a column of four punched holes and three ruled lines. The first draft put two holes above two wide slots and read unmistakably as a face (eyes over teeth) at every size, which is why the holes are now a vertical column."
|
||||
],
|
||||
"verification": [
|
||||
{
|
||||
"command": "UTILS_ROOT=/home/kuhy/src/utils bash scripts/check_md_naming.sh README.md",
|
||||
"result": "pass",
|
||||
"evidence": "Exit 0. With UTILS_ROOT=/nonexistent the same shim fails with 'shared gate not found at /nonexistent/scripts/check_md_naming.sh', so the override is what the fix relies on and it is proven in both directions."
|
||||
},
|
||||
{
|
||||
"command": "python3 -m pytest python_pkg/app_icons/tests -q",
|
||||
"result": "pass",
|
||||
"evidence": "44 passed. test_registry.py iterates every entry in APPS, so the new punchme entry's accent, glyph reference and repo path are all asserted without a bespoke test."
|
||||
},
|
||||
{
|
||||
"command": "python3 -m pytest python_pkg/app_icons/tests --cov=python_pkg/app_icons",
|
||||
"result": "pass",
|
||||
"evidence": "100% on every app_icons module, including _glyph_art_more.py with the new glyph."
|
||||
},
|
||||
{
|
||||
"command": "pre-commit run --files python_pkg/app_icons/_glyph_art_more.py python_pkg/app_icons/apps.py python_pkg/app_icons/glyphs.py",
|
||||
"result": "pass",
|
||||
"evidence": "All hooks passed, including pylint, bandit and 'pytest with coverage enforcement'."
|
||||
},
|
||||
{
|
||||
"command": "PYTHONPATH=~/src/testsAndMisc python3 -m python_pkg.app_icons generate --app punchme --android",
|
||||
"result": "pass",
|
||||
"evidence": "Wrote 6 asset files and regenerated the Android mipmaps with no safe-box warning. Rendered at full size, circle-masked and 48dp and reviewed each; legible at all three."
|
||||
}
|
||||
],
|
||||
"risks": [
|
||||
"The utils checkout pins no ref, so a breaking change to the shared gate lands in this repo's CI without a commit here. That matches how md-naming.yml already behaves, so this change does not add a new class of risk.",
|
||||
"CI_GREEN_SKIP was set on the commit because the red baseline the gate objects to is the very thing this commit fixes."
|
||||
],
|
||||
"rollback": [
|
||||
"git revert the commit; the workflow returns to its previous (red) state and no other behaviour changes.",
|
||||
"After rollback, confirm `Pre-commit checks` is the only red workflow and that app_icons tests still pass, since the icon change is independent of the CI fix."
|
||||
]
|
||||
}
|
||||
@ -1,48 +0,0 @@
|
||||
{
|
||||
"intent": "Give com.kuhy.focus_owner (the Device Owner enforcer on the Pixel 6a) a real shared-family launcher icon instead of the off-family hand-written placeholder that was added only to unblock its build.",
|
||||
"scope": [
|
||||
"python_pkg/app_icons/apps.py - register focus_owner",
|
||||
"python_pkg/app_icons/glyphs.py - register the padlock-closed glyph",
|
||||
"python_pkg/app_icons/_glyph_art_more.py - the glyph's SVG fragment",
|
||||
"Non-goal: no new accent. The family accent #B8862E is shared; apps are distinguished by glyph shape only.",
|
||||
"Non-goal: no change to the 13 existing glyphs or to any other app's entry.",
|
||||
"The consuming repo (~/src/phone-focus-mode) is committed separately; it blocks binaries, so the icon lands there in vector form."
|
||||
],
|
||||
"changes": [
|
||||
"Added AppIcon(key='focus_owner', repo=~/src/phone-focus-mode/focus-owner, glyph='padlock-closed', accent='#B8862E', linux=False). The repo path points into the monorepo because focus-owner was absorbed as a subtree on 2026-08-24 and both the standalone clone and its GitHub repo are gone.",
|
||||
"Added the _PADLOCK_CLOSED glyph fragment: shackle stroked (inheriting the wrapping group's round caps), body filled in the accent, keyhole punched back out in the field colour via fill-rule='evenodd'. Splitting stroke and fill is deliberate - a fully stroked padlock loses its keyhole at 48dp, a fully filled one loses the shackle's opening.",
|
||||
"Geometry computed against style.py rather than eyeballed, per the lesson recorded on _NOTE_ASCENDER: body 292..732 x 520..792; shackle centreline r=132 about (512,520) giving an outer top of 352, inside the 232 safe edge, with legs at x=380/644 inside the body; keyhole leaves 60px of body above and 76px below the stem, both clear of MIN_NEGATIVE_SPACE=36."
|
||||
],
|
||||
"verification": [
|
||||
{
|
||||
"command": "pre-commit run --files python_pkg/app_icons/apps.py python_pkg/app_icons/glyphs.py python_pkg/app_icons/_glyph_art_more.py",
|
||||
"result": "pass",
|
||||
"evidence": "ruff, ruff format, mypy, pylint, bandit, codespell, secret scan and 'pytest with coverage enforcement' all Passed."
|
||||
},
|
||||
{
|
||||
"command": "python3 -m pytest python_pkg/app_icons/tests -q --cov=python_pkg.app_icons",
|
||||
"result": "pass",
|
||||
"evidence": "44 passed. Every app_icons module reports 100% branch coverage (apps.py, glyphs.py, _glyph_art_more.py, render.py, generate.py, cli.py, preview.py, style.py all 100%)."
|
||||
},
|
||||
{
|
||||
"command": "render.build_svg + render.rasterise at 512px and 48px into the session scratchpad",
|
||||
"result": "pass",
|
||||
"evidence": "Rendered and inspected both. Charcoal #1B1D21 field with the #B8862E padlock; centre_offset resolved to (0.0, -24.0). At 48px the keyhole is still a distinct hole rather than merging into the body - the specific failure the negative-space sums were computed to avoid."
|
||||
},
|
||||
{
|
||||
"command": "python3 -c \"from python_pkg.app_icons.glyphs import get_glyph; get_glyph('padlock-closed')\"",
|
||||
"result": "pass",
|
||||
"evidence": "Resolves to 'padlock-closed - Closed padlock with a punched-out keyhole'."
|
||||
}
|
||||
],
|
||||
"risks": [
|
||||
"The glyph is new artwork, so it is judged by eye; mitigated by rendering at both 512px and 48px and by computing the clearances instead of estimating them.",
|
||||
"apps.py now hardcodes a path into ~/phone-focus-mode. If that repo is ever relocated the entry goes stale - the same exposure every other entry in the registry already has.",
|
||||
"Android vector drawables support a subset of SVG. The arc and evenodd constructs used here convert cleanly, but the generated drawable is verified by an actual release build in the consuming repo rather than assumed."
|
||||
],
|
||||
"rollback": [
|
||||
"git revert this commit; nothing else imports _PADLOCK_CLOSED or the focus_owner key.",
|
||||
"After rollback, re-run python3 -m pytest python_pkg/app_icons/tests -q and confirm 44 tests still pass and coverage stays at 100%.",
|
||||
"The consuming repo's icon commit must be reverted too, or focus-owner is left referencing a glyph the registry no longer defines."
|
||||
]
|
||||
}
|
||||
@ -1,42 +0,0 @@
|
||||
{
|
||||
"intent": "Restore G29 force feedback in BeamNG.drive native Linux and give the wheel an installable/revertible new-lg4ff driver upgrade.",
|
||||
"scope": [
|
||||
"linux_configuration/fixes/install_new_lg4ff.sh (new)",
|
||||
"linux_configuration/docs/DOCS-g29-ffb.md (new), docs/DOCS-g29-shifter.md (cross-link)",
|
||||
"The SDL env var itself lives in steam-backlog-enforcer (_desktop_env.py, commit 0f5d234) and in the BeamNG Steam launch options; not in this repo"
|
||||
],
|
||||
"changes": [
|
||||
"install_new_lg4ff.sh: installs new-lg4ff-dkms-git from AUR, verifies dkms placed hid-logitech under updates/dkms/, regenerates initramfs presets that bundle hid-logitech, reloads the module when the wheel is present; --status and --revert (pacman -Rns + depmod + reload)",
|
||||
"DOCS-g29-ffb.md: the SDL3 hidapi lg4ff diagnosis (12,201 'Failed to get joystick3 FFB effect status' lines), narrow vs broad SDL hint, the missing %command% launch-option bug, new-lg4ff traps"
|
||||
],
|
||||
"verification": [
|
||||
{
|
||||
"command": "grep -c 'FFB effect status' ~/.local/share/BeamNG/BeamNG.drive/current/beamng.log (after relaunch with SDL_JOYSTICK_HIDAPI_LG4FF=0)",
|
||||
"result": "pass",
|
||||
"evidence": "0 (was 12201); haptic feature line 'Constant' (kernel path); user confirmed force + self-centring in a car"
|
||||
},
|
||||
{
|
||||
"command": "python3 /tmp/g29_ff_probe.py (evdev FF_CONSTANT +/-0x7fff, 2 s each)",
|
||||
"result": "pass",
|
||||
"evidence": "user: 'it moved all the way to the left and to the right' - kernel driver drives the motor"
|
||||
},
|
||||
{
|
||||
"command": "vmbox lg4ff: makepkg new-lg4ff-dkms-git; install_new_lg4ff.sh; --status; --revert",
|
||||
"result": "pass",
|
||||
"evidence": "install rc=0 (module file updates/dkms/hid-logitech.ko.zst), status rc=0, revert rc=0 (module file back to kernel/drivers/hid/, package removed). Passed in vmbox only; host run pending"
|
||||
},
|
||||
{
|
||||
"command": "shellcheck linux_configuration/fixes/install_new_lg4ff.sh; pre-commit run --files <changed>",
|
||||
"result": "pass",
|
||||
"evidence": "shellcheck clean; all hooks Passed"
|
||||
}
|
||||
],
|
||||
"risks": [
|
||||
"Host initramfs bundles hid-logitech (autodetect); the script regenerates all presets with mkinitcpio -P, which touches /boot",
|
||||
"modprobe -r hid_logitech while a game holds the evdev node drops that handle; run with games closed"
|
||||
],
|
||||
"rollback": [
|
||||
"install_new_lg4ff.sh --revert (pacman -Rns new-lg4ff-dkms-git, depmod, mkinitcpio -P, module reload)",
|
||||
"Validate: --status shows module file under kernel/drivers/hid/ and ff caps 300040000 0"
|
||||
]
|
||||
}
|
||||
@ -1,37 +0,0 @@
|
||||
{
|
||||
"intent": "Commit the G29 shifter HID-BPF fix that has been live on the host since 2026-09-11 but was never added to git, splitting its two over-cap files on the way.",
|
||||
"scope": [
|
||||
"linux_configuration/fixes/fix_g29_shifter.sh, g29_shifter.bpf.c, g29_shifter_capture.py, systemd/g29-shifter-bpf.service, systemd/99-g29-shifter-ensure.rules",
|
||||
"linux_configuration/tests/test_g29_shifter_capture.py (split), test_g29_shifter_capture_cli.py (new), conftest.py (G29Reports fixture)",
|
||||
"linux_configuration/docs/DOCS-g29-shifter.md",
|
||||
"No behaviour change to the installed fix; the host attachment was not touched"
|
||||
],
|
||||
"changes": [
|
||||
"Test file split at the record/CLI boundary (256 -> 164 + 107 lines); report builders moved into a conftest fixture class because the naming hook allows only test_*.py in tests/",
|
||||
"g29_shifter_capture.py module docstring tightened (255 -> 252 -> under cap after ruff format)",
|
||||
"DOCS-g29-shifter.md gains a cross-link to DOCS-g29-ffb.md"
|
||||
],
|
||||
"verification": [
|
||||
{
|
||||
"command": "python3 -m pytest -q linux_configuration/tests/test_g29_shifter_capture.py linux_configuration/tests/test_g29_shifter_capture_cli.py --cov=g29_shifter_capture --cov-branch",
|
||||
"result": "pass",
|
||||
"evidence": "29 passed; g29_shifter_capture.py 151 stmts, 52 branches, 100%"
|
||||
},
|
||||
{
|
||||
"command": "linux_configuration/fixes/fix_g29_shifter.sh --status",
|
||||
"result": "pass",
|
||||
"evidence": "persistent: /etc/udev-hid-bpf/g29_shifter.bpf.o + rule; attached now: yes; safety net: enabled (host, 2026-09-14 after the new-lg4ff module reload)"
|
||||
},
|
||||
{
|
||||
"command": "pre-commit run --files <all shifter files>",
|
||||
"result": "pass",
|
||||
"evidence": "all hooks Passed"
|
||||
}
|
||||
],
|
||||
"risks": [
|
||||
"None at runtime: the committed files are byte-identical in behaviour to what is installed"
|
||||
],
|
||||
"rollback": [
|
||||
"fix_g29_shifter.sh --remove detaches the program; git revert restores the untracked state"
|
||||
]
|
||||
}
|
||||
@ -1,35 +0,0 @@
|
||||
{
|
||||
"intent": "Per kuhy (2026-09-18: 'I do not want it to work like this, I want it to just work normally'): drop the desktop notification and the 5-minute retry loop added earlier today. PS4 mode (046D:C260) cannot be supported on this PC at all (no kernel driver, SDL lg4ff has no c260 either, so FFB is dead too) and the selector is physical; the switch is now on PS3, where the fix attaches by itself at enumeration.",
|
||||
"scope": [
|
||||
"linux_configuration/fixes/g29_wheel_mode.sh: g29_notify_desktop, G29_RUN_USER_DIR and G29_NOTIFY_ID removed; mode detection kept",
|
||||
"linux_configuration/fixes/fix_g29_shifter.sh: --ensure in PS4/compat mode still exits 1 naming the mode, no notification",
|
||||
"linux_configuration/fixes/systemd/g29-shifter-bpf.service: Restart/RestartSec/StartLimitIntervalSec removed",
|
||||
"linux_configuration/tests/test_fix_g29_shifter.bats: notify tests removed (11 remain)",
|
||||
"linux_configuration/docs/DOCS-g29-shifter.md"
|
||||
],
|
||||
"changes": [
|
||||
"The failure in PS4 mode stays visible in the journal and systemctl --failed with the mode named, which is what was missing on the 2026-09-15/17 boots; nothing nags.",
|
||||
"The test notification fired at 10:42 from root never appeared on kuhy's screen anyway; not debugged because the feature is gone."
|
||||
],
|
||||
"verification": [
|
||||
{
|
||||
"command": "bats linux_configuration/tests/test_fix_g29_shifter.bats",
|
||||
"result": "pass",
|
||||
"evidence": "1..11, all ok"
|
||||
},
|
||||
{
|
||||
"command": "linux_configuration/fixes/fix_g29_shifter.sh (install mode, on the host)",
|
||||
"result": "pass",
|
||||
"evidence": "systemctl cat g29-shifter-bpf.service has no Restart/StartLimit lines; --status: wheel mode: native, attached now: yes, safety net: enabled"
|
||||
},
|
||||
{
|
||||
"command": "pre-commit run --files <changed files>",
|
||||
"result": "pass",
|
||||
"evidence": "no Failed hooks"
|
||||
}
|
||||
],
|
||||
"risks": [
|
||||
"A future PS4-mode boot is silent again except for a failed unit in the journal / systemctl --failed / --status"
|
||||
],
|
||||
"rollback": ["git revert, re-run fix_g29_shifter.sh to reinstall the unit"]
|
||||
}
|
||||
@ -1,47 +0,0 @@
|
||||
{
|
||||
"intent": "The G29 shifter fix stops failing silently when the wheel is powered on with its selector in the PS4 position: the ensure unit names the mode, sends a critical desktop notification, and retries every 5 minutes until the selector is flipped; a wheel that is merely absent no longer fails the unit.",
|
||||
"scope": [
|
||||
"linux_configuration/fixes/g29_wheel_mode.sh (new, sourced): mode detection native/ps4/compat/absent, explanation strings, root-safe desktop notification",
|
||||
"linux_configuration/fixes/fix_g29_shifter.sh: find_device/--ensure/--status use the helper; udev reload moved after the ensure rule is installed",
|
||||
"linux_configuration/fixes/systemd/g29-shifter-bpf.service: Restart=on-failure, RestartSec=5min, StartLimitIntervalSec=0",
|
||||
"linux_configuration/fixes/systemd/99-g29-shifter-ensure.rules: also fires on 046D:C260",
|
||||
"linux_configuration/tests/test_fix_g29_shifter.bats (new)",
|
||||
"linux_configuration/docs/DOCS-g29-shifter.md",
|
||||
"non-goal: supporting PS4 mode itself (C260 has a different report layout and no kernel driver; only the physical selector leaves it)"
|
||||
],
|
||||
"changes": [
|
||||
"Root cause from the journal: boots on 2026-09-15 and 2026-09-17 enumerated the wheel as 046D:C260 (PS4 mode, hid-generic). Every layer keyed on C24F, so the unit exited 1 with 'no G29 found' once at boot and nothing was said; 1st/3rd stayed on the firmware decode until the selector was flipped a day later (C260 -> disconnect -> C294 -> C24F).",
|
||||
"g29_wheel_mode() classifies the HID bus; --ensure exits 0 when absent, exits 1 + notify-send -u critical (fixed id, replaces) when ps4/compat, attaches when native.",
|
||||
"Unit retries on failure every 5 min so a notification lost before the session bus exists at boot is re-sent; ensure udev rule matches C260 so a wheel powered on in PS4 mode is reported at hotplug too."
|
||||
],
|
||||
"verification": [
|
||||
{
|
||||
"command": "bats linux_configuration/tests/test_fix_g29_shifter.bats",
|
||||
"result": "pass",
|
||||
"evidence": "1..13, all ok (mode classification, notify via sudo -u, --ensure exit codes 0/1 with fake sysfs, --status mode line)"
|
||||
},
|
||||
{
|
||||
"command": "linux_configuration/fixes/fix_g29_shifter.sh (install mode, on the host)",
|
||||
"result": "pass",
|
||||
"evidence": "rebuilt + installed; systemctl cat shows Restart=on-failure/RestartSec=5min; rule contains C260; udevadm trigger re-ran the unit: 'already attached; nothing to do'; --status: wheel mode: native, attached now: yes"
|
||||
},
|
||||
{
|
||||
"command": "sudo bash -c 'source g29_wheel_mode.sh; g29_notify_desktop ...' (root context, real /run/user/1000/bus)",
|
||||
"result": "pass",
|
||||
"evidence": "sent as uid 0 via sudo -u kuhy; on-screen appearance is user-confirmed"
|
||||
},
|
||||
{
|
||||
"command": "pre-commit run --files <the six changed files>",
|
||||
"result": "pass",
|
||||
"evidence": "shellcheck clean after replacing printf-stub with quoted heredoc and '! grep' with 'run ! grep'; 250-line cap, shell-library-has-tests, codespell all pass"
|
||||
}
|
||||
],
|
||||
"risks": [
|
||||
"PS4-mode notification repeats every 5 minutes until the selector is flipped (same notification id, so it replaces rather than stacks) - intended, but noisy if PS4 mode is ever wanted on purpose",
|
||||
"vmbox deliberately skipped: the guest has no G29, so a sandbox pass proves nothing for a physical-input fix"
|
||||
],
|
||||
"rollback": [
|
||||
"git revert the commit, then re-run linux_configuration/fixes/fix_g29_shifter.sh to reinstall the previous unit + rule",
|
||||
"validate with fix_g29_shifter.sh --status and systemctl cat g29-shifter-bpf.service"
|
||||
]
|
||||
}
|
||||
@ -1,38 +0,0 @@
|
||||
{
|
||||
"intent": "Make migrate_helium_passwords_to_basic.py actually work on the user's box: the first version handed back twice never showed the keyring dialog, and the running-Helium guard false-positived on an idle machine.",
|
||||
"scope": [
|
||||
"linux_configuration/installers/migrate_helium_passwords_to_basic.py (unlock via secretstorage; anchored process check)",
|
||||
"linux_configuration/installers/verify_helium.py (anchored process check)",
|
||||
"linux_configuration/installers/install_helium.sh (+python-secretstorage dep)",
|
||||
".pre-commit-config.yaml pylint deps, meta/requirements.txt (+secretstorage 3.5.0)"
|
||||
],
|
||||
"changes": [
|
||||
"`secret-tool lookup` is non-interactive: on a locked keyring it returns nothing and shows no dialog. A busctl Unlock+Prompt sequence also cannot work because Secret Service prompt objects die with the connection that created them. secretstorage keeps one D-Bus connection: get_default_collection().unlock() pops the gcr dialog and blocks until answered.",
|
||||
"`pgrep -f /opt/helium-browser-bin` matched the invoking shell whose argv mentioned the path, so the guard reported Helium running on an idle machine; the pattern is now anchored to `^/opt/helium-browser-bin/`."
|
||||
],
|
||||
"verification": [
|
||||
{
|
||||
"command": "linux_configuration/installers/migrate_helium_passwords_to_basic.py (run by Claude, user answered the gcr dialog)",
|
||||
"result": "pass",
|
||||
"evidence": "keyring is locked — answer the unlock dialog on screen; backup: Default/Login Data.pre-basic-20260920-182556; converted 5 v11 -> v10 rows; 0 already v10; added --password-store=basic to ~/.config/helium-browser-flags.conf"
|
||||
},
|
||||
{
|
||||
"command": "keyring re-locked via Secret.Service.Lock; /usr/local/bin/helium-browser (wrapper) on the real profile under Xvfb; chrome://password-manager/passwords deep-text; chrome://version command line; /json/list",
|
||||
"result": "pass",
|
||||
"evidence": "all 5 sites listed (kuhy-cloud.duckdns.org, google.com, typeracer.com, leetcode.com, infakt.pl); command line has --password-store=basic and the LeechBlock --load-extension; uBO background_page present; keyring still locked afterwards; Login Data still 5 rows, all v10 (nothing deleted on read)"
|
||||
},
|
||||
{
|
||||
"command": "pre-commit run --files <changed files> (in the commit worktree)",
|
||||
"result": "pass",
|
||||
"evidence": "ruff, ruff-format, pylint, shellcheck, codespell, file-length, dependency-freshness green"
|
||||
}
|
||||
],
|
||||
"risks": [
|
||||
"secretstorage's unlock() blocks with no timeout until the dialog is answered or dismissed; the script says so on stdout before it blocks.",
|
||||
"The backup file Login Data.pre-basic-<ts> still holds the v11 (keyring-encrypted) blobs; it is useless without the keyring key and can be deleted once the user is happy."
|
||||
],
|
||||
"rollback": [
|
||||
"Restore Default/'Login Data.pre-basic-20260920-182556' over 'Login Data' and remove --password-store=basic from ~/.config/helium-browser-flags.conf.",
|
||||
"Validate: Helium prompts for the keyring again and the 5 logins decrypt after unlock."
|
||||
]
|
||||
}
|
||||
@ -1,53 +0,0 @@
|
||||
{
|
||||
"intent": "Get full uBlock Origin (Manifest V2, not Lite) back on a FOSS Chromium-based browser after Chromium 151 dropped MV2: install Helium, clone the Chromium profile into it wholesale, keep the wrapper/policy/LeechBlock setup intact, and stop the gnome-keyring unlock prompt that autologin causes.",
|
||||
"scope": [
|
||||
"linux_configuration/installers/{install_helium.sh,verify_helium.py,migrate_helium_passwords_to_basic.py} (new)",
|
||||
"linux_configuration/fresh-install/aur_packages.txt, periodic_background/lib/{periodic_browser,services_browser}.sh (register helium-browser)",
|
||||
".pre-commit-config.yaml (pylint deps websockets+cryptography; codespell ignores the vendor name `imput`)",
|
||||
"meta/requirements.txt (+cryptography 50.0.1; anthropic 1.6.0 -> 1.7.0 for the freshness gate), meta/requirements-test.txt (anthropic)",
|
||||
"Non-goals: default-browser switch (current default is librewolf), removing ungoogled-chromium, refreshing the stale host copy of browser-preexec-wrapper"
|
||||
],
|
||||
"changes": [
|
||||
"install_helium.sh: pacman deps, AUR install via yay or makepkg (+validpgpkeys import), /usr/local/bin/helium-browser -> browser-preexec-wrapper, --clone-profile with downgrade check (Helium's Chromium base parsed from `--version`) and running-browser refusal, then runs the verifier.",
|
||||
"verify_helium.py: Xvfb + DevTools (port published via DevToolsActivePort, websockets sync client): asserts uBO background_page + manifest_version 2, every /etc/chromium/policies key on chrome://policy (shadow-DOM text walk), adblock-tester.com >= 95. d3ward was the first choice and is archived/dead.",
|
||||
"migrate_helium_passwords_to_basic.py: re-encrypts v11 (keyring) `logins` rows as v10 (basic) with the Chromium Linux os_crypt scheme; round-trip check per row, backup first, refuses while Helium runs, appends --password-store=basic to the flags file only after success.",
|
||||
"Profile clone on the host: rsync ~/.config/chromium -> ~/.config/net.imput.helium minus Singleton*/DevToolsActivePort/Crash Reports/BrowserMetrics*."
|
||||
],
|
||||
"verification": [
|
||||
{
|
||||
"command": "linux_configuration/installers/verify_helium.py",
|
||||
"result": "pass",
|
||||
"evidence": "[PASS] uBlock Origin is MV2 (full, not Lite): name=uBlock Origin version=1.75.0 manifest_version=2 background_page=True; [PASS] 7 policies from /etc/chromium/policies/managed applied; [PASS] adblock-tester score >= 95/100: score=96"
|
||||
},
|
||||
{
|
||||
"command": "vm new helium; vm scp ...; vm run helium './install_helium.sh --clone-profile'",
|
||||
"result": "pass",
|
||||
"evidence": "pristine guest: deps installed, helium-browser-bin built via makepkg after gpg --recv-keys, clone + downgrade check ok, verifier 3/3 PASS (0 policies in the guest), score 96"
|
||||
},
|
||||
{
|
||||
"command": "linux_configuration/installers/install_helium.sh (host)",
|
||||
"result": "pass",
|
||||
"evidence": "package already installed; linked /usr/local/bin/helium-browser -> browser-preexec-wrapper; verifier 3/3 PASS"
|
||||
},
|
||||
{
|
||||
"command": "python: decrypt + encrypt round-trip of 5 real v10 cookie blobs with derive_key(b'peanuts')",
|
||||
"result": "pass",
|
||||
"evidence": "5/5 blobs unpad cleanly and round-trip; a wrong key fails PKCS#7 unpadding, so this validates the os_crypt scheme without touching the keyring"
|
||||
},
|
||||
{
|
||||
"command": "pre-commit run --files <all staged files>",
|
||||
"result": "pass",
|
||||
"evidence": "ruff, ruff-format, pylint (import-error gone once websockets+cryptography were added to the hook), shellcheck, shfmt, codespell, file-length, dependency-freshness all pass; pytest-coverage fails only on python_pkg/android_ui/cli.py 96% and python_pkg/phone_lease 0-65%, which are another session's uncommitted files (not staged here)"
|
||||
}
|
||||
],
|
||||
"risks": [
|
||||
"The 5 keyring-encrypted passwords are unusable in Helium until the user runs migrate_helium_passwords_to_basic.py (it needs the keyring unlocked once); until then the keyring prompt persists.",
|
||||
"Basic store = fixed key at rest, i.e. anyone with file access reads the passwords; on an autologin box that is already the practical state and matches the existing 36 v10 cookies.",
|
||||
"adblock-tester.com is a third-party page; if it changes its score text the gate fails closed (score=-1)."
|
||||
],
|
||||
"rollback": [
|
||||
"yay -Rns helium-browser-bin; sudo rm /usr/local/bin/helium-browser; rm -rf ~/.config/net.imput.helium ~/.config/helium-browser-flags.conf — ~/.config/chromium was never modified.",
|
||||
"Passwords: restore Default/'Login Data.pre-basic-<ts>' over 'Login Data' in the Helium profile.",
|
||||
"Validate: chromium starts on its own profile; verify_helium.py exits 2 (binary absent)."
|
||||
]
|
||||
}
|
||||
@ -1,47 +0,0 @@
|
||||
{
|
||||
"intent": "Run Home Assistant on the PC so the Viomi V3 robot vacuum (viomi.vacuum.v13) can be controlled over the LAN from a FOSS UI, including the Home Assistant Android app, with tcp/8123 reachable from the LAN and WireGuard only.",
|
||||
"scope": [
|
||||
"linux_configuration/features/setup_homeassistant.sh (new)",
|
||||
"linux_configuration/features/setup_wireguard_ssh.sh, lib/wg_sshd.sh, lib/wg_firewall.sh: persisted ALLOW_HOMEASSISTANT flag + allow-homeassistant subcommand",
|
||||
"Non-goal: exposing Home Assistant to the internet; rooting the robot"
|
||||
],
|
||||
"changes": [
|
||||
"setup_homeassistant.sh writes ~/services/homeassistant/docker-compose.yml (host networking, image pinned by digest to 2026.9.4), installs al-one/hass-xiaomi-miot v1.1.5 into config/custom_components, starts the container and waits for the API; `status` self-diagnoses container, API, component version and firewall rule",
|
||||
"render_nftables_ruleset emits `ip saddr <LAN> tcp dport 8123 accept` and `iifname wg0 tcp dport 8123 accept` when ALLOW_HOMEASSISTANT=true; allow_homeassistant() persists the flag and applies it like allow_dns"
|
||||
],
|
||||
"verification": [
|
||||
{
|
||||
"command": "vm new hafw; sudo bash ~/lc/features/setup_wireguard_ssh.sh allow-homeassistant; nft list chain inet filter input; setup_wireguard_ssh.sh verify",
|
||||
"result": "pass",
|
||||
"evidence": "vmbox guest: both 8123 rules present in live input chain, ALLOW_HOMEASSISTANT=\"true\" persisted, verify: /etc/nftables.conf matches generator, forward chain lets Docker through"
|
||||
},
|
||||
{
|
||||
"command": "render_nftables_ruleset into tmp; diff vs /etc/nftables.conf; nft -c -f; then host apply + sudo setup_wireguard_ssh.sh verify",
|
||||
"result": "pass",
|
||||
"evidence": "host diff was exactly the two 8123 lines; nft -c OK; verify: file matches generator and live chain. Applied via nft add rule (no flush) to avoid restarting dockerd under running containers"
|
||||
},
|
||||
{
|
||||
"command": "bash linux_configuration/features/setup_homeassistant.sh setup",
|
||||
"result": "pass",
|
||||
"evidence": "Installed xiaomi_miot v1.1.5; container started; HA answers on http://127.0.0.1:8123 (HTTP 401)"
|
||||
},
|
||||
{
|
||||
"command": "HA REST: POST /api/services/vacuum/start then vacuum/return_to_base on vacuum.viomi_v13_9f32_robot_cleaner",
|
||||
"result": "pass",
|
||||
"evidence": "state cleaning (miio status 5 Sweeping) then returning -> docked"
|
||||
},
|
||||
{
|
||||
"command": "Pixel 6a, HA companion minimal 2026.6.5: login to http://192.168.1.43:8123, tap play then dock on the robot card",
|
||||
"result": "pass",
|
||||
"evidence": "state cleaning after play; returning -> docked 56 s after dock tap"
|
||||
}
|
||||
],
|
||||
"risks": [
|
||||
"hass-xiaomi-miot issue #2677 (Developer-tools send_command broken since v1.1.0) is still open; vacuum start/dock verified working through entity services",
|
||||
"Host networking: HA listens on 0.0.0.0:8123; only the nftables input rules keep it LAN/wg0-only"
|
||||
],
|
||||
"rollback": [
|
||||
"docker compose -f ~/services/homeassistant/docker-compose.yml down; set ALLOW_HOMEASSISTANT=\"false\" in features/.wireguard_ssh.conf and re-run sudo setup_wireguard_ssh.sh allow-web (regenerates the ruleset)",
|
||||
"Validate: sudo setup_wireguard_ssh.sh verify passes and no dport 8123 rule remains"
|
||||
]
|
||||
}
|
||||
@ -1,44 +0,0 @@
|
||||
{
|
||||
"intent": "Show today's gaming budget in the i3 bar: time left, used/total, and which bonuses (workout, LeetCode, reading) can still be earned.",
|
||||
"scope": [
|
||||
"linux_configuration/i3blocks/gaming_budget.sh (new block), linux_configuration/i3blocks/config",
|
||||
"linux_configuration/tests/test_i3blocks_gaming_budget.sh, .github/workflows/shell-tests.yml",
|
||||
"Non-goal: no changes to steam-backlog-enforcer, screen-locker, leetcode-guard or book-guard; the block only reads /api/budget"
|
||||
],
|
||||
"changes": [
|
||||
"New interval=30 block reads steam-backlog-enforcer GET /api/budget with one curl + one jq pass; colours follow the enforcer warn_at (1h yellow, 30m orange, 10m/blocked red)",
|
||||
"Unearned-bonus sizes come from ~/.config/steam_backlog_enforcer/config.json via jq --slurpfile (never on argv, the file holds secrets), falling back to the enforcer defaults 2h/1h/1h",
|
||||
"Never blank: dead server shows cached text marked (stale) in orange or 'budget server down' in red; bad state_status, ok:false and unparseable bodies are red and do not overwrite the cache",
|
||||
"Behaviour test with a PATH-stubbed curl, wired into the shell-tests CI list"
|
||||
],
|
||||
"verification": [
|
||||
{
|
||||
"command": "~/.config/i3blocks/gaming_budget.sh",
|
||||
"result": "pass",
|
||||
"evidence": "Live API: '🎮 2h00 left · 6h00/8h00 +📖1h' / '🎮 2h00' / #50FA7B, matching /api/budget (21600s used of 28800, reading bonus 0)"
|
||||
},
|
||||
{
|
||||
"command": "i3-msg restart (run by the user)",
|
||||
"result": "pass",
|
||||
"evidence": "User confirmed the block renders in the bar as intended"
|
||||
},
|
||||
{
|
||||
"command": "bash linux_configuration/tests/test_i3blocks_gaming_budget.sh",
|
||||
"result": "pass",
|
||||
"evidence": "All gaming_budget checks passed (thresholds, blocked, overspent, config bonus sizes, stale, down, error states)"
|
||||
},
|
||||
{
|
||||
"command": "bash linux_configuration/tests/test_i3blocks_efficiency.sh",
|
||||
"result": "pass",
|
||||
"evidence": "All i3blocks efficiency regression tests passed"
|
||||
}
|
||||
],
|
||||
"risks": [
|
||||
"Depends on the /api/budget JSON shape (today.*, rules.bonuses.*); a renamed field renders 'unreadable budget' in red rather than a wrong number",
|
||||
"Bonus fallback sizes duplicate the enforcer defaults; they only matter when config.json leaves the fields unset"
|
||||
],
|
||||
"rollback": [
|
||||
"git revert the commit and remove ~/.config/i3blocks/gaming_budget.sh symlink",
|
||||
"i3-msg restart and confirm the bar renders without the block"
|
||||
]
|
||||
}
|
||||
@ -1,40 +0,0 @@
|
||||
{
|
||||
"intent": "Put the screen-locker status back on the i3 bar, so a miscounting or disarmed locker is visible without being asked. The GTK tray icon it replaces had been commented out of ~/.config/i3/config since 2026-08-21, and in that window the locker counted one workout twice, believed the week met at 5/5 when the real total was 3, and skipped enforcement for two ISO weeks with nothing on the machine saying so.",
|
||||
"scope": [
|
||||
"linux_configuration/i3blocks/screen_locker.sh (new block script)",
|
||||
"linux_configuration/i3blocks/config (register the block before shutdown_countdown)",
|
||||
"Non-goal: the block never mutates locker state; clicking only opens the read-only UI",
|
||||
"Depends on screen-locker-web.service (127.0.0.1:8770) from the screen-locker repo"
|
||||
],
|
||||
"changes": [
|
||||
"New block renders '<count>/<minimum> · <gaming budget>' from GET /api/status, coloured by compliance_state (green ok / yellow warn / red lock).",
|
||||
"Arming from GET /api/health overrides the compliance colour: a timer that is disabled or has no next trigger shows 'DISARMED' in red, because a locker that cannot fire is the worse failure and the one that hides.",
|
||||
"A dead status server renders the last known value marked '(stale)' in orange, or 'status server down' in red when there is no cache — never a blank block.",
|
||||
"Missing curl/jq degrades to a visible marker rather than an empty block or a bash error."
|
||||
],
|
||||
"verification": [
|
||||
{
|
||||
"command": "~/.config/i3blocks/screen_locker.sh",
|
||||
"result": "pass",
|
||||
"evidence": "Prints '💪 3/5 · 6h' / '💪 3/5' / '#F1FA8C' — 3/5 is the corrected weekly count, 6h the cut gaming budget for a day with no workout."
|
||||
},
|
||||
{
|
||||
"command": "systemctl --user stop screen-locker-web.service && ~/.config/i3blocks/screen_locker.sh",
|
||||
"result": "pass",
|
||||
"evidence": "With cache: '💪 3/5 · 6h (stale)' in orange. After rm of the cache: '💪 status server down' in red. Restored to '💪 3/5 · 6h' once the unit was started again."
|
||||
},
|
||||
{
|
||||
"command": "i3-msg reload && scrot",
|
||||
"result": "pass",
|
||||
"evidence": "Block visible in the HDMI-0 bar between PC:warn and the 23:00 shutdown countdown, rendering '💪 3/5 · 6h' in yellow. i3blocks does not re-read its config on i3-msg reload alone; the running process had to exit first."
|
||||
}
|
||||
],
|
||||
"risks": [
|
||||
"Two curl calls every 30s. Both are loopback with --max-time 2, so a hung server delays the block by at most 4s rather than wedging the bar.",
|
||||
"The block reports the gaming budget using its own 8h/6h constants. If steam-backlog-enforcer's configured values change, this display drifts from the enforcer until updated."
|
||||
],
|
||||
"rollback": [
|
||||
"Remove the [screen_locker] stanza from linux_configuration/i3blocks/config, delete linux_configuration/i3blocks/screen_locker.sh and the ~/.config/i3blocks symlink, then i3-msg reload and kill the running i3blocks so it re-reads the config.",
|
||||
"After rollback, confirm the bar still renders the remaining blocks (cpu, gpu, memory, disk, volume, ethernet, claude_usage, pc_startup, shutdown_countdown, time)."
|
||||
]
|
||||
}
|
||||
@ -1,67 +0,0 @@
|
||||
{
|
||||
"intent": "Make linux_configuration/install_core_system.sh -- the documented way to set this machine up from scratch -- actually complete on a fresh machine, and make the hosts blocking it installs genuinely enforced rather than only claiming to be.",
|
||||
"scope": [
|
||||
"linux_configuration/install_core_system.sh: two modules pointed at python_pkg/ paths that stopped existing when screen_locker and steam_backlog_enforcer were extracted; nothing installed guard-lib, so the CORE midnight-shutdown module always died with 'guardctl not found on PATH'.",
|
||||
"linux_configuration/lib/extracted_repos.sh: gained clone-if-missing so a bare machine can complete the documented single command.",
|
||||
"linux_configuration/tests/lib/security_apps.sh: five screen_locker checks silently skipped against an extracted path.",
|
||||
"Sibling repos changed in the same pass: hosts-blocker (guard registration), screen-locker (missing runtime deps), steam-backlog-enforcer (non-interactive install).",
|
||||
"Non-goals: boot-repair (its BIOS/GRUB guard landed in aec2b275), the run_installer/ask_install machinery, and retiring the legacy hosts-guard layer (that stays in the monorepo one-shot with --rollback/--status)."
|
||||
],
|
||||
"changes": [
|
||||
"extracted_repo_clone/require_extracted_repo now clone a missing sibling repo from github.com/kuhyx/<name> instead of only printing instructions; the clone drops privileges to the invoking human via sudo -u so a sudo-run installer cannot create a root-owned checkout in the user's home. A clone that fails is still a loud, fatal error for the module.",
|
||||
"install_core_system.sh routes every module through install_module_from_repo/install_root_module, so all seven modules plus a new CORE guard-lib module resolve through the same clone-if-missing path. guard-lib is addressed as the 'utils' repo (it is a subdirectory of github.com/kuhyx/utils, so asking for 'guard-lib' would resolve to ~/guard-lib and derive a clone URL that does not exist) and is installed FIRST, before the modules that need guardctl.",
|
||||
"hosts-blocker: new lib/hosts_guard_setup.sh registers the hosts, nsswitch and resolved file-guard instances and installs their plugins; install.sh calls it AFTER write_hosts_file so the canonical snapshot is of the post-write file. This is the fresh-install half of a migration that previously only ran when a one-shot script under fixes/ was invoked by hand.",
|
||||
"hosts-blocker: stop_hosts_guard/restart_hosts_guard now handle the guard-lib instances (guard-file@hosts.*, guard-bind-mount@hosts.service) as well as the legacy per-file units, and refresh the canonical copy with 'guardctl file-guard sync hosts' -- without this a SECOND install run fought a live read-only bind mount and the install was not idempotent.",
|
||||
"screen-locker: install_systemd.sh installs its own runtime dependencies (tk for the tkinter shared library, plus the pinned gatelock and crdt-sync git dependencies). It previously installed none, so the post-install workout check crashed and the installer exited 1 on any fresh machine.",
|
||||
"steam-backlog-enforcer: install.sh no longer aborts under `set -euo pipefail` when a bare `read` finds no stdin; it defaults to not installing the systemd service when stdin is not a TTY, with STEAM_ENFORCER_INSTALL_SERVICE=y to opt in.",
|
||||
"tests/lib/security_apps.sh: the screen_locker section resolves the extracted repo via extracted_repo_dir instead of a hardcoded $HOME/src/testsAndMisc/python_pkg path, and its assertions were rewritten against the current implementation (SPORT_CHOICES, SPORT_TABLE_TENNIS, _validate_table_tennis, sets_won, MANUAL_WORKOUT_REFLECTION_MIN_CHARS) because the four symbols it used to grep for no longer exist anywhere in the codebase."
|
||||
],
|
||||
"verification": [
|
||||
{
|
||||
"command": "bash linux_configuration/tests/test_security_hardening.sh (host, before)",
|
||||
"result": "18 passed / 1 failed / 5 skipped",
|
||||
"evidence": "All 5 skips were the screen_locker section reporting 'Not found at expected location' -- checks that had silently stopped running when the package was extracted. The 1 failure is a genuine pre-existing host finding: signal-desktop and discord are installed but not wrapped with block-compulsive-opening."
|
||||
},
|
||||
{
|
||||
"command": "bash linux_configuration/tests/test_security_hardening.sh (host, after the path+assertion fix)",
|
||||
"result": "23 passed / 0 skipped",
|
||||
"evidence": "The five screen_locker checks now execute against ~/src/screen-locker and pass. Note this is a changed measuring instrument as well as a changed measurement: the suite is 24 checks today, so the task file's '18 passed / 6 skipped out of 17' target was stale and could not be matched literally."
|
||||
},
|
||||
{
|
||||
"command": "vm run <guest> 'bash guest_verify.sh' -- pristine vmbox guest, installer clones its own siblings",
|
||||
"result": "guard registration verified",
|
||||
"evidence": "guardctl file-guard status hosts reports target /etc/hosts, canonical /etc/guard-lib/canonical/hosts, bind_mount yes, 'path unit: active'; findmnt shows a read-only bind mount; lsattr shows ----i-----------------; /etc/guard-lib/targets contains hosts.conf, nsswitch.conf, resolved.conf, shutdown-schedule.conf. Before this change a fresh guest had chattr +i and no watcher, no canonical copy and no bind mount."
|
||||
},
|
||||
{
|
||||
"command": "bash linux_configuration/tests/test_security_hardening.sh (inside the pristine guest, after install)",
|
||||
"result": "23 passed / 0 failed / 1 skipped",
|
||||
"evidence": "Bare guest before any install measured 0 passed / 17 skipped. The single remaining skip is environment-gated by design -- 'Canonical hosts copy exists: not a configured host (CI/container)' -- and cannot pass inside a VM."
|
||||
},
|
||||
{
|
||||
"command": "echo y | bash linux_configuration/install_core_system.sh --all (pristine guest, first run then immediate re-run)",
|
||||
"result": "progressively fixed across runs; see evidence",
|
||||
"evidence": "Run A: Failed(2) -- screen-locker (tk missing) and steam-backlog-enforcer (read with no stdin). Run B after the steam fix: Installed(7)/Failed(1) on ModuleNotFoundError gatelock. Run C after adding gatelock+crdt-sync: install_systemd.sh exits 0 in the guest. guard-lib, hosts blocking and the midnight shutdown timer install from a bare guest throughout, which is the module that previously always died on 'guardctl not found on PATH'."
|
||||
},
|
||||
{
|
||||
"command": "shellcheck -x on every changed shell file",
|
||||
"result": "pass",
|
||||
"evidence": "install_core_system.sh, lib/extracted_repos.sh, tests/lib/security_apps.sh, fixes/migrate_hosts_guard_to_guard_lib.sh, and hosts-blocker's install.sh, lib/hosts_guard_setup.sh, lib/hosts_guard.sh all clean. An unused REPO_ROOT left behind by the module rewrite was deleted rather than suppressed."
|
||||
},
|
||||
{
|
||||
"command": "python3 -m screen_locker.screen_lock --status (host, before the host install run)",
|
||||
"result": "lock would be skipped",
|
||||
"evidence": "Today is recorded as sick_day, and _compliance_state.py's 'sick_day' predicate is terminal with reason 'Sick day logged today - lock skipped.', so install_systemd.sh's post-install --production invocation cannot raise a lock window on the host today."
|
||||
}
|
||||
],
|
||||
"risks": [
|
||||
"The installer now reaches the network by default. A machine with no connectivity and a missing sibling repo gets a loud module failure rather than a silent skip, which is the intended trade but is a behaviour change.",
|
||||
"Guard registration is a root operation that bind-mounts and chattr +i's /etc/hosts. It is ordered after write_hosts_file so the canonical snapshot matches the written file; getting that order wrong would make the guard fight the install.",
|
||||
"The escape hatch is not symmetric: migrate_hosts_guard_to_guard_lib.sh --rollback requires /var/lib/guard-lib-migration, which does not exist on this host because the one-shot was never run here. Rollback is therefore verifiable in the sandbox only.",
|
||||
"The screen_locker hardening assertions were rewritten, so they now guard different symbols than before. They assert the behaviour that exists today rather than the pre-extraction API."
|
||||
],
|
||||
"rollback": [
|
||||
"git revert the commit in each affected repo independently; the four commits are separable and the testsAndMisc one is the only one that changes the installer's control flow.",
|
||||
"In the sandbox, sudo bash linux_configuration/fixes/migrate_hosts_guard_to_guard_lib.sh --rollback uninstalls the guard-lib instances (keeping the canonical copy) and restores the legacy hooks and units.",
|
||||
"After reverting, re-run bash linux_configuration/tests/test_security_hardening.sh and confirm the count returns to its previous value, and check guardctl file-guard status hosts to see whether the instances are still registered -- reverting the code does not by itself unregister them."
|
||||
]
|
||||
}
|
||||
@ -1,50 +0,0 @@
|
||||
{
|
||||
"intent": "Keep enough systemd journal on the Arch desktop to diagnose a failure two weeks later: raise the installed cap from 300M to 4G and only vacuum a journal that is actually past it.",
|
||||
"scope": [
|
||||
"linux_configuration/fixes/lib/arch_hardware.sh (tweak_journal)",
|
||||
"linux_configuration/fixes/lib/arch_perf_fixes.sh (fix_journal)",
|
||||
"linux_configuration/fixes/lib/arch_perf_report.sh (check_journal_size, apply_safe_fixes)",
|
||||
"the three test suites that pin those values",
|
||||
"Non-goal: ubuntu_perf_more.sh keeps 300M (not this machine)"
|
||||
],
|
||||
"changes": [
|
||||
"SystemMaxUse=300M -> 4G in the drop-in each script writes and in the already-configured check.",
|
||||
"Vacuum threshold: usage must be reported in G with an integer part >= 4, instead of any value in G; vacuum target 4G.",
|
||||
"Report: 'journal is large' only past 4G, so a 2G journal is deliberate retention, not a finding."
|
||||
],
|
||||
"verification": [
|
||||
{
|
||||
"command": "bash linux_configuration/fixes/lib/tests/test_arch_hardware_boot.sh",
|
||||
"result": "pass",
|
||||
"evidence": "arch_hardware (boot/journal): 26 passed, 0 failed"
|
||||
},
|
||||
{
|
||||
"command": "bash linux_configuration/fixes/lib/tests/test_arch_perf_fixes.sh",
|
||||
"result": "pass",
|
||||
"evidence": "arch_perf_fixes: 21 passed, 0 failed"
|
||||
},
|
||||
{
|
||||
"command": "bash linux_configuration/fixes/lib/tests/test_arch_perf_report_fixes.sh; bash linux_configuration/fixes/lib/tests/test_arch_perf_report.sh",
|
||||
"result": "pass",
|
||||
"evidence": "arch_perf_report (fixes/summary): 21 passed; arch_perf_report (probes): 17 passed"
|
||||
},
|
||||
{
|
||||
"command": "shellcheck lib/arch_hardware.sh lib/arch_perf_fixes.sh lib/arch_perf_report.sh",
|
||||
"result": "pass",
|
||||
"evidence": "no findings"
|
||||
},
|
||||
{
|
||||
"command": "printf '[Journal]\\nSystemMaxUse=4G\\n' > /etc/systemd/journald.conf.d/size-limit.conf && systemctl restart systemd-journald (host)",
|
||||
"result": "pass",
|
||||
"evidence": "journalctl --disk-usage: 305.1M in the file system; cap now 4G"
|
||||
}
|
||||
],
|
||||
"risks": [
|
||||
"Up to 4G of /var/log/journal on a 3.6T disk with 1.8T free -- negligible.",
|
||||
"Three copies of the same logic still exist; they were changed together but remain duplicates."
|
||||
],
|
||||
"rollback": [
|
||||
"git revert the commit and re-run the fix script, or write SystemMaxUse=300M into the drop-in and restart systemd-journald.",
|
||||
"Validate with journalctl --disk-usage and cat /etc/systemd/journald.conf.d/size-limit.conf."
|
||||
]
|
||||
}
|
||||
@ -1,48 +0,0 @@
|
||||
{
|
||||
"intent": "Rename this repo's markdown onto the four shared namespaces (README.md / CLAUDE*.md / DOCS*.md / TODO*.md) and gate them, so a future session can tell a live task from a finished record without reading the file to find out.",
|
||||
"scope": [
|
||||
"40 markdown files renamed via git mv (history preserved)",
|
||||
"scripts/check_md_naming.sh: thin shim delegating to kuhyx/utils",
|
||||
".pre-commit-config.yaml: adds the md-naming hook",
|
||||
".github/workflows/md-naming.yml: same gate on push/PR",
|
||||
"Non-goal: executing the outstanding work these TODO files describe",
|
||||
"Non-goal: any change to source code, tests or build config"
|
||||
],
|
||||
"changes": [
|
||||
"Task files became TODO-<slug>.md and gained the literal line 'REMOVE ME AFTER FINISH'; record files became DOCS-<slug>.md with no marker.",
|
||||
"Classification was by file CONTENT, not filename: RESTRUCTURE_HANDOFF.md and INSTALLER_FIX_TASK.md are settled records (DOCS), while docs/superpowers/plans/2026-05-01-phone-focus-recovery.md is still a task (TODO).",
|
||||
"Deleted refactor_claude_todo.md and refactor_claude_todo_resume.md: the 250-line gate exits 0 in this repo, so the work they describe is provably finished.",
|
||||
"Inbound references to the renamed files were rewritten in the same commit."
|
||||
],
|
||||
"verification": [
|
||||
{
|
||||
"command": "bash scripts/check_md_naming.sh --all",
|
||||
"result": "pass",
|
||||
"evidence": "exit 0; 0 problems across the whole tree"
|
||||
},
|
||||
{
|
||||
"command": "pre-commit run md-naming --all-files",
|
||||
"result": "pass",
|
||||
"evidence": "markdown naming convention.....Passed"
|
||||
},
|
||||
{
|
||||
"command": "bash scripts/check_file_length.sh --all",
|
||||
"result": "pass",
|
||||
"evidence": "exit 0 -- the basis for deleting both refactor todos"
|
||||
},
|
||||
{
|
||||
"command": "python3 -m pytest md_naming/tests/ (in ~/src/utils)",
|
||||
"result": "pass",
|
||||
"evidence": "71 passed; 100% branch coverage on the gate"
|
||||
}
|
||||
],
|
||||
"risks": [
|
||||
"A rename breaks an inbound link not caught by the reference sweep. Mitigated: the sweep covered ~/.claude/CLAUDE.md, rules/, skills/, memory/ and every tracked .md; a bare-basename match was deliberately narrowed after a dry run showed it would corrupt vendored node_modules files.",
|
||||
"A doc classified TASK is actually finished, or vice versa. Mitigated: verdicts came from content audits cross-checked against repo code, and are recorded in ~/src/utils/md_naming/migrate/classify.py.",
|
||||
"The gate depends on kuhyx/utils being present; CI checks it out, and the shim fails loudly with install instructions if it is missing."
|
||||
],
|
||||
"rollback": [
|
||||
"git revert the commit -- every rename is a git mv, so history follows and the revert restores the original names.",
|
||||
"After rollback, confirm bash scripts/check_md_naming.sh --all is no longer wired by removing the md-naming hook from .pre-commit-config.yaml and deleting .github/workflows/md-naming.yml."
|
||||
]
|
||||
}
|
||||
@ -1,28 +0,0 @@
|
||||
{
|
||||
"intent": "Stop install_new_lg4ff.sh from regenerating the initramfs when the dkms pacman hook has already done it.",
|
||||
"scope": ["linux_configuration/fixes/install_new_lg4ff.sh regen_initramfs()"],
|
||||
"changes": [
|
||||
"Regenerate only when an image carries hid-logitech from the location modprobe no longer resolves to; images without the module, or already matching, are left alone"
|
||||
],
|
||||
"verification": [
|
||||
{
|
||||
"command": "linux_configuration/fixes/install_new_lg4ff.sh --status",
|
||||
"result": "pass",
|
||||
"evidence": "rc=0 on the host with new-lg4ff installed (module file updates/dkms/hid-logitech.ko.zst, loaded hid-logitech-new)"
|
||||
},
|
||||
{
|
||||
"command": "lsinitcpio -l /boot/initramfs-linux.img | grep hid-logitech",
|
||||
"result": "pass",
|
||||
"evidence": "empty after the host install: the hook-regenerated image no longer bundles the module, so the conditional path reports 'already matches' instead of a 20 s rebuild"
|
||||
},
|
||||
{
|
||||
"command": "shellcheck linux_configuration/fixes/install_new_lg4ff.sh",
|
||||
"result": "pass",
|
||||
"evidence": "clean"
|
||||
}
|
||||
],
|
||||
"risks": [
|
||||
"A hand-built initramfs that bundles the in-tree copy is still caught: it lists kernel/drivers/hid/ while modprobe resolves updates/dkms/"
|
||||
],
|
||||
"rollback": ["git revert; the unconditional regen returns"]
|
||||
}
|
||||
@ -1,42 +0,0 @@
|
||||
{
|
||||
"intent": "Stop the WireGuard/SSH firewall from silently cutting every Docker container off the internet, and rebuild Docker's own nft rules after the ruleset flush that deletes them.",
|
||||
"scope": [
|
||||
"linux_configuration/features/lib/wg_firewall.sh (write_nftables_ruleset, verify_nftables_then_apply)",
|
||||
"Non-goal: no change to the input chain, the WireGuard port, sshd hardening or the LAN/DNS rules",
|
||||
"Non-goal: no change to Docker's own iptables management (DOCKER-USER stays untouched)"
|
||||
],
|
||||
"changes": [
|
||||
"forward chain keeps policy drop but now accepts established/related plus traffic in or out of docker0 and any br-* bridge, so it stops vetoing Docker's own chains at the same netfilter hook",
|
||||
"new restore_docker_rules() restarts dockerd after a successful apply, because 'flush ruleset' deletes Docker's tables and dockerd only rebuilds them on its next network event"
|
||||
],
|
||||
"verification": [
|
||||
{
|
||||
"command": "vm new nftfw && vm run nftfw 'bash /tmp/nft_docker_guest_test.sh'",
|
||||
"result": "pass",
|
||||
"evidence": "Arch guest, docker installed, alpine wget probe to 1.1.1.1: BASELINE=reachable -> OLD(unpatched ruleset)=blocked -> NEW(patched ruleset)=reachable; sshd active throughout. The OLD leg reproduces the outage this fixes."
|
||||
},
|
||||
{
|
||||
"command": "nft -c -f <rendered ruleset>",
|
||||
"result": "pass",
|
||||
"evidence": "NFT SYNTAX OK for the file write_nftables_ruleset() generates with ALLOW_WEB/ALLOW_DNS both true."
|
||||
},
|
||||
{
|
||||
"command": "shellcheck -x --severity=style linux_configuration/features/lib/wg_firewall.sh",
|
||||
"result": "pass",
|
||||
"evidence": "no findings"
|
||||
},
|
||||
{
|
||||
"command": "live host check after applying the same five rules by hand",
|
||||
"result": "pass",
|
||||
"evidence": "signal-api container regained DNS and outbound; /v1/devices/+420720355763 went 400 'Closed unexpectedly' -> 200; the Signal bot answered !cat and !dog live at 18:28."
|
||||
}
|
||||
],
|
||||
"risks": [
|
||||
"Containers may now forward to the LAN, which is Docker's own default posture but is a relaxation of a bare default-drop forward chain",
|
||||
"restore_docker_rules restarts dockerd, so containers without a restart policy stay down until started again"
|
||||
],
|
||||
"rollback": [
|
||||
"git revert the commit, then re-run setup_wireguard_ssh.sh (or nft -f /etc/nftables.conf) to reapply the previous ruleset",
|
||||
"After rollback, expect bridged containers to lose outbound networking again: verify with 'docker run --rm --network bridge alpine wget -q -O /dev/null -T 6 https://1.1.1.1'"
|
||||
]
|
||||
}
|
||||
@ -1,73 +0,0 @@
|
||||
{
|
||||
"intent": "A drift check that nobody runs is not a check. Install systemd units that run `setup_wireguard_ssh.sh verify` at every boot and daily, so a stale /etc/nftables.conf fails a unit instead of waiting to be noticed after every bridged container has silently lost the internet.",
|
||||
"scope": [
|
||||
"linux_configuration/features/lib/wg_verify_units.sh",
|
||||
"linux_configuration/features/setup_wireguard_ssh.sh",
|
||||
"linux_configuration/features/lib/wg_sshd.sh",
|
||||
"linux_configuration/features/lib/tests/test_wg_verify_units.sh",
|
||||
"Non-goal: changing the ruleset or the two invariants verify_nft already gates on",
|
||||
"Non-goal: notifications; a failed unit in `systemctl --failed` is the gate",
|
||||
"meta/requirements.txt (unrelated baseline fix, see changes)"
|
||||
],
|
||||
"changes": [
|
||||
"New lib wg_verify_units.sh splits rendering from installing, the same way render/write_nftables_ruleset already do, so the generated unit CONTENT is assertable without a daemon-reload.",
|
||||
"nftables-drift.service is Type=oneshot, After=nftables.service docker.service network-online.target. network-online.target is load-bearing: detect_lan_subnet shells out to `ip route get 1.1.1.1` and dies when there is no route, which at boot would be indistinguishable from real drift.",
|
||||
"Deliberately NOT Requires=nftables.service: a dead nftables.service means the ruleset is not loaded at all, the most serious version of the fault, and the unit must run and fail rather than be skipped as unsatisfiable.",
|
||||
"ProtectSystem=strict + ProtectHome=read-only: verify only reads, and the saved LAN_SUBNET lives in the repo under $HOME, where a root-owned rewrite would lock the non-root subcommands out of their own config. The unit makes that write impossible instead of merely unlikely.",
|
||||
"ExecStart points at the repo checkout, not a copy under /usr/local/bin: the check is 'does /etc match what THIS repo generates', and a stale copy of a drift detector agrees with the drift.",
|
||||
"The units are installed by `setup`, by `allow-web` and `allow-dns` (the commands that regenerate /etc/nftables.conf, so exactly when the check must exist), and by a new `install-verify` subcommand for retrofitting an existing machine.",
|
||||
"Bumped the gatelock pin gattag v0.7.2 -> v0.7.3 in meta/requirements.txt to clear a pre-existing red dependency-freshness baseline on main, unrelated to this change but blocking the commit via the ci-baseline-green hook."
|
||||
],
|
||||
"verification": [
|
||||
{
|
||||
"command": "linux_configuration/features/lib/tests/test_wg_verify_units.sh",
|
||||
"result": "pass",
|
||||
"evidence": "12 passed, 0 failed. Asserts ExecStart, Type=oneshot, all three After= orderings, the absence of Requires=, both Protect* settings, WantedBy=multi-user.target, and the timer's Unit=/Persistent=/WantedBy=."
|
||||
},
|
||||
{
|
||||
"command": "meta/scripts/shell_coverage_jail.sh --subject linux_configuration/features/lib/tests/run_all.sh $(cat .../jail_args) --min 0 --fail-on-case-error -- ''",
|
||||
"result": "pass",
|
||||
"evidence": "The whole features/lib suite passes under the jail with the new file added; exit 0, no FAIL lines. Same invocation meta/scripts/ci_mirror_shell.sh uses pre-push."
|
||||
},
|
||||
{
|
||||
"command": "vm run nftdrift 'sudo setup_wireguard_ssh.sh install-verify' then a reboot with /etc/nftables.conf drifted",
|
||||
"result": "pass",
|
||||
"evidence": "Boot-time run FAILED as required: `systemctl is-failed` -> failed; `systemctl --failed` lists it; the journal carries the unified diff of the removed `iifname \"docker0\" accept` line AND '[ERROR] live forward chain does NOT accept docker0 -- bridged containers have no network.' Both invariants fired."
|
||||
},
|
||||
{
|
||||
"command": "vm run nftdrift 'sudo setup_wireguard_ssh.sh allow-web' then a second reboot",
|
||||
"result": "pass",
|
||||
"evidence": "No false positive: unit active, journal shows '[ OK ] /etc/nftables.conf matches what this script generates.' and '[ OK ] live forward chain lets Docker through.'; systemctl --failed count for it is 0; timer armed for 00:00."
|
||||
},
|
||||
{
|
||||
"command": "sudo linux_configuration/features/setup_wireguard_ssh.sh install-verify && sudo systemctl start nftables-drift.service (host)",
|
||||
"result": "pass",
|
||||
"evidence": "Installed and enabled on the real machine; the run is green on both invariants and nftables-drift.timer is listed for Mon 2026-08-31 00:00:00 CEST. Not yet observed across a real host reboot -- that was proven in vmbox."
|
||||
},
|
||||
{
|
||||
"command": "shellcheck -x --severity=style on every changed shell file",
|
||||
"result": "pass",
|
||||
"evidence": "clean"
|
||||
},
|
||||
{
|
||||
"command": "systemd-analyze verify on the rendered units",
|
||||
"result": "pass",
|
||||
"evidence": "No findings for either unit; the only output was pre-existing noise about unrelated host units."
|
||||
},
|
||||
{
|
||||
"command": "git ls-remote --tags https://github.com/kuhyx/utils | grep gatelock",
|
||||
"result": "pass",
|
||||
"evidence": "gatelock-v0.7.3 is the newest tag; the pin now names it, which is what the freshness gate compares against."
|
||||
}
|
||||
],
|
||||
"risks": [
|
||||
"ExecStart hardcodes the repo path resolved at install time. Moving or renaming the checkout leaves the unit failing at every boot -- loudly, which is the correct direction, but it is a failure about the unit rather than about the firewall.",
|
||||
"The host installation has not yet survived a real reboot; only the vmbox guest has. The first real reboot is the true test, and a false positive there would show as a failed unit rather than as a network outage.",
|
||||
"allow-web/allow-dns now also (re)install the units, so those commands touch /etc/systemd/system where they previously did not."
|
||||
],
|
||||
"rollback": [
|
||||
"sudo systemctl disable --now nftables-drift.timer && sudo systemctl disable nftables-drift.service",
|
||||
"sudo rm /etc/systemd/system/nftables-drift.{service,timer} && sudo systemctl daemon-reload",
|
||||
"git revert the commit. After rollback: confirm `systemctl list-unit-files nftables-drift.*` reports nothing, and remember that `setup_wireguard_ssh.sh verify` is once again a check nobody runs."
|
||||
]
|
||||
}
|
||||
@ -1,40 +0,0 @@
|
||||
{
|
||||
"intent": "Make the Docker cut-off caused by a stale /etc/nftables.conf detectable instead of silent. The 2026-08-29 fix landed in wg_firewall.sh but was only ever hand-applied to the live ruleset; /etc/nftables.conf kept the bare default-drop forward chain, so the 2026-08-30 reboot reloaded it and cut every bridged container off again. Nothing compared the generated ruleset with the persisted one, so the regression was invisible until the Signal bot went deaf for a second time.",
|
||||
"scope": [
|
||||
"linux_configuration/features/lib/wg_firewall.sh (split render_nftables_ruleset out of write_nftables_ruleset; add verify_nft)",
|
||||
"linux_configuration/features/setup_wireguard_ssh.sh (new 'verify' subcommand, added to the require_root list)",
|
||||
"linux_configuration/features/lib/wg_sshd.sh (usage text only)",
|
||||
"Non-goal: no change to the generated ruleset itself — the forward-chain fix from 334c0cd9 is unchanged",
|
||||
"Non-goal: no change to the input chain, WireGuard, sshd hardening or DuckDNS"
|
||||
],
|
||||
"changes": [
|
||||
"render_nftables_ruleset(target) is split out of write_nftables_ruleset so the ruleset can be regenerated into a scratch file with no backup and no side effects; write_nftables_ruleset keeps the backup and delegates to it",
|
||||
"verify_nft() gates on two invariants and exits non-zero: (1) /etc/nftables.conf is byte-identical to what the script generates, printing a unified diff when it is not, and (2) the LIVE 'inet filter forward' chain still contains 'iifname \"docker0\" accept'. It names the exact re-apply command on failure",
|
||||
"'verify' wired into main() and into usage; it needs root because 'nft list chain' does"
|
||||
],
|
||||
"verification": [
|
||||
{
|
||||
"command": "sudo setup_wireguard_ssh.sh verify (on the live host, before re-applying)",
|
||||
"result": "pass",
|
||||
"evidence": "Exits non-zero and reports both failures on the currently-broken host: '/etc/nftables.conf has DRIFTED from this script' with a diff showing exactly the missing forward-chain block (ct established,related + docker0/br-* accepts), and 'live forward chain does NOT accept docker0'. This is the regression reproducing, which is what the gate is for."
|
||||
},
|
||||
{
|
||||
"command": "diff of rendered output vs /etc/nftables.conf",
|
||||
"result": "pass",
|
||||
"evidence": "The only difference is the intended forward-chain block; the input and output chains, DHCP/DNS/web rules and LAN subnet render byte-identical. Confirms the render/write refactor did not change the generated ruleset."
|
||||
},
|
||||
{
|
||||
"command": "shellcheck -x --severity=style linux_configuration/features/setup_wireguard_ssh.sh",
|
||||
"result": "pass",
|
||||
"evidence": "exit 0, no findings (follows the sourced libs)"
|
||||
}
|
||||
],
|
||||
"risks": [
|
||||
"verify_nft needs root for 'nft list chain'; run without sudo it reports the live-chain check as failed rather than as unknown",
|
||||
"The drift check is byte-exact, so an intentional hand-edit of /etc/nftables.conf will be reported as drift — which is the intended behaviour, but means the fix is always 're-run allow-web', never 'edit the file'"
|
||||
],
|
||||
"rollback": [
|
||||
"git revert this commit; write_nftables_ruleset keeps working because render_nftables_ruleset is only a refactor of its body",
|
||||
"After rollback, re-run 'sudo setup_wireguard_ssh.sh allow-web' if /etc/nftables.conf is still stale, and confirm with: docker run --rm --network bridge redis:8-alpine sh -c 'nc -w4 -z 1.1.1.1 443 && echo OK'"
|
||||
]
|
||||
}
|
||||
@ -1,29 +0,0 @@
|
||||
{
|
||||
"intent": "Make the arch_perf_probes lib tests independent of the host's real load average so the pre-push ci-mirror gate is not red whenever a game is running.",
|
||||
"scope": [
|
||||
"linux_configuration/fixes/lib/arch_perf_probes.sh (LOADAVG_FILE override, default /proc/loadavg)",
|
||||
"linux_configuration/fixes/lib/tests/arch_perf_harness.sh (fixture written by arch_reset)",
|
||||
"linux_configuration/fixes/lib/tests/test_arch_perf_probes.sh (deterministic load cases)"
|
||||
],
|
||||
"changes": [
|
||||
"collect_basics reads the 1-minute load from ${LOADAVG_FILE:-/proc/loadavg}; behaviour on the host is unchanged",
|
||||
"The harness exports LOADAVG_FILE into TEST_TMPDIR and arch_reset writes a quiet fixture (0.10)",
|
||||
"The load test now asserts the (16/16) finding from a 16.50 fixture instead of SKIPping when the host is idle; a missing file case asserts no finding"
|
||||
],
|
||||
"verification": [
|
||||
{
|
||||
"command": "./linux_configuration/fixes/lib/tests/test_arch_perf_probes.sh",
|
||||
"result": "pass",
|
||||
"evidence": "arch_perf_probes (basics/governor): 17 passed, 0 failed (was 16 with one SKIP, and 1 failed at host load 22/16)"
|
||||
},
|
||||
{
|
||||
"command": "./linux_configuration/fixes/lib/tests/run_all.sh (host load ~19/16, BeamNG running)",
|
||||
"result": "pass",
|
||||
"evidence": "All fixes lib test files passed."
|
||||
}
|
||||
],
|
||||
"risks": ["None on the host: the default path is the real /proc/loadavg"],
|
||||
"rollback": [
|
||||
"git revert the commit; the tests return to reading the real load and go red under host load"
|
||||
]
|
||||
}
|
||||
@ -1,39 +0,0 @@
|
||||
{
|
||||
"intent": "Several Claude sessions act on one phone at once without interrupting each other; a raw `adb shell input tap` (no lease) had landed in another session's app on 2026-09-25.",
|
||||
"scope": [
|
||||
"python_pkg/phone_guard (new: classify, _remote, _need, vd_state, hook, CLI, tests)",
|
||||
"python_pkg/android_ui (--display, _a11y_helper + A11yDump.java, -d on every input, tests split under the 250-line cap)"
|
||||
],
|
||||
"changes": [
|
||||
"phone_guard.classify maps every adb call in a shell command to a Need: nothing, screen, one app, whole phone, or a block",
|
||||
"phone_guard.hook applies it as the Claude Code PreToolUse hook: acquires as this session, waits 40 s, exits 2 on refusal",
|
||||
"android_ui --display self|<id> resolves the session's phone_vd display and takes its package lease; a foreign display is refused (exit 5)",
|
||||
"_a11y_helper builds a tiny instrumentation APK (javac, d8, aapt2, apksigner; no Gradle), cached by source hash, reading getWindowsOnAllDisplays()",
|
||||
"The helper source sits flat beside the module (A11yDump.java, a11y_dump_manifest.xml) to respect the two-directory path cap"
|
||||
],
|
||||
"verification": [
|
||||
{
|
||||
"command": "pytest python_pkg/android_ui python_pkg/phone_guard --cov --cov-branch",
|
||||
"result": "pass",
|
||||
"evidence": "152 passed; every android_ui and phone_guard module 100% line and branch"
|
||||
},
|
||||
{
|
||||
"command": "python3 -c 'from python_pkg.android_ui import _a11y_helper as h; print(h.build())'",
|
||||
"result": "pass",
|
||||
"evidence": "~/.cache/android_ui/a11ydump-fb2baa8888080a3c.apk built with the real SDK"
|
||||
},
|
||||
{
|
||||
"command": "pre-commit run --files <changed-files>",
|
||||
"result": "pass",
|
||||
"evidence": "all hooks passed"
|
||||
}
|
||||
],
|
||||
"risks": [
|
||||
"The hook passes unknown commands: it guards against interrupting other sessions, it is not a sandbox.",
|
||||
"The helper must stay in phone-focus-mode's whitelist or the focus-owner sweep hides it."
|
||||
],
|
||||
"rollback": [
|
||||
"git revert the commit",
|
||||
"Remove the phone_guard_pretool.sh entry from ~/.claude/settings.json PreToolUse"
|
||||
]
|
||||
}
|
||||
@ -1,33 +0,0 @@
|
||||
{
|
||||
"intent": "Two Claude sessions can no longer drive the phone at the same time: every android_ui call and every phone_deploy.sh run takes a per-serial lease owned by the session, and a foreign holder is refused by name with the alternatives spelled out.",
|
||||
"scope": [
|
||||
"python_pkg/phone_lease (new): lease file, owner identity, acquire/release/status, CLI",
|
||||
"python_pkg/android_ui/cli.py: acquire the lease before every command (exit 3 when held elsewhere)",
|
||||
"Non-goal: parallel driving of one screen; that is what the virtual display / emulator scripts in ~/.claude/scripts are for"
|
||||
],
|
||||
"changes": [
|
||||
"flock-guarded JSON lease per serial under ~/.cache/phone-lease/, 180 s TTL refreshed on every call",
|
||||
"owner = nearest ancestor `claude` process (one per session); PHONE_LEASE_OWNER overrides",
|
||||
"android_ui main() refuses with exit 3 and the holder's name; phone_deploy.sh exits 40"
|
||||
],
|
||||
"verification": [
|
||||
{
|
||||
"command": "pre-commit run --files python_pkg/phone_lease/... python_pkg/android_ui/...",
|
||||
"result": "pass",
|
||||
"evidence": "all hooks passed; python_pkg 1886 passed, 100% line+branch coverage"
|
||||
},
|
||||
{
|
||||
"command": "python3 -m python_pkg.android_ui -s 23181JEGR08034 focus; PHONE_LEASE_OWNER=other python3 -m python_pkg.android_ui -s 23181JEGR08034 focus",
|
||||
"result": "pass",
|
||||
"evidence": "first call refreshed claude:4102845's lease; the foreign call waited 60 s and exited 3: 'phone 23181JEGR08034 held by claude:4102845 (android_ui focus) until 17:32:00'"
|
||||
}
|
||||
],
|
||||
"risks": [
|
||||
"A session that keeps calling android_ui holds the phone indefinitely (by design); `python3 -m python_pkg.phone_lease release <serial>` drops it",
|
||||
"Raw adb use bypasses the lease; only the sanctioned tools take it"
|
||||
],
|
||||
"rollback": [
|
||||
"Revert this commit; android_ui returns to lease-free operation",
|
||||
"Validate: python3 -m python_pkg.android_ui focus runs without touching ~/.cache/phone-lease"
|
||||
]
|
||||
}
|
||||
@ -1,45 +0,0 @@
|
||||
{
|
||||
"intent": "Several Claude sessions can use one phone at once without interrupting each other: leases are per app, a whole-phone lease is short and cleans up after itself, and every caller agrees on the serial.",
|
||||
"scope": [
|
||||
"python_pkg/phone_lease only (lease, ops, owner, serial, _cleanup, CLI, tests)",
|
||||
"Callers unchanged: phone_deploy.sh's `acquire \"$SERIAL\" --note --wait` and android_ui's acquire(serial, note) keep the real-screen scope",
|
||||
"phone_deploy/android_ui/raw-adb hook changes belong to the image-attachments-todo-dufs session"
|
||||
],
|
||||
"changes": [
|
||||
"Scopes: pkg:<package> (default pkg:__screen__) and whole-phone; per-package leases coexist, whole-phone conflicts with every foreign lease",
|
||||
"whole-phone ttl capped at 300 s; --cleanup commands run exactly once on release or expiry, expiry enforced by a detached `reap` process that wakes at least every 5 s",
|
||||
"One JSON file per phone holds all leases; pre-scope single-object files are read as a screen lease",
|
||||
"resolve_serial: explicit, $ANDROID_SERIAL, $ADB_SERIAL, `adb get-serialno`; never a 'default' placeholder (the cause of the 2026-09-25 clash)",
|
||||
"CLI: optional serial, --package/--whole-phone/--cleanup, status --json, holds, whoami; exit 4 when no phone",
|
||||
"acquire(serial, note, *, scope, terms=Terms(ttl, wait, cleanup), owner, clock) to stay within max-args 6"
|
||||
],
|
||||
"verification": [
|
||||
{
|
||||
"command": "two owners via PHONE_LEASE_OWNER against a temp PHONE_LEASE_DIR",
|
||||
"result": "pass",
|
||||
"evidence": "pkg:a + pkg:b coexist (0 0); B on pkg:a -> 3; B whole-phone while A holds a package -> 3; ttl 999 clamped to +300 s; release ran cleanup; unreleased whole-phone ttl 3 refreshed at +2 s was reaped only after the refreshed expiry"
|
||||
},
|
||||
{
|
||||
"command": "~/.claude/scripts/phone_vd.sh start as testA (umbrella) and testB (settings) on the real phone",
|
||||
"result": "pass",
|
||||
"evidence": "displays 7 and 8 side by side; tap on 7 changed only 7; display 0 unchanged; third owner on umbrella exit 43; stop released both package leases"
|
||||
},
|
||||
{
|
||||
"command": "pytest python_pkg/phone_lease --cov --cov-branch",
|
||||
"result": "pass",
|
||||
"evidence": "65 passed; every phone_lease module 100% line and branch"
|
||||
},
|
||||
{
|
||||
"command": "pre-commit run --files <changed-files>",
|
||||
"result": "pass",
|
||||
"evidence": "all hooks passed"
|
||||
}
|
||||
],
|
||||
"risks": [
|
||||
"A reaper killed before expiry leaves cleanup to the next locked update of that phone's lease file, which runs it then."
|
||||
],
|
||||
"rollback": [
|
||||
"git revert the commit; old lease files remain readable by the reverted code only if they hold a single object, so delete ~/.cache/phone-lease/*.json after reverting",
|
||||
"Re-run pytest python_pkg/phone_lease"
|
||||
]
|
||||
}
|
||||
@ -1,47 +0,0 @@
|
||||
{
|
||||
"intent": "Make `git push` return in about a second instead of ~13s, by not paying Node's startup cost to re-check markdown/JSON/YAML files whose content, prettier version and prettier config are all unchanged since their last clean check.",
|
||||
"scope": [
|
||||
"meta/scripts/run_prettier_capped.sh - the prettier hook's runner",
|
||||
"endurain/README.md and linux_configuration/.github/copilot-instructions.md - reflowed by prettier after the ~ reorganisation lengthened paths inside them",
|
||||
"Non-goal: changing what prettier checks, its config, or the ci-mirror gate. The set of files that must be clean is identical before and after.",
|
||||
"Non-goal: the first push after a requirements.txt change, which still rebuilds a clean venv by design."
|
||||
],
|
||||
"changes": [
|
||||
"run_prettier_capped.sh now keys a result cache on sha256(file content) plus a fingerprint of `prettier --version` and every prettier config file, so a cached verdict cannot survive an edit, a prettier upgrade or a config change.",
|
||||
"Files with a cached clean verdict are dropped from the argv; when the argv empties, prettier is never started at all.",
|
||||
"Only a clean verdict is recorded - a failing check is re-run next time, so the cache can never mask a formatting regression.",
|
||||
"The cache is wholesale-pruned past 20000 entries, which costs one slow run rather than correctness.",
|
||||
"The systemd-run memory cap (MemoryMax=512M, MemorySwapMax=0) is preserved exactly, per the oom-prevention skill."
|
||||
],
|
||||
"verification": [
|
||||
{
|
||||
"command": "bash meta/scripts/run_prettier_capped.sh <40 tracked markdown files> # cold, then warm",
|
||||
"result": "pass",
|
||||
"evidence": "cold 0.652s with 40 cache entries written; warm 0.057s printing 'prettier: 40 file(s) unchanged since their last clean check'. 11x faster with no prettier process started."
|
||||
},
|
||||
{
|
||||
"command": "append trailing whitespace to README.md, re-run the same file list",
|
||||
"result": "pass",
|
||||
"evidence": "[warn] README.md - the edited file misses the cache and is re-checked; after `git checkout -- README.md` the run is clean again. Cache keyed on content, so an edit cannot be masked."
|
||||
},
|
||||
{
|
||||
"command": "git push --dry-run (pre-push: prettier + ci-mirror + pre-commit pre-push stage)",
|
||||
"result": "pass",
|
||||
"evidence": "13.0s before this change, 2.9s after, on the same 106-file push range. ci-mirror itself is 0.02s when its green-tree cache hits; the remainder was prettier's repeated Node startups."
|
||||
},
|
||||
{
|
||||
"command": "shellcheck -x -S warning meta/scripts/run_prettier_capped.sh",
|
||||
"result": "pass",
|
||||
"evidence": "no findings; shfmt applied, matching the repo's tab style."
|
||||
}
|
||||
],
|
||||
"risks": [
|
||||
"A cached verdict is trusted without re-running prettier. It is invalidated by file content, prettier version and prettier config, so the remaining exposure is a prettier behaviour change that ships without a version bump - which does not happen for a pinned release.",
|
||||
"The cache lives under XDG_CACHE_HOME, outside the repo, so it is per-machine and a fresh clone or a cleared cache simply pays one full run.",
|
||||
"prettier errors rather than warns on an explicitly-listed symbolic link (CLAUDE.md -> AGENTS.md). That behaviour is unchanged by this patch, but it means a push range containing CLAUDE.md fails the hook for a reason unrelated to formatting."
|
||||
],
|
||||
"rollback": [
|
||||
"git revert the commit, or restore the previous run_prettier_capped.sh which called `prettier --check \"$@\"` unconditionally inside the same systemd-run scope.",
|
||||
"After rollback, run `git push --dry-run` and confirm the prettier hook still passes; delete ${XDG_CACHE_HOME:-~/.cache}/prettier-gate, which is then unused."
|
||||
]
|
||||
}
|
||||
@ -1,44 +0,0 @@
|
||||
{
|
||||
"intent": "Make ruff, mypy and pylint grade this repo against the same Python the code actually runs on, so the fleet's 'exactly one Python, always the newest' decision holds for the graders and not just for CI.",
|
||||
"scope": [
|
||||
"meta/pyproject.toml: ruff target-version, mypy python_version, pylint py-version",
|
||||
"python_pkg/**: the source rewrites those three knobs then required",
|
||||
"Non-goal: dependency pins, CI workflow files, and anything under app/ or web/"
|
||||
],
|
||||
"changes": [
|
||||
"pin_ci_python.py rewrote target-version py310 -> py314, python_version 3.10 -> 3.14 and py-version 3.10 -> 3.14 in meta/pyproject.toml",
|
||||
"ruff --fix applied UP017 (datetime.timezone.utc -> datetime.UTC) across the tree",
|
||||
"ruff-format applied PEP 758 in its stable style: except (A, B): becomes except A, B:",
|
||||
"Deleted a comment in python_pkg/wsg_grabber/models.py that justified avoiding StrEnum under mypy at 3.10, directly above a class Verdict(StrEnum) that had already stopped obeying it"
|
||||
],
|
||||
"verification": [
|
||||
{
|
||||
"command": "pre-commit run ruff --all-files",
|
||||
"result": "pass",
|
||||
"evidence": "ruff (legacy alias) Passed"
|
||||
},
|
||||
{
|
||||
"command": "pre-commit run ruff-format --all-files",
|
||||
"result": "pass",
|
||||
"evidence": "ruff format Passed"
|
||||
},
|
||||
{
|
||||
"command": "pre-commit run mypy --all-files",
|
||||
"result": "pass",
|
||||
"evidence": "mypy Passed"
|
||||
},
|
||||
{
|
||||
"command": "pre-commit run pylint --all-files",
|
||||
"result": "pass",
|
||||
"evidence": "pylint Passed"
|
||||
}
|
||||
],
|
||||
"risks": [
|
||||
"PEP 758 makes the source a SyntaxError on 3.13 and older, and except A, B: is visually identical to the Python 2 except E, name: idiom that meant something entirely different. requires-python already said >=3.14; kuhy chose this over holding ruff a release back on 2026-08-29.",
|
||||
"A future ruff release could add further target-gated formatter changes; the same three knobs are where that would surface."
|
||||
],
|
||||
"rollback": [
|
||||
"git revert the commit: the three knobs and the source rewrites move together, so reverting restores a self-consistent py310 target.",
|
||||
"After rollback, re-run pre-commit run --all-files; ruff at py310 will not reintroduce the UP017 or PEP 758 forms."
|
||||
]
|
||||
}
|
||||
@ -1,36 +0,0 @@
|
||||
{
|
||||
"intent": "betting-sim's Flutter app gets a launcher icon from the shared family instead of the stock Flutter beach icon.",
|
||||
"scope": [
|
||||
"python_pkg/app_icons/apps.py — one new AppIcon entry",
|
||||
"Non-goal: no change to the generator, the glyph set, or any other app's icon"
|
||||
],
|
||||
"changes": [
|
||||
"Registered betting_sim: repo ~/src/betting-sim/app, accent #B8862E, glyph track-bars (rising bars with a plotted trend point, which reads as odds movement).",
|
||||
"Set linux=false: betting-sim's desktop target is a Chrome-wrapped web build rather than the GTK embedder, so the hicolor PNG set would go unread."
|
||||
],
|
||||
"verification": [
|
||||
{
|
||||
"command": "PYTHONPATH=. python3 -m python_pkg.app_icons list",
|
||||
"result": "pass",
|
||||
"evidence": "betting_sim #B8862E track-bars /home/kuhy/src/betting-sim/app"
|
||||
},
|
||||
{
|
||||
"command": "PYTHONPATH=. python3 -m python_pkg.app_icons generate --app betting_sim --android",
|
||||
"result": "pass",
|
||||
"evidence": "wrote 6 asset files to /home/kuhy/src/betting-sim/app/assets/icon; mipmaps generated after adding flutter_launcher_icons to the app pubspec"
|
||||
},
|
||||
{
|
||||
"command": "visual check of assets/icon/icon.png",
|
||||
"result": "pass",
|
||||
"evidence": "charcoal #211D1B field, #B8862E glyph — on-family, no stock beach icon"
|
||||
}
|
||||
],
|
||||
"risks": [
|
||||
"None to existing apps: the change is purely additive, one tuple entry.",
|
||||
"If betting-sim later grows a real GTK Linux target, linux must flip to true or the hicolor set will be missing."
|
||||
],
|
||||
"rollback": [
|
||||
"Remove the betting_sim AppIcon entry from python_pkg/app_icons/apps.py.",
|
||||
"Re-run `app_icons list` and confirm the other 14 apps are unchanged."
|
||||
]
|
||||
}
|
||||
@ -1,31 +0,0 @@
|
||||
{
|
||||
"intent": "book-guard's new Flutter app gets a launcher icon from the shared family, and android_ui's CLI test stops depending on the host's live phone lease.",
|
||||
"scope": [
|
||||
"python_pkg/app_icons/_glyph_art_more.py, glyphs.py, apps.py: new open-book glyph + book_guard_app registry entry",
|
||||
"python_pkg/android_ui/tests/test_cli.py: test_serial_is_forwarded_to_the_driver patches _lease_phone",
|
||||
"Non-goal: no change to style.py or any other app's icon"
|
||||
],
|
||||
"changes": [
|
||||
"Added the filled open-book glyph (two pages, 48px spine gap >= MIN_NEGATIVE_SPACE) and registered book_guard_app -> ~/src/book-guard/app",
|
||||
"Patched _lease_phone in the serial-forwarding test: unpatched it read the real lease and exited busy whenever another session held the phone"
|
||||
],
|
||||
"verification": [
|
||||
{
|
||||
"command": "python3 -m pytest python_pkg/app_icons/tests python_pkg/android_ui/tests/test_cli.py",
|
||||
"result": "pass",
|
||||
"evidence": "44 app_icons tests pass at 100% branch coverage; test_cli 20 passed, including while a phone lease was held"
|
||||
},
|
||||
{
|
||||
"command": "python3 -m python_pkg.app_icons preview; generate --app book_guard_app --android",
|
||||
"result": "pass",
|
||||
"evidence": "Open book reads at full size, circle mask and 48dp; APK with the icon installed on the Pixel 6a"
|
||||
}
|
||||
],
|
||||
"risks": [
|
||||
"Registry invariant tests assume every app shares the accent -- satisfied (#B8862E)"
|
||||
],
|
||||
"rollback": [
|
||||
"git revert this commit; regenerate book-guard's icon if the glyph goes away",
|
||||
"Re-run python_pkg/app_icons tests after rollback"
|
||||
]
|
||||
}
|
||||
@ -1,78 +0,0 @@
|
||||
{
|
||||
"intent": "Repair the live self-repair mechanisms that the earlier flatten and extractions silently broke: hosts-file-monitor could not restore /etc/hosts, dns-blocklist-refresh.service pointed at a deleted path, media-organizer.service likewise, setup_periodic_system.sh read templates from a directory holding only build residue, and a sudoers drop-in granted NOPASSWD on a nonexistent file.",
|
||||
"scope": [
|
||||
"linux_configuration/lib/extracted_repos.sh — handle invocation as root with no SUDO_USER",
|
||||
"linux_configuration/periodic_background/setup_periodic_system.sh — resolve templates from the extracted system-maintenance repo",
|
||||
"LIVE (not in repo): /usr/local/bin/hosts-file-monitor.sh, /usr/local/bin/periodic-system-maintenance.sh, /etc/sudoers.d/hosts-install-no-passwd, /etc/systemd/system/dns-blocklist-refresh.service, /etc/systemd/system/media-organizer.service",
|
||||
"NON-GOAL: the 2 remaining stale references, which name digital_wellbeing paths and can only be fixed once that unit is extracted"
|
||||
],
|
||||
"changes": [
|
||||
"extracted_repo_home() previously resolved SUDO_USER, then USER, then `id -un`. Under systemd all three yield root, so it returned /root/<repo>, which never exists. It now detects the root case and falls back to REPO_OWNER_USER, defaulting to the owner of the lib file itself -- the checkout the caller is running from is the best available evidence of whose home the sibling repos live in.",
|
||||
"setup_periodic_system.sh read its ten templates from $CONFIG_DIR/periodic_background/system-maintenance, which since the system-maintenance extraction has contained nothing but untracked .pyc/.coverage residue. Every `install -m 0644 \"$TEMPLATE_...\"` was reading a nonexistent file. Now resolved via require_extracted_repo system-maintenance.",
|
||||
"Rewrote the embedded HOSTS_INSTALL_SCRIPT path in the two installed copies under /usr/local/bin, backed up to /var/tmp/*.pre-extract first.",
|
||||
"Rewrote the sudoers drop-in to name the new installer path, validated with `visudo -cf` on a temp file BEFORE installing, then re-validated the whole tree.",
|
||||
"Rewrote the two systemd units off the pre-flatten linux_configuration/scripts/single_use/ prefix, via validated temp files rather than in-place sed."
|
||||
],
|
||||
"verification": [
|
||||
{
|
||||
"command": "sudo grep -c 'not found at' /var/log/hosts-file-monitor.log",
|
||||
"result": "pass",
|
||||
"evidence": "37 logged failures to restore /etc/hosts before the fix -- the monitor was active the whole time and could not run its installer."
|
||||
},
|
||||
{
|
||||
"command": "sudo systemctl start dns-blocklist-refresh.service (BEFORE the resolver fix)",
|
||||
"result": "fail",
|
||||
"evidence": "[ERROR] Feed generator not found: /root/hosts-blocker/generate_hosts_file.sh -- the systemd-as-root case. Static path checks did not catch this; only running the unit did."
|
||||
},
|
||||
{
|
||||
"command": "sudo systemctl start dns-blocklist-refresh.service (AFTER the resolver fix)",
|
||||
"result": "pass",
|
||||
"evidence": "[ OK ] Feed refreshed and dnsmasq reloaded (189038 lines). Finished Rebuild the LAN DNS blocklist and reload dnsmasq."
|
||||
},
|
||||
{
|
||||
"command": "resolver across all four invocation modes",
|
||||
"result": "pass",
|
||||
"evidence": "as user -> /home/kuhy/src/hosts-blocker; via sudo -> same; as root with SUDO_USER/USER unset -> same; HOSTS_BLOCKER_DIR=/opt/hb -> /opt/hb (override still wins)."
|
||||
},
|
||||
{
|
||||
"command": "sudo visudo -c",
|
||||
"result": "pass",
|
||||
"evidence": "hosts-install-no-passwd: parsed OK, and every other drop-in still parses. Validated on a temp file before install, so a malformed file could never reach /etc/sudoers.d."
|
||||
},
|
||||
{
|
||||
"command": "systemctl is-active hosts-file-monitor.service after restart",
|
||||
"result": "pass",
|
||||
"evidence": "active; log shows '=== Hosts File Monitor Started ===' and 'Using inotify for file monitoring' with the corrected installer path present on disk."
|
||||
},
|
||||
{
|
||||
"command": "lsattr /etc/hosts; guardctl file-guard status hosts; wc -l /etc/hosts",
|
||||
"result": "pass",
|
||||
"evidence": "----i---------e------- (still immutable), bind_mount: yes, 176888 lines -- the live blocker was never disturbed."
|
||||
},
|
||||
{
|
||||
"command": "getent ahostsv4 doubleclick.net",
|
||||
"result": "pass",
|
||||
"evidence": "0.0.0.0 -- blocking works through the extracted repo. (The IPv6 lookup returns a real address, which is pre-existing: the StevenBlack feed lists IPv4 entries only. Not introduced here.)"
|
||||
},
|
||||
{
|
||||
"command": "diff live-state-before.txt live-state-after2.txt",
|
||||
"result": "pass",
|
||||
"evidence": "Removals only: 8 stale references down to 4, then to 2 after the systemd units. None added. The 2 that remain are digital_wellbeing paths."
|
||||
},
|
||||
{
|
||||
"command": "shellcheck on both changed files",
|
||||
"result": "pass",
|
||||
"evidence": "clean, no severity filter."
|
||||
}
|
||||
],
|
||||
"risks": [
|
||||
"extracted_repo_home() now infers the owning user from the lib file's owner when running as bare root. On a machine where the checkout is owned by root this returns /root/<repo> as before -- no worse than the previous behaviour, and REPO_OWNER_USER overrides it.",
|
||||
"The two /usr/local/bin copies were patched in place rather than regenerated by setup_periodic_system.sh. Re-running that installer is the durable fix and it now resolves its templates correctly; it was not run here because it writes systemd units and sudoers on the live host (sandbox-first).",
|
||||
"2 stale references remain, both naming digital_wellbeing paths in /usr/local/bin/periodic-system-maintenance.sh. They stay broken until that unit is extracted."
|
||||
],
|
||||
"rollback": [
|
||||
"Repo: git revert this commit.",
|
||||
"Live artifacts: restore /var/tmp/hosts-file-monitor.sh.pre-extract, /var/tmp/periodic-system-maintenance.sh.pre-extract, /var/tmp/hosts-install-no-passwd.pre-extract, /var/tmp/dns-blocklist-refresh.service.pre-fix and /var/tmp/media-organizer.service.pre-fix to their original locations, then systemctl daemon-reload.",
|
||||
"After rollback validate: sudo visudo -c parses clean, and note that dns-blocklist-refresh.service will fail again on its next 00:01 run."
|
||||
]
|
||||
}
|
||||
@ -1,47 +0,0 @@
|
||||
{
|
||||
"intent": "Run the SyncYomi server from kuhy's own fork instead of the published image, so server-side changes reach the live service, and stop the setup script from breaking the shared Caddy edge on every re-run.",
|
||||
"scope": [
|
||||
"linux_configuration/features/setup_syncyomi.sh",
|
||||
"linux_configuration/features/lib/syncyomi_stack.sh",
|
||||
"linux_configuration/features/lib/syncyomi_expose.sh",
|
||||
"Non-goal: changing SyncYomi's schema, credentials or the phone's sync settings"
|
||||
],
|
||||
"changes": [
|
||||
"SYNCYOMI_IMAGE defaults to the local build syncyomi-kuhy:local, overridable back to a registry tag; adds SYNCYOMI_SRC and the fork URL.",
|
||||
"ensure_local_image() builds that image from ~/vendor/syncyomi-src (cloning the fork if absent) before the container starts, under DOCKER_BUILDKIT=1 because the Dockerfile reads $BUILDPLATFORM; SYNCYOMI_REBUILD=1 forces a rebuild.",
|
||||
"configure_caddy() now owns gitea/sites/<subdomain>.caddy instead of appending to the shared Caddyfile, deletes a legacy inline block, and validates the config before reloading.",
|
||||
"Preflight installs git and docker-buildx alongside the existing packages."
|
||||
],
|
||||
"verification": [
|
||||
{
|
||||
"command": "./linux_configuration/features/setup_syncyomi.sh setup",
|
||||
"result": "pass",
|
||||
"evidence": "Second (idempotent) run: 'Local image syncyomi-kuhy:local already built', 'Caddy snippet already current', caddy validate+reload OK, report printed. The first run caught the bug this fixes: 'ambiguous site definition: kuhy-sync.duckdns.org'."
|
||||
},
|
||||
{
|
||||
"command": "docker ps --filter name=^syncyomi$ + docker logs syncyomi",
|
||||
"result": "pass",
|
||||
"evidence": "Image syncyomi-kuhy:local, 'Version: v1.1.14-24-g21c6acd-kuhy', commit 21c6acd."
|
||||
},
|
||||
{
|
||||
"command": "systemctl --user start syncyomi-guard.service",
|
||||
"result": "pass",
|
||||
"evidence": "ok: 2163 manga, 69776 chapters, 1 categories, 23 sources (13346096 bytes) - unchanged across the image swap."
|
||||
},
|
||||
{
|
||||
"command": "TachiyomiSY 'Sync library' on the phone, then read docker logs + sync_data row",
|
||||
"result": "pass",
|
||||
"evidence": "'Sync data replaced' at 14:42:34; payload length 13346096 before and after; https://kuhy-sync.duckdns.org/api/auth/onboard returns 403 (user exists)."
|
||||
}
|
||||
],
|
||||
"risks": [
|
||||
"The deployed build is develop (v1.1.14 + dependency bumps), not the v1.1.11 image it replaced; schema stays at version 8 and internal/database is unchanged between those points, so no migration runs.",
|
||||
"A machine without ~/vendor/syncyomi-src now clones the fork on first setup, which needs network and a Docker build (~4 min) instead of an image pull.",
|
||||
"The Caddy snippet is only rewritten when its content differs, so hand edits to that file are overwritten by design."
|
||||
],
|
||||
"rollback": [
|
||||
"SYNCYOMI_IMAGE=ghcr.io/syncyomi/syncyomi:v1.1.11 ./linux_configuration/features/setup_syncyomi.sh setup",
|
||||
"Cold database copy: ~/archive/2026-08-28-syncyomi_safe/pre-local-build/cold/syncyomi.db (schema version 8).",
|
||||
"After rollback, re-run the guard and confirm 2163 manga, then a phone sync round-trip."
|
||||
]
|
||||
}
|
||||
@ -1,44 +0,0 @@
|
||||
{
|
||||
"intent": "token_audit reports what share of API messages carried more than one tool call, so batching discipline is measured by a tested code path instead of an ad-hoc script.",
|
||||
"scope": [
|
||||
"python_pkg/token_audit/{model,parse,attribute,report}.py",
|
||||
"python_pkg/token_audit/tests/{test_attribute,test_report}.py",
|
||||
"Non-goal: changing how turns or cost are counted; reconciled totals are untouched"
|
||||
],
|
||||
"changes": [
|
||||
"Turn gains message_id and tool_calls; the docstring records that the harness writes one JSONL record per tool_use block, all sharing message.id",
|
||||
"attribute.Batching dataclass counts messages/batched/calls grouped by message_id and exposes share and per_message",
|
||||
"report.py renders a Tool-call batching section and persists tool_messages, batched_messages and batched_calls in the snapshot so the next run has a baseline",
|
||||
"Axes was at the 12-attribute pylint cap, so the counters were grouped rather than suppressed; the test helper was split into _tool_turn rather than raising the arg-count limit"
|
||||
],
|
||||
"verification": [
|
||||
{
|
||||
"command": "python3 -m pytest python_pkg/token_audit/tests -q",
|
||||
"result": "pass",
|
||||
"evidence": "122 passed (was 116; 6 new tests cover both Batching branches and the zero-message guard)"
|
||||
},
|
||||
{
|
||||
"command": "python3 -m coverage run --branch --source=python_pkg/token_audit -m pytest python_pkg/token_audit/tests",
|
||||
"result": "pass",
|
||||
"evidence": "TOTAL 609 statements, 140 branches, 0 missing - 100%"
|
||||
},
|
||||
{
|
||||
"command": "pre-commit run --files python_pkg/token_audit/{model,parse,attribute,report}.py python_pkg/token_audit/tests/{test_attribute,test_report}.py",
|
||||
"result": "pass",
|
||||
"evidence": "All hooks pass; ruff/pylint findings were fixed by refactor, no suppressions added"
|
||||
},
|
||||
{
|
||||
"command": "python3 -m python_pkg.token_audit --days 7 --no-write",
|
||||
"result": "pass",
|
||||
"evidence": "Tool-call batching: 439 of 23,650 messages carried >1 call (1.9%), 1.02 calls/message. Reproduces an independent hand count over the same window."
|
||||
}
|
||||
],
|
||||
"risks": [
|
||||
"message_id is empty on records that predate it; those turns are skipped, so the denominator counts only messages that can be grouped",
|
||||
"The autopsy's 7% lifetime batching figure is computed per record, not per message_id, so the two are not directly comparable"
|
||||
],
|
||||
"rollback": [
|
||||
"git revert the commit; the axis is additive and nothing else reads the new fields",
|
||||
"Re-run python3 -m python_pkg.token_audit and confirm WEEKLY.md renders without the batching section"
|
||||
]
|
||||
}
|
||||
@ -1,48 +0,0 @@
|
||||
{
|
||||
"intent": "Run the personal website's browser post editor on the server as a loopback systemd unit, reachable only over an SSH tunnel, with no Caddy route.",
|
||||
"scope": [
|
||||
"linux_configuration/features/lib/website_admin.sh (new)",
|
||||
"linux_configuration/features/lib/tests/test_website_admin.sh (new)",
|
||||
"linux_configuration/features/setup_personal_website.sh (sources the new lib)",
|
||||
"linux_configuration/features/lib/website_build.sh (calls deploy_admin_service and admin_status_lines)",
|
||||
"Non-goal: any public route for the editor; the site stays a static dist/ behind Caddy"
|
||||
],
|
||||
"changes": [
|
||||
"build_website_admin runs `pnpm run build:admin` and fails unless BOTH the service bundle and the editor page exist, because a missing page starts cleanly and serves a blank document.",
|
||||
"ensure_admin_secrets generates a password and a session secret once, stores only the scrypt hash in a 0600 env file, and is idempotent so re-running setup does not rotate credentials under an open tunnel.",
|
||||
"admin_unit_text writes a hardened unit: ProtectSystem=strict, ProtectHome=read-only, ReadWritePaths=<checkout>, and a PATH carrying nvm's node, without which every save fails at the prerender step.",
|
||||
"start_admin_service enables the unit and polls 127.0.0.1:4321/admin, failing loudly instead of reporting a service that never came up."
|
||||
],
|
||||
"verification": [
|
||||
{
|
||||
"command": "pre-commit run --files linux_configuration/features/setup_personal_website.sh linux_configuration/features/lib/website_build.sh linux_configuration/features/lib/website_admin.sh linux_configuration/features/lib/tests/test_website_admin.sh",
|
||||
"result": "pass",
|
||||
"evidence": "All hooks pass, shellcheck included; the 250-line cap holds for all three shell files."
|
||||
},
|
||||
{
|
||||
"command": "meta/scripts/shell_coverage_jail.sh --subject linux_configuration/features/lib/tests/test_website_admin.sh --bind /etc --measure website_admin.sh --min 0 --fail-on-case-error -- \"\"",
|
||||
"result": "pass",
|
||||
"evidence": "website_admin.sh: 53/66 lines = 80.30%; every assertion passed under --fail-on-case-error."
|
||||
},
|
||||
{
|
||||
"command": "systemd-analyze verify <generated unit>",
|
||||
"result": "pass",
|
||||
"evidence": "No findings against the generated unit; the only output was pre-existing warnings about unrelated units on this host."
|
||||
},
|
||||
{
|
||||
"command": "systemd-run --user --wait -p ProtectSystem=strict -p ProtectHome=read-only -p ReadWritePaths=/home/kuhy/src/personal-website ... running the three real build steps",
|
||||
"result": "pass",
|
||||
"evidence": "vite build ok / ssr build ok / prerender ok, and `touch $HOME/.pw-sandbox-should-fail` was refused with 'Read-only file system'. The sandbox permits exactly the checkout."
|
||||
}
|
||||
],
|
||||
"risks": [
|
||||
"The unit has not yet been installed or started on the real server; only its text, its systemd-analyze verdict, and its sandbox behaviour under systemd-run have been checked here.",
|
||||
"The generated password lands in a 0600 file on the server. Anyone who can already read that user's home can read it, which is the same trust boundary as the checkout itself.",
|
||||
"PATH is pinned to whichever node is on PATH at setup time. An nvm version switch that removes that directory breaks the unit until setup is re-run."
|
||||
],
|
||||
"rollback": [
|
||||
"sudo systemctl disable --now personal-website-admin.service && sudo rm /etc/systemd/system/personal-website-admin.service && sudo systemctl daemon-reload",
|
||||
"Revert this commit; the website container, the Caddy snippet and the public site are untouched by it, so the site keeps serving either way.",
|
||||
"Confirm https://kuhy.duckdns.org/ still returns 200 and that nothing listens on 127.0.0.1:4321."
|
||||
]
|
||||
}
|
||||
@ -1,36 +0,0 @@
|
||||
{
|
||||
"intent": "Make kuhy.duckdns.org serve each prerendered blog post's own HTML file instead of falling through to the SPA shell, so a shared post URL has the right title and link preview.",
|
||||
"scope": [
|
||||
"linux_configuration/features/lib/website_build.sh (the inner Caddyfile template only)",
|
||||
"Non-goal: no change to the reverse-proxy snippet, the compose stack, ports, or the build command"
|
||||
],
|
||||
"changes": [
|
||||
"try_files gains the middle term {path}/index.html, so a request for /blog/<slug> resolves to dist/blog/<slug>/index.html",
|
||||
"Added a comment recording why the middle term is load-bearing, because its absence fails silently"
|
||||
],
|
||||
"verification": [
|
||||
{
|
||||
"command": "docker run caddy:2.8 with the old directive, curl each route",
|
||||
"result": "fail",
|
||||
"evidence": "/, /blog, /blog/hello-world and /blog/nope ALL returned <title>Krzysztof Rudnicki</title> — every post served as the landing page"
|
||||
},
|
||||
{
|
||||
"command": "docker run caddy:2.8 with the new directive, curl each route",
|
||||
"result": "pass",
|
||||
"evidence": "/blog -> 'Blog — Krzysztof Rudnicki'; /blog/hello-world -> 'Hello, world — Krzysztof Rudnicki'; /blog/nope still falls back to the SPA shell as intended"
|
||||
},
|
||||
{
|
||||
"command": "shellcheck linux_configuration/features/lib/website_build.sh",
|
||||
"result": "pass",
|
||||
"evidence": "no findings"
|
||||
}
|
||||
],
|
||||
"risks": [
|
||||
"A directory in dist/ that shares a name with a client-side route would now win over the SPA fallback; today only /blog/<slug> directories exist, and they are exactly the ones that should win",
|
||||
"The change only takes effect after setup_personal_website.sh rewrites the managed Caddyfile and restarts the container"
|
||||
],
|
||||
"rollback": [
|
||||
"Restore the single-term try_files line and rerun setup_personal_website.sh",
|
||||
"After rollback, confirm / still serves and accept that blog posts revert to SPA-shell titles"
|
||||
]
|
||||
}
|
||||
@ -1,42 +0,0 @@
|
||||
{
|
||||
"intent": "Make the website deploy apply Caddyfile changes and survive a replaced dist/ directory, instead of reporting a healthy container that serves 404s.",
|
||||
"scope": [
|
||||
"linux_configuration/features/lib/website_build.sh (start_website and wait_for_website only)",
|
||||
"Non-goal: no change to the Gitea stack, the firewall step, the build, or the edge reload"
|
||||
],
|
||||
"changes": [
|
||||
"start_website now runs docker compose up -d --force-recreate; compose cannot detect a bind-mounted Caddyfile's contents changing, nor a bind mount left on an unlinked inode, so it reported 'Running' and applied neither",
|
||||
"wait_for_website records the actual HTTP status instead of using curl -sf, so a failure distinguishes 'no response' from 'answered 404' -- different bugs that had one message",
|
||||
"The 404 message names the two real causes: a stale /srv bind mount, or an empty dist/"
|
||||
],
|
||||
"verification": [
|
||||
{
|
||||
"command": "observed live after setup_personal_website.sh",
|
||||
"result": "fail",
|
||||
"evidence": "Container 'Up 6 hours', Caddy logs healthy, but docker exec ls /srv returned empty and curl 127.0.0.1:8088/ returned 404; dist/ had been replaced with a new inode (84695030) after container start"
|
||||
},
|
||||
{
|
||||
"command": "rm -rf dist && pnpm build, then curl",
|
||||
"result": "fail",
|
||||
"evidence": "Reproduced deterministically: inode 84695030 -> 84695031, container /srv empty, curl -> 404"
|
||||
},
|
||||
{
|
||||
"command": "source website_build.sh && start_website (patched), against that broken state",
|
||||
"result": "pass",
|
||||
"evidence": "Container Recreated; /srv lists assets blog cv.pdf index.html; / -> 200, /blog/hello-world -> 200 with title 'Hello, world — Krzysztof Rudnicki'"
|
||||
},
|
||||
{
|
||||
"command": "shellcheck + bash -n linux_configuration/features/lib/website_build.sh",
|
||||
"result": "pass",
|
||||
"evidence": "no findings"
|
||||
}
|
||||
],
|
||||
"risks": [
|
||||
"Every deploy now restarts the static container, so there is a sub-second window where the loopback port refuses connections; the edge Caddy retries and the wait loop tolerates it",
|
||||
"--force-recreate discards any state inside the container; this service is a read-only static file server and holds none"
|
||||
],
|
||||
"rollback": [
|
||||
"Drop --force-recreate and restore the curl -sf wait loop",
|
||||
"After rollback, verify a Caddyfile edit actually takes effect by curling a prerendered blog post and checking its <title>, since that is what silently regresses"
|
||||
]
|
||||
}
|
||||
@ -45,7 +45,11 @@ from python_pkg.endurain_import.ledger import (
|
||||
file_digest,
|
||||
now_iso,
|
||||
)
|
||||
from python_pkg.endurain_import.sources import inbox_files, pull_from_phone
|
||||
from python_pkg.endurain_import.sources import (
|
||||
inbox_files,
|
||||
processed_dir_for,
|
||||
pull_from_phone,
|
||||
)
|
||||
from python_pkg.endurain_import.upload import (
|
||||
EndurainClient,
|
||||
Outcome,
|
||||
@ -186,7 +190,7 @@ def main() -> int:
|
||||
return 1
|
||||
|
||||
ledger = Ledger(state / "ledger.json")
|
||||
processed_dir = inbox / "processed"
|
||||
processed_dir = processed_dir_for(inbox)
|
||||
|
||||
if os.environ.get("ENDURAIN_NO_ADB") != "1":
|
||||
pull_from_phone(inbox)
|
||||
|
||||
@ -27,6 +27,8 @@ _logger = logging.getLogger(__name__)
|
||||
# RunnerUp's File synchronizer target on the phone.
|
||||
PHONE_EXPORT_DIR = "/sdcard/Documents/RunnerUp"
|
||||
ACTIVITY_SUFFIXES = (".tcx", ".gpx", ".fit")
|
||||
# Subdirectory of the inbox that holds files already delivered to Endurain.
|
||||
PROCESSED_DIRNAME = "processed"
|
||||
_ADB_TIMEOUT = 60
|
||||
# Pins which phone to pull from when more than one device is attached. Without
|
||||
# it every adb call fails with "more than one device/emulator" -- silently, as
|
||||
@ -38,6 +40,15 @@ ADB_SERIAL_ENV = "ENDURAIN_ADB_SERIAL"
|
||||
_ADB_BIN = shutil.which("adb") or "adb"
|
||||
|
||||
|
||||
def processed_dir_for(inbox: Path) -> Path:
|
||||
"""Return where ``inbox`` keeps files this importer has already delivered.
|
||||
|
||||
Single definition shared by the importer (which moves files there) and the
|
||||
phone fallback (which must not pull them back out of the phone again).
|
||||
"""
|
||||
return inbox / PROCESSED_DIRNAME
|
||||
|
||||
|
||||
def inbox_files(inbox: Path) -> list[Path]:
|
||||
"""Return activity files waiting in the WebDAV inbox, oldest first.
|
||||
|
||||
@ -128,9 +139,12 @@ def resolve_serial() -> str | None:
|
||||
def pull_from_phone(inbox: Path) -> list[Path]:
|
||||
"""Copy RunnerUp exports from the phone into ``inbox``.
|
||||
|
||||
Returns the paths newly written. Files already present in the inbox are
|
||||
left alone; content-level deduplication happens later against the ledger,
|
||||
so a name collision here is not authoritative either way.
|
||||
Returns the paths newly written. A file is skipped when its name already
|
||||
sits in the inbox (waiting to be imported) or in ``processed/`` (already
|
||||
imported). The phone keeps every export forever, so without the
|
||||
``processed/`` check each run re-pulls the whole history into the inbox,
|
||||
where it re-triggers every inotify watcher on that directory. Content-level
|
||||
deduplication still happens later against the ledger.
|
||||
"""
|
||||
serial = resolve_serial()
|
||||
if serial is None:
|
||||
@ -148,22 +162,39 @@ def pull_from_phone(inbox: Path) -> list[Path]:
|
||||
if line.strip().lower().endswith(ACTIVITY_SUFFIXES)
|
||||
]
|
||||
inbox.mkdir(parents=True, exist_ok=True)
|
||||
processed = processed_dir_for(inbox)
|
||||
pulled: list[Path] = []
|
||||
pending = imported = failed = 0
|
||||
for name in names:
|
||||
target = inbox / name
|
||||
if target.exists():
|
||||
pending += 1
|
||||
continue
|
||||
if (processed / name).exists():
|
||||
imported += 1
|
||||
continue
|
||||
staged = inbox / f".{name}.partial"
|
||||
ok, err = _adb(["pull", f"{PHONE_EXPORT_DIR}/{name}", str(staged)], serial)
|
||||
if not ok:
|
||||
_logger.warning("adb pull failed for %s: %s", name, err.strip())
|
||||
staged.unlink(missing_ok=True)
|
||||
failed += 1
|
||||
continue
|
||||
# Rename only after a complete pull, so a partial transfer is never
|
||||
# picked up as a whole activity by this or any concurrent run.
|
||||
shutil.move(str(staged), str(target))
|
||||
pulled.append(target)
|
||||
|
||||
if pulled:
|
||||
_logger.info("pulled %d file(s) from the phone", len(pulled))
|
||||
# Failed pulls are each reported above; the summary escalates too so a
|
||||
# partially broken fallback is visible in the one line people read.
|
||||
_logger.log(
|
||||
logging.WARNING if failed else logging.INFO,
|
||||
"phone fallback: %d on phone, %d pulled, %d skipped as already "
|
||||
"imported, %d skipped as already in the inbox, %d failed",
|
||||
len(names),
|
||||
len(pulled),
|
||||
imported,
|
||||
pending,
|
||||
failed,
|
||||
)
|
||||
return pulled
|
||||
|
||||
126
python_pkg/endurain_import/tests/test_phone_pull.py
Normal file
126
python_pkg/endurain_import/tests/test_phone_pull.py
Normal file
@ -0,0 +1,126 @@
|
||||
"""The phone fallback skips what the importer already has, and says so.
|
||||
|
||||
RunnerUp never deletes its exports, so the phone lists every run ever
|
||||
recorded. Each one is either waiting in the inbox, already imported into
|
||||
``processed/``, or genuinely new; only the last kind may be pulled.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from python_pkg.endurain_import import sources
|
||||
|
||||
|
||||
def test_processed_dir_for_is_the_inbox_processed_subdir(tmp_path: Path) -> None:
|
||||
"""Importer and phone fallback agree on one location for imported files."""
|
||||
assert sources.processed_dir_for(tmp_path) == tmp_path / "processed"
|
||||
assert sources.processed_dir_for(tmp_path).name == sources.PROCESSED_DIRNAME
|
||||
|
||||
|
||||
def _phone_listing(
|
||||
names: list[str], *, failing: frozenset[str] = frozenset()
|
||||
) -> tuple[list[str], object]:
|
||||
"""Fake ``_adb`` serving ``names`` and recording every pull it is asked for.
|
||||
|
||||
Pulls of a name in ``failing`` report an adb error; the rest succeed.
|
||||
"""
|
||||
pulls: list[str] = []
|
||||
|
||||
def _adb(args: list[str], _serial: str | None = None) -> tuple[bool, str]:
|
||||
if args[0] == "shell":
|
||||
return True, "".join(f"{n}\n" for n in names)
|
||||
name = args[1].rsplit("/", 1)[-1]
|
||||
pulls.append(name)
|
||||
if name in failing:
|
||||
return False, "remote object does not exist"
|
||||
Path(args[2]).write_text("pulled")
|
||||
return True, ""
|
||||
|
||||
return pulls, _adb
|
||||
|
||||
|
||||
def test_pull_skips_files_already_imported(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
"""The phone keeps every export; imported ones must not be pulled back."""
|
||||
monkeypatch.setattr(sources, "resolve_serial", lambda: "ABC123")
|
||||
pulls, fake = _phone_listing(["old.tcx", "new.tcx"])
|
||||
monkeypatch.setattr(sources, "_adb", fake)
|
||||
processed = sources.processed_dir_for(tmp_path)
|
||||
processed.mkdir()
|
||||
(processed / "old.tcx").write_text("imported last week")
|
||||
|
||||
pulled = sources.pull_from_phone(tmp_path)
|
||||
|
||||
assert [p.name for p in pulled] == ["new.tcx"]
|
||||
assert pulls == ["new.tcx"]
|
||||
assert not (tmp_path / "old.tcx").exists()
|
||||
assert (processed / "old.tcx").read_text() == "imported last week"
|
||||
|
||||
|
||||
def test_pull_does_not_re_pull_a_file_waiting_in_the_inbox(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
monkeypatch.setattr(sources, "resolve_serial", lambda: "ABC123")
|
||||
pulls, fake = _phone_listing(["waiting.tcx"])
|
||||
monkeypatch.setattr(sources, "_adb", fake)
|
||||
(tmp_path / "waiting.tcx").write_text("local copy")
|
||||
|
||||
assert sources.pull_from_phone(tmp_path) == []
|
||||
assert pulls == []
|
||||
assert (tmp_path / "waiting.tcx").read_text() == "local copy"
|
||||
|
||||
|
||||
def test_pull_summary_counts_every_outcome_at_info(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
monkeypatch.setattr(sources, "resolve_serial", lambda: "ABC123")
|
||||
_, fake = _phone_listing(["a.tcx", "b.tcx", "c.tcx", "notes.txt"])
|
||||
monkeypatch.setattr(sources, "_adb", fake)
|
||||
processed = sources.processed_dir_for(tmp_path)
|
||||
processed.mkdir()
|
||||
(processed / "a.tcx").write_text("x")
|
||||
(tmp_path / "b.tcx").write_text("x")
|
||||
|
||||
with caplog.at_level("INFO", logger=sources.__name__):
|
||||
sources.pull_from_phone(tmp_path)
|
||||
|
||||
summaries = [r for r in caplog.records if "phone fallback" in r.getMessage()]
|
||||
assert len(summaries) == 1
|
||||
assert summaries[0].levelname == "INFO"
|
||||
assert summaries[0].getMessage() == (
|
||||
"phone fallback: 3 on phone, 1 pulled, 1 skipped as already imported, "
|
||||
"1 skipped as already in the inbox, 0 failed"
|
||||
)
|
||||
|
||||
|
||||
def test_pull_summary_escalates_to_warning_on_failures(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""A partially broken fallback must be visible in the one summary line."""
|
||||
monkeypatch.setattr(sources, "resolve_serial", lambda: "ABC123")
|
||||
_, fake = _phone_listing(["ok.tcx", "bad.tcx"], failing=frozenset({"bad.tcx"}))
|
||||
monkeypatch.setattr(sources, "_adb", fake)
|
||||
|
||||
with caplog.at_level("INFO", logger=sources.__name__):
|
||||
pulled = sources.pull_from_phone(tmp_path)
|
||||
|
||||
assert [p.name for p in pulled] == ["ok.tcx"]
|
||||
summaries = [r for r in caplog.records if "phone fallback" in r.getMessage()]
|
||||
assert len(summaries) == 1
|
||||
assert summaries[0].levelname == "WARNING"
|
||||
assert (
|
||||
summaries[0]
|
||||
.getMessage()
|
||||
.endswith(
|
||||
"1 pulled, 0 skipped as already imported, "
|
||||
"0 skipped as already in the inbox, 1 failed"
|
||||
)
|
||||
)
|
||||
Loading…
Reference in New Issue
Block a user