merge: dmux worktree (selective install, orchestration, observer fixes)

This commit is contained in:
Affaan Mustafa
2026-03-14 12:55:56 -07:00
79 changed files with 11085 additions and 1149 deletions

View File

@@ -82,7 +82,7 @@ If the user chooses niche or core + niche, continue to category selection below
### 2b: Choose Skill Categories
There are 41 skills organized into 8 categories. Use `AskUserQuestion` with `multiSelect: true`:
There are 7 selectable category groups below. The detailed confirmation lists that follow cover 41 skills across 8 categories, plus 1 standalone template. Use `AskUserQuestion` with `multiSelect: true`:
```
Question: "Which skill categories do you want to install?"
@@ -90,7 +90,6 @@ Options:
- "Framework & Language" — "Django, Spring Boot, Go, Python, Java, Frontend, Backend patterns"
- "Database" — "PostgreSQL, ClickHouse, JPA/Hibernate patterns"
- "Workflow & Quality" — "TDD, verification, learning, security review, compaction"
- "Business & Content" — "Article writing, content engine, market research, investor materials, outreach"
- "Research & APIs" — "Deep research, Exa search, Claude API patterns"
- "Social & Content Distribution" — "X/Twitter API, crossposting alongside content-engine"
- "Media Generation" — "fal.ai image/video/audio alongside VideoDB"

View File

@@ -33,15 +33,6 @@ resolve_python_cmd() {
return 0
fi
# FIX: Windows Git Bash — probe Python install paths directly because
# `command -v python` can hit the Microsoft Store alias instead.
for win_py in /c/Users/"$USER"/AppData/Local/Programs/Python/Python3*/python; do
if [ -x "$win_py" ]; then
printf '%s\n' "$win_py"
return 0
fi
done
if command -v python3 >/dev/null 2>&1; then
printf '%s\n' python3
return 0
@@ -83,62 +74,44 @@ if [ -n "$STDIN_CWD" ] && [ -d "$STDIN_CWD" ]; then
fi
# ─────────────────────────────────────────────
# Configuration
# Lightweight config and automated session guards
# ─────────────────────────────────────────────
CONFIG_DIR="${HOME}/.claude/homunculus"
MAX_FILE_SIZE_MB=10
# Skip if disabled globally
# Skip if disabled
if [ -f "$CONFIG_DIR/disabled" ]; then
exit 0
fi
# ─────────────────────────────────────────────
# Automated session guards
# Prevents observe.sh from firing on non-human sessions to avoid:
# Prevent observe.sh from firing on non-human sessions to avoid:
# - ECC observing its own Haiku observer sessions (self-loop)
# - ECC observing other tools' automated sessions (e.g. claude-mem)
# - All-night Haiku usage with no human activity
# Run these before project detection so skipped sessions cannot mutate
# project-scoped observer state.
# ─────────────────────────────────────────────
# - ECC observing other tools' automated sessions
# - automated sessions creating project-scoped homunculus metadata
# Env-var checks first (cheapest — no subprocess spawning):
# Layer 1: CLAUDE_CODE_ENTRYPOINT — set by Claude Code itself to indicate how
# it was invoked. Only interactive terminal sessions should continue; treat any
# explicit non-cli entrypoint as automated so future entrypoint types fail closed
# without requiring updates here.
# Layer 1: entrypoint. Only interactive terminal sessions should continue.
case "${CLAUDE_CODE_ENTRYPOINT:-cli}" in
cli) ;;
*) exit 0 ;;
esac
# Layer 2: Respect ECC_HOOK_PROFILE=minimal — suppresses non-essential hooks
# Layer 2: minimal hook profile suppresses non-essential hooks.
[ "${ECC_HOOK_PROFILE:-standard}" = "minimal" ] && exit 0
# Layer 3: Cooperative skip env var — tools like claude-mem can set this
# (export ECC_SKIP_OBSERVE=1) before spawning their automated sessions
# Layer 3: cooperative skip env var for automated sessions.
[ "${ECC_SKIP_OBSERVE:-0}" = "1" ] && exit 0
# Layer 4: Skip subagent sessions — agent_id is only present when a hook fires
# inside a subagent (automated by definition, never a human interactive session).
# Placed after env-var checks to avoid a Python subprocess on sessions that
# already exit via Layers 1-3.
# Layer 4: subagent sessions are automated by definition.
_ECC_AGENT_ID=$(echo "$INPUT_JSON" | "$PYTHON_CMD" -c "import json,sys; print(json.load(sys.stdin).get('agent_id',''))" 2>/dev/null || true)
[ -n "$_ECC_AGENT_ID" ] && exit 0
# Layer 5: CWD path exclusions — skip known observer-session directories.
# Add custom paths via ECC_OBSERVE_SKIP_PATHS (comma-separated substrings).
# Whitespace is trimmed from each pattern; empty patterns are skipped to
# prevent an empty-string glob from matching every path.
# Layer 5: known observer-session path exclusions.
_ECC_SKIP_PATHS="${ECC_OBSERVE_SKIP_PATHS:-observer-sessions,.claude-mem}"
if [ -n "$STDIN_CWD" ]; then
IFS=',' read -ra _ECC_SKIP_ARRAY <<< "$_ECC_SKIP_PATHS"
for _pattern in "${_ECC_SKIP_ARRAY[@]}"; do
_pattern="${_pattern#"${_pattern%%[![:space:]]*}"}" # trim leading whitespace
_pattern="${_pattern%"${_pattern##*[![:space:]]}"}" # trim trailing whitespace
_pattern="${_pattern#"${_pattern%%[![:space:]]*}"}"
_pattern="${_pattern%"${_pattern##*[![:space:]]}"}"
[ -z "$_pattern" ] && continue
case "$STDIN_CWD" in *"$_pattern"*) exit 0 ;; esac
done
@@ -156,20 +129,12 @@ SKILL_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
source "${SKILL_ROOT}/scripts/detect-project.sh"
PYTHON_CMD="${CLV2_PYTHON_CMD:-$PYTHON_CMD}"
# ─────────────────────────────────────────────
# Configuration
# ─────────────────────────────────────────────
OBSERVATIONS_FILE="${PROJECT_DIR}/observations.jsonl"
SENTINEL_FILE="${CLV2_OBSERVER_SENTINEL_FILE:-${PROJECT_ROOT:-$PROJECT_DIR}/.observer.lock}"
write_guard_sentinel() {
printf '%s\n' 'observer paused: confirmation or permission prompt detected; rerun start-observer.sh --reset after reviewing observer.log' > "$SENTINEL_FILE"
}
# Skip if a previous run already aborted due to confirmation/permission prompt.
# This is the circuit-breaker — stops retrying after a non-interactive failure.
if [ -f "$SENTINEL_FILE" ]; then
echo "[observe] Skipping: previous run aborted due to confirmation/permission prompt. Remove ${SENTINEL_FILE} to re-enable." >&2
exit 0
fi
MAX_FILE_SIZE_MB=10
# Auto-purge observation files older than 30 days (runs once per session)
PURGE_MARKER="${PROJECT_DIR}/.last-purge"
@@ -263,46 +228,46 @@ if [ -f "$OBSERVATIONS_FILE" ]; then
fi
fi
# Detect confirmation/permission prompts in observer output and fail closed.
# A non-interactive background observer must never ask for user confirmation.
if echo "$PARSED" | grep -E -i -q "$CLV2_OBSERVER_PROMPT_PATTERN"; then
echo "[observe] OBSERVER_ABORT: Confirmation or permission prompt detected in observer output. This observer run is non-actionable." >&2
echo "[observe] Writing sentinel to suppress retries: ${SENTINEL_FILE}" >&2
write_guard_sentinel
exit 2
fi
# Build and write observation (now includes project context)
# Scrub common secret patterns from tool I/O before persisting
timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
export PROJECT_ID_ENV="$PROJECT_ID"
export PROJECT_NAME_ENV="$PROJECT_NAME"
export TIMESTAMP="$timestamp"
echo "$PARSED" | "$PYTHON_CMD" -c '
import json, sys, os, re
parsed = json.load(sys.stdin)
observation = {
"timestamp": os.environ["TIMESTAMP"],
"event": parsed["event"],
"tool": parsed["tool"],
"session": parsed["session"],
"project_id": os.environ.get("PROJECT_ID_ENV", "global"),
"project_name": os.environ.get("PROJECT_NAME_ENV", "global")
"timestamp": os.environ["TIMESTAMP"],
"event": parsed["event"],
"tool": parsed["tool"],
"session": parsed["session"],
"project_id": os.environ.get("PROJECT_ID_ENV", "global"),
"project_name": os.environ.get("PROJECT_NAME_ENV", "global")
}
# Scrub secrets: match common key=value, key: value, and key"value patterns
# Includes optional auth scheme (e.g., "Bearer", "Basic") before token
_SECRET_RE = re.compile(
r"(?i)(api[_-]?key|token|secret|password|authorization|credentials?|auth)"
r"""(["'"'"'\s:=]+)"""
r"([A-Za-z]+\s+)?"
r"([A-Za-z0-9_\-/.+=]{8,})"
r"(?i)(api[_-]?key|token|secret|password|authorization|credentials?|auth)"
r"""(["'"'"'\s:=]+)"""
r"([A-Za-z]+\s+)?"
r"([A-Za-z0-9_\-/.+=]{8,})"
)
def scrub(val):
if val is None:
return None
return _SECRET_RE.sub(lambda m: m.group(1) + m.group(2) + (m.group(3) or "") + "[REDACTED]", str(val))
if val is None:
return None
return _SECRET_RE.sub(lambda m: m.group(1) + m.group(2) + (m.group(3) or "") + "[REDACTED]", str(val))
if parsed["input"]:
observation["input"] = scrub(parsed["input"])
observation["input"] = scrub(parsed["input"])
if parsed["output"] is not None:
observation["output"] = scrub(parsed["output"])
observation["output"] = scrub(parsed["output"])
print(json.dumps(observation))
' >> "$OBSERVATIONS_FILE"

View File

@@ -8,16 +8,14 @@ origin: ECC
Distribute content across multiple social platforms with platform-native adaptation.
## When to Use
## When to Activate
- User wants to post content to multiple platforms
- Publishing announcements, launches, or updates across social media
- Repurposing a post from one platform to others
- User says "crosspost", "post everywhere", "share on all platforms", or "distribute this"
## How It Works
### Core Rules
## Core Rules
1. **Never post identical content cross-platform.** Each platform gets a native adaptation.
2. **Primary platform first.** Post to the main platform, then adapt for others.
@@ -25,7 +23,7 @@ Distribute content across multiple social platforms with platform-native adaptat
4. **One idea per post.** If the source content has multiple ideas, split across posts.
5. **Attribution matters.** If crossposting someone else's content, credit the source.
### Platform Specifications
## Platform Specifications
| Platform | Max Length | Link Handling | Hashtags | Media |
|----------|-----------|---------------|----------|-------|
@@ -34,7 +32,7 @@ Distribute content across multiple social platforms with platform-native adaptat
| Threads | 500 chars | Separate link attachment | None typical | Images, video |
| Bluesky | 300 chars | Via facets (rich text) | None (use feeds) | Images |
### Workflow
## Workflow
### Step 1: Create Source Content
@@ -89,7 +87,7 @@ Post adapted versions to remaining platforms:
- Stagger timing (not all at once — 30-60 min gaps)
- Include cross-platform references where appropriate ("longer thread on X" etc.)
## Examples
## Content Adaptation Examples
### Source: Product Launch
@@ -164,7 +162,7 @@ resp = requests.post(
"threads": {"text": threads_version}
}
},
timeout=30
timeout=30,
)
resp.raise_for_status()
```

View File

@@ -150,7 +150,7 @@ Example `plan.json`:
{
"sessionName": "skill-audit",
"baseRef": "HEAD",
"launcherCommand": "codex exec --cwd {worktree_path_sh} --task-file {task_file_sh}",
"launcherCommand": "codex exec --cwd {worktree_path} --task-file {task_file}",
"workers": [
{ "name": "docs-a", "task": "Fix skills 1-4 and write handoff notes." },
{ "name": "docs-b", "task": "Fix skills 5-8 and write handoff notes." }
@@ -176,7 +176,7 @@ Use `seedPaths` when workers need access to dirty or untracked local files that
"scripts/lib/tmux-worktree-orchestrator.js",
".claude/plan/workflow-e2e-test.json"
],
"launcherCommand": "bash {repo_root_sh}/scripts/orchestrate-codex-worker.sh {task_file_sh} {handoff_file_sh} {status_file_sh}",
"launcherCommand": "bash {repo_root}/scripts/orchestrate-codex-worker.sh {task_file} {handoff_file} {status_file}",
"workers": [
{ "name": "seed-check", "task": "Verify seeded files are present before starting work." }
]

View File

@@ -24,17 +24,14 @@ Exa MCP server must be configured. Add to `~/.claude.json`:
```json
"exa-web-search": {
"command": "npx",
"args": [
"-y",
"exa-mcp-server",
"tools=web_search_exa,web_search_advanced_exa,get_code_context_exa,crawling_exa,company_research_exa,people_search_exa,deep_researcher_start,deep_researcher_check"
],
"args": ["-y", "exa-mcp-server"],
"env": { "EXA_API_KEY": "YOUR_EXA_API_KEY_HERE" }
}
```
Get an API key at [exa.ai](https://exa.ai).
If you omit the `tools=...` argument, only a smaller default tool set may be enabled.
This repo's current Exa setup documents the tool surface exposed here: `web_search_exa` and `get_code_context_exa`.
If your Exa server exposes additional tools, verify their exact names before depending on them in docs or prompts.
## Core Tools
@@ -51,29 +48,9 @@ web_search_exa(query: "latest AI developments 2026", numResults: 5)
|-------|------|---------|-------|
| `query` | string | required | Search query |
| `numResults` | number | 8 | Number of results |
### web_search_advanced_exa
Filtered search with domain and date constraints.
```
web_search_advanced_exa(
query: "React Server Components best practices",
numResults: 5,
includeDomains: ["github.com", "react.dev"],
startPublishedDate: "2025-01-01"
)
```
**Parameters:**
| Param | Type | Default | Notes |
|-------|------|---------|-------|
| `query` | string | required | Search query |
| `numResults` | number | 8 | Number of results |
| `includeDomains` | string[] | none | Limit to specific domains |
| `excludeDomains` | string[] | none | Exclude specific domains |
| `startPublishedDate` | string | none | ISO date filter (start) |
| `endPublishedDate` | string | none | ISO date filter (end) |
| `type` | string | `auto` | Search mode |
| `livecrawl` | string | `fallback` | Prefer live crawling when needed |
| `category` | string | none | Optional focus such as `company` or `research paper` |
### get_code_context_exa
Find code examples and documentation from GitHub, Stack Overflow, and docs sites.
@@ -89,52 +66,6 @@ get_code_context_exa(query: "Python asyncio patterns", tokensNum: 3000)
| `query` | string | required | Code or API search query |
| `tokensNum` | number | 5000 | Content tokens (1000-50000) |
### company_research_exa
Research companies for business intelligence and news.
```
company_research_exa(companyName: "Anthropic", numResults: 5)
```
**Parameters:**
| Param | Type | Default | Notes |
|-------|------|---------|-------|
| `companyName` | string | required | Company name |
| `numResults` | number | 5 | Number of results |
### people_search_exa
Find professional profiles and bios.
```
people_search_exa(query: "AI safety researchers at Anthropic", numResults: 5)
```
### crawling_exa
Extract full page content from a URL.
```
crawling_exa(url: "https://example.com/article", tokensNum: 5000)
```
**Parameters:**
| Param | Type | Default | Notes |
|-------|------|---------|-------|
| `url` | string | required | URL to extract |
| `tokensNum` | number | 5000 | Content tokens |
### deep_researcher_start / deep_researcher_check
Start an AI research agent that runs asynchronously.
```
# Start research
deep_researcher_start(query: "comprehensive analysis of AI code editors in 2026")
# Check status (returns results when complete)
deep_researcher_check(researchId: "<id from start>")
```
## Usage Patterns
### Quick Lookup
@@ -147,27 +78,24 @@ web_search_exa(query: "Node.js 22 new features", numResults: 3)
get_code_context_exa(query: "Rust error handling patterns Result type", tokensNum: 3000)
```
### Company Due Diligence
### Company or People Research
```
company_research_exa(companyName: "Vercel", numResults: 5)
web_search_advanced_exa(query: "Vercel funding valuation 2026", numResults: 3)
web_search_exa(query: "Vercel funding valuation 2026", numResults: 3, category: "company")
web_search_exa(query: "site:linkedin.com/in AI safety researchers Anthropic", numResults: 5)
```
### Technical Deep Dive
```
# Start async research
deep_researcher_start(query: "WebAssembly component model status and adoption")
# ... do other work ...
deep_researcher_check(researchId: "<id>")
web_search_exa(query: "WebAssembly component model status and adoption", numResults: 5)
get_code_context_exa(query: "WebAssembly component model examples", tokensNum: 4000)
```
## Tips
- Use `web_search_exa` for broad queries, `web_search_advanced_exa` for filtered results
- Use `web_search_exa` for current information, company lookups, and broad discovery
- Use search operators like `site:`, quoted phrases, and `intitle:` to narrow results
- Lower `tokensNum` (1000-2000) for focused code snippets, higher (5000+) for comprehensive context
- Combine `company_research_exa` with `web_search_advanced_exa` for thorough company analysis
- Use `crawling_exa` to get full content from specific URLs found in search results
- `deep_researcher_start` is best for comprehensive topics that benefit from AI synthesis
- Use `get_code_context_exa` when you need API usage or code examples rather than general web pages
## Related Skills

View File

@@ -92,7 +92,6 @@ def post_thread(oauth, tweets: list[str]) -> list[str]:
if reply_to:
payload["reply"] = {"in_reply_to_tweet_id": reply_to}
resp = oauth.post("https://api.x.com/2/tweets", json=payload)
resp.raise_for_status()
tweet_id = resp.json()["data"]["id"]
ids.append(tweet_id)
reply_to = tweet_id