@onlooker-community/ecosystem 0.7.2 → 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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.github/workflows/release.yml +17 -57
- package/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +41 -0
- package/hooks/hooks.json +40 -0
- package/package.json +2 -2
- package/release-please-config.json +13 -18
- package/scripts/hooks/task-tracker.sh +61 -0
- package/scripts/hooks/worktree-tracker.sh +121 -0
- package/scripts/lib/onlooker-event.mjs +111 -0
- package/scripts/lib/onlooker-schema.sh +2 -0
- package/scripts/lib/task-tracker.sh +84 -0
- package/scripts/lib/worktree-tracker.sh +148 -0
- package/test/bats/config.bats +44 -0
- package/test/bats/task-tracker.bats +99 -0
- package/test/bats/worktree-tracker.bats +129 -0
- package/test/fixtures/hook-inputs/task-completed.json +12 -0
- package/test/fixtures/hook-inputs/task-created.json +12 -0
- package/test/fixtures/hook-inputs/worktree-create.json +8 -0
- package/test/fixtures/hook-inputs/worktree-remove.json +8 -0
- package/test/node/schema-events.test.mjs +90 -1
- package/.claude/settings.local.json +0 -5
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
name: Release
|
|
2
|
-
|
|
3
|
-
concurrency:
|
|
4
|
-
group: ${{ github.workflow }}
|
|
5
|
-
|
|
1
|
+
name: Release Please
|
|
6
2
|
on:
|
|
7
3
|
push:
|
|
8
4
|
branches:
|
|
@@ -11,65 +7,29 @@ on:
|
|
|
11
7
|
permissions:
|
|
12
8
|
contents: write
|
|
13
9
|
pull-requests: write
|
|
14
|
-
issues: write
|
|
15
|
-
# Required for npm publish --provenance (OIDC attestation)
|
|
16
|
-
id-token: write
|
|
17
10
|
|
|
18
11
|
jobs:
|
|
19
12
|
release-please:
|
|
20
|
-
name: Release Please
|
|
21
13
|
runs-on: ubuntu-latest
|
|
22
14
|
steps:
|
|
23
|
-
-
|
|
15
|
+
- uses: googleapis/release-please-action@v4
|
|
24
16
|
id: release
|
|
25
|
-
uses: googleapis/release-please-action@16a9c90856f42705d54a6fda1823352bdc62cf38 # v4.4.0
|
|
26
17
|
with:
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
# deliberately do not fan out to other workflows; that policy
|
|
35
|
-
# leaves release PRs unchecked. Set RELEASE_PLEASE_PAT to a
|
|
36
|
-
# fine-grained token with Contents:write + Pull requests:write
|
|
37
|
-
# scoped to this repo.
|
|
38
|
-
token: ${{ secrets.RELEASE_PLEASE_PAT }}
|
|
39
|
-
|
|
40
|
-
- uses: actions/checkout@v6
|
|
41
|
-
if: ${{ steps.release.outputs.releases_created == 'true'}}
|
|
42
|
-
|
|
43
|
-
- uses: actions/setup-node@v6
|
|
44
|
-
if: ${{ steps.release.outputs.releases_created == 'true'}}
|
|
18
|
+
token: ${{ secrets.RELEASE_PLEASE_TOKEN }}
|
|
19
|
+
|
|
20
|
+
- uses: actions/checkout@v4
|
|
21
|
+
if: ${{ steps.release.outputs.release_created }}
|
|
22
|
+
|
|
23
|
+
- uses: actions/setup-node@v4
|
|
24
|
+
if: ${{ steps.release.outputs.release_created }}
|
|
45
25
|
with:
|
|
46
26
|
node-version: '22'
|
|
47
|
-
registry-url: https://registry.npmjs.org
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
27
|
+
registry-url: 'https://registry.npmjs.org'
|
|
28
|
+
|
|
29
|
+
- run: npm ci
|
|
30
|
+
if: ${{ steps.release.outputs.release_created }}
|
|
31
|
+
|
|
32
|
+
- run: npm publish
|
|
33
|
+
if: ${{ steps.release.outputs.release_created }}
|
|
52
34
|
env:
|
|
53
|
-
|
|
54
|
-
PATHS_RELEASED: ${{ steps.release.outputs.paths_released }}
|
|
55
|
-
run: |
|
|
56
|
-
set -euo pipefail
|
|
57
|
-
if [[ -z "${NPM_TOKEN}" ]]; then
|
|
58
|
-
echo "NPM_TOKEN secret is required to publish to npm." >&2
|
|
59
|
-
exit 1
|
|
60
|
-
fi
|
|
61
|
-
printf '%s\n' "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" >> "${HOME}/.npmrc"
|
|
62
|
-
npm ci
|
|
63
|
-
# paths_released is JSON (e.g. ["."], not bare paths)
|
|
64
|
-
mapfile -t release_paths < <(echo "${PATHS_RELEASED}" | jq -r '.[]')
|
|
65
|
-
if [[ "${#release_paths[@]}" -eq 0 ]]; then
|
|
66
|
-
echo "No paths in paths_released; skipping npm publish." >&2
|
|
67
|
-
exit 0
|
|
68
|
-
fi
|
|
69
|
-
for path in "${release_paths[@]}"; do
|
|
70
|
-
if [[ "$path" == "." ]]; then
|
|
71
|
-
npm publish --access public --provenance
|
|
72
|
-
else
|
|
73
|
-
(cd "$path" && npm publish --access public --provenance)
|
|
74
|
-
fi
|
|
75
|
-
done
|
|
35
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,46 @@
|
|
|
1
|
+
# [0.8.0](https://github.com/onlooker-community/ecosystem/compare/v0.7.2...v0.8.0) (2026-05-22)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* **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))
|
|
7
|
+
|
|
1
8
|
# Changelog
|
|
2
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
|
+
|
|
3
44
|
## [0.7.2](https://github.com/onlooker-community/ecosystem/compare/v0.7.1...v0.7.2) (2026-05-22)
|
|
4
45
|
|
|
5
46
|
|
package/hooks/hooks.json
CHANGED
|
@@ -137,6 +137,46 @@
|
|
|
137
137
|
}
|
|
138
138
|
]
|
|
139
139
|
}
|
|
140
|
+
],
|
|
141
|
+
"TaskCreated": [
|
|
142
|
+
{
|
|
143
|
+
"hooks": [
|
|
144
|
+
{
|
|
145
|
+
"type": "command",
|
|
146
|
+
"command": "\"$CLAUDE_PLUGIN_ROOT\"/scripts/hooks/task-tracker.sh"
|
|
147
|
+
}
|
|
148
|
+
]
|
|
149
|
+
}
|
|
150
|
+
],
|
|
151
|
+
"TaskCompleted": [
|
|
152
|
+
{
|
|
153
|
+
"hooks": [
|
|
154
|
+
{
|
|
155
|
+
"type": "command",
|
|
156
|
+
"command": "\"$CLAUDE_PLUGIN_ROOT\"/scripts/hooks/task-tracker.sh"
|
|
157
|
+
}
|
|
158
|
+
]
|
|
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
|
+
}
|
|
140
180
|
]
|
|
141
181
|
}
|
|
142
182
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onlooker-community/ecosystem",
|
|
3
|
-
"version": "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": {
|
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json",
|
|
3
2
|
"packages": {
|
|
4
3
|
".": {
|
|
5
|
-
"release-type": "node",
|
|
6
|
-
"pull-request-title-pattern": "chore: release ${version}",
|
|
7
4
|
"changelog-path": "CHANGELOG.md",
|
|
8
|
-
"
|
|
9
|
-
"
|
|
5
|
+
"release-type": "node",
|
|
6
|
+
"bump-minor-pre-major": false,
|
|
7
|
+
"bump-patch-for-minor-pre-major": false,
|
|
8
|
+
"draft": false,
|
|
9
|
+
"prerelease": false,
|
|
10
10
|
"extra-files": [
|
|
11
|
+
{
|
|
12
|
+
"type": "json",
|
|
13
|
+
"path": "package.json",
|
|
14
|
+
"jsonpath": "$.version"
|
|
15
|
+
},
|
|
11
16
|
{
|
|
12
17
|
"type": "json",
|
|
13
18
|
"path": ".claude-plugin/plugin.json",
|
|
@@ -16,20 +21,10 @@
|
|
|
16
21
|
{
|
|
17
22
|
"type": "json",
|
|
18
23
|
"path": ".claude-plugin/marketplace.json",
|
|
19
|
-
"jsonpath": "$.plugins
|
|
24
|
+
"jsonpath": "$.plugins[0].version"
|
|
20
25
|
}
|
|
21
|
-
],
|
|
22
|
-
"changelog-sections": [
|
|
23
|
-
{ "type": "feat", "section": "Features" },
|
|
24
|
-
{ "type": "fix", "section": "Bug Fixes" },
|
|
25
|
-
{ "type": "perf", "section": "Performance" },
|
|
26
|
-
{ "type": "refactor", "section": "Refactoring" },
|
|
27
|
-
{ "type": "docs", "section": "Documentation" },
|
|
28
|
-
{ "type": "test", "section": "Tests" },
|
|
29
|
-
{ "type": "chore", "section": "Chores" },
|
|
30
|
-
{ "type": "ci", "section": "CI/CD" },
|
|
31
|
-
{ "type": "build", "section": "Build" }
|
|
32
26
|
]
|
|
33
27
|
}
|
|
34
|
-
}
|
|
28
|
+
},
|
|
29
|
+
"$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json"
|
|
35
30
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Onlooker Task Tracker
|
|
3
|
+
# Invoked by TaskCreated and TaskCompleted when agent team tasks are created or completed.
|
|
4
|
+
#
|
|
5
|
+
# Records canonical task.start and task.complete events to:
|
|
6
|
+
# ~/.onlooker/session-history/<session_id>.jsonl
|
|
7
|
+
# ~/.onlooker/logs/onlooker-events.jsonl
|
|
8
|
+
#
|
|
9
|
+
# Usage:
|
|
10
|
+
# echo "$INPUT" | task-tracker.sh
|
|
11
|
+
|
|
12
|
+
set -uo pipefail # No -e: never block task create/complete
|
|
13
|
+
|
|
14
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
15
|
+
source "$SCRIPT_DIR/../lib/validate-path.sh"
|
|
16
|
+
source "$SCRIPT_DIR/../lib/onlooker-schema.sh"
|
|
17
|
+
source "$SCRIPT_DIR/../lib/session-tracker.sh"
|
|
18
|
+
source "$SCRIPT_DIR/../lib/tool-history.sh"
|
|
19
|
+
source "$SCRIPT_DIR/../lib/task-tracker.sh"
|
|
20
|
+
|
|
21
|
+
hook_register "task-tracker" "Task Tracker" "Records task.start and task.complete canonical events"
|
|
22
|
+
|
|
23
|
+
INPUT=$(cat)
|
|
24
|
+
|
|
25
|
+
HOOK_EVENT=$(echo "$INPUT" | jq -r '.hook_event_name // ""')
|
|
26
|
+
hook_set_context "$INPUT" "$HOOK_EVENT"
|
|
27
|
+
|
|
28
|
+
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // "unknown"')
|
|
29
|
+
TASK_ID=$(echo "$INPUT" | jq -r '.task_id // ""')
|
|
30
|
+
|
|
31
|
+
turn_state_export "$SESSION_ID"
|
|
32
|
+
|
|
33
|
+
case "$HOOK_EVENT" in
|
|
34
|
+
TaskCreated)
|
|
35
|
+
task_tracker_record_created "$SESSION_ID" "$TASK_ID" \
|
|
36
|
+
|| hook_failure "Failed to record task start time"
|
|
37
|
+
;;
|
|
38
|
+
TaskCompleted)
|
|
39
|
+
DURATION_MS=$(task_tracker_duration_ms "$SESSION_ID" "$TASK_ID")
|
|
40
|
+
if [[ -n "$DURATION_MS" ]]; then
|
|
41
|
+
export ONLOOKER_TASK_DURATION_MS="$DURATION_MS"
|
|
42
|
+
fi
|
|
43
|
+
;;
|
|
44
|
+
*)
|
|
45
|
+
hook_success
|
|
46
|
+
exit 0
|
|
47
|
+
;;
|
|
48
|
+
esac
|
|
49
|
+
|
|
50
|
+
RECORD=$(task_tracker_build_record "$INPUT")
|
|
51
|
+
if [[ -n "$RECORD" ]]; then
|
|
52
|
+
task_tracker_append "$SESSION_ID" "$RECORD" || hook_failure "Failed to append session history"
|
|
53
|
+
onlooker_append_event "$RECORD" || hook_failure "Failed to append global event log"
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
if [[ "$HOOK_EVENT" == "TaskCompleted" && -n "$TASK_ID" ]]; then
|
|
57
|
+
task_tracker_clear "$SESSION_ID" "$TASK_ID"
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
hook_success
|
|
61
|
+
exit 0
|
|
@@ -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
|
|
@@ -9,6 +9,8 @@ import { join } from 'node:path';
|
|
|
9
9
|
import {
|
|
10
10
|
createEvent,
|
|
11
11
|
SKILL_INVOKED,
|
|
12
|
+
TASK_COMPLETE,
|
|
13
|
+
TASK_START,
|
|
12
14
|
TOOL_AGENT_COMPLETE,
|
|
13
15
|
TOOL_AGENT_SPAWN,
|
|
14
16
|
TOOL_FILE_EDIT,
|
|
@@ -145,6 +147,109 @@ export function mapSkillHookInput(hookInput, options) {
|
|
|
145
147
|
return { valid: true, event: result.event };
|
|
146
148
|
}
|
|
147
149
|
|
|
150
|
+
/**
|
|
151
|
+
* Map TaskCreated / TaskCompleted hook input to task.start or task.complete.
|
|
152
|
+
* Returns null when the hook input is not a task lifecycle event.
|
|
153
|
+
*/
|
|
154
|
+
export function mapTaskHookInput(hookInput, options) {
|
|
155
|
+
const { onlookerDir, plugin, runtime = 'claude-code', adapter_id = 'ecosystem.hooks' } = options;
|
|
156
|
+
const hookEvent = hookInput?.hook_event_name;
|
|
157
|
+
const sessionId = hookInput?.session_id ?? 'unknown';
|
|
158
|
+
const taskSubject = hookInput?.task_subject;
|
|
159
|
+
if (!taskSubject) return null;
|
|
160
|
+
|
|
161
|
+
let eventType;
|
|
162
|
+
let payload;
|
|
163
|
+
|
|
164
|
+
if (hookEvent === 'TaskCreated') {
|
|
165
|
+
eventType = TASK_START;
|
|
166
|
+
payload = stripUndefined({
|
|
167
|
+
task_summary: taskSubject,
|
|
168
|
+
});
|
|
169
|
+
} else if (hookEvent === 'TaskCompleted') {
|
|
170
|
+
eventType = TASK_COMPLETE;
|
|
171
|
+
const durationRaw = process.env.ONLOOKER_TASK_DURATION_MS;
|
|
172
|
+
const durationMs = durationRaw != null && durationRaw !== '' ? Number.parseInt(String(durationRaw), 10) : undefined;
|
|
173
|
+
const description = hookInput?.task_description;
|
|
174
|
+
payload = stripUndefined({
|
|
175
|
+
success: true,
|
|
176
|
+
duration_ms: Number.isFinite(durationMs) && durationMs >= 0 ? durationMs : undefined,
|
|
177
|
+
output_summary: description ? summarizeText(description, 500) : summarizeText(taskSubject, 500),
|
|
178
|
+
});
|
|
179
|
+
} else {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const event = buildCanonicalEvent({
|
|
184
|
+
onlookerDir,
|
|
185
|
+
runtime,
|
|
186
|
+
adapter_id,
|
|
187
|
+
plugin,
|
|
188
|
+
session_id: sessionId,
|
|
189
|
+
event_type: eventType,
|
|
190
|
+
payload,
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
const result = validate(event);
|
|
194
|
+
if (!result.valid) {
|
|
195
|
+
return { valid: false, errors: result.errors, event_type: eventType };
|
|
196
|
+
}
|
|
197
|
+
return { valid: true, event: result.event };
|
|
198
|
+
}
|
|
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
|
+
|
|
148
253
|
/**
|
|
149
254
|
* Map Claude Code hook input to a canonical event.
|
|
150
255
|
* Returns null when the hook input is not mapped to a schema event type.
|
|
@@ -153,6 +258,12 @@ export function mapHookInputToCanonical(hookInput, options) {
|
|
|
153
258
|
const skillMapped = mapSkillHookInput(hookInput, options);
|
|
154
259
|
if (skillMapped) return skillMapped;
|
|
155
260
|
|
|
261
|
+
const taskMapped = mapTaskHookInput(hookInput, options);
|
|
262
|
+
if (taskMapped) return taskMapped;
|
|
263
|
+
|
|
264
|
+
const worktreeMapped = mapWorktreeHookInput(hookInput, options);
|
|
265
|
+
if (worktreeMapped) return worktreeMapped;
|
|
266
|
+
|
|
156
267
|
const { onlookerDir, plugin, runtime = 'claude-code', adapter_id = 'ecosystem.hooks' } = options;
|
|
157
268
|
|
|
158
269
|
const toolName = hookInput?.tool_name;
|
|
@@ -26,6 +26,8 @@ onlooker_event_from_hook() {
|
|
|
26
26
|
|
|
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
|
+
ONLOOKER_TASK_DURATION_MS="${ONLOOKER_TASK_DURATION_MS:-}" \
|
|
30
|
+
ONLOOKER_WORKTREE_DURATION_MS="${ONLOOKER_WORKTREE_DURATION_MS:-}" \
|
|
29
31
|
node "$_ONLOOKER_EVENT_JS" emit-from-hook 2>/dev/null
|
|
30
32
|
}
|
|
31
33
|
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Task lifecycle helpers — task.start / task.complete canonical events.
|
|
3
|
+
#
|
|
4
|
+
# Source after validate-path.sh, onlooker-schema.sh, session-tracker.sh, and tool-history.sh:
|
|
5
|
+
# source "$CLAUDE_PLUGIN_ROOT/scripts/lib/task-tracker.sh"
|
|
6
|
+
|
|
7
|
+
# Record task creation time in the per-session tracker for duration on complete.
|
|
8
|
+
# Usage: task_tracker_record_created "$SESSION_ID" "$TASK_ID"
|
|
9
|
+
task_tracker_record_created() {
|
|
10
|
+
local session_id="${1:-}"
|
|
11
|
+
local task_id="${2:-}"
|
|
12
|
+
[[ -z "$session_id" || "$session_id" == "null" || -z "$task_id" || "$task_id" == "null" ]] && return 0
|
|
13
|
+
|
|
14
|
+
turn_state_ensure_session "$session_id" || return 1
|
|
15
|
+
|
|
16
|
+
local tracker_file="${ONLOOKER_SESSION_TRACKERS_DIR}/${session_id}"
|
|
17
|
+
local now_ms
|
|
18
|
+
now_ms=$(session_tracker_now_ms)
|
|
19
|
+
|
|
20
|
+
local temp_file
|
|
21
|
+
temp_file=$(mktemp)
|
|
22
|
+
if jq --arg id "$task_id" --argjson ms "$now_ms" \
|
|
23
|
+
'.tasks[$id] = {start_time_ms: $ms}' \
|
|
24
|
+
"$tracker_file" >"$temp_file" 2>/dev/null; then
|
|
25
|
+
mv "$temp_file" "$tracker_file"
|
|
26
|
+
else
|
|
27
|
+
rm -f "$temp_file"
|
|
28
|
+
return 1
|
|
29
|
+
fi
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
# Compute duration since task creation; prints empty when unknown.
|
|
33
|
+
# Usage: duration_ms=$(task_tracker_duration_ms "$SESSION_ID" "$TASK_ID")
|
|
34
|
+
task_tracker_duration_ms() {
|
|
35
|
+
local session_id="${1:-}"
|
|
36
|
+
local task_id="${2:-}"
|
|
37
|
+
[[ -z "$session_id" || -z "$task_id" ]] && return 0
|
|
38
|
+
|
|
39
|
+
local tracker_file="${ONLOOKER_SESSION_TRACKERS_DIR}/${session_id}"
|
|
40
|
+
[[ ! -f "$tracker_file" ]] && return 0
|
|
41
|
+
|
|
42
|
+
local start_ms
|
|
43
|
+
start_ms=$(jq -r --arg id "$task_id" '.tasks[$id].start_time_ms // empty' "$tracker_file" 2>/dev/null)
|
|
44
|
+
[[ -z "$start_ms" || "$start_ms" == "null" ]] && return 0
|
|
45
|
+
|
|
46
|
+
local now_ms elapsed
|
|
47
|
+
now_ms=$(session_tracker_now_ms)
|
|
48
|
+
elapsed=$((now_ms - start_ms))
|
|
49
|
+
if [[ "$elapsed" -ge 0 ]]; then
|
|
50
|
+
printf '%s' "$elapsed"
|
|
51
|
+
fi
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
# Remove task timing entry after completion.
|
|
55
|
+
# Usage: task_tracker_clear "$SESSION_ID" "$TASK_ID"
|
|
56
|
+
task_tracker_clear() {
|
|
57
|
+
local session_id="${1:-}"
|
|
58
|
+
local task_id="${2:-}"
|
|
59
|
+
[[ -z "$session_id" || -z "$task_id" ]] && return 0
|
|
60
|
+
|
|
61
|
+
local tracker_file="${ONLOOKER_SESSION_TRACKERS_DIR}/${session_id}"
|
|
62
|
+
[[ ! -f "$tracker_file" ]] && return 0
|
|
63
|
+
|
|
64
|
+
local temp_file
|
|
65
|
+
temp_file=$(mktemp)
|
|
66
|
+
if jq --arg id "$task_id" 'del(.tasks[$id])' "$tracker_file" >"$temp_file" 2>/dev/null; then
|
|
67
|
+
mv "$temp_file" "$tracker_file"
|
|
68
|
+
else
|
|
69
|
+
rm -f "$temp_file"
|
|
70
|
+
fi
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
# Build a canonical task.* event from hook stdin (empty when unmapped).
|
|
74
|
+
# Usage: record=$(task_tracker_build_record "$INPUT")
|
|
75
|
+
task_tracker_build_record() {
|
|
76
|
+
local input_json="${1:-}"
|
|
77
|
+
onlooker_event_from_hook "$input_json"
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
# Append a canonical task event to session history (reuses tool-history flock).
|
|
81
|
+
# Usage: task_tracker_append "$SESSION_ID" "$event_json"
|
|
82
|
+
task_tracker_append() {
|
|
83
|
+
tool_history_append "$1" "$2"
|
|
84
|
+
}
|
|
@@ -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
|
+
}
|
package/test/bats/config.bats
CHANGED
|
@@ -175,6 +175,50 @@ setup_file() {
|
|
|
175
175
|
[ "$status" -eq 0 ]
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
+
@test "hooks.json TaskCreated references task-tracker" {
|
|
179
|
+
local hook_cmd
|
|
180
|
+
hook_cmd=$(jq -r '.hooks.TaskCreated[0].hooks[0].command' "${REPO_ROOT}/hooks/hooks.json")
|
|
181
|
+
[[ "$hook_cmd" == *task-tracker.sh ]]
|
|
182
|
+
|
|
183
|
+
local script_path="${hook_cmd//\$CLAUDE_PLUGIN_ROOT/$REPO_ROOT}"
|
|
184
|
+
script_path="${script_path//\"/}"
|
|
185
|
+
run test -x "$script_path"
|
|
186
|
+
[ "$status" -eq 0 ]
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
@test "hooks.json TaskCompleted references task-tracker" {
|
|
190
|
+
local hook_cmd
|
|
191
|
+
hook_cmd=$(jq -r '.hooks.TaskCompleted[0].hooks[0].command' "${REPO_ROOT}/hooks/hooks.json")
|
|
192
|
+
[[ "$hook_cmd" == *task-tracker.sh ]]
|
|
193
|
+
|
|
194
|
+
local script_path="${hook_cmd//\$CLAUDE_PLUGIN_ROOT/$REPO_ROOT}"
|
|
195
|
+
script_path="${script_path//\"/}"
|
|
196
|
+
run test -x "$script_path"
|
|
197
|
+
[ "$status" -eq 0 ]
|
|
198
|
+
}
|
|
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
|
+
|
|
178
222
|
@test "plugin.json is valid JSON" {
|
|
179
223
|
run jq -e '.name and .version' "${REPO_ROOT}/.claude-plugin/plugin.json"
|
|
180
224
|
[ "$status" -eq 0 ]
|
|
@@ -0,0 +1,99 @@
|
|
|
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/task-tracker.sh
|
|
14
|
+
source "${REPO_ROOT}/scripts/lib/task-tracker.sh"
|
|
15
|
+
export CLAUDE_PLUGIN_ROOT="${REPO_ROOT}"
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
@test "task_tracker_build_record maps TaskCreated to task.start" {
|
|
19
|
+
local fixture="${REPO_ROOT}/test/fixtures/hook-inputs/task-created.json"
|
|
20
|
+
local record
|
|
21
|
+
record=$(task_tracker_build_record "$(cat "$fixture")")
|
|
22
|
+
echo "$record" | jq -e \
|
|
23
|
+
'.schema_version == "1.0"
|
|
24
|
+
and .event_type == "task.start"
|
|
25
|
+
and .payload.task_summary == "Implement user authentication"
|
|
26
|
+
and .session_id == "task-session-001"' \
|
|
27
|
+
>/dev/null
|
|
28
|
+
echo "$record" | onlooker_validate_event
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@test "task_tracker_build_record maps TaskCompleted to task.complete" {
|
|
32
|
+
local fixture="${REPO_ROOT}/test/fixtures/hook-inputs/task-completed.json"
|
|
33
|
+
export ONLOOKER_TASK_DURATION_MS=5000
|
|
34
|
+
local record
|
|
35
|
+
record=$(task_tracker_build_record "$(cat "$fixture")")
|
|
36
|
+
echo "$record" | jq -e \
|
|
37
|
+
'.event_type == "task.complete"
|
|
38
|
+
and .payload.success == true
|
|
39
|
+
and .payload.duration_ms == 5000
|
|
40
|
+
and .payload.output_summary == "Add login and signup endpoints"' \
|
|
41
|
+
>/dev/null
|
|
42
|
+
echo "$record" | onlooker_validate_event
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
@test "task-tracker records task.start on TaskCreated" {
|
|
46
|
+
local fixture="${REPO_ROOT}/test/fixtures/hook-inputs/task-created.json"
|
|
47
|
+
local history_file="${ONLOOKER_SESSION_HISTORY_DIR}/task-session-001.jsonl"
|
|
48
|
+
rm -f "$history_file" "${history_file}.lock"
|
|
49
|
+
rm -f "${ONLOOKER_SESSION_TRACKERS_DIR}/task-session-001"
|
|
50
|
+
|
|
51
|
+
run bash -c "cat '${fixture}' | '${REPO_ROOT}/scripts/hooks/task-tracker.sh' 2>/dev/null"
|
|
52
|
+
[ "$status" -eq 0 ]
|
|
53
|
+
[ -f "$history_file" ]
|
|
54
|
+
tail -n 1 "$history_file" | jq -e '.event_type == "task.start"' >/dev/null
|
|
55
|
+
tail -n 1 "$history_file" | onlooker_validate_event
|
|
56
|
+
|
|
57
|
+
jq -e '.tasks["task-001"].start_time_ms | type == "number"' \
|
|
58
|
+
"${ONLOOKER_SESSION_TRACKERS_DIR}/task-session-001" >/dev/null
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
@test "task-tracker records task.complete with duration on TaskCompleted" {
|
|
62
|
+
local created_fixture="${REPO_ROOT}/test/fixtures/hook-inputs/task-created.json"
|
|
63
|
+
local completed_fixture="${REPO_ROOT}/test/fixtures/hook-inputs/task-completed.json"
|
|
64
|
+
local history_file="${ONLOOKER_SESSION_HISTORY_DIR}/task-session-001.jsonl"
|
|
65
|
+
rm -f "$history_file" "${history_file}.lock"
|
|
66
|
+
rm -f "${ONLOOKER_SESSION_TRACKERS_DIR}/task-session-001"
|
|
67
|
+
|
|
68
|
+
cat "$created_fixture" | "${REPO_ROOT}/scripts/hooks/task-tracker.sh" >/dev/null 2>&1
|
|
69
|
+
|
|
70
|
+
local tracker="${ONLOOKER_SESSION_TRACKERS_DIR}/task-session-001"
|
|
71
|
+
local past_ms
|
|
72
|
+
past_ms=$(python3 -c 'import time; print(int((time.time() - 2) * 1000))' 2>/dev/null || echo 0)
|
|
73
|
+
jq --argjson start_ms "$past_ms" '.tasks["task-001"].start_time_ms = $start_ms' "$tracker" >"${tracker}.tmp"
|
|
74
|
+
mv "${tracker}.tmp" "$tracker"
|
|
75
|
+
|
|
76
|
+
run bash -c "cat '${completed_fixture}' | '${REPO_ROOT}/scripts/hooks/task-tracker.sh' 2>/dev/null"
|
|
77
|
+
[ "$status" -eq 0 ]
|
|
78
|
+
|
|
79
|
+
tail -n 1 "$history_file" | jq -e \
|
|
80
|
+
'.event_type == "task.complete"
|
|
81
|
+
and .payload.success == true
|
|
82
|
+
and (.payload.duration_ms | type) == "number"
|
|
83
|
+
and .payload.duration_ms >= 0' \
|
|
84
|
+
>/dev/null
|
|
85
|
+
tail -n 1 "$history_file" | onlooker_validate_event
|
|
86
|
+
|
|
87
|
+
run jq -e '.tasks["task-001"]' "$tracker"
|
|
88
|
+
[ "$status" -ne 0 ]
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
@test "task-tracker mirrors task events to global events log" {
|
|
92
|
+
local fixture="${REPO_ROOT}/test/fixtures/hook-inputs/task-created.json"
|
|
93
|
+
: >"$ONLOOKER_EVENTS_LOG"
|
|
94
|
+
|
|
95
|
+
cat "$fixture" | "${REPO_ROOT}/scripts/hooks/task-tracker.sh" >/dev/null 2>&1
|
|
96
|
+
|
|
97
|
+
tail -n 1 "$ONLOOKER_EVENTS_LOG" | jq -e '.event_type == "task.start"' >/dev/null
|
|
98
|
+
tail -n 1 "$ONLOOKER_EVENTS_LOG" | onlooker_validate_event
|
|
99
|
+
}
|
|
@@ -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,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"session_id": "task-session-001",
|
|
3
|
+
"transcript_path": "/tmp/transcript.jsonl",
|
|
4
|
+
"cwd": "/project/repo",
|
|
5
|
+
"permission_mode": "default",
|
|
6
|
+
"hook_event_name": "TaskCompleted",
|
|
7
|
+
"task_id": "task-001",
|
|
8
|
+
"task_subject": "Implement user authentication",
|
|
9
|
+
"task_description": "Add login and signup endpoints",
|
|
10
|
+
"teammate_name": "implementer",
|
|
11
|
+
"team_name": "my-project"
|
|
12
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"session_id": "task-session-001",
|
|
3
|
+
"transcript_path": "/tmp/transcript.jsonl",
|
|
4
|
+
"cwd": "/project/repo",
|
|
5
|
+
"permission_mode": "default",
|
|
6
|
+
"hook_event_name": "TaskCreated",
|
|
7
|
+
"task_id": "task-001",
|
|
8
|
+
"task_subject": "Implement user authentication",
|
|
9
|
+
"task_description": "Add login and signup endpoints",
|
|
10
|
+
"teammate_name": "implementer",
|
|
11
|
+
"team_name": "my-project"
|
|
12
|
+
}
|
|
@@ -5,7 +5,13 @@ import { join } from 'node:path';
|
|
|
5
5
|
import { test } from 'node:test';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
7
|
import { validate } from '@onlooker-community/schema';
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
buildCanonicalEvent,
|
|
10
|
+
mapHookInputToCanonical,
|
|
11
|
+
mapSkillHookInput,
|
|
12
|
+
mapTaskHookInput,
|
|
13
|
+
mapWorktreeHookInput,
|
|
14
|
+
} from '../../scripts/lib/onlooker-event.mjs';
|
|
9
15
|
|
|
10
16
|
const REPO_ROOT = join(fileURLToPath(new URL('../..', import.meta.url)));
|
|
11
17
|
const FIXTURES = join(REPO_ROOT, 'test/fixtures/hook-inputs');
|
|
@@ -72,6 +78,89 @@ test('mapSkillHookInput maps PreToolUse Skill to skill.invoked', () => {
|
|
|
72
78
|
assert.equal(validate(mapped.event).valid, true);
|
|
73
79
|
});
|
|
74
80
|
|
|
81
|
+
test('mapTaskHookInput maps TaskCreated to task.start', () => {
|
|
82
|
+
const hookInput = loadFixture('task-created.json');
|
|
83
|
+
const tmpDir = join(REPO_ROOT, 'test/tmp-schema-events');
|
|
84
|
+
const mapped = mapTaskHookInput(hookInput, {
|
|
85
|
+
onlookerDir: tmpDir,
|
|
86
|
+
plugin: 'onlooker',
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
assert.equal(mapped.valid, true);
|
|
90
|
+
assert.equal(mapped.event.event_type, 'task.start');
|
|
91
|
+
assert.equal(mapped.event.payload.task_summary, 'Implement user authentication');
|
|
92
|
+
assert.equal(validate(mapped.event).valid, true);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('mapTaskHookInput maps TaskCompleted to task.complete', () => {
|
|
96
|
+
const hookInput = loadFixture('task-completed.json');
|
|
97
|
+
const tmpDir = join(REPO_ROOT, 'test/tmp-schema-events');
|
|
98
|
+
const prev = process.env.ONLOOKER_TASK_DURATION_MS;
|
|
99
|
+
process.env.ONLOOKER_TASK_DURATION_MS = '1200';
|
|
100
|
+
const mapped = mapTaskHookInput(hookInput, {
|
|
101
|
+
onlookerDir: tmpDir,
|
|
102
|
+
plugin: 'onlooker',
|
|
103
|
+
});
|
|
104
|
+
if (prev === undefined) delete process.env.ONLOOKER_TASK_DURATION_MS;
|
|
105
|
+
else process.env.ONLOOKER_TASK_DURATION_MS = prev;
|
|
106
|
+
|
|
107
|
+
assert.equal(mapped.valid, true);
|
|
108
|
+
assert.equal(mapped.event.event_type, 'task.complete');
|
|
109
|
+
assert.equal(mapped.event.payload.success, true);
|
|
110
|
+
assert.equal(mapped.event.payload.duration_ms, 1200);
|
|
111
|
+
assert.equal(mapped.event.payload.output_summary, 'Add login and signup endpoints');
|
|
112
|
+
assert.equal(validate(mapped.event).valid, true);
|
|
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
|
+
|
|
152
|
+
test('mapHookInputToCanonical routes TaskCreated through task mapping', () => {
|
|
153
|
+
const hookInput = loadFixture('task-created.json');
|
|
154
|
+
const tmpDir = join(REPO_ROOT, 'test/tmp-schema-events');
|
|
155
|
+
const mapped = mapHookInputToCanonical(hookInput, {
|
|
156
|
+
onlookerDir: tmpDir,
|
|
157
|
+
plugin: 'onlooker',
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
assert.equal(mapped.valid, true);
|
|
161
|
+
assert.equal(mapped.event.event_type, 'task.start');
|
|
162
|
+
});
|
|
163
|
+
|
|
75
164
|
test('buildCanonicalEvent assigns monotonic file-backed sequence', () => {
|
|
76
165
|
const tmpDir = mkdtempSync(join(tmpdir(), 'onlooker-seq-'));
|
|
77
166
|
const a = buildCanonicalEvent({
|