feat(our-gdrive-organizer): add new skill at slot 82, rename old 82 → 92

New Python CLI + dual SKILL.md (Code + Desktop) for organizing Google
Drive folders under OurDigital conventions:

- Refresh root README index (preserves manual Topics/Notes between
  AUTO-STRUCTURE markers)
- Ensure per-subfolder README.md meta files
- Propose filename + folder renames (D.intelligence → OurDigital with
  SEO-context caveat documented in patterns/gotchas.md)
- Propose moves for cluttered files (screenshots, temp downloads)
- Sensitive-folder skip list (04_Case Studies, 99_Project Archive,
  *Archive*, 진단*)
- shared/patterns/ gotcha library: canonical-files, canonical-folders,
  categorization-rules, 12 known gotchas — grows over time as the
  system encounters new edge cases

Slash command: /organize. CLI: ~/.local/bin/our-gdrive-organize.

82-tui-design-template renumbered to 92 (no content change) to free
slot 82. AGENTS.md and CLAUDE.md updated for both moves.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-10 23:02:45 +09:00
parent e1bd799672
commit c750fa7f5e
16 changed files with 1731 additions and 5 deletions

View File

@@ -195,10 +195,17 @@ Task(
- **Always ask user consent** before executing any cleanup or system changes - **Always ask user consent** before executing any cleanup or system changes
- Scripts are in `custom-skills/81-mac-optimizer/scripts/` - Scripts are in `custom-skills/81-mac-optimizer/scripts/`
- Reference docs in `custom-skills/81-mac-optimizer/references/` - Reference docs in `custom-skills/81-mac-optimizer/references/`
- **82-tui-design-template**: TUI wizard interface design (Norton Commander / Gopher style, Rich) - **82-our-gdrive-organizer**: Organize a Google Drive folder under OurDigital conventions
- Refreshes root `README.md` index (Snapshot + Structure section between AUTO-STRUCTURE markers; preserves manual Topics/Notes)
- Ensures per-subfolder `README.md` meta files with auto-indexed Contents block
- Proposes filename renames (`D.intelligence``OurDigital`) and moves (screenshots, temp downloads → tidy subfolders)
- Skips sensitive folders by default: `04_Case Studies/`, `99_Project Archive/`, `*Archive$`, `진단*`
- Pure Python stdlib CLI: `~/.local/bin/our-gdrive-organize [TARGET] [--apply] [--scope full|index|subreadmes|rename|move]`
- Slash command: `/organize`
### Reference Curator Skills (90-91) ### Reference Curator Skills (90-91) & Design Templates (92)
- **92-tui-design-template**: TUI wizard interface design (Norton Commander / Gopher style, Rich) — moved from slot 82 to make room for `our-gdrive-organizer`
- Use **reference-curator-pipeline** for full automated curation workflows - Use **reference-curator-pipeline** for full automated curation workflows
- Runs as background task, coordinates all 6 skills in sequence - Runs as background task, coordinates all 6 skills in sequence
- Handles QA loops automatically (max 3 refactor, 2 deep_research iterations) - Handles QA loops automatically (max 3 refactor, 2 deep_research iterations)

View File

@@ -158,7 +158,7 @@ This is a Claude Skills collection repository containing:
|---|-------|---------|---------| |---|-------|---------|---------|
| 80 | claude-settings-optimizer | Claude settings optimization & token audit | "settings audit", "exceed response limit", "MCP error" | | 80 | claude-settings-optimizer | Claude settings optimization & token audit | "settings audit", "exceed response limit", "MCP error" |
| 81 | mac-optimizer | macOS system health audit & optimization (Claude Code only) | "audit my mac", "system health", "clean up caches", "check security", "update packages" | | 81 | mac-optimizer | macOS system health audit & optimization (Claude Code only) | "audit my mac", "system health", "clean up caches", "check security", "update packages" |
| 82 | tui-design-template | TUI wizard interface design (Norton Commander / Gopher style, Rich) | "build TUI", "CLI wizard", "terminal UI", "Rich TUI" | | 82 | our-gdrive-organizer | Organize a Google Drive folder under OurDigital conventions: refresh README index, refresh per-subfolder READMEs, propose renames + moves | "/organize", "organize the Drive folder", "refresh the index", "rescan the folder" |
## Dual-Platform Skill Structure ## Dual-Platform Skill Structure
@@ -261,7 +261,7 @@ our-claude-skills/
│ │ │ │
│ ├── 80-claude-settings-optimizer/ │ ├── 80-claude-settings-optimizer/
│ ├── 81-mac-optimizer/ │ ├── 81-mac-optimizer/
│ ├── 82-tui-design-template/ │ ├── 82-our-gdrive-organizer/
│ │ │ │
│ ├── 90-reference-curator/ # Modular reference documentation suite │ ├── 90-reference-curator/ # Modular reference documentation suite
│ │ ├── 01-reference-discovery/ │ │ ├── 01-reference-discovery/
@@ -275,7 +275,9 @@ our-claude-skills/
│ │ ├── shared/ │ │ ├── shared/
│ │ └── install.sh │ │ └── install.sh
│ │ │ │
── 91-multi-agent-guide/ ── 91-multi-agent-guide/
│ │
│ └── 92-tui-design-template/
├── example-skills/skills-main/ ├── example-skills/skills-main/
├── official-skills/ ├── official-skills/

View File

@@ -0,0 +1,96 @@
# our-gdrive-organizer
CLI + Claude skill for organizing OurDigital Google Drive folders.
## Purpose
Generalize the README-index-refresh pattern (originally built for
`02_SEO in Action/`) so it works on any 2nd-level subfolder of the user's
Google Drive Stream. Adds:
- Per-subfolder `README.md` meta files (with AUTO-STRUCTURE blocks)
- Proposed filename normalization (`D.intelligence``OurDigital`)
- Proposed file moves for cluttered roots (screenshots, temp downloads)
- Sensitive-folder skip list so client archives are never touched
## Activation
- **As a Claude Code skill**: triggered by phrases like "organize the Drive
folder", "refresh the index", "/organize", or "/our-gdrive-organizer".
- **As a CLI tool**: run `our-gdrive-organize [TARGET]` from any terminal.
## Structure
```
our-gdrive-organizer/
├── code/
│ ├── SKILL.md # Claude Code skill definition
│ └── organizer.py # Main Python CLI (stdlib only)
├── desktop/
│ └── SKILL.md # Claude Desktop skill definition
├── shared/
│ └── conventions.md # Canonical naming convention spec
├── docs/
│ └── CHANGELOG.md
└── README.md # ← you are here
```
## Install
The repository expects two symlinks:
```bash
# Skill discovery for Claude Code
ln -s ~/Project/our-claude-skills/custom-skills/our-gdrive-organizer/code \
~/.claude/skills/our-gdrive-organizer
# CLI on PATH
ln -s ~/Project/our-claude-skills/custom-skills/our-gdrive-organizer/code/organizer.py \
~/.local/bin/our-gdrive-organize
```
A slash-command alias lives at `~/.claude/commands/organize.md`.
## Usage
```bash
# Dry-run report (default — never modifies files except writing the README)
our-gdrive-organize ~/Library/CloudStorage/GoogleDrive-*/My\ Drive/02_SEO\ in\ Action
# Apply all proposed renames + moves
our-gdrive-organize <TARGET> --apply
# Only refresh the root README index
our-gdrive-organize <TARGET> --scope index
# Only ensure each top-level subfolder has a README with AUTO block
our-gdrive-organize <TARGET> --scope subreadmes
# Only propose renames (still requires --apply to execute)
our-gdrive-organize <TARGET> --scope rename
# Machine-readable output
our-gdrive-organize <TARGET> --json
```
## Conventions
See `shared/conventions.md` for the full naming and structure spec. The
script's rules (`RENAME_RULES`, `MOVE_RULES`, `SENSITIVE_SUBFOLDER_PATTERNS`)
in `code/organizer.py` are the source of truth for what's actually enforced;
keep them in sync with `conventions.md`.
## What it never touches
- Files inside `04_Case Studies/`, `99_Project Archive/`, any `*Archive*`
folder, or any folder starting with `진단` (real client material).
- The manually-curated Topics / Notes sections of the root README.
- Cell content of `.gsheet` / body of `.gdoc` / slides of `.gslides`
filesystem renames only change the local stub filename.
## Requirements
- Python 3.9+ (uses `from __future__ import annotations`, dataclasses, pathlib)
- macOS (paths assume Google Drive Cloud Storage at
`~/Library/CloudStorage/GoogleDrive-*/My Drive/`); easily adapted for other
OSes by changing the target path.

View File

@@ -0,0 +1,209 @@
---
name: our-gdrive-organizer
description: |
Organize a Google Drive folder under OurDigital conventions: refresh root
README.md index, refresh per-subfolder README.md meta files, propose renames
for files using the old D.intelligence brand, and propose moves for cluttered
files (screenshots at the wrong level, temp/partial downloads).
Triggers:
- "organize the Drive folder", "organize this folder"
- "refresh the index", "rescan the folder", "update README"
- "clean up cluttered files", "propose renames"
- "/organize", "/organize-drive", "/our-gdrive-organizer"
Default target is the current working directory. Generalized to work on any
2nd-level subfolder of the user's Google Drive Stream (My Drive/00_..., 01_...,
02_..., etc.) — not specific to one folder.
version: "1.0"
author: OurDigital
environment: Code
---
# our-gdrive-organizer (Code)
Walks a target directory (3 levels deep), refreshes the index README, proposes
renames + moves under OurDigital naming conventions, and optionally applies.
Source of truth for the conventions: `../shared/conventions.md`.
## Activation
The user wants to organize one of their Drive folders. Cues:
- An explicit ask: "organize", "refresh index", "rename per convention", "scan for changes"
- The user is sitting inside a 2nd-level Drive folder (`~/Library/CloudStorage/GoogleDrive-*/My Drive/NN_*/`)
- Slash invocation: `/organize`, `/our-gdrive-organizer`
## Workflow
### Step 1 — Run dry-run, then summarize
```bash
~/.local/bin/our-gdrive-organize "$TARGET"
```
(`$TARGET` defaults to cwd; pass an absolute path for a specific folder.)
The script writes the README index immediately (idempotent — skips when no
structural change), but never renames or moves files without `--apply`.
Read the report and tell the user, in plain language:
- How many structural changes were folded into the README
- Each rename proposal (old → new, reason)
- Each move proposal (file → destination subfolder, reason)
- Subfolders that were skipped because they're "sensitive" (`04_Case Studies`,
`99_Project Archive`, `*Archive*`, `진단*`)
### Step 2 — Confirm with user before applying
If the user says go ahead (or "apply", "yes", "do it"):
```bash
~/.local/bin/our-gdrive-organize "$TARGET" --apply
```
If the user wants only part of the work:
```bash
~/.local/bin/our-gdrive-organize "$TARGET" --scope rename --apply
~/.local/bin/our-gdrive-organize "$TARGET" --scope move --apply
~/.local/bin/our-gdrive-organize "$TARGET" --scope index # always writes
~/.local/bin/our-gdrive-organize "$TARGET" --scope subreadmes # always writes
```
### Step 3 — Verify
After applying, run the dry-run once more and confirm the proposal list is
empty (or only contains items the user explicitly skipped).
## Important guardrails
- **Never rename or move files inside `04_Case Studies/`, `99_Project Archive/`,
any `*Archive*` folder, or any folder starting with `진단`.** Those contain
real client engagement records that must keep their original filenames.
- The script's rename/move rules live in `code/organizer.py` near the top of
the file (`RENAME_RULES`, `MOVE_RULES`, `SENSITIVE_SUBFOLDER_PATTERNS`). If
the user asks to add or change a rule, edit there and re-run.
- Filename renames on `.gsheet` / `.gdoc` stub files only change the local
filename — the actual Google Drive document and its sharing links are
preserved (the stub holds a Doc ID, not the content).
## What the script does NOT do
- Does not edit cell content inside `.gsheet` / `.gdoc` / `.xlsx` / `.pdf`
only local-filesystem renames. Cell-level neutralization stays a manual task.
- Does not delete anything.
- Does not modify the manually-curated Topics / Notes sections of the root
README — only the AUTO-STRUCTURE block.
- Does not categorize files by reading their **content** — for that, use the
Content-based reorganization workflow below.
## Folder-rename support
The script proposes folder renames (in addition to file renames) using the
same `RENAME_RULES`. Guardrails:
- **Top-level subfolders are NEVER renamed automatically.** Names like
`00_Brand Management/` or `02_SEO Audit Toolkit/` are user-curated
practice areas. If the user wants one renamed, do it as a one-off `mv`.
- Sensitive folders are skipped entirely (not renamed, not recursed into).
- Eligible folders: depth-2 and deeper (e.g.,
`00_Brand Management/D.intelligence SEO Audit/``…/OurDigital SEO Audit/`).
When `--scope rename --apply` runs, file renames execute first, then folder
renames — order matters because renaming a folder first would invalidate
the file rename paths inside it.
## Content-based reorganization (interactive)
The script handles deterministic naming-pattern work. For judgment calls
(which folder does this file truly belong in?), use this workflow.
### When to use
The user says any of:
- "look at the contents and reorganize"
- "this folder feels cluttered, suggest a better layout"
- "categorize the files in {subfolder}"
- "audit my folder structure"
Or you notice during a regular `/organize` run that:
- A subfolder root has many loose files that should plausibly be grouped
- Files appear duplicated across subfolders
- Filenames hint at content that doesn't match their location
### Workflow
1. **Anchor yourself** — read the patterns library before proposing anything:
- `../shared/patterns/canonical-folders.md` — what well-organized shapes look like
- `../shared/patterns/canonical-files.md` — what well-named files look like
- `../shared/patterns/categorization-rules.md` — IF→THEN placement rules
- `../shared/patterns/gotchas.md` — known edge cases
These are the gotcha library. Re-read them every session — they grow over time.
2. **Pick one subfolder at a time.** Don't try to reorganize an entire
2nd-level folder in one pass — too much for the user to review.
3. **Sample file contents.** For each file in the chosen subfolder:
- Markdown / txt / json: `Read` directly.
- `.gsheet` / `.gdoc` stubs: read the JSON to extract the doc_id, but
accept that you can't see actual cell content. Use the FILENAME
pattern + adjacent context.
- PDFs / .docx / .pptx / .xlsx: you can't read content with stdlib.
Either ask the user to summarize, or skip and rely on filename.
- For each file, note 12 sentences: what's it about, where would it
belong by content?
4. **Build proposals**, grouped by destination:
```
Move from `02_SEO Audit Toolkit/` → `04_Case Studies/`:
- `signed-acme-contract.pdf` (real client name + signed contract content)
Move from `02_SEO Audit Toolkit/` → `05_Working Template/`:
- `OurDigital-OOO Audit Template.gsheet` (placeholder name → template)
```
5. **Present one batch (one source folder) at a time.** Ask:
"Should I apply these N moves from `{source}/`? Yes / No / partial (which)."
6. **Apply via `Bash mv`** for each confirmed move:
```bash
mv "/path/to/source/file" "/path/to/destination/file"
```
Then refresh the index:
```bash
our-gdrive-organize "$TARGET" --scope index
```
7. **Capture new gotchas.** If you encountered an ambiguous case the
patterns library didn't cover, append it to
`../shared/patterns/gotchas.md` before ending the session — that's how
the system learns.
### Sensitive-folder reminder
When proposing moves in content-based mode, the same guardrails apply:
**never propose moving files INTO or OUT OF**:
- `04_Case Studies/`
- `99_Project Archive/`
- Any `*Archive*` folder
- Any folder starting with `진단`
If you notice something in those folders that looks misplaced, flag it to
the user as a manual review item — don't propose an automated move.
## When the user asks to extend
To add a new rename rule, edit `RENAME_RULES` in `code/organizer.py`:
```python
(re.compile(r"oldpattern", re.I), "newpattern", "human-readable reason"),
```
To add a new move rule, edit `MOVE_RULES`:
```python
(re.compile(r"^pattern\.ext$"), "destination_subfolder", "reason"),
```
Update `../shared/conventions.md` whenever rules change.

View File

@@ -0,0 +1,520 @@
#!/usr/bin/env python3
"""
our-gdrive-organizer — organize a Google Drive folder under OurDigital conventions.
Scans the target directory (default: 2nd-level Drive subfolder), refreshes the
README.md index, proposes renames + moves to bring files into convention, and
optionally applies them.
Usage:
organizer.py [TARGET] # dry-run report
organizer.py [TARGET] --apply # apply all proposed changes
organizer.py [TARGET] --scope index # only refresh root README
organizer.py [TARGET] --scope subreadmes # refresh per-subfolder READMEs
organizer.py [TARGET] --scope rename # only propose/apply renames
organizer.py [TARGET] --scope move # only propose/apply moves
organizer.py [TARGET] --scope full # everything (default)
organizer.py [TARGET] --json # machine-readable output
If TARGET is omitted, uses the current working directory.
The script is stdlib-only (Python 3.9+). Safe by default — never writes unless
--apply is passed (or running --scope index, which always writes the README).
"""
from __future__ import annotations
import argparse
import json
import os
import re
import shutil
import sys
from dataclasses import dataclass, field, asdict
from datetime import datetime, timezone
from pathlib import Path
from typing import Iterable
# ---------------------------------------------------------------------------
# Configuration: naming convention enforcement.
# Edit shared/conventions.md for the human-readable spec; this dict is the
# source of truth for the script.
# ---------------------------------------------------------------------------
RENAME_RULES: list[tuple[re.Pattern[str], str, str]] = [
# (pattern, replacement, reason)
# SEO context only — D.intelligence is the parent company; OurDigital is its
# SEO-specialty child brand. Only apply these renames when operating inside
# an SEO folder (02_SEO in Action, 04_SEO, etc.). Outside SEO context, keep
# D.intelligence names intact (they belong to the parent company). The
# script doesn't enforce this caveat — it relies on the caller pointing at
# the right target. See shared/patterns/gotchas.md "D.intelligence vs
# OurDigital" for the full rule.
(re.compile(r"D\.intelligence Lab-", re.I), "OurDigital-",
"parent-co SEO asset → OurDigital-"),
(re.compile(r"D\.intelligence", re.I), "OurDigital",
"parent-co SEO asset → OurDigital"),
# Variant without the dot (typo / informal spelling).
(re.compile(r"\bD intelligence\b", re.I), "OurDigital",
"parent-co SEO asset (no-dot variant) → OurDigital"),
]
# Files matching these patterns at the root of a top-level subfolder are
# proposed to be moved into a sibling subfolder.
MOVE_RULES: list[tuple[re.Pattern[str], str, str]] = [
(re.compile(r"^Screenshot \d{4}-\d{2}-\d{2}.*\.png$"), "screenshots",
"screenshot file → screenshots/ subfolder"),
(re.compile(r"^.*\.(crdownload|tmp|partial)$"), "_unsorted",
"incomplete / temp file → _unsorted/"),
]
# Skip these subfolders for invasive changes (renames + moves).
# Indexing still includes them, but their internal files aren't reorganized.
SENSITIVE_SUBFOLDER_PATTERNS = [
re.compile(r"^04_Case Studies$"),
re.compile(r"^99_Project Archive$"),
re.compile(r".*Archive$", re.I),
re.compile(r"^진단.*$"),
]
EXCLUDE_NAMES = {".DS_Store", "Icon\r"}
BEGIN_MARK = "<!-- AUTO-STRUCTURE:BEGIN -->"
END_MARK = "<!-- AUTO-STRUCTURE:END -->"
# ---------------------------------------------------------------------------
# Data classes
# ---------------------------------------------------------------------------
@dataclass
class FolderScan:
path: Path
files: list[str] = field(default_factory=list)
subfolders: dict[str, "FolderScan"] = field(default_factory=dict)
@dataclass
class Proposal:
kind: str # "rename" | "move"
src: Path
dst: Path
reason: str
def as_dict(self) -> dict:
return {"kind": self.kind, "src": str(self.src),
"dst": str(self.dst), "reason": self.reason}
# ---------------------------------------------------------------------------
# Scanning
# ---------------------------------------------------------------------------
def is_visible(name: str) -> bool:
return not name.startswith(".") and name not in EXCLUDE_NAMES
def list_visible_files(path: Path) -> list[str]:
try:
return sorted(e.name for e in os.scandir(path)
if e.is_file(follow_symlinks=False) and is_visible(e.name))
except OSError:
return []
def list_visible_dirs(path: Path) -> list[str]:
try:
return sorted(e.name for e in os.scandir(path)
if e.is_dir(follow_symlinks=False) and is_visible(e.name))
except OSError:
return []
def scan(path: Path, depth: int = 3) -> FolderScan:
"""Walk depth levels deep. depth=3 → root + 2 levels of subfolders."""
s = FolderScan(path=path, files=list_visible_files(path))
if depth > 1:
for d in list_visible_dirs(path):
s.subfolders[d] = scan(path / d, depth - 1)
else:
for d in list_visible_dirs(path):
s.subfolders[d] = FolderScan(path=path / d)
return s
def is_sensitive(name: str) -> bool:
return any(p.search(name) for p in SENSITIVE_SUBFOLDER_PATTERNS)
# ---------------------------------------------------------------------------
# Index generation (root README.md and per-subfolder READMEs)
# ---------------------------------------------------------------------------
def render_structure_block(scan_root: FolderScan) -> str:
out: list[str] = []
out.append("## Structure (3 levels)")
out.append("")
ts = datetime.now(timezone.utc).astimezone().strftime("%Y-%m-%d %H:%M %z")
out.append(f"_Auto-regenerated {ts}. Edits inside the AUTO-STRUCTURE markers will be overwritten._")
out.append("")
for d1_name in sorted(scan_root.subfolders):
d1 = scan_root.subfolders[d1_name]
nf1 = len(d1.files)
nd1 = len(d1.subfolders)
out.append(f"- **{d1_name}/** ({nf1} files, {nd1} subfolders)")
for d2_name in sorted(d1.subfolders):
d2 = d1.subfolders[d2_name]
nf2 = len(d2.files)
nd2 = len(d2.subfolders)
if nd2 > 0:
out.append(f" - {d2_name}/ ({nf2}f, {nd2}d)")
for d3_name in sorted(d2.subfolders):
d3 = d2.subfolders[d3_name]
nf3 = len(d3.files)
out.append(f" - {d3_name}/ ({nf3}f)")
else:
out.append(f" - {d2_name}/ ({nf2}f)")
if nf1 > 0:
preview = ", ".join(f"`{n}`" for n in d1.files[:5])
out.append(f" - _files_: {preview}")
out.append("")
return "\n".join(out).rstrip() + "\n"
def render_subfolder_block(scan_node: FolderScan) -> str:
"""Smaller block for per-subfolder README.md (2 levels deep)."""
out: list[str] = []
out.append("## Contents")
out.append("")
ts = datetime.now(timezone.utc).astimezone().strftime("%Y-%m-%d %H:%M %z")
out.append(f"_Auto-regenerated {ts}._")
out.append("")
if scan_node.files:
out.append(f"**Files at root** ({len(scan_node.files)}):")
for f in scan_node.files[:20]:
out.append(f"- `{f}`")
if len(scan_node.files) > 20:
out.append(f"- … and {len(scan_node.files) - 20} more")
out.append("")
if scan_node.subfolders:
out.append(f"**Subfolders** ({len(scan_node.subfolders)}):")
for d_name in sorted(scan_node.subfolders):
d = scan_node.subfolders[d_name]
nf = len(d.files)
nd = len(d.subfolders)
extras = f", {nd} dirs" if nd else ""
out.append(f"- `{d_name}/` ({nf} files{extras})")
out.append("")
return "\n".join(out).rstrip() + "\n"
def splice_readme(readme_path: Path, new_block: str, *, header_default: str | None = None) -> tuple[bool, str]:
"""Insert/replace the AUTO-STRUCTURE block inside readme_path.
Returns (changed, summary). Creates the file with header_default if missing.
"""
if not readme_path.exists():
if header_default is None:
return (False, f"skip: {readme_path} does not exist")
readme_path.write_text(
f"{header_default}\n\n{BEGIN_MARK}\n{new_block}{END_MARK}\n",
encoding="utf-8",
)
return (True, f"created: {readme_path}")
text = readme_path.read_text(encoding="utf-8")
# Update top-level Snapshot counts if they exist
parent = readme_path.parent
total_files = len(list_visible_files(parent))
total_dirs = len(list_visible_dirs(parent))
text = re.sub(r"^- \*\*Top-level files\*\*:.*$",
f"- **Top-level files**: {total_files}", text, count=1, flags=re.M)
text = re.sub(r"^- \*\*Top-level subfolders\*\*:.*$",
f"- **Top-level subfolders**: {total_dirs}", text, count=1, flags=re.M)
if BEGIN_MARK in text and END_MARK in text:
pattern = re.compile(re.escape(BEGIN_MARK) + r".*?" + re.escape(END_MARK), re.S)
new_text = pattern.sub(BEGIN_MARK + "\n" + new_block + END_MARK, text)
else:
# Append marker block before the trailing footer (if any) or at EOF
new_text = text.rstrip() + "\n\n" + BEGIN_MARK + "\n" + new_block + END_MARK + "\n"
if structurally_equal(text, new_text):
return (False, f"no structural changes: {readme_path}")
readme_path.write_text(new_text, encoding="utf-8")
return (True, f"updated: {readme_path}")
def structurally_equal(a: str, b: str) -> bool:
"""Strip the auto-regenerated timestamp lines before comparing."""
strip = re.compile(r"^_Auto-regenerated .*$", re.M)
return strip.sub("", a) == strip.sub("", b)
# ---------------------------------------------------------------------------
# Rename + move proposals
# ---------------------------------------------------------------------------
def propose_renames(scan_root: FolderScan) -> list[Proposal]:
proposals: list[Proposal] = []
def visit(node: FolderScan):
for f in node.files:
new = f
reasons: list[str] = []
for pat, repl, why in RENAME_RULES:
next_new = pat.sub(repl, new)
if next_new != new:
reasons.append(why)
new = next_new
if new != f:
proposals.append(Proposal("rename", node.path / f,
node.path / new, "; ".join(reasons)))
for sub_name, sub in node.subfolders.items():
if is_sensitive(sub_name):
continue
visit(sub)
visit(scan_root)
return proposals
def propose_folder_renames(scan_root: FolderScan) -> list[Proposal]:
"""Propose folder renames matching RENAME_RULES.
Guardrails:
- Top-level subfolders (depth 1, e.g. "00_Brand Management/") are NEVER
renamed automatically — they're the user's manually curated practice
areas. Use a one-off `mv` if you need to rename one.
- Sensitive folders are skipped entirely (not renamed, not recursed into).
"""
proposals: list[Proposal] = []
def visit(node: FolderScan, parent_depth: int):
sub_depth = parent_depth + 1
for sub_name, sub in node.subfolders.items():
if is_sensitive(sub_name):
continue
if sub_depth >= 2: # depth-2+ folders are eligible for rename
new_name = sub_name
reasons: list[str] = []
for pat, repl, why in RENAME_RULES:
nxt = pat.sub(repl, new_name)
if nxt != new_name:
reasons.append(why)
new_name = nxt
if new_name != sub_name:
proposals.append(Proposal(
"rename-folder", sub.path,
sub.path.parent / new_name,
"; ".join(reasons),
))
visit(sub, sub_depth)
visit(scan_root, parent_depth=0)
return proposals
def propose_moves(scan_root: FolderScan) -> list[Proposal]:
proposals: list[Proposal] = []
# Only operate on top-level subfolders of the target — that's where the
# "cluttered files at root" pattern most commonly appears.
for sub_name, sub in scan_root.subfolders.items():
if is_sensitive(sub_name):
continue
for f in sub.files:
for pat, dest_subdir, why in MOVE_RULES:
if pat.search(f):
proposals.append(Proposal(
"move",
sub.path / f,
sub.path / dest_subdir / f,
why,
))
break
return proposals
def apply_proposals(props: Iterable[Proposal]) -> list[tuple[Proposal, str]]:
results: list[tuple[Proposal, str]] = []
for p in props:
try:
if p.kind == "move":
p.dst.parent.mkdir(parents=True, exist_ok=True)
if p.dst.exists():
results.append((p, "skip: destination exists"))
continue
shutil.move(str(p.src), str(p.dst))
results.append((p, "ok"))
except OSError as e:
results.append((p, f"error: {e}"))
return results
# ---------------------------------------------------------------------------
# Per-subfolder README.md ensuring + refresh
# ---------------------------------------------------------------------------
def refresh_subfolder_readmes(scan_root: FolderScan, *, write: bool) -> list[str]:
"""For each top-level subfolder, ensure README.md exists with AUTO block."""
log: list[str] = []
for sub_name in sorted(scan_root.subfolders):
if is_sensitive(sub_name):
log.append(f"skip (sensitive): {sub_name}/")
continue
sub = scan_root.subfolders[sub_name]
readme = sub.path / "README.md"
block = render_subfolder_block(sub)
header = f"# {sub_name}\n\n_OurDigital — auto-indexed subfolder README._"
if not write:
log.append(f"would refresh: {readme}")
continue
changed, summary = splice_readme(readme, block, header_default=header)
log.append(summary)
return log
# ---------------------------------------------------------------------------
# Reporting
# ---------------------------------------------------------------------------
def text_report(target: Path, renames: list[Proposal], folder_renames: list[Proposal],
moves: list[Proposal], index_summary: str, sub_log: list[str]) -> str:
lines = []
lines.append(f"# our-gdrive-organizer report")
lines.append(f"target: {target}")
lines.append(f"scan time: {datetime.now().strftime('%F %T')}")
lines.append("")
lines.append("## Index")
lines.append(index_summary or "(skipped)")
lines.append("")
lines.append("## Subfolder READMEs")
if sub_log:
for line in sub_log:
lines.append(f"- {line}")
else:
lines.append("(skipped)")
lines.append("")
lines.append(f"## File rename proposals ({len(renames)})")
if renames:
for p in renames:
lines.append(f"- {p.src.relative_to(target)}")
lines.append(f"{p.dst.relative_to(target)}")
lines.append(f" ({p.reason})")
else:
lines.append("(none)")
lines.append("")
lines.append(f"## Folder rename proposals ({len(folder_renames)})")
if folder_renames:
for p in folder_renames:
lines.append(f"- {p.src.relative_to(target)}/")
lines.append(f"{p.dst.relative_to(target)}/")
lines.append(f" ({p.reason})")
else:
lines.append("(none)")
lines.append("")
lines.append(f"## Move proposals ({len(moves)})")
if moves:
for p in moves:
lines.append(f"- {p.src.relative_to(target)}")
lines.append(f"{p.dst.relative_to(target)}")
lines.append(f" ({p.reason})")
else:
lines.append("(none)")
return "\n".join(lines) + "\n"
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(
description="OurDigital Google Drive folder organizer",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__,
)
parser.add_argument("target", nargs="?", default=os.getcwd(),
help="target directory (default: cwd)")
parser.add_argument("--apply", action="store_true",
help="actually perform renames and moves (default: dry-run)")
parser.add_argument("--scope", choices=["full", "index", "subreadmes",
"rename", "move"], default="full",
help="restrict the operation (default: full)")
parser.add_argument("--json", action="store_true",
help="emit machine-readable JSON instead of a text report")
args = parser.parse_args(argv)
target = Path(args.target).expanduser().resolve()
if not target.is_dir():
print(f"target is not a directory: {target}", file=sys.stderr)
return 1
scan_root = scan(target, depth=3)
index_summary = ""
sub_log: list[str] = []
renames: list[Proposal] = []
folder_renames: list[Proposal] = []
moves: list[Proposal] = []
if args.scope in ("full", "index"):
readme = target / "README.md"
block = render_structure_block(scan_root)
header = f"# {target.name}\n\n_OurDigital — auto-indexed root README._\n\n## Snapshot\n\n- **Top-level files**: {len(scan_root.files)}\n- **Top-level subfolders**: {len(scan_root.subfolders)}"
changed, index_summary = splice_readme(readme, block, header_default=header)
if args.scope in ("full", "subreadmes"):
sub_log = refresh_subfolder_readmes(scan_root, write=True)
# Order matters when --apply: file renames first, then folder renames.
# If folders are renamed first, file paths inside them become invalid.
if args.scope in ("full", "rename"):
renames = propose_renames(scan_root)
if args.apply and renames:
results = apply_proposals(renames)
for p, status in results:
p.reason = f"{p.reason} [{status}]"
folder_renames = propose_folder_renames(scan_root)
if args.apply and folder_renames:
# Apply deepest folders first — renaming a parent invalidates the
# path of any nested rename. shutil.move on the deepest target
# leaves the parent's name intact and avoids stale paths.
ordered = sorted(folder_renames, key=lambda p: len(p.src.parts), reverse=True)
results = apply_proposals(ordered)
for p, status in results:
p.reason = f"{p.reason} [{status}]"
if args.scope in ("full", "move"):
moves = propose_moves(scan_root)
if args.apply and moves:
results = apply_proposals(moves)
for p, status in results:
p.reason = f"{p.reason} [{status}]"
if args.json:
out = {
"target": str(target),
"scope": args.scope,
"applied": args.apply,
"index": index_summary,
"subfolder_readmes": sub_log,
"renames": [p.as_dict() for p in renames],
"folder_renames": [p.as_dict() for p in folder_renames],
"moves": [p.as_dict() for p in moves],
}
print(json.dumps(out, ensure_ascii=False, indent=2))
else:
print(text_report(target, renames, folder_renames, moves, index_summary, sub_log))
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -0,0 +1,103 @@
---
name: our-gdrive-organizer
description: |
Organize a Google Drive folder under OurDigital conventions: refresh the
root README.md index, refresh per-subfolder README.md meta files, propose
filename normalizations (D.intelligence → OurDigital, client name → OOO
placeholder), and propose moves for cluttered files (screenshots, temp
downloads).
Triggers:
- "organize the Drive folder", "organize this folder"
- "refresh the index", "rescan the folder", "update README"
- "clean up cluttered files", "propose renames"
- "/organize", "/our-gdrive-organizer"
Designed for any 2nd-level Drive folder (not specific to one project).
In Claude Desktop, this is a guide — the actual filesystem operations
are performed by the user via the bundled `organizer.py` CLI in a terminal,
or by Claude with a filesystem MCP tool if available.
version: "1.0"
author: OurDigital
environment: Desktop
---
# our-gdrive-organizer (Desktop)
Guide for organizing a Drive folder under OurDigital naming + structure
conventions. In Claude Desktop without filesystem write access, walk the user
through running the bundled CLI script themselves.
## Activation
User says any of:
- "organize my Drive folder"
- "refresh the index README"
- "scan for changes and update the README"
- "rename files to follow OurDigital convention"
- "/organize" (slash command)
## Workflow
### Step 1 — Identify the target folder
Ask the user which folder they want to organize, or assume the conversation's
current 2nd-level Drive folder (e.g., `02_SEO in Action/`,
`01_Brand in Action/`, `00_OurDigital/`).
### Step 2 — Have the user run the CLI
The Python script lives in the user's local `~/Project/our-claude-skills/`
repo and is symlinked to `~/.local/bin/our-gdrive-organize`. The user runs:
```bash
our-gdrive-organize "/Users/ourdigital/Library/CloudStorage/GoogleDrive-andrew.yim@ourdigital.org/My Drive/NN_FolderName"
```
This is a dry-run by default. It always writes the README index (idempotent),
and it prints proposals for renames and moves without applying them.
Ask the user to paste back the report.
### Step 3 — Summarize and confirm
Read the report, summarize each proposed rename and move in plain language, and
ask the user which (if any) they want to apply. They can apply all:
```bash
our-gdrive-organize "/path/to/folder" --apply
```
Or just one scope at a time:
```bash
our-gdrive-organize "/path/to/folder" --scope rename --apply
out-gdrive-organize "/path/to/folder" --scope move --apply
```
## Important guardrails
The script automatically skips invasive changes (renames, moves) inside:
- `04_Case Studies/`
- `99_Project Archive/`
- Any folder ending in `Archive`
- Any folder starting with `진단`
These are client-engagement records that must keep their original filenames.
If the user wants to override that, they need to edit
`SENSITIVE_SUBFOLDER_PATTERNS` in `~/Project/our-claude-skills/custom-skills/our-gdrive-organizer/code/organizer.py`.
## Naming convention
See `../shared/conventions.md` for the canonical spec. Highlights:
- `D.intelligence``OurDigital` (rebrand)
- Client-specific quote/template files → `OurDigital-{topic}-OOO {date}.{ext}`
- Top-level subfolders → `NN_descriptive name`
- Client subfolders inside Active Workspaces / Project Archive → `NN_{client name}`
## Troubleshooting
- **"command not found: our-gdrive-organize"** — the symlink may not be set up.
Ask the user to run: `ln -s ~/Project/our-claude-skills/custom-skills/our-gdrive-organizer/code/organizer.py ~/.local/bin/our-gdrive-organize`
- **Script changes README but the user doesn't see the update** — Google Drive
sync delay; usually 5-30 seconds.

View File

@@ -0,0 +1,80 @@
# Changelog
## 1.2 — 2026-05-10
First content-based reorganization session. Pattern library grew from 7 to
12 gotchas. RENAME_RULES extended.
### Added
- `RENAME_RULES` now catches `D intelligence` (no-dot variant) in addition
to the dot-form and `D.intelligence Lab-` prefix.
- Five new gotcha entries in `shared/patterns/gotchas.md`:
- "D.intelligence vs OurDigital" — parent company / SEO child brand;
rebrand only applies in SEO context
- "Brand-variant typos that escape the regex" — how to add new variants
without false positives
- "Real client names in 예시 자료 모음/" — half-done neutralization;
rename + TODO for cell content
- "Near-duplicate templates across 문서 양식 and 05_Working Template" —
different Doc IDs ≠ different content; needs human comparison
- "Stray screenshot that turns out to be a process diagram" — view
binary screenshots before applying default move rule
### Changed
- `shared/conventions.md` "Brand prefix" section rewritten to clarify the
parent-company / child-brand distinction and the SEO-context caveat.
## 1.1 — 2026-05-10
Added folder-rename support and the patterns gotcha library.
### Added
- `propose_folder_renames()` in `organizer.py` — applies `RENAME_RULES` to
folder names at depth 2+. Top-level subfolders and sensitive folders are
guarded.
- File renames execute before folder renames during `--apply` so paths stay
valid.
- New report section "Folder rename proposals" in both text and JSON output.
- `shared/patterns/` gotcha library:
- `canonical-files.md` — examples of well-named files in their proper homes
- `canonical-folders.md` — well-organized folder shapes
- `categorization-rules.md` — IF→THEN placement rules
- `gotchas.md` — known edge cases with resolutions
- Content-based reorganization workflow in `code/SKILL.md`. Claude reads
files (not just names), consults the patterns library, and proposes moves
one folder at a time for user confirmation. Applied via `Bash mv`.
### Changed
- `text_report()` and `--json` output now include `folder_renames`.
- `code/SKILL.md` expanded with content-based workflow + patterns references.
- `shared/conventions.md` cross-references the patterns library.
## 1.0 — 2026-05-10
Initial release. Generalizes the `seo-drive-index` skill pattern to work on
any 2nd-level Drive subfolder.
### Added
- Pure-Python CLI (`code/organizer.py`, stdlib only)
- Root README.md index refresh (Snapshot counts + Structure section between
AUTO-STRUCTURE markers; preserves manual Topics / Notes)
- Per-subfolder README.md meta-file ensuring (`--scope subreadmes`)
- Rename proposals enforcing `D.intelligence``OurDigital` rebrand
- Move proposals for screenshots and temp downloads cluttering folder roots
- Sensitive-folder skip list (`04_Case Studies`, `99_Project Archive`,
`*Archive*`, `진단*`)
- Dual SKILL.md (Claude Code + Claude Desktop)
- Slash-command alias `/organize`
- `shared/conventions.md` canonical naming spec
### Removed (during this work)
- `~/.claude/skills/seo-drive-index/` (superseded by this skill)
- `~/Library/LaunchAgents/org.ourdigital.seo-in-action-index.plist` (no
scheduled job; skill is invoked on demand instead)
- `~/.local/bin/seo-in-action-index.sh` (old bash version)

View File

@@ -0,0 +1,153 @@
# OurDigital Drive Folder Conventions
Canonical reference for naming and organization patterns enforced by
`our-gdrive-organizer`. The script's rules live in `code/organizer.py`
keep this document and that code in sync.
For the **gotcha library** (canonical examples + edge cases consulted
during interactive content-based reorganization), see `patterns/`:
- `patterns/canonical-files.md`
- `patterns/canonical-folders.md`
- `patterns/categorization-rules.md`
- `patterns/gotchas.md`
The brand prefix rules (`D.intelligence …``OurDigital …`) now apply to
folder names too, at depth 2+. Top-level folders (e.g., `00_Brand Management/`)
are never auto-renamed.
## Folder naming
### 2nd-level Drive folders (top-level practice areas)
`NN_descriptive name`, where NN is a 2-digit numeric prefix indicating the
"sort bucket":
| Range | Meaning |
|-------|---------|
| 0009 | Brand / core / cross-cutting |
| 1019 | Product or specialty practice |
| 2029 | Data / integrations |
| 9099 | Archive |
Examples observed: `00_OurDigital`, `01_Brand in Action`, `02_SEO in Action`,
`10_OurSEO Agent`, `20_DataForSEO`, `99_Project Archive`.
### 3rd-level subfolders
`NN_descriptive name` again, scoped to the parent. Maturity ordering:
| Range | Meaning |
|-------|---------|
| 0005 | Frameworks / always-on assets |
| 1013 | Productized tooling / specialty modules |
| 20 | Data |
| 90+ | Archive within this practice |
### Client / project subfolders
Inside `01_Active Workspaces/` or `99_Project Archive/`:
`NN_{client name}` — e.g., `00_Jamie Clinic`, `04_오현이혼상속센터`,
`05_1가다`. NN is assigned in chronological / engagement order.
### Reserved / system subfolders
- `screenshots/` — auto-created by the organizer when moving stray screenshot
files out of a parent folder root.
- `_unsorted/` — auto-created by the organizer for incomplete downloads
(`.crdownload`, `.tmp`, `.partial`).
## File naming
### Templates and quotes (in `05_Working Template/`)
`OurDigital-{Service}-OOO {date}.{ext}` or
`OurDigital-{Service}-OOOOOO {modifier}.{ext}`.
`OOO` and `OOOOOO` are the canonical placeholder strings for "client name
goes here" — chosen to be visually obvious and easy to grep for. Use
whichever makes more visual sense in the filename.
Examples:
- `OurDigital-SEO Coaching-OOO 견적-20240612.gsheet`
- `OurDigital-SEO Treatment-OOO 호텔 견적-20240903.gsheet`
- `OurDigital-OOO 호텔 SEO Basic 패키지-20250416.gsheet`
- `OurDigital-Data Access & Management Agreement - 2026.gsheet`
### Brand prefix — parent company / SEO child brand
D.intelligence is the **parent company**; OurDigital is its **SEO-specialty
child brand**. The rebrand-to-OurDigital rule only applies when the asset
is SEO-related.
Rules (filesystem rename only — does not touch cell/body content):
- `D.intelligence Lab-``OurDigital-`
- `D.intelligence` (anywhere) → `OurDigital`
- `D intelligence` (no-dot variant) → `OurDigital`
The organizer applies these to both files and folders at depth 2+. Top-level
folders (`NN_…`) are never auto-renamed.
**Important**: The script doesn't enforce the SEO-context caveat. When you
point the organizer at the whole Drive Stream or a non-SEO folder, it will
rename ALL D.intelligence references — which is wrong outside SEO context.
Only run the organizer on SEO folders (`02_SEO in Action/`,
`00_OurDigital/04_SEO/`, etc.). For non-SEO D.intelligence assets, do not
rename — they belong to the parent company.
See `patterns/gotchas.md` "D.intelligence vs OurDigital" for full guidance.
### Archive client files
Files inside `99_Project Archive/{client}/` should KEEP their original
filenames including real client names. The script skips these folders for
rename/move operations.
### Date stamps
Use `YYYYMMDD` (no separator) when embedding a date in a filename:
`OurDigital-Data Access & Management Agreement - 2026.gsheet`
`OurDigital-SEO Coaching-OOO 견적-20240612.gsheet`
When a date range is needed: `YYYYMMDD-YYYYMMDD`.
### Extensions and stub files
Google Drive Apps formats appear as 180-byte JSON stubs on the local
filesystem with these extensions:
| Drive type | Extension |
|-----------|-----------|
| Docs | `.gdoc` |
| Sheets | `.gsheet` |
| Slides | `.gslides`|
| Forms | `.gform` |
Renaming the stub on the filesystem only changes the local filename — the
backing Google document, its Doc ID, sharing links, and revision history are
untouched. Safe to rename. Cell-level / body-level changes still require
opening the file in the corresponding Google app.
## What the organizer does NOT enforce
- Cell content inside `.gsheet`, body content inside `.gdoc`, slide content
inside `.gslides`, or any binary file (`.pdf`, `.pptx`, `.xlsx`). Those need
manual editing in the source app. Track them with a `TODO.md` if needed
(see `02_SEO in Action/98_Training/TODO.md` for an example pattern).
- Folder maturity ordering — the NN prefix system is a convention, but the
script doesn't reorder or renumber folders.
- Per-folder `README.md` content beyond the AUTO-STRUCTURE block — manual
Topics / Notes sections are preserved.
## Sensitive folders (never auto-modified)
Internal regex list (`SENSITIVE_SUBFOLDER_PATTERNS` in `organizer.py`):
- `^04_Case Studies$`
- `^99_Project Archive$`
- `.*Archive$` (case-insensitive)
- `^진단.*$`
These folders are still indexed (counts appear in the README structure),
but no files inside them are renamed or moved.

View File

@@ -0,0 +1,31 @@
# Patterns library — gotcha folder
The "gotcha folder" Claude consults when doing **content-based reorganization**.
Filename pattern matching is handled deterministically by `code/organizer.py`;
this folder is for the judgment-calls Claude makes when reading file contents
to decide where something belongs.
## Files
- `canonical-files.md` — examples of well-named files in their proper homes.
When Claude sees a file and isn't sure where it goes, it pattern-matches
against these canonical examples.
- `canonical-folders.md` — well-organized folder shapes. What goes in
`02_SEO Audit Toolkit/` vs `03_SEO Hack Library/` vs `99_Project Archive/`.
- `categorization-rules.md` — explicit IF→THEN rules for placement decisions.
These trump heuristics; if a rule applies, follow it.
- `gotchas.md` — edge cases, ambiguous files, common mistakes, and how to
resolve them.
## How to extend
When the user encounters a new edge case or pattern that the script can't
handle automatically, add it here:
1. If the rule is deterministic (filename pattern → action), put it in
`code/organizer.py` (`RENAME_RULES`, `MOVE_RULES`) and document in
`shared/conventions.md`.
2. If the rule is judgment-based (read content, decide), add a new entry
here and reference it from `code/SKILL.md`'s content-based workflow.
The patterns library should grow over time as Claude + user encounter more
edge cases. Each addition is a "gotcha" the system has learned.

View File

@@ -0,0 +1,92 @@
# Canonical file examples
Reference set of well-named files in their correct homes. When Claude sees
an unfamiliar file, it should pattern-match against these and ask: "what
home best fits this file's shape?"
## Templates / blank artifacts → `05_Working Template/`
Filenames are generic and use placeholder client names (`OOO`, `OOOOOO`):
- `OurDigital-SEO Audit Template-20240922.gsheet`
- `OurDigital-Local SEO Audit 견적서.xlsx`
- `OurDigital-OOO 호텔 SEO Basic 패키지-20250416.gsheet`
- `OurDigital-OOOOOO SEO Audit & Treatment_20250120.gsheet`
- `OurDigital-SEO Coaching-OOO 견적-20240612.gsheet`
- `OurDigital-on-Page SEO-T&D Generator.gsheet`
- `OurDigital-SEO job tracker-2025.gsheet`
- `OurDigital-Data Access & Management Agreement - 2026.gsheet`
Pattern: `OurDigital-{topic}-{OOO/OOOOOO placeholder}-{YYYYMMDD}.{ext}`
## Audit toolkit reusable assets → `02_SEO Audit Toolkit/`
Reusable proposal templates, on-page elements kits, framework documents:
- `D.intelligence Lab-SEO Audit & Treatment 2025-Proposal-Template.pdf`
(legacy — should be renamed to `OurDigital-…`)
- `OurDigital-on-Pages Elements Kit-20250529.gsheet`
- `SEO Audit & Check_NotebookLM.wav`
Subfolders typical here: `참고 자료/`, `예시 자료 모음/`, `문서 양식/`,
`교육 자료/`, `Google Business Profile/`, `robots.txt/`, `sitemap.xml/`.
## Reference library / external research → `03_SEO Hack Library/`
Third-party PDFs, webinar materials, structured-data references, academic
papers, framework PPTs:
- `1-1-what-is-structured-data-structured-data-for-beginners.pdf`
- `2-1-the-yoast-seo-graph-structured-data-for-beginners.pdf`
- `SEO_Advanced_구조화 된 데이터 스키마의 이해.pptx`
- `OurDigital-SEO-curl-commands.pdf`
- `opensurvey_trend_search_2024.pdf`
Pattern: external-source-derived names (paper IDs, course module numbering,
vendor names) — not OurDigital-prefixed.
## Active engagement working files → `01_Active Workspaces/{NN_client}/`
Real client name in path; files reference the engagement directly:
- `01_Active Workspaces/00_Jamie Clinic/JAM-FAQ 엔트리 추출-정리-20260304.gsheet`
- `01_Active Workspaces/01_신라호텔 SEO/SLA - SEO KPIs and goal setup_20251020.gsheet`
- `01_Active Workspaces/01_신라호텔 SEO/Shilla_SEO_Performance_Scorecard.xlsx`
- `01_Active Workspaces/02_K Beauty CC 우승기업/[Note] K-Beauty CC 우승 기업 컨설팅_20260210.gdoc`
Pattern: `{ClientPrefix}-{topic}-{date}.{ext}` or `[Note] {description}.gdoc`.
Client name appears in BOTH the folder name AND the filename — that's expected.
## Past engagement deliverables → `04_Case Studies/`
Polished proposal/audit PDFs from completed work, used as portfolio reference.
Real client names retained:
- `신라호텔 SEO 제안서 개요.pdf`
- `소노 인터내셔널 Preliminary SEO Audit.pdf`
- `JHR-조선호텔 웹사이트 리뉴얼 SEO 전략 검토-20260303.gdoc`
- `그랜드 머큐어 임페리얼 팰리스 호텔 SEO 진단 컨설팅 계약서.pdf`
Pattern: client name first, deliverable type, optional date. Always real
names (this folder is the case-study portfolio).
## Archived completed projects → `99_Project Archive/{NN_client}/`
Full engagement archive — all working files preserved with original names:
- `99_Project Archive/01_그랜드 머큐어 임페리얼 SEO Basic/00_성과 측정 기준/...`
- `99_Project Archive/02_TNS 유학원 SEO 진단/Crawling data/...`
- `99_Project Archive/04_오현이혼상속센터/D.intelligence Lab-SEO Coaching-양제민 변호사-20240806.gsheet`
Pattern: client name in folder, original delivery filenames preserved
(do NOT normalize — these are historical records).
## Training materials → `98_Training/`
Generic, anonymized lesson content. Client-specific lesson notes belong
in the archive, NOT here:
- `98_Training/01_수업 계획_코스/Content Marketing Workout.md`
- `98_Training/01_수업 계획_코스/SEO 입문 기본교육 과정.gdoc`
- `98_Training/04_강의자료 스크린샷/Screenshot 2024-09-22 at *.png`
- `98_Training/09_외부 강의/Shopee e-Commerce SEO/Global e-Commerce SEO in Action 101.pdf`

View File

@@ -0,0 +1,96 @@
# Canonical folder shapes
What "well-organized" looks like at each level. Use these as anchors when
deciding whether to create a new subfolder or which existing one a file
should live in.
## Top-level practice area (e.g., `02_SEO in Action/`)
```
NN_Practice Area/
├── README.md # auto-indexed root
├── 00_Brand Management/ # 0005: frameworks / always-on
├── 01_Active Workspaces/ # current client engagements
├── 02_{Practice} Audit Toolkit/ # reusable audit assets
├── 03_{Practice} Hack Library/ # external reference / research
├── 04_Case Studies/ # past engagement portfolio
├── 05_Working Template/ # blank quote/audit/coaching templates
├── 10_{Productized Tool}/ # 1013: productized specialty
├── 11_{Specialty Module}/
├── 20_{Data Source}/ # 20s: data integrations
├── 98_Training/ # 98: training materials
└── 99_Project Archive/ # 99: completed engagements
```
Maturity convention by NN range:
- 0005: foundational / framework / always-on
- 1013: productized tools / specialties
- 2029: data integrations
- 9099: archive / training / closed work
## Active workspace shape (`01_Active Workspaces/`)
```
01_Active Workspaces/
├── README.md
├── 00_{Client A}/
│ ├── {client-prefix}-{deliverable}-{date}.{ext}
│ ├── [Note] {topic} {date}.gdoc
│ └── AI Reference/ # if engagement uses AI tools
└── 01_{Client B}/
└── ...
```
Each client gets one subfolder, NN-prefixed in chronological onboarding
order. Files use a short client prefix (e.g., `JAM-` for Jamie, `SLA-` for
신라호텔, `JHR-` for 조선호텔).
## Case study shape (`04_Case Studies/`)
Flat — files at the root, no subfolders:
```
04_Case Studies/
├── README.md
├── {ClientName} SEO {DeliverableType}.pdf
├── {ClientName} {DeliverableType} {date}.gdoc
└── ...
```
Each file is a portfolio-quality deliverable. No working files here.
## Project archive shape (`99_Project Archive/`)
```
99_Project Archive/
├── README.md
├── 01_{First archived client}/
│ ├── 00_{topic}/ # may have its own NN-prefixed subfolders
│ ├── 09_사전 진단 / # diagnostic phase
│ ├── 10_계약 관련/ # contract phase
│ ├── 90_고객사 수급자료/ # client-supplied materials
│ └── {original-filename}.{ext} # plus loose root files
└── 02_{Second archived client}/
└── ...
```
Sub-folder structure inside each archived client mirrors the original
engagement folder layout — nothing renamed or normalized.
## Audit toolkit shape (`02_SEO Audit Toolkit/`)
```
02_{Practice} Audit Toolkit/
├── README.md
├── 참고 자료/ # references
├── 예시 자료 모음/ # neutralized example deliverables
├── 문서 양식/ # blank document forms
├── 교육 자료/ # internal training assets
├── {tool}_standard check_{date}/ # versioned tool snapshots
├── Google Business Profile/ # specialty subdomain
├── robots.txt/ # specialty subdomain
└── sitemap.xml/ # specialty subdomain
```
Subfolders here are **topic-grouped reusable assets**. Each subfolder is
purpose-named (Korean OK) — not NN-prefixed because order doesn't matter.

View File

@@ -0,0 +1,62 @@
# Categorization rules
IF→THEN rules for deciding where a file belongs. Apply in order; first
match wins.
## Hard-coded rules (always honor)
| If file… | Then it belongs in… |
|---|---|
| Lives inside `04_Case Studies/` or `99_Project Archive/` | leave it (sensitive — never auto-move) |
| Filename starts with `Screenshot YYYY-MM-DD at` | parent folder's `screenshots/` subdir |
| Has extension `.crdownload`, `.tmp`, `.partial` | `_unsorted/` (temp / interrupted) |
| Has `D.intelligence` in name | rename to `OurDigital` (filesystem rename only) |
## Content + filename heuristics (judgment calls)
### Templates vs Case Studies
| Signal | Verdict |
|---|---|
| Contains placeholder strings `OOO` / `OOOOOO` in filename | Template — `05_Working Template/` |
| Filename is `OurDigital-{topic}-{specific-client}-{date}` AND client is real | Case Study or Active Workspace, NOT template |
| Real client name in filename + signed contract content | `04_Case Studies/` |
| Real client name + WIP / current-quarter date | `01_Active Workspaces/{NN_client}/` |
| Real client name + closure-marker content (final report, sign-off) | `99_Project Archive/{NN_client}/` |
### Reference library vs Audit toolkit
| Signal | Verdict |
|---|---|
| Source is external (academic paper, vendor whitepaper, foreign URL slug, paper ID `2286-Article…`) | `03_{Practice} Hack Library/` |
| Source is OurDigital-authored, intended as reusable proposal/audit asset | `02_{Practice} Audit Toolkit/` |
| Korean filename about a niche specialty (sitemap.xml, robots.txt) | `02_…/{specialty}/` subdirectory |
### Training material vs Engagement notes
| Signal | Verdict |
|---|---|
| Generic title, no client name (`SEO 입문 기본교육 과정.pdf`) | `98_Training/01_수업 계획_코스/` |
| Lesson notes referencing real lawyer/doctor/consultant by name | `99_Project Archive/{NN_client}/` (it's a record of the engagement) |
| Screenshot from a lesson | `98_Training/04_강의자료 스크린샷/` if generic; archive if it shows specific client data |
| External lecture deck (Shopee, Google event) | `98_Training/09_외부 강의/{Topic}/` |
### Quote / proposal / contract documents
| Signal | Verdict |
|---|---|
| Quote sheet for a specific client, engagement won → archived | `99_Project Archive/{NN_client}/` |
| Quote sheet for a specific client, engagement won → ongoing | `01_Active Workspaces/{NN_client}/` |
| Quote sheet that's been neutralized (client → OOO) | `05_Working Template/` |
| Signed contract PDF | usually `04_Case Studies/` (portfolio) or archive |
## Anti-rules (do NOT do these)
- **Do not rename files inside `04_Case Studies/` or `99_Project Archive/`**
even if they have `D.intelligence` in the name. Those are historical records
and the brand at the time was D.intelligence — preserving that is correct.
- **Do not change Korean folder names to English** (or vice versa). Language
is intentional.
- **Do not collapse client subfolders** even if they only have 12 files.
- **Do not move things to satisfy "everything balanced"** — empty subfolders
are fine if they represent reserved slots.

View File

@@ -0,0 +1,275 @@
# Gotchas
Edge cases the system has learned. When in doubt during interactive
content-based reorganization, check here first.
Each gotcha follows the format: **Pattern → Why it's tricky → Resolution**.
---
### Lesson notes for a specific lawyer/doctor/consultant
**Pattern**: `Notes [레슨] SEO 진단 & 관리 수업 - 양제민 변호사 6회차 (대면).gdoc`
or similar lesson-format file referencing a real client by name.
**Why tricky**: Looks like training material (it IS a lesson note), but it's
client-specific (양제민 variant of 오현이혼상속센터 engagement).
**Resolution**: Belongs in `99_Project Archive/{NN_그_클라이언트}/`, NOT in
`98_Training/`. Training is for generic, reusable content. Client-specific
lesson notes are engagement records.
---
### Quote sheet with `D.intelligence Lab-` prefix and a client name
**Pattern**: `D.intelligence Lab-SEO Coaching-오현법률사무소-20240612.gsheet`
**Why tricky**: Has both a brand-rebrand candidate (`D.intelligence Lab-`
`OurDigital-`) AND a real client name that should become `OOO`. But cell
content can't be normalized by filesystem rename.
**Resolution**:
1. Filesystem rename: `OurDigital-SEO Coaching-OOO 견적-20240612.gsheet`
2. Add to a `TODO.md` reminding the user to open the sheet in Google Sheets
and replace `오현법률사무소` and contact info inside cells with `OOO` /
placeholder text.
3. Move to `05_Working Template/` once both filesystem AND cell content are
neutralized. Until then, leaving the file in place with the rename done
is a valid intermediate state.
---
### Files at the root that look like they should be in a subfolder
**Pattern**: A top-level subfolder root contains 30+ loose files plus 0
subfolders. E.g., a `screenshots` collection directly at the top of
`98_Training/`.
**Why tricky**: The script's `MOVE_RULES` only catches very specific
patterns (`Screenshot YYYY-MM-DD…`). Manual moves often need judgment —
which subfolder should be created, what should be its name?
**Resolution**: Interactive content-based mode. Claude reads filenames in
batches, proposes a subfolder name (matching local language convention —
Korean if rest of folder is Korean), confirms with user, then moves with
`mv`. Update the parent README afterward via `our-gdrive-organize --scope index`.
---
### Korean vs English filename mixing inside one subfolder
**Pattern**: `02_SEO Audit Toolkit/` contains both English files
(`OurDigital-on-Pages Elements Kit-20250529.gsheet`) and Korean subfolders
(`참고 자료/`, `문서 양식/`).
**Why tricky**: Looks inconsistent at first glance, but is intentional —
files use English when they're "OurDigital products" and Korean when
they're "Korean-language reference materials."
**Resolution**: Don't normalize. Language tracks function:
- OurDigital-authored asset → English filename, OurDigital prefix
- External / Korean reference → Korean filename
- Subfolder for grouping Korean references → Korean folder name
---
### Empty subfolders
**Pattern**: `99_Project Archive/03_소노펠리체CC Local SEO/09_수급 정보/`
contains 0 files.
**Why tricky**: Tempting to delete to "clean up." But empty subfolders often
represent reserved engagement phases that the project just didn't reach,
or pending document deliveries.
**Resolution**: Leave empty subfolders alone unless the user explicitly
says to clean them up. They don't break anything.
---
### Numbered duplicates: `(1)`, `(2)` suffixes
**Pattern**: `D.intelligence Lab-SEO Coaching-오현법률사무소-20240612 (1).gsheet`
exists alongside the same name without the `(1)`.
**Why tricky**: Looks like a Drive sync duplicate, but `cmp` shows different
bytes — they're DIFFERENT Drive documents that happen to have the same name.
**Resolution**: `cmp` the two `.gsheet` stubs. If different (which they
usually are for `(N)`-suffixed files), preserve both with a suffix like
`(v1)`, `(v2)`, or `(legacy)`. Never overwrite blindly. If actually
identical bytes, ask user which to keep.
---
### `_unsorted/` accumulates over time
**Pattern**: After several `--scope move --apply` runs, `_unsorted/`
accumulates `.crdownload` / `.tmp` files that the user never went back to.
**Why tricky**: These are usually legitimate trash but occasionally a real
in-progress download.
**Resolution**: Don't auto-delete. Periodically prompt the user: "Your
`_unsorted/` has N files older than 30 days. Want to review?"
---
### A folder that mixes archive + active work
**Pattern**: A subfolder under `01_Active Workspaces/` contains both
ongoing work AND files from a finished engagement that should have been
archived.
**Why tricky**: Hard to tell from filenames alone. Need to check mtimes
and content (last-modified-recently → active; older + closure-marker docs →
should be archived).
**Resolution**: Interactive mode. Claude reads file mtimes + samples
content, proposes splitting into a new `99_Project Archive/{NN_client}/`
entry. User confirms before moving.
---
### D.intelligence vs OurDigital — parent company / child brand
**Pattern**: Files or folders named `D.intelligence …`, `D intelligence …`,
or with the legacy `D.intelligence Lab-` prefix.
**Why tricky**: D.intelligence is the **parent company**; OurDigital is its
**SEO-specialty child brand**. The rebrand-to-OurDigital rule only applies
when the asset is SEO-related. Non-SEO D.intelligence assets (consulting,
data, training in other practices) keep the D.intelligence name because
they belong to the parent company, not to OurDigital.
**Resolution**:
- Inside an SEO context (`02_SEO in Action/`, `00_OurDigital/04_SEO/`, or
any folder whose name contains "SEO"): apply the standard rename
`D.intelligence … → OurDigital …`.
- Outside SEO context: **do not rename**. Flag for user review and
document the asset's intended owning practice.
- The `RENAME_RULES` in `code/organizer.py` cover three variants
(`D.intelligence Lab-`, `D.intelligence`, and the no-dot `D intelligence`
typo). The rules don't enforce the SEO-context caveat — that's the
caller's responsibility (point the script at an SEO folder, not the
whole Drive Stream).
---
### Brand-variant typos that escape the regex
**Pattern**: A filename uses an off-spec spelling of `D.intelligence`
e.g., `D intelligence SEO Audit & Treatment.pdf` (no dot), or
`OurDigitial-…` (transposed letters), or `Techincal SEO` (transposed).
**Why tricky**: The standard `D\.intelligence` regex requires the literal
dot, so the no-dot variant slips through. Same for OurDigital typos —
they don't match the brand pattern at all and look like normal filenames.
**Resolution**:
1. When you find one during a manual review, do the rename via `mv` and
immediately consider whether to add a regex variant to `RENAME_RULES`.
2. The current rules cover: `D.intelligence Lab-`, `D.intelligence`, and
`D intelligence` (no-dot, word-boundaries to avoid false positives).
3. Common typos that are NOT in regex (because they're one-off mistakes):
`OurDigitial`, `Techincal`. Catch with `mv` during manual review.
---
### Real client names in `예시 자료 모음/`
**Pattern**: Files in `02_…/예시 자료 모음/` that still have real client
names in the filename — e.g., `OurDigital-SEO Audit-1gada.com-20240703.xlsx`,
`OurDigital-Sono International-Preliminary SEO Audit-20240927.gdoc`.
**Why tricky**: The folder's canonical role is "neutralized example
deliverables" — examples to show in pre-sales without exposing real client
data. A file with a real client name in this folder is a half-done
neutralization. The original engagement copy usually exists elsewhere
(`99_Project Archive/{NN_client}/` or `04_Case Studies/`).
**Resolution**:
1. Filesystem rename to neutralize the FILENAME using `OOO`-style
placeholders: `OurDigital-OOO 호텔 체인-Preliminary SEO Audit-…gdoc`.
2. Add to a `TODO.md` reminding the user to also neutralize CELL CONTENT
(real names, contact info, URLs, keyword examples) inside the source
Sheet/Doc — filesystem rename doesn't touch cell content.
3. Don't delete the file even though the original exists elsewhere — the
neutralized example serves a different purpose (sales / training) than
the archived original (engagement record).
4. If the original doesn't exist elsewhere, copy it to the right archive
folder FIRST before neutralizing the example.
---
### Near-duplicate templates across `문서 양식/` and `05_Working Template/`
**Pattern**: Same template name in both
`02_…/문서 양식/OurDigital-SEO Audit Template-{date1}.gsheet` and
`05_Working Template/OurDigital-SEO Audit Template-{date2}.gsheet` with
different dates (and different Doc IDs).
**Why tricky**: Looks like the same template at v1 and v2 (good cleanup
target — keep the newer, archive the older). But sometimes they're
genuinely different templates that just happen to share a name.
**Resolution**:
1. `cmp` the .gsheet stubs first. Always different (different Doc IDs)
for files at different dates — that just confirms they're separate
Drive Docs, not bytes-identical stubs.
2. The Doc IDs alone can't tell you whether the cell content is similar.
Open both Sheets in Google Drive. Usually one is a direct refinement
of the other (older = v1, newer = v2 with added rows/columns).
3. If clearly v1 / v2 of same template: delete v1, OR move v1 to
`05_Working Template/` with `(legacy v1)` suffix.
4. If genuinely different (e.g., one is "quick check" and other is
"comprehensive"): rename to disambiguate explicitly.
5. Always defer to the user for the open-and-compare step. Add to
`TODO.md` with both Doc IDs + paths so the user knows what to compare.
---
### Stray screenshot that turns out to be a process diagram
**Pattern**: A `Screenshot_YYYY-MM-DD…png` in a folder of templates that
the script's `MOVE_RULES` would normally route to a `screenshots/` subdir.
**Why tricky**: The MOVE_RULES regex (`^Screenshot \d{4}-\d{2}-\d{2}…`)
treats anything with that prefix as junk to be tucked away. But sometimes
the screenshot is actually a captured workflow diagram, org chart, or
reference visualization that has real value AND a meaningful home elsewhere.
**Resolution**:
- Always view the screenshot before moving it (use `Read` on the .png).
- If it's a diagram / reference visualization: rename to a descriptive
filename and move to the most relevant subfolder (often `참고 자료/`
for audit-toolkit context, `docs/` for code-related).
- If it's an actual junk screenshot (UI snapshot during work): apply the
default rule and move to `screenshots/`.
- The MOVE_RULES regex pattern uses the macOS default
`Screenshot YYYY-MM-DD at HH.MM.SS AM/PM.png` (with spaces). The
underscore variant `Screenshot_YYYY-MM-DD_at_*` does NOT match —
catch those manually during content review.
---
## Adding new gotchas
When you (Claude) encounter a new ambiguous case during a content-based
reorganization session, add an entry here BEFORE moving on. Format:
```
### Short pattern title
**Pattern**: filename / structure example.
**Why tricky**: what makes this hard.
**Resolution**: what to do.
---
```
This is how the system gets smarter over time. The patterns library is
the institutional memory.