@uzysjung/agent-harness 26.83.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.ko.md +279 -0
- package/README.md +306 -0
- package/dist/chunk-SDVAM5JZ.js +775 -0
- package/dist/chunk-SDVAM5JZ.js.map +1 -0
- package/dist/index.js +5412 -0
- package/dist/index.js.map +1 -0
- package/dist/trust-tier-drift.js +67 -0
- package/dist/trust-tier-drift.js.map +1 -0
- package/package.json +53 -0
- package/scripts/prune-ecc.sh +310 -0
- package/templates/CLAUDE.md +86 -0
- package/templates/agents/build-error-resolver.md +114 -0
- package/templates/agents/code-reviewer.md +237 -0
- package/templates/agents/data-analyst.md +69 -0
- package/templates/agents/plan-checker.md +118 -0
- package/templates/agents/reviewer.md +128 -0
- package/templates/agents/security-reviewer.md +108 -0
- package/templates/agents/silent-failure-hunter.md +50 -0
- package/templates/agents/strategist.md +86 -0
- package/templates/antigravity/AGENTS.md.template +58 -0
- package/templates/codex/AGENTS.md.template +94 -0
- package/templates/codex/README.md +69 -0
- package/templates/codex/config.toml.template +108 -0
- package/templates/codex/hooks/README.md +40 -0
- package/templates/codex/hooks/gate-check.sh +7 -0
- package/templates/codex/hooks/hito-counter.sh +7 -0
- package/templates/codex/hooks/session-start.sh +7 -0
- package/templates/codex/hooks/uncommitted-check.sh +7 -0
- package/templates/codex/skills/uzys-build/SKILL.md +24 -0
- package/templates/codex/skills/uzys-plan/SKILL.md +24 -0
- package/templates/codex/skills/uzys-review/SKILL.md +24 -0
- package/templates/codex/skills/uzys-ship/SKILL.md +24 -0
- package/templates/codex/skills/uzys-spec/SKILL.md +28 -0
- package/templates/codex/skills/uzys-test/SKILL.md +24 -0
- package/templates/commands/ecc/checkpoint.md +32 -0
- package/templates/commands/ecc/e2e.md +105 -0
- package/templates/commands/ecc/eval.md +88 -0
- package/templates/commands/ecc/evolve.md +7 -0
- package/templates/commands/ecc/harness-audit.md +73 -0
- package/templates/commands/ecc/instinct-status.md +8 -0
- package/templates/commands/ecc/promote.md +10 -0
- package/templates/commands/ecc/security-scan.md +10 -0
- package/templates/commands/uzys/auto.md +190 -0
- package/templates/commands/uzys/build.md +42 -0
- package/templates/commands/uzys/plan.md +55 -0
- package/templates/commands/uzys/review.md +44 -0
- package/templates/commands/uzys/ship.md +49 -0
- package/templates/commands/uzys/spec.md +93 -0
- package/templates/commands/uzys/test.md +58 -0
- package/templates/docs/PLAN.template.md +102 -0
- package/templates/hooks/agentshield-gate.sh +101 -0
- package/templates/hooks/checkpoint-snapshot.sh +115 -0
- package/templates/hooks/gate-check.sh +138 -0
- package/templates/hooks/hito-counter.sh +26 -0
- package/templates/hooks/karpathy-gate.sh +59 -0
- package/templates/hooks/mcp-pre-exec.sh +104 -0
- package/templates/hooks/protect-files.sh +41 -0
- package/templates/hooks/session-start.sh +40 -0
- package/templates/hooks/spec-drift-check.sh +86 -0
- package/templates/mcp-allowlist.example +24 -0
- package/templates/mcp.json +20 -0
- package/templates/opencode/.opencode/commands/uzys-build.md +22 -0
- package/templates/opencode/.opencode/commands/uzys-plan.md +22 -0
- package/templates/opencode/.opencode/commands/uzys-review.md +22 -0
- package/templates/opencode/.opencode/commands/uzys-ship.md +22 -0
- package/templates/opencode/.opencode/commands/uzys-spec.md +28 -0
- package/templates/opencode/.opencode/commands/uzys-test.md +22 -0
- package/templates/opencode/.opencode/plugins/uzys-harness.ts +146 -0
- package/templates/opencode/AGENTS.md.template +98 -0
- package/templates/opencode/README.md +34 -0
- package/templates/opencode/opencode.json.template +42 -0
- package/templates/project-claude/_base.md +23 -0
- package/templates/project-claude/fragments/csr-fastapi/active-rules.md +13 -0
- package/templates/project-claude/fragments/csr-fastapi/agents.md +5 -0
- package/templates/project-claude/fragments/csr-fastapi/boundaries.md +18 -0
- package/templates/project-claude/fragments/csr-fastapi/commands.md +6 -0
- package/templates/project-claude/fragments/csr-fastapi/plugins.md +2 -0
- package/templates/project-claude/fragments/csr-fastapi/skills.md +5 -0
- package/templates/project-claude/fragments/csr-fastapi/stack.md +6 -0
- package/templates/project-claude/fragments/csr-fastapi/tagline.md +1 -0
- package/templates/project-claude/fragments/csr-fastapi/workflow.md +8 -0
- package/templates/project-claude/fragments/csr-fastify/active-rules.md +13 -0
- package/templates/project-claude/fragments/csr-fastify/agents.md +5 -0
- package/templates/project-claude/fragments/csr-fastify/boundaries.md +18 -0
- package/templates/project-claude/fragments/csr-fastify/commands.md +6 -0
- package/templates/project-claude/fragments/csr-fastify/plugins.md +2 -0
- package/templates/project-claude/fragments/csr-fastify/skills.md +5 -0
- package/templates/project-claude/fragments/csr-fastify/stack.md +6 -0
- package/templates/project-claude/fragments/csr-fastify/tagline.md +1 -0
- package/templates/project-claude/fragments/csr-fastify/workflow.md +8 -0
- package/templates/project-claude/fragments/csr-supabase/active-rules.md +12 -0
- package/templates/project-claude/fragments/csr-supabase/agents.md +5 -0
- package/templates/project-claude/fragments/csr-supabase/boundaries.md +19 -0
- package/templates/project-claude/fragments/csr-supabase/commands.md +6 -0
- package/templates/project-claude/fragments/csr-supabase/plugins.md +4 -0
- package/templates/project-claude/fragments/csr-supabase/skills.md +7 -0
- package/templates/project-claude/fragments/csr-supabase/stack.md +6 -0
- package/templates/project-claude/fragments/csr-supabase/supabase-auth.md +21 -0
- package/templates/project-claude/fragments/csr-supabase/tagline.md +1 -0
- package/templates/project-claude/fragments/csr-supabase/workflow.md +8 -0
- package/templates/project-claude/fragments/data/active-rules.md +10 -0
- package/templates/project-claude/fragments/data/agents.md +6 -0
- package/templates/project-claude/fragments/data/boundaries.md +20 -0
- package/templates/project-claude/fragments/data/commands.md +6 -0
- package/templates/project-claude/fragments/data/plugins.md +2 -0
- package/templates/project-claude/fragments/data/skills.md +3 -0
- package/templates/project-claude/fragments/data/stack.md +7 -0
- package/templates/project-claude/fragments/data/tagline.md +1 -0
- package/templates/project-claude/fragments/data/workflow.md +9 -0
- package/templates/project-claude/fragments/executive/active-rules.md +6 -0
- package/templates/project-claude/fragments/executive/agents.md +6 -0
- package/templates/project-claude/fragments/executive/boundaries.md +17 -0
- package/templates/project-claude/fragments/executive/commands.md +11 -0
- package/templates/project-claude/fragments/executive/plugins.md +1 -0
- package/templates/project-claude/fragments/executive/skills.md +7 -0
- package/templates/project-claude/fragments/executive/stack.md +4 -0
- package/templates/project-claude/fragments/executive/tagline.md +1 -0
- package/templates/project-claude/fragments/executive/workflow.md +10 -0
- package/templates/project-claude/fragments/growth-marketing/active-rules.md +7 -0
- package/templates/project-claude/fragments/growth-marketing/agents.md +6 -0
- package/templates/project-claude/fragments/growth-marketing/boundaries.md +17 -0
- package/templates/project-claude/fragments/growth-marketing/commands.md +11 -0
- package/templates/project-claude/fragments/growth-marketing/plugins.md +9 -0
- package/templates/project-claude/fragments/growth-marketing/skills.md +8 -0
- package/templates/project-claude/fragments/growth-marketing/stack.md +7 -0
- package/templates/project-claude/fragments/growth-marketing/tagline.md +1 -0
- package/templates/project-claude/fragments/growth-marketing/workflow.md +11 -0
- package/templates/project-claude/fragments/project-management/active-rules.md +7 -0
- package/templates/project-claude/fragments/project-management/agents.md +6 -0
- package/templates/project-claude/fragments/project-management/boundaries.md +16 -0
- package/templates/project-claude/fragments/project-management/commands.md +10 -0
- package/templates/project-claude/fragments/project-management/plugins.md +6 -0
- package/templates/project-claude/fragments/project-management/skills.md +5 -0
- package/templates/project-claude/fragments/project-management/stack.md +4 -0
- package/templates/project-claude/fragments/project-management/tagline.md +1 -0
- package/templates/project-claude/fragments/project-management/workflow.md +12 -0
- package/templates/project-claude/fragments/ssr-htmx/active-rules.md +11 -0
- package/templates/project-claude/fragments/ssr-htmx/agents.md +5 -0
- package/templates/project-claude/fragments/ssr-htmx/boundaries.md +20 -0
- package/templates/project-claude/fragments/ssr-htmx/commands.md +6 -0
- package/templates/project-claude/fragments/ssr-htmx/plugins.md +2 -0
- package/templates/project-claude/fragments/ssr-htmx/skills.md +3 -0
- package/templates/project-claude/fragments/ssr-htmx/stack.md +6 -0
- package/templates/project-claude/fragments/ssr-htmx/tagline.md +1 -0
- package/templates/project-claude/fragments/ssr-htmx/workflow.md +8 -0
- package/templates/project-claude/fragments/ssr-nextjs/active-rules.md +12 -0
- package/templates/project-claude/fragments/ssr-nextjs/agents.md +5 -0
- package/templates/project-claude/fragments/ssr-nextjs/boundaries.md +20 -0
- package/templates/project-claude/fragments/ssr-nextjs/commands.md +6 -0
- package/templates/project-claude/fragments/ssr-nextjs/plugins.md +2 -0
- package/templates/project-claude/fragments/ssr-nextjs/skills.md +5 -0
- package/templates/project-claude/fragments/ssr-nextjs/stack.md +5 -0
- package/templates/project-claude/fragments/ssr-nextjs/tagline.md +1 -0
- package/templates/project-claude/fragments/ssr-nextjs/workflow.md +8 -0
- package/templates/project-claude/fragments/tooling/active-rules.md +11 -0
- package/templates/project-claude/fragments/tooling/agents.md +5 -0
- package/templates/project-claude/fragments/tooling/boundaries.md +17 -0
- package/templates/project-claude/fragments/tooling/commands.md +4 -0
- package/templates/project-claude/fragments/tooling/skills.md +4 -0
- package/templates/project-claude/fragments/tooling/stack.md +5 -0
- package/templates/project-claude/fragments/tooling/tagline.md +1 -0
- package/templates/project-claude/fragments/tooling/workflow.md +5 -0
- package/templates/rules/api-contract.md +33 -0
- package/templates/rules/change-management.md +80 -0
- package/templates/rules/cli-development.md +39 -0
- package/templates/rules/code-style.md +23 -0
- package/templates/rules/data-analysis.md +61 -0
- package/templates/rules/database.md +29 -0
- package/templates/rules/design-workflow.md +17 -0
- package/templates/rules/error-handling.md +23 -0
- package/templates/rules/gates-taxonomy.md +21 -0
- package/templates/rules/git-policy.md +102 -0
- package/templates/rules/htmx.md +42 -0
- package/templates/rules/nextjs.md +35 -0
- package/templates/rules/playwright-launch.md +66 -0
- package/templates/rules/pyside6.md +59 -0
- package/templates/rules/shadcn.md +33 -0
- package/templates/rules/ship-checklist.md +24 -0
- package/templates/rules/tauri.md +40 -0
- package/templates/rules/test-policy.md +62 -0
- package/templates/settings.json +71 -0
- package/templates/skills/agent-introspection-debugging/SKILL.md +153 -0
- package/templates/skills/continuous-learning-v2/SKILL.md +365 -0
- package/templates/skills/continuous-learning-v2/config.json +8 -0
- package/templates/skills/continuous-learning-v2/hooks/observe.sh +428 -0
- package/templates/skills/continuous-learning-v2/scripts/detect-project.sh +228 -0
- package/templates/skills/continuous-learning-v2/scripts/instinct-cli.py +1426 -0
- package/templates/skills/deep-research/SKILL.md +155 -0
- package/templates/skills/deep-research/agents/openai.yaml +7 -0
- package/templates/skills/e2e-testing/SKILL.md +326 -0
- package/templates/skills/e2e-testing/agents/openai.yaml +7 -0
- package/templates/skills/eval-harness/SKILL.md +279 -0
- package/templates/skills/eval-harness/agents/openai.yaml +7 -0
- package/templates/skills/gh-issue-workflow/ISSUE.template.md +58 -0
- package/templates/skills/gh-issue-workflow/SKILL.md +184 -0
- package/templates/skills/investor-materials/SKILL.md +96 -0
- package/templates/skills/investor-outreach/SKILL.md +91 -0
- package/templates/skills/market-research/SKILL.md +75 -0
- package/templates/skills/market-research/agents/openai.yaml +7 -0
- package/templates/skills/nextjs-turbopack/SKILL.md +44 -0
- package/templates/skills/north-star/NORTH_STAR.template.md +114 -0
- package/templates/skills/north-star/SKILL.md +103 -0
- package/templates/skills/python-patterns/SKILL.md +750 -0
- package/templates/skills/python-testing/SKILL.md +816 -0
- package/templates/skills/spec-scaling/SKILL.md +89 -0
- package/templates/skills/strategic-compact/SKILL.md +131 -0
- package/templates/skills/strategic-compact/suggest-compact.sh +54 -0
- package/templates/skills/ui-visual-review/SKILL.md +154 -0
- package/templates/skills/verification-loop/SKILL.md +126 -0
- package/templates/skills/verification-loop/agents/openai.yaml +7 -0
- package/templates/track-mcp-map.tsv +15 -0
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Continuous Learning v2 - Observation Hook
|
|
3
|
+
#
|
|
4
|
+
# Captures tool use events for pattern analysis.
|
|
5
|
+
# Claude Code passes hook data via stdin as JSON.
|
|
6
|
+
#
|
|
7
|
+
# v2.1: Project-scoped observations — detects current project context
|
|
8
|
+
# and writes observations to project-specific directory.
|
|
9
|
+
#
|
|
10
|
+
# Registered via plugin hooks/hooks.json (auto-loaded when plugin is enabled).
|
|
11
|
+
# Can also be registered manually in ~/.claude/settings.json.
|
|
12
|
+
|
|
13
|
+
set -e
|
|
14
|
+
|
|
15
|
+
# Hook phase from CLI argument: "pre" (PreToolUse) or "post" (PostToolUse)
|
|
16
|
+
HOOK_PHASE="${1:-post}"
|
|
17
|
+
|
|
18
|
+
# ─────────────────────────────────────────────
|
|
19
|
+
# Read stdin first (before project detection)
|
|
20
|
+
# ─────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
# Read JSON from stdin (Claude Code hook format)
|
|
23
|
+
INPUT_JSON=$(cat)
|
|
24
|
+
|
|
25
|
+
# Exit if no input
|
|
26
|
+
if [ -z "$INPUT_JSON" ]; then
|
|
27
|
+
exit 0
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
resolve_python_cmd() {
|
|
31
|
+
if [ -n "${CLV2_PYTHON_CMD:-}" ] && command -v "$CLV2_PYTHON_CMD" >/dev/null 2>&1; then
|
|
32
|
+
printf '%s\n' "$CLV2_PYTHON_CMD"
|
|
33
|
+
return 0
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
if command -v python3 >/dev/null 2>&1; then
|
|
37
|
+
printf '%s\n' python3
|
|
38
|
+
return 0
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
if command -v python >/dev/null 2>&1; then
|
|
42
|
+
printf '%s\n' python
|
|
43
|
+
return 0
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
return 1
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
PYTHON_CMD="$(resolve_python_cmd 2>/dev/null || true)"
|
|
50
|
+
if [ -z "$PYTHON_CMD" ]; then
|
|
51
|
+
echo "[observe] No python interpreter found, skipping observation" >&2
|
|
52
|
+
exit 0
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
# ─────────────────────────────────────────────
|
|
56
|
+
# Extract cwd from stdin for project detection
|
|
57
|
+
# ─────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
# Extract cwd from the hook JSON to use for project detection.
|
|
60
|
+
# If cwd is a subdirectory inside a git repo, resolve it to the repo root so
|
|
61
|
+
# observations attach to the project instead of a nested path.
|
|
62
|
+
STDIN_CWD=$(echo "$INPUT_JSON" | "$PYTHON_CMD" -c '
|
|
63
|
+
import json, sys
|
|
64
|
+
try:
|
|
65
|
+
data = json.load(sys.stdin)
|
|
66
|
+
cwd = data.get("cwd", "")
|
|
67
|
+
print(cwd)
|
|
68
|
+
except(KeyError, TypeError, ValueError):
|
|
69
|
+
print("")
|
|
70
|
+
' 2>/dev/null || echo "")
|
|
71
|
+
|
|
72
|
+
# If cwd was provided in stdin, use it for project detection
|
|
73
|
+
if [ -n "$STDIN_CWD" ] && [ -d "$STDIN_CWD" ]; then
|
|
74
|
+
_GIT_ROOT=$(git -C "$STDIN_CWD" rev-parse --show-toplevel 2>/dev/null || true)
|
|
75
|
+
export CLAUDE_PROJECT_DIR="${_GIT_ROOT:-$STDIN_CWD}"
|
|
76
|
+
fi
|
|
77
|
+
|
|
78
|
+
# ─────────────────────────────────────────────
|
|
79
|
+
# Lightweight config and automated session guards
|
|
80
|
+
# ─────────────────────────────────────────────
|
|
81
|
+
#
|
|
82
|
+
# IMPORTANT: keep these guards above detect-project.sh.
|
|
83
|
+
# Sourcing detect-project.sh creates project-scoped directories and updates
|
|
84
|
+
# projects.json, so automated sessions must return before that point.
|
|
85
|
+
|
|
86
|
+
CONFIG_DIR="${HOME}/.claude/homunculus"
|
|
87
|
+
|
|
88
|
+
# Skip if disabled (check both default and CLV2_CONFIG-derived locations)
|
|
89
|
+
if [ -f "$CONFIG_DIR/disabled" ]; then
|
|
90
|
+
exit 0
|
|
91
|
+
fi
|
|
92
|
+
if [ -n "${CLV2_CONFIG:-}" ] && [ -f "$(dirname "$CLV2_CONFIG")/disabled" ]; then
|
|
93
|
+
exit 0
|
|
94
|
+
fi
|
|
95
|
+
|
|
96
|
+
# Prevent observe.sh from firing on non-human sessions to avoid:
|
|
97
|
+
# - ECC observing its own Haiku observer sessions (self-loop)
|
|
98
|
+
# - ECC observing other tools' automated sessions
|
|
99
|
+
# - automated sessions creating project-scoped homunculus metadata
|
|
100
|
+
|
|
101
|
+
# Layer 1: entrypoint. Only interactive terminal sessions should continue.
|
|
102
|
+
# sdk-ts: Agent SDK sessions can be human-interactive (e.g. via Happy).
|
|
103
|
+
# Non-interactive SDK automation is still filtered by Layers 2-5 below
|
|
104
|
+
# (ECC_HOOK_PROFILE=minimal, ECC_SKIP_OBSERVE=1, agent_id, path exclusions).
|
|
105
|
+
case "${CLAUDE_CODE_ENTRYPOINT:-cli}" in
|
|
106
|
+
cli|sdk-ts) ;;
|
|
107
|
+
*) exit 0 ;;
|
|
108
|
+
esac
|
|
109
|
+
|
|
110
|
+
# Layer 2: minimal hook profile suppresses non-essential hooks.
|
|
111
|
+
[ "${ECC_HOOK_PROFILE:-standard}" = "minimal" ] && exit 0
|
|
112
|
+
|
|
113
|
+
# Layer 3: cooperative skip env var for automated sessions.
|
|
114
|
+
[ "${ECC_SKIP_OBSERVE:-0}" = "1" ] && exit 0
|
|
115
|
+
|
|
116
|
+
# Layer 4: subagent sessions are automated by definition.
|
|
117
|
+
_ECC_AGENT_ID=$(echo "$INPUT_JSON" | "$PYTHON_CMD" -c "import json,sys; print(json.load(sys.stdin).get('agent_id',''))" 2>/dev/null || true)
|
|
118
|
+
[ -n "$_ECC_AGENT_ID" ] && exit 0
|
|
119
|
+
|
|
120
|
+
# Layer 5: known observer-session path exclusions.
|
|
121
|
+
_ECC_SKIP_PATHS="${ECC_OBSERVE_SKIP_PATHS:-observer-sessions,.claude-mem}"
|
|
122
|
+
if [ -n "$STDIN_CWD" ]; then
|
|
123
|
+
IFS=',' read -ra _ECC_SKIP_ARRAY <<< "$_ECC_SKIP_PATHS"
|
|
124
|
+
for _pattern in "${_ECC_SKIP_ARRAY[@]}"; do
|
|
125
|
+
_pattern="${_pattern#"${_pattern%%[![:space:]]*}"}"
|
|
126
|
+
_pattern="${_pattern%"${_pattern##*[![:space:]]}"}"
|
|
127
|
+
[ -z "$_pattern" ] && continue
|
|
128
|
+
case "$STDIN_CWD" in *"$_pattern"*) exit 0 ;; esac
|
|
129
|
+
done
|
|
130
|
+
fi
|
|
131
|
+
|
|
132
|
+
# ─────────────────────────────────────────────
|
|
133
|
+
# Project detection
|
|
134
|
+
# ─────────────────────────────────────────────
|
|
135
|
+
|
|
136
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
137
|
+
SKILL_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
138
|
+
|
|
139
|
+
# Source shared project detection helper
|
|
140
|
+
# This sets: PROJECT_ID, PROJECT_NAME, PROJECT_ROOT, PROJECT_DIR
|
|
141
|
+
source "${SKILL_ROOT}/scripts/detect-project.sh"
|
|
142
|
+
PYTHON_CMD="${CLV2_PYTHON_CMD:-$PYTHON_CMD}"
|
|
143
|
+
|
|
144
|
+
# ─────────────────────────────────────────────
|
|
145
|
+
# Configuration
|
|
146
|
+
# ─────────────────────────────────────────────
|
|
147
|
+
|
|
148
|
+
OBSERVATIONS_FILE="${PROJECT_DIR}/observations.jsonl"
|
|
149
|
+
MAX_FILE_SIZE_MB=10
|
|
150
|
+
|
|
151
|
+
# Auto-purge observation files older than 30 days (runs once per session)
|
|
152
|
+
PURGE_MARKER="${PROJECT_DIR}/.last-purge"
|
|
153
|
+
if [ ! -f "$PURGE_MARKER" ] || [ "$(find "$PURGE_MARKER" -mtime +1 2>/dev/null)" ]; then
|
|
154
|
+
find "${PROJECT_DIR}" -name "observations-*.jsonl" -mtime +30 -delete 2>/dev/null || true
|
|
155
|
+
touch "$PURGE_MARKER" 2>/dev/null || true
|
|
156
|
+
fi
|
|
157
|
+
|
|
158
|
+
# Parse using Python via stdin pipe (safe for all JSON payloads)
|
|
159
|
+
# Pass HOOK_PHASE via env var since Claude Code does not include hook type in stdin JSON
|
|
160
|
+
PARSED=$(echo "$INPUT_JSON" | HOOK_PHASE="$HOOK_PHASE" "$PYTHON_CMD" -c '
|
|
161
|
+
import json
|
|
162
|
+
import sys
|
|
163
|
+
import os
|
|
164
|
+
|
|
165
|
+
try:
|
|
166
|
+
data = json.load(sys.stdin)
|
|
167
|
+
|
|
168
|
+
# Determine event type from CLI argument passed via env var.
|
|
169
|
+
# Claude Code does NOT include a "hook_type" field in the stdin JSON,
|
|
170
|
+
# so we rely on the shell argument ("pre" or "post") instead.
|
|
171
|
+
hook_phase = os.environ.get("HOOK_PHASE", "post")
|
|
172
|
+
event = "tool_start" if hook_phase == "pre" else "tool_complete"
|
|
173
|
+
|
|
174
|
+
# Extract fields - Claude Code hook format
|
|
175
|
+
tool_name = data.get("tool_name", data.get("tool", "unknown"))
|
|
176
|
+
tool_input = data.get("tool_input", data.get("input", {}))
|
|
177
|
+
tool_output = data.get("tool_response")
|
|
178
|
+
if tool_output is None:
|
|
179
|
+
tool_output = data.get("tool_output", data.get("output", ""))
|
|
180
|
+
session_id = data.get("session_id", "unknown")
|
|
181
|
+
tool_use_id = data.get("tool_use_id", "")
|
|
182
|
+
cwd = data.get("cwd", "")
|
|
183
|
+
|
|
184
|
+
# Truncate large inputs/outputs
|
|
185
|
+
if isinstance(tool_input, dict):
|
|
186
|
+
tool_input_str = json.dumps(tool_input)[:5000]
|
|
187
|
+
else:
|
|
188
|
+
tool_input_str = str(tool_input)[:5000]
|
|
189
|
+
|
|
190
|
+
if isinstance(tool_output, dict):
|
|
191
|
+
tool_response_str = json.dumps(tool_output)[:5000]
|
|
192
|
+
else:
|
|
193
|
+
tool_response_str = str(tool_output)[:5000]
|
|
194
|
+
|
|
195
|
+
print(json.dumps({
|
|
196
|
+
"parsed": True,
|
|
197
|
+
"event": event,
|
|
198
|
+
"tool": tool_name,
|
|
199
|
+
"input": tool_input_str if event == "tool_start" else None,
|
|
200
|
+
"output": tool_response_str if event == "tool_complete" else None,
|
|
201
|
+
"session": session_id,
|
|
202
|
+
"tool_use_id": tool_use_id,
|
|
203
|
+
"cwd": cwd
|
|
204
|
+
}))
|
|
205
|
+
except Exception as e:
|
|
206
|
+
print(json.dumps({"parsed": False, "error": str(e)}))
|
|
207
|
+
')
|
|
208
|
+
|
|
209
|
+
# Check if parsing succeeded
|
|
210
|
+
PARSED_OK=$(echo "$PARSED" | "$PYTHON_CMD" -c "import json,sys; print(json.load(sys.stdin).get('parsed', False))" 2>/dev/null || echo "False")
|
|
211
|
+
|
|
212
|
+
if [ "$PARSED_OK" != "True" ]; then
|
|
213
|
+
# Fallback: log raw input for debugging (scrub secrets before persisting)
|
|
214
|
+
timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
215
|
+
export TIMESTAMP="$timestamp"
|
|
216
|
+
echo "$INPUT_JSON" | "$PYTHON_CMD" -c '
|
|
217
|
+
import json, sys, os, re
|
|
218
|
+
|
|
219
|
+
_SECRET_RE = re.compile(
|
|
220
|
+
r"(?i)(api[_-]?key|token|secret|password|authorization|credentials?|auth)"
|
|
221
|
+
r"""(["'"'"'\s:=]+)"""
|
|
222
|
+
r"([A-Za-z]+\s+)?"
|
|
223
|
+
r"([A-Za-z0-9_\-/.+=]{8,})"
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
raw = sys.stdin.read()[:2000]
|
|
227
|
+
raw = _SECRET_RE.sub(lambda m: m.group(1) + m.group(2) + (m.group(3) or "") + "[REDACTED]", raw)
|
|
228
|
+
print(json.dumps({"timestamp": os.environ["TIMESTAMP"], "event": "parse_error", "raw": raw}))
|
|
229
|
+
' >> "$OBSERVATIONS_FILE"
|
|
230
|
+
exit 0
|
|
231
|
+
fi
|
|
232
|
+
|
|
233
|
+
# Archive if file too large (atomic: rename with unique suffix to avoid race)
|
|
234
|
+
if [ -f "$OBSERVATIONS_FILE" ]; then
|
|
235
|
+
file_size_mb=$(du -m "$OBSERVATIONS_FILE" 2>/dev/null | cut -f1)
|
|
236
|
+
if [ "${file_size_mb:-0}" -ge "$MAX_FILE_SIZE_MB" ]; then
|
|
237
|
+
archive_dir="${PROJECT_DIR}/observations.archive"
|
|
238
|
+
mkdir -p "$archive_dir"
|
|
239
|
+
mv "$OBSERVATIONS_FILE" "$archive_dir/observations-$(date +%Y%m%d-%H%M%S)-$$.jsonl" 2>/dev/null || true
|
|
240
|
+
fi
|
|
241
|
+
fi
|
|
242
|
+
|
|
243
|
+
# Build and write observation (now includes project context)
|
|
244
|
+
# Scrub common secret patterns from tool I/O before persisting
|
|
245
|
+
timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
246
|
+
|
|
247
|
+
export PROJECT_ID_ENV="$PROJECT_ID"
|
|
248
|
+
export PROJECT_NAME_ENV="$PROJECT_NAME"
|
|
249
|
+
export TIMESTAMP="$timestamp"
|
|
250
|
+
|
|
251
|
+
echo "$PARSED" | "$PYTHON_CMD" -c '
|
|
252
|
+
import json, sys, os, re
|
|
253
|
+
|
|
254
|
+
parsed = json.load(sys.stdin)
|
|
255
|
+
observation = {
|
|
256
|
+
"timestamp": os.environ["TIMESTAMP"],
|
|
257
|
+
"event": parsed["event"],
|
|
258
|
+
"tool": parsed["tool"],
|
|
259
|
+
"session": parsed["session"],
|
|
260
|
+
"project_id": os.environ.get("PROJECT_ID_ENV", "global"),
|
|
261
|
+
"project_name": os.environ.get("PROJECT_NAME_ENV", "global")
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
# Scrub secrets: match common key=value, key: value, and key"value patterns
|
|
265
|
+
# Includes optional auth scheme (e.g., "Bearer", "Basic") before token
|
|
266
|
+
_SECRET_RE = re.compile(
|
|
267
|
+
r"(?i)(api[_-]?key|token|secret|password|authorization|credentials?|auth)"
|
|
268
|
+
r"""(["'"'"'\s:=]+)"""
|
|
269
|
+
r"([A-Za-z]+\s+)?"
|
|
270
|
+
r"([A-Za-z0-9_\-/.+=]{8,})"
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
def scrub(val):
|
|
274
|
+
if val is None:
|
|
275
|
+
return None
|
|
276
|
+
return _SECRET_RE.sub(lambda m: m.group(1) + m.group(2) + (m.group(3) or "") + "[REDACTED]", str(val))
|
|
277
|
+
|
|
278
|
+
if parsed["input"]:
|
|
279
|
+
observation["input"] = scrub(parsed["input"])
|
|
280
|
+
if parsed["output"] is not None:
|
|
281
|
+
observation["output"] = scrub(parsed["output"])
|
|
282
|
+
|
|
283
|
+
print(json.dumps(observation))
|
|
284
|
+
' >> "$OBSERVATIONS_FILE"
|
|
285
|
+
|
|
286
|
+
# Lazy-start observer if enabled but not running (first-time setup)
|
|
287
|
+
# Use flock for atomic check-then-act to prevent race conditions
|
|
288
|
+
# Fallback for macOS (no flock): use lockfile or skip
|
|
289
|
+
LAZY_START_LOCK="${PROJECT_DIR}/.observer-start.lock"
|
|
290
|
+
_CHECK_OBSERVER_RUNNING() {
|
|
291
|
+
local pid_file="$1"
|
|
292
|
+
if [ -f "$pid_file" ]; then
|
|
293
|
+
local pid
|
|
294
|
+
pid=$(cat "$pid_file" 2>/dev/null)
|
|
295
|
+
# Validate PID is a positive integer (>1) to prevent signaling invalid targets
|
|
296
|
+
case "$pid" in
|
|
297
|
+
''|*[!0-9]*|0|1)
|
|
298
|
+
rm -f "$pid_file" 2>/dev/null || true
|
|
299
|
+
return 1
|
|
300
|
+
;;
|
|
301
|
+
esac
|
|
302
|
+
if kill -0 "$pid" 2>/dev/null; then
|
|
303
|
+
return 0 # Process is alive
|
|
304
|
+
fi
|
|
305
|
+
# Stale PID file - remove it
|
|
306
|
+
rm -f "$pid_file" 2>/dev/null || true
|
|
307
|
+
fi
|
|
308
|
+
return 1 # No PID file or process dead
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if [ -f "${CONFIG_DIR}/disabled" ]; then
|
|
312
|
+
OBSERVER_ENABLED=false
|
|
313
|
+
else
|
|
314
|
+
OBSERVER_ENABLED=false
|
|
315
|
+
CONFIG_FILE="${SKILL_ROOT}/config.json"
|
|
316
|
+
# Allow CLV2_CONFIG override
|
|
317
|
+
if [ -n "${CLV2_CONFIG:-}" ]; then
|
|
318
|
+
CONFIG_FILE="$CLV2_CONFIG"
|
|
319
|
+
fi
|
|
320
|
+
# Use effective config path for both existence check and reading
|
|
321
|
+
EFFECTIVE_CONFIG="$CONFIG_FILE"
|
|
322
|
+
if [ -f "$EFFECTIVE_CONFIG" ] && [ -n "$PYTHON_CMD" ]; then
|
|
323
|
+
_enabled=$(CLV2_CONFIG_PATH="$EFFECTIVE_CONFIG" "$PYTHON_CMD" -c "
|
|
324
|
+
import json, os
|
|
325
|
+
with open(os.environ['CLV2_CONFIG_PATH']) as f:
|
|
326
|
+
cfg = json.load(f)
|
|
327
|
+
print(str(cfg.get('observer', {}).get('enabled', False)).lower())
|
|
328
|
+
" 2>/dev/null || echo "false")
|
|
329
|
+
if [ "$_enabled" = "true" ]; then
|
|
330
|
+
OBSERVER_ENABLED=true
|
|
331
|
+
fi
|
|
332
|
+
fi
|
|
333
|
+
fi
|
|
334
|
+
|
|
335
|
+
# Check both project-scoped AND global PID files (with stale PID recovery)
|
|
336
|
+
if [ "$OBSERVER_ENABLED" = "true" ]; then
|
|
337
|
+
# Clean up stale PID files first
|
|
338
|
+
_CHECK_OBSERVER_RUNNING "${PROJECT_DIR}/.observer.pid" || true
|
|
339
|
+
_CHECK_OBSERVER_RUNNING "${CONFIG_DIR}/.observer.pid" || true
|
|
340
|
+
|
|
341
|
+
# Check if observer is now running after cleanup
|
|
342
|
+
if [ ! -f "${PROJECT_DIR}/.observer.pid" ] && [ ! -f "${CONFIG_DIR}/.observer.pid" ]; then
|
|
343
|
+
# Use flock if available (Linux), fallback for macOS
|
|
344
|
+
if command -v flock >/dev/null 2>&1; then
|
|
345
|
+
(
|
|
346
|
+
flock -n 9 || exit 0
|
|
347
|
+
# Double-check PID files after acquiring lock
|
|
348
|
+
_CHECK_OBSERVER_RUNNING "${PROJECT_DIR}/.observer.pid" || true
|
|
349
|
+
_CHECK_OBSERVER_RUNNING "${CONFIG_DIR}/.observer.pid" || true
|
|
350
|
+
if [ ! -f "${PROJECT_DIR}/.observer.pid" ] && [ ! -f "${CONFIG_DIR}/.observer.pid" ]; then
|
|
351
|
+
nohup "${SKILL_ROOT}/agents/start-observer.sh" start >/dev/null 2>&1 &
|
|
352
|
+
fi
|
|
353
|
+
) 9>"$LAZY_START_LOCK"
|
|
354
|
+
else
|
|
355
|
+
# macOS fallback: use lockfile if available, otherwise mkdir-based lock
|
|
356
|
+
if command -v lockfile >/dev/null 2>&1; then
|
|
357
|
+
# Use subshell to isolate exit and add trap for cleanup
|
|
358
|
+
(
|
|
359
|
+
trap 'rm -f "$LAZY_START_LOCK" 2>/dev/null || true' EXIT
|
|
360
|
+
lockfile -r 1 -l 30 "$LAZY_START_LOCK" 2>/dev/null || exit 0
|
|
361
|
+
_CHECK_OBSERVER_RUNNING "${PROJECT_DIR}/.observer.pid" || true
|
|
362
|
+
_CHECK_OBSERVER_RUNNING "${CONFIG_DIR}/.observer.pid" || true
|
|
363
|
+
if [ ! -f "${PROJECT_DIR}/.observer.pid" ] && [ ! -f "${CONFIG_DIR}/.observer.pid" ]; then
|
|
364
|
+
nohup "${SKILL_ROOT}/agents/start-observer.sh" start >/dev/null 2>&1 &
|
|
365
|
+
fi
|
|
366
|
+
rm -f "$LAZY_START_LOCK" 2>/dev/null || true
|
|
367
|
+
)
|
|
368
|
+
else
|
|
369
|
+
# POSIX fallback: mkdir is atomic -- fails if dir already exists
|
|
370
|
+
(
|
|
371
|
+
trap 'rmdir "${LAZY_START_LOCK}.d" 2>/dev/null || true' EXIT
|
|
372
|
+
mkdir "${LAZY_START_LOCK}.d" 2>/dev/null || exit 0
|
|
373
|
+
_CHECK_OBSERVER_RUNNING "${PROJECT_DIR}/.observer.pid" || true
|
|
374
|
+
_CHECK_OBSERVER_RUNNING "${CONFIG_DIR}/.observer.pid" || true
|
|
375
|
+
if [ ! -f "${PROJECT_DIR}/.observer.pid" ] && [ ! -f "${CONFIG_DIR}/.observer.pid" ]; then
|
|
376
|
+
nohup "${SKILL_ROOT}/agents/start-observer.sh" start >/dev/null 2>&1 &
|
|
377
|
+
fi
|
|
378
|
+
)
|
|
379
|
+
fi
|
|
380
|
+
fi
|
|
381
|
+
fi
|
|
382
|
+
fi
|
|
383
|
+
|
|
384
|
+
# Throttle SIGUSR1: only signal observer every N observations (#521)
|
|
385
|
+
# This prevents rapid signaling when tool calls fire every second,
|
|
386
|
+
# which caused runaway parallel Claude analysis processes.
|
|
387
|
+
SIGNAL_EVERY_N="${ECC_OBSERVER_SIGNAL_EVERY_N:-20}"
|
|
388
|
+
SIGNAL_COUNTER_FILE="${PROJECT_DIR}/.observer-signal-counter"
|
|
389
|
+
ACTIVITY_FILE="${PROJECT_DIR}/.observer-last-activity"
|
|
390
|
+
|
|
391
|
+
touch "$ACTIVITY_FILE" 2>/dev/null || true
|
|
392
|
+
|
|
393
|
+
should_signal=0
|
|
394
|
+
if [ -f "$SIGNAL_COUNTER_FILE" ]; then
|
|
395
|
+
counter=$(cat "$SIGNAL_COUNTER_FILE" 2>/dev/null || echo 0)
|
|
396
|
+
counter=$((counter + 1))
|
|
397
|
+
if [ "$counter" -ge "$SIGNAL_EVERY_N" ]; then
|
|
398
|
+
should_signal=1
|
|
399
|
+
counter=0
|
|
400
|
+
fi
|
|
401
|
+
echo "$counter" > "$SIGNAL_COUNTER_FILE"
|
|
402
|
+
else
|
|
403
|
+
echo "1" > "$SIGNAL_COUNTER_FILE"
|
|
404
|
+
fi
|
|
405
|
+
|
|
406
|
+
# Signal observer if running and throttle allows (check both project-scoped and global observer, deduplicate)
|
|
407
|
+
if [ "$should_signal" -eq 1 ]; then
|
|
408
|
+
signaled_pids=" "
|
|
409
|
+
for pid_file in "${PROJECT_DIR}/.observer.pid" "${CONFIG_DIR}/.observer.pid"; do
|
|
410
|
+
if [ -f "$pid_file" ]; then
|
|
411
|
+
observer_pid=$(cat "$pid_file" 2>/dev/null || true)
|
|
412
|
+
# Validate PID is a positive integer (>1)
|
|
413
|
+
case "$observer_pid" in
|
|
414
|
+
''|*[!0-9]*|0|1) rm -f "$pid_file" 2>/dev/null || true; continue ;;
|
|
415
|
+
esac
|
|
416
|
+
# Deduplicate: skip if already signaled this pass
|
|
417
|
+
case "$signaled_pids" in
|
|
418
|
+
*" $observer_pid "*) continue ;;
|
|
419
|
+
esac
|
|
420
|
+
if kill -0 "$observer_pid" 2>/dev/null; then
|
|
421
|
+
kill -USR1 "$observer_pid" 2>/dev/null || true
|
|
422
|
+
signaled_pids="${signaled_pids}${observer_pid} "
|
|
423
|
+
fi
|
|
424
|
+
fi
|
|
425
|
+
done
|
|
426
|
+
fi
|
|
427
|
+
|
|
428
|
+
exit 0
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Continuous Learning v2 - Project Detection Helper
|
|
3
|
+
#
|
|
4
|
+
# Shared logic for detecting current project context.
|
|
5
|
+
# Sourced by observe.sh and start-observer.sh.
|
|
6
|
+
#
|
|
7
|
+
# Exports:
|
|
8
|
+
# _CLV2_PROJECT_ID - Short hash identifying the project (or "global")
|
|
9
|
+
# _CLV2_PROJECT_NAME - Human-readable project name
|
|
10
|
+
# _CLV2_PROJECT_ROOT - Absolute path to project root
|
|
11
|
+
# _CLV2_PROJECT_DIR - Project-scoped storage directory under homunculus
|
|
12
|
+
#
|
|
13
|
+
# Also sets unprefixed convenience aliases:
|
|
14
|
+
# PROJECT_ID, PROJECT_NAME, PROJECT_ROOT, PROJECT_DIR
|
|
15
|
+
#
|
|
16
|
+
# Detection priority:
|
|
17
|
+
# 1. CLAUDE_PROJECT_DIR env var (if set)
|
|
18
|
+
# 2. git remote URL (hashed for uniqueness across machines)
|
|
19
|
+
# 3. git repo root path (fallback, machine-specific)
|
|
20
|
+
# 4. "global" (no project context detected)
|
|
21
|
+
|
|
22
|
+
_CLV2_HOMUNCULUS_DIR="${HOME}/.claude/homunculus"
|
|
23
|
+
_CLV2_PROJECTS_DIR="${_CLV2_HOMUNCULUS_DIR}/projects"
|
|
24
|
+
_CLV2_REGISTRY_FILE="${_CLV2_HOMUNCULUS_DIR}/projects.json"
|
|
25
|
+
|
|
26
|
+
_clv2_resolve_python_cmd() {
|
|
27
|
+
if [ -n "${CLV2_PYTHON_CMD:-}" ] && command -v "$CLV2_PYTHON_CMD" >/dev/null 2>&1; then
|
|
28
|
+
printf '%s\n' "$CLV2_PYTHON_CMD"
|
|
29
|
+
return 0
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
if command -v python3 >/dev/null 2>&1; then
|
|
33
|
+
printf '%s\n' python3
|
|
34
|
+
return 0
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
if command -v python >/dev/null 2>&1; then
|
|
38
|
+
printf '%s\n' python
|
|
39
|
+
return 0
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
return 1
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
_CLV2_PYTHON_CMD="$(_clv2_resolve_python_cmd 2>/dev/null || true)"
|
|
46
|
+
CLV2_PYTHON_CMD="$_CLV2_PYTHON_CMD"
|
|
47
|
+
export CLV2_PYTHON_CMD
|
|
48
|
+
|
|
49
|
+
CLV2_OBSERVER_PROMPT_PATTERN='Can you confirm|requires permission|Awaiting (user confirmation|confirmation|approval|permission)|confirm I should proceed|once granted access|grant.*access'
|
|
50
|
+
export CLV2_OBSERVER_PROMPT_PATTERN
|
|
51
|
+
|
|
52
|
+
_clv2_detect_project() {
|
|
53
|
+
local project_root=""
|
|
54
|
+
local project_name=""
|
|
55
|
+
local project_id=""
|
|
56
|
+
local source_hint=""
|
|
57
|
+
|
|
58
|
+
# 1. Try CLAUDE_PROJECT_DIR env var
|
|
59
|
+
if [ -n "$CLAUDE_PROJECT_DIR" ] && [ -d "$CLAUDE_PROJECT_DIR" ]; then
|
|
60
|
+
project_root="$CLAUDE_PROJECT_DIR"
|
|
61
|
+
source_hint="env"
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
# 2. Try git repo root from CWD (only if git is available)
|
|
65
|
+
if [ -z "$project_root" ] && command -v git &>/dev/null; then
|
|
66
|
+
project_root=$(git rev-parse --show-toplevel 2>/dev/null || true)
|
|
67
|
+
if [ -n "$project_root" ]; then
|
|
68
|
+
source_hint="git"
|
|
69
|
+
fi
|
|
70
|
+
fi
|
|
71
|
+
|
|
72
|
+
# 3. No project detected — fall back to global
|
|
73
|
+
if [ -z "$project_root" ]; then
|
|
74
|
+
_CLV2_PROJECT_ID="global"
|
|
75
|
+
_CLV2_PROJECT_NAME="global"
|
|
76
|
+
_CLV2_PROJECT_ROOT=""
|
|
77
|
+
_CLV2_PROJECT_DIR="${_CLV2_HOMUNCULUS_DIR}"
|
|
78
|
+
return 0
|
|
79
|
+
fi
|
|
80
|
+
|
|
81
|
+
# Derive project name from directory basename
|
|
82
|
+
project_name=$(basename "$project_root")
|
|
83
|
+
|
|
84
|
+
# Derive project ID: prefer git remote URL hash (portable across machines),
|
|
85
|
+
# fall back to path hash (machine-specific but still useful)
|
|
86
|
+
local remote_url=""
|
|
87
|
+
if command -v git &>/dev/null; then
|
|
88
|
+
if [ "$source_hint" = "git" ] || [ -e "${project_root}/.git" ]; then
|
|
89
|
+
remote_url=$(git -C "$project_root" remote get-url origin 2>/dev/null || true)
|
|
90
|
+
fi
|
|
91
|
+
fi
|
|
92
|
+
|
|
93
|
+
# Compute hash from the original remote URL (legacy, for backward compatibility)
|
|
94
|
+
local legacy_hash_input="${remote_url:-$project_root}"
|
|
95
|
+
|
|
96
|
+
# Strip embedded credentials from remote URL (e.g., https://ghp_xxxx@github.com/...)
|
|
97
|
+
if [ -n "$remote_url" ]; then
|
|
98
|
+
remote_url=$(printf '%s' "$remote_url" | sed -E 's|://[^@]+@|://|')
|
|
99
|
+
fi
|
|
100
|
+
|
|
101
|
+
local hash_input="${remote_url:-$project_root}"
|
|
102
|
+
# Prefer Python for consistent SHA256 behavior across shells/platforms.
|
|
103
|
+
if [ -n "$_CLV2_PYTHON_CMD" ]; then
|
|
104
|
+
project_id=$(printf '%s' "$hash_input" | "$_CLV2_PYTHON_CMD" -c "import sys,hashlib; print(hashlib.sha256(sys.stdin.buffer.read()).hexdigest()[:12])" 2>/dev/null)
|
|
105
|
+
fi
|
|
106
|
+
|
|
107
|
+
# Fallback if Python is unavailable or hash generation failed.
|
|
108
|
+
if [ -z "$project_id" ]; then
|
|
109
|
+
project_id=$(printf '%s' "$hash_input" | shasum -a 256 2>/dev/null | cut -c1-12 || \
|
|
110
|
+
printf '%s' "$hash_input" | sha256sum 2>/dev/null | cut -c1-12 || \
|
|
111
|
+
echo "fallback")
|
|
112
|
+
fi
|
|
113
|
+
|
|
114
|
+
# Backward compatibility: if credentials were stripped and the hash changed,
|
|
115
|
+
# check if a project dir exists under the legacy hash and reuse it
|
|
116
|
+
if [ "$legacy_hash_input" != "$hash_input" ] && [ -n "$_CLV2_PYTHON_CMD" ]; then
|
|
117
|
+
local legacy_id=""
|
|
118
|
+
legacy_id=$(printf '%s' "$legacy_hash_input" | "$_CLV2_PYTHON_CMD" -c "import sys,hashlib; print(hashlib.sha256(sys.stdin.buffer.read()).hexdigest()[:12])" 2>/dev/null)
|
|
119
|
+
if [ -n "$legacy_id" ] && [ -d "${_CLV2_PROJECTS_DIR}/${legacy_id}" ] && [ ! -d "${_CLV2_PROJECTS_DIR}/${project_id}" ]; then
|
|
120
|
+
# Migrate legacy directory to new hash
|
|
121
|
+
mv "${_CLV2_PROJECTS_DIR}/${legacy_id}" "${_CLV2_PROJECTS_DIR}/${project_id}" 2>/dev/null || project_id="$legacy_id"
|
|
122
|
+
fi
|
|
123
|
+
fi
|
|
124
|
+
|
|
125
|
+
# Export results
|
|
126
|
+
_CLV2_PROJECT_ID="$project_id"
|
|
127
|
+
_CLV2_PROJECT_NAME="$project_name"
|
|
128
|
+
_CLV2_PROJECT_ROOT="$project_root"
|
|
129
|
+
_CLV2_PROJECT_DIR="${_CLV2_PROJECTS_DIR}/${project_id}"
|
|
130
|
+
|
|
131
|
+
# Ensure project directory structure exists
|
|
132
|
+
mkdir -p "${_CLV2_PROJECT_DIR}/instincts/personal"
|
|
133
|
+
mkdir -p "${_CLV2_PROJECT_DIR}/instincts/inherited"
|
|
134
|
+
mkdir -p "${_CLV2_PROJECT_DIR}/observations.archive"
|
|
135
|
+
mkdir -p "${_CLV2_PROJECT_DIR}/evolved/skills"
|
|
136
|
+
mkdir -p "${_CLV2_PROJECT_DIR}/evolved/commands"
|
|
137
|
+
mkdir -p "${_CLV2_PROJECT_DIR}/evolved/agents"
|
|
138
|
+
|
|
139
|
+
# Update project registry (lightweight JSON mapping)
|
|
140
|
+
_clv2_update_project_registry "$project_id" "$project_name" "$project_root" "$remote_url"
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
_clv2_update_project_registry() {
|
|
144
|
+
local pid="$1"
|
|
145
|
+
local pname="$2"
|
|
146
|
+
local proot="$3"
|
|
147
|
+
local premote="$4"
|
|
148
|
+
local pdir="$_CLV2_PROJECT_DIR"
|
|
149
|
+
|
|
150
|
+
mkdir -p "$(dirname "$_CLV2_REGISTRY_FILE")"
|
|
151
|
+
|
|
152
|
+
if [ -z "$_CLV2_PYTHON_CMD" ]; then
|
|
153
|
+
return 0
|
|
154
|
+
fi
|
|
155
|
+
|
|
156
|
+
# Pass values via env vars to avoid shell→python injection.
|
|
157
|
+
# Python reads them with os.environ, which is safe for any string content.
|
|
158
|
+
_CLV2_REG_PID="$pid" \
|
|
159
|
+
_CLV2_REG_PNAME="$pname" \
|
|
160
|
+
_CLV2_REG_PROOT="$proot" \
|
|
161
|
+
_CLV2_REG_PREMOTE="$premote" \
|
|
162
|
+
_CLV2_REG_PDIR="$pdir" \
|
|
163
|
+
_CLV2_REG_FILE="$_CLV2_REGISTRY_FILE" \
|
|
164
|
+
"$_CLV2_PYTHON_CMD" -c '
|
|
165
|
+
import json, os, tempfile
|
|
166
|
+
from datetime import datetime, timezone
|
|
167
|
+
|
|
168
|
+
registry_path = os.environ["_CLV2_REG_FILE"]
|
|
169
|
+
project_dir = os.environ["_CLV2_REG_PDIR"]
|
|
170
|
+
project_file = os.path.join(project_dir, "project.json")
|
|
171
|
+
|
|
172
|
+
os.makedirs(project_dir, exist_ok=True)
|
|
173
|
+
|
|
174
|
+
def atomic_write_json(path, payload):
|
|
175
|
+
fd, tmp_path = tempfile.mkstemp(
|
|
176
|
+
prefix=f".{os.path.basename(path)}.tmp.",
|
|
177
|
+
dir=os.path.dirname(path),
|
|
178
|
+
text=True,
|
|
179
|
+
)
|
|
180
|
+
try:
|
|
181
|
+
with os.fdopen(fd, "w") as f:
|
|
182
|
+
json.dump(payload, f, indent=2)
|
|
183
|
+
f.write("\n")
|
|
184
|
+
os.replace(tmp_path, path)
|
|
185
|
+
finally:
|
|
186
|
+
if os.path.exists(tmp_path):
|
|
187
|
+
os.unlink(tmp_path)
|
|
188
|
+
|
|
189
|
+
try:
|
|
190
|
+
with open(registry_path) as f:
|
|
191
|
+
registry = json.load(f)
|
|
192
|
+
except (FileNotFoundError, json.JSONDecodeError):
|
|
193
|
+
registry = {}
|
|
194
|
+
|
|
195
|
+
now = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
|
196
|
+
entry = registry.get(os.environ["_CLV2_REG_PID"], {})
|
|
197
|
+
|
|
198
|
+
metadata = {
|
|
199
|
+
"id": os.environ["_CLV2_REG_PID"],
|
|
200
|
+
"name": os.environ["_CLV2_REG_PNAME"],
|
|
201
|
+
"root": os.environ["_CLV2_REG_PROOT"],
|
|
202
|
+
"remote": os.environ["_CLV2_REG_PREMOTE"],
|
|
203
|
+
"created_at": entry.get("created_at", now),
|
|
204
|
+
"last_seen": now,
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
registry[os.environ["_CLV2_REG_PID"]] = metadata
|
|
208
|
+
|
|
209
|
+
atomic_write_json(project_file, metadata)
|
|
210
|
+
atomic_write_json(registry_path, registry)
|
|
211
|
+
' 2>/dev/null || true
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
# Auto-detect on source
|
|
215
|
+
_clv2_detect_project
|
|
216
|
+
|
|
217
|
+
# Convenience aliases for callers (short names pointing to prefixed vars)
|
|
218
|
+
PROJECT_ID="$_CLV2_PROJECT_ID"
|
|
219
|
+
PROJECT_NAME="$_CLV2_PROJECT_NAME"
|
|
220
|
+
PROJECT_ROOT="$_CLV2_PROJECT_ROOT"
|
|
221
|
+
PROJECT_DIR="$_CLV2_PROJECT_DIR"
|
|
222
|
+
|
|
223
|
+
if [ -n "$PROJECT_ROOT" ]; then
|
|
224
|
+
CLV2_OBSERVER_SENTINEL_FILE="${PROJECT_ROOT}/.observer.lock"
|
|
225
|
+
else
|
|
226
|
+
CLV2_OBSERVER_SENTINEL_FILE="${PROJECT_DIR}/.observer.lock"
|
|
227
|
+
fi
|
|
228
|
+
export CLV2_OBSERVER_SENTINEL_FILE
|