@onlooker-community/ecosystem 0.34.1 → 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-plugin/plugin.json +1 -1
- package/.github/workflows/release.yml +14 -14
- package/.release-please-manifest.json +4 -4
- package/AGENTS.md +5 -0
- package/CHANGELOG.md +168 -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/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/librarian/.claude-plugin/plugin.json +1 -1
- package/plugins/librarian/CHANGELOG.md +64 -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 +344 -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 +56 -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 -11
- package/plugins/tribunal/scripts/lib/tribunal-gate.sh +193 -15
- package/plugins/tribunal/skills/tribunal/SKILL.md +5 -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-omission.bats +202 -0
- package/test/bats/cartographer-resolve.bats +287 -0
- package/test/bats/cartographer-run-audit.bats +325 -0
- package/test/bats/librarian-author-key.bats +477 -0
- 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 +34 -0
- 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/node/lesson-validate-agreement.test.mjs +35 -6
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
# 1. discover — collect all auditable files
|
|
9
9
|
# 2. extract — per-file content hash (incremental cache key)
|
|
10
10
|
# 3. relate — contradiction + dead_rule analysis (LLM)
|
|
11
|
-
# 4. synthesize — stale_ref + scope_collision +
|
|
11
|
+
# 4. synthesize — stale_ref + scope_collision + undocumented_entity + hash
|
|
12
12
|
# 5. emit — persist findings atomically, emit events for new findings
|
|
13
13
|
#
|
|
14
14
|
# Environment:
|
|
@@ -28,13 +28,18 @@ source "$PLUGIN_ROOT/scripts/lib/cartographer-config.sh"
|
|
|
28
28
|
source "$PLUGIN_ROOT/scripts/lib/cartographer-ulid.sh"
|
|
29
29
|
source "$PLUGIN_ROOT/scripts/lib/cartographer-project-key.sh"
|
|
30
30
|
source "$PLUGIN_ROOT/scripts/lib/cartographer-events.sh"
|
|
31
|
+
source "$PLUGIN_ROOT/scripts/lib/cartographer-resolve.sh"
|
|
32
|
+
source "$PLUGIN_ROOT/scripts/lib/cartographer-filter.sh"
|
|
31
33
|
source "$PLUGIN_ROOT/scripts/lib/cartographer-collect.sh"
|
|
32
34
|
source "$PLUGIN_ROOT/scripts/lib/cartographer-analyze.sh"
|
|
35
|
+
source "$PLUGIN_ROOT/scripts/lib/cartographer-omission.sh"
|
|
33
36
|
|
|
34
37
|
CARTOGRAPHER_DIR="${CARTOGRAPHER_DIR:?CARTOGRAPHER_DIR must be set}"
|
|
35
38
|
TRIGGER="${CARTOGRAPHER_TRIGGER:-manual}"
|
|
36
39
|
TARGET_FILE="${CARTOGRAPHER_TARGET_FILE:-}"
|
|
37
40
|
REPO_ROOT="${CARTOGRAPHER_REPO_ROOT:-$(pwd)}"
|
|
41
|
+
TYPE_FILTER="${CARTOGRAPHER_TYPE_FILTER:-}"
|
|
42
|
+
SCOPE_PATH="${CARTOGRAPHER_SCOPE_PATH:-}"
|
|
38
43
|
AUDIT_ID=$(cartographer_ulid)
|
|
39
44
|
START_TS=$(date +%s)
|
|
40
45
|
|
|
@@ -49,6 +54,16 @@ PHASES_COMPLETED=()
|
|
|
49
54
|
PHASES_FAILED=()
|
|
50
55
|
ALL_FINDINGS="[]"
|
|
51
56
|
|
|
57
|
+
# Load config before any accessor runs. Without this every accessor below falls
|
|
58
|
+
# through to its hardcoded default, so exclude_paths, the timeouts, the models,
|
|
59
|
+
# and the token caps were all silently unconfigurable — the orchestrator was the
|
|
60
|
+
# one place that never loaded (ecosystem-88v).
|
|
61
|
+
#
|
|
62
|
+
# This is the single load for the whole audit. The analysis sub-shells take
|
|
63
|
+
# their settings as positional parameters and read no config of their own, so
|
|
64
|
+
# the values resolved here are the ones that take effect everywhere.
|
|
65
|
+
cartographer_config_load "$REPO_ROOT"
|
|
66
|
+
|
|
52
67
|
_phase_timeout=$(cartographer_config_phase_timeout)
|
|
53
68
|
_total_timeout=$(cartographer_config_total_timeout)
|
|
54
69
|
log() { printf '[cartographer] %s\n' "$*" >>"$CARTOGRAPHER_DIR/audit.log" 2>&1; }
|
|
@@ -59,11 +74,36 @@ _model_synthesis=$(cartographer_config_model_synthesis)
|
|
|
59
74
|
_max_tokens_extraction=$(cartographer_config_max_output_tokens_extraction)
|
|
60
75
|
_max_tokens_synthesis=$(cartographer_config_max_output_tokens_synthesis)
|
|
61
76
|
_exclude_json=$(cartographer_config_exclude_paths)
|
|
77
|
+
_undocumented_enabled=$(cartographer_config_undocumented_enabled)
|
|
78
|
+
_undocumented_globs=$(cartographer_config_undocumented_globs)
|
|
79
|
+
_undocumented_exclude=$(cartographer_config_undocumented_exclude)
|
|
80
|
+
_undocumented_max=$(cartographer_config_undocumented_max_findings)
|
|
62
81
|
|
|
63
82
|
emit_safe() {
|
|
64
83
|
cartographer_emit_event "$1" "$2" 2>>"$CARTOGRAPHER_DIR/audit.log" || true
|
|
65
84
|
}
|
|
66
85
|
|
|
86
|
+
# Announce one finding the resolution sweep retired.
|
|
87
|
+
#
|
|
88
|
+
# Passed to cartographer_resolve_absent_findings by name rather than called
|
|
89
|
+
# after it, so the announcement happens inside the sweep's own guards — a run
|
|
90
|
+
# that must not resolve cannot announce either. Handing over a function name
|
|
91
|
+
# also keeps cartographer-resolve.sh free of any dependency on the event bus.
|
|
92
|
+
_emit_resolved() {
|
|
93
|
+
emit_safe "cartographer.issue.resolved" \
|
|
94
|
+
"$(cartographer_issue_resolved_payload "$AUDIT_ID" "$1")"
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
# Reject an unrecognized type rather than filtering everything away. An empty
|
|
98
|
+
# audit is indistinguishable from a clean repo, so a typo would read as good
|
|
99
|
+
# news — the same class of silent-nothing this flag was filed for.
|
|
100
|
+
if [[ -n "$TYPE_FILTER" ]] && ! cartographer_filter_valid_type "$TYPE_FILTER"; then
|
|
101
|
+
log "error: unknown type filter '${TYPE_FILTER}' (expected one of: ${CARTOGRAPHER_FINDING_TYPES})"
|
|
102
|
+
exit 1
|
|
103
|
+
fi
|
|
104
|
+
[[ -n "$TYPE_FILTER" ]] && log "filter type=${TYPE_FILTER}"
|
|
105
|
+
[[ -n "$SCOPE_PATH" ]] && log "filter scope=${SCOPE_PATH}"
|
|
106
|
+
|
|
67
107
|
# ── Phase 1: Discover ──────────────────────────────────────────────────────────
|
|
68
108
|
run_discover() {
|
|
69
109
|
log "phase=discover starting"
|
|
@@ -82,6 +122,14 @@ run_discover() {
|
|
|
82
122
|
GLOBAL_FILES=$(printf '%s\n' "$raw_global" | grep -v '^$' | jq -R . | jq -s .)
|
|
83
123
|
fi
|
|
84
124
|
|
|
125
|
+
# Scope narrows what gets analyzed, not what the repo root is: stale_ref
|
|
126
|
+
# resolves path tokens against the real root, and re-rooting would make
|
|
127
|
+
# every reference outside the scope look broken.
|
|
128
|
+
if [[ -n "$SCOPE_PATH" ]]; then
|
|
129
|
+
DISCOVERED_FILES=$(cartographer_filter_scope \
|
|
130
|
+
"$DISCOVERED_FILES" "$REPO_ROOT" "$SCOPE_PATH")
|
|
131
|
+
fi
|
|
132
|
+
|
|
85
133
|
local file_count
|
|
86
134
|
file_count=$(printf '%s' "$DISCOVERED_FILES" | jq 'length')
|
|
87
135
|
log "phase=discover files=${file_count}"
|
|
@@ -119,11 +167,19 @@ run_extract() {
|
|
|
119
167
|
# ── Phase 3: Relate (contradiction + dead_rule) ────────────────────────────────
|
|
120
168
|
run_relate() {
|
|
121
169
|
log "phase=relate starting"
|
|
170
|
+
|
|
171
|
+
# One LLM pass emits both contradiction and dead_rule, so asking for either
|
|
172
|
+
# runs it and the unwanted type is filtered out of the result below.
|
|
173
|
+
if ! cartographer_filter_wants "contradiction" "$TYPE_FILTER"; then
|
|
174
|
+
log "phase=relate skipped by type filter"
|
|
175
|
+
RELATE_FINDINGS="[]"
|
|
176
|
+
PHASES_COMPLETED+=("relate")
|
|
177
|
+
return 0
|
|
178
|
+
fi
|
|
179
|
+
|
|
122
180
|
local findings
|
|
123
181
|
findings=$($_TIMEOUT_CMD "$_phase_timeout" bash -c \
|
|
124
|
-
"source '$PLUGIN_ROOT/scripts/lib/cartographer-
|
|
125
|
-
source '$PLUGIN_ROOT/scripts/lib/cartographer-analyze.sh'
|
|
126
|
-
cartographer_config_load '$REPO_ROOT'
|
|
182
|
+
"source '$PLUGIN_ROOT/scripts/lib/cartographer-analyze.sh'
|
|
127
183
|
cartographer_analyze_contradiction '$DISCOVERED_FILES' \
|
|
128
184
|
'$_model_extraction' '$_max_tokens_extraction' '$_phase_timeout'" \
|
|
129
185
|
2>>"$CARTOGRAPHER_DIR/audit.log") || {
|
|
@@ -131,7 +187,7 @@ run_relate() {
|
|
|
131
187
|
PHASES_FAILED+=("relate")
|
|
132
188
|
return 1
|
|
133
189
|
}
|
|
134
|
-
RELATE_FINDINGS
|
|
190
|
+
RELATE_FINDINGS=$(cartographer_filter_findings "${findings:-[]}" "$TYPE_FILTER")
|
|
135
191
|
local count
|
|
136
192
|
count=$(printf '%s' "$RELATE_FINDINGS" | jq 'length' 2>/dev/null || printf '0')
|
|
137
193
|
log "phase=relate findings=${count}"
|
|
@@ -142,22 +198,39 @@ run_relate() {
|
|
|
142
198
|
run_synthesize() {
|
|
143
199
|
log "phase=synthesize starting"
|
|
144
200
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
201
|
+
# Each analyzer skipped under a type filter is an LLM call not made, which
|
|
202
|
+
# is where the flag earns its keep.
|
|
203
|
+
local stale_findings="[]" scope_findings="[]"
|
|
204
|
+
if cartographer_filter_wants "stale_ref" "$TYPE_FILTER"; then
|
|
205
|
+
stale_findings=$($_TIMEOUT_CMD "$_phase_timeout" bash -c \
|
|
206
|
+
"source '$PLUGIN_ROOT/scripts/lib/cartographer-analyze.sh'
|
|
207
|
+
cartographer_analyze_stale_ref '$DISCOVERED_FILES' '$REPO_ROOT' \
|
|
208
|
+
'$_model_synthesis' '$_max_tokens_synthesis' '$_phase_timeout'" \
|
|
209
|
+
2>>"$CARTOGRAPHER_DIR/audit.log") || stale_findings="[]"
|
|
210
|
+
fi
|
|
211
|
+
|
|
212
|
+
if cartographer_filter_wants "scope_collision" "$TYPE_FILTER"; then
|
|
213
|
+
scope_findings=$($_TIMEOUT_CMD "$_phase_timeout" bash -c \
|
|
214
|
+
"source '$PLUGIN_ROOT/scripts/lib/cartographer-analyze.sh'
|
|
215
|
+
cartographer_analyze_scope_collision '$GLOBAL_FILES' '$DISCOVERED_FILES' \
|
|
216
|
+
'$_model_synthesis' '$_max_tokens_synthesis' '$_phase_timeout'" \
|
|
217
|
+
2>>"$CARTOGRAPHER_DIR/audit.log") || scope_findings="[]"
|
|
218
|
+
fi
|
|
219
|
+
|
|
220
|
+
# Disk → doc. Skipped on targeted post-write audits: DISCOVERED_FILES is a
|
|
221
|
+
# single file there, so grepping it for every entity name would report
|
|
222
|
+
# nearly the whole enumeration as undocumented, and the emit phase would
|
|
223
|
+
# dedup-sentinel those false findings permanently. scope_collision already
|
|
224
|
+
# no-ops on targeted runs for the same reason.
|
|
225
|
+
local omission_findings="[]"
|
|
226
|
+
if [[ -z "$TARGET_FILE" && "$_undocumented_enabled" == "true" ]] \
|
|
227
|
+
&& cartographer_filter_wants "undocumented_entity" "$TYPE_FILTER"; then
|
|
228
|
+
omission_findings=$($_TIMEOUT_CMD "$_phase_timeout" bash -c \
|
|
229
|
+
"source '$PLUGIN_ROOT/scripts/lib/cartographer-omission.sh'
|
|
230
|
+
cartographer_analyze_undocumented_entity '$DISCOVERED_FILES' '$REPO_ROOT' \
|
|
231
|
+
'$_undocumented_globs' '$_undocumented_exclude' '$_undocumented_max'" \
|
|
232
|
+
2>>"$CARTOGRAPHER_DIR/audit.log") || omission_findings="[]"
|
|
233
|
+
fi
|
|
161
234
|
|
|
162
235
|
# Merge all raw findings
|
|
163
236
|
local raw_all
|
|
@@ -165,7 +238,8 @@ run_synthesize() {
|
|
|
165
238
|
--argjson relate "${RELATE_FINDINGS:-[]}" \
|
|
166
239
|
--argjson stale "${stale_findings:-[]}" \
|
|
167
240
|
--argjson scope "${scope_findings:-[]}" \
|
|
168
|
-
|
|
241
|
+
--argjson omission "${omission_findings:-[]}" \
|
|
242
|
+
'$relate + $stale + $scope + $omission')
|
|
169
243
|
|
|
170
244
|
# Add finding_hash to each finding
|
|
171
245
|
ALL_FINDINGS="[]"
|
|
@@ -213,14 +287,11 @@ run_emit() {
|
|
|
213
287
|
now=$(date +%s)
|
|
214
288
|
|
|
215
289
|
if [[ -f "$dedup_sentinel" ]]; then
|
|
216
|
-
# Known finding —
|
|
290
|
+
# Known finding — refresh it atomically, no bus event. Refreshing
|
|
291
|
+
# also reopens it; see cartographer_refresh_finding for why that is
|
|
292
|
+
# load-bearing rather than tidiness.
|
|
217
293
|
(( known_count++ )) || true
|
|
218
|
-
|
|
219
|
-
local updated
|
|
220
|
-
updated=$(jq --argjson ts "$now" '.last_seen_at=$ts' "$finding_file" 2>/dev/null) || true
|
|
221
|
-
[[ -n "$updated" ]] && printf '%s\n' "$updated" >"${finding_file}.tmp" \
|
|
222
|
-
&& mv -f "${finding_file}.tmp" "$finding_file"
|
|
223
|
-
fi
|
|
294
|
+
cartographer_refresh_finding "$finding_file" "$now" || true
|
|
224
295
|
else
|
|
225
296
|
# New finding — write file atomically, emit bus event, then mark dedup
|
|
226
297
|
local with_ts
|
|
@@ -229,22 +300,20 @@ run_emit() {
|
|
|
229
300
|
printf '%s\n' "$with_ts" >"${finding_file}.tmp"
|
|
230
301
|
mv -f "${finding_file}.tmp" "$finding_file"
|
|
231
302
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
--arg summary "$fdesc" \
|
|
247
|
-
'{"audit_id":$audit_id,"finding_hash":$finding_hash,"finding_type":$finding_type,"severity":$severity,"affected_files":$affected_files,"summary":$summary}')"
|
|
303
|
+
# Branch on the builder rather than passing its output straight
|
|
304
|
+
# to emit_safe. A typeless finding makes it return non-zero, and
|
|
305
|
+
# an unchecked substitution would hand emit_safe an empty payload
|
|
306
|
+
# that cartographer_emit_event rejects silently — relocating the
|
|
307
|
+
# silence this fix exists to end (ecosystem-ci0). Its stderr is
|
|
308
|
+
# appended to the same audit.log emit_safe writes to, because the
|
|
309
|
+
# substitution runs before emit_safe and escapes that redirect.
|
|
310
|
+
local found_payload
|
|
311
|
+
if found_payload=$(cartographer_issue_found_payload \
|
|
312
|
+
"$AUDIT_ID" "$fhash" "$finding" 2>>"$CARTOGRAPHER_DIR/audit.log"); then
|
|
313
|
+
emit_safe "cartographer.issue.found" "$found_payload"
|
|
314
|
+
else
|
|
315
|
+
log "emit: no issue.found for ${fhash} — finding carries no type"
|
|
316
|
+
fi
|
|
248
317
|
|
|
249
318
|
touch "$dedup_sentinel"
|
|
250
319
|
(( new_count++ )) || true
|
|
@@ -253,6 +322,23 @@ run_emit() {
|
|
|
253
322
|
|
|
254
323
|
log "phase=emit new=${new_count} known=${known_count}"
|
|
255
324
|
|
|
325
|
+
# Retire findings this run did not observe. Guarded inside against targeted
|
|
326
|
+
# audits and partial runs, both of which see too little to treat absence as
|
|
327
|
+
# evidence the drift is gone.
|
|
328
|
+
local resolved_count
|
|
329
|
+
resolved_count=$(cartographer_resolve_absent_findings \
|
|
330
|
+
"$FINDINGS_DIR" "$START_TS" "$TARGET_FILE" "${#PHASES_FAILED[@]}" "" _emit_resolved)
|
|
331
|
+
log "phase=emit resolved=${resolved_count}"
|
|
332
|
+
|
|
333
|
+
# Only a run that actually swept may report a count. A targeted or partial
|
|
334
|
+
# run leaves the field off entirely rather than reporting 0, which would
|
|
335
|
+
# read as "swept, found nothing to retire". This asks the same predicate the
|
|
336
|
+
# sweep guards on, so the two cannot drift apart on what the run proved.
|
|
337
|
+
local resolved_arg=""
|
|
338
|
+
if cartographer_resolution_is_sound "$TARGET_FILE" "${#PHASES_FAILED[@]}"; then
|
|
339
|
+
resolved_arg="$resolved_count"
|
|
340
|
+
fi
|
|
341
|
+
|
|
256
342
|
local end_ts duration_ms total_count
|
|
257
343
|
end_ts=$(date +%s)
|
|
258
344
|
duration_ms=$(( (end_ts - START_TS) * 1000 ))
|
|
@@ -265,20 +351,17 @@ run_emit() {
|
|
|
265
351
|
--arg trigger "$TRIGGER" \
|
|
266
352
|
--argjson new_finding_count "$new_count" \
|
|
267
353
|
--argjson known_finding_count "$known_count" \
|
|
354
|
+
--argjson resolved_finding_count "$resolved_count" \
|
|
268
355
|
--argjson total_finding_count "$total_count" \
|
|
269
356
|
--argjson duration_ms "$duration_ms" \
|
|
270
357
|
--argjson phases_completed "$(printf '%s\n' "${PHASES_COMPLETED[@]}" | jq -R . | jq -s .)" \
|
|
271
358
|
--argjson phases_failed "$(printf '%s\n' "${PHASES_FAILED[@]:-}" | jq -R . | jq -s .)" \
|
|
272
|
-
'{"audit_id":$audit_id,"trigger":$trigger,"new_finding_count":$new_finding_count,"known_finding_count":$known_finding_count,"total_finding_count":$total_finding_count,"duration_ms":$duration_ms,"phases_completed":$phases_completed,"phases_failed":$phases_failed}' \
|
|
359
|
+
'{"audit_id":$audit_id,"trigger":$trigger,"new_finding_count":$new_finding_count,"known_finding_count":$known_finding_count,"resolved_finding_count":$resolved_finding_count,"total_finding_count":$total_finding_count,"duration_ms":$duration_ms,"phases_completed":$phases_completed,"phases_failed":$phases_failed}' \
|
|
273
360
|
>"${run_file}.tmp" && mv -f "${run_file}.tmp" "$run_file"
|
|
274
361
|
|
|
275
|
-
emit_safe "cartographer.audit.complete"
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
--argjson new_finding_count "$new_count" \
|
|
279
|
-
--argjson total_finding_count "$total_count" \
|
|
280
|
-
--argjson duration_ms "$duration_ms" \
|
|
281
|
-
'{"audit_id":$audit_id,"trigger":$trigger,"new_finding_count":$new_finding_count,"total_finding_count":$total_finding_count,"duration_ms":$duration_ms}')"
|
|
362
|
+
emit_safe "cartographer.audit.complete" \
|
|
363
|
+
"$(cartographer_audit_complete_payload \
|
|
364
|
+
"$AUDIT_ID" "$TRIGGER" "$new_count" "$total_count" "$duration_ms" "$resolved_arg")"
|
|
282
365
|
|
|
283
366
|
PHASES_COMPLETED+=("emit")
|
|
284
367
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: cartographer
|
|
3
|
-
description: Audit CLAUDE.md, AGENTS.md, and .claude/rules/ instruction files for contradictions, stale references, dead rules, and
|
|
3
|
+
description: Audit CLAUDE.md, AGENTS.md, and .claude/rules/ instruction files for contradictions, stale references, dead rules, scope collisions, and entities no instruction file mentions. Runs a full audit in the foreground (with lock). Use when the user explicitly invokes /cartographer, or when they want immediate feedback after editing an instruction file. Supports --scope, --type, --verbose, --status, and --force flags.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Cartographer Skill
|
|
@@ -55,14 +55,15 @@ After the audit completes, read findings and render them to the conversation gro
|
|
|
55
55
|
|
|
56
56
|
### `/cartographer --verbose` — all findings
|
|
57
57
|
|
|
58
|
-
Shows ALL known findings (new + previously seen) from `$CARTOGRAPHER_DIR/findings
|
|
58
|
+
Shows ALL known findings (new + previously seen) from `$CARTOGRAPHER_DIR/findings/`, including ones already resolved — that is what makes this the verbose view. Resolved findings are tagged so "did my fix take?" is answerable here. Does NOT re-emit bus events — renders to in-conversation output only.
|
|
59
59
|
|
|
60
60
|
```bash
|
|
61
61
|
echo "## Known Cartographer Findings"
|
|
62
62
|
echo ""
|
|
63
63
|
for f in "$CARTOGRAPHER_DIR/findings/"*.json; do
|
|
64
64
|
[[ -f "$f" ]] || continue
|
|
65
|
-
jq -r '
|
|
65
|
+
jq -r '(if .resolved == true then "[RESOLVED] " else "" end) as $r |
|
|
66
|
+
"\($r)**[\(.severity | ascii_upcase)]** \(.type) — \(.description)\n Files: \(.file_a // "n/a") / \(.file_b // "n/a")\n Fix: \(.suggested_fix // "n/a")\n"' "$f" 2>/dev/null
|
|
66
67
|
done
|
|
67
68
|
```
|
|
68
69
|
|
|
@@ -86,7 +87,10 @@ fi
|
|
|
86
87
|
|
|
87
88
|
runs=$(ls "$CARTOGRAPHER_DIR/runs/" 2>/dev/null | wc -l | tr -d ' ')
|
|
88
89
|
total=$(ls "$CARTOGRAPHER_DIR/findings/" 2>/dev/null | wc -l | tr -d ' ')
|
|
89
|
-
|
|
90
|
+
open=$(cat "$CARTOGRAPHER_DIR/findings/"*.json 2>/dev/null \
|
|
91
|
+
| jq -s '[.[] | select(.resolved != true)] | length' 2>/dev/null || echo 0)
|
|
92
|
+
echo "Open findings: $open"
|
|
93
|
+
echo "Total findings on disk: $total (includes resolved)"
|
|
90
94
|
echo "Audit runs recorded: $runs"
|
|
91
95
|
```
|
|
92
96
|
|
|
@@ -107,18 +111,37 @@ fi
|
|
|
107
111
|
# Then proceed as a normal full audit
|
|
108
112
|
```
|
|
109
113
|
|
|
110
|
-
### `/cartographer --
|
|
114
|
+
### `/cartographer --type=<type>` — single finding type
|
|
111
115
|
|
|
112
|
-
|
|
113
|
-
|
|
116
|
+
Narrows the audit to one finding type: `contradiction`, `dead_rule`, `stale_ref`, `scope_collision`, or `undocumented_entity`. Export `CARTOGRAPHER_TYPE_FILTER` before running.
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
export CARTOGRAPHER_TYPE_FILTER="stale_ref"
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Analyzers that cannot produce the requested type are skipped entirely, so this is cheaper than a full audit — each skipped analyzer is an LLM call not made. Useful for re-checking one class of drift after fixing it without paying for the rest.
|
|
123
|
+
|
|
124
|
+
One caveat, because the saving is not uniform: `contradiction` and `dead_rule` come out of a single LLM pass, so asking for either runs that pass and discards the other type from the results. Requesting one of those two costs the same as running both; the other three types are genuinely skippable.
|
|
125
|
+
|
|
126
|
+
An unrecognized type is an error rather than a filter that matches nothing — an empty audit looks exactly like a clean repo, so a typo would read as good news.
|
|
114
127
|
|
|
115
128
|
### `/cartographer --scope=<path>` — scoped audit
|
|
116
129
|
|
|
117
|
-
Limits
|
|
130
|
+
Limits analysis to instruction files under `<path>`, relative to the repo root or absolute. Export `CARTOGRAPHER_SCOPE_PATH` before running.
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
export CARTOGRAPHER_SCOPE_PATH="plugins/tribunal"
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
The scope narrows which files are analyzed; it does not become the repo root. `stale_ref` resolves path-like tokens against the real root, and re-rooting would report every reference outside the scope as broken.
|
|
137
|
+
|
|
138
|
+
Both flags compose, and both apply to manual runs only — the SessionStart and PostToolUse hooks always run unfiltered.
|
|
118
139
|
|
|
119
140
|
## Rendering Findings
|
|
120
141
|
|
|
121
|
-
After a manual audit completes, render findings grouped by severity
|
|
142
|
+
After a manual audit completes, render findings grouped by severity. Resolved
|
|
143
|
+
findings are skipped — a finding whose drift the user already fixed must stop
|
|
144
|
+
appearing here, or the advice to fix it never stops being given:
|
|
122
145
|
|
|
123
146
|
```bash
|
|
124
147
|
echo "## Cartographer Findings"
|
|
@@ -129,6 +152,7 @@ for severity in error warning informational; do
|
|
|
129
152
|
output=""
|
|
130
153
|
for f in "$CARTOGRAPHER_DIR/findings/"*.json; do
|
|
131
154
|
[[ -f "$f" ]] || continue
|
|
155
|
+
jq -e '.resolved != true' "$f" >/dev/null 2>&1 || continue
|
|
132
156
|
fsev=$(jq -r '.severity // "warning"' "$f" 2>/dev/null)
|
|
133
157
|
[[ "$fsev" != "$severity" ]] && continue
|
|
134
158
|
(( count++ )) || true
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "librarian",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.2",
|
|
4
4
|
"description": "Consolidation layer between archivist's per-session artifacts and the user's durable typed memory store. Detects which session decisions, dead-ends, and open questions deserve to live across sessions, classifies them into the user/feedback/project/reference types, and queues them as proposals for explicit confirmation. Auto-promotion is opt-in. Builds on the Onlooker ecosystem plugin.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Onlooker Community",
|
|
@@ -1,5 +1,69 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.13.2](https://github.com/onlooker-community/ecosystem/compare/librarian-v0.13.1...librarian-v0.13.2) (2026-08-18)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* **librarian:** put budget skips on the bus at last :satellite: ([#180](https://github.com/onlooker-community/ecosystem/issues/180)) ([bc91749](https://github.com/onlooker-community/ecosystem/commit/bc917495dab053cba825ea28ccdd1993fdacb3e9))
|
|
9
|
+
* **librarian:** stop stage 5 from holding a session open :hourglass: ([#178](https://github.com/onlooker-community/ecosystem/issues/178)) ([4f12a1e](https://github.com/onlooker-community/ecosystem/commit/4f12a1ee25300e0dc372542814e0922df5dbd335))
|
|
10
|
+
|
|
11
|
+
## [0.13.1](https://github.com/onlooker-community/ecosystem/compare/librarian-v0.13.0...librarian-v0.13.1) (2026-08-15)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### Bug Fixes
|
|
15
|
+
|
|
16
|
+
* **librarian:** make proposals the sole dedup source, and say so :broom: ([#156](https://github.com/onlooker-community/ecosystem/issues/156)) ([729d9ea](https://github.com/onlooker-community/ecosystem/commit/729d9ea3e328c2734712cfc5adf556c6798334aa))
|
|
17
|
+
|
|
18
|
+
## [0.13.0](https://github.com/onlooker-community/ecosystem/compare/librarian-v0.12.1...librarian-v0.13.0) (2026-08-15)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
### Features
|
|
22
|
+
|
|
23
|
+
* **tribunal,librarian:** make rubric weights and min_pass floors real :straight_ruler: ([#150](https://github.com/onlooker-community/ecosystem/issues/150)) ([f8f8e28](https://github.com/onlooker-community/ecosystem/commit/f8f8e28b60f6d13d5a2a54e26ca284137b77f99a))
|
|
24
|
+
|
|
25
|
+
## [0.12.1](https://github.com/onlooker-community/ecosystem/compare/librarian-v0.12.0...librarian-v0.12.1) (2026-08-14)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
### Bug Fixes
|
|
29
|
+
|
|
30
|
+
* **librarian:** close four follow-up defects from the promotion epic :broom: ([#148](https://github.com/onlooker-community/ecosystem/issues/148)) ([895df74](https://github.com/onlooker-community/ecosystem/commit/895df7430fa8ba75976d38964d061c21bff785d3))
|
|
31
|
+
|
|
32
|
+
## [0.12.0](https://github.com/onlooker-community/ecosystem/compare/librarian-v0.11.0...librarian-v0.12.0) (2026-08-13)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
### Features
|
|
36
|
+
|
|
37
|
+
* **librarian:** land judged lessons in the approved pool :package: ([#146](https://github.com/onlooker-community/ecosystem/issues/146)) ([f7adc72](https://github.com/onlooker-community/ecosystem/commit/f7adc7263a985d4cb7695b46d6f9dbab77dce587))
|
|
38
|
+
|
|
39
|
+
## [0.11.0](https://github.com/onlooker-community/ecosystem/compare/librarian-v0.10.0...librarian-v0.11.0) (2026-08-13)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
### Features
|
|
43
|
+
|
|
44
|
+
* **librarian:** derive an author identity that cannot be linked across scopes :closed_lock_with_key: ([#144](https://github.com/onlooker-community/ecosystem/issues/144)) ([29b3042](https://github.com/onlooker-community/ecosystem/commit/29b3042ef6cd6e7582206cfea6fcf37ebe661021))
|
|
45
|
+
|
|
46
|
+
## [0.10.0](https://github.com/onlooker-community/ecosystem/compare/librarian-v0.9.0...librarian-v0.10.0) (2026-08-12)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
### Features
|
|
50
|
+
|
|
51
|
+
* **librarian:** judge lessons before they leave the machine :balance_scale: ([#142](https://github.com/onlooker-community/ecosystem/issues/142)) ([a877527](https://github.com/onlooker-community/ecosystem/commit/a87752714961613b6b2bb768239c666f78d950f8))
|
|
52
|
+
|
|
53
|
+
## [0.9.0](https://github.com/onlooker-community/ecosystem/compare/librarian-v0.8.0...librarian-v0.9.0) (2026-08-11)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
### Features
|
|
57
|
+
|
|
58
|
+
* **librarian:** let a human take back a lesson confirmation :leftwards_arrow_with_hook: ([#139](https://github.com/onlooker-community/ecosystem/issues/139)) ([a4c4ac8](https://github.com/onlooker-community/ecosystem/commit/a4c4ac8ed5b79f5596f4e22abd1d23a8b0d3e834))
|
|
59
|
+
|
|
60
|
+
## [0.8.0](https://github.com/onlooker-community/ecosystem/compare/librarian-v0.7.1...librarian-v0.8.0) (2026-08-11)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
### Features
|
|
64
|
+
|
|
65
|
+
* **librarian:** let a human decide what leaves the machine :raised_hand: ([#137](https://github.com/onlooker-community/ecosystem/issues/137)) ([16d7673](https://github.com/onlooker-community/ecosystem/commit/16d76734833aee9c8818f84c1c1f704be4cdfc33))
|
|
66
|
+
|
|
3
67
|
## [0.7.1](https://github.com/onlooker-community/ecosystem/compare/librarian-v0.7.0...librarian-v0.7.1) (2026-08-10)
|
|
4
68
|
|
|
5
69
|
|
|
@@ -18,7 +18,36 @@
|
|
|
18
18
|
},
|
|
19
19
|
"lesson_transform": {
|
|
20
20
|
"model": "claude-haiku-4-5-20251001",
|
|
21
|
-
"timeout_seconds": 20
|
|
21
|
+
"timeout_seconds": 20,
|
|
22
|
+
"total_budget_ms": 8000
|
|
23
|
+
},
|
|
24
|
+
"lesson_judging": {
|
|
25
|
+
"min_criterion_coverage": 0.6,
|
|
26
|
+
"rubrics": [
|
|
27
|
+
{
|
|
28
|
+
"id": "lesson-promotion",
|
|
29
|
+
"criteria": [
|
|
30
|
+
{ "name": "grounding", "weight": 0.45, "min_pass": 0.7 },
|
|
31
|
+
{ "name": "scope_accuracy", "weight": 0.35, "min_pass": 0.7 },
|
|
32
|
+
{ "name": "generality", "weight": 0.2, "min_pass": 0.6 }
|
|
33
|
+
],
|
|
34
|
+
"score_threshold": 0.75,
|
|
35
|
+
"judge_types": ["standard", "adversarial"],
|
|
36
|
+
"gate_policy": "majority"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"id": "lesson-promotion-public",
|
|
40
|
+
"criteria": [
|
|
41
|
+
{ "name": "grounding", "weight": 0.32, "min_pass": 0.7 },
|
|
42
|
+
{ "name": "scope_accuracy", "weight": 0.24, "min_pass": 0.7 },
|
|
43
|
+
{ "name": "generality", "weight": 0.14, "min_pass": 0.6 },
|
|
44
|
+
{ "name": "disclosure", "weight": 0.3, "min_pass": 0.9 }
|
|
45
|
+
],
|
|
46
|
+
"score_threshold": 0.75,
|
|
47
|
+
"judge_types": ["standard", "adversarial"],
|
|
48
|
+
"gate_policy": "majority"
|
|
49
|
+
}
|
|
50
|
+
]
|
|
22
51
|
},
|
|
23
52
|
"durability_filter": {
|
|
24
53
|
"marker_phrases": [
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# ADR-002: Agent definitions are shared assets; hooks are not
|
|
2
|
+
|
|
3
|
+
## Status
|
|
4
|
+
|
|
5
|
+
Accepted.
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
|
|
9
|
+
CLAUDE.md states: "Plugins communicate by emitting events to the JSONL log —
|
|
10
|
+
they do not call each other directly. All plugins depend on the ecosystem
|
|
11
|
+
substrate; no plugin depends on another plugin directly."
|
|
12
|
+
|
|
13
|
+
Lesson judging (`ecosystem-4z8.3`) needs a jury. Tribunal ships three judge
|
|
14
|
+
agent definitions and the rubric vocabulary. The stage couples librarian and
|
|
15
|
+
tribunal in some direction no matter how it is arranged: either librarian
|
|
16
|
+
reaches for tribunal's judges, or tribunal reaches into librarian's
|
|
17
|
+
project-keyed proposal files and writes verdicts back into them.
|
|
18
|
+
|
|
19
|
+
## Decision
|
|
20
|
+
|
|
21
|
+
The invariant forbids **runtime** coupling — one plugin's hook or library
|
|
22
|
+
calling another's, which makes one plugin's failure another's. It does not
|
|
23
|
+
forbid reusing a **published agent definition** by name.
|
|
24
|
+
|
|
25
|
+
Librarian therefore owns the `judge` verb, both rubrics, the aggregate, and the
|
|
26
|
+
gate decision, and dispatches `tribunal-judge-standard` and
|
|
27
|
+
`tribunal-judge-adversarial` by name.
|
|
28
|
+
|
|
29
|
+
Librarian does **not** source any file under `plugins/tribunal/`. It implements
|
|
30
|
+
its own aggregate and gate — roughly twenty lines — rather than calling
|
|
31
|
+
`tribunal_aggregate` or `tribunal_gate_decide`.
|
|
32
|
+
|
|
33
|
+
## Rationale
|
|
34
|
+
|
|
35
|
+
An agent definition is declarative: a markdown file with frontmatter and a
|
|
36
|
+
prompt. It has no runtime surface and cannot fail at call time in a way that
|
|
37
|
+
propagates — the harness resolves it, librarian never invokes tribunal's code
|
|
38
|
+
directly. That is a real difference from sourcing tribunal's bash, where a
|
|
39
|
+
signature change to `tribunal_gate_decide` breaks librarian mid-call and
|
|
40
|
+
neither plugin's tests would catch it.
|
|
41
|
+
|
|
42
|
+
It is a weaker guarantee than depending on a published schema, though.
|
|
43
|
+
`@onlooker-community/schema` is registered and drift-checked in CI (see
|
|
44
|
+
[ADR-005](../../../../docs/adr/005-runtime-emitter-fails-open.md)): a breaking
|
|
45
|
+
change to it fails a build before it ships. A judge agent's prompt carries no
|
|
46
|
+
such contract — nothing in either plugin's test suite stops a maintainer from
|
|
47
|
+
narrowing or repurposing what `tribunal-judge-standard` returns without
|
|
48
|
+
touching the agent's name. Calling that "closer to a published schema than to
|
|
49
|
+
calling another plugin's code" overstated the guarantee; only the *loud*
|
|
50
|
+
failure modes — the judge being renamed or removed — actually behave like a
|
|
51
|
+
schema break. See Consequences for what happens when the drift is quiet
|
|
52
|
+
instead.
|
|
53
|
+
|
|
54
|
+
Keeping the lifecycle in librarian also keeps `ecosystem-4z8.4`'s pool and
|
|
55
|
+
ledger in one plugin instead of splitting them across two.
|
|
56
|
+
|
|
57
|
+
## Consequences
|
|
58
|
+
|
|
59
|
+
A judge agent renamed or removed in tribunal breaks lesson judging at dispatch
|
|
60
|
+
time. That is a visible, loud failure at the moment a human invokes the verb —
|
|
61
|
+
not a silent one — and the "could not judge" path already handles it: the
|
|
62
|
+
candidate stays `confirmed` and nothing is written.
|
|
63
|
+
|
|
64
|
+
A judge agent silently edited in place is a different, unmitigated risk. If a
|
|
65
|
+
maintainer changes what a judge returns without renaming it — dropping
|
|
66
|
+
`judge_type`, redefining what `feedback_summary` means, rescaling `score` —
|
|
67
|
+
nothing in this pipeline notices at edit time, because the agent definition
|
|
68
|
+
carries no drift check the way `@onlooker-community/schema` does. The
|
|
69
|
+
`usable` panel check in `librarian-lesson-judge.sh` only catches the subset of
|
|
70
|
+
that risk that changes the verdict's *shape*: it requires `judge_type` to be a
|
|
71
|
+
string, `score` a number, and `passed` a boolean, and returns UNJUDGED rather
|
|
72
|
+
than a false rejection if any is missing or mistyped. A *semantic* change —
|
|
73
|
+
same shape, different meaning, such as a score scale moving from 0–1 to
|
|
74
|
+
0–100 — passes that check and is judged normally, with no signal to librarian
|
|
75
|
+
or its maintainers that the verdict no longer means what the aggregate and
|
|
76
|
+
gate assume it means. This ADR accepts that risk rather than closing it;
|
|
77
|
+
closing it would require either a real schema contract for judge output or
|
|
78
|
+
librarian validating semantics it does not own.
|
|
79
|
+
|
|
80
|
+
Librarian's gate logic can drift from tribunal's. Accepted deliberately: they
|
|
81
|
+
answer different questions. Tribunal gates an Actor's output with retry;
|
|
82
|
+
librarian gates a fixed artifact with none.
|
|
@@ -435,11 +435,32 @@ done
|
|
|
435
435
|
#
|
|
436
436
|
# Runs over the same durability survivors the classifier saw. Each artifact is
|
|
437
437
|
# independent: a decline or an outage on one never stops the rest.
|
|
438
|
+
#
|
|
439
|
+
# Budgeted in aggregate, not just per call. Each transform carries a 20s
|
|
440
|
+
# ceiling of its own, but nothing bounded KEPT_COUNT of them end to end, so a
|
|
441
|
+
# backlog could hold SessionEnd open for minutes (ecosystem-qwi). The check is
|
|
442
|
+
# per iteration rather than once before the loop: a pre-loop gate only decides
|
|
443
|
+
# whether to start, and once started the cost is still unbounded — which is the
|
|
444
|
+
# gap the classifier loop above still has.
|
|
445
|
+
#
|
|
446
|
+
# Skipping is the safe direction. Untransformed artifacts are reconsidered on a
|
|
447
|
+
# later session, so the cost of stopping early is a delay; the cost of not
|
|
448
|
+
# stopping is a session that will not close.
|
|
438
449
|
# ---------------------------------------------------------------------------
|
|
439
450
|
LESSON_PROPOSED=0
|
|
440
451
|
LESSON_DECLINED=0
|
|
452
|
+
LESSONS_SKIPPED=0
|
|
453
|
+
|
|
454
|
+
LESSON_BUDGET_MS=$(librarian_config_get '.librarian.lesson_transform.total_budget_ms' 2>/dev/null)
|
|
455
|
+
[[ -z "$LESSON_BUDGET_MS" || "$LESSON_BUDGET_MS" == "null" ]] && LESSON_BUDGET_MS=8000
|
|
456
|
+
LESSON_START_MS=$(librarian_now_ms)
|
|
441
457
|
|
|
442
458
|
for ((li = 0; li < KEPT_COUNT; li++)); do
|
|
459
|
+
if [[ $(( $(librarian_now_ms) - LESSON_START_MS )) -ge "$LESSON_BUDGET_MS" ]]; then
|
|
460
|
+
LESSONS_SKIPPED=$(( KEPT_COUNT - li ))
|
|
461
|
+
break
|
|
462
|
+
fi
|
|
463
|
+
|
|
443
464
|
LESSON_ARTIFACT=$(printf '%s' "$KEPT" | jq -c ".[$li]")
|
|
444
465
|
[[ -z "$LESSON_ARTIFACT" || "$LESSON_ARTIFACT" == "null" ]] && continue
|
|
445
466
|
|
|
@@ -450,6 +471,11 @@ for ((li = 0; li < KEPT_COUNT; li++)); do
|
|
|
450
471
|
esac
|
|
451
472
|
done
|
|
452
473
|
|
|
474
|
+
# LESSONS_SKIPPED rides on scan.complete below rather than becoming an event of
|
|
475
|
+
# its own. A truncated stage 5 is not a truncated scan: the scan finishes
|
|
476
|
+
# normally and only this stage stops early, so the count belongs beside a
|
|
477
|
+
# healthy outcome rather than replacing it.
|
|
478
|
+
|
|
453
479
|
# ----------------------------------------------------------------------------
|
|
454
480
|
# Watermark advance + scan.complete.
|
|
455
481
|
# ----------------------------------------------------------------------------
|
|
@@ -465,12 +491,14 @@ librarian_emit "librarian.scan.complete" "$SESSION_ID" "$(jq -cn \
|
|
|
465
491
|
--arg outcome "$OUTCOME" \
|
|
466
492
|
--argjson candidates_proposed "$PROPOSED_COUNT" \
|
|
467
493
|
--argjson candidates_dropped "$TOTAL_DROPPED" \
|
|
494
|
+
--argjson lessons_skipped "$LESSONS_SKIPPED" \
|
|
468
495
|
--argjson duration_ms "$DURATION_MS" \
|
|
469
496
|
--argjson artifact_count_in_window "$ARTIFACT_COUNT" \
|
|
470
497
|
'{
|
|
471
498
|
outcome: $outcome,
|
|
472
499
|
candidates_proposed: $candidates_proposed,
|
|
473
500
|
candidates_dropped: $candidates_dropped,
|
|
501
|
+
lessons_skipped: $lessons_skipped,
|
|
474
502
|
duration_ms: $duration_ms,
|
|
475
503
|
artifact_count_in_window: $artifact_count_in_window
|
|
476
504
|
}')"
|