Interactive Menu
When invoked interactively, present a two-level menu. See references/interactive-menu.md for the full menu structure.
When to Use
- Google Drive cleanup and organization
- Duplicate file detection and merging
- Folder structure optimization
- Preference-based auto-organization rules
- Drive health monitoring and reporting
Bower
Bower keeps Google Drive organized without ever deleting anything. It learns your organizational style from your existing structure, applies domain-native logic where it detects known domains, builds a personalized preference profile, and over time auto-approves patterns you consistently accept. The goal: you go to sleep and wake up to a Drive that looks the way you would have organized it yourself.
Current status (2026-07-19): Weekly deep-scan cadence established. Drive has ~23,699 total folders; 6 curated root folders (Bookshelf, Archive, Home, Projects, Professional, Authenticator Backups) holding ~516 direct files. The ~23.7K folders are a large nested backup/Takeout tree (dominant subtree Archive with 9,692 descendant folders) — out of Bower's reorganization scope (Bower organizes, never deletes). Deep scans are sampled (scan_coverage: 0.5): enumerate all folders once (cached), sample 300 direct children per curated root. 2 prescriptive domains detected (projects, home). Auto-approval has not yet triggered — needs proposal review/approval. 16 pending move proposals remain from 2026-07-14, most of which are title-keyword false positives (see Gotchas). Drive root clean (0 loose files). Main value: monitoring drift, confirming health, and executing reviewed proposals.
Trigger conditions
- "Organize my Drive"
- "Clean up my Google Drive"
- "What's disorganized in my Drive?"
- "Show me what Bower found" / "Run a Drive scan"
- "Apply the pending Bower proposals"
- "What has Bower learned about my preferences?"
- "What would you do to this folder?" / "Simulate Bower on my Projects folder"
- "Turn on quiet mode" / "Run silently"
- Bower's background scan job fires on schedule
When NOT to Use
- Deleting files — Bower never deletes
- Managing sharing permissions — Bower doesn't touch permissions
- Creating top-level taxonomy from scratch — Bower infers from existing structure
- Interacting with non-Drive storage — Bower is Drive-only
- Applying domain logic to undetected domains — needs 5+ files or 2+ subfolders to activate
- Web research or document analysis — use Sift
- Chronicle ingestion
Responsibility boundary
Bower does: scan Drive structure and file contents, build a preference profile from evidence, detect and apply domain-specific organization logic, identify outliers, propose folder moves, renames, and description writes, auto-approve promoted patterns, apply approved changes using the system's Google Drive access, maintain a full audit trail.
Adjacent responsibility: Sift handles web research and document analysis. Bower emits entity signals in journal payloads for Chronicle ingestion for all Drive artifacts and entities encountered during scans.
Ontology types
- Thing/DigitalArtifact — Drive files and folders that Bower scans, classifies, and organizes. Bower includes signals in journal payloads for all discovered Drive artifacts.
- Entity/Person — People referenced in documents, shared-with metadata, and collaborators encountered during scans.
- Place — Locations found in documents (travel documents, address lists, venue information).
- Concept/Event — Events, projects, or topics that documents are about (e.g., a folder of wedding planning docs, a project kickoff deck).
- Concept/Idea — Themes and topics reflected by folder structure and document content (e.g., recurring interest in machine learning across multiple folders).
Signal emission
Bower includes structured signals in journal payloads for all entities and artifacts encountered during scans. All signals carry user_relevance: "user". Five signal types are emitted: Thing/DigitalArtifact, Entity/Person, Place, Concept/Event, Concept/Idea. One signal per unique artifact/entity, deduplicated by file_id (artifacts) or email (persons). Signals are written to the signal payload field during bower.scan.deep and bower.scan.light.
For full JSON schema examples, see references/signal_examples.md.
Commands
| Command |
Summary |
bower.scan.deep |
Full Drive crawl, folder-by-folder. --founding for first use. --analyze-now for early results. |
bower.scan.light |
Incremental scan of recent changes. Arrival detection + auto-apply if quiet mode. |
bower.analyze |
Domain logic + generic rules → ranked proposals. Read-only. |
bower.simulate |
Read-only scan of a folder. Shows what Bower would do. |
bower.proposals.review |
List pending proposals by folder, confidence, domain. |
bower.proposals.approve |
Approve a subset. Requires explicit scope. |
bower.proposals.reject |
Reject proposals. Suppresses patterns. |
bower.apply |
Execute approved proposals. --dry-run to preview. |
bower.undo |
Reverse moves, renames, description writes. |
bower.preferences.show |
Display preference profile. |
bower.preferences.lock |
Mark a preference field or pattern as fixed (prevents auto-inference from overwriting it). |
bower.preferences.quiet |
Toggle quiet mode (suppresses digest only). |
bower.feedback.clear |
Clear suppression patterns or demotions. |
bower.status |
SkillStatus summary. --trend for 8-week health. |
bower.init |
First-use initialization. |
Full flag descriptions and semantics: references/command_reference.md
Workflow
The Bower organization pipeline: scan → analyze → propose → apply → learn.
- Scan Drive structure and file contents
- Analyze with domain-specific logic (taxes by year, projects by name, etc.)
- Propose non-destructive moves/renames
- Apply approved changes
- Learn from accepted patterns for auto-approval
Execution flow
First use (founding run)
bower.init → bower.scan.deep --founding (Phase 1: tree discovery; Phase 2: scan folders one at a time, resume across sessions) → bower.analyze → present high-confidence proposals as batch → if accepted: bower.apply. Founding run batch approval grants immediate pattern promotion credit. Use --analyze-now for early results before all folders scanned.
Steady state
Daily light scan at 02:00 PT: bower.scan.light → arrival detection → auto-apply promoted high-confidence matches if quiet mode on. Weekly deep scan Sunday 01:00 PT: run the sampled deep scan (commons/data/ocas-bower/deep_scan_sampled.py — NOT full enumeration) → bower.analyze (run scripts/bower_analyze.py, which reads the canonical data dir) → emit Drive health signal to Vesper. Silent unless something needs attention. On this ~24K-folder Drive the weekly deep scan stays sampled; it never attempts full enumeration.
Running scans on this host (verified recipe)
The canonical scan scripts live under the indigo profile data dir, NOT the skill's own scripts/. Use these exact commands (verified 2026-07-24):
- Light scan:
/usr/bin/python3 $HERMES_HOME/../indigo/commons/data/ocas-bower/run_light_scan.py
- Interpreter:
/usr/bin/python3 (3.14) — has BOTH googleapiclient and requests. A stray python3 on PATH (a project .venv, 3.13) lacks requests and produces a false auth_or_build_failed (see Gotchas).
- Credentials:
<gworkspace-creds>/credentials/<user-google-email>.json, read by scripts/google_auth.py → get_service. The script inserts $HERMES_HOME/../indigo/scripts onto sys.path itself, so run it from any cwd.
- Exit 0 + JSON
"status": "OK" = success. Artifacts: light_scan_latest.json, appended scan_events.jsonl / evidence.jsonl, and an Observation Journal under commons/journals/ocas-bower/YYYY-MM-DD/.
- Deep scan (weekly, sampled):
/usr/bin/python3 $HERMES_HOME/../indigo/commons/data/ocas-bower/deep_scan_sampled.py (use the sampled script, never scripts/bower_full_scan.py).
Never trust a search_files 0 results for google_auth.py — the ripgrep-backed index has returned phantom relative paths and missed real files under the profile tree. If a dependency check fails, confirm with find ~/.hermes -name 'google_auth*' and absolute ls before concluding auth is broken (see Gotchas).
Arrival detection
After every light scan, for each new/modified file: classify → check pattern_key against auto_approved_patterns. High-confidence match: generate approved proposal (auto-apply if quiet mode). Medium-confidence: pending. No match: normal pending.
Simulation
Read-only scan of specified folder → apply full analysis pipeline → print narrative report. No proposals, logs, journals, or state changes written. See references/organization_rules.md for simulation output format.
Apply run
Description auto-writes first → sort by confidence tier → apply apply_cap → per-proposal staleness check → execute via Google Drive → log to move_log.jsonl → produce digest (suppressed in quiet mode if all succeeded) → write Action Journal.
Verification after apply: After bower.apply completes, read back applied proposal IDs from move_log.jsonl and confirm each file exists at its new destination via Google Drive list. Report any mismatches (file not found at destination) as failed moves. Verify the move log entry count matches the number of executed proposals.
Undo run
Read move log records → staleness check → restore previous_value → execute reversal → log to undo_log.jsonl → record feedback → trigger pattern demotion if auto-approved → write Action Journal.
Decision model
Read these reference files before the operations they govern:
| File |
When to read |
references/organization_rules.md |
Before every bower.analyze run; defines preference inference, pattern promotion, taxonomy inference, all proposal generation rules, permission lookup, feedback suppression, recalibration, scan resume, cap behavior, digest format, and review narrative |
references/domains.md |
Before every bower.analyze run; defines domain detection, prescriptive/descriptive mode, canonical structures, and per-domain filing rules for Taxes, Projects, Home, Finance, Legal, Medical, Archive, Education |
references/analysis_schema.md |
Before bower.scan.deep or bower.analyze; defines all data schemas including preference profile, folder_index, scan_progress, proposals, move log, undo log, feedback log, and config |
See references/decision-invariants.md for the full list of safety invariants.
Light scan structural baseline check (MANDATORY):
Before running the modifiedTime query in bower.scan.light, ALWAYS perform a structural baseline comparison:
Why this matters (2026-06-14 incident): A Drive restructuring placed 89+ files and 12+ folders at root level. All files had modifiedTime dates before the last scan's cutoff, so the modifiedTime query returned 0 results. The drift was invisible to the light scan. Only a root-level count comparison caught it. Without this check, the light scan would have reported "no new files" while the Drive was completely restructured.
Cron implementation: The pageSize=10 query is fast (~2s). If nextPageToken is present, root has 100+ items — immediately compare against baseline. Do not wait for full pagination.
Scan output
bower.scan.deep produces: folder_index.json (Phase 1), scans/{folder_id}.json per folder tree (Phase 2), drive_digest.json (updated per folder), scan_progress.json, scan event in scan_events.jsonl.
bower.scan.light produces: updated scans/{folder_id}.json files, scan event with drift_rate (aborts if drift exceeds threshold).
bower.analyze produces: outlier report in analysis_events.jsonl, expired proposals marked in proposals.jsonl, new proposals appended with status: pending and expires_at.
Google Drive access
Bower uses Google Drive access for: list files/folders, read file content, move file to folder, rename file/folder, create folder, update file description. Bower never calls delete operations. Phase 1 lists all folders (fast metadata query). Phase 2 processes one folder tree at a time, capturing: id, name, mimeType, parents, modifiedTime, starred, size, trashed, description. Exclude trashed files. Fetch permissions for each folder; if unavailable, set permissions_available: false and suppress all move proposals.
Background tasks
| Job |
Schedule |
Action |
bower:scan |
Daily 02:00 PT |
bower.scan.light → arrival detection → auto-apply promoted matches if quiet mode on |
bower:weekly-deep |
Sunday 01:00 PT |
bower.scan.deep → bower.analyze → emit Drive health signal to Vesper |
Register during bower.init. Check for existing scheduled tasks before registering to avoid duplicates. All cron jobs use sessionTarget: isolated, lightContext: true, wakeMode: next-heartbeat.
Vesper Drive health signal
Emitted weekly after Sunday deep scan as an InsightProposal with proposal_type: routine_prediction containing: Drive health score delta, files organized in past 7 days, active auto-approved patterns, domains that gained/lost structure, suppressed outlier classes worth surfacing. Vesper decides whether to include it in the weekly briefing.
Optional skill cooperation
- Vesper — Bower emits a weekly Drive health InsightProposal after each Sunday deep scan. If Vesper is absent, the signal is dropped silently.
- Chronicle — Bower emits structured signals in journal payloads for all Drive artifacts and entities encountered during scans.
- Mentor — Bower's journals are evaluated by Mentor for OKR scoring. No action required from Bower.
Inter-skill interfaces
Bower emits to:
- the
briefing payload field — weekly Drive health InsightProposal (Sunday deep scan only)
- the
signal payload field — entity and artifact signals for all Drive content (every scan)
Bower receives from: none.
Journal outputs
Scan commands (bower.scan.deep, bower.scan.light) and bower.analyze emit Observation Journals. bower.apply and bower.undo emit Action Journals.
All Observation Journals from scan commands include entities_observed, relationships_observed, and preferences_observed in decision.payload. Journal path: {agent_root}/commons/journals/ocas-bower/YYYY-MM-DD/{run_id}.json.
Recovery Behavior
Implements the recovery contract from spec-ocas-recovery.md.
- Evidence: Every scheduled run writes to
evidence.jsonl, including no-op runs. not_activity_reason is mandatory when no side effects occur.
- Gap detection: On every wake, checks evidence log for most recent completed run. If gap exceeds cadence (24h light, 7d deep), logs
gap_detected and runs a compact remedial pass.
- Degraded mode: When Google Drive access fails, enters degraded mode and produces a partial Drive health report. Evidence log records
degraded: google_drive.
- Log compaction: Evidence and decision logs older than 30 days (no-op) or 90 days (error/gap) compacted to weekly summaries. Escalation records never auto-deleted. Last 7 days of raw entries always retained.
Storage layout
See references/storage-layout.md for the full directory structure.
OKRs
See references/okrs.md for all targets (folder coverage, proposal accuracy, user preference learning, schedule adherence, data integrity).
Tracked metrics: proposal_precision (≥0.80), apply_success_rate (≥0.95), staleness_skip_rate (≤0.05), auto_approve_precision (≥0.90), false_positive_rate (≤0.10), scan_coverage (1.0), proposal_expiry_rate (≤0.20), plus tracking-only: content_influence_rate, description_coverage_rate, domain_proposal_rate, feedback_suppression_rate.
Initialization
bower.init: creates data/journal directories, writes config.json with defaults, registers cron jobs bower:scan and bower:weekly-deep (check platform registry first to avoid duplicates).
Self-update
bower.update pulls the latest package from the source: URL in frontmatter. Compares local vs. remote version via GitHub API. If different: downloads tarball, extracts, replaces. Retries once on failure. Output: I updated Bower from version {old} to {new}. Silent if already current.
Visibility
public
Gotchas
Stale data after major Drive changes — Between scans, the Drive may be cleaned up, migrated, or restructured catastrophically (e.g., 381K files → 17). When a deep scan detects a >50% change in total file/folder count compared to scan_progress.json or drive_digest.json, treat the previous scan data as stale: reset scan_progress.json to phase: complete with the new counts, update drive_digest.json with new totals, and add a scan_notes field documenting the change. Do NOT carry forward old proposals — the old proposals.jsonl records reference file/folders that may no longer exist. Let the new scan drive fresh proposals. Optionally archive old scan data (scans/, proposals.jsonl) to a dated archive directory.
Cron jobs cannot use execute_code — Scheduled cron runs (light and deep scans on this profile) execute in an isolated context where execute_code is blocked. All scan logic must use native Hermes tools (List Google Drive files, Search Google Drive, write_file, terminal with >> for .jsonl append). Do not write Python scripts that expect to be run via execute_code for scheduled work. The scripts/ directory is for interactive/scripted runs only.
Small Drive efficiency — On Drives with <500 total files, the modifiedTime query may return mostly batch-imported content (e.g., 97 books imported at once). Group by timestamp to identify batch imports vs. real user activity. See references/scan-debug.md → "Small Drive light scan efficiency" for the triage pattern.
Drift threshold aborts light scans — If the light scan detects significant structural drift, it aborts entirely rather than producing partial results. A subsequent deep scan is needed to re-establish the baseline.
Staleness checks execute per-proposal — Even auto-approved, high-confidence proposals pass through a staleness check immediately before execution. A file moved between scan and apply can cause a proposal to quietly skip.
Permission fetch suppresses all move proposals — If folder permissions are unavailable (API error or scope missing), Bower suppresses all move proposals for that folder—not just the affected files—and falls back to description-only suggestions.
Simulation writes absolutely nothing — bower.simulate produces no proposals, logs, journals, or state changes. It is safe to run repeatedly but provides no persistent output.
Medical file redaction — Medical folder contents are never logged, journaled, or surfaced by filename. Only folder paths and file counts appear in apply digests and simulation output.
Quiet mode suppresses only the digest — Enabling quiet mode hides the apply digest output but does not bypass approval requirements, staleness checks, or any safety gate.
Small Drive below domain thresholds — When the Drive has fewer than 5 files or 2 subfolders total, no domain logic activates. Analysis falls entirely on generic outlier rules (depth outliers, name inconsistencies). This is expected — report the Drive as "too small for domain detection" and focus proposals on obvious misplacements (files at root that belong in named folders, duplicate filenames).
Shared files appear in modifiedTime queries — The Drive API modifiedTime filter returns shared files/folders that were recently modified by their owners, even though they're outside the user's Drive tree. These appear with parents: null and ownedByMe: False. Always check ownedByMe and parent location before generating proposals. Shared files are never actionable by Bower. See references/scan-debug.md → "Shared files in modifiedTime results" for the full triage pattern.
Light scan misses bulk-moved files — The bower.scan.light queries by modifiedTime, which only catches files modified since the last scan. Files that were bulk-moved or bulk-created without recent modification timestamps are invisible to this query. The mandatory structural baseline check (root-level count comparison) before the modifiedTime query catches this. Without it, a completely restructured Drive can be reported as "no new files." See the "Light scan structural baseline check" section above.
write_file overwrites — use terminal >> for .jsonl append — The write_file tool always overwrites the entire file. For append-only logs (scan_events.jsonl, evidence.jsonl, move_log.jsonl, undo_log.jsonl, feedback_log.jsonl, proposals.jsonl, health_history.jsonl, decisions.jsonl, intents.jsonl, analysis_events.jsonl), use terminal with >> to append, or build the full content and write once. Accidentally overwriting these files destroys history. When appending scan events or evidence entries, prefer: terminal > command: "cat >> path.jsonl << 'EOF'\n{...}\nEOF" . Never use write_file on a .jsonl unless you intend to replace the entire file.
Drive file/folder IDs are 33 chars — never truncate — A valid Drive ID looks like 1uBwL8OJ-XrXaBo4Uv9niZ_Qdx3JaqWHS (33 chars). If you print/echo/copy an ID and it gets truncated to ~24 (a common terminal wrap or manual copy slip), a later files().get() returns HttpError 404 File not found. The file is not missing — your truncated ID is wrong. Always copy the full 33-char ID verbatim. Confirmed 2026-07-17: five parent lookups 404'd solely due to truncated IDs; the real IDs resolved all 21 arrivals correctly. See references/light-scan-triage.md for the full triage recipe (resolving light_scan_latest.json parent IDs to folder names + grouping arrivals).
OAuth invalid_grant — two distinct causes, only one is fatal — invalid_grant: Bad Request surfaces as either (a) a permanently dead/revoked refresh token (no recovery short of user re-auth), OR (b) a recoverable client_id/refresh-token mismatch: a script loads a cached token file whose embedded client_id differs from the client_id it passes when constructing Credentials. Google rejects the token as issued for another client. Symptom of (b): the deep scan works but the light scan fails — because deep uses get_service (which always pairs the right client secret with the right client_id from _CLIENTS[account]), while the broken light-scan script hand-builds Credentials with a hardcoded, mismatched client_id. Confirmed 2026-06-29 → 2026-07-14: light scans died nightly for 16 days while deep scans succeeded. Fix for (b): route the scan through get_service; never construct Credentials from a token file plus a separate hardcoded client_id. See references/cron-drive-fallback.md (ALWAYS-use-get_service note). For (a), handle at the scan entry point: catch RefreshError/HTTPError 401, write degraded: google_drive to evidence.jsonl, write an aborted scan event to scan_events.jsonl, enter degraded mode, report, and do NOT retry within the same run. If get_service() raises RuntimeError ("No valid Google credentials found"), that is condition (a) through a different path — handle identically.
Interpretter / requests missing looks like invalid_grant — run_light_scan.py imports get_service from $HERMES_HOME/../indigo/scripts/google_auth.py, which does import requests at module load. If the python3 the cron/shell invokes lacks requests (e.g. it resolves to a project .venv whose site-packages only has googleapiclient), the scan logs auth_or_build_failed with No module named 'requests' — which looks EXACTLY like an OAuth failure but is NOT. Tell them apart: the error string is No module named 'requests' and no HTTP 400 invalid_grant: Bad Request appears. Fix: invoke the script with an interpreter that has BOTH googleapiclient and requests. On this host the working interpreter is /usr/bin/python3 (3.14); a stray python3 on PATH (a project .venv, 3.13) did not. Always confirm python3 -c "import googleapiclient, requests" succeeds before trusting a cron run. A auth_or_build_failed that recurs nightly is the classic signature of this mismatch (cf. the 16-day June 2026 invalid_grant episode — same degraded output, different root cause). See references/cron-drive-fallback.md → "Operational pitfalls".
search_files can return phantom paths and miss real files under the profile tree — During the 2026-07-24 light scan, search_files returned relative phantom paths (<fs-root>/commons/... that didn't exist from cwd) AND a literal 0 results for google_auth.py under ~/.hermes, even though find ~/.hermes -name 'google_auth*' proved the file at $HERMES_HOME/../indigo/scripts/google_auth.py (the ls/find were truncated or symlink-indexed). This almost caused a false "DEGRADED: auth module missing" conclusion. When a dependency/locator check returns empty or suspicious, DO NOT trust search_files alone: confirm with find ~/.hermes -name '<file>' and an absolute-path ls before concluding anything is missing. The ripgrep-backed index appears to miss files under nested profile dirs and to emit cwd-relative paths.
Light-scan query window repeats until the next deep scan — run_light_scan.py sets cutoff = drive_digest.json["last_updated"], which is updated ONLY by bower.scan.deep (weekly Sunday run). It is NOT "since the last light scan" or "since yesterday." Consequence: between deep scans, the SAME set of arrivals recurs on every light scan. Identical owned/shared counts day-to-day = "no NEW activity since the last deep scan," NOT a stuck or duplicating scan. Do not conclude the scan is broken when consecutive days return the same owned arrivals — the window simply never advanced. New activity only surfaces after the next deep scan resets last_updated. (This is also why a light scan is the wrong tool to detect "what arrived today" — use it for drift-safety + the standing disorganization among already-known arrivals. Run scripts/bower_light_triage.py to turn those arrivals into a report.)
Timestamp-folder false positives in triage — when flagging YYYY-MM-DD_HH-MM-SS-style export/checkpoint folder piles, use the strict regex ^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}$. A naive name[:4].isdigit() test false-positives on legitimately-named folders like 2442 Kuhio Avenue 702 (a Real Estate subfolder). scripts/bower_light_triage.py already uses the strict regex.
Large Drive founding scans timeout — On Drives with 10K+ items, a full founding deep scan can exceed the 10-minute cron timeout during folder/file enumeration, and the Drive API may return 500 Internal Errors under heavy pagination. Solution: use the sampled deep scan (commons/data/ocas-bower/deep_scan_sampled.py): enumerate all folders once (cached to folder_full_cache.json for reuse), then sample up to 300 direct children per curated root. Set scan_coverage: 0.5 in drive_digest.json. See references/large-drive-scanning.md. The weekly bower:weekly-deep run ALSO uses the sampled strategy — it never attempts full enumeration on this ~24K-folder Drive. Always wrap Drive API list calls with exponential backoff for 500/503 errors.
Bundled scripts/ scan scripts are unsafe for this Drive — scripts/bower_full_scan.py (the skill's own deep-scan script) does a FULL file enumeration (times out on ~24K folders) AND builds its Drive service via the hardcoded-credential get_drive_service() path that triggers invalid_grant (see cron-drive-fallback). For real scans, run the maintained scripts in the canonical data dir: commons/data/ocas-bower/deep_scan_sampled.py (deep) and run_light_scan.py (light). These use get_service, honor drift/stale-data handling, and write analyze-compatible artifacts. scripts/bower_analyze.py is usable (it reads the canonical commons/data/ocas-bower dir) but is stale — prefer it over bower_full_scan.py, and never use bower_full_scan.py for scan runs.
location_outlier move proposals are title-keyword false-positive prone — bower.analyze flags a file as location_outlier when its name contains a domain keyword (home/work/project/house) and it sits outside that domain's root. For books in Bookshelf this is almost always WRONG: e.g. "Learn Hawaiian at Home", "American House Styles", "Work Like A Spy", "Project Hail Mary" are ebooks, not Home/Projects documents. Do NOT auto-apply such proposals. Rule of thumb: never move contents of an already-curated semantic root (Bookshelf, Archive) to another domain based on title-substring matches alone — require file content/type or explicit domain-folder membership. (Also seen: duplicate proposals — the same file listed twice, e.g. Project Hail Mary. The analyzer should dedupe by source_id.) Review the pending queue (bower.proposals.review) and reject the false positives before any bower.apply.
Weekly deep scan yields 0 NEW proposals when prior ones persist — bower_analyze.py dedupes against existing pending/approved proposals, so a routine weekly deep scan legitimately produces 0 new proposals even when 16 are still pending. That is NOT a sign the scan failed. The deliverable of a weekly deep scan is the refreshed folder_index.json + drive_digest.json (which advances the light-scan modifiedTime cutoff) + the still-pending queue — not new proposals. Report "0 new, N pending" as healthy.
Stale digest causes permanent drift abort loop — drive_digest.json can get a wrong baseline if a light scan incorrectly concludes that a large-drift event "reverted to baseline." Subsequent scans compare against the wrong baseline and permanently detect 95%+ drift, aborting every run and generating no proposals. Diagnosis: root_level_file_count and root_level_folder_count in drive_digest.json don't match what the Drive API actually returns. Fix: query the Drive API directly (mimeType = 'application/vnd.google-apps.folder' and parentId = 'root' for folders; parentId = 'root' and mimeType != 'application/vnd.google-apps.folder' for files), then update drive_digest.json with the real counts, clear scan_progress.json's drift_detected: true flag, and update folder_index.json with the actual folder IDs. Never write a digest update concluding drift has "reverted" unless you verified the root-level counts match the previous baseline. Confirmed June 2026.
Support File Map
| File |
When to read |
references/organization_rules.md |
Before every bower.analyze run; defines preference inference, pattern promotion, taxonomy inference, all proposal generation rules, permission lookup, feedback suppression, recalibration, scan resume, cap behavior, digest format, and review narrative |
references/domains.md |
Before every bower.analyze run; defines domain detection, prescriptive/descriptive mode, canonical structures, and per-domain filing rules for Taxes, Projects, Home, Finance, Legal, Medical, Archive, Education |
references/analysis_schema.md |
Before bower.scan.deep or bower.analyze; defines all data schemas including preference profile, folder_index, scan_progress, proposals, move log, undo log, feedback log, and config |
references/signal_examples.md |
Before emitting signals; JSON schema examples for all five signal types |
references/scan-debug.md |
When debugging scan issues, resume failures, or light scan anomalies |
references/command_reference.md |
When you need full command flag descriptions and semantics |
references/decision-invariants.md |
Before every bower.analyze or bower.apply run; safety invariants that govern all operations |
references/large-drive-scanning.md |
When founding deep scan on Drive with >10K items or cron timeout; sampled scan strategy, 500 error handling, trade-offs |
commons/data/ocas-bower/*.py (canonical data dir) |
The RUNNABLE scan/analyze scripts: deep_scan_sampled.py (weekly deep, sampled), run_light_scan.py (light), plus artifacts (folder_index.json, scans/, proposals.jsonl, drive_digest.json, evidence.jsonl). Skill scripts/ holds stale/unsafe equivalents — see Gotchas. |
references/drift-incident-2026-06-14.md |
When light scan detects major drift; lessons from 2026-06-14 restructuring incident, why modifiedTime-only queries miss bulk moves |
references/light-scan-triage.md |
After a light scan — resolving parent IDs to folder names and grouping arrivals into a disorganization report (the cron deliverable); includes the 33-char-ID truncation 404 pitfall |
references/interactive-menu.md |
When invoked interactively; the full two-level menu structure |
references/cron-drive-fallback.md |
When a scheduled scan hits OAuth/auth failures or you're wiring cron Drive access; ALWAYS-use-get_service note and operational pitfalls |
references/mcp-drive-tooling.md |
When driving Drive operations through MCP tooling instead of the python client |
references/spec-ocas-recovery.md |
Before implementing or auditing Recovery Behavior; the shared OCAS recovery contract (evidence, gap detection, degraded mode, compaction) |
references/okrs.md |
When reporting status/trend or reviewing targets; full OKR definitions |
Scan Debug & Operational Notes
Debug procedures, resume patterns, file layout facts, and light scan lessons.
Full documentation: references/scan-debug.md
1---2name: ocas-bower3description: Automatic Google Drive organizer. Scans Drive structure and file contents, builds a personalized preference profile, applies domain-specific logic (taxes by year, projects by name, home by system, finance by institution), and executes non-destructive moves, renames, and description writes. Learns organizational style over time and auto-approves consistently accepted patterns. Never deletes files. NOT for web research, document analysis, or Chronicle ingestion.4license: MIT5---67## Interactive Menu89When invoked interactively, present a two-level menu. See `references/interactive-menu.md` for the full menu structure.1011## When to Use1213- Google Drive cleanup and organization14- Duplicate file detection and merging15- Folder structure optimization16- Preference-based auto-organization rules17- Drive health monitoring and reporting1819# Bower2021Bower keeps Google Drive organized without ever deleting anything. It learns your organizational style from your existing structure, applies domain-native logic where it detects known domains, builds a personalized preference profile, and over time auto-approves patterns you consistently accept. The goal: you go to sleep and wake up to a Drive that looks the way you would have organized it yourself.2223**Current status (2026-07-19):** Weekly deep-scan cadence established. Drive has ~23,699 total folders; 6 curated root folders (Bookshelf, Archive, Home, Projects, Professional, Authenticator Backups) holding ~516 direct files. The ~23.7K folders are a large nested backup/Takeout tree (dominant subtree `Archive` with 9,692 descendant folders) — out of Bower's reorganization scope (Bower organizes, never deletes). Deep scans are **sampled** (`scan_coverage: 0.5`): enumerate all folders once (cached), sample 300 direct children per curated root. 2 prescriptive domains detected (projects, home). Auto-approval has not yet triggered — needs proposal review/approval. 16 pending move proposals remain from 2026-07-14, most of which are title-keyword false positives (see Gotchas). Drive root clean (0 loose files). Main value: monitoring drift, confirming health, and executing reviewed proposals.2425## Trigger conditions2627- "Organize my Drive"28- "Clean up my Google Drive"29- "What's disorganized in my Drive?"30- "Show me what Bower found" / "Run a Drive scan"31- "Apply the pending Bower proposals"32- "What has Bower learned about my preferences?"33- "What would you do to this folder?" / "Simulate Bower on my Projects folder"34- "Turn on quiet mode" / "Run silently"35- Bower's background scan job fires on schedule3637## When NOT to Use3839- Deleting files — Bower never deletes40- Managing sharing permissions — Bower doesn't touch permissions41- Creating top-level taxonomy from scratch — Bower infers from existing structure42- Interacting with non-Drive storage — Bower is Drive-only43- Applying domain logic to undetected domains — needs 5+ files or 2+ subfolders to activate44- Web research or document analysis — use Sift45- Chronicle ingestion4647## Responsibility boundary4849Bower does: scan Drive structure and file contents, build a preference profile from evidence, detect and apply domain-specific organization logic, identify outliers, propose folder moves, renames, and description writes, auto-approve promoted patterns, apply approved changes using the system's Google Drive access, maintain a full audit trail.5051Adjacent responsibility: Sift handles web research and document analysis. Bower emits entity signals in journal payloads for Chronicle ingestion for all Drive artifacts and entities encountered during scans.5253## Ontology types5455- **Thing/DigitalArtifact** — Drive files and folders that Bower scans, classifies, and organizes. Bower includes signals in journal payloads for all discovered Drive artifacts.56- **Entity/Person** — People referenced in documents, shared-with metadata, and collaborators encountered during scans.57- **Place** — Locations found in documents (travel documents, address lists, venue information).58- **Concept/Event** — Events, projects, or topics that documents are about (e.g., a folder of wedding planning docs, a project kickoff deck).59- **Concept/Idea** — Themes and topics reflected by folder structure and document content (e.g., recurring interest in machine learning across multiple folders).6061## Signal emission6263Bower includes structured signals in journal payloads for all entities and artifacts encountered during scans. All signals carry `user_relevance: "user"`. Five signal types are emitted: Thing/DigitalArtifact, Entity/Person, Place, Concept/Event, Concept/Idea. One signal per unique artifact/entity, deduplicated by `file_id` (artifacts) or email (persons). Signals are written to the `signal` payload field during `bower.scan.deep` and `bower.scan.light`.6465For full JSON schema examples, see `references/signal_examples.md`.6667## Commands6869| Command | Summary |70|---------|---------|71| `bower.scan.deep` | Full Drive crawl, folder-by-folder. `--founding` for first use. `--analyze-now` for early results. |72| `bower.scan.light` | Incremental scan of recent changes. Arrival detection + auto-apply if quiet mode. |73| `bower.analyze` | Domain logic + generic rules → ranked proposals. Read-only. |74| `bower.simulate` | Read-only scan of a folder. Shows what Bower would do. |75| `bower.proposals.review` | List pending proposals by folder, confidence, domain. |76| `bower.proposals.approve` | Approve a subset. Requires explicit scope. |77| `bower.proposals.reject` | Reject proposals. Suppresses patterns. |78| `bower.apply` | Execute approved proposals. `--dry-run` to preview. |79| `bower.undo` | Reverse moves, renames, description writes. |80| `bower.preferences.show` | Display preference profile. |81| `bower.preferences.lock` | Mark a preference field or pattern as fixed (prevents auto-inference from overwriting it). |82| `bower.preferences.quiet` | Toggle quiet mode (suppresses digest only). |83| `bower.feedback.clear` | Clear suppression patterns or demotions. |84| `bower.status` | SkillStatus summary. `--trend` for 8-week health. |85| `bower.init` | First-use initialization. |8687Full flag descriptions and semantics: `references/command_reference.md`8889## Workflow9091The Bower organization pipeline: **scan → analyze → propose → apply → learn**.92931. Scan Drive structure and file contents942. Analyze with domain-specific logic (taxes by year, projects by name, etc.)953. Propose non-destructive moves/renames964. Apply approved changes975. Learn from accepted patterns for auto-approval9899## Execution flow100101### First use (founding run)102`bower.init` → `bower.scan.deep --founding` (Phase 1: tree discovery; Phase 2: scan folders one at a time, resume across sessions) → `bower.analyze` → present high-confidence proposals as batch → if accepted: `bower.apply`. Founding run batch approval grants immediate pattern promotion credit. Use `--analyze-now` for early results before all folders scanned.103104### Steady state105Daily light scan at 02:00 PT: `bower.scan.light` → arrival detection → auto-apply promoted high-confidence matches if quiet mode on. Weekly deep scan Sunday 01:00 PT: run the **sampled** deep scan (`commons/data/ocas-bower/deep_scan_sampled.py` — NOT full enumeration) → `bower.analyze` (run `scripts/bower_analyze.py`, which reads the canonical data dir) → emit Drive health signal to Vesper. Silent unless something needs attention. On this ~24K-folder Drive the weekly deep scan stays sampled; it never attempts full enumeration.106107### Running scans on this host (verified recipe)108109The canonical scan scripts live under the indigo profile data dir, NOT the skill's own `scripts/`. Use these exact commands (verified 2026-07-24):110111- **Light scan:** `/usr/bin/python3 $HERMES_HOME/../indigo/commons/data/ocas-bower/run_light_scan.py`112 - Interpreter: `/usr/bin/python3` (3.14) — has BOTH `googleapiclient` and `requests`. A stray `python3` on PATH (a project `.venv`, 3.13) lacks `requests` and produces a false `auth_or_build_failed` (see Gotchas).113 - Credentials: `<gworkspace-creds>/credentials/<user-google-email>.json`, read by `scripts/google_auth.py` → `get_service`. The script inserts `$HERMES_HOME/../indigo/scripts` onto `sys.path` itself, so run it from any cwd.114 - Exit 0 + JSON `"status": "OK"` = success. Artifacts: `light_scan_latest.json`, appended `scan_events.jsonl` / `evidence.jsonl`, and an Observation Journal under `commons/journals/ocas-bower/YYYY-MM-DD/`.115- **Deep scan (weekly, sampled):** `/usr/bin/python3 $HERMES_HOME/../indigo/commons/data/ocas-bower/deep_scan_sampled.py` (use the sampled script, never `scripts/bower_full_scan.py`).116117Never trust a `search_files` `0 results` for `google_auth.py` — the ripgrep-backed index has returned phantom relative paths and missed real files under the profile tree. If a dependency check fails, confirm with `find ~/.hermes -name 'google_auth*'` and absolute `ls` before concluding auth is broken (see Gotchas).118119### Arrival detection120After every light scan, for each new/modified file: classify → check `pattern_key` against `auto_approved_patterns`. High-confidence match: generate `approved` proposal (auto-apply if quiet mode). Medium-confidence: `pending`. No match: normal `pending`.121122### Simulation123Read-only scan of specified folder → apply full analysis pipeline → print narrative report. No proposals, logs, journals, or state changes written. See `references/organization_rules.md` for simulation output format.124125### Apply run126Description auto-writes first → sort by confidence tier → apply `apply_cap` → per-proposal staleness check → execute via Google Drive → log to `move_log.jsonl` → produce digest (suppressed in quiet mode if all succeeded) → write Action Journal.127128**Verification after apply**: After `bower.apply` completes, read back applied proposal IDs from `move_log.jsonl` and confirm each file exists at its new destination via Google Drive list. Report any mismatches (file not found at destination) as failed moves. Verify the move log entry count matches the number of executed proposals.129130### Undo run131Read move log records → staleness check → restore `previous_value` → execute reversal → log to `undo_log.jsonl` → record feedback → trigger pattern demotion if auto-approved → write Action Journal.132133## Decision model134135Read these reference files before the operations they govern:136137| File | When to read |138|------|-------------|139| `references/organization_rules.md` | Before every `bower.analyze` run; defines preference inference, pattern promotion, taxonomy inference, all proposal generation rules, permission lookup, feedback suppression, recalibration, scan resume, cap behavior, digest format, and review narrative |140| `references/domains.md` | Before every `bower.analyze` run; defines domain detection, prescriptive/descriptive mode, canonical structures, and per-domain filing rules for Taxes, Projects, Home, Finance, Legal, Medical, Archive, Education |141| `references/analysis_schema.md` | Before `bower.scan.deep` or `bower.analyze`; defines all data schemas including preference profile, folder_index, scan_progress, proposals, move log, undo log, feedback log, and config |142143See `references/decision-invariants.md` for the full list of safety invariants.144145**Light scan structural baseline check (MANDATORY):**146147Before running the `modifiedTime` query in `bower.scan.light`, ALWAYS perform a structural baseline comparison:148149- [ ] Query root-level items: `GOOGLEDRIVE_FIND_FILE` with `q="'root' in parents and trashed = false"`, `pageSize=10`150- [ ] Compare counts against `drive_digest.json` → `root_level_file_count` and `root_level_folder_count`151- [ ] If root-level counts differ by more than 15% from the stored baseline, **flag structural drift immediately** — before processing the `modifiedTime` query results152- [ ] If drift exceeds threshold, abort the light scan, write `drift_detected` to `scan_events.jsonl`, and request a deep scan153154**Why this matters (2026-06-14 incident):** A Drive restructuring placed 89+ files and 12+ folders at root level. All files had `modifiedTime` dates before the last scan's cutoff, so the `modifiedTime` query returned 0 results. The drift was invisible to the light scan. Only a root-level count comparison caught it. Without this check, the light scan would have reported "no new files" while the Drive was completely restructured.155156**Cron implementation:** The `pageSize=10` query is fast (~2s). If `nextPageToken` is present, root has 100+ items — immediately compare against baseline. Do not wait for full pagination.157158## Scan output159160`bower.scan.deep` produces: `folder_index.json` (Phase 1), `scans/{folder_id}.json` per folder tree (Phase 2), `drive_digest.json` (updated per folder), `scan_progress.json`, scan event in `scan_events.jsonl`.161162`bower.scan.light` produces: updated `scans/{folder_id}.json` files, scan event with drift_rate (aborts if drift exceeds threshold).163164`bower.analyze` produces: outlier report in `analysis_events.jsonl`, expired proposals marked in `proposals.jsonl`, new proposals appended with `status: pending` and `expires_at`.165166## Google Drive access167168Bower uses Google Drive access for: list files/folders, read file content, move file to folder, rename file/folder, create folder, update file description. Bower never calls delete operations. Phase 1 lists all folders (fast metadata query). Phase 2 processes one folder tree at a time, capturing: id, name, mimeType, parents, modifiedTime, starred, size, trashed, description. Exclude trashed files. Fetch permissions for each folder; if unavailable, set `permissions_available: false` and suppress all move proposals.169170## Background tasks171172| Job | Schedule | Action |173|-----|----------|--------|174| `bower:scan` | Daily 02:00 PT | `bower.scan.light` → arrival detection → auto-apply promoted matches if quiet mode on |175| `bower:weekly-deep` | Sunday 01:00 PT | `bower.scan.deep` → `bower.analyze` → emit Drive health signal to Vesper |176177Register during `bower.init`. Check for existing scheduled tasks before registering to avoid duplicates. All cron jobs use `sessionTarget: isolated`, `lightContext: true`, `wakeMode: next-heartbeat`.178179### Vesper Drive health signal180181Emitted weekly after Sunday deep scan as an InsightProposal with `proposal_type: routine_prediction` containing: Drive health score delta, files organized in past 7 days, active auto-approved patterns, domains that gained/lost structure, suppressed outlier classes worth surfacing. Vesper decides whether to include it in the weekly briefing.182183## Optional skill cooperation184185- **Vesper** — Bower emits a weekly Drive health InsightProposal after each Sunday deep scan. If Vesper is absent, the signal is dropped silently.186- **Chronicle** — Bower emits structured signals in journal payloads for all Drive artifacts and entities encountered during scans.187- **Mentor** — Bower's journals are evaluated by Mentor for OKR scoring. No action required from Bower.188189## Inter-skill interfaces190191Bower emits to:192- the `briefing` payload field — weekly Drive health InsightProposal (Sunday deep scan only)193- the `signal` payload field — entity and artifact signals for all Drive content (every scan)194195Bower receives from: none.196197## Journal outputs198199Scan commands (`bower.scan.deep`, `bower.scan.light`) and `bower.analyze` emit **Observation Journals**. `bower.apply` and `bower.undo` emit **Action Journals**.200201All Observation Journals from scan commands include `entities_observed`, `relationships_observed`, and `preferences_observed` in `decision.payload`. Journal path: `{agent_root}/commons/journals/ocas-bower/YYYY-MM-DD/{run_id}.json`.202203## Recovery Behavior204205Implements the recovery contract from `spec-ocas-recovery.md`.206207- **Evidence**: Every scheduled run writes to `evidence.jsonl`, including no-op runs. `not_activity_reason` is mandatory when no side effects occur.208- **Gap detection**: On every wake, checks evidence log for most recent completed run. If gap exceeds cadence (24h light, 7d deep), logs `gap_detected` and runs a compact remedial pass.209- **Degraded mode**: When Google Drive access fails, enters degraded mode and produces a partial Drive health report. Evidence log records `degraded: google_drive`.210- **Log compaction**: Evidence and decision logs older than 30 days (no-op) or 90 days (error/gap) compacted to weekly summaries. Escalation records never auto-deleted. Last 7 days of raw entries always retained.211212## Storage layout213214See `references/storage-layout.md` for the full directory structure.215216## OKRs217218See `references/okrs.md` for all targets (folder coverage, proposal accuracy, user preference learning, schedule adherence, data integrity).219220Tracked metrics: `proposal_precision` (≥0.80), `apply_success_rate` (≥0.95), `staleness_skip_rate` (≤0.05), `auto_approve_precision` (≥0.90), `false_positive_rate` (≤0.10), `scan_coverage` (1.0), `proposal_expiry_rate` (≤0.20), plus tracking-only: `content_influence_rate`, `description_coverage_rate`, `domain_proposal_rate`, `feedback_suppression_rate`.221222## Initialization223224`bower.init`: creates data/journal directories, writes `config.json` with defaults, registers cron jobs `bower:scan` and `bower:weekly-deep` (check platform registry first to avoid duplicates).225226## Self-update227228`bower.update` pulls the latest package from the `source:` URL in frontmatter. Compares local vs. remote version via GitHub API. If different: downloads tarball, extracts, replaces. Retries once on failure. Output: `I updated Bower from version {old} to {new}`. Silent if already current.229230## Visibility231232public233234## Gotchas235236- **Stale data after major Drive changes** — Between scans, the Drive may be cleaned up, migrated, or restructured catastrophically (e.g., 381K files → 17). When a deep scan detects a >50% change in total file/folder count compared to `scan_progress.json` or `drive_digest.json`, treat the previous scan data as stale: reset `scan_progress.json` to `phase: complete` with the new counts, update `drive_digest.json` with new totals, and add a `scan_notes` field documenting the change. Do NOT carry forward old proposals — the old `proposals.jsonl` records reference file/folders that may no longer exist. Let the new scan drive fresh proposals. Optionally archive old scan data (`scans/`, `proposals.jsonl`) to a dated archive directory.237- **Cron jobs cannot use `execute_code`** — Scheduled cron runs (light and deep scans on this profile) execute in an isolated context where `execute_code` is blocked. All scan logic must use native Hermes tools (List Google Drive files, Search Google Drive, `write_file`, `terminal` with `>>` for `.jsonl` append). Do not write Python scripts that expect to be run via `execute_code` for scheduled work. The `scripts/` directory is for interactive/scripted runs only.238- **Small Drive efficiency** — On Drives with <500 total files, the modifiedTime query may return mostly batch-imported content (e.g., 97 books imported at once). Group by timestamp to identify batch imports vs. real user activity. See `references/scan-debug.md` → "Small Drive light scan efficiency" for the triage pattern.239- **Drift threshold aborts light scans** — If the light scan detects significant structural drift, it aborts entirely rather than producing partial results. A subsequent deep scan is needed to re-establish the baseline.240- **Staleness checks execute per-proposal** — Even auto-approved, high-confidence proposals pass through a staleness check immediately before execution. A file moved between scan and apply can cause a proposal to quietly skip.241- **Permission fetch suppresses all move proposals** — If folder permissions are unavailable (API error or scope missing), Bower suppresses *all* move proposals for that folder—not just the affected files—and falls back to description-only suggestions.242- **Simulation writes absolutely nothing** — `bower.simulate` produces no proposals, logs, journals, or state changes. It is safe to run repeatedly but provides no persistent output.243- **Medical file redaction** — Medical folder contents are never logged, journaled, or surfaced by filename. Only folder paths and file counts appear in apply digests and simulation output.244- **Quiet mode suppresses only the digest** — Enabling quiet mode hides the apply digest output but does not bypass approval requirements, staleness checks, or any safety gate.245- **Small Drive below domain thresholds** — When the Drive has fewer than 5 files or 2 subfolders total, no domain logic activates. Analysis falls entirely on generic outlier rules (depth outliers, name inconsistencies). This is expected — report the Drive as "too small for domain detection" and focus proposals on obvious misplacements (files at root that belong in named folders, duplicate filenames).246247- **Shared files appear in modifiedTime queries** — The Drive API `modifiedTime` filter returns shared files/folders that were recently modified by their owners, even though they're outside the user's Drive tree. These appear with `parents: null` and `ownedByMe: False`. Always check `ownedByMe` and parent location before generating proposals. Shared files are never actionable by Bower. See `references/scan-debug.md` → "Shared files in modifiedTime results" for the full triage pattern.248249- **Light scan misses bulk-moved files** — The `bower.scan.light` queries by `modifiedTime`, which only catches files *modified* since the last scan. Files that were bulk-moved or bulk-created without recent modification timestamps are invisible to this query. The mandatory structural baseline check (root-level count comparison) before the `modifiedTime` query catches this. Without it, a completely restructured Drive can be reported as "no new files." See the "Light scan structural baseline check" section above.250251- **write_file overwrites — use terminal >> for .jsonl append** — The `write_file` tool always overwrites the entire file. For append-only logs (`scan_events.jsonl`, `evidence.jsonl`, `move_log.jsonl`, `undo_log.jsonl`, `feedback_log.jsonl`, `proposals.jsonl`, `health_history.jsonl`, `decisions.jsonl`, `intents.jsonl`, `analysis_events.jsonl`), use `terminal` with `>>` to append, or build the full content and write once. Accidentally overwriting these files destroys history. When appending scan events or evidence entries, prefer: `terminal` > `command: "cat >> path.jsonl << 'EOF'\n{...}\nEOF"` . Never use `write_file` on a `.jsonl` unless you intend to replace the entire file.252253- **Drive file/folder IDs are 33 chars — never truncate** — A valid Drive ID looks like `1uBwL8OJ-XrXaBo4Uv9niZ_Qdx3JaqWHS` (33 chars). If you print/echo/copy an ID and it gets truncated to ~24 (a common terminal wrap or manual copy slip), a later `files().get()` returns `HttpError 404 File not found`. The file is **not** missing — your truncated ID is wrong. Always copy the full 33-char ID verbatim. Confirmed 2026-07-17: five parent lookups 404'd solely due to truncated IDs; the real IDs resolved all 21 arrivals correctly. See `references/light-scan-triage.md` for the full triage recipe (resolving `light_scan_latest.json` parent IDs to folder names + grouping arrivals).254- **OAuth invalid_grant — two distinct causes, only one is fatal** — `invalid_grant: Bad Request` surfaces as either (a) a *permanently* dead/revoked refresh token (no recovery short of user re-auth), OR (b) a *recoverable* client_id/refresh-token mismatch: a script loads a cached token file whose embedded `client_id` differs from the `client_id` it passes when constructing `Credentials`. Google rejects the token as issued for another client. Symptom of (b): the *deep* scan works but the *light* scan fails — because deep uses `get_service` (which always pairs the right client secret with the right client_id from `_CLIENTS[account]`), while the broken light-scan script hand-builds `Credentials` with a hardcoded, mismatched `client_id`. Confirmed 2026-06-29 → 2026-07-14: light scans died nightly for 16 days while deep scans succeeded. **Fix for (b):** route the scan through `get_service`; never construct `Credentials` from a token file plus a separate hardcoded client_id. See `references/cron-drive-fallback.md` (ALWAYS-use-get_service note). For (a), handle at the scan entry point: catch `RefreshError`/`HTTPError 401`, write `degraded: google_drive` to `evidence.jsonl`, write an aborted scan event to `scan_events.jsonl`, enter degraded mode, report, and do NOT retry within the same run. If `get_service()` raises `RuntimeError` ("No valid Google credentials found"), that is condition (a) through a different path — handle identically.255- **Interpretter / `requests` missing looks like `invalid_grant`** — `run_light_scan.py` imports `get_service` from `$HERMES_HOME/../indigo/scripts/google_auth.py`, which does `import requests` at module load. If the `python3` the cron/shell invokes lacks `requests` (e.g. it resolves to a project `.venv` whose site-packages only has `googleapiclient`), the scan logs `auth_or_build_failed` with `No module named 'requests'` — which looks EXACTLY like an OAuth failure but is NOT. Tell them apart: the error string is `No module named 'requests'` and no HTTP 400 `invalid_grant: Bad Request` appears. Fix: invoke the script with an interpreter that has BOTH `googleapiclient` and `requests`. On this host the working interpreter is `/usr/bin/python3` (3.14); a stray `python3` on PATH (a project `.venv`, 3.13) did not. Always confirm `python3 -c "import googleapiclient, requests"` succeeds before trusting a cron run. A `auth_or_build_failed` that recurs nightly is the classic signature of this mismatch (cf. the 16-day June 2026 `invalid_grant` episode — same degraded output, different root cause). See `references/cron-drive-fallback.md` → "Operational pitfalls".256- **`search_files` can return phantom paths and miss real files under the profile tree** — During the 2026-07-24 light scan, `search_files` returned relative phantom paths (`<fs-root>/commons/...` that didn't exist from cwd) AND a literal `0 results` for `google_auth.py` under `~/.hermes`, even though `find ~/.hermes -name 'google_auth*'` proved the file at `$HERMES_HOME/../indigo/scripts/google_auth.py` (the `ls`/`find` were truncated or symlink-indexed). This almost caused a false "DEGRADED: auth module missing" conclusion. When a dependency/locator check returns empty or suspicious, DO NOT trust `search_files` alone: confirm with `find ~/.hermes -name '<file>'` and an absolute-path `ls` before concluding anything is missing. The ripgrep-backed index appears to miss files under nested profile dirs and to emit cwd-relative paths.257- **Light-scan query window repeats until the next deep scan** — `run_light_scan.py` sets `cutoff = drive_digest.json["last_updated"]`, which is updated ONLY by `bower.scan.deep` (weekly Sunday run). It is NOT "since the last light scan" or "since yesterday." Consequence: between deep scans, the SAME set of arrivals recurs on every light scan. Identical owned/shared counts day-to-day = "no NEW activity since the last deep scan," NOT a stuck or duplicating scan. Do not conclude the scan is broken when consecutive days return the same owned arrivals — the window simply never advanced. New activity only surfaces after the next deep scan resets `last_updated`. (This is also why a light scan is the wrong tool to detect "what arrived today" — use it for drift-safety + the standing disorganization among already-known arrivals. Run `scripts/bower_light_triage.py` to turn those arrivals into a report.)258- **Timestamp-folder false positives in triage** — when flagging `YYYY-MM-DD_HH-MM-SS`-style export/checkpoint folder piles, use the strict regex `^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}$`. A naive `name[:4].isdigit()` test false-positives on legitimately-named folders like `2442 Kuhio Avenue 702` (a Real Estate subfolder). `scripts/bower_light_triage.py` already uses the strict regex.259260- **Large Drive founding scans timeout** — On Drives with 10K+ items, a full founding deep scan can exceed the 10-minute cron timeout during folder/file enumeration, and the Drive API may return 500 Internal Errors under heavy pagination. Solution: use the **sampled deep scan** (`commons/data/ocas-bower/deep_scan_sampled.py`): enumerate all folders once (cached to `folder_full_cache.json` for reuse), then sample up to 300 direct children per curated root. Set `scan_coverage: 0.5` in `drive_digest.json`. See `references/large-drive-scanning.md`. **The weekly `bower:weekly-deep` run ALSO uses the sampled strategy** — it never attempts full enumeration on this ~24K-folder Drive. Always wrap Drive API list calls with exponential backoff for 500/503 errors.261262- **Bundled `scripts/` scan scripts are unsafe for this Drive** — `scripts/bower_full_scan.py` (the skill's own deep-scan script) does a FULL file enumeration (times out on ~24K folders) AND builds its Drive service via the hardcoded-credential `get_drive_service()` path that triggers `invalid_grant` (see cron-drive-fallback). For real scans, run the maintained scripts in the canonical data dir: `commons/data/ocas-bower/deep_scan_sampled.py` (deep) and `run_light_scan.py` (light). These use `get_service`, honor drift/stale-data handling, and write analyze-compatible artifacts. `scripts/bower_analyze.py` is usable (it reads the canonical `commons/data/ocas-bower` dir) but is stale — prefer it over `bower_full_scan.py`, and never use `bower_full_scan.py` for scan runs.263264- **location_outlier move proposals are title-keyword false-positive prone** — `bower.analyze` flags a file as `location_outlier` when its *name* contains a domain keyword (home/work/project/house) and it sits outside that domain's root. For books in **Bookshelf** this is almost always WRONG: e.g. *"Learn Hawaiian at Home"*, *"American House Styles"*, *"Work Like A Spy"*, *"Project Hail Mary"* are ebooks, not Home/Projects documents. Do NOT auto-apply such proposals. Rule of thumb: never move contents of an already-curated semantic root (Bookshelf, Archive) to another domain based on title-substring matches alone — require file *content/type* or explicit domain-folder membership. (Also seen: duplicate proposals — the same file listed twice, e.g. *Project Hail Mary*. The analyzer should dedupe by `source_id`.) Review the pending queue (`bower.proposals.review`) and reject the false positives before any `bower.apply`.265266- **Weekly deep scan yields 0 NEW proposals when prior ones persist** — `bower_analyze.py` dedupes against existing `pending`/`approved` proposals, so a routine weekly deep scan legitimately produces 0 new proposals even when 16 are still pending. That is NOT a sign the scan failed. The deliverable of a weekly deep scan is the refreshed `folder_index.json` + `drive_digest.json` (which advances the light-scan `modifiedTime` cutoff) + the still-pending queue — not new proposals. Report "0 new, N pending" as healthy.267268- **Stale digest causes permanent drift abort loop** — `drive_digest.json` can get a wrong baseline if a light scan incorrectly concludes that a large-drift event "reverted to baseline." Subsequent scans compare against the wrong baseline and permanently detect 95%+ drift, aborting every run and generating no proposals. Diagnosis: `root_level_file_count` and `root_level_folder_count` in `drive_digest.json` don't match what the Drive API actually returns. Fix: query the Drive API directly (`mimeType = 'application/vnd.google-apps.folder' and parentId = 'root'` for folders; `parentId = 'root' and mimeType != 'application/vnd.google-apps.folder'` for files), then update `drive_digest.json` with the real counts, clear `scan_progress.json`'s `drift_detected: true` flag, and update `folder_index.json` with the actual folder IDs. **Never write a digest update concluding drift has "reverted" unless you verified the root-level counts match the previous baseline.** Confirmed June 2026.269270## Support File Map271272| File | When to read |273|------|-------------|274| `references/organization_rules.md` | Before every `bower.analyze` run; defines preference inference, pattern promotion, taxonomy inference, all proposal generation rules, permission lookup, feedback suppression, recalibration, scan resume, cap behavior, digest format, and review narrative |275| `references/domains.md` | Before every `bower.analyze` run; defines domain detection, prescriptive/descriptive mode, canonical structures, and per-domain filing rules for Taxes, Projects, Home, Finance, Legal, Medical, Archive, Education |276| `references/analysis_schema.md` | Before `bower.scan.deep` or `bower.analyze`; defines all data schemas including preference profile, folder_index, scan_progress, proposals, move log, undo log, feedback log, and config |277| `references/signal_examples.md` | Before emitting signals; JSON schema examples for all five signal types |278| `references/scan-debug.md` | When debugging scan issues, resume failures, or light scan anomalies |279| `references/command_reference.md` | When you need full command flag descriptions and semantics |280| `references/decision-invariants.md` | Before every `bower.analyze` or `bower.apply` run; safety invariants that govern all operations |281| `references/large-drive-scanning.md` | When founding deep scan on Drive with >10K items or cron timeout; sampled scan strategy, 500 error handling, trade-offs |282| `commons/data/ocas-bower/*.py` (canonical data dir) | The RUNNABLE scan/analyze scripts: `deep_scan_sampled.py` (weekly deep, sampled), `run_light_scan.py` (light), plus artifacts (`folder_index.json`, `scans/`, `proposals.jsonl`, `drive_digest.json`, `evidence.jsonl`). Skill `scripts/` holds stale/unsafe equivalents — see Gotchas. |283| `references/drift-incident-2026-06-14.md` | When light scan detects major drift; lessons from 2026-06-14 restructuring incident, why modifiedTime-only queries miss bulk moves |284| `references/light-scan-triage.md` | After a light scan — resolving parent IDs to folder names and grouping arrivals into a disorganization report (the cron deliverable); includes the 33-char-ID truncation 404 pitfall |285| `references/interactive-menu.md` | When invoked interactively; the full two-level menu structure |286| `references/cron-drive-fallback.md` | When a scheduled scan hits OAuth/auth failures or you're wiring cron Drive access; ALWAYS-use-get_service note and operational pitfalls |287| `references/mcp-drive-tooling.md` | When driving Drive operations through MCP tooling instead of the python client |288| `references/spec-ocas-recovery.md` | Before implementing or auditing Recovery Behavior; the shared OCAS recovery contract (evidence, gap detection, degraded mode, compaction) |289| `references/okrs.md` | When reporting status/trend or reviewing targets; full OKR definitions |290291## Scan Debug & Operational Notes292293Debug procedures, resume patterns, file layout facts, and light scan lessons.294Full documentation: `references/scan-debug.md`