@onlooker-community/ecosystem 0.7.2 → 0.8.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 +18 -57
- package/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +7 -0
- package/hooks/hooks.json +20 -0
- package/package.json +1 -1
- package/release-please-config.json +13 -18
- package/scripts/hooks/task-tracker.sh +61 -0
- package/scripts/lib/onlooker-event.mjs +55 -0
- package/scripts/lib/onlooker-schema.sh +1 -0
- package/scripts/lib/task-tracker.sh +84 -0
- package/test/bats/config.bats +22 -0
- package/test/bats/task-tracker.bats +99 -0
- package/test/fixtures/hook-inputs/task-completed.json +12 -0
- package/test/fixtures/hook-inputs/task-created.json +12 -0
- package/test/node/schema-events.test.mjs +52 -1
|
@@ -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,30 @@ 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
|
-
|
|
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
|
+
release-type: node
|
|
19
|
+
token: ${{ secrets.RELEASE_PLEASE_TOKEN }}
|
|
20
|
+
|
|
21
|
+
- uses: actions/checkout@v4
|
|
22
|
+
if: ${{ steps.release.outputs.release_created }}
|
|
23
|
+
|
|
24
|
+
- uses: actions/setup-node@v4
|
|
25
|
+
if: ${{ steps.release.outputs.release_created }}
|
|
45
26
|
with:
|
|
46
27
|
node-version: '22'
|
|
47
|
-
registry-url: https://registry.npmjs.org
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
28
|
+
registry-url: 'https://registry.npmjs.org'
|
|
29
|
+
|
|
30
|
+
- run: npm ci
|
|
31
|
+
if: ${{ steps.release.outputs.release_created }}
|
|
32
|
+
|
|
33
|
+
- run: npm publish
|
|
34
|
+
if: ${{ steps.release.outputs.release_created }}
|
|
52
35
|
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
|
|
36
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
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
|
|
|
3
10
|
## [0.7.2](https://github.com/onlooker-community/ecosystem/compare/v0.7.1...v0.7.2) (2026-05-22)
|
package/hooks/hooks.json
CHANGED
|
@@ -137,6 +137,26 @@
|
|
|
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
|
+
}
|
|
140
160
|
]
|
|
141
161
|
}
|
|
142
162
|
}
|
package/package.json
CHANGED
|
@@ -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
|
|
@@ -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,56 @@ 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
|
+
|
|
148
200
|
/**
|
|
149
201
|
* Map Claude Code hook input to a canonical event.
|
|
150
202
|
* Returns null when the hook input is not mapped to a schema event type.
|
|
@@ -153,6 +205,9 @@ export function mapHookInputToCanonical(hookInput, options) {
|
|
|
153
205
|
const skillMapped = mapSkillHookInput(hookInput, options);
|
|
154
206
|
if (skillMapped) return skillMapped;
|
|
155
207
|
|
|
208
|
+
const taskMapped = mapTaskHookInput(hookInput, options);
|
|
209
|
+
if (taskMapped) return taskMapped;
|
|
210
|
+
|
|
156
211
|
const { onlookerDir, plugin, runtime = 'claude-code', adapter_id = 'ecosystem.hooks' } = options;
|
|
157
212
|
|
|
158
213
|
const toolName = hookInput?.tool_name;
|
|
@@ -26,6 +26,7 @@ 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:-}" \
|
|
29
30
|
node "$_ONLOOKER_EVENT_JS" emit-from-hook 2>/dev/null
|
|
30
31
|
}
|
|
31
32
|
|
|
@@ -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
|
+
}
|
package/test/bats/config.bats
CHANGED
|
@@ -175,6 +175,28 @@ 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
|
+
|
|
178
200
|
@test "plugin.json is valid JSON" {
|
|
179
201
|
run jq -e '.name and .version' "${REPO_ROOT}/.claude-plugin/plugin.json"
|
|
180
202
|
[ "$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,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,12 @@ 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
|
+
} from '../../scripts/lib/onlooker-event.mjs';
|
|
9
14
|
|
|
10
15
|
const REPO_ROOT = join(fileURLToPath(new URL('../..', import.meta.url)));
|
|
11
16
|
const FIXTURES = join(REPO_ROOT, 'test/fixtures/hook-inputs');
|
|
@@ -72,6 +77,52 @@ test('mapSkillHookInput maps PreToolUse Skill to skill.invoked', () => {
|
|
|
72
77
|
assert.equal(validate(mapped.event).valid, true);
|
|
73
78
|
});
|
|
74
79
|
|
|
80
|
+
test('mapTaskHookInput maps TaskCreated to task.start', () => {
|
|
81
|
+
const hookInput = loadFixture('task-created.json');
|
|
82
|
+
const tmpDir = join(REPO_ROOT, 'test/tmp-schema-events');
|
|
83
|
+
const mapped = mapTaskHookInput(hookInput, {
|
|
84
|
+
onlookerDir: tmpDir,
|
|
85
|
+
plugin: 'onlooker',
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
assert.equal(mapped.valid, true);
|
|
89
|
+
assert.equal(mapped.event.event_type, 'task.start');
|
|
90
|
+
assert.equal(mapped.event.payload.task_summary, 'Implement user authentication');
|
|
91
|
+
assert.equal(validate(mapped.event).valid, true);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('mapTaskHookInput maps TaskCompleted to task.complete', () => {
|
|
95
|
+
const hookInput = loadFixture('task-completed.json');
|
|
96
|
+
const tmpDir = join(REPO_ROOT, 'test/tmp-schema-events');
|
|
97
|
+
const prev = process.env.ONLOOKER_TASK_DURATION_MS;
|
|
98
|
+
process.env.ONLOOKER_TASK_DURATION_MS = '1200';
|
|
99
|
+
const mapped = mapTaskHookInput(hookInput, {
|
|
100
|
+
onlookerDir: tmpDir,
|
|
101
|
+
plugin: 'onlooker',
|
|
102
|
+
});
|
|
103
|
+
if (prev === undefined) delete process.env.ONLOOKER_TASK_DURATION_MS;
|
|
104
|
+
else process.env.ONLOOKER_TASK_DURATION_MS = prev;
|
|
105
|
+
|
|
106
|
+
assert.equal(mapped.valid, true);
|
|
107
|
+
assert.equal(mapped.event.event_type, 'task.complete');
|
|
108
|
+
assert.equal(mapped.event.payload.success, true);
|
|
109
|
+
assert.equal(mapped.event.payload.duration_ms, 1200);
|
|
110
|
+
assert.equal(mapped.event.payload.output_summary, 'Add login and signup endpoints');
|
|
111
|
+
assert.equal(validate(mapped.event).valid, true);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('mapHookInputToCanonical routes TaskCreated through task mapping', () => {
|
|
115
|
+
const hookInput = loadFixture('task-created.json');
|
|
116
|
+
const tmpDir = join(REPO_ROOT, 'test/tmp-schema-events');
|
|
117
|
+
const mapped = mapHookInputToCanonical(hookInput, {
|
|
118
|
+
onlookerDir: tmpDir,
|
|
119
|
+
plugin: 'onlooker',
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
assert.equal(mapped.valid, true);
|
|
123
|
+
assert.equal(mapped.event.event_type, 'task.start');
|
|
124
|
+
});
|
|
125
|
+
|
|
75
126
|
test('buildCanonicalEvent assigns monotonic file-backed sequence', () => {
|
|
76
127
|
const tmpDir = mkdtempSync(join(tmpdir(), 'onlooker-seq-'));
|
|
77
128
|
const a = buildCanonicalEvent({
|