Skip to content

Mission: Build professional, robust, and efficient software solutions using world-class standards.

Note: These are production standards. For learning side projects, prioritize speed and experimentation over perfection.

AI Agents: The operative subset of these rules (package managers, Python standards, web stack) is also in ~/.claude/CLAUDE.md → ## Dev Standards, which AI agents read at session start. Keep both in sync when updating core tooling decisions.

  • PRIORITY 1: Clear, semantic, understandable code that enhances maintainability
  • Simple and clear is ALWAYS superior to clever and complex
  • Exception: When execution speed is explicitly paramount, provide clear explanatory comments
  • Code should be self-documenting; comments explain WHY, not WHAT
  • Never use hacks or rigid fixed solutions; use robust solutions that flexibly handle all edge cases

When providing solutions, ALWAYS:

  • Ask for more details if needed to be very clear on the objective
  • Add “or better” to any objective to be achieved
  • Explore innovative, unconventional solutions combining effective insights in novel ways

Definitions of “Better” for Tech/Code:

  • Simpler, more understandable, more maintainable
  • Lower security risks, lower future risk (tech discontinuation)
  • Less vendor lock-in, better FOSS alignment
  • More efficient resource usage
  • Better error handling and debugging capability
  • Primary AI Tools: Currently using Claude Code, Cursor 2.0, Gemini 3.0
  • Supporting AI Tools: Crew AI, n8n when appropriate
  • Leverage AI for: Code generation, documentation, testing, refactoring, project orchestration
  • Critical: ALWAYS review and test AI-generated code. Never blindly commit.
  • Prefer FOSS solutions when functionality and deliverability don’t suffer
  • Avoid monthly subscription lock-ins unless significantly superior
  • Balance: Don’t be penny-wise and pound-foolish with time vs. cost
  • Priority Order: Functional and robust first. Then optimize for speed.
  • Speed is a feature, but not at the expense of correctness or maintainability
  • Ship minimal JavaScript
  • Optimize assets
  • Use SSG (Static Site Generation) by default
  • Profile before optimizing; document performance trade-offs
  • Site Framework: Astro (latest) for all websites - static content, blogs, marketing sites
  • Web Applications / PWAs: SvelteKit (adapter-static + Cloudflare Pages) for complex, state-heavy applications
  • UI Components: Use framework-appropriate component libraries
    • Currently using Shadcn-Svelte for Svelte/SvelteKit projects — add components via CLI (npx shadcn-svelte@latest add <component>)
  • Styling: Tailwind CSS
    • v4 is the target standard; SvelteKit apps currently use v3 for stability (migrate when shadcn-svelte v4 integration matures)
    • Use utility classes for layout and spacing
    • Use @apply or CSS variables for theming and reusable components
  • i18n: Paraglide (@inlang/paraglide-sveltekit) for SvelteKit apps requiring multiple languages — compiler-based, type-safe, zero runtime overhead
  • Design System: CSS variables + Utopia fluid responsive design
  • Content: Markdown (.md) or MDX (.mdx) for blog/documentation content
  • CMS: Currently using Decap CMS for blog content management
  • Static content site/blog: Astro + minimal JavaScript
  • Lightweight interactivity (menus, modals, simple state): Alpine.js or vanilla JS
  • Complex web application: Svelte or React-based framework
  • Default Rule: Use the simplest appropriate tool. Avoid over-engineering.
  • Node.js: Currently using pnpm (fast, disk-efficient)
  • Python: Currently using UV (replaces pip/poetry)
  • TypeScript: Strict mode required for production code. No any unless absolutely necessary.
  • Python: Type hints required for all non-trivial scripts. Follow PEP 8.
  • Primary: Currently using MariaDB for direct EspoCRM and Sendy integration
  • Modern Projects: Currently using PostgreSQL with Supabase
  • Local Development: SQLite with proper migration path to production
  • ORM: Currently using Prisma for type-safe database access
  • REST APIs: Follow RESTful conventions
  • Authentication: Currently using Supabase Auth or EspoCRM Portal
  • Error Handling: Consistent error response format
  • Rate Limiting: Implement for all public endpoints
  • Documentation: OpenAPI/Swagger for all endpoints
  • Primary: Currently using Cloudflare Pages for unbeatable performance
  • Alternative: Vercel for Jamstack hosting
  • CDN: Leverage Cloudflare’s global network
  • Build: Automatic builds from Git commits
  • Managed VPS: Currently using Cloudways on DigitalOcean Toronto
  • PHP Applications: EspoCRM, Sendy on LAMP stack
  • Node.js: For Astro SSR and APIs
  • Database: MariaDB on same VPS for performance
  • Version Control: GitHub with GitHub Actions for CI/CD
  • Secrets Management: Platform built-ins (Cloudflare Pages, n8n Cloud) or currently using Doppler
  • Monitoring: Currently using Uptime Kuma for service monitoring
  • Error Tracking: Currently using Sentry or similar for production error logging
project/
├── src/
│ ├── pages/ # Astro pages or app routes
│ ├── components/ # Reusable components
│ ├── layouts/ # Layout templates
│ ├── content/ # Markdown content (if applicable)
│ ├── lib/ # Utilities and helpers
│ ├── styles/ # Global CSS + design tokens
│ ├── assets/ # Images, fonts, static files
│ └── config/ # Configuration files
├── scripts/ # Build scripts, utilities
├── public/ # Static assets
├── dist/ # Build output (gitignored)
├── tests/ # Test files
├── docs/ # Documentation
│ └── Lessons.md # Learnings from development process
└── README.md # Setup and deployment guide
  • Files/Directories: kebab-case (e.g., my-component.astro, user-profile/)
  • Components: PascalCase (e.g., Button.astro, HeroSection.svelte)
  • Variables/Functions: camelCase (e.g., getUserData, isValid)
  • Constants: UPPER_SNAKE_CASE (e.g., MAX_RETRIES, API_BASE_URL)
  • Use descriptive, searchable names - avoid abbreviations and mental mapping
  • Functions/methods: Use verbs (calculateTotal, getUserData)
  • Variables: Use nouns (userEmail, totalAmount)
  • Follow SOLID principles
  • Prefer composition over inheritance
  • Use dependency injection for testability
  • Design for failure - assume things will break
  • Graceful Failure: Applications should never crash silently
  • Fail fast and fail clearly
  • Use specific exception types
  • Logging:
    • Dev: Console logging
    • Prod: Structured logging with correlation IDs in markdown format
    • Log errors with context (what, when, why)
  • Notification: Critical errors must notify developer (Email/SMS)
  • Provide actionable error messages
  • README.md for every project with setup instructions
  • Inline comments for complex business logic only (WHY, not WHAT)
  • API documentation for all public interfaces
  • Keep documentation close to code (avoid separate wikis)
  • Architecture Decisions: Document in Obsidian knowledge base as ADRs
  • Maintain single source of truth for architectural decisions
  • README.md: Setup, deployment, and contribution guidelines for every project
  • docs/Lessons.md: Capture learnings from development process
    • Document when AI or human struggles to produce results
    • Record solutions that worked well
    • Note patterns to avoid
    • Purpose: Make future development more effective and efficient
  • Inline Comments: Only for complex business logic (explain WHY, not WHAT)
  • API Documentation: Generate from OpenAPI specifications for all endpoints
  • Component Stories: Use Storybook or similar for component documentation when appropriate
  • ADRs (Architecture Decision Records): Document in Obsidian knowledge base
  • Tech Stack Decisions: Link to relevant business processes and rationale
  • Single Source of Truth: This Global-Dev-Rules.md for all development standards
  • Never commit secrets or credentials
  • Validate all inputs (client-side + server-side verification)
  • Use parameterized queries only (prevent SQL injection)
  • Follow principle of least privilege
  • Content Security Policy: Strict CSP headers
  • HTTPS: Enforce SSL/TLS everywhere
  • XSS Prevention: Escape all user content
  • CORS: Strict CORS policy configuration
  • Regular CVE scanning: Use tools like uv audit, pip-audit, safety check, or snyk
  • Keep dependencies updated to patched versions
  • Automate security scanning in CI/CD pipeline
  • Balance security patches with stability (not bleeding-edge, but not stale)
  • LCP (Largest Contentful Paint): Optimize images, lazy loading, critical resource hints
  • FID (First Input Delay): Minimize JavaScript, defer non-critical scripts
  • CLS (Cumulative Layout Shift): Define image dimensions, avoid layout shifts
  • Target: All metrics in “Good” range (green)
  • Images: Use modern formats (WebP, AVIF), responsive images with proper dimensions
  • CSS: Critical CSS inline, defer non-critical styles
  • JavaScript: Code splitting, tree shaking, minimal bundles
  • Fonts: Font display swap, preload critical fonts
  • Static Assets: Long cache headers (1 year)
  • HTML: Short cache with ETags
  • API Responses: Appropriate cache based on content type
  • CDN: Leverage Cloudflare’s caching capabilities
  • ARIA labels for interactive elements
  • Semantic HTML
  • Keyboard navigation support
  • Color contrast compliance (WCAG AA minimum)
  • Screen reader testing for critical flows

Mobile browsers enforce a minimum ~44-45px touch target on <button> elements, overriding CSS width/height regardless of units (rem, em, px). A small toggle button styled as a pill (e.g. 34×19px) will render as a square on mobile.

Fix: Use <span role="switch" tabindex="0"> (or <div role="...">) for custom-styled interactive controls. Inline/block elements don’t receive touch-target inflation. Add keyboard handlers (keydown for Space/Enter) for full accessibility if needed beyond basic dev tools.

Cloudflare _headers not applied by local dev servers

Section titled “Cloudflare _headers not applied by local dev servers”

public/_headers (Cloudflare Pages response header rules) is served as a plain static file by Python http.server and most local dev servers — it is not interpreted as HTTP headers. CSP-dependent bugs (blocked iframes, ORB-blocked images, mixed content) are invisible locally and only appear on the deployed Cloudflare Pages site.

Fix: Always test CSP-related behavior with a production or preview deployment, not local. For the monorepo template site: use web-deploy template online (not local) when debugging anything blocked by response headers.

Caching / headers / PWA freshness (hard-won, focus-pages bug recurred 3×)

Section titled “Caching / headers / PWA freshness (hard-won, focus-pages bug recurred 3×)”
  • Identify the actual web host BEFORE applying any header/cache fix — the mechanism is server-specific. .htaccess is Apache-only (IIS ignores it and even 404s .ht*); IIS/Plesk-Windows needs web.config (<clientCache cacheControlMode="DisableCache"/>); Cloudflare Pages needs _headers. Applying the wrong file is a silent no-op. Check first: curl -sI <url> → read the server: header. (talbotstevens.com = IIS today, migrating to Apache — so its focus pages ship BOTH web.config + .htaccess.)
  • Never trust HTTP cache behavior for Android PWA freshness. A no-cache header only governs future responses — it cannot evict an already-poisoned heuristic cache entry, and location.reload() honors that entry instead of hitting the network (iOS/Safari revalidates; Android Chrome does not). The durable, host-independent fix is client-side: stamp a unique <meta name="focus-build"> per build, and on load/resume fetch(url+'?_='+Date.now(), {cache:'no-store'}) — if the live build id differs, location.replace('?v='+id) (guaranteed cache-miss → fresh). Degrade gracefully offline.
  • Verify the mechanism actually landed live — uploaded ≠ applied. After any header/cache change, confirm on the wire: curl -sI <url> | grep -i cache-control. A deployed file proves nothing; only the live response header does.
  • Dedicated Gemini API key per utility — Gemini’s free tier quota is shared across all tools using the same key. Each AI utility must have its own GEMINI_API_KEY in its local .env to avoid cross-tool quota conflicts (e.g. pdf2md/.env, source_summarizer/.env).
  • Write .env files from WSL, not Windows — Copy-pasting .env content from Windows silently embeds Unicode/emoji characters in comments, garbling the file when Python’s dotenv reads it. Always write .env directly in WSL. Use ASCII-only comments in .env.example.
  • WSL cron + Windows binary paths — When a WSL cron job calls Windows binaries, use /mnt/c/... paths for discovery (os.path.exists, glob.glob). WSL interop handles execution transparently, but Linux Python cannot resolve C:\... backslash paths. Triggered whenever a refactor switches from uv.exe (Windows Python) to uv run (Linux Python) without updating path constants.
  • uv tool install breaks __file__-relative paths — When a Python CLI is installed via uv tool install, __file__ resolves to the tool’s site-packages, not the source tree. Use Path(os.environ.get("TOOL_HOME", "/known/absolute/path")) for config/data file paths in tools that have a fixed install location.
  • load_dotenv() with no path uses CWD — unreliable in cron — CWD is unpredictable in cron jobs. Always use load_dotenv(os.path.join(os.path.dirname(os.path.abspath(__file__)), '.env')) so the .env loads from the module’s own directory, regardless of where the process was launched.
  • Schedule monitoring jobs AFTER the jobs they monitor — A monitor that runs at 6 AM before the 10 AM jobs it watches will always false-alarm. Set the monitor’s cron time to after the last monitored job in the day (e.g. if last job is 1 PM, run monitor at 2 PM).
  • set VAR=value && in cmd.exe includes trailing space in the value — set PYTHONUTF8=1 && sets PYTHONUTF8 to "1 " (with trailing space). Python’s PYTHONUTF8 only accepts exactly "0" or "1" — the space causes a fatal crash before any Python runs. Use the quoted form set "VAR=value" && (strips trailing space) or just omit set entirely if not needed.
  • Path("D:") in Python is CWD-relative, not drive root — Path("D:") / "foo" resolves to D:<cwd>\foo, not D:\foo. When addressing a Windows drive root, use Path("D:/") or Path("D:\\") explicitly.
  • External HTTP data sources must fail gracefully, not crash the pipeline — Secondary/validation sources (price feeds, external APIs) will go down or add bot-verification walls. Guard all external HTTP calls with except requests.exceptions.RequestException: return [] so one unavailable source doesn’t abort the whole update. stooq.com added a JS bot-challenge ~2026-06-14 and is no longer usable via requests.
  • PWA manifest background_color must contrast with favicon SVG foreground — If the favicon SVG is monochromatic (e.g. #004425 leaf on transparent), using the same color as background_color makes it invisible on the Android splash screen and homescreen icon. Use white (#ffffff) or a neutral background when the favicon has no built-in background fill.
  • WSL→Windows .exe interop breaks silently when /etc/binfmt.d/WSLInterop.conf exists but is empty — WSL distros booting with systemd=true ([boot] systemd=true in /etc/wsl.conf, or now default on newer WSL) register the WSLInterop binfmt handler via this conf file instead of automatically via legacy init. If the file exists but has no content, systemctl status systemd-binfmt reports success while registering nothing — so any WSL process trying to exec a Windows .exe (powershell.exe, cmd.exe, etc. via subprocess) silently fails with no clear error pointing at the cause. Check cat /proc/sys/fs/binfmt_misc/WSLInterop (should print enabled) and the conf file’s actual content, not just its existence, before assuming interop is broken for some other reason. Fix: echo ':WSLInterop:M::MZ::/init:PF' | sudo tee /etc/binfmt.d/WSLInterop.conf && sudo systemctl restart systemd-binfmt. Discovered: web-deploy auto-open-browser regression, 2026-07-08.
  • A WSL Claude session cannot live-verify Windows-native Selenium/browser automation — no local Chrome/Selenium is available in WSL, and WSL→Windows interop may itself be down (as above). curl/WebFetch can’t render JS or hold an authenticated session, so a post-login SPA page (or any JS-rendered content) is invisible from WSL. Don’t guess at a DOM/selector fix blind. Instead: broaden error handling, add failure-diagnostic capture (screenshot + page source + URL, written to a debug folder on the next real failure) to the script itself, and ask the user to trigger one real run on Windows — then read the captured artifacts to write the actual fix. Discovered: my_backup Dynalist-capture timeout, 2026-07-08/09.
  • Cron’s minimal PATH resolves system node, not nvm’s — cron jobs don’t source an interactive shell’s rc files, so a script’s export PATH=... line that only adds pnpm/.local/bin still leaves cron falling back to /usr/bin/node (whatever apt installed) instead of nvm’s version, even when the interactive shell resolves the right one via .bashrc. Symptom is silent/delayed — a tool with a hard version floor (e.g. wrangler needs Node ≥22) starts failing in cron only, working fine when run manually. Fix: explicitly prepend the nvm node bin dir ($HOME/.nvm/versions/node/vX.Y.Z/bin) to the script’s own PATH export — don’t rely on nvm’s shell init being sourced. Verify with env -i HOME="$HOME" PATH="/usr/bin:/bin" ./script.sh to reproduce cron’s minimal PATH locally instead of trusting an interactive-shell test run. Discovered: mBR rate-scanner daily-run.sh — Cloudflare Pages deploy silently failed daily from 2026-07-13 (Wrangler requires at least Node.js v22.0.0. You are using v20.19.6) until traced to this, 2026-07-15.
  • A comment claiming “script X updates this later” is a claim, not a fact — verify it, don’t trust it — runner.py wrote ops-log status fields as "pending" with a comment saying daily-run.sh would update them after export/deploy; the referenced script never actually did, so the fields silently read “pending” forever, even on fully successful runs, masking whether deploys were actually succeeding. Any comment describing cross-file behavior (“X handles this”, “Y updates this later”) is an assumption baked in at write time that can rot the moment either file changes — grep the referenced file for the claimed behavior before relying on it, especially when auditing “is this working” claims. Discovered: same investigation as above, 2026-07-15.
  • A cron job’s >> file.log redirect adds no timestamp of its own — a script that just print()s its progress produces a log where every line looks identical run to run; only the file’s mtime tells you how stale it is, and that’s useless once the file keeps growing. Any script whose output is redirected by cron (not logged via a framework that timestamps automatically) should prefix its own lines with [YYYY-MM-DD HH:MM:SS] before printing. Discovered: my_backup’s send_status_report.py — logs/send_status_report.log had no way to tell which run produced which line, 2026-07-18.
  • A Windows Scheduled Task with RunLevel: Highest doesn’t resolve bare executable names via PATH — a task whose action is uv (not a fully-qualified path) works fine when run manually in an interactive PowerShell (which resolves PATH normally), but fails silently every time as a scheduled, elevated action (LastTaskResult: 2147942402 / 0x80070002 / ERROR_FILE_NOT_FOUND) — NumberOfMissedRuns climbs while Get-ScheduledTaskInfo shows it “ran” each time. Elevated Task Scheduler process launches don’t resolve Execute through the registry-persisted User PATH the way an interactive shell does. Fix: always use the fully-qualified path (C:\Users\<user>\.local\bin\uv.exe) in a scheduled task’s action; if the task doesn’t actually need admin rights, drop RunLevel to Limited entirely rather than patching the path — this also removes the separate risk of an unattended run silently hanging on a UAC prompt nobody’s there to click. Also: the task’s real registered name can differ from the utility’s name in its comment/description (e.g. a diskcheck util registered as task DiskSpaceCheck) — schtasks /query /tn "<expected-name>" and PowerShell Get-ScheduledTask -TaskName can both fail to find it by the name you’d guess; grep the full task list’s content (schtasks /query /fo LIST /v) instead of assuming the query-by-name failure means the task doesn’t exist. Verify a fix like this by actually triggering the task (Start-ScheduledTask, then check LastTaskResult), not just confirming the edit was accepted — a “command succeeded” result only proves the edit landed, not that the scheduled/unattended path works. Discovered: diskcheck/DiskSpaceCheck weekly disk-space monitor silently failing since ~Feb 2026 despite showing Enabled, System-Maintenance ongoing project, 2026-07-21.
  • A uv project’s default .venv on a cross-mounted drive is a silent WSL/Windows collision trap — if a WSL cron and a Windows Scheduled Task both invoke uv run in the same repo dir on a shared drive (e.g. D:) with neither passing UV_PROJECT_ENVIRONMENT, they share one .venv. A bare uv run <anything> typed manually from the other OS (e.g. testing a fix from WSL bash) silently rebuilds that .venv for its own platform — a Windows uv.exe cannot sync a Linux-built venv (symlinks like lib64 -> lib fail with Access is denied on Windows), so the production entrypoint on the other OS starts failing with no code change and no obvious cause. Fix is symmetric isolation, not remembering to set an env var: give both OS-side entrypoints their own UV_PROJECT_ENVIRONMENT on that OS’s own local disk (e.g. WSL → /home/ta/.venvs/<project>, Windows → C:\Users\<user>\.venvs\<project>) so neither ever touches the shared-drive default path — then a stray manual uv run just poisons an unused folder instead of breaking production. Discovered: my_backup Windows daily-backup task silently failed 07-19 through 07-26 (8 days) after a manual WSL verification run rebuilt the shared D: default .venv as Linux; Core/IT/Tasks/my_backup-no-backup.md, 2026-07-26.
  • schtasks /query /xml output piped through WSL is not real UTF-16, even though it declares encoding="UTF-16" — redirecting schtasks.exe /query /tn ... /xml > file from a WSL shell writes single-byte (ASCII/UTF-8-ish) bytes under a UTF-16 XML declaration; schtasks /create /xml on that same file then fails with ERROR: unable to switch the encoding. Editing the file’s text (e.g. changing the <Actions> block) doesn’t fix this — the declaration and the bytes must actually match. Fix: re-encode properly before importing — open(path, encoding='utf-8').read() then open(path, 'w', encoding='utf-16').write(data) (Python’s utf-16 codec writes a real BOM + 2-byte-per-char stream) — and verify with xxd (fffe 3c00 3f00 ... = real UTF-16LE-BOM) before handing the file to schtasks /create. Also: a cp/cleanup step done after this fix can silently re-clobber the corrected file with a stale copy from earlier in the same session — re-verify the final file’s bytes right before the user runs the command, don’t assume an earlier fix survived later shell operations. Discovered: same my_backup-no-backup task, 2026-07-26.
  • A Windows Scheduled Task switched to Password logon (“run whether user is logged on or not”) can run with nobody logged in — but WSL doesn’t auto-start in that unattended session. Under the old Interactive logon type, \\wsl$\<distro>\... paths only ever got hit while the user was logged in, and WSL was usually already running incidentally (an open terminal). Once the task can run fully unattended, any step touching \\wsl$ (a backup source, a wsl.exe heartbeat call) silently fails — resolveSymlink: stat: CreateFile \\wsl$\...: The specified network name is no longer available, followed by “WSL not running”. Fix: add wsl.exe -d <DistroName> -e true at the top of the wrapper script/task action, before anything touches \\wsl$ — this boots the distro explicitly regardless of logon state. Verify with a real schtasks /run + exit-code check after the change, not just that the edit was accepted. Discovered: my_backup-backup-stale, 2026-07-30 — surfaced as a second-order bug from fixing the original “task skipped when nobody logged in” issue.
  • Triggering a Windows Scheduled Task from WSL fails with a misleading “system cannot find the file specified,” even when the task and its target both exist. schtasks.exe /Run /TN "<name>", schtasks.exe /Query /TN "<name>", and PowerShell’s Start-ScheduledTask -TaskName "<name>" (invoked via powershell.exe from a WSL shell) all fail with this exact error for a task confirmed present and Ready via Get-ScheduledTask seconds earlier — the batch file the task points to also verifiably exists. Get-ScheduledTask (a read-only listing) works fine from WSL; anything that talks to the Task Scheduler engine to actually start/query a specific task (/Run, Start-ScheduledTask, /Query without /FO LIST) does not — a session/token limitation of the WSL→Windows process-interop boundary, not a real missing file. Fix: don’t try to trigger the task through the scheduler from WSL — run the task’s actual target executable/batch file directly instead (cmd.exe /c "<path-to-the-.bat-the-task-points-at>"), which exercises the identical production command without going through the scheduler engine. Discovered: my_backup-ecco-main-recovery, 2026-08-13, trying to force an ad hoc run of the “Daily Backup” task to verify a same-session config fix.
  • schtasks /create needs a genuinely elevated token — /query working proves nothing about /create. A non-elevated shell (WSL bash calling /mnt/c/Windows/System32/schtasks.exe, or a plain unelevated Windows terminal) can read every task via /query but fails /create with ERROR: Access is denied., even for an account in the Administrators group — whoami /groups on that token shows BUILTIN\Administrators ... Group used for deny only (UAC-filtered split token). This is true regardless of who runs it or from where; the only fix is a genuinely elevated shell (“Run as administrator”). Don’t waste rounds retrying the same command from a different non-elevated context. Discovered: my_backup-job-monitors-stale, 2026-09-17.
  • PowerShell does not parse \" as an escaped quote — that’s cmd.exe syntax. A command built for cmd.exe that embeds a literal " via backslash-escaping (/tr "wscript.exe \"C:\path\file.vbs\"") mis-parses in PowerShell: the parser mis-splits the argument and swallows the next flag into it (symptom: Invalid syntax. Mandatory option 'X' is missing. for a flag that’s clearly present in the typed command). Fix: wrap the value in single quotes instead — PowerShell passes a single-quoted string through literally, inner " included (/tr 'wscript.exe "C:\path\file.vbs"'). Verify a corrected command’s syntax cheaply by running it unelevated first: Invalid syntax means still-broken quoting, Access is denied (for anything needing elevation) means the syntax is now correct. Discovered: same task, 2026-09-17.
  • A Windows Task Scheduler “Run whether user is logged on or not” change can silently fail to save if the post-OK password prompt is cancelled or left blank — the Properties dialog shows the correct radio button selected even when the change never actually committed; there’s no error, just a dialog that looks right and a task that stays on the old logon mode. A screenshot of the dialog with the right option selected is not evidence the change landed. Verify with schtasks /query /tn "<name>" /v → Logon Mode: (Interactive/Background = fixed, Interactive only = still broken) after every such change, redoing it and actually entering the password at the prompt if it didn’t stick. Discovered: same task, 2026-09-17 — a round-2 fix believed done (with a supporting screenshot) turned out unsaved when checked against live system state in round 5.
  • A WSL symlink’s ls -la size is the target PATH STRING’s length, not the pointed-to file’s size — and Windows/\\wsl$ doesn’t follow WSL symlinks at all. ls -la on a symlink (e.g. 108 bytes for a ~100-char target path) looks like file corruption if misread as content size; readlink -f + cat show the real target and its actual content. Separately, apps reading over \\wsl$\<distro>\... from Windows (Explorer, browsers, most non-WSL-aware tools) do not resolve WSL symlinks — a symlink that works perfectly from WSL itself can appear broken/empty/tiny from the Windows side, with no error. Check both angles (stat vs. content, WSL-side vs. Windows-side) before concluding a symlinked file lost its content. If a file needs to be opened from Windows, use a real copy instead of a symlink. Discovered: web-deploy-artifacts task, 2026-09-26 — a “broken, 108 bytes” report was neither: the file’s real content (549 lines, populated dashboard) was intact, only the Windows-side symlink view was the problem.

3.10 Dev Project Artifact Placement (screenshots, PDFs, review bundles)

Section titled “3.10 Dev Project Artifact Placement (screenshots, PDFs, review bundles)”

The KB holds context and pointers, never the artifacts themselves. A task/log markdown file in the vault should never contain a binary reference-image, generated PDF, or review bundle directly under Tasks//Logs/ — that folder is scanned as a task/log list, and non-task binaries dropped there get misread as tasks (confirmed: LevPro screenshots left in SDC/IT/Tasks/<task>-review/ showed up in Tasks tables). The artifact’s real home is the dev project’s own repo, sorted by what kind of artifact it is:

KindExampleHomeCommitted?
Context/reference (used to build against; low churn, small, has standing reference value)Screenshots of a legacy app being ported, a vendor’s spec PDF<package>/docs/reference/ (mirrors sd-math/docs/accuracy-audit.md’s existing pattern)Yes — it’s the SSOT for “what were we matching”
Testing artifact — ephemeral (proves one session’s verification pass; no standing value once superseded)A screenshot confirming a form round-trips, a one-off Playwright captureThe agent’s scratchpad (/tmp/claude-*/.../scratchpad) or the repo’s already-gitignored test-results/playwright-reportNo — discard after the session that produced it is done
Testing artifact — audit-worthy (the record a later reviewer or task-close depends on: golden-fixture cross-checks, a generated PDF diffed against a spec, a full before/after bundle)sd-math’s M5 audit artifacts, a full-parity review bundle for a closed task<package>/docs/audit/<task-slug>/Yes — same footing as accuracy-audit.md

Rule for the KB task/log file itself: name the artifact and give the exact repo path + regeneration command (or note “one-off, not reproducible” if that’s true) — never embed or copy the binary into the vault. This is the same shape as the vault’s existing SSOT rule (one home, zero duplicates) applied specifically to dev artifacts, and the same “KB = context + why, repo = the thing itself” split the CONSTITUTION already draws for code.

(Discovered: sdc-sdapp-full-levpro-port, round 7 moved a LevPro-port review bundle — screenshots + a generated PDF — into SDC/IT/Tasks/<task>-review/, which Talbot correctly rejected round 8: it polluted the Tasks table and was the wrong home regardless. Round 9 wrote this rule and relocated the bundle into the repo per the table above.)

Section titled “3.11 Clickable links in VS Code’s Markdown preview”

VS Code’s built-in Markdown preview uses markdown-it, and its link-destination validator rejects the file: scheme outright — [label](file:///mnt/d/...) renders as literal unparsed bracket/paren text, not a clickable link (confirmed: md.validateLink('file:///...') returns false). This applies from a WSL-Remote window even when the path exists on the remote filesystem.

  • Link to a file in the repo: plain relative markdown link, [label](relative/path/to/file.ext) — opens in the editor.
  • Link to a folder: does not open — the preview’s link handler tries to open it as a file and silently fails. Always link the individual files inside the folder, never the folder path itself.
  • Link to a file outside the repo (e.g. a KB-vault note on a Windows drive): use VS Code’s own vscode://file/ protocol handler instead of file:// — D:\FSS\KB\SDC\IT\Tasks\foo.md → [label](vscode://file/D:/FSS/KB/SDC/IT/Tasks/foo.md) (forward slashes after the drive letter, %20 for spaces). This validates under markdown-it and opens the file directly on its native drive, sidestepping WSL-remote path-resolution entirely. It opens as plain markdown, not in Obsidian — that’s the ceiling for a one-click open from VS Code.

(Discovered: create-review-guide-skill, 2026-08-31 — the Review-LevPro.md guide’s static-proof and KB-note links looked plausible but were dead in practice; both failure modes reproduced locally with markdown-it before shipping the fix, rather than guessing again from a screenshot.)

3.12 Porting existing software — source the full archive, not screenshots-on-request

Section titled “3.12 Porting existing software — source the full archive, not screenshots-on-request”

When porting a legacy app, find and read its full source/spec archive before asking the user for screenshots one gap at a time. A complete archive (form source, string tables, sample output, layout specs) gives every workstream a deterministic on-disk acceptance target — enabling safe autonomous/parallel execution — whereas screenshot-by-screenshot elicitation is slow, interactive, and finds gaps only when someone happens to ask.

(Discovered: sdc-sdapp-full-levpro-port, rounds 1–11 sourced spec from screenshots the user produced on request, one gap at a time — frustrating and slow. Round 12 found the legacy app’s full VB6 source archive (7,972 lines of .frm form source, both language files, a sample PDF printout, layout .doc specs) already sitting on disk. Every remaining workstream then had a deterministic acceptance artifact, which is what made round 13’s 3-way parallel-subagent fan-out safe.)

Concurrent-subagent write protocol for a shared file (e.g. i18n strings): each agent owns a distinct key prefix, must read-modify-write the shared file (never overwrite), and dumps its own key set to a handoff file (docs/handoff/<agent>-<resource>.json) for the orchestrator to reconcile against the live file afterward. This makes a lost concurrent write detectable and repairable instead of silently invisible.

(Discovered: same task, round 13–14 — 4 agents wrote to the same i18n files concurrently; the handoff-file protocol verified 228/228 EN/FR keys survived with zero lost keys and zero value drift across all four writers.)

3.13 A generated file should be read-only, not just labelled

Section titled “3.13 A generated file should be read-only, not just labelled”

A “DO NOT HAND EDIT” banner asks. A read-only file attribute refuses. Any generator that owns a file end-to-end should chmod 444 it after writing, and chmod 644 it again immediately before the next rewrite.

This works across the WSL/Windows boundary, which is the part worth knowing. Verified 2026-09-21 on D: (drvfs/9p):

Terminal window
chmod 444 <file on /mnt/d/...>
cmd.exe /c attrib <file> # → A R D:\...
powershell Add-Content <file> # → UnauthorizedAccessException

So a chmod from WSL sets the Windows read-only attribute, and Obsidian refuses to save. The edit fails loudly instead of being silently overwritten on the next generator run.

Only for wholly-generated files. A file with a generated section and hand-written prose around it — Core/Misc/OrgChart.md is the live example, where only the ## Tree block is generated — must stay writable. Locking it would block the human half.

Keep the rule in each generator, not in a global hook. A vault-wide enforcer would need a registry of “which files are generated”, which is a second source of truth about a fact the generator already knows. Reference implementation: ~/utils/strategy-lint/ (make_writable / make_readonly), registered at Core/IT/Utils/Custom/strategy-lint.md.

The protection does not survive a copy. Deno.copyFile (and cp) preserve the source’s mode bits, so a build/sync pipeline that copies a read-only generated file into a local working copy (e.g. sync-content.ts copying vault content into a site’s src/content/docs/) produces a read-only copy too — and any later pipeline step that needs to rewrite that copy (a transform, a link-converter, a sidebar generator) breaks on it. The fix belongs at the copy site: chmod the destination writable immediately after copying. The protection is for the source, not for a disposable downstream copy. (Discovered: web-deploy-bug, 2026-09-26 — sync-content.ts copied strategy-lint’s read-only index.md files into 3 projects’ local content dirs; convert-wikilinks.ts then crashed trying to rewrite them.)

(Raised by Talbot 2026-09-22: “Is there a more robust way to ensure that files that should not be human edited CAN’T be edited?” — answer: yes, and it is three lines.)

  • Commit Messages: Use conventional commits format
    • feat:, fix:, docs:, refactor:, test:, chore:
  • Branching: Feature branches, no direct commits to main
  • Code Reviews: Required for all changes
  • Atomic Commits: One logical change per commit
  1. Plan: Define objective and requirements clearly, incorporating insights from docs/Lessons.md
  2. Generate: Use AI tools for boilerplate, refactoring, or implementations
  3. Verify: ALWAYS review and test AI-generated code
  4. Update docs/Lessons.md: Document useful learnings from dev process, especially when AI or human struggles to produce results, so future development efforts are more effective and efficient
  5. Iterate: Refine based on testing and world-class standards

AI agents: for how you must test your own work before reporting completion (autonomous testing, Levels 1–4, web/UI verification, anti-patterns), see the SSOT AI-Testing-Standards. The rules below are the codebase’s human + AI dev-testing philosophy.

  • Test behavior, not implementation
  • Unit tests for business logic
  • Integration tests for system boundaries
  • E2E tests for critical user paths
  • Aim for 80% coverage, focus on critical paths
  • Unit Tests: Vitest for component and utility testing
  • E2E Tests: Playwright for user flow testing
  • Performance: Lighthouse CI for performance regression
  • Accessibility: Axe-core for automated a11y testing
  • Linting: ESLint + Prettier for code consistency
  • Type Safety: TypeScript strict mode, Python type hints
  • Bundle Analysis: Analyze and optimize bundle size
  • Performance Budgets: Set and enforce performance budgets
  • Email: Amazon SES via Sendy for bulk emails
  • Analytics: Plausible Analytics for privacy-first tracking
  • Forms: Web3Forms or Formspree for form handling
  • Payments: Stripe for payment processing
  • Search: Meilisearch for site search functionality
  • Workflow Automation: Currently using n8n for data processing
  • Webhooks: Implement for real-time integrations
  • Cron Jobs: Server-side scheduling for maintenance tasks
  • Scripts and tools to automate tasks (Python/Bash)
  • Type hints for Python
  • Clear error handling and logging
  • Speed > Perfection
  • Experiment freely with new technologies
  • Document learnings, not every detail
  • Quality, Testing, and Stability are paramount
  • Follow ALL standards in this document
  • Comprehensive testing required
  • Monitoring from day one
  • Proper backup strategy (automated, tested, documented)

IF ANY TASK IS NOT VERY CLEAR OR ADDITIONAL INFORMATION IS NEEDED OR EVEN WOULD BE HELPFUL, ALWAYS ASK. IT IS MUCH MORE EFFECTIVE TO BE 100% CLEAR ON WHAT IS EXPECTED FROM THE START.

  • Follow naming conventions from this document and related naming systems
  • Never use hacks - find robust solutions
  • Review AI-generated code carefully
  • Write tests for critical paths
  • Document complex decisions
  • Run security scans
  • Check performance metrics
  • Verify error monitoring is active
  • Test backup and restore procedures
  • Review secrets management

Document Version: 1.0.0
Last Updated: 2024
Single Source of Truth: This document supersedes all previous development guidelines