@onlooker-community/ecosystem 0.8.0 → 0.9.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.
@@ -12,7 +12,7 @@
12
12
  "name": "ecosystem",
13
13
  "source": "./",
14
14
  "description": "Fill this out",
15
- "version": "0.8.0",
15
+ "version": "0.9.0",
16
16
  "author": {
17
17
  "name": "Onlooker Community"
18
18
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ecosystem",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "TODO fill this out",
5
5
  "author": {
6
6
  "name": "Onlooker Community",
@@ -15,7 +15,6 @@ jobs:
15
15
  - uses: googleapis/release-please-action@v4
16
16
  id: release
17
17
  with:
18
- release-type: node
19
18
  token: ${{ secrets.RELEASE_PLEASE_TOKEN }}
20
19
 
21
20
  - uses: actions/checkout@v4
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "0.8.0"
2
+ ".": "0.9.0"
3
3
  }
package/CHANGELOG.md CHANGED
@@ -7,6 +7,40 @@
7
7
 
8
8
  # Changelog
9
9
 
10
+ ## [0.9.0](https://github.com/onlooker-community/ecosystem/compare/ecosystem-v0.8.0...ecosystem-v0.9.0) (2026-05-22)
11
+
12
+
13
+ ### Features
14
+
15
+ * add configuration and hooks for agent spawn tracking ([3ef4590](https://github.com/onlooker-community/ecosystem/commit/3ef459006bbbda246604bdd1ffaf9af0a59f9740))
16
+ * add settings.json for plugin configuration ([67fbdfe](https://github.com/onlooker-community/ecosystem/commit/67fbdfe37f067a45801e7d0355c4a533b687f6b2))
17
+ * **hooks:** add PreCompact and PostCompact context compaction trackers ([#15](https://github.com/onlooker-community/ecosystem/issues/15)) ([1ec5632](https://github.com/onlooker-community/ecosystem/commit/1ec5632404676ed8b35d324b79ad71a2e9093505))
18
+ * **hooks:** add SessionStart and SessionEnd session trackers ([#10](https://github.com/onlooker-community/ecosystem/issues/10)) ([a48d680](https://github.com/onlooker-community/ecosystem/commit/a48d680dd24c98e79ef1c0401b07483ecebf9e8b))
19
+ * **hooks:** add TaskCreated and TaskCompleted task lifecycle trackers ([#21](https://github.com/onlooker-community/ecosystem/issues/21)) ([986ffa8](https://github.com/onlooker-community/ecosystem/commit/986ffa84bdd857a464ca0d556671628190ed27bc))
20
+ * **hooks:** add UserPromptSubmit turn and session duration trackers ([#12](https://github.com/onlooker-community/ecosystem/issues/12)) ([cbb7657](https://github.com/onlooker-community/ecosystem/commit/cbb7657979ed144efce506e6b487e037679b9462))
21
+ * **hooks:** add WorktreeCreate and WorktreeRemove lifecycle trackers ([#24](https://github.com/onlooker-community/ecosystem/issues/24)) ([ff55e39](https://github.com/onlooker-community/ecosystem/commit/ff55e397a0c0adc3e76f66aba12c6b237149ad17))
22
+ * **hooks:** emit canonical schema events for tool history :sparkles: ([1e49a24](https://github.com/onlooker-community/ecosystem/commit/1e49a24bfb930942fa477b594395ef352618f574))
23
+ * **hooks:** track skill usage via skill.invoked events ([23fff0f](https://github.com/onlooker-community/ecosystem/commit/23fff0f0bfad8ab91788d8c45a0457d099d2e870))
24
+ * **hooks:** track tool call sequence on every PreToolUse :sparkles: ([0ad9546](https://github.com/onlooker-community/ecosystem/commit/0ad95465cc22a237e26115a67814a6e7b2951b1d))
25
+
26
+
27
+ ### Bug Fixes
28
+
29
+ * **ci:** apply release-please extra-files for Claude plugin manifests ([#17](https://github.com/onlooker-community/ecosystem/issues/17)) ([da9913c](https://github.com/onlooker-community/ecosystem/commit/da9913ca4f7497280edc34f8c64baa903c1e6754))
30
+ * **ci:** checkout release tag before npm publish :relieved: ([bc7bbdc](https://github.com/onlooker-community/ecosystem/commit/bc7bbdc7a886a55ba8f04fe09bfa60043648c766))
31
+ * **ci:** grant id-token write for npm provenance on publish ([c78c9f0](https://github.com/onlooker-community/ecosystem/commit/c78c9f054c1d48ca8a83d0d26b76ce991fffe51b))
32
+ * **ci:** parse release-please paths_released JSON for npm publish ([749e1a0](https://github.com/onlooker-community/ecosystem/commit/749e1a02b563f37f81a8da21fc3f6e10e179314a))
33
+ * **ci:** stop upgrading npm globally before publish ([a7c7a0e](https://github.com/onlooker-community/ecosystem/commit/a7c7a0e1f25aee1bbb75bdd2af130dbc276480a6))
34
+ * **ci:** use HTTPS repository URL for npm provenance ([a7e8927](https://github.com/onlooker-community/ecosystem/commit/a7e89275c5a025a8afee009853265b717091f6ca))
35
+ * **package:** update repository URL format in package.json ([591ce9f](https://github.com/onlooker-community/ecosystem/commit/591ce9f54dd605ec04ceb77b9dcca40b3e08621e))
36
+
37
+ ## [0.8.0](https://github.com/onlooker-community/ecosystem/compare/v0.7.2...v0.8.0) (2026-05-22)
38
+
39
+
40
+ ### Features
41
+
42
+ * **hooks:** add TaskCreated and TaskCompleted task lifecycle trackers ([#21](https://github.com/onlooker-community/ecosystem/issues/21)) ([986ffa8](https://github.com/onlooker-community/ecosystem/commit/986ffa84bdd857a464ca0d556671628190ed27bc))
43
+
10
44
  ## [0.7.2](https://github.com/onlooker-community/ecosystem/compare/v0.7.1...v0.7.2) (2026-05-22)
11
45
 
12
46
 
package/hooks/hooks.json CHANGED
@@ -157,6 +157,26 @@
157
157
  }
158
158
  ]
159
159
  }
160
+ ],
161
+ "WorktreeCreate": [
162
+ {
163
+ "hooks": [
164
+ {
165
+ "type": "command",
166
+ "command": "\"$CLAUDE_PLUGIN_ROOT\"/scripts/hooks/worktree-tracker.sh"
167
+ }
168
+ ]
169
+ }
170
+ ],
171
+ "WorktreeRemove": [
172
+ {
173
+ "hooks": [
174
+ {
175
+ "type": "command",
176
+ "command": "\"$CLAUDE_PLUGIN_ROOT\"/scripts/hooks/worktree-tracker.sh"
177
+ }
178
+ ]
179
+ }
160
180
  ]
161
181
  }
162
182
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onlooker-community/ecosystem",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Agents, skills, hooks, commands, rules, and MCP configurations that power [Onlooker](https://onlooker.dev)",
5
5
  "author": {
6
6
  "name": "Onlooker Community",
@@ -9,7 +9,7 @@
9
9
  "license": "MIT",
10
10
  "repository": {
11
11
  "type": "git",
12
- "url": "https://github.com/onlooker-community/ecosystem"
12
+ "url": "git+https://github.com/onlooker-community/ecosystem.git"
13
13
  },
14
14
  "homepage": "https://github.com/onlooker-community/ecosystem#readme",
15
15
  "bugs": {
@@ -0,0 +1,121 @@
1
+ #!/usr/bin/env bash
2
+ # Onlooker Worktree Tracker
3
+ # Invoked by WorktreeCreate and WorktreeRemove for isolated agent/git worktree sessions.
4
+ #
5
+ # WorktreeCreate replaces default git behavior: this hook creates the worktree, records
6
+ # telemetry, and prints the absolute worktree path on stdout (stderr for diagnostics).
7
+ # WorktreeRemove records telemetry and removes the git worktree when present.
8
+ #
9
+ # Records canonical tool.shell.exec events (interim until worktree.* schema types exist) to:
10
+ # ~/.onlooker/session-history/<session_id>.jsonl
11
+ # ~/.onlooker/logs/onlooker-events.jsonl
12
+ #
13
+ # Usage:
14
+ # echo "$INPUT" | worktree-tracker.sh
15
+
16
+ set -uo pipefail
17
+
18
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
19
+ source "$SCRIPT_DIR/../lib/validate-path.sh"
20
+ source "$SCRIPT_DIR/../lib/onlooker-schema.sh"
21
+ source "$SCRIPT_DIR/../lib/session-tracker.sh"
22
+ source "$SCRIPT_DIR/../lib/tool-history.sh"
23
+ source "$SCRIPT_DIR/../lib/worktree-tracker.sh"
24
+
25
+ hook_register "worktree-tracker" "Worktree Tracker" "Creates/removes git worktrees and records lifecycle telemetry"
26
+
27
+ INPUT=$(cat)
28
+
29
+ HOOK_EVENT=$(echo "$INPUT" | jq -r '.hook_event_name // ""')
30
+ hook_set_context "$INPUT" "$HOOK_EVENT"
31
+
32
+ SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // "unknown"')
33
+ CWD=$(echo "$INPUT" | jq -r '.cwd // ""')
34
+
35
+ turn_state_export "$SESSION_ID"
36
+
37
+ worktree_tracker_emit() {
38
+ local enriched_input="${1:-}"
39
+ local record
40
+ record=$(worktree_tracker_build_record "$enriched_input")
41
+ if [[ -n "$record" ]]; then
42
+ worktree_tracker_append "$SESSION_ID" "$record" || hook_failure "Failed to append session history"
43
+ onlooker_append_event "$record" || hook_failure "Failed to append global event log"
44
+ fi
45
+ }
46
+
47
+ case "$HOOK_EVENT" in
48
+ WorktreeCreate)
49
+ NAME=$(echo "$INPUT" | jq -r '.name // ""')
50
+ if [[ -z "$NAME" ]]; then
51
+ echo "WorktreeCreate requires a worktree name" >&2
52
+ exit 1
53
+ fi
54
+
55
+ REPO_ROOT=$(worktree_tracker_repo_root "$CWD")
56
+ if [[ -z "$REPO_ROOT" ]]; then
57
+ echo "WorktreeCreate requires a git repository (cwd: ${CWD:-unknown})" >&2
58
+ exit 1
59
+ fi
60
+
61
+ START_MS=$(session_tracker_now_ms)
62
+ WORKTREE_PATH=$(worktree_tracker_git_create "$REPO_ROOT" "$NAME")
63
+ if [[ -z "$WORKTREE_PATH" ]]; then
64
+ echo "Failed to create git worktree for name: $NAME" >&2
65
+ exit 1
66
+ fi
67
+
68
+ BRANCH="worktree-${NAME}"
69
+ worktree_tracker_record_created "$SESSION_ID" "$NAME" "$WORKTREE_PATH" "$BRANCH" \
70
+ || hook_failure "Failed to record worktree start time"
71
+
72
+ END_MS=$(session_tracker_now_ms)
73
+ export ONLOOKER_WORKTREE_DURATION_MS="$((END_MS - START_MS))"
74
+
75
+ ENRICHED=$(echo "$INPUT" | jq \
76
+ --arg path "$WORKTREE_PATH" \
77
+ --arg branch "$BRANCH" \
78
+ --arg repo "$REPO_ROOT" \
79
+ '. + {worktree_path: $path, branch_name: $branch, repo_root: $repo}')
80
+ RECORD=$(worktree_tracker_build_record "$ENRICHED")
81
+ if [[ -n "$RECORD" ]]; then
82
+ worktree_tracker_append "$SESSION_ID" "$RECORD" \
83
+ || hook_failure "Failed to append session history"
84
+ onlooker_append_event "$RECORD" \
85
+ || hook_failure "Failed to append global event log"
86
+ fi
87
+
88
+ # stdout must be only the absolute worktree path for Claude Code
89
+ printf '%s' "$WORKTREE_PATH"
90
+ exit 0
91
+ ;;
92
+ WorktreeRemove)
93
+ WORKTREE_PATH=$(echo "$INPUT" | jq -r '.worktree_path // ""')
94
+ if [[ -z "$WORKTREE_PATH" ]]; then
95
+ hook_success
96
+ exit 0
97
+ fi
98
+
99
+ REPO_ROOT=$(worktree_tracker_repo_root "$CWD")
100
+ DURATION_MS=$(worktree_tracker_duration_ms "$SESSION_ID" "$WORKTREE_PATH")
101
+ if [[ -n "$DURATION_MS" ]]; then
102
+ export ONLOOKER_WORKTREE_DURATION_MS="$DURATION_MS"
103
+ fi
104
+
105
+ ENRICHED=$(echo "$INPUT" | jq --arg repo "${REPO_ROOT:-}"} '. + {repo_root: $repo}')
106
+ worktree_tracker_emit "$ENRICHED"
107
+
108
+ if [[ -n "$REPO_ROOT" ]]; then
109
+ worktree_tracker_git_remove "$REPO_ROOT" "$WORKTREE_PATH"
110
+ fi
111
+
112
+ worktree_tracker_clear_by_path "$SESSION_ID" "$WORKTREE_PATH"
113
+
114
+ hook_success
115
+ exit 0
116
+ ;;
117
+ *)
118
+ hook_success
119
+ exit 0
120
+ ;;
121
+ esac
@@ -197,6 +197,59 @@ export function mapTaskHookInput(hookInput, options) {
197
197
  return { valid: true, event: result.event };
198
198
  }
199
199
 
200
+ /**
201
+ * Map WorktreeCreate / WorktreeRemove hook input to tool.shell.exec (interim until
202
+ * worktree.* event types exist in @onlooker-community/schema).
203
+ */
204
+ export function mapWorktreeHookInput(hookInput, options) {
205
+ const { onlookerDir, plugin, runtime = 'claude-code', adapter_id = 'ecosystem.hooks' } = options;
206
+ const hookEvent = hookInput?.hook_event_name;
207
+ const sessionId = hookInput?.session_id ?? 'unknown';
208
+ const cwd = hookInput?.cwd;
209
+
210
+ let command;
211
+ let worktreePath = hookInput?.worktree_path;
212
+
213
+ if (hookEvent === 'WorktreeCreate') {
214
+ const name = hookInput?.name;
215
+ if (!name) return null;
216
+ const branch = hookInput?.branch_name ?? `worktree-${name}`;
217
+ worktreePath = hookInput?.worktree_path;
218
+ command = `worktree:create name=${name} branch=${branch}${worktreePath ? ` path=${worktreePath}` : ''}`;
219
+ } else if (hookEvent === 'WorktreeRemove') {
220
+ if (!worktreePath) return null;
221
+ command = `worktree:remove path=${worktreePath}`;
222
+ } else {
223
+ return null;
224
+ }
225
+
226
+ const durationRaw = process.env.ONLOOKER_WORKTREE_DURATION_MS;
227
+ const durationMs = durationRaw != null && durationRaw !== '' ? Number.parseInt(String(durationRaw), 10) : undefined;
228
+
229
+ const payload = stripUndefined({
230
+ command,
231
+ exit_code: 0,
232
+ duration_ms: Number.isFinite(durationMs) && durationMs >= 0 ? durationMs : undefined,
233
+ working_directory: cwd,
234
+ });
235
+
236
+ const event = buildCanonicalEvent({
237
+ onlookerDir,
238
+ runtime,
239
+ adapter_id,
240
+ plugin,
241
+ session_id: sessionId,
242
+ event_type: TOOL_SHELL_EXEC,
243
+ payload,
244
+ });
245
+
246
+ const result = validate(event);
247
+ if (!result.valid) {
248
+ return { valid: false, errors: result.errors, event_type: TOOL_SHELL_EXEC };
249
+ }
250
+ return { valid: true, event: result.event };
251
+ }
252
+
200
253
  /**
201
254
  * Map Claude Code hook input to a canonical event.
202
255
  * Returns null when the hook input is not mapped to a schema event type.
@@ -208,6 +261,9 @@ export function mapHookInputToCanonical(hookInput, options) {
208
261
  const taskMapped = mapTaskHookInput(hookInput, options);
209
262
  if (taskMapped) return taskMapped;
210
263
 
264
+ const worktreeMapped = mapWorktreeHookInput(hookInput, options);
265
+ if (worktreeMapped) return worktreeMapped;
266
+
211
267
  const { onlookerDir, plugin, runtime = 'claude-code', adapter_id = 'ecosystem.hooks' } = options;
212
268
 
213
269
  const toolName = hookInput?.tool_name;
@@ -27,6 +27,7 @@ onlooker_event_from_hook() {
27
27
  printf '%s' "$hook_input" | ONLOOKER_DIR="$ONLOOKER_DIR" ONLOOKER_PLUGIN_NAME="$ONLOOKER_PLUGIN_NAME" \
28
28
  ONLOOKER_TURN_NUMBER="${ONLOOKER_TURN_NUMBER:-}" \
29
29
  ONLOOKER_TASK_DURATION_MS="${ONLOOKER_TASK_DURATION_MS:-}" \
30
+ ONLOOKER_WORKTREE_DURATION_MS="${ONLOOKER_WORKTREE_DURATION_MS:-}" \
30
31
  node "$_ONLOOKER_EVENT_JS" emit-from-hook 2>/dev/null
31
32
  }
32
33
 
@@ -0,0 +1,148 @@
1
+ #!/usr/bin/env bash
2
+ # Worktree lifecycle helpers — git worktree create/remove and telemetry state.
3
+ #
4
+ # Source after validate-path.sh, onlooker-schema.sh, session-tracker.sh, and tool-history.sh:
5
+ # source "$CLAUDE_PLUGIN_ROOT/scripts/lib/worktree-tracker.sh"
6
+
7
+ # Resolve repository root from hook cwd (must be inside a git work tree).
8
+ # Usage: repo_root=$(worktree_tracker_repo_root "$CWD")
9
+ worktree_tracker_repo_root() {
10
+ local cwd="${1:-}"
11
+ [[ -z "$cwd" ]] && return 1
12
+ git -C "$cwd" rev-parse --show-toplevel 2>/dev/null
13
+ }
14
+
15
+ # Create a Claude-style git worktree; prints absolute path on stdout, diagnostics on stderr.
16
+ # Usage: path=$(worktree_tracker_git_create "$REPO_ROOT" "$NAME")
17
+ worktree_tracker_git_create() {
18
+ local repo_root="${1:-}"
19
+ local name="${2:-}"
20
+ [[ -z "$repo_root" || -z "$name" ]] && return 1
21
+
22
+ local worktree_dir="${repo_root}/.claude/worktrees/${name}"
23
+ local branch="worktree-${name}"
24
+
25
+ mkdir -p "${repo_root}/.claude/worktrees"
26
+
27
+ if [[ -d "$worktree_dir" ]]; then
28
+ (cd "$worktree_dir" && pwd -P)
29
+ return 0
30
+ fi
31
+
32
+ local base_ref="HEAD"
33
+ if git -C "$repo_root" rev-parse --abbrev-ref HEAD >/dev/null 2>&1; then
34
+ base_ref=$(git -C "$repo_root" rev-parse --abbrev-ref HEAD)
35
+ fi
36
+
37
+ if git -C "$repo_root" show-ref --verify --quiet "refs/heads/${branch}" 2>/dev/null; then
38
+ git -C "$repo_root" worktree add "$worktree_dir" "$branch" >&2 || return 1
39
+ else
40
+ git -C "$repo_root" worktree add -b "$branch" "$worktree_dir" "$base_ref" >&2 || return 1
41
+ fi
42
+
43
+ (cd "$worktree_dir" && pwd -P)
44
+ }
45
+
46
+ # Remove a git worktree when it is registered for the repo (best-effort).
47
+ # Usage: worktree_tracker_git_remove "$REPO_ROOT" "$WORKTREE_PATH"
48
+ worktree_tracker_git_remove() {
49
+ local repo_root="${1:-}"
50
+ local worktree_path="${2:-}"
51
+ [[ -z "$repo_root" || -z "$worktree_path" ]] && return 0
52
+
53
+ local resolved_path="$worktree_path"
54
+ if [[ -d "$worktree_path" ]]; then
55
+ resolved_path=$(cd "$worktree_path" && pwd -P)
56
+ fi
57
+
58
+ if git -C "$repo_root" worktree list --porcelain 2>/dev/null | grep -Fq "worktree ${resolved_path}"; then
59
+ git -C "$repo_root" worktree remove --force "$resolved_path" >&2 || true
60
+ fi
61
+ }
62
+
63
+ # Record worktree creation in the per-session tracker for duration on remove.
64
+ # Usage: worktree_tracker_record_created "$SESSION_ID" "$NAME" "$PATH" "$BRANCH"
65
+ worktree_tracker_record_created() {
66
+ local session_id="${1:-}"
67
+ local name="${2:-}"
68
+ local worktree_path="${3:-}"
69
+ local branch="${4:-}"
70
+ [[ -z "$session_id" || -z "$name" || -z "$worktree_path" ]] && return 0
71
+
72
+ turn_state_ensure_session "$session_id" || return 1
73
+
74
+ local tracker_file="${ONLOOKER_SESSION_TRACKERS_DIR}/${session_id}"
75
+ local now_ms
76
+ now_ms=$(session_tracker_now_ms)
77
+
78
+ local temp_file
79
+ temp_file=$(mktemp)
80
+ if jq --arg name "$name" --arg path "$worktree_path" --arg branch "$branch" --argjson ms "$now_ms" \
81
+ '.worktrees[$name] = {path: $path, branch: $branch, start_time_ms: $ms}' \
82
+ "$tracker_file" >"$temp_file" 2>/dev/null; then
83
+ mv "$temp_file" "$tracker_file"
84
+ else
85
+ rm -f "$temp_file"
86
+ return 1
87
+ fi
88
+ }
89
+
90
+ # Compute duration for a worktree path; prints empty when unknown.
91
+ # Usage: duration_ms=$(worktree_tracker_duration_ms "$SESSION_ID" "$WORKTREE_PATH")
92
+ worktree_tracker_duration_ms() {
93
+ local session_id="${1:-}"
94
+ local worktree_path="${2:-}"
95
+ [[ -z "$session_id" || -z "$worktree_path" ]] && return 0
96
+
97
+ local tracker_file="${ONLOOKER_SESSION_TRACKERS_DIR}/${session_id}"
98
+ [[ ! -f "$tracker_file" ]] && return 0
99
+
100
+ local start_ms
101
+ start_ms=$(
102
+ jq -r --arg path "$worktree_path" '
103
+ [.worktrees // {} | to_entries[] | select(.value.path == $path) | .value.start_time_ms] | first // empty
104
+ ' "$tracker_file" 2>/dev/null
105
+ )
106
+ [[ -z "$start_ms" || "$start_ms" == "null" ]] && return 0
107
+
108
+ local now_ms elapsed
109
+ now_ms=$(session_tracker_now_ms)
110
+ elapsed=$((now_ms - start_ms))
111
+ if [[ "$elapsed" -ge 0 ]]; then
112
+ printf '%s' "$elapsed"
113
+ fi
114
+ }
115
+
116
+ # Remove worktree timing entry after removal telemetry is recorded.
117
+ # Usage: worktree_tracker_clear_by_path "$SESSION_ID" "$WORKTREE_PATH"
118
+ worktree_tracker_clear_by_path() {
119
+ local session_id="${1:-}"
120
+ local worktree_path="${2:-}"
121
+ [[ -z "$session_id" || -z "$worktree_path" ]] && return 0
122
+
123
+ local tracker_file="${ONLOOKER_SESSION_TRACKERS_DIR}/${session_id}"
124
+ [[ ! -f "$tracker_file" ]] && return 0
125
+
126
+ local temp_file
127
+ temp_file=$(mktemp)
128
+ if jq --arg path "$worktree_path" '
129
+ .worktrees |= with_entries(select(.value.path != $path))
130
+ ' "$tracker_file" >"$temp_file" 2>/dev/null; then
131
+ mv "$temp_file" "$tracker_file"
132
+ else
133
+ rm -f "$temp_file"
134
+ fi
135
+ }
136
+
137
+ # Build a canonical event from hook stdin (empty when unmapped).
138
+ # Usage: record=$(worktree_tracker_build_record "$INPUT")
139
+ worktree_tracker_build_record() {
140
+ local input_json="${1:-}"
141
+ onlooker_event_from_hook "$input_json"
142
+ }
143
+
144
+ # Append a canonical worktree event to session history.
145
+ # Usage: worktree_tracker_append "$SESSION_ID" "$event_json"
146
+ worktree_tracker_append() {
147
+ tool_history_append "$1" "$2"
148
+ }
@@ -197,6 +197,28 @@ setup_file() {
197
197
  [ "$status" -eq 0 ]
198
198
  }
199
199
 
200
+ @test "hooks.json WorktreeCreate references worktree-tracker" {
201
+ local hook_cmd
202
+ hook_cmd=$(jq -r '.hooks.WorktreeCreate[0].hooks[0].command' "${REPO_ROOT}/hooks/hooks.json")
203
+ [[ "$hook_cmd" == *worktree-tracker.sh ]]
204
+
205
+ local script_path="${hook_cmd//\$CLAUDE_PLUGIN_ROOT/$REPO_ROOT}"
206
+ script_path="${script_path//\"/}"
207
+ run test -x "$script_path"
208
+ [ "$status" -eq 0 ]
209
+ }
210
+
211
+ @test "hooks.json WorktreeRemove references worktree-tracker" {
212
+ local hook_cmd
213
+ hook_cmd=$(jq -r '.hooks.WorktreeRemove[0].hooks[0].command' "${REPO_ROOT}/hooks/hooks.json")
214
+ [[ "$hook_cmd" == *worktree-tracker.sh ]]
215
+
216
+ local script_path="${hook_cmd//\$CLAUDE_PLUGIN_ROOT/$REPO_ROOT}"
217
+ script_path="${script_path//\"/}"
218
+ run test -x "$script_path"
219
+ [ "$status" -eq 0 ]
220
+ }
221
+
200
222
  @test "plugin.json is valid JSON" {
201
223
  run jq -e '.name and .version' "${REPO_ROOT}/.claude-plugin/plugin.json"
202
224
  [ "$status" -eq 0 ]
@@ -0,0 +1,129 @@
1
+ #!/usr/bin/env bats
2
+
3
+ setup() {
4
+ # shellcheck source=../helpers/setup.bash
5
+ source "${BATS_TEST_DIRNAME}/../helpers/setup.bash"
6
+ load_validate_path
7
+ # shellcheck source=../../scripts/lib/onlooker-schema.sh
8
+ source "${REPO_ROOT}/scripts/lib/onlooker-schema.sh"
9
+ # shellcheck source=../../scripts/lib/session-tracker.sh
10
+ source "${REPO_ROOT}/scripts/lib/session-tracker.sh"
11
+ # shellcheck source=../../scripts/lib/tool-history.sh
12
+ source "${REPO_ROOT}/scripts/lib/tool-history.sh"
13
+ # shellcheck source=../../scripts/lib/worktree-tracker.sh
14
+ source "${REPO_ROOT}/scripts/lib/worktree-tracker.sh"
15
+ export CLAUDE_PLUGIN_ROOT="${REPO_ROOT}"
16
+
17
+ GIT_REPO="${BATS_TEST_TMPDIR}/git-repo"
18
+ rm -rf "$GIT_REPO"
19
+ mkdir -p "$GIT_REPO"
20
+ git -C "$GIT_REPO" init -q
21
+ git -C "$GIT_REPO" config user.email "test@example.com"
22
+ git -C "$GIT_REPO" config user.name "Test User"
23
+ echo "hello" >"$GIT_REPO/README.md"
24
+ git -C "$GIT_REPO" add README.md
25
+ git -C "$GIT_REPO" commit -q -m "init"
26
+ }
27
+
28
+ @test "worktree_tracker_build_record maps WorktreeCreate to tool.shell.exec" {
29
+ local fixture="${REPO_ROOT}/test/fixtures/hook-inputs/worktree-create.json"
30
+ local enriched
31
+ enriched=$(jq \
32
+ --arg path "${GIT_REPO}/.claude/worktrees/feature-auth" \
33
+ --arg branch "worktree-feature-auth" \
34
+ '. + {worktree_path: $path, branch_name: $branch}' \
35
+ "$fixture")
36
+ export ONLOOKER_WORKTREE_DURATION_MS=42
37
+ local record
38
+ record=$(worktree_tracker_build_record "$enriched")
39
+ echo "$record" | jq -e \
40
+ '.event_type == "tool.shell.exec"
41
+ and .payload.exit_code == 0
42
+ and .payload.duration_ms == 42
43
+ and (.payload.command | test("worktree:create"))
44
+ and .payload.working_directory == "/project/repo"' \
45
+ >/dev/null
46
+ echo "$record" | onlooker_validate_event
47
+ }
48
+
49
+ @test "worktree_tracker_build_record maps WorktreeRemove to tool.shell.exec" {
50
+ local fixture="${REPO_ROOT}/test/fixtures/hook-inputs/worktree-remove.json"
51
+ export ONLOOKER_WORKTREE_DURATION_MS=9000
52
+ local record
53
+ record=$(worktree_tracker_build_record "$(cat "$fixture")")
54
+ echo "$record" | jq -e \
55
+ '.event_type == "tool.shell.exec"
56
+ and (.payload.command | test("worktree:remove"))
57
+ and .payload.duration_ms == 9000' \
58
+ >/dev/null
59
+ echo "$record" | onlooker_validate_event
60
+ }
61
+
62
+ @test "worktree-tracker WorktreeCreate prints absolute path on stdout" {
63
+ local history_file="${ONLOOKER_SESSION_HISTORY_DIR}/worktree-session-001.jsonl"
64
+ rm -f "$history_file" "${history_file}.lock"
65
+ rm -f "${ONLOOKER_SESSION_TRACKERS_DIR}/worktree-session-001"
66
+
67
+ local input_file="${BATS_TEST_TMPDIR}/worktree-create-input.json"
68
+ jq \
69
+ --arg cwd "$GIT_REPO" \
70
+ --arg sid "worktree-session-001" \
71
+ '.cwd = $cwd | .session_id = $sid' \
72
+ "${REPO_ROOT}/test/fixtures/hook-inputs/worktree-create.json" >"$input_file"
73
+
74
+ local worktree_path
75
+ worktree_path=$(cat "$input_file" | "${REPO_ROOT}/scripts/hooks/worktree-tracker.sh")
76
+ [ -d "$worktree_path" ]
77
+ [[ "$worktree_path" == "$(cd "$worktree_path" && pwd -P)" ]]
78
+ [ -f "$history_file" ]
79
+ tail -n 1 "$history_file" | jq -e '.event_type == "tool.shell.exec"' >/dev/null
80
+ }
81
+
82
+ @test "worktree-tracker WorktreeRemove records telemetry and removes worktree" {
83
+ local history_file="${ONLOOKER_SESSION_HISTORY_DIR}/worktree-session-001.jsonl"
84
+ rm -f "$history_file" "${history_file}.lock"
85
+ rm -f "${ONLOOKER_SESSION_TRACKERS_DIR}/worktree-session-001"
86
+
87
+ local create_input_file="${BATS_TEST_TMPDIR}/worktree-create-input.json"
88
+ local remove_input_file="${BATS_TEST_TMPDIR}/worktree-remove-input.json"
89
+ jq \
90
+ --arg cwd "$GIT_REPO" \
91
+ --arg sid "worktree-session-001" \
92
+ '.cwd = $cwd | .session_id = $sid' \
93
+ "${REPO_ROOT}/test/fixtures/hook-inputs/worktree-create.json" >"$create_input_file"
94
+
95
+ local worktree_path
96
+ worktree_path=$(cat "$create_input_file" | "${REPO_ROOT}/scripts/hooks/worktree-tracker.sh")
97
+
98
+ jq \
99
+ --arg cwd "$GIT_REPO" \
100
+ --arg path "$worktree_path" \
101
+ --arg sid "worktree-session-001" \
102
+ '.cwd = $cwd | .worktree_path = $path | .session_id = $sid' \
103
+ "${REPO_ROOT}/test/fixtures/hook-inputs/worktree-remove.json" >"$remove_input_file"
104
+
105
+ run bash -c "cat '${remove_input_file}' | '${REPO_ROOT}/scripts/hooks/worktree-tracker.sh' 2>/dev/null"
106
+ [ "$status" -eq 0 ]
107
+ [ ! -d "$worktree_path" ]
108
+
109
+ tail -n 1 "$history_file" | jq -e \
110
+ '.event_type == "tool.shell.exec"
111
+ and (.payload.command | test("worktree:remove"))' \
112
+ >/dev/null
113
+ }
114
+
115
+ @test "worktree-tracker mirrors worktree events to global events log" {
116
+ local input
117
+ input=$(jq \
118
+ --arg cwd "$GIT_REPO" \
119
+ '.cwd = $cwd' \
120
+ "${REPO_ROOT}/test/fixtures/hook-inputs/worktree-create.json")
121
+ : >"$ONLOOKER_EVENTS_LOG"
122
+
123
+ printf '%s' "$input" | "${REPO_ROOT}/scripts/hooks/worktree-tracker.sh" >/dev/null
124
+
125
+ tail -n 1 "$ONLOOKER_EVENTS_LOG" | jq -e \
126
+ '.event_type == "tool.shell.exec"
127
+ and (.payload.command | test("worktree:create"))' \
128
+ >/dev/null
129
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "session_id": "worktree-session-001",
3
+ "transcript_path": "/tmp/transcript.jsonl",
4
+ "cwd": "/project/repo",
5
+ "permission_mode": "default",
6
+ "hook_event_name": "WorktreeCreate",
7
+ "name": "feature-auth"
8
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "session_id": "worktree-session-001",
3
+ "transcript_path": "/tmp/transcript.jsonl",
4
+ "cwd": "/project/repo",
5
+ "permission_mode": "default",
6
+ "hook_event_name": "WorktreeRemove",
7
+ "worktree_path": "/project/repo/.claude/worktrees/feature-auth"
8
+ }
@@ -10,6 +10,7 @@ import {
10
10
  mapHookInputToCanonical,
11
11
  mapSkillHookInput,
12
12
  mapTaskHookInput,
13
+ mapWorktreeHookInput,
13
14
  } from '../../scripts/lib/onlooker-event.mjs';
14
15
 
15
16
  const REPO_ROOT = join(fileURLToPath(new URL('../..', import.meta.url)));
@@ -111,6 +112,43 @@ test('mapTaskHookInput maps TaskCompleted to task.complete', () => {
111
112
  assert.equal(validate(mapped.event).valid, true);
112
113
  });
113
114
 
115
+ test('mapWorktreeHookInput maps WorktreeCreate to tool.shell.exec', () => {
116
+ const hookInput = {
117
+ ...loadFixture('worktree-create.json'),
118
+ worktree_path: '/project/repo/.claude/worktrees/feature-auth',
119
+ branch_name: 'worktree-feature-auth',
120
+ };
121
+ const tmpDir = join(REPO_ROOT, 'test/tmp-schema-events');
122
+ const prev = process.env.ONLOOKER_WORKTREE_DURATION_MS;
123
+ process.env.ONLOOKER_WORKTREE_DURATION_MS = '15';
124
+ const mapped = mapWorktreeHookInput(hookInput, {
125
+ onlookerDir: tmpDir,
126
+ plugin: 'onlooker',
127
+ });
128
+ if (prev === undefined) delete process.env.ONLOOKER_WORKTREE_DURATION_MS;
129
+ else process.env.ONLOOKER_WORKTREE_DURATION_MS = prev;
130
+
131
+ assert.equal(mapped.valid, true);
132
+ assert.equal(mapped.event.event_type, 'tool.shell.exec');
133
+ assert.equal(mapped.event.payload.exit_code, 0);
134
+ assert.equal(mapped.event.payload.duration_ms, 15);
135
+ assert.match(mapped.event.payload.command, /worktree:create/);
136
+ assert.equal(validate(mapped.event).valid, true);
137
+ });
138
+
139
+ test('mapWorktreeHookInput maps WorktreeRemove to tool.shell.exec', () => {
140
+ const hookInput = loadFixture('worktree-remove.json');
141
+ const tmpDir = join(REPO_ROOT, 'test/tmp-schema-events');
142
+ const mapped = mapWorktreeHookInput(hookInput, {
143
+ onlookerDir: tmpDir,
144
+ plugin: 'onlooker',
145
+ });
146
+
147
+ assert.equal(mapped.valid, true);
148
+ assert.match(mapped.event.payload.command, /worktree:remove/);
149
+ assert.equal(validate(mapped.event).valid, true);
150
+ });
151
+
114
152
  test('mapHookInputToCanonical routes TaskCreated through task mapping', () => {
115
153
  const hookInput = loadFixture('task-created.json');
116
154
  const tmpDir = join(REPO_ROOT, 'test/tmp-schema-events');
@@ -1,5 +0,0 @@
1
- {
2
- "enabledPlugins": {
3
- "ecosystem@onlooker-community": true
4
- }
5
- }