@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.
Files changed (212) hide show
  1. package/LICENSE +21 -0
  2. package/README.ko.md +279 -0
  3. package/README.md +306 -0
  4. package/dist/chunk-SDVAM5JZ.js +775 -0
  5. package/dist/chunk-SDVAM5JZ.js.map +1 -0
  6. package/dist/index.js +5412 -0
  7. package/dist/index.js.map +1 -0
  8. package/dist/trust-tier-drift.js +67 -0
  9. package/dist/trust-tier-drift.js.map +1 -0
  10. package/package.json +53 -0
  11. package/scripts/prune-ecc.sh +310 -0
  12. package/templates/CLAUDE.md +86 -0
  13. package/templates/agents/build-error-resolver.md +114 -0
  14. package/templates/agents/code-reviewer.md +237 -0
  15. package/templates/agents/data-analyst.md +69 -0
  16. package/templates/agents/plan-checker.md +118 -0
  17. package/templates/agents/reviewer.md +128 -0
  18. package/templates/agents/security-reviewer.md +108 -0
  19. package/templates/agents/silent-failure-hunter.md +50 -0
  20. package/templates/agents/strategist.md +86 -0
  21. package/templates/antigravity/AGENTS.md.template +58 -0
  22. package/templates/codex/AGENTS.md.template +94 -0
  23. package/templates/codex/README.md +69 -0
  24. package/templates/codex/config.toml.template +108 -0
  25. package/templates/codex/hooks/README.md +40 -0
  26. package/templates/codex/hooks/gate-check.sh +7 -0
  27. package/templates/codex/hooks/hito-counter.sh +7 -0
  28. package/templates/codex/hooks/session-start.sh +7 -0
  29. package/templates/codex/hooks/uncommitted-check.sh +7 -0
  30. package/templates/codex/skills/uzys-build/SKILL.md +24 -0
  31. package/templates/codex/skills/uzys-plan/SKILL.md +24 -0
  32. package/templates/codex/skills/uzys-review/SKILL.md +24 -0
  33. package/templates/codex/skills/uzys-ship/SKILL.md +24 -0
  34. package/templates/codex/skills/uzys-spec/SKILL.md +28 -0
  35. package/templates/codex/skills/uzys-test/SKILL.md +24 -0
  36. package/templates/commands/ecc/checkpoint.md +32 -0
  37. package/templates/commands/ecc/e2e.md +105 -0
  38. package/templates/commands/ecc/eval.md +88 -0
  39. package/templates/commands/ecc/evolve.md +7 -0
  40. package/templates/commands/ecc/harness-audit.md +73 -0
  41. package/templates/commands/ecc/instinct-status.md +8 -0
  42. package/templates/commands/ecc/promote.md +10 -0
  43. package/templates/commands/ecc/security-scan.md +10 -0
  44. package/templates/commands/uzys/auto.md +190 -0
  45. package/templates/commands/uzys/build.md +42 -0
  46. package/templates/commands/uzys/plan.md +55 -0
  47. package/templates/commands/uzys/review.md +44 -0
  48. package/templates/commands/uzys/ship.md +49 -0
  49. package/templates/commands/uzys/spec.md +93 -0
  50. package/templates/commands/uzys/test.md +58 -0
  51. package/templates/docs/PLAN.template.md +102 -0
  52. package/templates/hooks/agentshield-gate.sh +101 -0
  53. package/templates/hooks/checkpoint-snapshot.sh +115 -0
  54. package/templates/hooks/gate-check.sh +138 -0
  55. package/templates/hooks/hito-counter.sh +26 -0
  56. package/templates/hooks/karpathy-gate.sh +59 -0
  57. package/templates/hooks/mcp-pre-exec.sh +104 -0
  58. package/templates/hooks/protect-files.sh +41 -0
  59. package/templates/hooks/session-start.sh +40 -0
  60. package/templates/hooks/spec-drift-check.sh +86 -0
  61. package/templates/mcp-allowlist.example +24 -0
  62. package/templates/mcp.json +20 -0
  63. package/templates/opencode/.opencode/commands/uzys-build.md +22 -0
  64. package/templates/opencode/.opencode/commands/uzys-plan.md +22 -0
  65. package/templates/opencode/.opencode/commands/uzys-review.md +22 -0
  66. package/templates/opencode/.opencode/commands/uzys-ship.md +22 -0
  67. package/templates/opencode/.opencode/commands/uzys-spec.md +28 -0
  68. package/templates/opencode/.opencode/commands/uzys-test.md +22 -0
  69. package/templates/opencode/.opencode/plugins/uzys-harness.ts +146 -0
  70. package/templates/opencode/AGENTS.md.template +98 -0
  71. package/templates/opencode/README.md +34 -0
  72. package/templates/opencode/opencode.json.template +42 -0
  73. package/templates/project-claude/_base.md +23 -0
  74. package/templates/project-claude/fragments/csr-fastapi/active-rules.md +13 -0
  75. package/templates/project-claude/fragments/csr-fastapi/agents.md +5 -0
  76. package/templates/project-claude/fragments/csr-fastapi/boundaries.md +18 -0
  77. package/templates/project-claude/fragments/csr-fastapi/commands.md +6 -0
  78. package/templates/project-claude/fragments/csr-fastapi/plugins.md +2 -0
  79. package/templates/project-claude/fragments/csr-fastapi/skills.md +5 -0
  80. package/templates/project-claude/fragments/csr-fastapi/stack.md +6 -0
  81. package/templates/project-claude/fragments/csr-fastapi/tagline.md +1 -0
  82. package/templates/project-claude/fragments/csr-fastapi/workflow.md +8 -0
  83. package/templates/project-claude/fragments/csr-fastify/active-rules.md +13 -0
  84. package/templates/project-claude/fragments/csr-fastify/agents.md +5 -0
  85. package/templates/project-claude/fragments/csr-fastify/boundaries.md +18 -0
  86. package/templates/project-claude/fragments/csr-fastify/commands.md +6 -0
  87. package/templates/project-claude/fragments/csr-fastify/plugins.md +2 -0
  88. package/templates/project-claude/fragments/csr-fastify/skills.md +5 -0
  89. package/templates/project-claude/fragments/csr-fastify/stack.md +6 -0
  90. package/templates/project-claude/fragments/csr-fastify/tagline.md +1 -0
  91. package/templates/project-claude/fragments/csr-fastify/workflow.md +8 -0
  92. package/templates/project-claude/fragments/csr-supabase/active-rules.md +12 -0
  93. package/templates/project-claude/fragments/csr-supabase/agents.md +5 -0
  94. package/templates/project-claude/fragments/csr-supabase/boundaries.md +19 -0
  95. package/templates/project-claude/fragments/csr-supabase/commands.md +6 -0
  96. package/templates/project-claude/fragments/csr-supabase/plugins.md +4 -0
  97. package/templates/project-claude/fragments/csr-supabase/skills.md +7 -0
  98. package/templates/project-claude/fragments/csr-supabase/stack.md +6 -0
  99. package/templates/project-claude/fragments/csr-supabase/supabase-auth.md +21 -0
  100. package/templates/project-claude/fragments/csr-supabase/tagline.md +1 -0
  101. package/templates/project-claude/fragments/csr-supabase/workflow.md +8 -0
  102. package/templates/project-claude/fragments/data/active-rules.md +10 -0
  103. package/templates/project-claude/fragments/data/agents.md +6 -0
  104. package/templates/project-claude/fragments/data/boundaries.md +20 -0
  105. package/templates/project-claude/fragments/data/commands.md +6 -0
  106. package/templates/project-claude/fragments/data/plugins.md +2 -0
  107. package/templates/project-claude/fragments/data/skills.md +3 -0
  108. package/templates/project-claude/fragments/data/stack.md +7 -0
  109. package/templates/project-claude/fragments/data/tagline.md +1 -0
  110. package/templates/project-claude/fragments/data/workflow.md +9 -0
  111. package/templates/project-claude/fragments/executive/active-rules.md +6 -0
  112. package/templates/project-claude/fragments/executive/agents.md +6 -0
  113. package/templates/project-claude/fragments/executive/boundaries.md +17 -0
  114. package/templates/project-claude/fragments/executive/commands.md +11 -0
  115. package/templates/project-claude/fragments/executive/plugins.md +1 -0
  116. package/templates/project-claude/fragments/executive/skills.md +7 -0
  117. package/templates/project-claude/fragments/executive/stack.md +4 -0
  118. package/templates/project-claude/fragments/executive/tagline.md +1 -0
  119. package/templates/project-claude/fragments/executive/workflow.md +10 -0
  120. package/templates/project-claude/fragments/growth-marketing/active-rules.md +7 -0
  121. package/templates/project-claude/fragments/growth-marketing/agents.md +6 -0
  122. package/templates/project-claude/fragments/growth-marketing/boundaries.md +17 -0
  123. package/templates/project-claude/fragments/growth-marketing/commands.md +11 -0
  124. package/templates/project-claude/fragments/growth-marketing/plugins.md +9 -0
  125. package/templates/project-claude/fragments/growth-marketing/skills.md +8 -0
  126. package/templates/project-claude/fragments/growth-marketing/stack.md +7 -0
  127. package/templates/project-claude/fragments/growth-marketing/tagline.md +1 -0
  128. package/templates/project-claude/fragments/growth-marketing/workflow.md +11 -0
  129. package/templates/project-claude/fragments/project-management/active-rules.md +7 -0
  130. package/templates/project-claude/fragments/project-management/agents.md +6 -0
  131. package/templates/project-claude/fragments/project-management/boundaries.md +16 -0
  132. package/templates/project-claude/fragments/project-management/commands.md +10 -0
  133. package/templates/project-claude/fragments/project-management/plugins.md +6 -0
  134. package/templates/project-claude/fragments/project-management/skills.md +5 -0
  135. package/templates/project-claude/fragments/project-management/stack.md +4 -0
  136. package/templates/project-claude/fragments/project-management/tagline.md +1 -0
  137. package/templates/project-claude/fragments/project-management/workflow.md +12 -0
  138. package/templates/project-claude/fragments/ssr-htmx/active-rules.md +11 -0
  139. package/templates/project-claude/fragments/ssr-htmx/agents.md +5 -0
  140. package/templates/project-claude/fragments/ssr-htmx/boundaries.md +20 -0
  141. package/templates/project-claude/fragments/ssr-htmx/commands.md +6 -0
  142. package/templates/project-claude/fragments/ssr-htmx/plugins.md +2 -0
  143. package/templates/project-claude/fragments/ssr-htmx/skills.md +3 -0
  144. package/templates/project-claude/fragments/ssr-htmx/stack.md +6 -0
  145. package/templates/project-claude/fragments/ssr-htmx/tagline.md +1 -0
  146. package/templates/project-claude/fragments/ssr-htmx/workflow.md +8 -0
  147. package/templates/project-claude/fragments/ssr-nextjs/active-rules.md +12 -0
  148. package/templates/project-claude/fragments/ssr-nextjs/agents.md +5 -0
  149. package/templates/project-claude/fragments/ssr-nextjs/boundaries.md +20 -0
  150. package/templates/project-claude/fragments/ssr-nextjs/commands.md +6 -0
  151. package/templates/project-claude/fragments/ssr-nextjs/plugins.md +2 -0
  152. package/templates/project-claude/fragments/ssr-nextjs/skills.md +5 -0
  153. package/templates/project-claude/fragments/ssr-nextjs/stack.md +5 -0
  154. package/templates/project-claude/fragments/ssr-nextjs/tagline.md +1 -0
  155. package/templates/project-claude/fragments/ssr-nextjs/workflow.md +8 -0
  156. package/templates/project-claude/fragments/tooling/active-rules.md +11 -0
  157. package/templates/project-claude/fragments/tooling/agents.md +5 -0
  158. package/templates/project-claude/fragments/tooling/boundaries.md +17 -0
  159. package/templates/project-claude/fragments/tooling/commands.md +4 -0
  160. package/templates/project-claude/fragments/tooling/skills.md +4 -0
  161. package/templates/project-claude/fragments/tooling/stack.md +5 -0
  162. package/templates/project-claude/fragments/tooling/tagline.md +1 -0
  163. package/templates/project-claude/fragments/tooling/workflow.md +5 -0
  164. package/templates/rules/api-contract.md +33 -0
  165. package/templates/rules/change-management.md +80 -0
  166. package/templates/rules/cli-development.md +39 -0
  167. package/templates/rules/code-style.md +23 -0
  168. package/templates/rules/data-analysis.md +61 -0
  169. package/templates/rules/database.md +29 -0
  170. package/templates/rules/design-workflow.md +17 -0
  171. package/templates/rules/error-handling.md +23 -0
  172. package/templates/rules/gates-taxonomy.md +21 -0
  173. package/templates/rules/git-policy.md +102 -0
  174. package/templates/rules/htmx.md +42 -0
  175. package/templates/rules/nextjs.md +35 -0
  176. package/templates/rules/playwright-launch.md +66 -0
  177. package/templates/rules/pyside6.md +59 -0
  178. package/templates/rules/shadcn.md +33 -0
  179. package/templates/rules/ship-checklist.md +24 -0
  180. package/templates/rules/tauri.md +40 -0
  181. package/templates/rules/test-policy.md +62 -0
  182. package/templates/settings.json +71 -0
  183. package/templates/skills/agent-introspection-debugging/SKILL.md +153 -0
  184. package/templates/skills/continuous-learning-v2/SKILL.md +365 -0
  185. package/templates/skills/continuous-learning-v2/config.json +8 -0
  186. package/templates/skills/continuous-learning-v2/hooks/observe.sh +428 -0
  187. package/templates/skills/continuous-learning-v2/scripts/detect-project.sh +228 -0
  188. package/templates/skills/continuous-learning-v2/scripts/instinct-cli.py +1426 -0
  189. package/templates/skills/deep-research/SKILL.md +155 -0
  190. package/templates/skills/deep-research/agents/openai.yaml +7 -0
  191. package/templates/skills/e2e-testing/SKILL.md +326 -0
  192. package/templates/skills/e2e-testing/agents/openai.yaml +7 -0
  193. package/templates/skills/eval-harness/SKILL.md +279 -0
  194. package/templates/skills/eval-harness/agents/openai.yaml +7 -0
  195. package/templates/skills/gh-issue-workflow/ISSUE.template.md +58 -0
  196. package/templates/skills/gh-issue-workflow/SKILL.md +184 -0
  197. package/templates/skills/investor-materials/SKILL.md +96 -0
  198. package/templates/skills/investor-outreach/SKILL.md +91 -0
  199. package/templates/skills/market-research/SKILL.md +75 -0
  200. package/templates/skills/market-research/agents/openai.yaml +7 -0
  201. package/templates/skills/nextjs-turbopack/SKILL.md +44 -0
  202. package/templates/skills/north-star/NORTH_STAR.template.md +114 -0
  203. package/templates/skills/north-star/SKILL.md +103 -0
  204. package/templates/skills/python-patterns/SKILL.md +750 -0
  205. package/templates/skills/python-testing/SKILL.md +816 -0
  206. package/templates/skills/spec-scaling/SKILL.md +89 -0
  207. package/templates/skills/strategic-compact/SKILL.md +131 -0
  208. package/templates/skills/strategic-compact/suggest-compact.sh +54 -0
  209. package/templates/skills/ui-visual-review/SKILL.md +154 -0
  210. package/templates/skills/verification-loop/SKILL.md +126 -0
  211. package/templates/skills/verification-loop/agents/openai.yaml +7 -0
  212. 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