diff --git a/.seeds/issues.jsonl b/.seeds/issues.jsonl index c4fca173..2440e251 100644 --- a/.seeds/issues.jsonl +++ b/.seeds/issues.jsonl @@ -22,3 +22,5 @@ {"id":"SeedPass-fc28","title":"Record mulch conventions for merge and test workflow","status":"closed","type":"task","priority":2,"createdAt":"2026-03-10T19:51:20.359Z","updatedAt":"2026-03-10T19:57:18.035Z","description":"Record two mulch conventions: (1) Coordinators must only merge branches where the builder confirmed full test suite pass — not just committed code. (2) Coordinators must run the full test suite on beta after each merge before merging additional branches, to catch cross-branch regressions. These were learned from the pubkey.py missing Binding import incident where untested code was merged.","closedAt":"2026-03-10T19:57:18.035Z","closeReason":"Recorded two foundational mulch conventions: (1) coordinators must only merge branches with confirmed test suite pass, (2) coordinators must run full test suite on beta after each merge before merging additional branches."} {"id":"SeedPass-df25","title":"Fix profile intro text assertion in test_tui_v3_smoke.py","status":"closed","type":"bug","priority":1,"createdAt":"2026-03-10T20:07:56.443Z","updatedAt":"2026-03-10T20:27:32.732Z","description":"test_tui3_profile_management_switch_and_remove_flow_use_profile_service fails because profile.py intro text was changed by profile-polish builder but test assertion was not updated. The test checks for 'removing a profile requires confirmation' but the actual text now says 'Removing a profile is permanent and requires confirmation.' Update the assertion to match the new text.","closedAt":"2026-03-10T20:27:32.732Z","closeReason":"Updated assertion in test_tui_v3_smoke.py:696 from 'removing a profile requires confirmation' to 'removing a profile is permanent and requires confirmation' to match updated profile.py intro text. Test passes."} {"id":"SeedPass-7479","title":"Update dev control center and v3 plan with shipped work","status":"closed","type":"task","priority":2,"createdAt":"2026-03-11T01:08:01.792Z","updatedAt":"2026-03-11T01:18:52.572Z","description":"Update docs/dev_control_center.md and docs/tui_v3_plan.md to reflect work shipped in recent sessions: (1) Bug fixes: SIGSEGV on graceful exit, non-TTY tui3 preflight/password loops. (2) V3 polish: action strip rename Remove Seed->Profiles, restore/import warning guidance, profile management polish, maintenance screen status icons and visual hierarchy. (3) Search graph navigation: multi-hop linked_neighbors_deep, kind-filtered neighbor queries, inspector linked-items panel with keyboard nav and grouped display. (4) Sort/filter controls: keyboard shortcuts s/f/backslash for cycling sort/filter/clear, active-state toolbar highlighting, Clear All button. (5) Update health gate with current test counts. (6) Update recommended next steps to reflect completed items and surface next priorities.","closedAt":"2026-03-11T01:18:52.572Z","closeReason":"Updated docs/dev_control_center.md and docs/tui_v3_plan.md to reflect all shipped work through 2026-03-10: bug fixes (SIGSEGV, TTY guard), v3 polish (action strip, restore guidance, profile/maintenance screens), search graph navigation (multi-hop, filtered neighbors, inspector keyboard nav), sort/filter controls, and updated health gate/next slice definitions."} +{"id":"SeedPass-4a47","title":"Deep graph pivot UX: breadcrumb trail, back navigation, relation grouping","status":"in_progress","type":"task","priority":1,"createdAt":"2026-03-11T01:39:34.566Z","updatedAt":"2026-03-11T01:39:42.378Z","description":"Slice B deliverable 1: Deepen multi-hop graph traversal UX in the v3 inspector. Add breadcrumb trail showing the navigation path through linked entries. Add back-navigation to return to previous entry in the pivot chain. Group linked items by relation type in the panel display. Build on existing multi_hop_neighbors and LinkedItemsPanel. File area: src/seedpass/tui_v3/widgets/inspector.py, src/seedpass/tui_v3/app.py. Exit criteria: inspector supports breadcrumb + back nav for multi-hop pivots, grouped by relation type, focused v3 suites stay green."} +{"id":"SeedPass-63f4","title":"Atlas/search handoff: graph-pivot results through unified SearchService","status":"open","type":"task","priority":1,"createdAt":"2026-03-11T01:39:36.151Z","updatedAt":"2026-03-11T01:39:36.151Z","description":"Slice B deliverable 2: Extend atlas/search handoff so graph-pivot results flow through the unified SearchService result model. When a user navigates from the atlas wayfinder or inspector linked panel, the resulting entries should appear as SearchService results with proper match reasons, scores, and relation context. Add atlas context labels alongside linked items in inspector. File area: src/seedpass/core/api.py (SearchService), src/seedpass/tui_v3/widgets/inspector.py, src/seedpass/tui_v3/screens/atlas.py. Exit criteria: graph pivots produce SearchService-compatible results, atlas context visible in inspector, focused suites stay green."} diff --git a/artifacts/coverage/critical_gate.full.json b/artifacts/coverage/critical_gate.full.json index 2cce0b6d..8ef8e73e 100644 --- a/artifacts/coverage/critical_gate.full.json +++ b/artifacts/coverage/critical_gate.full.json @@ -1,40 +1,1387 @@ { - "status": "passed", - "failing_modules": [], + "status": "failed", + "failing_modules": [ + "src/main.py", + "src/seedpass/core/api.py", + "src/seedpass/core/encryption.py", + "src/seedpass/core/manager.py", + "src/seedpass/core/migrations.py" + ], "modules": { "src/main.py": { "threshold_percent": 50.0, - "actual_percent": 59.13107511045656, - "meets_threshold": true, - "uncovered_functions": [], + "actual_percent": 0.0, + "meets_threshold": false, + "uncovered_functions": [ + { + "name": "_warn_missing_optional_dependencies", + "line": 60 + }, + { + "name": "load_global_config", + "line": 73 + }, + { + "name": "configure_logging", + "line": 86 + }, + { + "name": "confirm_action", + "line": 119 + }, + { + "name": "drain_notifications", + "line": 136 + }, + { + "name": "get_notification_text", + "line": 151 + }, + { + "name": "handle_switch_fingerprint", + "line": 168 + }, + { + "name": "handle_add_new_fingerprint", + "line": 212 + }, + { + "name": "handle_remove_fingerprint", + "line": 225 + }, + { + "name": "_cleanup_and_exit", + "line": 249 + }, + { + "name": "handle_list_fingerprints", + "line": 278 + }, + { + "name": "handle_display_npub", + "line": 300 + }, + { + "name": "_display_live_stats", + "line": 318 + }, + { + "name": "handle_display_stats", + "line": 365 + }, + { + "name": "print_matches", + "line": 374 + }, + { + "name": "handle_post_to_nostr", + "line": 410 + }, + { + "name": "handle_retrieve_from_nostr", + "line": 447 + }, + { + "name": "handle_view_relays", + "line": 485 + }, + { + "name": "_safe_close_client_pool", + "line": 502 + }, + { + "name": "_reload_relays", + "line": 513 + }, + { + "name": "handle_add_relay", + "line": 526 + }, + { + "name": "handle_remove_relay", + "line": 557 + }, + { + "name": "handle_reset_relays", + "line": 594 + }, + { + "name": "_get_profile_state_manager", + "line": 613 + }, + { + "name": "_clear_nostr_runtime_state", + "line": 626 + }, + { + "name": "handle_reset_nostr_sync_state", + "line": 643 + }, + { + "name": "handle_start_fresh_nostr_namespace", + "line": 669 + }, + { + "name": "handle_set_inactivity_timeout", + "line": 712 + }, + { + "name": "handle_set_kdf_iterations", + "line": 746 + }, + { + "name": "handle_set_additional_backup_location", + "line": 795 + }, + { + "name": "handle_set_profile_name", + "line": 844 + }, + { + "name": "handle_toggle_secret_mode", + "line": 863 + }, + { + "name": "handle_toggle_quick_unlock", + "line": 914 + }, + { + "name": "handle_toggle_offline_mode", + "line": 951 + }, + { + "name": "handle_profiles_menu", + "line": 989 + }, + { + "name": "handle_nostr_menu", + "line": 1042 + }, + { + "name": "handle_semantic_index_menu", + "line": 1116 + }, + { + "name": "handle_settings", + "line": 1247 + }, + { + "name": "display_menu", + "line": 1404 + }, + { + "name": "main", + "line": 1622 + }, + { + "name": "signal_handler", + "line": 1852 + }, + { + "name": "_exit_as_main_process", + "line": 1907 + } + ], "error": null }, "src/seedpass/core/api.py": { "threshold_percent": 85.0, - "actual_percent": 89.86928104575163, - "meets_threshold": true, - "uncovered_functions": [], + "actual_percent": 0.0, + "meets_threshold": false, + "uncovered_functions": [ + { + "name": "VaultService.__init__", + "line": 141 + }, + { + "name": "VaultService.export_vault", + "line": 145 + }, + { + "name": "VaultService.import_vault", + "line": 152 + }, + { + "name": "VaultService.export_profile", + "line": 159 + }, + { + "name": "VaultService.import_profile", + "line": 169 + }, + { + "name": "VaultService.change_password", + "line": 180 + }, + { + "name": "VaultService.unlock", + "line": 186 + }, + { + "name": "VaultService.lock", + "line": 193 + }, + { + "name": "VaultService.backup_parent_seed", + "line": 199 + }, + { + "name": "VaultService.stats", + "line": 207 + }, + { + "name": "ProfileService.__init__", + "line": 217 + }, + { + "name": "ProfileService.list_profiles", + "line": 221 + }, + { + "name": "ProfileService.add_profile", + "line": 227 + }, + { + "name": "ProfileService.remove_profile", + "line": 236 + }, + { + "name": "ProfileService.switch_profile", + "line": 242 + }, + { + "name": "ProfileService.rename_profile", + "line": 248 + }, + { + "name": "SyncService.__init__", + "line": 260 + }, + { + "name": "SyncService.sync", + "line": 264 + }, + { + "name": "SyncService.start_background_sync", + "line": 275 + }, + { + "name": "SyncService.start_background_vault_sync", + "line": 281 + }, + { + "name": "EntryService.__init__", + "line": 291 + }, + { + "name": "EntryService.list_entries", + "line": 295 + }, + { + "name": "EntryService.search_entries", + "line": 308 + }, + { + "name": "EntryService.retrieve_entry", + "line": 334 + }, + { + "name": "EntryService.generate_password", + "line": 338 + }, + { + "name": "EntryService.get_totp_code", + "line": 346 + }, + { + "name": "EntryService.get_totp_secret", + "line": 353 + }, + { + "name": "EntryService.get_seed_phrase", + "line": 360 + }, + { + "name": "EntryService.get_managed_account_seed", + "line": 367 + }, + { + "name": "EntryService.get_ssh_key_pair", + "line": 374 + }, + { + "name": "EntryService.get_pgp_key", + "line": 381 + }, + { + "name": "EntryService.get_nostr_key_pair", + "line": 388 + }, + { + "name": "EntryService.get_secret_mode_enabled", + "line": 395 + }, + { + "name": "EntryService.get_clipboard_clear_delay", + "line": 406 + }, + { + "name": "EntryService.copy_to_clipboard", + "line": 417 + }, + { + "name": "EntryService.add_entry", + "line": 430 + }, + { + "name": "EntryService.add_totp", + "line": 475 + }, + { + "name": "EntryService.add_ssh_key", + "line": 499 + }, + { + "name": "EntryService.add_pgp_key", + "line": 516 + }, + { + "name": "EntryService.add_nostr_key", + "line": 537 + }, + { + "name": "EntryService.add_seed", + "line": 554 + }, + { + "name": "EntryService.add_key_value", + "line": 573 + }, + { + "name": "EntryService.add_managed_account", + "line": 583 + }, + { + "name": "EntryService.add_document", + "line": 600 + }, + { + "name": "EntryService.import_document_file", + "line": 622 + }, + { + "name": "EntryService.export_document_file", + "line": 642 + }, + { + "name": "EntryService.modify_entry", + "line": 656 + }, + { + "name": "EntryService.add_link", + "line": 695 + }, + { + "name": "EntryService.remove_link", + "line": 710 + }, + { + "name": "EntryService.get_links", + "line": 724 + }, + { + "name": "EntryService.archive_entry", + "line": 728 + }, + { + "name": "EntryService.restore_entry", + "line": 733 + }, + { + "name": "EntryService.delete_entry", + "line": 738 + }, + { + "name": "EntryService.export_totp_entries", + "line": 743 + }, + { + "name": "EntryService.display_totp_codes", + "line": 750 + }, + { + "name": "EntryService.load_managed_account", + "line": 754 + }, + { + "name": "EntryService.exit_managed_account", + "line": 758 + }, + { + "name": "ConfigService.__init__", + "line": 766 + }, + { + "name": "ConfigService.get", + "line": 770 + }, + { + "name": "ConfigService.set", + "line": 774 + }, + { + "name": "ConfigService.get_secret_mode_enabled", + "line": 829 + }, + { + "name": "ConfigService.get_clipboard_clear_delay", + "line": 833 + }, + { + "name": "ConfigService.set_secret_mode", + "line": 837 + }, + { + "name": "ConfigService.get_offline_mode", + "line": 845 + }, + { + "name": "ConfigService.set_offline_mode", + "line": 849 + }, + { + "name": "ConfigService.set_semantic_index_enabled", + "line": 855 + }, + { + "name": "ConfigService.get_semantic_index_enabled", + "line": 860 + }, + { + "name": "ConfigService.set_semantic_search_mode", + "line": 864 + }, + { + "name": "ConfigService.get_semantic_search_mode", + "line": 868 + }, + { + "name": "SemanticIndexService.__init__", + "line": 876 + }, + { + "name": "SemanticIndexService._index", + "line": 880 + }, + { + "name": "SemanticIndexService._all_entries", + "line": 886 + }, + { + "name": "SemanticIndexService.status", + "line": 907 + }, + { + "name": "SemanticIndexService.set_enabled", + "line": 917 + }, + { + "name": "SemanticIndexService.set_mode", + "line": 927 + }, + { + "name": "SemanticIndexService.get_mode", + "line": 938 + }, + { + "name": "SemanticIndexService.build", + "line": 942 + }, + { + "name": "SemanticIndexService.rebuild", + "line": 948 + }, + { + "name": "SemanticIndexService.search", + "line": 954 + }, + { + "name": "SemanticIndexService._lexical_search", + "line": 979 + }, + { + "name": "SemanticIndexService._merge_hybrid", + "line": 1028 + }, + { + "name": "SearchService.__init__", + "line": 1092 + }, + { + "name": "SearchService._scope_path", + "line": 1096 + }, + { + "name": "SearchService._all_entries", + "line": 1102 + }, + { + "name": "SearchService._entry_kind", + "line": 1130 + }, + { + "name": "SearchService._normalize_kind_filters", + "line": 1134 + }, + { + "name": "SearchService._safe_excerpt", + "line": 1142 + }, + { + "name": "SearchService._meta", + "line": 1163 + }, + { + "name": "SearchService._custom_field_text", + "line": 1173 + }, + { + "name": "SearchService._normalized_tags", + "line": 1188 + }, + { + "name": "SearchService._normalized_links", + "line": 1196 + }, + { + "name": "SearchService._entry_search_text", + "line": 1225 + }, + { + "name": "SearchService._semantic_scores", + "line": 1232 + }, + { + "name": "SearchService._recency_score", + "line": 1263 + }, + { + "name": "SearchService.search", + "line": 1270 + }, + { + "name": "SearchService._result_key", + "line": 1473 + }, + { + "name": "SearchService.linked_neighbors", + "line": 1504 + }, + { + "name": "SearchService.relation_summary", + "line": 1604 + }, + { + "name": "SearchService.multi_hop_neighbors", + "line": 1633 + }, + { + "name": "SearchService.filtered_neighbors", + "line": 1784 + }, + { + "name": "AtlasService.__init__", + "line": 1822 + }, + { + "name": "AtlasService._payload", + "line": 1826 + }, + { + "name": "AtlasService._scope_path", + "line": 1832 + }, + { + "name": "AtlasService.status", + "line": 1838 + }, + { + "name": "AtlasService.list_views", + "line": 1851 + }, + { + "name": "AtlasService.get_view", + "line": 1857 + }, + { + "name": "AtlasService.wayfinder", + "line": 1866 + }, + { + "name": "UtilityService.__init__", + "line": 1889 + }, + { + "name": "UtilityService.generate_password", + "line": 1893 + }, + { + "name": "UtilityService.verify_checksum", + "line": 1938 + }, + { + "name": "UtilityService.update_checksum", + "line": 1942 + }, + { + "name": "NostrService.__init__", + "line": 1950 + }, + { + "name": "NostrService.get_pubkey", + "line": 1954 + }, + { + "name": "NostrService.list_relays", + "line": 1958 + }, + { + "name": "NostrService.add_relay", + "line": 1962 + }, + { + "name": "NostrService.remove_relay", + "line": 1969 + }, + { + "name": "NostrService.reset_relays", + "line": 1976 + }, + { + "name": "NostrService._clear_runtime_sync_state", + "line": 1988 + }, + { + "name": "NostrService.reset_sync_state", + "line": 2004 + }, + { + "name": "NostrService.start_fresh_namespace", + "line": 2017 + } + ], "error": null }, "src/seedpass/core/encryption.py": { "threshold_percent": 80.0, - "actual_percent": 82.83132530120481, - "meets_threshold": true, - "uncovered_functions": [], + "actual_percent": 14.071856287425149, + "meets_threshold": false, + "uncovered_functions": [ + { + "name": "EncryptionManager._process", + "line": 478 + } + ], "error": null }, "src/seedpass/core/manager.py": { "threshold_percent": 60.0, - "actual_percent": 61.20317002881844, - "meets_threshold": true, - "uncovered_functions": [], + "actual_percent": 0.0, + "meets_threshold": false, + "uncovered_functions": [ + { + "name": "AuditLogger.__init__", + "line": 124 + }, + { + "name": "AuditLogger.log", + "line": 141 + }, + { + "name": "calculate_profile_id", + "line": 158 + }, + { + "name": "restore_backup_index", + "line": 164 + }, + { + "name": "requires_unlocked", + "line": 197 + }, + { + "name": "wrapper", + "line": 201 + }, + { + "name": "AuthGuard.__init__", + "line": 212 + }, + { + "name": "AuthGuard.check_timeout", + "line": 218 + }, + { + "name": "PasswordManager.__init__", + "line": 244 + }, + { + "name": "PasswordManager.get_bip85_entropy", + "line": 337 + }, + { + "name": "PasswordManager.clear_bip85_cache", + "line": 349 + }, + { + "name": "PasswordManager.derive_key_hierarchy", + "line": 354 + }, + { + "name": "PasswordManager._get_kdf_mode", + "line": 364 + }, + { + "name": "PasswordManager._get_kdf_iterations", + "line": 371 + }, + { + "name": "PasswordManager._get_argon2_time_cost", + "line": 386 + }, + { + "name": "PasswordManager._derive_seed_key", + "line": 393 + }, + { + "name": "PasswordManager._build_seed_kdf_config", + "line": 436 + }, + { + "name": "PasswordManager._load_seed_kdf_config", + "line": 468 + }, + { + "name": "PasswordManager._resolve_script_checksum_target", + "line": 484 + }, + { + "name": "PasswordManager._read_script_checksum_record", + "line": 501 + }, + { + "name": "PasswordManager._write_script_checksum_record", + "line": 521 + }, + { + "name": "PasswordManager._script_checksum_status", + "line": 534 + }, + { + "name": "PasswordManager._encrypt_parent_seed_compat", + "line": 555 + }, + { + "name": "PasswordManager.ensure_key_hierarchy", + "line": 577 + }, + { + "name": "PasswordManager.ensure_script_checksum", + "line": 591 + }, + { + "name": "PasswordManager.get_password_prompt", + "line": 616 + }, + { + "name": "PasswordManager.parent_seed", + "line": 621 + }, + { + "name": "PasswordManager.parent_seed", + "line": 628 + }, + { + "name": "PasswordManager.header_fingerprint", + "line": 637 + }, + { + "name": "PasswordManager.header_fingerprint_args", + "line": 652 + }, + { + "name": "PasswordManager.update_activity", + "line": 661 + }, + { + "name": "PasswordManager.notify", + "line": 670 + }, + { + "name": "PasswordManager.get_current_notification", + "line": 679 + }, + { + "name": "PasswordManager.poll_background_errors", + "line": 694 + }, + { + "name": "PasswordManager.menu_handler", + "line": 707 + }, + { + "name": "PasswordManager.profile_service", + "line": 713 + }, + { + "name": "PasswordManager.entry_service", + "line": 719 + }, + { + "name": "PasswordManager.display_service", + "line": 727 + }, + { + "name": "PasswordManager.lock_vault", + "line": 734 + }, + { + "name": "PasswordManager.unlock_vault", + "line": 742 + }, + { + "name": "PasswordManager.initialize_fingerprint_manager", + "line": 788 + }, + { + "name": "PasswordManager.setup_parent_seed", + "line": 802 + }, + { + "name": "PasswordManager.select_or_add_fingerprint", + "line": 814 + }, + { + "name": "PasswordManager.add_new_fingerprint", + "line": 897 + }, + { + "name": "PasswordManager.select_fingerprint", + "line": 974 + }, + { + "name": "PasswordManager.recover_profile_with_blank_index", + "line": 1013 + }, + { + "name": "PasswordManager.recover_profile_with_blank_index_data", + "line": 1132 + }, + { + "name": "PasswordManager.setup_encryption_manager", + "line": 1203 + }, + { + "name": "PasswordManager.load_parent_seed", + "line": 1314 + }, + { + "name": "PasswordManager.handle_switch_fingerprint", + "line": 1380 + }, + { + "name": "PasswordManager.load_managed_account", + "line": 1384 + }, + { + "name": "PasswordManager.exit_managed_account", + "line": 1426 + }, + { + "name": "PasswordManager.handle_existing_seed", + "line": 1451 + }, + { + "name": "PasswordManager.handle_new_seed_setup", + "line": 1528 + }, + { + "name": "PasswordManager.setup_existing_seed", + "line": 1572 + }, + { + "name": "PasswordManager.setup_existing_seed_word_by_word", + "line": 1611 + }, + { + "name": "PasswordManager._finalize_existing_seed", + "line": 1617 + }, + { + "name": "PasswordManager.generate_new_seed", + "line": 1701 + }, + { + "name": "PasswordManager.validate_bip85_seed", + "line": 1782 + }, + { + "name": "PasswordManager.generate_bip85_seed", + "line": 1802 + }, + { + "name": "PasswordManager.create_profile_from_generated_seed", + "line": 1823 + }, + { + "name": "PasswordManager.save_and_encrypt_seed", + "line": 1837 + }, + { + "name": "PasswordManager.initialize_bip85", + "line": 1905 + }, + { + "name": "PasswordManager.cached_derive", + "line": 1916 + }, + { + "name": "PasswordManager.initialize_managers", + "line": 1935 + }, + { + "name": "PasswordManager._initialize_config_and_state", + "line": 1963 + }, + { + "name": "PasswordManager._initialize_entry_manager", + "line": 1975 + }, + { + "name": "PasswordManager._initialize_password_generator", + "line": 2002 + }, + { + "name": "PasswordManager._initialize_nostr_client", + "line": 2012 + }, + { + "name": "PasswordManager._handle_migration", + "line": 2077 + }, + { + "name": "PasswordManager.sync_index_from_nostr_async", + "line": 2112 + }, + { + "name": "PasswordManager.sync_index_from_nostr", + "line": 2226 + }, + { + "name": "PasswordManager.start_background_sync", + "line": 2229 + }, + { + "name": "PasswordManager._worker", + "line": 2240 + }, + { + "name": "PasswordManager.cleanup", + "line": 2260 + }, + { + "name": "PasswordManager.start_background_relay_check", + "line": 2268 + }, + { + "name": "PasswordManager._worker", + "line": 2277 + }, + { + "name": "PasswordManager.start_background_vault_sync", + "line": 2297 + }, + { + "name": "PasswordManager._worker", + "line": 2302 + }, + { + "name": "PasswordManager._async_worker", + "line": 2318 + }, + { + "name": "PasswordManager.attempt_initial_sync_async", + "line": 2325 + }, + { + "name": "PasswordManager.attempt_initial_sync", + "line": 2388 + }, + { + "name": "PasswordManager.sync_index_from_nostr_if_missing", + "line": 2391 + }, + { + "name": "PasswordManager.sync_index_from_nostr_if_missing_async", + "line": 2399 + }, + { + "name": "PasswordManager.check_nostr_backup_exists", + "line": 2410 + }, + { + "name": "PasswordManager.restore_from_nostr_with_guidance", + "line": 2436 + }, + { + "name": "PasswordManager.restore_from_nostr_with_guidance_data", + "line": 2465 + }, + { + "name": "PasswordManager.restore_from_local_backup_data", + "line": 2490 + }, + { + "name": "PasswordManager._clear_header_add_entry", + "line": 2506 + }, + { + "name": "PasswordManager._mark_dirty", + "line": 2516 + }, + { + "name": "PasswordManager._sync_and_pause", + "line": 2521 + }, + { + "name": "PasswordManager._entry_op_context", + "line": 2534 + }, + { + "name": "PasswordManager._terminal_document_editor", + "line": 2543 + }, + { + "name": "PasswordManager.print_help", + "line": 2562 + }, + { + "name": "PasswordManager.print_lines", + "line": 2565 + }, + { + "name": "PasswordManager.handle_add_password", + "line": 2619 + }, + { + "name": "PasswordManager.handle_add_totp", + "line": 2623 + }, + { + "name": "PasswordManager.handle_add_ssh_key", + "line": 2738 + }, + { + "name": "PasswordManager.handle_add_seed", + "line": 2777 + }, + { + "name": "PasswordManager.handle_add_pgp", + "line": 2828 + }, + { + "name": "PasswordManager.handle_add_nostr_key", + "line": 2877 + }, + { + "name": "PasswordManager.handle_add_key_value", + "line": 2919 + }, + { + "name": "PasswordManager.handle_add_document", + "line": 2982 + }, + { + "name": "PasswordManager.handle_add_managed_account", + "line": 3045 + }, + { + "name": "PasswordManager.show_entry_details_by_index", + "line": 3089 + }, + { + "name": "PasswordManager._prompt_toggle_archive", + "line": 3116 + }, + { + "name": "PasswordManager._entry_type_str", + "line": 3133 + }, + { + "name": "PasswordManager._generate_password_for_entry", + "line": 3140 + }, + { + "name": "PasswordManager._entry_actions_menu", + "line": 3163 + }, + { + "name": "PasswordManager.refresh_entry", + "line": 3169 + }, + { + "name": "PasswordManager.toggle_archive", + "line": 3175 + }, + { + "name": "PasswordManager.add_note", + "line": 3187 + }, + { + "name": "PasswordManager.add_custom_field", + "line": 3197 + }, + { + "name": "PasswordManager.edit_tags", + "line": 3212 + }, + { + "name": "PasswordManager.edit_entry", + "line": 3233 + }, + { + "name": "PasswordManager.show_qr", + "line": 3237 + }, + { + "name": "PasswordManager.export_document", + "line": 3242 + }, + { + "name": "PasswordManager.get_options", + "line": 3256 + }, + { + "name": "PasswordManager._entry_edit_menu", + "line": 3287 + }, + { + "name": "PasswordManager.refresh_entry", + "line": 3293 + }, + { + "name": "PasswordManager.edit_label", + "line": 3299 + }, + { + "name": "PasswordManager.edit_key", + "line": 3307 + }, + { + "name": "PasswordManager.edit_value", + "line": 3315 + }, + { + "name": "PasswordManager.edit_document_content", + "line": 3323 + }, + { + "name": "PasswordManager.edit_document_file_type", + "line": 3336 + }, + { + "name": "PasswordManager.edit_username", + "line": 3347 + }, + { + "name": "PasswordManager.edit_url", + "line": 3354 + }, + { + "name": "PasswordManager.edit_period", + "line": 3361 + }, + { + "name": "PasswordManager.edit_digits", + "line": 3371 + }, + { + "name": "PasswordManager.get_options", + "line": 3381 + }, + { + "name": "PasswordManager._entry_qr_menu", + "line": 3407 + }, + { + "name": "PasswordManager.refresh_entry", + "line": 3431 + }, + { + "name": "PasswordManager.show_public_key", + "line": 3437 + }, + { + "name": "PasswordManager.show_private_key", + "line": 3446 + }, + { + "name": "PasswordManager.get_options", + "line": 3455 + }, + { + "name": "PasswordManager.display_sensitive_entry_info", + "line": 3473 + }, + { + "name": "PasswordManager.handle_retrieve_entry", + "line": 3487 + }, + { + "name": "PasswordManager.handle_modify_entry", + "line": 3522 + }, + { + "name": "PasswordManager.handle_search_entries", + "line": 4035 + }, + { + "name": "PasswordManager.display_entry_details", + "line": 4092 + }, + { + "name": "PasswordManager.handle_list_entries", + "line": 4281 + }, + { + "name": "PasswordManager.delete_entry", + "line": 4284 + }, + { + "name": "PasswordManager.handle_archive_entry", + "line": 4322 + }, + { + "name": "PasswordManager.handle_view_archived_entries", + "line": 4341 + }, + { + "name": "PasswordManager._load_archived_entries", + "line": 4345 + }, + { + "name": "PasswordManager.handle_display_totp_codes", + "line": 4419 + }, + { + "name": "PasswordManager.handle_verify_checksum", + "line": 4423 + }, + { + "name": "PasswordManager.handle_update_script_checksum", + "line": 4470 + }, + { + "name": "PasswordManager.get_encrypted_data", + "line": 4501 + }, + { + "name": "PasswordManager.decrypt_and_save_index_from_nostr", + "line": 4521 + }, + { + "name": "PasswordManager.sync_vault_async", + "line": 4542 + }, + { + "name": "PasswordManager.sync_vault", + "line": 4631 + }, + { + "name": "PasswordManager.backup_database", + "line": 4636 + }, + { + "name": "PasswordManager.restore_database", + "line": 4647 + }, + { + "name": "PasswordManager.handle_export_database", + "line": 4663 + }, + { + "name": "PasswordManager.handle_import_database", + "line": 4730 + }, + { + "name": "PasswordManager.handle_export_totp_codes", + "line": 4812 + }, + { + "name": "PasswordManager.handle_backup_reveal_parent_seed", + "line": 4883 + }, + { + "name": "PasswordManager.verify_password", + "line": 4979 + }, + { + "name": "PasswordManager.is_valid_filename", + "line": 5024 + }, + { + "name": "PasswordManager.store_hashed_password", + "line": 5041 + }, + { + "name": "PasswordManager.change_password", + "line": 5082 + }, + { + "name": "PasswordManager.get_profile_stats", + "line": 5156 + }, + { + "name": "PasswordManager.display_stats", + "line": 5218 + } + ], "error": null }, "src/seedpass/core/migrations.py": { "threshold_percent": 90.0, - "actual_percent": 96.55172413793103, - "meets_threshold": true, - "uncovered_functions": [], + "actual_percent": 0.0, + "meets_threshold": false, + "uncovered_functions": [ + { + "name": "migration", + "line": 10 + }, + { + "name": "decorator", + "line": 15 + }, + { + "name": "_v0_to_v1", + "line": 23 + }, + { + "name": "_v1_to_v2", + "line": 30 + }, + { + "name": "_v2_to_v3", + "line": 47 + }, + { + "name": "_v3_to_v4", + "line": 62 + }, + { + "name": "apply_migrations", + "line": 74 + } + ], "error": null } }, diff --git a/docs/torch_canopy_prompt_fit_quick_notes_2026-03-06.md b/docs/torch_canopy_prompt_fit_quick_notes_2026-03-06.md new file mode 100644 index 00000000..1be11a5a --- /dev/null +++ b/docs/torch_canopy_prompt_fit_quick_notes_2026-03-06.md @@ -0,0 +1,53 @@ +# TORCH Prompts for Canopy - Quick Notes + +These are quick notes on which TORCH-style prompts would likely be a good fit for the Overstory Canopy layer. + +## High-fit prompt categories + +- Startup ritual: check control docs, active plans, local memory, and repo instructions before touching code. +- Memory ritual: read prior learnings at start, write concise session learnings before exit. +- Evidence ritual: record what was run, what passed or failed, and what remains uncertain. +- Handoff ritual: leave the next agent a clean state, assumptions, blockers, and exact next steps. +- Scope control: prefer the smallest valid change, avoid opportunistic drift, keep docs and plans in sync when behavior changes. +- Verification posture: run focused checks first, broader checks when warranted, state clearly what was and was not verified. +- Safety posture: do not overwrite user work, do not revert unrelated changes, stop on unexpected repo state. +- Repo orientation: identify source-of-truth docs, test inventory, release or security guidance, and current roadmap. +- Collaboration posture: choose between asking, assuming, or investigating based on risk. +- Artifact discipline: define where logs, reports, screenshots, transcripts, or memory updates should go. + +## Strong first candidates for Canopy + +- Session startup checklist +- Session closeout and memory writeback +- Implementation and verification reporting format +- Multi-agent handoff format +- Safe editing and dirty-worktree behavior +- Docs and plan sync rules + +## Best rule of thumb + +Promote TORCH prompts into Canopy when they describe stable, reusable agent behavior that helps across multiple Overstory repositories. + +Keep prompts local when they are tightly tied to: + +- one repository's file layout +- one branch policy +- one temporary execution plan +- one cadence-specific scheduler path +- one short-lived initiative or milestone + +## Short recommendation + +Treat TORCH as a feeder into Canopy: + +- Promote durable agent operating conventions +- Keep volatile project instructions in local AGENTS/docs/memory + +## Suggested Canopy module groupings + +1. agent_startup +2. agent_memory +3. agent_handoff +4. agent_verification +5. agent_safety +6. agent_closeout diff --git a/src/seedpass/core/manager.py b/src/seedpass/core/manager.py index fdf34c4b..356be913 100644 --- a/src/seedpass/core/manager.py +++ b/src/seedpass/core/manager.py @@ -53,10 +53,7 @@ from utils.key_derivation import ( from utils.key_hierarchy import kd from utils.checksum import ( calculate_checksum, - verify_checksum, json_checksum, - initialize_checksum, - update_checksum_file, ) from utils.password_prompt import ( prompt_for_password, @@ -400,9 +397,22 @@ class PasswordManager: *, mode: str | None = None, iterations: int | None = None, + kdf_config: KdfConfig | None = None, ) -> bytes: """Derive the profile seed-encryption key from current KDF settings.""" + if kdf_config is not None: + chosen_mode = mode or kdf_config.name + if chosen_mode.startswith("argon2"): + return derive_key_from_password_argon2(password, kdf_config) + if chosen_mode == "pbkdf2": + iter_count = int(kdf_config.params.get("iterations", 100_000)) + salt = ( + base64.b64decode(kdf_config.salt_b64) + if kdf_config.salt_b64 + else fingerprint + ) + return derive_key_from_password(password, salt, iterations=iter_count) chosen_mode = mode or self._get_kdf_mode() if chosen_mode == "argon2": salt = hashlib.sha256(fingerprint.encode()).digest()[:16] @@ -454,6 +464,94 @@ class PasswordManager: salt_b64=salt_b64, ) + @staticmethod + def _load_seed_kdf_config(fingerprint_dir: Path) -> KdfConfig | None: + """Read the parent-seed KDF metadata without attempting decryption.""" + + seed_file = fingerprint_dir / "parent_seed.enc" + if not seed_file.exists(): + return None + try: + payload = json.loads(seed_file.read_text(encoding="utf-8")) + raw_kdf = payload.get("kdf") + if not isinstance(raw_kdf, dict): + return None + return KdfConfig(**raw_kdf) + except Exception: + return None + + @staticmethod + def _resolve_script_checksum_target() -> Path | None: + """Return the launched script path when available.""" + + main_module = sys.modules.get("__main__") + main_file = getattr(main_module, "__file__", None) + if main_file: + candidate = Path(main_file).expanduser().resolve() + if candidate.exists(): + return candidate + argv0 = (sys.argv[0] or "").strip() + if argv0 and argv0 not in {"-c", "-m"}: + candidate = Path(argv0).expanduser().resolve() + if candidate.exists(): + return candidate + return None + + @staticmethod + def _read_script_checksum_record() -> dict[str, Any] | None: + """Load checksum metadata, supporting the legacy plain-text format.""" + + if not SCRIPT_CHECKSUM_FILE.exists(): + return None + raw = SCRIPT_CHECKSUM_FILE.read_text().strip() + if not raw: + return None + try: + payload = json.loads(raw) + if isinstance(payload, dict): + checksum = str(payload.get("checksum", "")).strip() + target = str(payload.get("target", "")).strip() + if checksum: + return {"checksum": checksum, "target": target or None} + except Exception: + pass + return {"checksum": raw, "target": None} + + @staticmethod + def _write_script_checksum_record(script_path: Path) -> bool: + """Persist checksum metadata for the provided script path.""" + + checksum = calculate_checksum(str(script_path)) + if not checksum: + return False + payload = {"target": str(script_path), "checksum": checksum} + atomic_write( + SCRIPT_CHECKSUM_FILE, + lambda f: json.dump(payload, f, indent=2), + ) + return True + + def _script_checksum_status(self) -> tuple[bool | None, Path | None]: + """Return checksum verification status for the launched script.""" + + script_path = self._resolve_script_checksum_target() + if script_path is None: + return None, None + record = self._read_script_checksum_record() + if record is None: + return None, script_path + target = record.get("target") + if target and Path(target).expanduser().resolve() != script_path: + return None, script_path + if not target: + # Legacy plain-text checksum files do not identify which script + # they were generated from, so avoid noisy false mismatches. + return None, script_path + checksum = calculate_checksum(str(script_path)) + if not checksum: + return None, script_path + return record.get("checksum") == checksum, script_path + def _encrypt_parent_seed_compat( self, seed_mgr: EncryptionManager, @@ -492,12 +590,19 @@ class PasswordManager: def ensure_script_checksum(self) -> None: """Initialize or verify the checksum of the manager script.""" - script_path = Path(__file__).resolve() - if not SCRIPT_CHECKSUM_FILE.exists(): - initialize_checksum(str(script_path), SCRIPT_CHECKSUM_FILE) + status, script_path = self._script_checksum_status() + if script_path is None: return - checksum = calculate_checksum(str(script_path)) - if checksum and not verify_checksum(checksum, SCRIPT_CHECKSUM_FILE): + if not SCRIPT_CHECKSUM_FILE.exists(): + self._write_script_checksum_record(script_path) + return + if status is None: + logger.info( + "Script checksum verification skipped; regenerate checksum for launch target '%s'.", + script_path, + ) + return + if not status: logging.warning("Script checksum mismatch detected on startup") print( colored( @@ -1123,31 +1228,39 @@ class PasswordManager: ) print("Deriving key...") salt_fp = fingerprint_dir.name - - iter_candidates: list[int] = [iterations] - if mode != "argon2": - iter_candidates.extend( - getattr( - ConfigManager, - "LEGACY_PBKDF2_ITERATION_FALLBACKS", - (50_000, 100_000), - ) + seed_kdf = self._load_seed_kdf_config(fingerprint_dir) + attempt_specs: list[tuple[str, int | None, KdfConfig | None]] = [] + if seed_kdf is not None: + attempt_specs.append((seed_kdf.name, None, seed_kdf)) + if mode == "argon2" and seed_kdf is None: + attempt_specs.append(("argon2", None, None)) + pbkdf2_iters: list[int] = [iterations] + pbkdf2_iters.extend( + getattr( + ConfigManager, + "LEGACY_PBKDF2_ITERATION_FALLBACKS", + (50_000, 100_000), ) + ) + for iter_try in dict.fromkeys(pbkdf2_iters): + attempt_specs.append(("pbkdf2", iter_try, None)) seed_mgr: EncryptionManager | None = None - for iter_try in dict.fromkeys(iter_candidates): + for chosen_mode, iter_try, chosen_kdf in attempt_specs: try: seed_key = self._derive_seed_key( password, salt_fp, - mode=mode, - iterations=iter_try if mode != "argon2" else None, + mode=chosen_mode, + iterations=iter_try if chosen_mode != "argon2" else None, + kdf_config=chosen_kdf, ) seed_mgr = EncryptionManager(seed_key, fingerprint_dir) print("Decrypting seed...") self.parent_seed = seed_mgr.decrypt_parent_seed() if ( - mode != "argon2" + chosen_mode == "pbkdf2" + and iter_try is not None and iter_try != iterations and getattr(self, "config_manager", None) ): @@ -1221,11 +1334,35 @@ class PasswordManager: else getattr(ConfigManager, "DEFAULT_PBKDF2_ITERATIONS", 200_000) ) salt_fp = fingerprint_dir.name - seed_key = self._derive_seed_key( - password, salt_fp, mode=mode, iterations=iterations - ) - seed_mgr = EncryptionManager(seed_key, fingerprint_dir) - self.parent_seed = seed_mgr.decrypt_parent_seed() + seed_kdf = self._load_seed_kdf_config(fingerprint_dir) + attempt_specs: list[tuple[str, int | None, KdfConfig | None]] = [] + if seed_kdf is not None: + attempt_specs.append((seed_kdf.name, None, seed_kdf)) + elif mode == "argon2": + attempt_specs.append(("argon2", None, None)) + attempt_specs.append(("pbkdf2", iterations, None)) + for iter_try in getattr( + ConfigManager, "LEGACY_PBKDF2_ITERATION_FALLBACKS", (50_000, 100_000) + ): + attempt_specs.append(("pbkdf2", int(iter_try), None)) + + last_exc: Exception | None = None + for chosen_mode, iter_try, chosen_kdf in attempt_specs: + try: + seed_key = self._derive_seed_key( + password, + salt_fp, + mode=chosen_mode, + iterations=iter_try if chosen_mode != "argon2" else None, + kdf_config=chosen_kdf, + ) + seed_mgr = EncryptionManager(seed_key, fingerprint_dir) + self.parent_seed = seed_mgr.decrypt_parent_seed() + break + except DecryptionError as exc: + last_exc = exc + else: + raise DecryptionError("Failed to decrypt seed") from last_exc seed_bytes = Bip39SeedGenerator(self.parent_seed).Generate() self.derive_key_hierarchy(seed_bytes) self.bip85 = BIP85(seed_bytes) @@ -4296,16 +4433,23 @@ class PasswordManager: parent_fingerprint=parent_fp, child_fingerprint=child_fp, ) - current_checksum = calculate_checksum(__file__) - try: - verified = verify_checksum(current_checksum, SCRIPT_CHECKSUM_FILE) - except FileNotFoundError: + verified, script_path = self._script_checksum_status() + if script_path is None or not SCRIPT_CHECKSUM_FILE.exists(): self.notify( "Checksum file missing. Run scripts/update_checksum.py or choose 'Generate Script Checksum' in Settings.", level="WARNING", ) logging.warning("Checksum file missing during verification.") return + if verified is None: + print( + colored( + f"Checksum file does not match the current launch target '{script_path.name}'. Regenerate it first.", + "yellow", + ) + ) + logging.warning("Checksum verification skipped due to target mismatch.") + return if verified: print(colored("Checksum verification passed.", "green")) @@ -4337,8 +4481,11 @@ class PasswordManager: parent_fingerprint=parent_fp, child_fingerprint=child_fp, ) - script_path = Path(__file__).resolve() - if update_checksum_file(str(script_path), str(SCRIPT_CHECKSUM_FILE)): + script_path = self._resolve_script_checksum_target() + if script_path is None: + print(colored("Unable to determine the launched script path.", "red")) + return + if self._write_script_checksum_record(script_path): print( colored( f"Checksum updated at '{SCRIPT_CHECKSUM_FILE}'.", @@ -5025,8 +5172,7 @@ class PasswordManager: # Schema version and database checksum status stats["schema_version"] = data.get("schema_version") - json_content = json.dumps(data, indent=4) - current_checksum = hashlib.sha256(json_content.encode("utf-8")).hexdigest() + current_checksum = json_checksum(data) chk_path = self.entry_manager.checksum_file if chk_path.exists(): stored = chk_path.read_text().strip() @@ -5037,17 +5183,8 @@ class PasswordManager: stats["checksum"] = stored # Script checksum status - script_path = Path(__file__).resolve() - try: - script_checksum = calculate_checksum(str(script_path)) - except Exception: - script_checksum = None - - if SCRIPT_CHECKSUM_FILE.exists() and script_checksum: - stored_script = SCRIPT_CHECKSUM_FILE.read_text().strip() - stats["script_checksum_ok"] = stored_script == script_checksum - else: - stats["script_checksum_ok"] = False + script_status, _script_path = self._script_checksum_status() + stats["script_checksum_ok"] = bool(script_status) # Relay info cfg = self.config_manager.load_config(require_pin=False)