@onlooker-community/ecosystem 0.28.1 → 0.29.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 +13 -0
- package/.claude-plugin/plugin.json +1 -1
- package/.release-please-manifest.json +2 -2
- package/CHANGELOG.md +7 -0
- package/CLAUDE.md +2 -0
- package/docs/plugin-catalog.md +125 -0
- package/package.json +3 -3
- package/plugins/compass/.claude-plugin/plugin.json +1 -1
- package/plugins/compass/CHANGELOG.md +7 -0
- package/plugins/compass/README.md +1 -3
- package/plugins/compass/config.json +1 -2
- package/plugins/compass/docs/design.md +1 -2
- package/plugins/compass/scripts/hooks/compass-bash-gate.sh +8 -1
- package/plugins/compass/scripts/hooks/compass-pre-tool-use.sh +8 -1
- package/plugins/compass/scripts/hooks/compass-record-write.sh +5 -0
- package/plugins/compass/scripts/hooks/compass-session-start.sh +0 -8
- package/plugins/compass/scripts/lib/compass-evaluator.sh +58 -98
- package/plugins/compass/scripts/lib/compass-gate.sh +15 -18
- package/plugins/compass/scripts/lib/compass-sanitizer.sh +4 -4
- package/plugins/compass/scripts/lib/compass-transcript.sh +79 -112
- package/plugins/inspector/.claude-plugin/plugin.json +14 -0
- package/plugins/inspector/README.md +155 -0
- package/plugins/inspector/config.json +25 -0
- package/plugins/inspector/docs/design.md +286 -0
- package/plugins/inspector/hooks/hooks.json +33 -0
- package/plugins/inspector/scripts/hooks/inspector-post-write.sh +124 -0
- package/plugins/inspector/scripts/lib/inspector-config.sh +108 -0
- package/plugins/inspector/scripts/lib/inspector-events.sh +82 -0
- package/plugins/inspector/scripts/lib/inspector-project-key.sh +55 -0
- package/plugins/inspector/scripts/lib/inspector-run.sh +305 -0
- package/plugins/inspector/scripts/lib/inspector-ulid.sh +45 -0
- package/test/bats/archivist-project-key.bats +79 -0
- package/test/bats/archivist-storage.bats +79 -0
- package/test/bats/compact-tracker.bats +125 -0
- package/test/bats/compass-config.bats +65 -0
- package/test/bats/compass-gate.bats +129 -0
- package/test/bats/compass-sanitizer.bats +69 -0
- package/test/bats/compass-symbolic-skip.bats +88 -0
- package/test/bats/compass-transcript.bats +80 -0
- package/test/bats/inspector-config.bats +118 -0
- package/test/bats/inspector-events.bats +156 -0
- package/test/bats/inspector-post-write-hook.bats +164 -0
- package/test/bats/inspector-project-key.bats +68 -0
- package/test/bats/inspector-ulid.bats +34 -0
- package/test/bats/onlooker-schema.bats +111 -0
- package/test/bats/prompt-rules.bats +98 -0
- package/test/bats/session-tracker.bats +260 -0
- package/test/bats/skill-usage-tracker.bats +63 -0
- package/test/bats/task-tracker.bats +102 -0
- package/test/bats/turn-tracker.bats +180 -0
- package/test/bats/validate-path.bats +125 -0
- package/test/bats/worktree-tracker.bats +167 -0
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# inspector-run.sh — execute the configured checks for a single touched file.
|
|
3
|
+
#
|
|
4
|
+
# Reads: $INSPECTOR_FILE, $INSPECTOR_FILE_RELATIVE, $INSPECTOR_REPO_ROOT,
|
|
5
|
+
# $INSPECTOR_PROJECT_KEY, $INSPECTOR_TOOL_NAME
|
|
6
|
+
# Emits: inspector.check.passed / .failed / .skipped (per check), then
|
|
7
|
+
# inspector.run.completed (once).
|
|
8
|
+
# Writes: a compact per-file summary to stdout, intended to be shown to the
|
|
9
|
+
# agent as PostToolUse additional context.
|
|
10
|
+
|
|
11
|
+
set -uo pipefail
|
|
12
|
+
|
|
13
|
+
_inspector_now_ms() {
|
|
14
|
+
if date +%s%3N &>/dev/null && [[ "$(date +%s%3N)" =~ ^[0-9]{13}$ ]]; then
|
|
15
|
+
date +%s%3N
|
|
16
|
+
else
|
|
17
|
+
python3 -c 'import time; print(int(time.time() * 1000))'
|
|
18
|
+
fi
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
# Substitute ${file}, ${file_relative}, ${repo_root} in each argv element.
|
|
22
|
+
_inspector_expand_argv() {
|
|
23
|
+
local raw_argv_json="$1"
|
|
24
|
+
jq -c \
|
|
25
|
+
--arg file "$INSPECTOR_FILE" \
|
|
26
|
+
--arg rel "$INSPECTOR_FILE_RELATIVE" \
|
|
27
|
+
--arg root "$INSPECTOR_REPO_ROOT" \
|
|
28
|
+
'map(
|
|
29
|
+
gsub("\\$\\{file\\}"; $file)
|
|
30
|
+
| gsub("\\$\\{file_relative\\}"; $rel)
|
|
31
|
+
| gsub("\\$\\{repo_root\\}"; $root)
|
|
32
|
+
)' <<<"$raw_argv_json"
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
# Run a single check. Stdout: the captured combined output, truncated to
|
|
36
|
+
# output_excerpt_max_bytes. Exit code: the underlying command's exit, or 124 on
|
|
37
|
+
# timeout, or 127 when the command is not on PATH.
|
|
38
|
+
_inspector_invoke_check() {
|
|
39
|
+
local expanded_json="$1"
|
|
40
|
+
local timeout_s="$2"
|
|
41
|
+
local max_bytes="$3"
|
|
42
|
+
|
|
43
|
+
# Build argv array from JSON for safe execution.
|
|
44
|
+
# bash 3.2 (macOS default) has no `mapfile`; collect with a while-read loop.
|
|
45
|
+
local expanded_argv=()
|
|
46
|
+
local _line
|
|
47
|
+
while IFS= read -r _line; do
|
|
48
|
+
expanded_argv+=("$_line")
|
|
49
|
+
done < <(jq -r '.[]' <<<"$expanded_json")
|
|
50
|
+
local cmd="${expanded_argv[0]:-}"
|
|
51
|
+
if [[ -z "$cmd" ]]; then
|
|
52
|
+
printf 'inspector: empty argv\n'
|
|
53
|
+
return 127
|
|
54
|
+
fi
|
|
55
|
+
if ! command -v "$cmd" >/dev/null 2>&1; then
|
|
56
|
+
return 127
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
local output_file rc=0
|
|
60
|
+
output_file=$(mktemp -t inspector-out.XXXXXX 2>/dev/null) \
|
|
61
|
+
|| output_file="/tmp/inspector-out.$$"
|
|
62
|
+
|
|
63
|
+
if command -v timeout >/dev/null 2>&1; then
|
|
64
|
+
timeout "${timeout_s}s" "${expanded_argv[@]}" >"$output_file" 2>&1
|
|
65
|
+
rc=$?
|
|
66
|
+
else
|
|
67
|
+
"${expanded_argv[@]}" >"$output_file" 2>&1
|
|
68
|
+
rc=$?
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
# Truncate output for the event payload and the agent-facing line.
|
|
72
|
+
local bytes=0
|
|
73
|
+
bytes=$(wc -c <"$output_file" 2>/dev/null || printf '0')
|
|
74
|
+
if (( bytes > max_bytes )); then
|
|
75
|
+
head -c "$max_bytes" "$output_file"
|
|
76
|
+
printf '\n…[truncated]\n'
|
|
77
|
+
else
|
|
78
|
+
cat "$output_file"
|
|
79
|
+
fi
|
|
80
|
+
rm -f "$output_file"
|
|
81
|
+
return "$rc"
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
# Count issues from output — best-effort. One non-empty, non-whitespace line per
|
|
85
|
+
# issue, ignoring trivial header/footer markers. Returns the literal string
|
|
86
|
+
# "null" when the output is empty or only whitespace.
|
|
87
|
+
_inspector_count_issues() {
|
|
88
|
+
local text="$1"
|
|
89
|
+
[[ -z "$text" ]] && { printf 'null'; return; }
|
|
90
|
+
local count
|
|
91
|
+
count=$(printf '%s' "$text" | grep -cE '^[^[:space:]]' || true)
|
|
92
|
+
if [[ -z "$count" || "$count" == "0" ]]; then
|
|
93
|
+
printf 'null'
|
|
94
|
+
else
|
|
95
|
+
printf '%s' "$count"
|
|
96
|
+
fi
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
# Public entrypoint. Iterates the configured checks for the file's extension.
|
|
100
|
+
inspector_run() {
|
|
101
|
+
local checks_json="${1:-[]}"
|
|
102
|
+
local timeout_per_check
|
|
103
|
+
timeout_per_check=$(inspector_config_timeout_per_check)
|
|
104
|
+
local total_timeout
|
|
105
|
+
total_timeout=$(inspector_config_total_timeout)
|
|
106
|
+
local max_bytes
|
|
107
|
+
max_bytes=$(inspector_config_output_excerpt_max_bytes)
|
|
108
|
+
local show_clean=0
|
|
109
|
+
inspector_config_show_clean_runs && show_clean=1
|
|
110
|
+
|
|
111
|
+
local check_count
|
|
112
|
+
check_count=$(jq 'length' <<<"$checks_json")
|
|
113
|
+
|
|
114
|
+
local run_start
|
|
115
|
+
run_start=$(_inspector_now_ms)
|
|
116
|
+
local passed=0 failed=0 skipped=0 ran=0
|
|
117
|
+
|
|
118
|
+
# Buffer agent-facing output so we only print the file header when at least
|
|
119
|
+
# one issue (or, when show_clean is set, one check) is worth reporting.
|
|
120
|
+
local agent_lines=()
|
|
121
|
+
local issues_seen=0
|
|
122
|
+
|
|
123
|
+
local i
|
|
124
|
+
for (( i = 0; i < check_count; i++ )); do
|
|
125
|
+
# Budget check before each run.
|
|
126
|
+
local now_ms
|
|
127
|
+
now_ms=$(_inspector_now_ms)
|
|
128
|
+
if (( (now_ms - run_start) >= (total_timeout * 1000) )); then
|
|
129
|
+
local rem
|
|
130
|
+
rem=$(jq -c --argjson i "$i" '.[$i:]' <<<"$checks_json")
|
|
131
|
+
local rem_count
|
|
132
|
+
rem_count=$(jq 'length' <<<"$rem")
|
|
133
|
+
local j
|
|
134
|
+
for (( j = 0; j < rem_count; j++ )); do
|
|
135
|
+
local r_name r_kind
|
|
136
|
+
r_name=$(jq -r --argjson j "$j" '.[$j].name // "check"' <<<"$rem")
|
|
137
|
+
r_kind=$(jq -r --argjson j "$j" '.[$j].kind // "lint"' <<<"$rem")
|
|
138
|
+
_inspector_emit_skipped "$r_name" "$r_kind" "total_budget_exhausted"
|
|
139
|
+
(( skipped++ ))
|
|
140
|
+
done
|
|
141
|
+
break
|
|
142
|
+
fi
|
|
143
|
+
|
|
144
|
+
local check_name check_kind argv_raw
|
|
145
|
+
check_name=$(jq -r --argjson i "$i" '.[$i].name' <<<"$checks_json")
|
|
146
|
+
check_kind=$(jq -r --argjson i "$i" '.[$i].kind' <<<"$checks_json")
|
|
147
|
+
argv_raw=$(jq -c --argjson i "$i" '.[$i].argv' <<<"$checks_json")
|
|
148
|
+
local argv_expanded
|
|
149
|
+
argv_expanded=$(_inspector_expand_argv "$argv_raw")
|
|
150
|
+
|
|
151
|
+
local check_start
|
|
152
|
+
check_start=$(_inspector_now_ms)
|
|
153
|
+
local output rc=0
|
|
154
|
+
output=$(_inspector_invoke_check "$argv_expanded" "$timeout_per_check" "$max_bytes")
|
|
155
|
+
rc=$?
|
|
156
|
+
local check_end
|
|
157
|
+
check_end=$(_inspector_now_ms)
|
|
158
|
+
local dur=$(( check_end - check_start ))
|
|
159
|
+
|
|
160
|
+
if (( rc == 127 )); then
|
|
161
|
+
# Tool not on PATH.
|
|
162
|
+
_inspector_emit_skipped "$check_name" "$check_kind" "tool_missing"
|
|
163
|
+
(( skipped++ ))
|
|
164
|
+
continue
|
|
165
|
+
fi
|
|
166
|
+
|
|
167
|
+
if (( rc == 124 )); then
|
|
168
|
+
# Timed out — emit skipped + a single agent-facing line.
|
|
169
|
+
_inspector_emit_skipped "$check_name" "$check_kind" "timeout"
|
|
170
|
+
agent_lines+=(" · $check_name timed out after ${timeout_per_check}s")
|
|
171
|
+
(( skipped++ ))
|
|
172
|
+
(( issues_seen++ ))
|
|
173
|
+
continue
|
|
174
|
+
fi
|
|
175
|
+
|
|
176
|
+
(( ran++ ))
|
|
177
|
+
if (( rc == 0 )); then
|
|
178
|
+
(( passed++ ))
|
|
179
|
+
_inspector_emit_passed "$check_name" "$check_kind" "$argv_expanded" "$dur"
|
|
180
|
+
if (( show_clean )); then
|
|
181
|
+
agent_lines+=(" ✓ $check_name (${dur}ms)")
|
|
182
|
+
fi
|
|
183
|
+
else
|
|
184
|
+
(( failed++ ))
|
|
185
|
+
local issue_count
|
|
186
|
+
issue_count=$(_inspector_count_issues "$output")
|
|
187
|
+
_inspector_emit_failed "$check_name" "$check_kind" "$argv_expanded" "$dur" "$rc" "$issue_count" "$output"
|
|
188
|
+
local issues_label="${issue_count}"
|
|
189
|
+
if [[ "$issues_label" == "null" ]]; then
|
|
190
|
+
issues_label="issues"
|
|
191
|
+
else
|
|
192
|
+
issues_label="${issues_label} issue(s)"
|
|
193
|
+
fi
|
|
194
|
+
agent_lines+=(" ✗ $check_name (${issues_label}, exit ${rc})")
|
|
195
|
+
# Append up to 6 issue lines for at-a-glance context.
|
|
196
|
+
local snippet
|
|
197
|
+
snippet=$(printf '%s\n' "$output" | grep -E '^[^[:space:]]' | head -n 6 || true)
|
|
198
|
+
while IFS= read -r line; do
|
|
199
|
+
[[ -z "$line" ]] && continue
|
|
200
|
+
agent_lines+=(" $line")
|
|
201
|
+
done <<<"$snippet"
|
|
202
|
+
(( issues_seen++ ))
|
|
203
|
+
fi
|
|
204
|
+
done
|
|
205
|
+
|
|
206
|
+
local run_end
|
|
207
|
+
run_end=$(_inspector_now_ms)
|
|
208
|
+
local run_dur=$(( run_end - run_start ))
|
|
209
|
+
|
|
210
|
+
_inspector_emit_run_completed "$ran" "$passed" "$failed" "$skipped" "$run_dur"
|
|
211
|
+
|
|
212
|
+
if (( issues_seen > 0 )) || (( show_clean && (passed + failed) > 0 )); then
|
|
213
|
+
printf 'inspector: %s\n' "$INSPECTOR_FILE_RELATIVE"
|
|
214
|
+
printf '%s\n' "${agent_lines[@]}"
|
|
215
|
+
fi
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
_inspector_emit_passed() {
|
|
219
|
+
local name="$1" kind="$2" argv_json="$3" dur="$4"
|
|
220
|
+
local payload
|
|
221
|
+
payload=$(jq -n \
|
|
222
|
+
--arg file "$INSPECTOR_FILE" \
|
|
223
|
+
--arg rel "$INSPECTOR_FILE_RELATIVE" \
|
|
224
|
+
--arg tool "$INSPECTOR_TOOL_NAME" \
|
|
225
|
+
--arg name "$name" \
|
|
226
|
+
--arg kind "$kind" \
|
|
227
|
+
--arg pk "$INSPECTOR_PROJECT_KEY" \
|
|
228
|
+
--argjson argv "$argv_json" \
|
|
229
|
+
--argjson dur "$dur" \
|
|
230
|
+
'{file_path:$file,file_path_relative:$rel,tool_name:$tool,check_name:$name,check_kind:$kind,argv:$argv,duration_ms:$dur,project_key:$pk}')
|
|
231
|
+
inspector_emit_event "inspector.check.passed" "$payload" || true
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
_inspector_emit_failed() {
|
|
235
|
+
local name="$1" kind="$2" argv_json="$3" dur="$4" rc="$5" issues="$6" output="$7"
|
|
236
|
+
local issues_arg
|
|
237
|
+
if [[ "$issues" == "null" ]]; then
|
|
238
|
+
issues_arg="null"
|
|
239
|
+
else
|
|
240
|
+
issues_arg="$issues"
|
|
241
|
+
fi
|
|
242
|
+
local truncated="false"
|
|
243
|
+
[[ "$output" == *"…[truncated]"* ]] && truncated="true"
|
|
244
|
+
local payload
|
|
245
|
+
payload=$(jq -n \
|
|
246
|
+
--arg file "$INSPECTOR_FILE" \
|
|
247
|
+
--arg rel "$INSPECTOR_FILE_RELATIVE" \
|
|
248
|
+
--arg tool "$INSPECTOR_TOOL_NAME" \
|
|
249
|
+
--arg name "$name" \
|
|
250
|
+
--arg kind "$kind" \
|
|
251
|
+
--arg pk "$INSPECTOR_PROJECT_KEY" \
|
|
252
|
+
--arg output "$output" \
|
|
253
|
+
--argjson argv "$argv_json" \
|
|
254
|
+
--argjson dur "$dur" \
|
|
255
|
+
--argjson rc "$rc" \
|
|
256
|
+
--argjson issues "$issues_arg" \
|
|
257
|
+
--argjson truncated "$truncated" \
|
|
258
|
+
'{file_path:$file,file_path_relative:$rel,tool_name:$tool,check_name:$name,check_kind:$kind,argv:$argv,duration_ms:$dur,exit_code:$rc,issue_count:$issues,output_excerpt:$output,output_truncated:$truncated,project_key:$pk}')
|
|
259
|
+
inspector_emit_event "inspector.check.failed" "$payload" || true
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
_inspector_emit_skipped() {
|
|
263
|
+
local name="$1" kind="$2" reason="$3"
|
|
264
|
+
local payload
|
|
265
|
+
payload=$(jq -n \
|
|
266
|
+
--arg file "$INSPECTOR_FILE" \
|
|
267
|
+
--arg rel "$INSPECTOR_FILE_RELATIVE" \
|
|
268
|
+
--arg tool "$INSPECTOR_TOOL_NAME" \
|
|
269
|
+
--arg name "$name" \
|
|
270
|
+
--arg kind "$kind" \
|
|
271
|
+
--arg reason "$reason" \
|
|
272
|
+
--arg pk "$INSPECTOR_PROJECT_KEY" \
|
|
273
|
+
'{file_path:$file,file_path_relative:$rel,tool_name:$tool,check_name:$name,check_kind:$kind,reason:$reason,project_key:$pk}')
|
|
274
|
+
inspector_emit_event "inspector.check.skipped" "$payload" || true
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
inspector_emit_whole_file_skipped() {
|
|
278
|
+
local reason="$1"
|
|
279
|
+
local payload
|
|
280
|
+
payload=$(jq -n \
|
|
281
|
+
--arg file "${INSPECTOR_FILE:-}" \
|
|
282
|
+
--arg rel "${INSPECTOR_FILE_RELATIVE:-}" \
|
|
283
|
+
--arg tool "${INSPECTOR_TOOL_NAME:-}" \
|
|
284
|
+
--arg reason "$reason" \
|
|
285
|
+
--arg pk "${INSPECTOR_PROJECT_KEY:-}" \
|
|
286
|
+
'{file_path:$file,file_path_relative:$rel,tool_name:$tool,reason:$reason,project_key:$pk}')
|
|
287
|
+
inspector_emit_event "inspector.check.skipped" "$payload" || true
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
_inspector_emit_run_completed() {
|
|
291
|
+
local ran="$1" passed="$2" failed="$3" skipped="$4" dur="$5"
|
|
292
|
+
local payload
|
|
293
|
+
payload=$(jq -n \
|
|
294
|
+
--arg file "$INSPECTOR_FILE" \
|
|
295
|
+
--arg rel "$INSPECTOR_FILE_RELATIVE" \
|
|
296
|
+
--arg tool "$INSPECTOR_TOOL_NAME" \
|
|
297
|
+
--arg pk "$INSPECTOR_PROJECT_KEY" \
|
|
298
|
+
--argjson ran "$ran" \
|
|
299
|
+
--argjson passed "$passed" \
|
|
300
|
+
--argjson failed "$failed" \
|
|
301
|
+
--argjson skipped "$skipped" \
|
|
302
|
+
--argjson dur "$dur" \
|
|
303
|
+
'{file_path:$file,file_path_relative:$rel,tool_name:$tool,checks_run:$ran,checks_passed:$passed,checks_failed:$failed,checks_skipped:$skipped,duration_ms:$dur,project_key:$pk}')
|
|
304
|
+
inspector_emit_event "inspector.run.completed" "$payload" || true
|
|
305
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# inspector-ulid.sh — ULID generation for Inspector.
|
|
3
|
+
#
|
|
4
|
+
# 10-char Crockford Base32 timestamp + 16-char random component = 26 chars.
|
|
5
|
+
#
|
|
6
|
+
# Usage:
|
|
7
|
+
# id=$(inspector_ulid)
|
|
8
|
+
|
|
9
|
+
_INSPECTOR_ULID_ALPHABET="0123456789ABCDEFGHJKMNPQRSTVWXYZ"
|
|
10
|
+
|
|
11
|
+
_inspector_ulid_encode() {
|
|
12
|
+
local n="${1:-0}"
|
|
13
|
+
local len="${2:-10}"
|
|
14
|
+
local result=""
|
|
15
|
+
local i
|
|
16
|
+
for (( i = 0; i < len; i++ )); do
|
|
17
|
+
result="${_INSPECTOR_ULID_ALPHABET:$(( n & 31 )):1}${result}"
|
|
18
|
+
n=$(( n >> 5 ))
|
|
19
|
+
done
|
|
20
|
+
printf '%s' "$result"
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
inspector_ulid() {
|
|
24
|
+
local ts_ms
|
|
25
|
+
if date +%s%3N &>/dev/null && [[ "$(date +%s%3N)" =~ ^[0-9]{13}$ ]]; then
|
|
26
|
+
ts_ms=$(date +%s%3N)
|
|
27
|
+
else
|
|
28
|
+
ts_ms=$(python3 -c 'import time; print(int(time.time() * 1000))')
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
local ts_encoded
|
|
32
|
+
ts_encoded=$(_inspector_ulid_encode "$ts_ms" 10)
|
|
33
|
+
|
|
34
|
+
local rand_hex
|
|
35
|
+
rand_hex=$(openssl rand -hex 10 2>/dev/null) \
|
|
36
|
+
|| rand_hex=$(printf '%020x' $(( (RANDOM * RANDOM & 0xFFFFF) * 0x100000 + (RANDOM * RANDOM & 0xFFFFF) )))
|
|
37
|
+
|
|
38
|
+
local rand_hi rand_lo
|
|
39
|
+
rand_hi=$(( 16#${rand_hex:0:10} ))
|
|
40
|
+
rand_lo=$(( 16#${rand_hex:10:10} ))
|
|
41
|
+
local rand_encoded
|
|
42
|
+
rand_encoded="$(_inspector_ulid_encode "$rand_hi" 8)$(_inspector_ulid_encode "$rand_lo" 8)"
|
|
43
|
+
|
|
44
|
+
printf '%s%s' "$ts_encoded" "$rand_encoded"
|
|
45
|
+
}
|
|
@@ -73,3 +73,82 @@ setup() {
|
|
|
73
73
|
[ -n "$kb" ]
|
|
74
74
|
[ "$ka" != "$kb" ]
|
|
75
75
|
}
|
|
76
|
+
|
|
77
|
+
@test "archivist_project_remote_url prints origin url when set" {
|
|
78
|
+
local d="${BATS_TEST_TMPDIR}/remote-set"
|
|
79
|
+
mkdir -p "$d"
|
|
80
|
+
git -C "$d" init -q
|
|
81
|
+
git -C "$d" remote add origin https://example.com/x.git
|
|
82
|
+
|
|
83
|
+
run archivist_project_remote_url "$d"
|
|
84
|
+
[ "$status" -eq 0 ]
|
|
85
|
+
[ "$output" = "https://example.com/x.git" ]
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
@test "archivist_project_remote_url returns empty when repo has no remote" {
|
|
89
|
+
local d="${BATS_TEST_TMPDIR}/remote-none"
|
|
90
|
+
mkdir -p "$d"
|
|
91
|
+
git -C "$d" init -q
|
|
92
|
+
|
|
93
|
+
run archivist_project_remote_url "$d"
|
|
94
|
+
[ "$status" -eq 0 ]
|
|
95
|
+
[ -z "$output" ]
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
@test "archivist_project_remote_url returns empty for a non-git directory" {
|
|
99
|
+
local d="${BATS_TEST_TMPDIR}/remote-non-git"
|
|
100
|
+
mkdir -p "$d"
|
|
101
|
+
|
|
102
|
+
run archivist_project_remote_url "$d"
|
|
103
|
+
[ "$status" -eq 0 ]
|
|
104
|
+
[ -z "$output" ]
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
@test "archivist_project_remote_url returns empty for a nonexistent path" {
|
|
108
|
+
run archivist_project_remote_url "${BATS_TEST_TMPDIR}/does-not-exist"
|
|
109
|
+
[ "$status" -eq 0 ]
|
|
110
|
+
[ -z "$output" ]
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
@test "archivist_project_repo_root prints the repo toplevel realpath" {
|
|
114
|
+
local d="${BATS_TEST_TMPDIR}/root-repo"
|
|
115
|
+
mkdir -p "$d"
|
|
116
|
+
git -C "$d" init -q
|
|
117
|
+
|
|
118
|
+
# Physical path: matches the function's pwd -P resolution on platforms
|
|
119
|
+
# (e.g. macOS) where the temp dir is reached through a symlink.
|
|
120
|
+
local expected
|
|
121
|
+
expected="$(cd "$d" && pwd -P)"
|
|
122
|
+
|
|
123
|
+
run archivist_project_repo_root "$d"
|
|
124
|
+
[ "$status" -eq 0 ]
|
|
125
|
+
[ "$output" = "$expected" ]
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
@test "archivist_project_repo_root resolves toplevel from a subdirectory" {
|
|
129
|
+
local d="${BATS_TEST_TMPDIR}/root-repo-sub"
|
|
130
|
+
mkdir -p "$d/nested/deep"
|
|
131
|
+
git -C "$d" init -q
|
|
132
|
+
|
|
133
|
+
local expected
|
|
134
|
+
expected="$(cd "$d" && pwd -P)"
|
|
135
|
+
|
|
136
|
+
run archivist_project_repo_root "$d/nested/deep"
|
|
137
|
+
[ "$status" -eq 0 ]
|
|
138
|
+
[ "$output" = "$expected" ]
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
@test "archivist_project_repo_root returns empty for a non-git directory" {
|
|
142
|
+
local d="${BATS_TEST_TMPDIR}/root-non-git"
|
|
143
|
+
mkdir -p "$d"
|
|
144
|
+
|
|
145
|
+
run archivist_project_repo_root "$d"
|
|
146
|
+
[ "$status" -eq 0 ]
|
|
147
|
+
[ -z "$output" ]
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
@test "archivist_project_repo_root returns empty for a nonexistent path" {
|
|
151
|
+
run archivist_project_repo_root "${BATS_TEST_TMPDIR}/root-missing"
|
|
152
|
+
[ "$status" -eq 0 ]
|
|
153
|
+
[ -z "$output" ]
|
|
154
|
+
}
|
|
@@ -117,3 +117,82 @@ setup() {
|
|
|
117
117
|
first_id=$(printf '%s' "$ranked" | jq -r '.[0].id')
|
|
118
118
|
[ "$first_id" = "$newer_id" ]
|
|
119
119
|
}
|
|
120
|
+
|
|
121
|
+
@test "storage_root prints the archivist dir under ONLOOKER_DIR" {
|
|
122
|
+
run archivist_storage_root
|
|
123
|
+
[ "$status" -eq 0 ]
|
|
124
|
+
[ "$output" = "${ONLOOKER_DIR}/archivist" ]
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
@test "project_dir prints root joined with the key" {
|
|
128
|
+
local key="abc123def456"
|
|
129
|
+
run archivist_project_dir "$key"
|
|
130
|
+
[ "$status" -eq 0 ]
|
|
131
|
+
[ "$output" = "${ONLOOKER_DIR}/archivist/${key}" ]
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
@test "kind_dir prints the per-kind subdir under the project dir" {
|
|
135
|
+
local key="abc123def456"
|
|
136
|
+
run archivist_kind_dir "$key" "decisions"
|
|
137
|
+
[ "$status" -eq 0 ]
|
|
138
|
+
[ "$output" = "${ONLOOKER_DIR}/archivist/${key}/decisions" ]
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
@test "kind_dir honors an arbitrary kind name" {
|
|
142
|
+
local key="abc123def456"
|
|
143
|
+
run archivist_kind_dir "$key" "dead_ends"
|
|
144
|
+
[ "$status" -eq 0 ]
|
|
145
|
+
[ "$output" = "${ONLOOKER_DIR}/archivist/${key}/dead_ends" ]
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
@test "write_manifest creates manifest.json under the project dir" {
|
|
149
|
+
local key="abc123def456"
|
|
150
|
+
run archivist_storage_write_manifest "$key" "git@github.com:org/repo.git" "$REPO"
|
|
151
|
+
[ "$status" -eq 0 ]
|
|
152
|
+
[ -f "${ONLOOKER_DIR}/archivist/${key}/manifest.json" ]
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
@test "write_manifest records the project_key, remote_url, and repo_root" {
|
|
156
|
+
local key="abc123def456"
|
|
157
|
+
local remote="git@github.com:org/repo.git"
|
|
158
|
+
archivist_storage_write_manifest "$key" "$remote" "$REPO"
|
|
159
|
+
local manifest="${ONLOOKER_DIR}/archivist/${key}/manifest.json"
|
|
160
|
+
|
|
161
|
+
[ "$(jq -r '.project_key' "$manifest")" = "$key" ]
|
|
162
|
+
[ "$(jq -r '.remote_url' "$manifest")" = "$remote" ]
|
|
163
|
+
[ "$(jq -r '.repo_root' "$manifest")" = "$REPO" ]
|
|
164
|
+
[ "$(jq -r '.source' "$manifest")" = "local" ]
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
@test "write_manifest stamps an ISO-8601 last_compact_at timestamp" {
|
|
168
|
+
local key="abc123def456"
|
|
169
|
+
archivist_storage_write_manifest "$key" "remote" "$REPO"
|
|
170
|
+
local manifest="${ONLOOKER_DIR}/archivist/${key}/manifest.json"
|
|
171
|
+
|
|
172
|
+
local ts
|
|
173
|
+
ts=$(jq -r '.last_compact_at' "$manifest")
|
|
174
|
+
[[ "$ts" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$ ]]
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
@test "write_manifest stores null for an empty remote_url" {
|
|
178
|
+
local key="abc123def456"
|
|
179
|
+
archivist_storage_write_manifest "$key" "" "$REPO"
|
|
180
|
+
local manifest="${ONLOOKER_DIR}/archivist/${key}/manifest.json"
|
|
181
|
+
|
|
182
|
+
[ "$(jq -r '.remote_url' "$manifest")" = "null" ]
|
|
183
|
+
[ "$(jq '.remote_url == null' "$manifest")" = "true" ]
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
@test "write_manifest stores null for an empty repo_root" {
|
|
187
|
+
local key="abc123def456"
|
|
188
|
+
archivist_storage_write_manifest "$key" "remote" ""
|
|
189
|
+
local manifest="${ONLOOKER_DIR}/archivist/${key}/manifest.json"
|
|
190
|
+
|
|
191
|
+
[ "$(jq -r '.repo_root' "$manifest")" = "null" ]
|
|
192
|
+
[ "$(jq '.repo_root == null' "$manifest")" = "true" ]
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
@test "write_manifest rejects an empty key" {
|
|
196
|
+
run archivist_storage_write_manifest "" "remote" "$REPO"
|
|
197
|
+
[ "$status" -ne 0 ]
|
|
198
|
+
}
|
|
@@ -71,3 +71,128 @@ setup() {
|
|
|
71
71
|
@test "compact_tracker_estimate_tokens estimates from string length" {
|
|
72
72
|
[ "$(compact_tracker_estimate_tokens "abcdefghij" false)" -eq 2 ]
|
|
73
73
|
}
|
|
74
|
+
|
|
75
|
+
@test "compact_tracker_state_file prints path under compact trackers dir" {
|
|
76
|
+
local session_id="unit-state-file"
|
|
77
|
+
local result
|
|
78
|
+
result=$(compact_tracker_state_file "$session_id")
|
|
79
|
+
[ "$result" = "${ONLOOKER_COMPACT_TRACKERS_DIR}/${session_id}" ]
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
@test "compact_tracker_record_pre creates state file with pending and compact_count 1" {
|
|
83
|
+
local session_id="unit-record-pre-1"
|
|
84
|
+
local state_file="${ONLOOKER_COMPACT_TRACKERS_DIR}/${session_id}"
|
|
85
|
+
rm -f "$state_file"
|
|
86
|
+
|
|
87
|
+
compact_tracker_record_pre "$session_id" '{"trigger":"manual","transcript_path":""}'
|
|
88
|
+
|
|
89
|
+
[ -f "$state_file" ]
|
|
90
|
+
jq -e '.pending == true
|
|
91
|
+
and .trigger == "manual"
|
|
92
|
+
and .compact_count == 1
|
|
93
|
+
and (.started_ms | type) == "number"' \
|
|
94
|
+
"$state_file" >/dev/null
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
@test "compact_tracker_record_pre increments compact_count on second call" {
|
|
98
|
+
local session_id="unit-record-pre-2"
|
|
99
|
+
local state_file="${ONLOOKER_COMPACT_TRACKERS_DIR}/${session_id}"
|
|
100
|
+
rm -f "$state_file"
|
|
101
|
+
|
|
102
|
+
compact_tracker_record_pre "$session_id" '{"trigger":"manual","transcript_path":""}'
|
|
103
|
+
jq -e '.compact_count == 1' "$state_file" >/dev/null
|
|
104
|
+
|
|
105
|
+
compact_tracker_record_pre "$session_id" '{"trigger":"manual","transcript_path":""}'
|
|
106
|
+
jq -e '.compact_count == 2' "$state_file" >/dev/null
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
@test "compact_tracker_record_pre preserves custom_instructions when present" {
|
|
110
|
+
local session_id="unit-record-pre-3"
|
|
111
|
+
local state_file="${ONLOOKER_COMPACT_TRACKERS_DIR}/${session_id}"
|
|
112
|
+
rm -f "$state_file"
|
|
113
|
+
|
|
114
|
+
compact_tracker_record_pre "$session_id" \
|
|
115
|
+
'{"trigger":"manual","transcript_path":"","custom_instructions":"keep the API design notes"}'
|
|
116
|
+
|
|
117
|
+
jq -e '.custom_instructions == "keep the API design notes"' "$state_file" >/dev/null
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
@test "compact_tracker_append_summary appends a JSONL record" {
|
|
121
|
+
local session_id="unit-append-1"
|
|
122
|
+
local summary_file="${ONLOOKER_SESSION_SUMMARIES_DIR}/${session_id}.jsonl"
|
|
123
|
+
rm -f "$summary_file"
|
|
124
|
+
|
|
125
|
+
compact_tracker_append_summary "$session_id" \
|
|
126
|
+
'{"trigger":"manual","compact_summary":"first summary"}'
|
|
127
|
+
|
|
128
|
+
[ -f "$summary_file" ]
|
|
129
|
+
# Records are written as a JSON stream; count objects rather than physical lines.
|
|
130
|
+
[ "$(jq -s 'length' "$summary_file")" -eq 1 ]
|
|
131
|
+
jq -se '.[0].compact_summary == "first summary" and .[0].trigger == "manual"' "$summary_file" >/dev/null
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
@test "compact_tracker_append_summary appends a second line on second call" {
|
|
135
|
+
local session_id="unit-append-2"
|
|
136
|
+
local summary_file="${ONLOOKER_SESSION_SUMMARIES_DIR}/${session_id}.jsonl"
|
|
137
|
+
rm -f "$summary_file"
|
|
138
|
+
|
|
139
|
+
compact_tracker_append_summary "$session_id" '{"trigger":"manual","compact_summary":"one"}'
|
|
140
|
+
compact_tracker_append_summary "$session_id" '{"trigger":"auto","compact_summary":"two"}'
|
|
141
|
+
|
|
142
|
+
[ "$(jq -s 'length' "$summary_file")" -eq 2 ]
|
|
143
|
+
[ "$(jq -s -r '.[0].compact_summary' "$summary_file")" = "one" ]
|
|
144
|
+
[ "$(jq -s -r '.[1].trigger' "$summary_file")" = "auto" ]
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
@test "compact_tracker_append_summary is a no-op when compact_summary is empty" {
|
|
148
|
+
local session_id="unit-append-3"
|
|
149
|
+
local summary_file="${ONLOOKER_SESSION_SUMMARIES_DIR}/${session_id}.jsonl"
|
|
150
|
+
rm -f "$summary_file"
|
|
151
|
+
|
|
152
|
+
run compact_tracker_append_summary "$session_id" '{"trigger":"manual","compact_summary":""}'
|
|
153
|
+
[ "$status" -eq 0 ]
|
|
154
|
+
[ ! -f "$summary_file" ]
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
@test "compact_tracker_build_compact_payload uses tokens_before from state file" {
|
|
158
|
+
local session_id="unit-build-payload"
|
|
159
|
+
local state_file="${ONLOOKER_COMPACT_TRACKERS_DIR}/${session_id}"
|
|
160
|
+
printf '%s\n' '{"tokens_before":1000}' >"$state_file"
|
|
161
|
+
|
|
162
|
+
local payload
|
|
163
|
+
payload=$(compact_tracker_build_compact_payload "$session_id" \
|
|
164
|
+
'{"compact_summary":"a short compacted summary of the prior context"}')
|
|
165
|
+
|
|
166
|
+
echo "$payload" | jq -e '.tokens_before == 1000
|
|
167
|
+
and (.tokens_after | type) == "number"
|
|
168
|
+
and (.compression_ratio | type) == "number"' >/dev/null
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
@test "compact_tracker_record_post finalizes state and resets turn_tool_seq" {
|
|
172
|
+
local session_id="unit-record-post-1"
|
|
173
|
+
local state_file="${ONLOOKER_COMPACT_TRACKERS_DIR}/${session_id}"
|
|
174
|
+
local tracker_file="${ONLOOKER_SESSION_TRACKERS_DIR}/${session_id}"
|
|
175
|
+
|
|
176
|
+
printf '%s\n' '{"pending":true,"started_ms":1700000000000,"compact_count":1}' >"$state_file"
|
|
177
|
+
printf '%s\n' '{"turn_number":3,"turn_tool_seq":5}' >"$tracker_file"
|
|
178
|
+
|
|
179
|
+
compact_tracker_record_post "$session_id" '{"trigger":"manual","compact_summary":"done"}'
|
|
180
|
+
|
|
181
|
+
jq -e '.pending == false and (.completed_ms | type) == "number"' "$state_file" >/dev/null
|
|
182
|
+
jq -e '.turn_tool_seq == 0 and .turn_number == 3' "$tracker_file" >/dev/null
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
@test "compact_tracker_record_post falls back to create state when none exists" {
|
|
186
|
+
local session_id="unit-record-post-2"
|
|
187
|
+
local state_file="${ONLOOKER_COMPACT_TRACKERS_DIR}/${session_id}"
|
|
188
|
+
local tracker_file="${ONLOOKER_SESSION_TRACKERS_DIR}/${session_id}"
|
|
189
|
+
rm -f "$state_file" "$tracker_file"
|
|
190
|
+
|
|
191
|
+
compact_tracker_record_post "$session_id" '{"trigger":"auto","compact_summary":"recovered"}'
|
|
192
|
+
|
|
193
|
+
[ -f "$state_file" ]
|
|
194
|
+
jq -e '.pending == false
|
|
195
|
+
and (.completed_ms | type) == "number"
|
|
196
|
+
and .compact_count == 1' \
|
|
197
|
+
"$state_file" >/dev/null
|
|
198
|
+
}
|