Force ntn to authenticate as the same integration that writes the page by
injecting NOTION_API_TOKEN=$NOTION_API_KEY into every ntn subprocess env.
Notion scopes file uploads to the creating integration, so a mismatch caused
"Could not find file_upload with ID …" when attaching uploaded images.
Added test_upload_passes_token_env (9 total in test_ntn_files.py) and updated
code/CLAUDE.md to reflect that a separate ntn login is no longer required.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
FIX 1: _content_has_local_images and extract_local_images now skip lines inside
``` fences so a local-image ref inside a code block is not treated as real.
FIX 2: md_translate.translate() tracks fence state; callout/columns/mention
transforms are suppressed for lines inside ``` ... ``` blocks (pass verbatim).
FIX 3: main() catches NtnUploadError and prints a clean error + e.stderr before
sys.exit(1); body moved to _main_body(parser, args).
FIX 4: ntn_files.upload() raises NtnUploadError on empty stdout instead of
IndexError, giving a clean message combined with FIX 3.
FIX 5: Removed unused write_to_page() (both page paths are inlined).
FIX 6: create_page_markdown() omits the 'markdown' key when value is falsy,
avoiding empty-markdown sends on DB rows created without --file/--stdin.
FIX 7: CLAUDE.md documents --allow-deleting-content scope (markdown engine only).
TDD: Added test_fence_passthrough_no_transform (md_translate, suite now 8) and
test_local_image_inside_fence_ignored (engine_routing, suite now 6).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mirror the blocks path's error handling in the markdown engine:
- page + DB markdown two-phase image append now check append_to_page's
return and exit(1) on failure instead of printing success silently
- markdown DB upsert guards update_page_properties return
- restore blocks-engine page progress print + replace-failure print
- move unused parent computation into the create branch
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Merge-align the remaining OurDigital skill references to the authoritative
guides, preserving each skill's structure:
- 03-journal: brand identity, three-channel context, journal role/length
(English, 1,000-2,000 words), authority order, OurDigital-vs-Clinic boundary,
shared writing principles adapted for English. English voice preserved — the
Korean-blog-only rules (평서체, 전문용어 병기) were deliberately NOT imposed.
- 04-research: per-channel voice/tone aligned to Writing Style Guide v2.1;
fixed an incorrect 경어체 (~입니다) rule for the Korean blog → 평서체; SEO
numbers corrected (title ~40자/≤60, meta ≤155, English slug); added brand
identity + authority order + Clinic boundary.
05-document brand_config.json (corporate doc colors) and the claude-ai-export
snapshot were intentionally left out of scope.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The unquoted description contained "Costing methods: effort …" — the mid-string
": " made YAML treat it as a nested mapping ("mapping values are not allowed
here"), failing verify-skills since 2026-05-27. Single-quote the value (it has
inner double quotes but no apostrophes), preserving the exact text.
verify_skills.py: 76/76 skills valid.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
All estimates now archive to ~/Workspaces/ourdigital-space/estimates/
<YYYY-MM-DD>_<prospect|account>_<service>/ (single 견적 archive). Engine
SKILL.md documents the output-home convention (engine stays --out-dir-agnostic).
presales-seo Stage 5 writes the estimate to $EST (archive); Stage 6 deck reads
from $EST but stays in the engagement bundle.
(Workspace side, not in this repo: SHR estimate moved to the archive, pointer
left in the engagement, ourdigital-space/CLAUDE.md documents estimates/.)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The 숨고 GA4/GTM 중급 기본견적 is identical to the existing ga4_gtm_intermediate
course (17 lessons, ₩1,570,000) — labeled it as the 기본구성 standard + aliases
rather than duplicating. Added the genuinely distinct GA4/GTM 마케팅 애널리틱스
course (22 lessons incl. 메타/구글 솔루션 + 워크숍 + 트리트먼트) → list ₩4,070,000.
Validated: intermediate ₩1,570,000, marketing_analytics ₩4,070,000.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adopt: directory keeps its NN- ordering prefix; skill `name:` is the clean form
without it (dir 16-seo-schema-validator → name: seo-schema-validator). Nicer to
invoke, matches the original desktop/SKILL.md names, still globally unique.
- 71 root SKILL.md: name: NN-foo → name: foo (flat skills + reference-curator suite).
Plugins (mac-optimizer/multi-agent-guide/dintel-bootstrap) already clean; 95 already clean.
- scripts/migrate_skill_root.py: derive name = dirname minus NN- prefix (skill_name()).
- CLAUDE.md + SKILL-MIGRATION-GUIDE.md: document the dir-prefix / clean-name convention.
verify_skills.py: 0 name collisions across all renamed skills. (The ~/.claude/skills
symlinks were re-pointed to the clean names separately — filesystem only.)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Built from real D.intelligence docs (not the 견적 자료 folder, which has only
GA4/GTM + education — confirmed):
- digital_branding: TNS 유학원 디지털 브랜딩 진단 컨설팅 → ₩9,000,000 (5 fixed stages)
- digital_ads: 디하이브 디지털 광고·퍼포먼스 마케팅 대행 계약 → ₩6,000,000/월 retainer
(media-spend commission % is per-deal, kept as a parameter — not invented)
effort method now supports fixed-amount tasks (Unit Cost) and monthly retainers
(unit: monthly); render shows 고정/—//월 and retainer/commission notes.
Validated: branding ₩9.0M, ads ₩6.0M/월; no regression (SEO 25.0M, coaching 1.57M).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add a comprehensive load verifier and fix the two issues it found:
- scripts/verify_skills.py: validates every loadable unit (flat root, suite sub-skill,
plugin skill) with real YAML parsing, name regex + global uniqueness, frontmatter
<=1024, description sanity, plugin.json JSON validity, and orphan detection. Read-only.
- 92-tui-design-template (root + code/SKILL.md): fix invalid YAML `triggers:` block
(`- "a", "b"` multi-scalar list items) -> one phrase per list item.
- 17-seo-schema-generator: remove ">" from description ("generate -> validate" ->
"generate then validate"); angle brackets are disallowed in descriptions.
Result: 75/75 loadable skills valid — 0 failures, 0 name collisions, 0 orphans,
0 plugin-manifest errors (65 flat + 3 plugins + 7 suite sub-skills).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
pick_baseline now applies a premium floor: if prospect.vertical matches
rate_card.tiering.premium_verticals (luxury/premium/deluxe/5성/특1급…), the
auto-selected tier is floored to premium_min_tier (default basic) so a premium
single property won't drop to the smb entry tier.
Validated: L'Escape (hotel_luxury, 1 property) smb -> basic (₩10.5M);
non-luxury single -> smb (₩3.0M); SHR chain -> treatment (₩29.5M) unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Finish the migration for the dirs the bulk pass couldn't auto-handle:
- 90-reference-curator/SKILL.md: hand-authored suite orchestrator (pipeline overview,
7-stage table, /reference-curator run modes, install) — the single loadable entry.
- 90-reference-curator/0{1..7}-*/SKILL.md: generated from each sub-skill's desktop/SKILL.md.
- scripts/migrate_skill_root.py: generalized discovery to find nested suite sub-skills
(rglob desktop/code SKILL.md), so the migrator now handles suites too.
81-mac-optimizer, 91-multi-agent-guide, 94-dintel-bootstrap need NO root SKILL.md: they
are Claude Code plugins whose skill correctly lives at skills/<name>/SKILL.md (validated).
Adding a root SKILL.md there would violate plugin structure.
All SKILL.md repo-wide validate: flat-root=65, suite-sub=7, plugin-skills=3, 0 failures.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Run the additive migration pass from SKILL-MIGRATION-GUIDE: generate a root SKILL.md
for every skill that lacked one, copied from its desktop/SKILL.md (or code/SKILL.md),
with name set to the directory name and description + body preserved verbatim.
- scripts/migrate_skill_root.py: the reusable, non-destructive migrator (dry-run default).
- 61 new root SKILL.md (desktop source for most; code/SKILL.md for 61/62/92).
- Untouched: 16/17/95 (already had root); desktop/ and code/ packaging left intact.
- All 64 root SKILL.md validate: frontmatter <=1024, kebab name, description present.
Still MANUAL (no SKILL.md source — commands/README only), need hand-authored root SKILL.md:
81-mac-optimizer, 90-reference-curator, 91-multi-agent-guide, 94-dintel-bootstrap.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The full-merge commit removed 17's code/ and desktop/ folders along with the old
template-fill generator, leaving 17 structurally inconsistent with 16 (and the repo's
documented dual-platform convention). Restore them for the MERGED skill:
- code/CLAUDE.md — Claude Code directive pointing to the root two-mode pipeline
(no script duplication; root scripts/ stays the single source of truth).
- desktop/SKILL.md + skill.yaml — Claude Desktop directive for the two-mode skill.
- desktop/tools/{firecrawl,perplexity}.md — restored verbatim from git (still used by
Mode 1 crawl / Mode 2 research).
The old generator's logic stays retired; only the dual-platform folder structure and
relevant tool docs return. 17 now matches 16's layout.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Unify the two schema-generation scenarios into a single slot-17 skill, both
feeding one claims register -> build -> validate(16) pipeline:
- Mode 1 (existing site): NEW scripts/extract_site_claims.py turns URLs / local
HTML / a directory into a claims register. Existing JSON-LD -> CONFIRMED;
title/OpenGraph -> PENDING (never auto-shipped). + site-extraction-methodology.md
and bundled fixtures/site/ demo pages.
- Mode 2 (not-yet-published site): land the source-to-schema engine
(build_schema_drafts.py, type_templates.json, claims/source registers, 3 refs,
sample_claims.csv) from the Desktop builder.
- Rewrite SKILL.md (v2.0) around the two-mode framing; the claims register is the
shared pivot. Only CONFIRMED, non-conflicting claims become schema; unfilled
template slots are pruned, never emitted as placeholders.
- Retire the old template-fill generator (code/ + desktop/); update root CLAUDE.md.
Self-tested both chains end-to-end: Mode 2 sample -> build -> validate PASS (P0=0);
Mode 1 fixtures -> extract -> build -> validate PASS (P0=0), JSON-LD round-trips with
nested address intact. Fixed two adapter bugs (nested node promotion; relative-path URI).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add configurable sub-linear scope-scaling bands to rate_card.yaml; estimate.py
now multiplies monthly line-item rates by properties_total (local_seo) and
subbrands_total (onpage_entity), with the scope note written into the 견적.
Validated: L'Escape (1 property) stays at base 23-47M; SHR (25 properties,
5 sub-brands) scales to 54.8-110.6M (local ×4.5, on-page ×2.2).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Standardizes the pre-sales SEO + Knowledge Graph diagnostic (origin: Sono
Hotels & Resorts) into a reusable skill with findings→rate-card estimate,
editable PPTX sales deck, and Notion SEO Audit DB archiving.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
notion.blocks.children.list returns at most 100 blocks per call.
Without pagination, --replace only cleared the first 100 blocks
of a page, leaving the rest behind and producing surprising
partial-replace behavior.
Loop with next_cursor until has_more is false so the entire
page is cleared.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The `dintel-gtm-toolkit` Python scripts (analyze_container.py,
diff_versions.py, validate_tags.py, find_unused.py) have been
replaced by the DTM Agent skill set, which operates on live
containers via the GTM API instead of exported JSON.
- .claude/commands/gtm-guardian.md: rewrite as a deprecation
pointer with old→new mapping for every script/phase
(Phase 6 → /dtm-audit + /dtm-debug, Phase 7 → /dtm-taxonomy +
/dtm-lookup, etc.).
- custom-skills/62-gtm-validator/code/references/phase6-audit.md:
replace "D.intelligence GTM Toolkit Integration" section with
DTM Agent skill map, live-container workflow, and dtm CLI
examples; update objective #4.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
aiohttp was listed in requirements.txt but never imported by any of the
six Python scripts in custom-skills/31-notion-organizer/code/scripts.
The async HTTP work is done via notion-client 2.2.1, which uses httpx
internally (confirmed via `pip show notion-client`).
Removing the unused pin will stop future dependabot churn for a library
this skill doesn't depend on.
Verified before this change: aiohttp 3.13.4 (just merged via #6) was
inert — all 30 tests in test_notion_search.py pass, and all 6 scripts
import cleanly without aiohttp present.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces single-vendor (Ahrefs-only) tool defaults with a per-task
backend menu across all 14 SEO skills. Each skill now lists every
capable MCP in allowed-tools and documents how to pick between
Semrush, Ahrefs, OurSEO Agent (CLI + MCP), DataForSEO, and GSC
in its SKILL.md Data Source Selection section.
Tool stubs (~40 new files) populated per skill with capability
deltas, call patterns, and explicit "not for this skill when"
callouts so the menu is self-correcting.
Skills affected: 19-keyword-strategy, 20-serp-analysis,
21-position-tracking, 22-link-building, 23-content-strategy,
24-ecommerce, 25-kpi-framework, 26-international, 27-ai-visibility,
28-knowledge-graph, 31-competitor-intel, 32-crawl-budget,
33-migration-planner, 34-reporting-dashboard.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- D_intelligence (underscore) — typo, NOT a regex variant. Fix one-off
via mv; do not widen RENAME_RULES (false-positive risk on legit
underscore-separator filenames).
- Externally-generated filenames vs OurDigital convention. Default to
normalize {Client}_/Client_/client_ → CLIENT- inside Active Workspaces;
drop 14-digit timestamps to YYYYMMDD; preserve only when an external
system requires the literal name.
- Reference library naming inconsistency — DO NOT bulk-normalize. Mixed
naming reflects source provenance (Slideshare slugs, vendor whitepapers,
Korean blog captures). Group by topic into subfolders instead
(frameworks/, examples/, ko/) when count exceeds ~15 at root.
Patterns library now at 15 gotchas. Validated by live application during
01_Brand in Action audit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
Plaintext .env storage of NOTION_API_KEY is unnecessary risk — Notion
integration tokens grant write access to every connected page/database
and have no expiry. Document the 1Password CLI fetch pattern as the
preferred path; .env becomes a discouraged fallback for environments
without `op`.
SKILL.md adds a Credential handling section covering:
- One-shot inline fetch via `op read 'op://Development/Notion - Claude Agent/api-key'`
- Subshell-scoped export pattern for repeated use without shell-history exposure
- 1Password field-name conventions (api-key for the token)
- Anti-patterns to avoid (echo, CLI args, git commits, chat paste)
- Token rotation procedure
notion_writer.py error message updated to point at the 1Password recipe
when NOTION_API_KEY is missing, instead of telling users to create a .env.
Tested end-to-end: pushed a 22 KB markdown report (16 tables, 120 nested
checkboxes) using the inline 1Password fetch. Bot identity verified, all
markdown blocks rendered as native Notion blocks.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Final review caught that both --filter example locations used simplified
JSON ({"Status": "Done"}) that Notion's data_sources.query API rejects
with a 400. The script passes --filter verbatim, so users copy-pasting
the example would hit a confusing error.
Replace with Notion's actual filter shape:
{"property": "Status", "status": {"equals": "Done"}}
Also added a compound (and/or) example in CLAUDE.md so users have a
reference for combining filters.
30/30 tests still pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Slash command at custom-skills/31-notion-organizer/commands/notion-search.md
documents the CLI surface and JSON output schema. CLAUDE.md gains a
Semantic Search section explaining the 4-stage pipeline and env var
requirements. requirements.txt notes the optional anthropic SDK
dependency (the skill falls back to the claude CLI if missing).
Final task of Phase 3b-i. 30 tests passing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Inline code review polish:
- --filter is only meaningful in per-database mode (workspace search
doesn't accept Notion filter objects). Previously a user passing
--filter without --databases would have it silently parsed and
ignored. Now emit a stderr warning and clear the filter.
- cache_kwargs dict was built twice in run_search (once for cache_get,
once for cache_put). Build once before the rerank call.
30/30 tests still pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Builds a rerank prompt with title + flattened properties + excerpt for
each candidate, calls Claude, parses JSON, sorts by score descending,
takes top N. On any failure (LLM error, missing JSON, parse error,
non-list shape), falls back to candidates in input order with null
relevance/snippet.
4 tests: ordering, limit, parse-error fallback, exception fallback.
26 passing total.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Code review polish:
- Drop the unused `name` parameter from `_flatten_property` (it was
reserved for future per-property-name special-casing but never used).
- Add a regression test pinning that checkbox=False and number=0 are
preserved in the enriched output. The existing empty-value filter is
`if value not in (None, [], "")` which keeps falsy-but-meaningful
values, but the contract wasn't tested.
22/22 tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>