@onlooker-community/ecosystem 0.9.0 → 0.14.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 +39 -1
- package/.claude-plugin/plugin.json +2 -2
- package/.github/copilot-instructions.md +46 -0
- package/.github/workflows/coverage.yml +78 -0
- package/.github/workflows/release.yml +24 -8
- package/.github/workflows/test.yml +3 -0
- package/.markdownlintignore +3 -0
- package/.release-please-manifest.json +4 -1
- package/CHANGELOG.md +44 -0
- package/README.md +57 -13
- package/config.json +6 -1
- package/docs/adr/001-claude-code-hooks-as-integration-surface.md +43 -0
- package/docs/adr/002-centralized-jsonl-event-log.md +39 -0
- package/docs/adr/003-ulid-over-uuid.md +40 -0
- package/docs/adr/004-plugin-config-with-settings-overlay.md +34 -0
- package/docs/architecture.md +117 -0
- package/hooks/hooks.json +4 -0
- package/package.json +13 -7
- package/plugins/archivist/.claude-plugin/plugin.json +14 -0
- package/plugins/archivist/CHANGELOG.md +8 -0
- package/plugins/archivist/README.md +105 -0
- package/plugins/archivist/config.json +18 -0
- package/plugins/archivist/hooks/hooks.json +35 -0
- package/plugins/archivist/scripts/hooks/archivist-extract.sh +238 -0
- package/plugins/archivist/scripts/hooks/archivist-inject.sh +159 -0
- package/plugins/archivist/scripts/lib/archivist-config.sh +66 -0
- package/plugins/archivist/scripts/lib/archivist-project-key.sh +91 -0
- package/plugins/archivist/scripts/lib/archivist-storage.sh +215 -0
- package/plugins/archivist/scripts/lib/archivist-ulid.sh +52 -0
- package/plugins/echo/.claude-plugin/plugin.json +14 -0
- package/plugins/echo/CHANGELOG.md +24 -0
- package/plugins/echo/README.md +110 -0
- package/plugins/echo/config.json +15 -0
- package/plugins/echo/docs/adr/001-echo-as-separate-plugin.md +33 -0
- package/plugins/echo/docs/adr/002-direct-evaluation-vs-tribunal-pipeline.md +35 -0
- package/plugins/echo/docs/adr/003-stop-hook-trigger.md +40 -0
- package/plugins/echo/hooks/hooks.json +15 -0
- package/plugins/echo/scripts/hooks/echo-stop-gate.sh +366 -0
- package/plugins/echo/scripts/lib/echo-config.sh +108 -0
- package/plugins/echo/scripts/lib/echo-events.sh +74 -0
- package/plugins/echo/scripts/lib/echo-project-key.sh +81 -0
- package/plugins/echo/scripts/lib/echo-ulid.sh +46 -0
- package/plugins/tribunal/.claude-plugin/plugin.json +20 -0
- package/plugins/tribunal/CHANGELOG.md +10 -0
- package/plugins/tribunal/README.md +134 -0
- package/plugins/tribunal/agents/tribunal-actor.md +35 -0
- package/plugins/tribunal/agents/tribunal-judge-adversarial.md +51 -0
- package/plugins/tribunal/agents/tribunal-judge-security.md +47 -0
- package/plugins/tribunal/agents/tribunal-judge-standard.md +47 -0
- package/plugins/tribunal/agents/tribunal-meta-judge.md +61 -0
- package/plugins/tribunal/config.json +50 -0
- package/plugins/tribunal/docs/adr/001-actor-jury-meta-gate-loop.md +40 -0
- package/plugins/tribunal/docs/adr/002-majority-gate-policy.md +48 -0
- package/plugins/tribunal/hooks/hooks.json +15 -0
- package/plugins/tribunal/scripts/hooks/tribunal-stop-gate.sh +267 -0
- package/plugins/tribunal/scripts/lib/tribunal-aggregate.sh +65 -0
- package/plugins/tribunal/scripts/lib/tribunal-config.sh +101 -0
- package/plugins/tribunal/scripts/lib/tribunal-events.sh +97 -0
- package/plugins/tribunal/scripts/lib/tribunal-gate.sh +111 -0
- package/plugins/tribunal/scripts/lib/tribunal-jury.sh +102 -0
- package/plugins/tribunal/scripts/lib/tribunal-project-key.sh +84 -0
- package/plugins/tribunal/scripts/lib/tribunal-rubric.sh +153 -0
- package/plugins/tribunal/scripts/lib/tribunal-ulid.sh +50 -0
- package/plugins/tribunal/scripts/lib/tribunal-verdict.sh +127 -0
- package/plugins/tribunal/skills/tribunal/SKILL.md +129 -0
- package/release-please-config.json +43 -5
- package/scripts/coverage/bash-coverage.mjs +169 -0
- package/scripts/coverage/format-comment.mjs +120 -0
- package/scripts/coverage/run-coverage.mjs +151 -0
- package/scripts/hooks/agent-spawn-tracker.sh +4 -4
- package/scripts/hooks/prompt-rule-injector.sh +122 -0
- package/scripts/lib/onlooker-event.mjs +82 -10
- package/scripts/lib/portable-lock.sh +48 -0
- package/scripts/lib/prompt-rules.sh +207 -0
- package/scripts/lib/tool-history.sh +7 -8
- package/scripts/lib/validate-path.sh +4 -0
- package/scripts/lint/check-manifests.mjs +314 -0
- package/scripts/lint/check-references.mjs +311 -0
- package/skills/list-prompt-rules/SKILL.md +15 -0
- package/test/bats/archivist-config-files.bats +60 -0
- package/test/bats/archivist-config.bats +54 -0
- package/test/bats/archivist-inject.bats +73 -0
- package/test/bats/archivist-project-key.bats +75 -0
- package/test/bats/archivist-storage.bats +119 -0
- package/test/bats/archivist-ulid.bats +36 -0
- package/test/bats/config.bats +10 -10
- package/test/bats/echo-config.bats +90 -0
- package/test/bats/echo-events.bats +121 -0
- package/test/bats/echo-project-key.bats +115 -0
- package/test/bats/echo-stop-hook.bats +101 -0
- package/test/bats/echo-ulid.bats +38 -0
- package/test/bats/portable-lock.bats +62 -0
- package/test/bats/prompt-rules.bats +269 -0
- package/test/bats/read-chunk-tracking.bats +73 -0
- package/test/bats/tool-history-tracker.bats +1 -0
- package/test/bats/tribunal-aggregate.bats +77 -0
- package/test/bats/tribunal-config.bats +86 -0
- package/test/bats/tribunal-events.bats +209 -0
- package/test/bats/tribunal-gate.bats +95 -0
- package/test/bats/tribunal-jury.bats +80 -0
- package/test/bats/tribunal-rubric.bats +119 -0
- package/test/bats/tribunal-stop-hook.bats +73 -0
- package/test/bats/tribunal-verdict.bats +71 -0
- package/test/bats/validate-path.bats +1 -1
- package/test/fixtures/hook-inputs/post-tool-use-read-chunked.json +15 -0
- package/test/fixtures/hook-inputs/user-prompt-submit-rule-match.json +8 -0
- package/test/fixtures/hook-inputs/user-prompt-submit-rule-nomatch.json +8 -0
- package/test/helpers/setup.bash +9 -0
- package/test/node/check-manifests.test.mjs +173 -0
- package/test/node/check-references.test.mjs +279 -0
- package/test/node/coverage.test.mjs +143 -0
- package/test/node/schema-events.test.mjs +41 -1
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Canonical tribunal.* event emission.
|
|
3
|
+
#
|
|
4
|
+
# Thin wrapper around the ecosystem plugin's onlooker-event.mjs `emit` mode.
|
|
5
|
+
# Every emission is validated against @onlooker-community/schema v2.1.0+ before
|
|
6
|
+
# it is appended to ~/.onlooker/logs/onlooker-events.jsonl.
|
|
7
|
+
#
|
|
8
|
+
# Why a per-plugin wrapper: bash hooks should not have to know about node
|
|
9
|
+
# invocation paths or env wiring. This module centralizes that detail so the
|
|
10
|
+
# orchestrator and Stop hook both call `tribunal_emit_event <type> <payload>`.
|
|
11
|
+
#
|
|
12
|
+
# Requires:
|
|
13
|
+
# - $ONLOOKER_DIR (set by the ecosystem onlooker-schema.sh)
|
|
14
|
+
# - $ONLOOKER_EVENTS_LOG (same)
|
|
15
|
+
# - $_HOOK_SESSION_ID or $CLAUDE_SESSION_ID for the session id
|
|
16
|
+
# - The ecosystem onlooker-event.mjs reachable via $_ONLOOKER_EVENT_JS or
|
|
17
|
+
# under the ecosystem plugin root.
|
|
18
|
+
#
|
|
19
|
+
# Usage:
|
|
20
|
+
# tribunal_emit_event "tribunal.session.start" '{"task_id":"01J...","gate_policy":"majority"}'
|
|
21
|
+
|
|
22
|
+
_TRIBUNAL_PLUGIN_NAME="tribunal"
|
|
23
|
+
|
|
24
|
+
# Resolve the ecosystem onlooker-event.mjs even when CLAUDE_PLUGIN_ROOT points
|
|
25
|
+
# at the tribunal plugin (the wrapper script lives under ecosystem/scripts/lib).
|
|
26
|
+
_tribunal_event_js_path() {
|
|
27
|
+
if [[ -n "${_ONLOOKER_EVENT_JS:-}" && -f "$_ONLOOKER_EVENT_JS" ]]; then
|
|
28
|
+
printf '%s' "$_ONLOOKER_EVENT_JS"
|
|
29
|
+
return 0
|
|
30
|
+
fi
|
|
31
|
+
local plugin_root="${CLAUDE_PLUGIN_ROOT:-}"
|
|
32
|
+
local candidates=(
|
|
33
|
+
"${plugin_root}/scripts/lib/onlooker-event.mjs"
|
|
34
|
+
"${plugin_root}/../../scripts/lib/onlooker-event.mjs"
|
|
35
|
+
)
|
|
36
|
+
local c
|
|
37
|
+
for c in "${candidates[@]}"; do
|
|
38
|
+
[[ -f "$c" ]] && { printf '%s' "$c"; return 0; }
|
|
39
|
+
done
|
|
40
|
+
return 1
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
_tribunal_session_id() {
|
|
44
|
+
if [[ -n "${_HOOK_SESSION_ID:-}" ]]; then
|
|
45
|
+
printf '%s' "$_HOOK_SESSION_ID"
|
|
46
|
+
return 0
|
|
47
|
+
fi
|
|
48
|
+
if [[ -n "${CLAUDE_SESSION_ID:-}" ]]; then
|
|
49
|
+
printf '%s' "$CLAUDE_SESSION_ID"
|
|
50
|
+
return 0
|
|
51
|
+
fi
|
|
52
|
+
printf 'unknown'
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
# Emit a single tribunal event. Returns 0 on success, non-zero on validation
|
|
56
|
+
# failure (so callers can decide whether to abort the loop). Validation errors
|
|
57
|
+
# are written to stderr.
|
|
58
|
+
tribunal_emit_event() {
|
|
59
|
+
local event_type="${1:-}"
|
|
60
|
+
local payload="${2:-}"
|
|
61
|
+
[[ -z "$event_type" || -z "$payload" ]] && return 1
|
|
62
|
+
|
|
63
|
+
local event_js
|
|
64
|
+
event_js=$(_tribunal_event_js_path) || {
|
|
65
|
+
printf 'tribunal-events: cannot locate onlooker-event.mjs\n' >&2
|
|
66
|
+
return 1
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
local session_id
|
|
70
|
+
session_id=$(_tribunal_session_id)
|
|
71
|
+
|
|
72
|
+
local params
|
|
73
|
+
params=$(jq -n \
|
|
74
|
+
--arg plugin "$_TRIBUNAL_PLUGIN_NAME" \
|
|
75
|
+
--arg sid "$session_id" \
|
|
76
|
+
--arg type "$event_type" \
|
|
77
|
+
--argjson payload "$payload" \
|
|
78
|
+
'{plugin: $plugin, session_id: $sid, event_type: $type, payload: $payload}')
|
|
79
|
+
|
|
80
|
+
local event
|
|
81
|
+
local stderr_file
|
|
82
|
+
stderr_file=$(mktemp -t tribunal-event-err.XXXXXX 2>/dev/null) || stderr_file="/tmp/tribunal-event-err.$$"
|
|
83
|
+
event=$(printf '%s' "$params" \
|
|
84
|
+
| ONLOOKER_DIR="${ONLOOKER_DIR:-$HOME/.onlooker}" \
|
|
85
|
+
ONLOOKER_PLUGIN_NAME="$_TRIBUNAL_PLUGIN_NAME" \
|
|
86
|
+
node "$event_js" emit 2>"$stderr_file") || {
|
|
87
|
+
printf 'tribunal-events: schema validation failed for %s\n' "$event_type" >&2
|
|
88
|
+
[[ -s "$stderr_file" ]] && cat "$stderr_file" >&2
|
|
89
|
+
rm -f "$stderr_file"
|
|
90
|
+
return 1
|
|
91
|
+
}
|
|
92
|
+
rm -f "$stderr_file"
|
|
93
|
+
|
|
94
|
+
local log_path="${ONLOOKER_EVENTS_LOG:-${ONLOOKER_DIR:-$HOME/.onlooker}/logs/onlooker-events.jsonl}"
|
|
95
|
+
mkdir -p "$(dirname "$log_path")" 2>/dev/null || return 1
|
|
96
|
+
printf '%s\n' "$event" >>"$log_path"
|
|
97
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Gate decision for Tribunal.
|
|
3
|
+
#
|
|
4
|
+
# Resolves a gate verdict (passed | blocked + reason) from the four schema
|
|
5
|
+
# gate_policy values: strict, majority, unanimous, meta_override.
|
|
6
|
+
#
|
|
7
|
+
# Each policy operates over:
|
|
8
|
+
# - verdicts: JSON array of { judge_id, score, passed } from each Judge
|
|
9
|
+
# - aggregated_score: float, output of tribunal_aggregate
|
|
10
|
+
# - score_threshold: float from the active rubric
|
|
11
|
+
# - meta: JSON of the Meta-Judge's TribunalMetaCompletePayload (for bias_detected
|
|
12
|
+
# and override_recommendation)
|
|
13
|
+
# - dissent_score: float, output of tribunal_disagreement
|
|
14
|
+
# - dissent_threshold: float from rubric/config
|
|
15
|
+
#
|
|
16
|
+
# Echoes a JSON object: { passed: bool, reason?: string }
|
|
17
|
+
# reason is one of: low_score | meta_override | bias_detected | dissent_unresolved
|
|
18
|
+
#
|
|
19
|
+
# Usage: result=$(tribunal_gate_decide "$policy" "$verdicts" "$agg" "$thr" "$meta" "$dissent" "$dissent_thr")
|
|
20
|
+
|
|
21
|
+
tribunal_gate_decide() {
|
|
22
|
+
local policy="${1:-majority}"
|
|
23
|
+
local verdicts="${2:-[]}"
|
|
24
|
+
local aggregated_score="${3:-0}"
|
|
25
|
+
local score_threshold="${4:-0.75}"
|
|
26
|
+
local meta="${5:-{}}"
|
|
27
|
+
local dissent_score="${6:-0}"
|
|
28
|
+
local dissent_threshold="${7:-0.25}"
|
|
29
|
+
|
|
30
|
+
local meta_bias_detected meta_override
|
|
31
|
+
meta_bias_detected=$(printf '%s' "$meta" | jq -r '.bias_detected // false' 2>/dev/null)
|
|
32
|
+
meta_override=$(printf '%s' "$meta" | jq -r '.override_recommendation // empty' 2>/dev/null)
|
|
33
|
+
|
|
34
|
+
# meta_override policy: the Meta-Judge wins, regardless of jury.
|
|
35
|
+
if [[ "$policy" == "meta_override" ]]; then
|
|
36
|
+
case "$meta_override" in
|
|
37
|
+
accept)
|
|
38
|
+
printf '{"passed":true}'
|
|
39
|
+
return 0
|
|
40
|
+
;;
|
|
41
|
+
reject)
|
|
42
|
+
printf '{"passed":false,"reason":"meta_override"}'
|
|
43
|
+
return 0
|
|
44
|
+
;;
|
|
45
|
+
re-evaluate|"")
|
|
46
|
+
# No clear override → fall through to score-based decision.
|
|
47
|
+
;;
|
|
48
|
+
esac
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
# Bias detection short-circuit (any policy).
|
|
52
|
+
if [[ "$meta_bias_detected" == "true" && "$meta_override" == "reject" ]]; then
|
|
53
|
+
printf '{"passed":false,"reason":"bias_detected"}'
|
|
54
|
+
return 0
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
# Dissent short-circuit: if judges disagree past threshold AND the Meta-Judge
|
|
58
|
+
# has not provided an override, block with dissent_unresolved so the loop
|
|
59
|
+
# retries with a fresh Actor pass.
|
|
60
|
+
if awk -v d="$dissent_score" -v t="$dissent_threshold" 'BEGIN { exit !(d > t) }' \
|
|
61
|
+
&& [[ -z "$meta_override" || "$meta_override" == "re-evaluate" ]]; then
|
|
62
|
+
printf '{"passed":false,"reason":"dissent_unresolved"}'
|
|
63
|
+
return 0
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
local count passed_count
|
|
67
|
+
count=$(printf '%s' "$verdicts" | jq 'length' 2>/dev/null) || count=0
|
|
68
|
+
passed_count=$(printf '%s' "$verdicts" | jq '[.[] | select(.passed == true)] | length' 2>/dev/null) || passed_count=0
|
|
69
|
+
|
|
70
|
+
local jury_ok=1 # 0 = ok, 1 = not ok (shell convention)
|
|
71
|
+
case "$policy" in
|
|
72
|
+
strict|unanimous)
|
|
73
|
+
[[ "$count" -gt 0 && "$passed_count" -eq "$count" ]] && jury_ok=0
|
|
74
|
+
;;
|
|
75
|
+
majority)
|
|
76
|
+
# strictly greater than half
|
|
77
|
+
[[ "$count" -gt 0 ]] && (( passed_count * 2 > count )) && jury_ok=0
|
|
78
|
+
;;
|
|
79
|
+
meta_override)
|
|
80
|
+
# Already handled accept/reject above; fall back to majority for the
|
|
81
|
+
# re-evaluate / unset case.
|
|
82
|
+
[[ "$count" -gt 0 ]] && (( passed_count * 2 > count )) && jury_ok=0
|
|
83
|
+
;;
|
|
84
|
+
*)
|
|
85
|
+
printf 'tribunal-gate: unknown policy %s, falling back to majority\n' \
|
|
86
|
+
"$policy" >&2
|
|
87
|
+
[[ "$count" -gt 0 ]] && (( passed_count * 2 > count )) && jury_ok=0
|
|
88
|
+
;;
|
|
89
|
+
esac
|
|
90
|
+
|
|
91
|
+
local score_ok=1
|
|
92
|
+
awk -v s="$aggregated_score" -v t="$score_threshold" 'BEGIN { exit !(s >= t) }' && score_ok=0
|
|
93
|
+
|
|
94
|
+
if [[ "$jury_ok" -eq 0 && "$score_ok" -eq 0 ]]; then
|
|
95
|
+
printf '{"passed":true}'
|
|
96
|
+
return 0
|
|
97
|
+
fi
|
|
98
|
+
|
|
99
|
+
# Pick the most informative blocking reason.
|
|
100
|
+
if [[ "$score_ok" -ne 0 ]]; then
|
|
101
|
+
printf '{"passed":false,"reason":"low_score"}'
|
|
102
|
+
else
|
|
103
|
+
# Jury did not pass even though score cleared threshold — surface as
|
|
104
|
+
# meta_override when meta said reject, else dissent_unresolved.
|
|
105
|
+
if [[ "$meta_override" == "reject" ]]; then
|
|
106
|
+
printf '{"passed":false,"reason":"meta_override"}'
|
|
107
|
+
else
|
|
108
|
+
printf '{"passed":false,"reason":"dissent_unresolved"}'
|
|
109
|
+
fi
|
|
110
|
+
fi
|
|
111
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Jury resolution for Tribunal.
|
|
3
|
+
#
|
|
4
|
+
# Maps a list of judge_types (from a rubric or config) into a panel of judges
|
|
5
|
+
# the orchestrator can spawn. Each panel entry carries:
|
|
6
|
+
# - judge_id: ULID, unique per panel member
|
|
7
|
+
# - judge_type: schema enum (standard, security, maintainability, adversarial,
|
|
8
|
+
# domain, meta)
|
|
9
|
+
# - subagent: name of the subagent MD to invoke
|
|
10
|
+
# - model: resolved per-judge-type model from config
|
|
11
|
+
#
|
|
12
|
+
# Two judge_types are recognized by schema but not yet shipped as subagents:
|
|
13
|
+
# `maintainability` and `domain`. The jury degrades them to `standard` and emits
|
|
14
|
+
# a warning on stderr so the orchestrator can log it.
|
|
15
|
+
#
|
|
16
|
+
# Requires tribunal-config.sh and tribunal-ulid.sh to be sourced.
|
|
17
|
+
|
|
18
|
+
# Map a judge_type to the shipped subagent name. Echoes empty if there is no
|
|
19
|
+
# shipped subagent.
|
|
20
|
+
_tribunal_jury_subagent_for_type() {
|
|
21
|
+
case "$1" in
|
|
22
|
+
standard) printf 'tribunal-judge-standard' ;;
|
|
23
|
+
security) printf 'tribunal-judge-security' ;;
|
|
24
|
+
adversarial) printf 'tribunal-judge-adversarial' ;;
|
|
25
|
+
meta) printf 'tribunal-meta-judge' ;;
|
|
26
|
+
*) return 0 ;;
|
|
27
|
+
esac
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
# Build a jury from a JSON array of judge_types.
|
|
31
|
+
# Echoes a JSON array of { judge_id, judge_type, subagent, model } objects.
|
|
32
|
+
# Unsupported types (maintainability, domain) are remapped to standard with a
|
|
33
|
+
# warning on stderr.
|
|
34
|
+
#
|
|
35
|
+
# Usage: jury=$(tribunal_jury_empanel '["standard","adversarial"]')
|
|
36
|
+
tribunal_jury_empanel() {
|
|
37
|
+
local types_json="${1:-[]}"
|
|
38
|
+
[[ -z "$types_json" ]] && types_json="[]"
|
|
39
|
+
|
|
40
|
+
local panel='[]'
|
|
41
|
+
local count
|
|
42
|
+
count=$(printf '%s' "$types_json" | jq 'length' 2>/dev/null) || count=0
|
|
43
|
+
|
|
44
|
+
local i raw_type judge_type subagent model judge_id
|
|
45
|
+
for ((i = 0; i < count; i++)); do
|
|
46
|
+
raw_type=$(printf '%s' "$types_json" | jq -r ".[$i]" 2>/dev/null) || continue
|
|
47
|
+
[[ -z "$raw_type" || "$raw_type" == "null" ]] && continue
|
|
48
|
+
|
|
49
|
+
# Schema-known types we don't ship yet degrade to standard.
|
|
50
|
+
case "$raw_type" in
|
|
51
|
+
maintainability|domain)
|
|
52
|
+
printf 'tribunal-jury: judge_type "%s" not shipped in v0.1, degrading to standard\n' \
|
|
53
|
+
"$raw_type" >&2
|
|
54
|
+
judge_type="standard"
|
|
55
|
+
;;
|
|
56
|
+
meta)
|
|
57
|
+
# Meta is the Meta-Judge — never goes in the jury panel.
|
|
58
|
+
printf 'tribunal-jury: refusing to add judge_type "meta" to the jury (meta is the Meta-Judge)\n' >&2
|
|
59
|
+
continue
|
|
60
|
+
;;
|
|
61
|
+
standard|security|adversarial)
|
|
62
|
+
judge_type="$raw_type"
|
|
63
|
+
;;
|
|
64
|
+
*)
|
|
65
|
+
printf 'tribunal-jury: unknown judge_type "%s", degrading to standard\n' \
|
|
66
|
+
"$raw_type" >&2
|
|
67
|
+
judge_type="standard"
|
|
68
|
+
;;
|
|
69
|
+
esac
|
|
70
|
+
|
|
71
|
+
subagent=$(_tribunal_jury_subagent_for_type "$judge_type")
|
|
72
|
+
[[ -z "$subagent" ]] && continue
|
|
73
|
+
|
|
74
|
+
model=$(tribunal_config_judge_model "$judge_type")
|
|
75
|
+
judge_id=$(tribunal_ulid)
|
|
76
|
+
|
|
77
|
+
panel=$(printf '%s' "$panel" | jq -c \
|
|
78
|
+
--arg id "$judge_id" \
|
|
79
|
+
--arg type "$judge_type" \
|
|
80
|
+
--arg sub "$subagent" \
|
|
81
|
+
--arg model "$model" \
|
|
82
|
+
'. + [{
|
|
83
|
+
judge_id: $id,
|
|
84
|
+
judge_type: $type,
|
|
85
|
+
subagent: $sub,
|
|
86
|
+
model: (if $model == "" then null else $model end)
|
|
87
|
+
}]')
|
|
88
|
+
done
|
|
89
|
+
|
|
90
|
+
printf '%s' "$panel"
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
# Render a jury into the schema's TribunalJuryEmpaneledPayload.judges shape:
|
|
94
|
+
# [{ judge_id, judge_type, model_id }, ...]
|
|
95
|
+
tribunal_jury_to_schema_judges() {
|
|
96
|
+
local panel="${1:-[]}"
|
|
97
|
+
printf '%s' "$panel" | jq -c '[.[] | {
|
|
98
|
+
judge_id: .judge_id,
|
|
99
|
+
judge_type: .judge_type,
|
|
100
|
+
model_id: .model
|
|
101
|
+
} | with_entries(select(.value != null))]'
|
|
102
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Project key derivation for Tribunal.
|
|
3
|
+
#
|
|
4
|
+
# Mirrors the archivist project-key scheme so the two plugins partition storage
|
|
5
|
+
# identically. A project key is a stable 12-char hex identifier that survives:
|
|
6
|
+
# - local rename of the repo directory
|
|
7
|
+
# - cloning the same repo to a different path on the same machine
|
|
8
|
+
# - moving the repo between machines (as long as the git remote is preserved)
|
|
9
|
+
# - worktrees (a worktree shares its parent repo's key)
|
|
10
|
+
#
|
|
11
|
+
# Resolution order:
|
|
12
|
+
# 1. SHA256(`git remote get-url origin`) — preferred, machine-portable
|
|
13
|
+
# 2. SHA256(realpath of `git rev-parse --show-toplevel`) — fallback for repos
|
|
14
|
+
# without an origin remote (greenfield local-only work)
|
|
15
|
+
#
|
|
16
|
+
# Returns the first 12 hex chars. Returns empty string if neither resolution
|
|
17
|
+
# path works.
|
|
18
|
+
|
|
19
|
+
_tribunal_sha256_first12() {
|
|
20
|
+
local input="$1"
|
|
21
|
+
if command -v shasum >/dev/null 2>&1; then
|
|
22
|
+
printf '%s' "$input" | shasum -a 256 2>/dev/null | cut -c1-12
|
|
23
|
+
elif command -v sha256sum >/dev/null 2>&1; then
|
|
24
|
+
printf '%s' "$input" | sha256sum 2>/dev/null | cut -c1-12
|
|
25
|
+
else
|
|
26
|
+
return 1
|
|
27
|
+
fi
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
tribunal_project_remote_url() {
|
|
31
|
+
local cwd="${1:-}"
|
|
32
|
+
[[ -z "$cwd" || ! -d "$cwd" ]] && return 0
|
|
33
|
+
git -C "$cwd" remote get-url origin 2>/dev/null || true
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
# Worktree-aware: uses common-dir so worktrees share a key with the main repo.
|
|
37
|
+
tribunal_project_repo_root() {
|
|
38
|
+
local cwd="${1:-}"
|
|
39
|
+
[[ -z "$cwd" || ! -d "$cwd" ]] && return 0
|
|
40
|
+
|
|
41
|
+
if ! git -C "$cwd" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
42
|
+
return 0
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
local common_dir toplevel
|
|
46
|
+
common_dir=$(git -C "$cwd" rev-parse --git-common-dir 2>/dev/null) || return 0
|
|
47
|
+
|
|
48
|
+
if [[ -n "$common_dir" && "$common_dir" != /* ]]; then
|
|
49
|
+
common_dir="$(cd "$cwd" && cd "$common_dir" 2>/dev/null && pwd -P)" || common_dir=""
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
if [[ -n "$common_dir" && -d "$common_dir" ]]; then
|
|
53
|
+
toplevel="$(cd "$common_dir/.." 2>/dev/null && pwd -P)" || toplevel=""
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
if [[ -z "$toplevel" ]]; then
|
|
57
|
+
toplevel=$(git -C "$cwd" rev-parse --show-toplevel 2>/dev/null || true)
|
|
58
|
+
[[ -n "$toplevel" ]] && toplevel="$(cd "$toplevel" 2>/dev/null && pwd -P)"
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
printf '%s' "$toplevel"
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
# Compute the project key for the given cwd. Prints the key or empty string.
|
|
65
|
+
tribunal_project_key() {
|
|
66
|
+
local cwd="${1:-}"
|
|
67
|
+
[[ -z "$cwd" ]] && cwd="$(pwd)"
|
|
68
|
+
|
|
69
|
+
local remote
|
|
70
|
+
remote=$(tribunal_project_remote_url "$cwd")
|
|
71
|
+
if [[ -n "$remote" ]]; then
|
|
72
|
+
_tribunal_sha256_first12 "remote:$remote"
|
|
73
|
+
return 0
|
|
74
|
+
fi
|
|
75
|
+
|
|
76
|
+
local root
|
|
77
|
+
root=$(tribunal_project_repo_root "$cwd")
|
|
78
|
+
if [[ -n "$root" ]]; then
|
|
79
|
+
_tribunal_sha256_first12 "root:$root"
|
|
80
|
+
return 0
|
|
81
|
+
fi
|
|
82
|
+
|
|
83
|
+
return 0
|
|
84
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Rubric resolution for Tribunal.
|
|
3
|
+
#
|
|
4
|
+
# Layered lookup (latest wins, by rubric id):
|
|
5
|
+
# 1. Built-in rubrics from plugins/tribunal/config.json (.tribunal.rubric.builtins)
|
|
6
|
+
# 2. ~/.onlooker/tribunal.json (.rubrics)
|
|
7
|
+
# 3. <repo>/.claude/tribunal.json (.rubrics)
|
|
8
|
+
#
|
|
9
|
+
# Exposes:
|
|
10
|
+
# tribunal_rubric_load <repo_root> # populates _TRIBUNAL_RUBRICS (JSON array)
|
|
11
|
+
# tribunal_rubric_get <id> # echoes a single rubric as JSON, or empty
|
|
12
|
+
# tribunal_rubric_default_id # default rubric id from config
|
|
13
|
+
# tribunal_rubric_validate <rubric_json> # exits 0 if valid, 1 otherwise; prints reasons to stderr
|
|
14
|
+
#
|
|
15
|
+
# Schema:
|
|
16
|
+
# {
|
|
17
|
+
# "id": "default",
|
|
18
|
+
# "criteria": [ { "name": str, "weight": [0,1], "min_pass": [0,1] }, ... ],
|
|
19
|
+
# "score_threshold": [0,1],
|
|
20
|
+
# "max_iterations": int >= 1,
|
|
21
|
+
# "judge_types": ["standard" | "security" | ...],
|
|
22
|
+
# "gate_policy": "strict" | "majority" | "unanimous" | "meta_override",
|
|
23
|
+
# "aggregation_method": "mean" | "median" | "min" | "weighted_mean"
|
|
24
|
+
# }
|
|
25
|
+
#
|
|
26
|
+
# Requires tribunal-config.sh to be sourced and tribunal_config_load to have run.
|
|
27
|
+
|
|
28
|
+
_TRIBUNAL_RUBRICS="[]"
|
|
29
|
+
|
|
30
|
+
_tribunal_rubric_overlay_file() {
|
|
31
|
+
local file="$1"
|
|
32
|
+
local base="$2"
|
|
33
|
+
[[ -f "$file" ]] || { printf '%s' "$base"; return 0; }
|
|
34
|
+
local overlay
|
|
35
|
+
overlay=$(jq -c '.rubrics // []' "$file" 2>/dev/null) || { printf '%s' "$base"; return 0; }
|
|
36
|
+
[[ "$overlay" == "[]" || -z "$overlay" ]] && { printf '%s' "$base"; return 0; }
|
|
37
|
+
|
|
38
|
+
jq -c -n --argjson base "$base" --argjson overlay "$overlay" '
|
|
39
|
+
($base + $overlay)
|
|
40
|
+
| reduce .[] as $r ({}; .[$r.id] = $r)
|
|
41
|
+
| [.[]]
|
|
42
|
+
'
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
tribunal_rubric_load() {
|
|
46
|
+
local repo_root="${1:-}"
|
|
47
|
+
local home_dir="${HOME:-}"
|
|
48
|
+
|
|
49
|
+
local builtins
|
|
50
|
+
builtins=$(printf '%s' "$_TRIBUNAL_CONFIG" | jq -c '.tribunal.rubric.builtins // []' 2>/dev/null)
|
|
51
|
+
[[ -z "$builtins" ]] && builtins="[]"
|
|
52
|
+
|
|
53
|
+
local merged="$builtins"
|
|
54
|
+
merged=$(_tribunal_rubric_overlay_file "${home_dir}/.onlooker/tribunal.json" "$merged")
|
|
55
|
+
merged=$(_tribunal_rubric_overlay_file "${repo_root}/.claude/tribunal.json" "$merged")
|
|
56
|
+
|
|
57
|
+
_TRIBUNAL_RUBRICS="$merged"
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
tribunal_rubric_default_id() {
|
|
61
|
+
local id
|
|
62
|
+
id=$(tribunal_config_get '.tribunal.rubric.default_id')
|
|
63
|
+
[[ -z "$id" ]] && id="default"
|
|
64
|
+
printf '%s' "$id"
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
tribunal_rubric_get() {
|
|
68
|
+
local id="$1"
|
|
69
|
+
[[ -z "$id" ]] && return 0
|
|
70
|
+
printf '%s' "$_TRIBUNAL_RUBRICS" | jq -c --arg id "$id" \
|
|
71
|
+
'map(select(.id == $id)) | first // empty' 2>/dev/null
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
# Validate a single rubric JSON blob. Prints reasons to stderr; exits 1 on first
|
|
75
|
+
# problem so callers can pipe to a single failure path.
|
|
76
|
+
tribunal_rubric_validate() {
|
|
77
|
+
local rubric="${1:-}"
|
|
78
|
+
[[ -z "$rubric" || "$rubric" == "null" ]] && {
|
|
79
|
+
printf 'rubric is empty\n' >&2
|
|
80
|
+
return 1
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
local id
|
|
84
|
+
id=$(printf '%s' "$rubric" | jq -r '.id // empty' 2>/dev/null)
|
|
85
|
+
[[ -z "$id" ]] && { printf 'rubric missing .id\n' >&2; return 1; }
|
|
86
|
+
|
|
87
|
+
local criteria_count
|
|
88
|
+
criteria_count=$(printf '%s' "$rubric" | jq -r '(.criteria // []) | length' 2>/dev/null)
|
|
89
|
+
[[ "$criteria_count" -ge 1 ]] || {
|
|
90
|
+
printf 'rubric %s: criteria must be non-empty\n' "$id" >&2
|
|
91
|
+
return 1
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
# Each criterion: name non-empty, weight in [0,1], min_pass in [0,1].
|
|
95
|
+
local bad_crit
|
|
96
|
+
bad_crit=$(printf '%s' "$rubric" | jq -r '
|
|
97
|
+
[.criteria[]
|
|
98
|
+
| select(
|
|
99
|
+
(.name | type) != "string"
|
|
100
|
+
or (.name | length) == 0
|
|
101
|
+
or (.weight | type) != "number" or .weight < 0 or .weight > 1
|
|
102
|
+
or (.min_pass | type) != "number" or .min_pass < 0 or .min_pass > 1
|
|
103
|
+
)
|
|
104
|
+
| .name // "(unnamed)"]
|
|
105
|
+
| join(",")
|
|
106
|
+
' 2>/dev/null)
|
|
107
|
+
[[ -n "$bad_crit" ]] && {
|
|
108
|
+
printf 'rubric %s: invalid criteria: %s\n' "$id" "$bad_crit" >&2
|
|
109
|
+
return 1
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
# Weights must sum to ~1.0.
|
|
113
|
+
local weight_sum
|
|
114
|
+
weight_sum=$(printf '%s' "$rubric" | jq -r '[.criteria[].weight] | add' 2>/dev/null)
|
|
115
|
+
awk -v s="$weight_sum" 'BEGIN { exit !(s > 0.99 && s < 1.01) }' || {
|
|
116
|
+
printf 'rubric %s: criterion weights sum to %s (expected ~1.0)\n' "$id" "$weight_sum" >&2
|
|
117
|
+
return 1
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
# score_threshold in [0,1].
|
|
121
|
+
local thr
|
|
122
|
+
thr=$(printf '%s' "$rubric" | jq -r '.score_threshold // 0.75' 2>/dev/null)
|
|
123
|
+
awk -v t="$thr" 'BEGIN { exit !(t >= 0 && t <= 1) }' || {
|
|
124
|
+
printf 'rubric %s: score_threshold %s out of [0,1]\n' "$id" "$thr" >&2
|
|
125
|
+
return 1
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
# gate_policy enum.
|
|
129
|
+
local gp
|
|
130
|
+
gp=$(printf '%s' "$rubric" | jq -r '.gate_policy // "majority"' 2>/dev/null)
|
|
131
|
+
case "$gp" in
|
|
132
|
+
strict|majority|unanimous|meta_override) : ;;
|
|
133
|
+
*) printf 'rubric %s: invalid gate_policy %s\n' "$id" "$gp" >&2; return 1 ;;
|
|
134
|
+
esac
|
|
135
|
+
|
|
136
|
+
# aggregation_method enum.
|
|
137
|
+
local am
|
|
138
|
+
am=$(printf '%s' "$rubric" | jq -r '.aggregation_method // "weighted_mean"' 2>/dev/null)
|
|
139
|
+
case "$am" in
|
|
140
|
+
mean|median|min|weighted_mean) : ;;
|
|
141
|
+
*) printf 'rubric %s: invalid aggregation_method %s\n' "$id" "$am" >&2; return 1 ;;
|
|
142
|
+
esac
|
|
143
|
+
|
|
144
|
+
# max_iterations integer >= 1.
|
|
145
|
+
local mi
|
|
146
|
+
mi=$(printf '%s' "$rubric" | jq -r '.max_iterations // 3' 2>/dev/null)
|
|
147
|
+
[[ "$mi" =~ ^[0-9]+$ && "$mi" -ge 1 ]] || {
|
|
148
|
+
printf 'rubric %s: max_iterations must be integer >= 1 (got %s)\n' "$id" "$mi" >&2
|
|
149
|
+
return 1
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return 0
|
|
153
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Minimal ULID generator for Tribunal task_id and iteration_id values.
|
|
3
|
+
#
|
|
4
|
+
# Spec: https://github.com/ulid/spec
|
|
5
|
+
# - 48-bit timestamp (ms since epoch) → 10 chars Crockford Base32
|
|
6
|
+
# - 80-bit randomness → 16 chars Crockford Base32
|
|
7
|
+
# - lexicographically sortable, time-ordered
|
|
8
|
+
|
|
9
|
+
_TRIBUNAL_ULID_ALPHABET="0123456789ABCDEFGHJKMNPQRSTVWXYZ"
|
|
10
|
+
|
|
11
|
+
_tribunal_ulid_encode() {
|
|
12
|
+
local n="$1"
|
|
13
|
+
local len="$2"
|
|
14
|
+
local out=""
|
|
15
|
+
local i
|
|
16
|
+
for ((i = 0; i < len; i++)); do
|
|
17
|
+
out="${_TRIBUNAL_ULID_ALPHABET:$((n % 32)):1}${out}"
|
|
18
|
+
n=$((n / 32))
|
|
19
|
+
done
|
|
20
|
+
printf '%s' "$out"
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
tribunal_ulid() {
|
|
24
|
+
local now_ms
|
|
25
|
+
if [[ "$(uname)" == "Darwin" ]]; then
|
|
26
|
+
now_ms=$(python3 -c 'import time; print(int(time.time() * 1000))' 2>/dev/null) \
|
|
27
|
+
|| now_ms=$(($(date +%s) * 1000))
|
|
28
|
+
else
|
|
29
|
+
now_ms=$(date +%s%3N 2>/dev/null) || now_ms=$(($(date +%s) * 1000))
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
local rand_hex rand_hi rand_lo
|
|
33
|
+
rand_hex=$(openssl rand -hex 10 2>/dev/null)
|
|
34
|
+
if [[ -n "$rand_hex" && ${#rand_hex} -eq 20 ]]; then
|
|
35
|
+
rand_hi=$((16#${rand_hex:0:10}))
|
|
36
|
+
rand_lo=$((16#${rand_hex:10:10}))
|
|
37
|
+
else
|
|
38
|
+
rand_hi=$((RANDOM * 32768 + RANDOM))
|
|
39
|
+
rand_lo=$((RANDOM * 32768 + RANDOM))
|
|
40
|
+
rand_hi=$(((rand_hi * 256 + RANDOM % 256) & ((1 << 40) - 1)))
|
|
41
|
+
rand_lo=$(((rand_lo * 256 + RANDOM % 256) & ((1 << 40) - 1)))
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
local ts_part hi_part lo_part
|
|
45
|
+
ts_part=$(_tribunal_ulid_encode "$now_ms" 10)
|
|
46
|
+
hi_part=$(_tribunal_ulid_encode "$rand_hi" 8)
|
|
47
|
+
lo_part=$(_tribunal_ulid_encode "$rand_lo" 8)
|
|
48
|
+
|
|
49
|
+
printf '%s%s%s' "$ts_part" "$hi_part" "$lo_part"
|
|
50
|
+
}
|