@onlooker-community/ecosystem 0.7.1 → 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.
@@ -12,7 +12,7 @@
12
12
  "name": "ecosystem",
13
13
  "source": "./",
14
14
  "description": "Fill this out",
15
- "version": "0.7.1",
15
+ "version": "0.8.0",
16
16
  "author": {
17
17
  "name": "Onlooker Community"
18
18
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ecosystem",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "description": "TODO fill this out",
5
5
  "author": {
6
6
  "name": "Onlooker Community",
@@ -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,63 +7,30 @@ on:
11
7
  permissions:
12
8
  contents: write
13
9
  pull-requests: write
14
- # Required for npm publish --provenance (OIDC attestation)
15
- id-token: write
16
10
 
17
11
  jobs:
18
12
  release-please:
19
- name: Release Please
20
13
  runs-on: ubuntu-latest
21
14
  steps:
22
- - name: Create Release PR
15
+ - uses: googleapis/release-please-action@v4
23
16
  id: release
24
- uses: googleapis/release-please-action@16a9c90856f42705d54a6fda1823352bdc62cf38 # v4.4.0
25
17
  with:
26
- # Do not set release-type here — it overrides release-please-config.json
27
- # and skips extra-files (plugin.json, marketplace.json). Release type
28
- # is defined per-package in release-please-config.json.
29
- config-file: release-please-config.json
30
- manifest-file: .release-please-manifest.json
31
- # Use a PAT instead of GITHUB_TOKEN so the release PR triggers
32
- # downstream workflows (CI, etc.). Events caused by GITHUB_TOKEN
33
- # deliberately do not fan out to other workflows; that policy
34
- # leaves release PRs unchecked. Set RELEASE_PLEASE_PAT to a
35
- # fine-grained token with Contents:write + Pull requests:write
36
- # scoped to this repo.
37
- token: ${{ secrets.RELEASE_PLEASE_PAT }}
38
-
39
- - uses: actions/checkout@v6
40
- if: ${{ steps.release.outputs.releases_created == 'true'}}
41
-
42
- - uses: actions/setup-node@v6
43
- 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 }}
44
26
  with:
45
27
  node-version: '22'
46
- cache: npm
47
-
48
- - name: Publish tools packages to npm
49
- if: ${{ steps.release.outputs.releases_created == 'true' }}
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 }}
50
35
  env:
51
- NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
52
- PATHS_RELEASED: ${{ steps.release.outputs.paths_released }}
53
- run: |
54
- set -euo pipefail
55
- if [[ -z "${NPM_TOKEN}" ]]; then
56
- echo "NPM_TOKEN secret is required to publish to npm." >&2
57
- exit 1
58
- fi
59
- printf '%s\n' "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" >> "${HOME}/.npmrc"
60
- npm ci
61
- # paths_released is JSON (e.g. ["."], not bare paths)
62
- mapfile -t release_paths < <(echo "${PATHS_RELEASED}" | jq -r '.[]')
63
- if [[ "${#release_paths[@]}" -eq 0 ]]; then
64
- echo "No paths in paths_released; skipping npm publish." >&2
65
- exit 0
66
- fi
67
- for path in "${release_paths[@]}"; do
68
- if [[ "$path" == "." ]]; then
69
- npm publish --access public --provenance
70
- else
71
- (cd "$path" && npm publish --access public --provenance)
72
- fi
73
- done
36
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "0.7.1"
2
+ ".": "0.8.0"
3
3
  }
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
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.7.2](https://github.com/onlooker-community/ecosystem/compare/v0.7.1...v0.7.2) (2026-05-22)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * **ci:** grant id-token write for npm provenance on publish ([c78c9f0](https://github.com/onlooker-community/ecosystem/commit/c78c9f054c1d48ca8a83d0d26b76ce991fffe51b))
16
+
3
17
  ## [0.7.1](https://github.com/onlooker-community/ecosystem/compare/v0.7.0...v0.7.1) (2026-05-22)
4
18
 
5
19
 
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@onlooker-community/ecosystem",
3
- "version": "0.7.1",
3
+ "version": "0.8.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",
@@ -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
- "include-component-in-tag": false,
9
- "include-v-in-tag": true,
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..version"
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
+ }
@@ -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 { buildCanonicalEvent, mapHookInputToCanonical, mapSkillHookInput } from '../../scripts/lib/onlooker-event.mjs';
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({