Commit Graph

58 Commits

  • fix(observer): clean up prompt_file early and fix test for analysis_relpath
    - Remove prompt_file immediately after shell expansion into -p arg,
      avoiding stale temp files during long analysis windows (greptile feedback)
    - Update test assertion to check analysis_relpath instead of analysis_file,
      matching the cross-platform relative path change from earlier commits
    
    Signed-off-by: Lidang-Jiang <lidangjiang@gmail.com>
  • fix(observer): clean up temp files on cd failure early return
    The cd "$PROJECT_DIR" failure path returned without removing prompt_file
    and analysis_file, leaving stale temp files in .observer-tmp/.
    
    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
    
    Signed-off-by: Lidang-Jiang <lidangjiang@gmail.com>
  • fix(observer): guard cd failure with early return and log message
    Address reviewer feedback: under set +e, a failing cd would silently
    leave CWD unchanged, causing the relative analysis path to break.
    Add || return with a diagnostic log entry.
    
    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
    
    Signed-off-by: Lidang-Jiang <lidangjiang@gmail.com>
  • fix(observer): anchor CWD to PROJECT_DIR before Claude invocation
    Reviewers correctly identified that the relative analysis_relpath
    (.observer-tmp/<file>) only resolves when CWD equals PROJECT_DIR.
    Without an explicit cd, non-Windows users launching the observer from
    a different directory would fail to read the analysis file.
    
    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
    
    Signed-off-by: Lidang-Jiang <lidangjiang@gmail.com>
  • fix(observer): improve Windows compatibility for temp files and Haiku prompt
    Address remaining issues from #842 after PR #903 moved temp files to
    PROJECT_DIR/.observer-tmp:
    
    Bug A (path resolution): Use relative paths (.observer-tmp/filename)
    in the prompt instead of absolute paths from mktemp. On Windows
    Git Bash/MSYS2, absolute paths use MSYS-style prefixes (/c/Users/...)
    that the spawned Claude subprocess may fail to resolve.
    
    Bug B (asks for permission): Add explicit IMPORTANT instruction block
    at the prompt start telling the Haiku agent it is in non-interactive
    --print mode and must use the Write tool directly without asking for
    confirmation.
    
    Additional improvements:
    - Pass prompt via -p flag instead of stdin redirect for Windows compat
    - Add .observer-tmp/ to .gitignore to prevent accidental commits
    
    Fixes #842
    
    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
    
    Signed-off-by: Lidang-Jiang <lidangjiang@gmail.com>
  • feat: pending instinct TTL pruning and /prune command (#725)
    * feat: add pending instinct TTL pruning and /prune command
    
    Pending instincts generated by the observer accumulate indefinitely
    with no cleanup mechanism. This adds lifecycle management:
    
    - `instinct-cli.py prune` — delete pending instincts older than 30 days
      (configurable via --max-age). Supports --dry-run and --quiet flags.
    - Enhanced `status` command — shows pending count, warns at 5+,
      highlights instincts expiring within 7 days.
    - `observer-loop.sh` — runs prune before each analysis cycle.
    - `/prune` slash command — user-facing command for manual pruning.
    
    Design rationale: council consensus (4/4) rejected auto-promote in
    favor of TTL-based garbage collection. Frequency of observation does
    not establish correctness. Unreviewed pending instincts auto-delete
    after 30 days; if the pattern is real, the observer will regenerate it.
    
    Generated with [Claude Code](https://claude.ai/code)
    via [Happy](https://happy.engineering)
    
    Co-Authored-By: Claude <noreply@anthropic.com>
    Co-Authored-By: Happy <yesreply@happy.engineering>
    
    * fix: remove duplicate functions, broaden extension filter, fix prune output
    
    - Remove duplicate _collect_pending_dirs and _parse_created_date defs
    - Use ALLOWED_INSTINCT_EXTENSIONS (.md/.yaml/.yml) instead of .md-only
    - Track actually-deleted items separately from expired for accurate output
    - Update README.md and AGENTS.md command counts: 59 → 60
    
    Generated with [Claude Code](https://claude.ai/code)
    via [Happy](https://happy.engineering)
    
    Co-Authored-By: Claude <noreply@anthropic.com>
    Co-Authored-By: Happy <yesreply@happy.engineering>
    
    * fix: address Copilot and CodeRabbit review findings
    
    - Use is_dir() instead of exists() for pending path checks
    - Change > to >= for --max-age boundary (--max-age 0 now prunes all)
    - Use CLV2_PYTHON_CMD env var in observer-loop.sh prune call
    - Remove unused source_dupes variable
    - Remove extraneous f-string prefix on static string
    
    Generated with [Claude Code](https://claude.ai/code)
    via [Happy](https://happy.engineering)
    
    Co-Authored-By: Claude <noreply@anthropic.com>
    Co-Authored-By: Happy <yesreply@happy.engineering>
    
    * fix: update AGENTS.md project structure command count 59 → 60
    
    Co-Authored-By: Claude <noreply@anthropic.com>
    
    * fix: address cubic and coderabbit review findings
    
    - Fix status early return skipping pending instinct warnings (cubic #1)
    - Exclude already-expired items from expiring-soon filter (cubic #2)
    - Warn on unparseable pending instinct age instead of silent skip (cubic #4)
    - Log prune failures to observer.log instead of silencing (cubic #5)
    
    Co-Authored-By: Claude <noreply@anthropic.com>
    
    * fix: YAML single-quote unescaping, f-string cleanup, add /prune to README
    
    - Fix single-quoted YAML unescaping: use '' doubling (YAML spec) not
      backslash escaping which only applies to double-quoted strings (greptile P1)
    - Remove extraneous f-string prefix on static string (coderabbit)
    - Add /prune to README command catalog and file tree (cubic)
    
    Co-Authored-By: Claude <noreply@anthropic.com>
    
    ---------
    
    Co-authored-by: Claude <noreply@anthropic.com>
    Co-authored-by: Happy <yesreply@happy.engineering>
  • security: remove supply chain risks, external promotions, and unauthorized credits
    - Remove zenith.chat references and @DRodriguezFX shoutout from README
    - Remove Inspiration Credits section (already in CHANGELOG.md)
    - Remove awesome-agent-skills reference from Links
    - Remove Plankton H3 section by @alxfazio (skill stays in skills/)
    - Remove brand names (InsAIts, VideoDB, Evos) from v1.9.0 notes
    - Remove @ericcai0814 individual credit from README (kept in CHANGELOG)
    - Add Security Guide to Links section
    - Replace curl-pipe-to-bash in autonomous-loops with review warning
    - Replace git clone in plankton-code-quality with review warning
    - Replace pip install git+ in agent-eval with review warning
    - Replace npm install -g in dmux-workflows with review warning
    - Add commercial API notice to nutrient-document-processing
    - Remove VideoDB maintainer credit from videodb skill
    - Replace skill-creator.app link with ECC-Tools GitHub App reference
  • Merge pull request #664 from ymdvsymd/fix/observer-sandbox-access-661
    fix(clv2): add --allowedTools to observer Haiku invocation (#661)
  • fix(clv2): use -e instead of -d for .git check in detect-project.sh
    In git worktrees, .git is a file (not a directory) containing a gitdir
    pointer. The -d test fails for worktree checkouts, causing project
    detection to fall through to the "global" fallback. Changing to -e
    (exists) handles both regular repos and worktrees correctly.
    
    Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  • fix(clv2): add --allowedTools to observer Haiku invocation (#661)
    The observer's Haiku subprocess cannot access files outside the project
    sandbox (/tmp/ for observations, ~/.claude/homunculus/ for instincts).
    Adding --allowedTools "Read,Write" grants the necessary file access
    while keeping the subprocess constrained by --max-turns and timeout.
    
    Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  • fix(observe): allow sdk-ts entrypoint in observation hook (#614)
    Clean surgical fix allowing sdk-ts entrypoint in observe hook for Agent SDK sessions. Has APPROVED review.
  • fix: observer memory explosion with throttling, re-entrancy guard, and tail sampling (#536)
    Three fixes for the positive feedback loop causing runaway memory usage:
    
    1. SIGUSR1 throttling in observe.sh: Signal observer only every 20
       observations (configurable via ECC_OBSERVER_SIGNAL_EVERY_N) instead
       of on every tool call. Uses a counter file to track invocations.
    
    2. Re-entrancy guard in observer-loop.sh on_usr1(): ANALYZING flag
       prevents parallel Claude analysis processes from spawning when
       signals arrive while analysis is already running.
    
    3. Cooldown + tail-based sampling in observer-loop.sh:
       - 60s cooldown between analyses (ECC_OBSERVER_ANALYSIS_COOLDOWN)
       - Only last 500 lines sent to LLM (ECC_OBSERVER_MAX_ANALYSIS_LINES)
         instead of the entire observations file
    
    Closes #521
  • fix(continuous-learning-v2): add lazy-start observer logic (#508)
    * feat(continuous-learning-v2): add lazy-start observer logic
    
    Auto-starts observer when observer.enabled: true in config and no .observer.pid exists.
    
    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
    
    * fix(continuous-learning-v2): address PR review concerns
    
    - Use flock for atomic check-then-act to prevent race conditions
    - Check both project-scoped AND global PID files before starting
    - Support CLV2_CONFIG override for config file path
    - Check disabled file in lazy-start logic
    - Use double-check pattern after acquiring lock
    
    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
    
    * fix(observe.sh): address PR review comments
    
    - Add stale PID cleanup via _CHECK_OBSERVER_RUNNING function
    - Add macOS fallback using lockfile when flock unavailable
    - Fix CLV2_CONFIG override: use EFFECTIVE_CONFIG for both check and read
    - Use proper Python context manager (with open() as f)
    - Deduplicate signaled PIDs to avoid duplicate USR1 signals
    
    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
    
    * fix(observe.sh): wrap macOS lockfile fallback in subshell with trap
    
    - Wrap lockfile block in subshell so exit 0 only terminates that block
    - Add trap for EXIT to clean up lock file on script interruption
    - Add -l 30 (30 second expiry) to prevent permanent lock file stuck
    
    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
    
    * fix(observe.sh): address remaining PR review comments
    
    - Validate PID is a positive integer before kill calls to prevent
      signaling invalid targets (e.g. -1 could signal all processes)
    - Pass config path via env var instead of interpolating shell variable
      into Python -c string to prevent injection/breakage on special paths
    - Check CLV2_CONFIG-derived directory for disabled file so disable
      guard respects the same config source as lazy-start
    
    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
    
    ---------
    
    Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
  • feat: active hours + idle detection gates for session-guardian (#413)
    * feat: add project cooldown log to prevent rapid observer re-spawn
    
    Adds session-guardian.sh, called by observer-loop.sh before each Haiku
    spawn. It reads ~/.claude/observer-last-run.log and blocks the cycle if
    the same project was observed within OBSERVER_INTERVAL_SECONDS (default
    300s).
    
    Prevents self-referential loops where a spawned session triggers
    observe.sh, which signals the observer before the cooldown has elapsed.
    
    Uses a mkdir-based lock for safe concurrent access across multiple
    simultaneously-observed projects. Log entries use tab-delimited format
    to handle paths containing spaces. Fails open on lock contention.
    
    Config:
      OBSERVER_INTERVAL_SECONDS   default: 300
      OBSERVER_LAST_RUN_LOG       default: ~/.claude/observer-last-run.log
    
    No external dependencies. Works on macOS, Linux, Windows (Git Bash/MSYS2).
    
    * feat: extend session-guardian with time window and idle detection gates
    
    Adds Gate 1 (active hours check) and Gate 3 (system idle detection) to
    session-guardian.sh, building on the per-project cooldown log from PR 1.
    
    Gate 1 — Time Window:
    - OBSERVER_ACTIVE_HOURS_START/END (default 800–2300 local time)
    - Uses date +%k%M with 10# prefix to avoid octal crash at midnight
    - Toolless on all platforms; set both vars to 0 to disable
    
    Gate 3 — Idle Detection:
    - macOS: ioreg + awk (built-in, no deps)
    - Linux: xprintidle if available, else fail open
    - Windows (Git Bash/MSYS2): PowerShell GetLastInputInfo via Add-Type
    - Unknown/headless: always returns 0 (fail open)
    - OBSERVER_MAX_IDLE_SECONDS=0 disables gate
    
    Fixes in this commit:
    - 10# base-10 prefix prevents octal arithmetic crash on midnight minutes
      containing digits 8 or 9 (e.g. 00:08 = "008" is invalid octal)
    - PowerShell output piped through tr -d '\r' to strip Windows CRLF;
      also uses [long] cast to avoid TickCount 32-bit overflow after 24 days
    - mktemp now uses log file directory instead of TMPDIR to ensure
      same-filesystem mv on Linux (atomic rename instead of copy+unlink)
    - mkdir -p failure exits 0 (fail open) rather than crashing under set -e
    - Numeric validation on last_spawn prevents arithmetic error on corrupt log
    
    Gate execution order: 1 (time, ~0ms) → 2 (cooldown, ~1ms) → 3 (idle, ~50ms)
    
    * fix: harden session guardian gates
    
    ---------
    
    Co-authored-by: Affaan Mustafa <affaan@dcube.ai>
  • fix(observe): 5-layer automated session guard to prevent self-loop observations (#399)
    * fix(observe): add 5-layer automated session guard to prevent self-loop observations
    
    observe.sh currently fires for ALL hook events including automated/programmatic
    sessions: the ECC observer's own Haiku analysis runs, claude-mem observer
    sessions, CI pipelines, and any other tool that spawns `claude --print`.
    
    This causes an infinite feedback loop where automated sessions generate
    observations that trigger more automated analysis, burning Haiku tokens with
    no human activity.
    
    Add a 5-layer guard block after the `disabled` check:
    
    Layer 1: agent_id payload field — only present in subagent hooks; skip any
             subagent-scoped session (always automated by definition).
    
    Layer 2: CLAUDE_CODE_ENTRYPOINT env var — Claude Code sets this to sdk-ts,
             sdk-py, sdk-cli, mcp, or remote for programmatic/SDK invocations.
             Skip if any non-cli entrypoint is detected. This is universal: catches
             any tool using the Anthropic SDK without requiring tool cooperation.
    
    Layer 3: ECC_HOOK_PROFILE=minimal — existing ECC mechanism; respect it here
             to suppress non-essential hooks in observer contexts.
    
    Layer 4: ECC_SKIP_OBSERVE=1 — cooperative env var any external tool can set
             before spawning automated sessions (explicit opt-out contract).
    
    Layer 5: CWD path exclusions — skip sessions whose working directory matches
             known observer-session path patterns. Configurable via
             ECC_OBSERVE_SKIP_PATHS (comma-separated substrings, default:
             "observer-sessions,.claude-mem").
    
    Also fix observer-loop.sh to set ECC_SKIP_OBSERVE=1 and ECC_HOOK_PROFILE=minimal
    before spawning the Haiku analysis subprocess, making the observer loop
    self-aware and closing the ECC→ECC self-observation loop without needing
    external coordination.
    
    Fixes: observe.sh fires unconditionally on automated sessions (#398)
    
    * fix(observe): address review feedback — reorder guards cheapest-first, fix empty pattern bug
    
    Two issues flagged by Copilot and CodeRabbit in PR #399:
    
    1. Layer ordering: the agent_id check spawns a Python subprocess but ran
       before the cheap env-var checks (CLAUDE_CODE_ENTRYPOINT, ECC_HOOK_PROFILE,
       ECC_SKIP_OBSERVE). Reorder to put all env-var checks first (Layers 1-3),
       then the subprocess-requiring agent_id check (Layer 4). Automated sessions
       that set env vars — the common case — now exit without spawning Python.
    
    2. Empty pattern bug in Layer 5: if ECC_OBSERVE_SKIP_PATHS contains a trailing
       comma or spaces after commas (e.g. "path1, path2" or "path1,"), _pattern
       becomes empty or whitespace-only, and the glob *""* matches every CWD,
       silently disabling all observations. Fix: trim leading/trailing whitespace
       from each pattern and skip empty patterns with `continue`.
    
    * fix: fail closed for non-cli entrypoints
    
    ---------
    
    Co-authored-by: Affaan Mustafa <affaan@dcube.ai>
  • fix: add confirmation-prompt guard to start-observer.sh (issue #400)
    - Redirect observer output to temp log before appending to main log
    - Check temp log for confirmation/permission language immediately after start
    - Fail closed with exit 2 if detected, preventing retry loops
  • fix: read tool_response field in observe.sh (#377)
    Claude Code sends tool output as `tool_response` in PostToolUse hook
    payloads, but observe.sh only checked for `tool_output` and `output`.
    This caused all observations to have empty output fields, making the
    observer pipeline blind to tool results.
    
    Adds `tool_response` as the primary field to check, with backward-
    compatible fallback to the existing `tool_output` and `output` fields.
  • fix(hooks): scrub secrets and harden hook security (#348)
    * fix(hooks): scrub secrets and harden hook security
    
    - Scrub common secret patterns (api_key, token, password, etc.) from
      observation logs before persisting to JSONL (observe.sh)
    - Auto-purge observation files older than 30 days (observe.sh)
    - Strip embedded credentials from git remote URLs before saving to
      projects.json (detect-project.sh)
    - Add command prefix allowlist to runCommand — only git, node, npx,
      which, where are permitted (utils.js)
    - Sanitize CLAUDE_SESSION_ID in temp file paths to prevent path
      traversal (suggest-compact.js)
    
    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
    
    * fix(hooks): address review feedback from CodeRabbit and Cubic
    
    - Reject shell command-chaining operators (;|&`) in runCommand, strip
      quoted sections before checking to avoid false positives (utils.js)
    - Remove command string from blocked error message to avoid leaking
      secrets (utils.js)
    - Fix Python regex quoting: switch outer shell string from double to
      single quotes so regex compiles correctly (observe.sh)
    - Add optional auth scheme match (Bearer, Basic) to secret scrubber
      regex (observe.sh)
    - Scope auto-purge to current project dir and match only archived
      files (observations-*.jsonl), not live queue (observe.sh)
    - Add second fallback after session ID sanitization to prevent empty
      string (suggest-compact.js)
    - Preserve backward compatibility when credential stripping changes
      project hash — detect and migrate legacy directories
      (detect-project.sh)
    
    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
    
    * fix(hooks): block $() substitution, fix Bearer redaction, add security tests
    
    - Add $ and \n to blocked shell metacharacters in runCommand to prevent
      command substitution via $(cmd) and newline injection (utils.js)
    - Make auth scheme group capturing so Bearer/Basic is preserved in
      redacted output instead of being silently dropped (observe.sh)
    - Add 10 unit tests covering runCommand allowlist blocking (rm, curl,
      bash prefixes) and metacharacter rejection (;|&`$ chaining), plus
      error message leak prevention (utils.test.js)
    
    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
    
    * fix(hooks): scrub parse-error fallback, strengthen security tests
    
    Address remaining reviewer feedback from CodeRabbit and Cubic:
    
    - Scrub secrets in observe.sh parse-error fallback path (was writing
      raw unsanitized input to observations file)
    - Remove redundant re.IGNORECASE flag ((?i) inline flag already set)
    - Add inline comment documenting quote-stripping limitation trade-off
    - Fix misleading test name for error-output test
    - Add 5 new security tests: single-quote passthrough, mixed
      quoted+unquoted metacharacters, prefix boundary (no trailing space),
      npx acceptance, and newline injection
    - Improve existing quoted-metacharacter test to actually exercise
      quote-stripping logic
    
    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
    
    * fix(security): block $() and backtick inside quotes in runCommand
    
    Shell evaluates $() and backticks inside double quotes, so checking
    only the unquoted portion was insufficient. Now $ and ` are rejected
    anywhere in the command string, while ; | & remain quote-aware.
    
    Addresses CodeRabbit and Cubic review feedback on PR #348.
    
    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
    
    ---------
    
    Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
  • fix(continuous-learning-v2): observer background process crashes immediately (#312)
    * fix(continuous-learning-v2): observer background process crashes immediately
    
    Three bugs prevent the observer from running:
    
    1. Nested session detection: When launched from a Claude Code session,
       the child process inherits CLAUDECODE env var, causing `claude` CLI
       to refuse with "cannot be launched inside another session". Fix: unset
       CLAUDECODE in the background process.
    
    2. set -e kills the loop: The parent script's `set -e` is inherited by
       the subshell. When `claude` exits non-zero (e.g. max turns reached),
       the entire observer loop dies. Fix: `set +e` in the background process.
    
    3. Subshell dies when parent exits: `( ... ) & disown` loses IO handles
       when the parent shell exits, killing the background process. Fix: use
       `nohup /bin/bash -c '...'` for full detachment, and `sleep & wait`
       to allow SIGUSR1 to interrupt sleep without killing the process.
    
    Additionally, the prompt for Haiku now includes the exact instinct file
    format inline (YAML frontmatter with id/trigger/confidence/domain/source
    fields), since the previous prompt referenced "the observer agent spec"
    which Haiku could not actually read, resulting in instinct files that
    the CLI parser could not parse.
    
    * fix: address review feedback on observer process management
    
    - Use `env` to pass variables to child process instead of quote-splicing,
      avoiding shell injection risk from special chars in paths
    - Add USR1_FIRED flag to prevent double analysis when SIGUSR1 interrupts
      the sleep/wait cycle
    - Track SLEEP_PID and kill it in both TERM trap and USR1 handler to
      prevent orphaned sleep processes from accumulating
    - Consolidate cleanup logic into a dedicated cleanup() function
    
    * fix: guard PID file cleanup against race condition on restart
    
    Only remove PID file in cleanup trap if it still belongs to the
    current process, preventing a restarted observer from losing its
    PID file when the old process exits.
  • fix: resolve CI failures on main — lint, hooks validator, and test alignment
    - Fix MD012 trailing blank lines in commands/projects.md and commands/promote.md
    - Fix MD050 strong-style in continuous-learning-v2 (escape __tests__ as inline code)
    - Extract doc-file-warning hook to standalone script to fix hooks validator regex parsing
    - Update session-end test to match #317 behavior (always update summary content)
    - Allow shell script hooks in integration test format validation
    
    All 992 tests passing.
  • feat: project-scoped instinct isolation
    * feat: add project-scoped instinct isolation
    
    * fix(continuous-learning-v2): harden instinct loading and promotion safety; sync v2.1 command docs
    
    * fix(ci): make copilot-setup-steps a valid GitHub Actions workflow
    
    * fix(hooks): stabilize docs warning inline JS regex parsing
  • feat: add security guides and sanitize external links across repo
    New articles:
    - the-security-guide.md: "The Shorthand Guide to Securing Your Agent" (595 lines)
      Attack vectors, sandboxing, sanitization, OWASP Top 10, observability
    - the-openclaw-guide.md: "The Hidden Danger of OpenClaw" (470 lines)
      Security analysis of OpenClaw, MiniClaw thesis, industry evidence
    
    External link sanitization (22 files across EN, zh-CN, zh-TW, ja-JP, .cursor):
    - Removed third-party GitHub links from skills and guides
    - Replaced with inline descriptions to prevent transitive prompt injection
    - Kept official org links (Anthropic, Google, Supabase, Mixedbread)
  • Merge pull request #273 from bintocher/bintocher/issue-246
    LGTM — Origin metadata for distributed skills. Pure metadata addition.
  • fix: add origin metadata to skills for traceability
    Add origin field to all skill files to track their source repository.
    This enables users to identify where distributed skills originated from.
    Fixes affaan-m/everything-claude-code#246
  • address review: remove .cursor/ duplicate, use is not None checks
    Changes based on CodeRabbit review feedback:
    
    1. Remove entire .cursor/ directory — it was an identical copy of the
       main skills/commands/agents/rules, causing maintenance drift.
       Users of Cursor can reference the canonical files directly.
    
    2. Use explicit `is not None` checks instead of truthiness for
       parsed['input'] and parsed['output']. Empty strings or empty
       dicts are valid values that should be preserved.
  • fix: use CLI argument for hook phase detection in observe.sh
    The observe.sh script receives "pre" or "post" as $1 from the hook
    config, but the Python code was looking for a "hook_type" field in
    the stdin JSON. Claude Code does NOT include "hook_type" in the
    JSON payload passed to hooks, so it always defaulted to "unknown",
    causing all observations to be recorded as "tool_complete" —
    PreToolUse events were never distinguished from PostToolUse.
    
    Fix: capture $1 as HOOK_PHASE and pass it to Python via env var.
    This also fixes TIMESTAMP export in the .cursor copy where inline
    `VAR=val cmd` syntax didn't propagate to the python subprocess.
  • fix: correct TIMESTAMP environment variable syntax in observe.sh
    The inline environment variable syntax `TIMESTAMP="$timestamp" echo ...` 
    does not work correctly because:
    1. The pipe creates a subshell that doesn't inherit the variable
    2. The environment variable is set for echo, not for the piped python
    
    Fixed by using `export` and separating the commands:
    - export TIMESTAMP="$timestamp"
    - echo "$INPUT_JSON" | python3 -c "..."
    
    This ensures the TIMESTAMP variable is available to the python subprocess.
    
    Fixes #227
    
    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
  • fix: include .md files in instinct-cli glob (completes #216)
    The observer agent creates instinct files as .md with YAML frontmatter,
    but load_all_instincts() only globbed *.yaml and *.yml. Add *.md to the
    glob so instinct-cli status discovers all instinct files.