@onlooker-community/ecosystem 0.34.0 → 0.43.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/skills/writing-tests/SKILL.md +27 -0
- package/.claude-plugin/plugin.json +1 -1
- package/.github/workflows/release.yml +14 -14
- package/.release-please-manifest.json +7 -7
- package/AGENTS.md +116 -0
- package/CHANGELOG.md +175 -0
- package/CLAUDE.md +5 -0
- package/docs/lesson-promotion-pipeline.md +6 -6
- package/docs/superpowers/plans/2026-08-10-lesson-confirmation.md +1273 -0
- package/docs/superpowers/plans/2026-08-11-lesson-judging.md +1232 -0
- package/docs/superpowers/plans/2026-08-11-lesson-unconfirm.md +458 -0
- package/docs/superpowers/plans/2026-08-12-author-key.md +567 -0
- package/docs/superpowers/plans/2026-08-13-approved-pool.md +747 -0
- package/docs/superpowers/plans/2026-08-13-librarian-cluster.md +392 -0
- package/docs/superpowers/plans/2026-08-14-criterion-scores-consumers.md +1374 -0
- package/docs/superpowers/plans/2026-08-14-criterion-scores-schema.md +277 -0
- package/docs/superpowers/plans/2026-08-16-cartographer-undocumented-entity.md +741 -0
- package/docs/superpowers/specs/2026-08-10-lesson-confirmation-design.md +213 -0
- package/docs/superpowers/specs/2026-08-11-lesson-judging-design.md +267 -0
- package/docs/superpowers/specs/2026-08-11-lesson-unconfirm-design.md +152 -0
- package/docs/superpowers/specs/2026-08-12-author-key-design.md +317 -0
- package/docs/superpowers/specs/2026-08-13-approved-pool-design.md +232 -0
- package/docs/superpowers/specs/2026-08-14-criterion-scores-design.md +254 -0
- package/docs/superpowers/specs/2026-08-16-cartographer-undocumented-entity-design.md +249 -0
- package/package.json +2 -2
- package/plugins/assayer/.claude-plugin/plugin.json +1 -1
- package/plugins/assayer/CHANGELOG.md +7 -0
- package/plugins/assayer/scripts/lib/assayer-config.sh +6 -0
- package/plugins/cartographer/.claude-plugin/plugin.json +1 -1
- package/plugins/cartographer/CHANGELOG.md +54 -0
- package/plugins/cartographer/README.md +56 -1
- package/plugins/cartographer/config.json +7 -1
- package/plugins/cartographer/scripts/hooks/cartographer-post-write.sh +4 -4
- package/plugins/cartographer/scripts/hooks/cartographer-session-start.sh +7 -4
- package/plugins/cartographer/scripts/lib/cartographer-collect.sh +8 -1
- package/plugins/cartographer/scripts/lib/cartographer-config.sh +26 -0
- package/plugins/cartographer/scripts/lib/cartographer-events.sh +90 -0
- package/plugins/cartographer/scripts/lib/cartographer-filter.sh +105 -0
- package/plugins/cartographer/scripts/lib/cartographer-omission.sh +130 -0
- package/plugins/cartographer/scripts/lib/cartographer-resolve.sh +132 -0
- package/plugins/cartographer/scripts/run-audit.sh +136 -53
- package/plugins/cartographer/skills/cartographer/SKILL.md +33 -9
- package/plugins/curator/.claude-plugin/plugin.json +1 -1
- package/plugins/curator/CHANGELOG.md +7 -0
- package/plugins/curator/scripts/lib/curator-emit.sh +2 -1
- package/plugins/historian/.claude-plugin/plugin.json +1 -1
- package/plugins/historian/CHANGELOG.md +7 -0
- package/plugins/historian/scripts/lib/historian-emit.sh +2 -1
- package/plugins/librarian/.claude-plugin/plugin.json +1 -1
- package/plugins/librarian/CHANGELOG.md +71 -0
- package/plugins/librarian/config.json +30 -1
- package/plugins/librarian/docs/adr/002-agent-definitions-are-shared-assets.md +82 -0
- package/plugins/librarian/scripts/hooks/librarian-session-end.sh +28 -0
- package/plugins/librarian/scripts/hooks/librarian-session-start.sh +42 -12
- package/plugins/librarian/scripts/lib/librarian-author-key.sh +279 -0
- package/plugins/librarian/scripts/lib/librarian-cli.sh +346 -2
- package/plugins/librarian/scripts/lib/librarian-emit.sh +2 -1
- package/plugins/librarian/scripts/lib/librarian-lesson-judge.sh +349 -0
- package/plugins/librarian/scripts/lib/librarian-lesson-promote.sh +225 -0
- package/plugins/librarian/scripts/lib/librarian-lesson-review.sh +312 -0
- package/plugins/librarian/scripts/lib/librarian-lesson-rubric.sh +56 -0
- package/plugins/librarian/scripts/lib/librarian-lesson-storage.sh +82 -13
- package/plugins/librarian/scripts/lib/librarian-lesson-transform.sh +6 -1
- package/plugins/librarian/scripts/lib/librarian-lesson-validate.sh +131 -62
- package/plugins/librarian/skills/librarian/SKILL.md +98 -1
- package/plugins/tribunal/.claude-plugin/plugin.json +1 -1
- package/plugins/tribunal/CHANGELOG.md +63 -0
- package/plugins/tribunal/agents/tribunal-judge-adversarial.md +34 -0
- package/plugins/tribunal/agents/tribunal-judge-security.md +33 -0
- package/plugins/tribunal/agents/tribunal-judge-standard.md +12 -0
- package/plugins/tribunal/config.json +1 -0
- package/plugins/tribunal/scripts/lib/tribunal-aggregate.sh +154 -9
- package/plugins/tribunal/scripts/lib/tribunal-gate.sh +195 -16
- package/plugins/tribunal/skills/tribunal/SKILL.md +5 -3
- package/scripts/lib/prompt-rules.sh +6 -1
- package/test/bats/archivist-inject.bats +1 -1
- package/test/bats/assayer-extract.bats +2 -2
- package/test/bats/bursar-session-start.bats +3 -3
- package/test/bats/cartographer-config.bats +43 -0
- package/test/bats/cartographer-events.bats +267 -0
- package/test/bats/cartographer-filter.bats +169 -0
- package/test/bats/cartographer-lock.bats +3 -3
- package/test/bats/cartographer-omission.bats +202 -0
- package/test/bats/cartographer-resolve.bats +287 -0
- package/test/bats/cartographer-run-audit.bats +325 -0
- package/test/bats/compass-sanitizer.bats +11 -11
- package/test/bats/compass-transcript.bats +2 -2
- package/test/bats/config.bats +15 -15
- package/test/bats/curator-session-start.bats +10 -3
- package/test/bats/emit-payload-default.bats +52 -0
- package/test/bats/governor-ledger.bats +1 -1
- package/test/bats/historian-prompt-submit.bats +1 -1
- package/test/bats/inspector-post-write-hook.bats +4 -4
- package/test/bats/librarian-author-key.bats +477 -0
- package/test/bats/librarian-cli.bats +16 -16
- package/test/bats/librarian-lesson-judge.bats +884 -0
- package/test/bats/librarian-lesson-promote.bats +552 -0
- package/test/bats/librarian-lesson-review.bats +900 -0
- package/test/bats/librarian-lesson-transform.bats +17 -3
- package/test/bats/librarian-session-end.bats +161 -0
- package/test/bats/librarian-session-start.bats +36 -2
- package/test/bats/lineage-config.bats +1 -1
- package/test/bats/lineage-redact.bats +5 -5
- package/test/bats/session-tracker.bats +4 -4
- package/test/bats/tribunal-aggregate.bats +354 -1
- package/test/bats/tribunal-gate.bats +631 -0
- package/test/bats/tribunal-judge-agents.bats +119 -0
- package/test/bats/tribunal-jury.bats +1 -1
- package/test/bats/turn-tracker.bats +1 -1
- package/test/bats/warden-sanitizer.bats +3 -3
- package/test/bats/worktree-tracker.bats +2 -2
- package/test/node/lesson-validate-agreement.test.mjs +35 -6
|
@@ -15,27 +15,166 @@
|
|
|
15
15
|
# tribunal_disagreement <verdicts_json>
|
|
16
16
|
# echoes max(score) - min(score), or 0 if 0/1 verdicts
|
|
17
17
|
#
|
|
18
|
-
# weighted_mean uses *rubric criterion weights
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
18
|
+
# weighted_mean uses *rubric criterion weights*: average the judges' scores on
|
|
19
|
+
# each criterion, weight each criterion's mean, and normalize by the weights
|
|
20
|
+
# actually used. A criterion no judge scored contributes nothing and its weight
|
|
21
|
+
# is excluded from the denominator — absence is not a zero. When no criterion
|
|
22
|
+
# has any score (every verdict emitted before judges shipped criterion_scores),
|
|
23
|
+
# weighted_mean degrades to mean rather than collapsing to 0.
|
|
24
|
+
#
|
|
25
|
+
# Normalizing by the weights actually scored is only honest once the panel has
|
|
26
|
+
# covered enough of the rubric. The judge agents are told to omit any criterion
|
|
27
|
+
# they cannot assess, so a panel that scores one cheap criterion and skips the
|
|
28
|
+
# rest would otherwise produce an aggregate computed entirely from that one and
|
|
29
|
+
# renormalized to look complete — and .score, the judge's own overall verdict,
|
|
30
|
+
# is never read here to contradict it. Two guards, and weighted_mean degrades to
|
|
31
|
+
# the plain mean of .score — the panel's own summary judgment — on either:
|
|
32
|
+
#
|
|
33
|
+
# 1. Any criterion carrying a min_pass went unscored. A weight fraction is the
|
|
34
|
+
# wrong proxy for what a floor protects, because a floor protects one named
|
|
35
|
+
# criterion. In the shipped default rubric correctness (0.4) plus
|
|
36
|
+
# completeness (0.3) clears any coverage bar at or below 0.7 while skipping
|
|
37
|
+
# safety, the highest floor there is — which reproduced the exact inversion
|
|
38
|
+
# guard 2 was added to stop, one criterion further along.
|
|
39
|
+
# 2. The scored weight covers less than min_criterion_coverage of the rubric's
|
|
40
|
+
# total declared weight. Still load-bearing for rubrics that mix floored and
|
|
41
|
+
# unfloored criteria, where guard 1 has nothing to say.
|
|
42
|
+
#
|
|
43
|
+
# A criterion with no min_pass is exempt from guard 1 by design: absence is not a
|
|
44
|
+
# zero, and renormalizing over it stays correct. Only floors force the degrade,
|
|
45
|
+
# or every partial panel would collapse weighted_mean into mean.
|
|
46
|
+
#
|
|
47
|
+
# Degrading is deliberately not refusing — a blocked tribunal task retries. That
|
|
48
|
+
# is the asymmetry with librarian, which refuses (UNJUDGED) because a lesson
|
|
49
|
+
# published without its disclosure floor ever running is not recoverable.
|
|
50
|
+
|
|
51
|
+
# Echoes the panel with malformed verdicts removed, naming each on stderr.
|
|
52
|
+
#
|
|
53
|
+
# A judge that returns parseable JSON omitting `score` is malformed, not
|
|
54
|
+
# unanimous, but jq's `add` treats the missing key as null and `null + n == n`
|
|
55
|
+
# — so such a verdict contributed 0 to the numerator and 1 to the denominator,
|
|
56
|
+
# silently dragging the panel down rather than failing loudly. `min` was worse:
|
|
57
|
+
# it returned literal `null`, which the gate feeds to awk as the string "null"
|
|
58
|
+
# and coerces to 0.
|
|
59
|
+
#
|
|
60
|
+
# Dropping rather than refusing is deliberate. Tribunal has no UNJUDGED channel
|
|
61
|
+
# — tribunal_aggregate returns a float — so the choice is between dropping and
|
|
62
|
+
# inventing one. Dropping silently would be its own bug, hence the stderr line.
|
|
63
|
+
#
|
|
64
|
+
# A score outside [0,1] is malformed for the same reason a missing one is: it is
|
|
65
|
+
# not a verdict this scale can read. Dropping rather than clamping is what keeps
|
|
66
|
+
# one rule in this file — the criterion-score path in weighted_mean already
|
|
67
|
+
# filters to `number and >= 0 and <= 1` rather than clamping, and applying two
|
|
68
|
+
# different validity rules to the same concept is what let this through. The
|
|
69
|
+
# realistic trigger is a judge emitting a percentage instead of a fraction (95
|
|
70
|
+
# rather than 0.95), an ordinary LLM formatting slip. Nothing else at the seam
|
|
71
|
+
# rejects it: TribunalVerdictPayload declares score in [0,1], but the runtime
|
|
72
|
+
# emitter fails open (ADR-005), so this is the only place that re-checks.
|
|
73
|
+
#
|
|
74
|
+
# Bounds are inclusive. A judge that scored 0 rejected the work and a judge that
|
|
75
|
+
# scored 1 did not — dropping either would be its own fail-open, since 0 is
|
|
76
|
+
# precisely the verdict that has to survive to drag the panel below threshold.
|
|
77
|
+
#
|
|
78
|
+
# Private to this file. See ecosystem-up8 (missing) and ecosystem-7cl (range).
|
|
79
|
+
_tribunal_usable_verdicts() {
|
|
80
|
+
local verdicts="${1:-[]}"
|
|
81
|
+
local usable_def='def usable: (.score | type) == "number" and .score >= 0 and .score <= 1;'
|
|
82
|
+
local dropped
|
|
83
|
+
# One `def` reused by both passes, rather than the positive predicate here
|
|
84
|
+
# and a hand-negated copy below — that pair drifting is the whole bug class.
|
|
85
|
+
dropped=$(printf '%s' "$verdicts" | jq -r "$usable_def"'
|
|
86
|
+
[ .[]
|
|
87
|
+
| select(usable | not)
|
|
88
|
+
| (.judge_id // .judge_type // "unidentified") ] | join(", ")
|
|
89
|
+
' 2>/dev/null) || dropped=""
|
|
90
|
+
if [[ -n "$dropped" ]]; then
|
|
91
|
+
printf 'tribunal-aggregate: ignoring verdict(s) with no usable score (missing, non-numeric, or outside [0,1]): %s\n' \
|
|
92
|
+
"$dropped" >&2
|
|
93
|
+
fi
|
|
94
|
+
printf '%s' "$verdicts" | jq -c "$usable_def"'[ .[] | select(usable) ]' \
|
|
95
|
+
2>/dev/null || printf '[]'
|
|
96
|
+
}
|
|
24
97
|
|
|
25
98
|
tribunal_aggregate() {
|
|
26
99
|
local method="${1:-mean}"
|
|
27
100
|
local verdicts="${2:-[]}"
|
|
28
|
-
local
|
|
29
|
-
|
|
101
|
+
local rubric="${3:-}"
|
|
102
|
+
[ -z "$rubric" ] && rubric='{}'
|
|
103
|
+
|
|
104
|
+
# This lib is sourced standalone in tests and does not pull in
|
|
105
|
+
# tribunal-config.sh; fall back rather than adding a source line.
|
|
106
|
+
local min_coverage
|
|
107
|
+
if ! type tribunal_config_get >/dev/null 2>&1; then
|
|
108
|
+
min_coverage="0.6"
|
|
109
|
+
else
|
|
110
|
+
min_coverage=$(tribunal_config_get '.tribunal.rubric.min_criterion_coverage' 2>/dev/null)
|
|
111
|
+
fi
|
|
112
|
+
case "$min_coverage" in
|
|
113
|
+
''|null) min_coverage="0.6" ;;
|
|
114
|
+
esac
|
|
30
115
|
|
|
31
116
|
local count
|
|
32
117
|
count=$(printf '%s' "$verdicts" | jq 'length' 2>/dev/null) || count=0
|
|
33
118
|
[[ "$count" -eq 0 ]] && { printf '0'; return 0; }
|
|
34
119
|
|
|
120
|
+
# Malformed verdicts leave the panel before any method runs — including
|
|
121
|
+
# weighted_mean, which would otherwise still consume their criterion_scores.
|
|
122
|
+
# That is the sharpest form of this bug: once criterion_scores cover the
|
|
123
|
+
# rubric, weighted_mean never reads .score, so a judge that returned no
|
|
124
|
+
# verdict at all counted as a fully participating one, and it failed toward
|
|
125
|
+
# not blocking.
|
|
126
|
+
verdicts=$(_tribunal_usable_verdicts "$verdicts")
|
|
127
|
+
count=$(printf '%s' "$verdicts" | jq 'length' 2>/dev/null) || count=0
|
|
128
|
+
# No usable verdict aggregates to 0, matching the empty-panel line above:
|
|
129
|
+
# below every threshold, so the gate blocks rather than passing on nothing.
|
|
130
|
+
[[ "$count" -eq 0 ]] && { printf '0'; return 0; }
|
|
131
|
+
|
|
35
132
|
case "$method" in
|
|
36
|
-
mean
|
|
133
|
+
mean)
|
|
37
134
|
printf '%s' "$verdicts" | jq -r '[.[].score] | add / length'
|
|
38
135
|
;;
|
|
136
|
+
weighted_mean)
|
|
137
|
+
local weighted
|
|
138
|
+
weighted=$(printf '%s' "$verdicts" | jq -r \
|
|
139
|
+
--argjson rubric "$rubric" --argjson mincov "$min_coverage" '
|
|
140
|
+
. as $v
|
|
141
|
+
| ([ ($rubric.criteria // [])[]
|
|
142
|
+
| select((.weight | type) == "number") | .weight ] | add) as $total_w
|
|
143
|
+
| [ ($rubric.criteria // [])[]
|
|
144
|
+
| select((.name | type) == "string" and (.weight | type) == "number")
|
|
145
|
+
| . as $c
|
|
146
|
+
| ([ $v[]
|
|
147
|
+
| select((.criterion_scores | type) == "object")
|
|
148
|
+
| select(.criterion_scores | has($c.name))
|
|
149
|
+
| .criterion_scores[$c.name]
|
|
150
|
+
| select(type == "number" and . >= 0 and . <= 1) ]) as $scores
|
|
151
|
+
| select(($scores | length) > 0)
|
|
152
|
+
| { w: $c.weight, m: (($scores | add) / ($scores | length)) } ] as $covered
|
|
153
|
+
| [ ($rubric.criteria // [])[]
|
|
154
|
+
| select((.name | type) == "string" and (.min_pass | type) == "number")
|
|
155
|
+
| . as $c
|
|
156
|
+
| select([ $v[]
|
|
157
|
+
| select((.criterion_scores | type) == "object")
|
|
158
|
+
| select(.criterion_scores | has($c.name))
|
|
159
|
+
| .criterion_scores[$c.name]
|
|
160
|
+
| select(type == "number" and . >= 0 and . <= 1) ] | length == 0)
|
|
161
|
+
| $c.name ] as $unscored_floors
|
|
162
|
+
| ($covered | map(.w) | add) as $den
|
|
163
|
+
| if ($covered | length) == 0 or $den == null or $den <= 0 then empty
|
|
164
|
+
elif $total_w == null or $total_w <= 0 then empty
|
|
165
|
+
elif ($unscored_floors | length) > 0 then empty
|
|
166
|
+
elif ($den / $total_w) < $mincov then empty
|
|
167
|
+
else ($covered | map(.w * .m) | add) / $den
|
|
168
|
+
end
|
|
169
|
+
' 2>/dev/null)
|
|
170
|
+
if [ -n "$weighted" ]; then
|
|
171
|
+
printf '%s' "$weighted"
|
|
172
|
+
else
|
|
173
|
+
# No criterion carried a usable score, or the panel covered too
|
|
174
|
+
# little of the rubric to renormalize honestly — degrade to mean.
|
|
175
|
+
printf '%s' "$verdicts" | jq -r '[.[].score] | add / length'
|
|
176
|
+
fi
|
|
177
|
+
;;
|
|
39
178
|
median)
|
|
40
179
|
printf '%s' "$verdicts" | jq -r '
|
|
41
180
|
[.[].score] | sort as $s
|
|
@@ -58,6 +197,12 @@ tribunal_aggregate() {
|
|
|
58
197
|
|
|
59
198
|
tribunal_disagreement() {
|
|
60
199
|
local verdicts="${1:-[]}"
|
|
200
|
+
# Same filter as tribunal_aggregate, and this one crashed on a *single*
|
|
201
|
+
# scoreless verdict rather than needing a whole panel of them. A crash
|
|
202
|
+
# prints nothing to stdout, so the caller read an empty dissent score, awk
|
|
203
|
+
# coerced it to 0, and the gate's dissent short-circuit never fired —
|
|
204
|
+
# failing toward not blocking.
|
|
205
|
+
verdicts=$(_tribunal_usable_verdicts "$verdicts")
|
|
61
206
|
local count
|
|
62
207
|
count=$(printf '%s' "$verdicts" | jq 'length' 2>/dev/null) || count=0
|
|
63
208
|
[[ "$count" -lt 2 ]] && { printf '0'; return 0; }
|
|
@@ -15,31 +15,128 @@
|
|
|
15
15
|
#
|
|
16
16
|
# Echoes a JSON object: { passed: bool, reason?: string }
|
|
17
17
|
# reason is one of: low_score | meta_override | bias_detected | dissent_unresolved
|
|
18
|
+
# | criterion_floor (with failed_criterion naming the criterion)
|
|
18
19
|
#
|
|
19
|
-
# Usage: result=$(tribunal_gate_decide "$policy" "$verdicts" "$agg" "$thr" "$meta" "$dissent" "$dissent_thr")
|
|
20
|
+
# Usage: result=$(tribunal_gate_decide "$policy" "$verdicts" "$agg" "$thr" "$meta" "$dissent" "$dissent_thr" "$rubric")
|
|
21
|
+
|
|
22
|
+
# Names the first criterion sitting below its min_pass, or echoes nothing.
|
|
23
|
+
#
|
|
24
|
+
# The *lowest* score among the judges that scored it, not the mean: a floor
|
|
25
|
+
# means nobody may be below it. Using the mean let a specialist's finding be
|
|
26
|
+
# diluted by generalists who did not look — with safety floored at 0.8, a
|
|
27
|
+
# security judge scoring 0.6 against two generalists at 0.95 averaged to 0.83
|
|
28
|
+
# and passed, so empanelling more judges made every floor weaker. Weighting
|
|
29
|
+
# still uses the mean; only the floor is a minimum.
|
|
30
|
+
#
|
|
31
|
+
# A criterion no judge scored is NOT a violation: absence is not a zero, and
|
|
32
|
+
# treating it as one would fail every verdict emitted before judges shipped
|
|
33
|
+
# criterion_scores. tribunal_aggregate degrades weighted_mean to the plain mean
|
|
34
|
+
# when that happens, and the caller warns on stderr.
|
|
35
|
+
#
|
|
36
|
+
# A criterion a judge DID score but scored unreadably — outside [0,1], or not a
|
|
37
|
+
# number — is a different thing, and it is a violation. Absence means the judge
|
|
38
|
+
# declined to assess (the agents are told to omit what they cannot judge);
|
|
39
|
+
# unreadable means they assessed it and the answer did not survive. A floor is
|
|
40
|
+
# the rubric's hard constraint, so "cannot confirm this held" must fail closed.
|
|
41
|
+
#
|
|
42
|
+
# ANY unreadable report violates, not merely an all-unreadable criterion, and
|
|
43
|
+
# that follows from why the floor uses min rather than mean in the first place:
|
|
44
|
+
# a specialist's finding must not be diluted by generalists who did not look.
|
|
45
|
+
# Merely filtering the bad value reproduces the exact dilution this design was
|
|
46
|
+
# built to prevent — a security judge emitting 30 (plausibly 0.30, a violation)
|
|
47
|
+
# would be discarded so two generalists at 0.95 carry the floor.
|
|
48
|
+
#
|
|
49
|
+
# The range test matches _tribunal_usable_verdicts in tribunal-aggregate.sh and
|
|
50
|
+
# the criterion filter in weighted_mean. Those disagreeing is what let an
|
|
51
|
+
# out-of-range score satisfy a floor while aggregation refused to score it at
|
|
52
|
+
# all — see ecosystem-5fy.
|
|
53
|
+
#
|
|
54
|
+
# Private to this file. It exists as a function because the answer is needed in
|
|
55
|
+
# two places — once to veto `meta_override: accept`, once for the blocking
|
|
56
|
+
# precedence chain — and duplicating the jq is how the two drift apart.
|
|
57
|
+
_tribunal_floor_failed() {
|
|
58
|
+
local verdicts="${1:-[]}"
|
|
59
|
+
local rubric="${2:-}"
|
|
60
|
+
[ -z "$rubric" ] && rubric='{}'
|
|
61
|
+
printf '%s' "$verdicts" | jq -r --argjson rubric "$rubric" '
|
|
62
|
+
def readable: (type == "number") and . >= 0 and . <= 1;
|
|
63
|
+
. as $v
|
|
64
|
+
| [ ($rubric.criteria // [])[]
|
|
65
|
+
| select((.name | type) == "string" and (.min_pass | type) == "number")
|
|
66
|
+
| . as $c
|
|
67
|
+
| ([ $v[]
|
|
68
|
+
| select((.criterion_scores | type) == "object")
|
|
69
|
+
| select(.criterion_scores | has($c.name))
|
|
70
|
+
| .criterion_scores[$c.name] ]) as $reported
|
|
71
|
+
| ([ $reported[] | select(readable) ]) as $scores
|
|
72
|
+
| select(
|
|
73
|
+
# somebody reported a score this scale cannot read...
|
|
74
|
+
($reported | length) > ($scores | length)
|
|
75
|
+
# ...or the readable ones put somebody under the floor.
|
|
76
|
+
or (($scores | length) > 0 and ($scores | min) < $c.min_pass)
|
|
77
|
+
)
|
|
78
|
+
| $c.name ]
|
|
79
|
+
| first // empty
|
|
80
|
+
' 2>/dev/null || printf ''
|
|
81
|
+
}
|
|
20
82
|
|
|
21
83
|
tribunal_gate_decide() {
|
|
22
84
|
local policy="${1:-majority}"
|
|
23
85
|
local verdicts="${2:-[]}"
|
|
24
86
|
local aggregated_score="${3:-0}"
|
|
25
87
|
local score_threshold="${4:-0.75}"
|
|
26
|
-
local meta="${5:-
|
|
88
|
+
local meta="${5:-}"
|
|
89
|
+
[ -z "$meta" ] && meta='{}'
|
|
27
90
|
local dissent_score="${6:-0}"
|
|
28
91
|
local dissent_threshold="${7:-0.25}"
|
|
92
|
+
local rubric="${8:-}"
|
|
93
|
+
[ -z "$rubric" ] && rubric='{}'
|
|
29
94
|
|
|
30
95
|
local meta_bias_detected meta_override
|
|
31
96
|
meta_bias_detected=$(printf '%s' "$meta" | jq -r '.bias_detected // false' 2>/dev/null)
|
|
32
97
|
meta_override=$(printf '%s' "$meta" | jq -r '.override_recommendation // empty' 2>/dev/null)
|
|
33
98
|
|
|
99
|
+
# Which criterion, if any, sits below its floor. Computed here rather than
|
|
100
|
+
# beside the other blocking reasons because `meta_override: accept` returns
|
|
101
|
+
# before them, and a floor the Meta-Judge can lift is not a floor. See the
|
|
102
|
+
# longer note on min-not-mean further down.
|
|
103
|
+
local floor_failed
|
|
104
|
+
floor_failed=$(_tribunal_floor_failed "$verdicts" "$rubric")
|
|
105
|
+
|
|
106
|
+
# A violated floor is worth naming on whichever arm blocks: the worst
|
|
107
|
+
# failures drag the aggregate under the threshold too, so attaching
|
|
108
|
+
# failed_criterion only to the criterion_floor arm left the diagnostic
|
|
109
|
+
# absent exactly where it mattered most.
|
|
110
|
+
#
|
|
111
|
+
# Built HERE, beside floor_failed, rather than down with the final chain.
|
|
112
|
+
# It used to be defined below the short-circuit returns, which could
|
|
113
|
+
# therefore never reference it however much they should have — so
|
|
114
|
+
# dissent_unresolved carried the criterion from the chain arm and dropped it
|
|
115
|
+
# from the short-circuit, one reason string with two behaviors. See
|
|
116
|
+
# ecosystem-973. Every blocking return below now decorates, and the test
|
|
117
|
+
# "every blocking arm that can see a violated floor names it" holds a newly
|
|
118
|
+
# added arm to the same rule without anyone having to remember.
|
|
119
|
+
local floor_suffix=""
|
|
120
|
+
[[ -n "$floor_failed" ]] && floor_suffix=$(printf ',"failed_criterion":"%s"' "$floor_failed")
|
|
121
|
+
|
|
34
122
|
# meta_override policy: the Meta-Judge wins, regardless of jury.
|
|
35
123
|
if [[ "$policy" == "meta_override" ]]; then
|
|
36
124
|
case "$meta_override" in
|
|
37
125
|
accept)
|
|
126
|
+
# ...but not regardless of the rubric. `accept` overrules the
|
|
127
|
+
# jury's *opinion*; a floor is the rubric's hard constraint, so
|
|
128
|
+
# it survives. Every other short-circuit below already blocks,
|
|
129
|
+
# which made this the one arm that could turn a violated floor
|
|
130
|
+
# into a pass.
|
|
131
|
+
if [[ -n "$floor_failed" ]]; then
|
|
132
|
+
printf '{"passed":false,"reason":"criterion_floor","failed_criterion":"%s"}' "$floor_failed"
|
|
133
|
+
return 0
|
|
134
|
+
fi
|
|
38
135
|
printf '{"passed":true}'
|
|
39
136
|
return 0
|
|
40
137
|
;;
|
|
41
138
|
reject)
|
|
42
|
-
printf '{"passed":false,"reason":"meta_override"}'
|
|
139
|
+
printf '{"passed":false,"reason":"meta_override"%s}' "$floor_suffix"
|
|
43
140
|
return 0
|
|
44
141
|
;;
|
|
45
142
|
re-evaluate|"")
|
|
@@ -50,7 +147,7 @@ tribunal_gate_decide() {
|
|
|
50
147
|
|
|
51
148
|
# Bias detection short-circuit (any policy).
|
|
52
149
|
if [[ "$meta_bias_detected" == "true" && "$meta_override" == "reject" ]]; then
|
|
53
|
-
printf '{"passed":false,"reason":"bias_detected"}'
|
|
150
|
+
printf '{"passed":false,"reason":"bias_detected"%s}' "$floor_suffix"
|
|
54
151
|
return 0
|
|
55
152
|
fi
|
|
56
153
|
|
|
@@ -59,13 +156,36 @@ tribunal_gate_decide() {
|
|
|
59
156
|
# retries with a fresh Actor pass.
|
|
60
157
|
if awk -v d="$dissent_score" -v t="$dissent_threshold" 'BEGIN { exit !(d > t) }' \
|
|
61
158
|
&& [[ -z "$meta_override" || "$meta_override" == "re-evaluate" ]]; then
|
|
62
|
-
printf '{"passed":false,"reason":"dissent_unresolved"}'
|
|
159
|
+
printf '{"passed":false,"reason":"dissent_unresolved"%s}' "$floor_suffix"
|
|
63
160
|
return 0
|
|
64
161
|
fi
|
|
65
162
|
|
|
163
|
+
# A verdict tribunal_aggregate refuses to score cannot vote to approve, but
|
|
164
|
+
# its SEAT STILL COUNTS. The usability test below is the same one
|
|
165
|
+
# _tribunal_usable_verdicts applies in tribunal-aggregate.sh — a numeric
|
|
166
|
+
# score inside [0,1], so a judge emitting 95 instead of 0.95 is as unusable
|
|
167
|
+
# as one that emitted nothing. The two copies must stay in step, and
|
|
168
|
+
# "usable verdicts agree with tribunal_aggregate" in the bats file is what
|
|
169
|
+
# catches them drifting.
|
|
170
|
+
#
|
|
171
|
+
# Denying the vote rather than dropping the verdict is deliberate, and it
|
|
172
|
+
# is NOT what ecosystem-y7y originally asked for. Making both halves agree
|
|
173
|
+
# on panel membership sounds right and loosens the gate: dropping shrinks
|
|
174
|
+
# the panel, and a shrunken panel is a trivially satisfied majority. One
|
|
175
|
+
# real approval beside two verdicts that never arrived would become a
|
|
176
|
+
# 1-of-1 pass, and strict would clear on a panel that never fully reported
|
|
177
|
+
# — the hazard librarian's judge-type check exists to prevent. Measured
|
|
178
|
+
# across the five reachable cases, dropping fixed one and loosened three.
|
|
179
|
+
#
|
|
180
|
+
# A judge that failed to return a verdict did not leave the panel; it
|
|
181
|
+
# failed. So majority still needs a majority of everyone empaneled, and
|
|
182
|
+
# strict still needs all of them.
|
|
66
183
|
local count passed_count
|
|
67
184
|
count=$(printf '%s' "$verdicts" | jq 'length' 2>/dev/null) || count=0
|
|
68
|
-
passed_count=$(printf '%s' "$verdicts" | jq '
|
|
185
|
+
passed_count=$(printf '%s' "$verdicts" | jq '
|
|
186
|
+
def usable: (.score | type) == "number" and .score >= 0 and .score <= 1;
|
|
187
|
+
[.[] | select(.passed == true and usable)] | length
|
|
188
|
+
' 2>/dev/null) || passed_count=0
|
|
69
189
|
|
|
70
190
|
local jury_ok=1 # 0 = ok, 1 = not ok (shell convention)
|
|
71
191
|
case "$policy" in
|
|
@@ -91,21 +211,80 @@ tribunal_gate_decide() {
|
|
|
91
211
|
local score_ok=1
|
|
92
212
|
awk -v s="$aggregated_score" -v t="$score_threshold" 'BEGIN { exit !(s >= t) }' && score_ok=0
|
|
93
213
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
214
|
+
# A floor nobody scored is a silent hole in the rubric, so say so. The gate
|
|
215
|
+
# does not block on it — see _tribunal_floor_failed for why absence is not a
|
|
216
|
+
# zero — but tribunal_aggregate has already refused to renormalize around it,
|
|
217
|
+
# so the aggregate this gate just judged is the panel's plain mean.
|
|
218
|
+
#
|
|
219
|
+
# ABSENCE ONLY: this names criteria nobody reported, not ones reported
|
|
220
|
+
# unreadably. Those now violate the floor and block, so naming them here
|
|
221
|
+
# would print "their min_pass floors did not apply" about a floor that did
|
|
222
|
+
# apply and did block. The two also cannot share a predicate: conflating
|
|
223
|
+
# them meant the sole signal for "a floor got skipped" was emitted by the
|
|
224
|
+
# very defect that skipped it, which is how this stayed quiet.
|
|
225
|
+
local unscored_floors any_scored
|
|
226
|
+
any_scored=$(printf '%s' "$verdicts" | jq -r '
|
|
227
|
+
[.[] | select((.criterion_scores | type) == "object")
|
|
228
|
+
| select((.criterion_scores | length) > 0)] | length > 0
|
|
229
|
+
' 2>/dev/null) || any_scored="false"
|
|
230
|
+
|
|
231
|
+
if [[ "$any_scored" == "true" ]]; then
|
|
232
|
+
unscored_floors=$(printf '%s' "$verdicts" | jq -r --argjson rubric "$rubric" '
|
|
233
|
+
. as $v
|
|
234
|
+
| [ ($rubric.criteria // [])[]
|
|
235
|
+
| select((.name | type) == "string" and (.min_pass | type) == "number")
|
|
236
|
+
| . as $c
|
|
237
|
+
| select([ $v[]
|
|
238
|
+
| select((.criterion_scores | type) == "object")
|
|
239
|
+
| select(.criterion_scores | has($c.name))
|
|
240
|
+
| .criterion_scores[$c.name] ] | length == 0)
|
|
241
|
+
| $c.name ]
|
|
242
|
+
| join(", ")
|
|
243
|
+
' 2>/dev/null) || unscored_floors=""
|
|
244
|
+
if [[ -n "$unscored_floors" ]]; then
|
|
245
|
+
printf 'tribunal-gate: no judge scored these criteria, so their min_pass floors did not apply: %s\n' \
|
|
246
|
+
"$unscored_floors" >&2
|
|
247
|
+
fi
|
|
97
248
|
fi
|
|
98
249
|
|
|
99
|
-
# Pick the most informative blocking reason.
|
|
250
|
+
# Pick the most informative blocking reason. Every arm here blocks — this
|
|
251
|
+
# chain only decides what the Actor is TOLD, never whether the work passes.
|
|
252
|
+
# Keeping those two apart is the whole basis for the order:
|
|
253
|
+
#
|
|
254
|
+
# low_score — the aggregate missed the threshold, which is more
|
|
255
|
+
# actionable than any single criterion.
|
|
256
|
+
# jury arms — WHY the panel failed. A Meta-Judge rejection is
|
|
257
|
+
# the more useful thing to hand back on retry than
|
|
258
|
+
# which criterion sat low, and the floor still rides
|
|
259
|
+
# along as failed_criterion.
|
|
260
|
+
# criterion_floor — no jury complaint, so the floor IS the complaint.
|
|
261
|
+
#
|
|
262
|
+
# The jury arms sit ahead of criterion_floor deliberately (ecosystem-4d3),
|
|
263
|
+
# matching librarian's shipped decision — "lesson gate: jury policy still
|
|
264
|
+
# wins over criterion_floor". This does not weaken floors: a violated floor
|
|
265
|
+
# still blocks on the arm below, and `meta_override: accept` still cannot
|
|
266
|
+
# lift one, because that veto lives further up and returns before any of
|
|
267
|
+
# this. What changed is only that a floor no longer HIDES a rejection.
|
|
268
|
+
#
|
|
269
|
+
# Ordering it the other way is what made floor_suffix on the jury arms dead
|
|
270
|
+
# code (ecosystem-cs8): behind `elif [[ -n $floor_failed ]]`, floor_failed
|
|
271
|
+
# was guaranteed empty by the time those arms ran, so the suffix always
|
|
272
|
+
# rendered as the empty string while two shipped docs described it working.
|
|
273
|
+
#
|
|
274
|
+
# The tests under "blocking-reason precedence" pin every arm. Nothing pinned
|
|
275
|
+
# them before, which is how a new arm went in ahead of two others under a
|
|
276
|
+
# comment asserting the precedence was unchanged.
|
|
100
277
|
if [[ "$score_ok" -ne 0 ]]; then
|
|
101
|
-
printf '{"passed":false,"reason":"low_score"}'
|
|
102
|
-
|
|
103
|
-
# Jury did not pass even though score cleared threshold — surface as
|
|
104
|
-
# meta_override when meta said reject, else dissent_unresolved.
|
|
278
|
+
printf '{"passed":false,"reason":"low_score"%s}' "$floor_suffix"
|
|
279
|
+
elif [[ "$jury_ok" -ne 0 ]]; then
|
|
105
280
|
if [[ "$meta_override" == "reject" ]]; then
|
|
106
|
-
printf '{"passed":false,"reason":"meta_override"}'
|
|
281
|
+
printf '{"passed":false,"reason":"meta_override"%s}' "$floor_suffix"
|
|
107
282
|
else
|
|
108
|
-
printf '{"passed":false,"reason":"dissent_unresolved"}'
|
|
283
|
+
printf '{"passed":false,"reason":"dissent_unresolved"%s}' "$floor_suffix"
|
|
109
284
|
fi
|
|
285
|
+
elif [[ -n "$floor_failed" ]]; then
|
|
286
|
+
printf '{"passed":false,"reason":"criterion_floor","failed_criterion":"%s"}' "$floor_failed"
|
|
287
|
+
else
|
|
288
|
+
printf '{"passed":true}'
|
|
110
289
|
fi
|
|
111
290
|
}
|
|
@@ -117,9 +117,11 @@ For `iteration_number` from `0` while `iteration_number < max_iterations`:
|
|
|
117
117
|
7. **Gate.**
|
|
118
118
|
```bash
|
|
119
119
|
policy=$(printf '%s' "$rubric" | jq -r '.gate_policy // "majority"')
|
|
120
|
-
gate=$(tribunal_gate_decide "$policy" "$verdicts" "$aggregated" "$threshold" "$meta" "$dissent" "$dissent_threshold")
|
|
120
|
+
gate=$(tribunal_gate_decide "$policy" "$verdicts" "$aggregated" "$threshold" "$meta" "$dissent" "$dissent_threshold" "$rubric")
|
|
121
121
|
```
|
|
122
|
-
If `gate.passed == true`, emit `tribunal.gate.passed` with `final_score: aggregated` and break the loop with outcome `accepted`. Otherwise emit `tribunal.gate.blocked` with the `reason`, `will_retry: (iteration_number + 1 < max_iterations)`, and `retry_iteration_number` if retrying. Persist `gate.json` either way.
|
|
122
|
+
If `gate.passed == true`, emit `tribunal.gate.passed` with `final_score: aggregated` and break the loop with outcome `accepted`. Otherwise emit `tribunal.gate.blocked` with the `reason`, `will_retry: (iteration_number + 1 < max_iterations)`, and `retry_iteration_number` if retrying. **Whenever `gate.failed_criterion` is present, copy it onto the event payload as `failed_criterion` — whatever the `reason` is.** The gate names the criterion on **every** blocking arm, not only when `criterion_floor` won the precedence contest: a criterion scored far below its floor also drags the aggregate under `score_threshold`, so the *worst* failures surface as `low_score`. Copying it only on `criterion_floor` drops the diagnostic precisely where it matters most. That now holds for the Meta-Judge's short-circuit returns too (`bias_detected`, and the early `meta_override` / `dissent_unresolved` arms), so an absent `failed_criterion` means no floor was violated — one reason string never reports two different ways depending on which return produced it. Persist `gate.json` either way.
|
|
123
|
+
|
|
124
|
+
A `criterion_floor` block means the aggregate *cleared* its threshold and one criterion still failed its floor. Say so when you report it: "blocked on `safety` (0.30 < 0.80) despite an overall 0.82" is actionable, "blocked" is not.
|
|
123
125
|
|
|
124
126
|
If blocking and retrying, build the retry digest (lowest-scoring criteria + meta override + dissent summary) and feed it into the next iteration's Actor prompt.
|
|
125
127
|
|
|
@@ -146,6 +148,6 @@ Keep the summary terse. The artifacts on disk are the long form.
|
|
|
146
148
|
|
|
147
149
|
## Error handling
|
|
148
150
|
|
|
149
|
-
- If a judge subagent fails to return parseable JSON, treat that judge as `score: 0, passed: false, confidence: 0` and
|
|
151
|
+
- If a judge subagent fails to return parseable JSON, treat that judge as `score: 0, passed: false, confidence: 0` **and give it a `criterion_scores` object assigning `0` to every criterion in the active rubric.** Surface the parse error in `feedback_summary`. Do not abort the iteration — let the gate decide. A crashed judge was dispatched and produced nothing — that is the "assessed and failed" case, not the "did not assess" case. Omitting `criterion_scores` (or sending `{}`) would make the crash invisible to `weighted_mean`, which never reads `.score`, and the panel would score as though the judge had never been empaneled.
|
|
150
152
|
- If the Meta-Judge fails, default to `verdict_quality: "questionable", bias_detected: false` so the gate falls back to score-based logic.
|
|
151
153
|
- If event emission fails (schema validation), keep going and write a warning to stderr. The persisted artifacts on disk are still trustworthy.
|
|
@@ -148,7 +148,12 @@ prompt_rules_pattern_matches() {
|
|
|
148
148
|
prompt_rules_emit() {
|
|
149
149
|
local session_id="${1:-unknown}"
|
|
150
150
|
local event_type="${2:-}"
|
|
151
|
-
|
|
151
|
+
# Default the payload without inlining braces in the expansion. Both
|
|
152
|
+
# `${3:-{\}}` and `${3:-{}}` are wrong: the first keeps the backslash on
|
|
153
|
+
# bash 3.2 (macOS system bash), and the second appends a stray `}` to any
|
|
154
|
+
# payload that IS supplied, on every bash version.
|
|
155
|
+
local payload_json="${3:-}"
|
|
156
|
+
[ -z "$payload_json" ] && payload_json='{}'
|
|
152
157
|
[[ -z "$event_type" ]] && return 1
|
|
153
158
|
ensure_file_exists "$ONLOOKER_EVENTS_LOG" || return 1
|
|
154
159
|
|
|
@@ -46,7 +46,7 @@ setup() {
|
|
|
46
46
|
echo "$output" | jq -e '.hookSpecificOutput.hookEventName == "SessionStart"' >/dev/null
|
|
47
47
|
local ctx
|
|
48
48
|
ctx=$(echo "$output" | jq -r '.hookSpecificOutput.additionalContext')
|
|
49
|
-
[[ "$ctx" == *"use git remote SHA256 as project key"* ]]
|
|
49
|
+
[[ "$ctx" == *"use git remote SHA256 as project key"* ]] || return 1
|
|
50
50
|
[[ "$ctx" == *"Archivist injected 1"* ]]
|
|
51
51
|
}
|
|
52
52
|
|
|
@@ -70,7 +70,7 @@ setup() {
|
|
|
70
70
|
@test "extraction prompt includes the message and the JSON contract" {
|
|
71
71
|
run assayer_build_extraction_prompt "I ran the tests and they pass." 5
|
|
72
72
|
[ "$status" -eq 0 ]
|
|
73
|
-
[[ "$output" == *"I ran the tests and they pass."* ]]
|
|
74
|
-
[[ "$output" == *"TESTABLE SUCCESS CLAIM"* ]]
|
|
73
|
+
[[ "$output" == *"I ran the tests and they pass."* ]] || return 1
|
|
74
|
+
[[ "$output" == *"TESTABLE SUCCESS CLAIM"* ]] || return 1
|
|
75
75
|
[[ "$output" == *"at most 5 claims"* ]]
|
|
76
76
|
}
|
|
@@ -68,8 +68,8 @@ _run_hook() {
|
|
|
68
68
|
_seed_ledger
|
|
69
69
|
_run_hook
|
|
70
70
|
[ "$status" -eq 0 ]
|
|
71
|
-
[[ "$output" == *"hookSpecificOutput"* ]]
|
|
72
|
-
[[ "$output" == *"SessionStart"* ]]
|
|
71
|
+
[[ "$output" == *"hookSpecificOutput"* ]] || return 1
|
|
72
|
+
[[ "$output" == *"SessionStart"* ]] || return 1
|
|
73
73
|
[[ "$output" == *"burned \$0.42"* ]]
|
|
74
74
|
}
|
|
75
75
|
|
|
@@ -85,7 +85,7 @@ _run_hook() {
|
|
|
85
85
|
> "${dir}/sessions.jsonl"
|
|
86
86
|
_run_hook
|
|
87
87
|
[ "$status" -eq 0 ]
|
|
88
|
-
[[ "$output" == *"burned \$0.00"* ]]
|
|
88
|
+
[[ "$output" == *"burned \$0.00"* ]] || return 1
|
|
89
89
|
[[ "$output" != *"Enable governor"* ]]
|
|
90
90
|
}
|
|
91
91
|
|
|
@@ -80,3 +80,46 @@ setup() {
|
|
|
80
80
|
echo "$v" | jq -e 'type == "array" and length > 0' >/dev/null
|
|
81
81
|
echo "$v" | jq -e 'any(. == "node_modules")' >/dev/null
|
|
82
82
|
}
|
|
83
|
+
|
|
84
|
+
@test "undocumented_entity: defaults ship enabled with plugin and skill globs" {
|
|
85
|
+
cartographer_config_load ""
|
|
86
|
+
local enabled globs max
|
|
87
|
+
enabled=$(cartographer_config_undocumented_enabled)
|
|
88
|
+
globs=$(cartographer_config_undocumented_globs)
|
|
89
|
+
max=$(cartographer_config_undocumented_max_findings)
|
|
90
|
+
[ "$enabled" = "true" ]
|
|
91
|
+
[ "$max" = "20" ]
|
|
92
|
+
[ "$(printf '%s' "$globs" | jq -r 'length')" = "2" ] || return 1
|
|
93
|
+
[ "$(printf '%s' "$globs" | jq -r '.[0]')" = "plugins/*/" ]
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
@test "undocumented_entity: user settings can disable the phase" {
|
|
97
|
+
mkdir -p "${HOME}/.claude"
|
|
98
|
+
printf '%s\n' '{"cartographer":{"undocumented_entity":{"enabled":false}}}' \
|
|
99
|
+
> "${HOME}/.claude/settings.json"
|
|
100
|
+
cartographer_config_load ""
|
|
101
|
+
local enabled max
|
|
102
|
+
enabled=$(cartographer_config_undocumented_enabled)
|
|
103
|
+
max=$(cartographer_config_undocumented_max_findings)
|
|
104
|
+
[ "$enabled" = "false" ]
|
|
105
|
+
[ "$max" = "20" ]
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
@test "undocumented_entity: repo settings replace the glob list wholesale" {
|
|
109
|
+
local repo="${BATS_TEST_TMPDIR}/repo-glob"
|
|
110
|
+
mkdir -p "${repo}/.claude"
|
|
111
|
+
printf '%s\n' '{"cartographer":{"undocumented_entity":{"globs":["agents/*/"]}}}' \
|
|
112
|
+
> "${repo}/.claude/settings.json"
|
|
113
|
+
cartographer_config_load "$repo"
|
|
114
|
+
local globs
|
|
115
|
+
globs=$(cartographer_config_undocumented_globs)
|
|
116
|
+
[ "$(printf '%s' "$globs" | jq -r 'length')" = "1" ] || return 1
|
|
117
|
+
[ "$(printf '%s' "$globs" | jq -r '.[0]')" = "agents/*/" ]
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
@test "undocumented_entity: exclude defaults to empty" {
|
|
121
|
+
cartographer_config_load ""
|
|
122
|
+
local excl
|
|
123
|
+
excl=$(cartographer_config_undocumented_exclude)
|
|
124
|
+
[ "$(printf '%s' "$excl" | jq -r 'length')" = "0" ]
|
|
125
|
+
}
|