@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
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
|
|
3
|
+
# `run --separate-stderr` (used below) requires bats >= 1.5.0.
|
|
4
|
+
bats_require_minimum_version 1.5.0
|
|
5
|
+
|
|
6
|
+
setup() {
|
|
7
|
+
source "${BATS_TEST_DIRNAME}/../helpers/setup.bash"
|
|
8
|
+
setup_test_env
|
|
9
|
+
|
|
10
|
+
PLUGIN_ROOT="${REPO_ROOT}/plugins/librarian"
|
|
11
|
+
export CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT"
|
|
12
|
+
|
|
13
|
+
PROJECT_REPO="${BATS_TEST_TMPDIR}/repo"
|
|
14
|
+
mkdir -p "$PROJECT_REPO"
|
|
15
|
+
git -C "$PROJECT_REPO" init -q
|
|
16
|
+
git -C "$PROJECT_REPO" config user.email t@example.com
|
|
17
|
+
git -C "$PROJECT_REPO" config user.name "Test"
|
|
18
|
+
git -C "$PROJECT_REPO" remote add origin git@github.com:org/fixture.git
|
|
19
|
+
|
|
20
|
+
source "${PLUGIN_ROOT}/scripts/lib/librarian-config.sh"
|
|
21
|
+
source "${PLUGIN_ROOT}/scripts/lib/librarian-project-key.sh"
|
|
22
|
+
source "${PLUGIN_ROOT}/scripts/lib/librarian-storage.sh"
|
|
23
|
+
source "${PLUGIN_ROOT}/scripts/lib/librarian-lesson-storage.sh"
|
|
24
|
+
source "${PLUGIN_ROOT}/scripts/lib/librarian-author-key.sh"
|
|
25
|
+
source "${PLUGIN_ROOT}/scripts/lib/librarian-lesson-promote.sh"
|
|
26
|
+
source "${PLUGIN_ROOT}/scripts/lib/librarian-lesson-review.sh"
|
|
27
|
+
source "${PLUGIN_ROOT}/scripts/lib/librarian-lesson-rubric.sh"
|
|
28
|
+
source "${PLUGIN_ROOT}/scripts/lib/librarian-lesson-judge.sh"
|
|
29
|
+
source "${PLUGIN_ROOT}/scripts/lib/librarian-cli.sh"
|
|
30
|
+
librarian_config_load "$PROJECT_REPO"
|
|
31
|
+
|
|
32
|
+
PROJECT_KEY=$(librarian_project_key "$PROJECT_REPO")
|
|
33
|
+
librarian_lesson_storage_init "$PROJECT_KEY"
|
|
34
|
+
|
|
35
|
+
# Promotion must spend nothing. Any invocation of this stub is a failure.
|
|
36
|
+
STUB_BIN="${BATS_TEST_TMPDIR}/bin"
|
|
37
|
+
mkdir -p "$STUB_BIN"
|
|
38
|
+
cat > "${STUB_BIN}/claude" <<'STUB'
|
|
39
|
+
#!/usr/bin/env bash
|
|
40
|
+
echo "claude was invoked but promotion must spend no tokens" >&2
|
|
41
|
+
exit 99
|
|
42
|
+
STUB
|
|
43
|
+
chmod +x "${STUB_BIN}/claude"
|
|
44
|
+
export PATH="${STUB_BIN}:${PATH}"
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
_dir() { printf '%s' "$(librarian_lessons_dir "$PROJECT_KEY")"; }
|
|
48
|
+
|
|
49
|
+
# A judged proposal. $1 = id, $2 = visibility, $3 = status,
|
|
50
|
+
# $4 = verdict judges JSON array.
|
|
51
|
+
_seed_judged() {
|
|
52
|
+
local id="$1" visibility="$2" status="$3" judges="$4"
|
|
53
|
+
local now
|
|
54
|
+
now=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
55
|
+
jq -n --arg id "$id" --arg v "$visibility" --arg s "$status" \
|
|
56
|
+
--arg t "$now" --argjson j "$judges" \
|
|
57
|
+
'{ id: $id, artifact_id: "art-\($id)", status: $s, visibility: $v,
|
|
58
|
+
confirmed_at: $t, judged_at: $t,
|
|
59
|
+
candidate: { claim: "Prefer jq -c for compact output",
|
|
60
|
+
rationale: "Readable diffs",
|
|
61
|
+
evidence: { resolution: "Applied and verified" },
|
|
62
|
+
applies_to: { stack: ["bash"],
|
|
63
|
+
scope: { kind: "versioned", versions: ">=3.2" } } },
|
|
64
|
+
verdict: { rubric_id: "lesson-promotion", gate_policy: "majority",
|
|
65
|
+
score_threshold: 0.75, aggregate_score: 0.85,
|
|
66
|
+
passed: ($s == "approved"), reason: "gate_passed",
|
|
67
|
+
judges: $j } }' \
|
|
68
|
+
> "$(_dir)/proposals/${id}.json"
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
# Both fixtures carry criterion_scores covering the superset of the org and
|
|
72
|
+
# public rubrics. They are load-bearing, not decoration: librarian_lesson_judge
|
|
73
|
+
# refuses a panel that leaves a floored criterion unscored, so without them
|
|
74
|
+
# these panels are UNJUDGED (2) and never reach the promotion path under test.
|
|
75
|
+
# disclosure sits at 0.95 against its 0.9 floor, which is now a panel MINIMUM.
|
|
76
|
+
_two_passing() {
|
|
77
|
+
printf '%s' '[{"judge_type":"standard","score":0.9,"passed":true,"criterion_scores":{"grounding":0.9,"scope_accuracy":0.9,"generality":0.9,"disclosure":0.95}},{"judge_type":"adversarial","score":0.8,"passed":true,"criterion_scores":{"grounding":0.8,"scope_accuracy":0.8,"generality":0.8,"disclosure":0.95}}]'
|
|
78
|
+
}
|
|
79
|
+
_split() {
|
|
80
|
+
printf '%s' '[{"judge_type":"standard","score":0.95,"passed":true,"criterion_scores":{"grounding":0.95,"scope_accuracy":0.95,"generality":0.95,"disclosure":0.95}},{"judge_type":"adversarial","score":0.75,"passed":false,"criterion_scores":{"grounding":0.75,"scope_accuracy":0.75,"generality":0.75,"disclosure":0.95}}]'
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
@test "an approved org lesson becomes a pool entry with exactly ZLesson's keys" {
|
|
84
|
+
# ZLesson is a z.strictObject: an extra key fails ingest as surely as a
|
|
85
|
+
# missing one, so the key SET is the assertion, not a spot-check.
|
|
86
|
+
_seed_judged "org01" "org" "approved" "$(_two_passing)"
|
|
87
|
+
run librarian_lesson_promote "$PROJECT_KEY" "org01"
|
|
88
|
+
[ "$status" -eq 0 ]
|
|
89
|
+
|
|
90
|
+
local keys
|
|
91
|
+
keys=$(jq -r 'keys_unsorted | sort | join(" ")' "$(_dir)/approved/org01.json")
|
|
92
|
+
[ "$keys" = "applies_to author_key claim consensus evidence id promoted_at rationale schema_version source status superseded_by visibility" ]
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
@test "the pool entry carries the mapped source and a derived consensus" {
|
|
96
|
+
_seed_judged "org02" "org" "approved" "$(_two_passing)"
|
|
97
|
+
librarian_lesson_promote "$PROJECT_KEY" "org02"
|
|
98
|
+
|
|
99
|
+
local e
|
|
100
|
+
e=$(cat "$(_dir)/approved/org02.json")
|
|
101
|
+
[ "$(printf '%s' "$e" | jq -r '.source')" = "org" ]
|
|
102
|
+
[ "$(printf '%s' "$e" | jq -r '.visibility')" = "org" ]
|
|
103
|
+
[ "$(printf '%s' "$e" | jq -r '.schema_version')" = "2" ]
|
|
104
|
+
[ "$(printf '%s' "$e" | jq -r '.status')" = "active" ]
|
|
105
|
+
[ "$(printf '%s' "$e" | jq -r '.superseded_by')" = "null" ]
|
|
106
|
+
[ "$(printf '%s' "$e" | jq -r '.consensus.judges')" = "2" ]
|
|
107
|
+
[ "$(printf '%s' "$e" | jq -r '.consensus.agreed')" = "2" ]
|
|
108
|
+
[ "$(printf '%s' "$e" | jq -r '.author_key')" != "null" ]
|
|
109
|
+
printf '%s' "$e" | jq -e '.author_key | test("^[0-9a-f]{32}$")' >/dev/null || return 1
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
@test "a private lesson maps to source local with zero judges" {
|
|
113
|
+
# Deliberately NOT ingest-valid: ZConsensus requires judges >= 1. A
|
|
114
|
+
# private lesson never syncs, so it never reaches the validator. Do not
|
|
115
|
+
# "fix" this by synthesizing a jury that never sat.
|
|
116
|
+
_seed_judged "priv01" "private" "approved" '[]'
|
|
117
|
+
run librarian_lesson_promote "$PROJECT_KEY" "priv01"
|
|
118
|
+
[ "$status" -eq 0 ]
|
|
119
|
+
|
|
120
|
+
local e
|
|
121
|
+
e=$(cat "$(_dir)/approved/priv01.json")
|
|
122
|
+
[ "$(printf '%s' "$e" | jq -r '.source')" = "local" ]
|
|
123
|
+
[ "$(printf '%s' "$e" | jq -r '.visibility')" = "private" ]
|
|
124
|
+
[ "$(printf '%s' "$e" | jq -r '.consensus.judges')" = "0" ]
|
|
125
|
+
[ "$(printf '%s' "$e" | jq -r '.consensus.agreed')" = "0" ]
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
@test "a public lesson maps to source public" {
|
|
129
|
+
_seed_judged "pub01" "public" "approved" "$(_two_passing)"
|
|
130
|
+
librarian_lesson_promote "$PROJECT_KEY" "pub01"
|
|
131
|
+
[ "$(jq -r '.source' "$(_dir)/approved/pub01.json")" = "public" ]
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
@test "agreed never exceeds judges" {
|
|
135
|
+
# The contract's own ingest rule, which its schema deliberately cannot
|
|
136
|
+
# express (it would need .refine(), which z.toJSONSchema drops).
|
|
137
|
+
_seed_judged "cnt01" "org" "approved" "$(_split)"
|
|
138
|
+
librarian_lesson_promote "$PROJECT_KEY" "cnt01"
|
|
139
|
+
|
|
140
|
+
local e
|
|
141
|
+
e=$(cat "$(_dir)/approved/cnt01.json")
|
|
142
|
+
[ "$(printf '%s' "$e" | jq -r '.consensus.judges')" = "2" ]
|
|
143
|
+
[ "$(printf '%s' "$e" | jq -r '.consensus.agreed')" = "1" ]
|
|
144
|
+
printf '%s' "$e" | jq -e '.consensus.agreed <= .consensus.judges' >/dev/null || return 1
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
@test "a rejected lesson writes a declined row with a NESTED verdict and no pool entry" {
|
|
148
|
+
# .verdict must be an object, not a serialized string. A --arg/--argjson
|
|
149
|
+
# mistake produces a row that looks right and is unusable to a consumer,
|
|
150
|
+
# so assert by indexing into it rather than matching a substring.
|
|
151
|
+
_seed_judged "rej01" "public" "rejected" "$(_split)"
|
|
152
|
+
run librarian_lesson_promote "$PROJECT_KEY" "rej01"
|
|
153
|
+
[ "$status" -eq 0 ]
|
|
154
|
+
|
|
155
|
+
[ ! -f "$(_dir)/approved/rej01.json" ]
|
|
156
|
+
local row
|
|
157
|
+
row=$(grep 'art-rej01' "$(_dir)/declined.jsonl")
|
|
158
|
+
[ "$(printf '%s' "$row" | jq -r '.artifact_id')" = "art-rej01" ]
|
|
159
|
+
[ "$(printf '%s' "$row" | jq -r '.verdict | type')" = "object" ]
|
|
160
|
+
[ "$(printf '%s' "$row" | jq -r '.verdict.judges | length')" = "2" ]
|
|
161
|
+
[ "$(printf '%s' "$row" | jq -r '.verdict.rubric_id')" = "lesson-promotion" ]
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
@test "a stage-5 style decline still writes cleanly and has no verdict key" {
|
|
165
|
+
librarian_lesson_append_declined "$PROJECT_KEY" "art-t5" "transform_invalid" "malformed JSON"
|
|
166
|
+
local row
|
|
167
|
+
row=$(grep 'art-t5' "$(_dir)/declined.jsonl")
|
|
168
|
+
[ "$(printf '%s' "$row" | jq -r '.reason')" = "transform_invalid" ]
|
|
169
|
+
[ "$(printf '%s' "$row" | jq -r 'has("verdict")')" = "false" ]
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
@test "promoting twice leaves one pool entry with an unchanged promoted_at" {
|
|
173
|
+
_seed_judged "idem01" "org" "approved" "$(_two_passing)"
|
|
174
|
+
librarian_lesson_promote "$PROJECT_KEY" "idem01"
|
|
175
|
+
local first
|
|
176
|
+
first=$(cat "$(_dir)/approved/idem01.json")
|
|
177
|
+
|
|
178
|
+
run librarian_lesson_promote "$PROJECT_KEY" "idem01"
|
|
179
|
+
[ "$status" -eq 0 ]
|
|
180
|
+
[ "$(cat "$(_dir)/approved/idem01.json")" = "$first" ]
|
|
181
|
+
[ "$(ls "$(_dir)/approved" | grep -c idem01)" -eq 1 ]
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
@test "promotion is refused before the lesson has been judged" {
|
|
185
|
+
_seed_judged "conf01" "org" "confirmed" '[]'
|
|
186
|
+
run --separate-stderr librarian_lesson_promote "$PROJECT_KEY" "conf01"
|
|
187
|
+
[ "$status" -ne 0 ]
|
|
188
|
+
[ "$output" = "" ]
|
|
189
|
+
[[ "$stderr" == *"not been judged"* ]] || return 1
|
|
190
|
+
[ ! -f "$(_dir)/approved/conf01.json" ]
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
@test "promotion is refused from a passed lesson, naming the status" {
|
|
194
|
+
_seed_judged "pass01" "org" "passed" '[]'
|
|
195
|
+
run --separate-stderr librarian_lesson_promote "$PROJECT_KEY" "pass01"
|
|
196
|
+
[ "$status" -ne 0 ]
|
|
197
|
+
[[ "$stderr" == *"passed"* ]] || return 1
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
@test "a failing author_key leaves nothing written and the lesson still approved" {
|
|
201
|
+
# THE reconcile property. Promotion fails for reasons judging does not —
|
|
202
|
+
# a malformed secret, absent node, a full disk — and the lesson must stay
|
|
203
|
+
# exactly where a standalone re-run can pick it up.
|
|
204
|
+
_seed_judged "ak01" "org" "approved" "$(_two_passing)"
|
|
205
|
+
local secret_path
|
|
206
|
+
secret_path=$(librarian_author_secret_path)
|
|
207
|
+
mkdir -p "$(dirname "$secret_path")"
|
|
208
|
+
printf 'not-a-valid-secret\n' > "$secret_path"
|
|
209
|
+
chmod 0600 "$secret_path"
|
|
210
|
+
|
|
211
|
+
run --separate-stderr librarian_lesson_promote "$PROJECT_KEY" "ak01"
|
|
212
|
+
[ "$status" -ne 0 ]
|
|
213
|
+
[ ! -f "$(_dir)/approved/ak01.json" ]
|
|
214
|
+
[ "$(jq -r 'has("promoted_at")' "$(_dir)/proposals/ak01.json")" = "false" ]
|
|
215
|
+
[ "$(jq -r '.status' "$(_dir)/proposals/ak01.json")" = "approved" ]
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
@test "the proposal survives promotion and carries promoted_at" {
|
|
219
|
+
_seed_judged "keep01" "org" "approved" "$(_two_passing)"
|
|
220
|
+
librarian_lesson_promote "$PROJECT_KEY" "keep01"
|
|
221
|
+
[ -f "$(_dir)/proposals/keep01.json" ]
|
|
222
|
+
[ "$(jq -r '.status' "$(_dir)/proposals/keep01.json")" = "approved" ]
|
|
223
|
+
printf '%s' "$(jq -r '.promoted_at' "$(_dir)/proposals/keep01.json")" \
|
|
224
|
+
| grep -Eq '^[0-9]{4}-[0-9]{2}-[0-9]{2}T' || return 1
|
|
225
|
+
# The per-judge detail is why the proposal is kept: the pool entry has
|
|
226
|
+
# only consensus counts.
|
|
227
|
+
[ "$(jq -r '.verdict.judges | length' "$(_dir)/proposals/keep01.json")" = "2" ]
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
@test "a promoted rejection is still seen after its proposal is deleted" {
|
|
231
|
+
# The proposal is deleted after promoting, on purpose. librarian_lesson_seen
|
|
232
|
+
# scans proposals/ unconditionally of status, so leaving it in place would
|
|
233
|
+
# let this test pass even if promote wrote nothing at all. The property
|
|
234
|
+
# under test is that the TERMINAL RECORD — the declined row — is what
|
|
235
|
+
# marks the artifact handled once its proposal ages out, not the
|
|
236
|
+
# (now-gone) proposal file.
|
|
237
|
+
#
|
|
238
|
+
# The approved path has no equivalent, and cannot: see the test directly
|
|
239
|
+
# below, which pins that absence as the known limitation it is.
|
|
240
|
+
_seed_judged "seen02" "public" "rejected" "$(_split)"
|
|
241
|
+
librarian_lesson_promote "$PROJECT_KEY" "seen02"
|
|
242
|
+
rm -f "$(_dir)/proposals/seen02.json"
|
|
243
|
+
run librarian_lesson_seen "$PROJECT_KEY" "art-seen02"
|
|
244
|
+
[ "$status" -eq 0 ]
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
@test "an approved lesson is NOT seen once its proposal is gone — proposals are the sole dedup source" {
|
|
248
|
+
# The limitation ecosystem-d0m settled, made executable so it cannot drift.
|
|
249
|
+
#
|
|
250
|
+
# A rejection survives its proposal because the declined row is a terminal
|
|
251
|
+
# record keyed by artifact_id. An approval has no such record: the pool
|
|
252
|
+
# entry is ZLesson, a strictObject with no artifact_id, so nothing promote
|
|
253
|
+
# writes can answer "was this artifact handled?" Dedup for an approved
|
|
254
|
+
# lesson rests entirely on proposals/ never being pruned.
|
|
255
|
+
#
|
|
256
|
+
# This asserts status 1 deliberately. It is not endorsing the gap — it is
|
|
257
|
+
# the tripwire for it. If someone builds the artifact_id -> lesson id index
|
|
258
|
+
# and closes d0m, THIS TEST WILL FAIL, which is the point: the fix should
|
|
259
|
+
# have to come here and say so, rather than quietly leaving a stale
|
|
260
|
+
# assertion that the limitation still exists.
|
|
261
|
+
#
|
|
262
|
+
# Use a real promote, never a hand-written approved/*.json. The test this
|
|
263
|
+
# replaced fabricated {artifact_id: ...} into the pool — a shape promote
|
|
264
|
+
# cannot produce — and so reported coverage of a scan that never ran.
|
|
265
|
+
_seed_judged "seen03" "public" "approved" "$(_two_passing)"
|
|
266
|
+
librarian_lesson_promote "$PROJECT_KEY" "seen03"
|
|
267
|
+
# The pool entry exists and carries no artifact_id: that is the whole cause.
|
|
268
|
+
[ -f "$(_dir)/approved/seen03.json" ] || return 1
|
|
269
|
+
run jq -e 'has("artifact_id")' "$(_dir)/approved/seen03.json"
|
|
270
|
+
[ "$status" -ne 0 ] || return 1
|
|
271
|
+
|
|
272
|
+
# With the proposal present, dedup works — via proposals/, not the pool.
|
|
273
|
+
run librarian_lesson_seen "$PROJECT_KEY" "art-seen03"
|
|
274
|
+
[ "$status" -eq 0 ] || return 1
|
|
275
|
+
|
|
276
|
+
rm -f "$(_dir)/proposals/seen03.json"
|
|
277
|
+
run librarian_lesson_seen "$PROJECT_KEY" "art-seen03"
|
|
278
|
+
[ "$status" -eq 1 ]
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
@test "promoting a rejected lesson twice writes exactly one declined row" {
|
|
282
|
+
# Mirrors test 8's shape for the approved path. A plain double-promote
|
|
283
|
+
# never reaches the guard under test: a first call that succeeds
|
|
284
|
+
# outright already stamps promoted_at, and the second call short-circuits
|
|
285
|
+
# on that before ever looking at declined.jsonl. The guard only matters
|
|
286
|
+
# when the FIRST call's stamp fails AFTER the declined row has already
|
|
287
|
+
# landed — proposals/ turning read-only mid-promote is a plausible way
|
|
288
|
+
# to hit that, and the same shape test 11 uses for the author_key case.
|
|
289
|
+
_seed_judged "decl01" "public" "rejected" "$(_split)"
|
|
290
|
+
|
|
291
|
+
chmod 0500 "$(_dir)/proposals"
|
|
292
|
+
run --separate-stderr librarian_lesson_promote "$PROJECT_KEY" "decl01"
|
|
293
|
+
chmod 0700 "$(_dir)/proposals"
|
|
294
|
+
[ "$status" -ne 0 ]
|
|
295
|
+
[ "$(jq -r 'has("promoted_at")' "$(_dir)/proposals/decl01.json")" = "false" ]
|
|
296
|
+
[ "$(grep -c 'art-decl01' "$(_dir)/declined.jsonl")" -eq 1 ]
|
|
297
|
+
# This is the one failure path that writes something (the declined row
|
|
298
|
+
# above) before failing — the stderr message must say so, not return
|
|
299
|
+
# silently, or this state is indistinguishable from "nothing written."
|
|
300
|
+
[[ "$stderr" == *"terminal record written but promoted_at could not be stamped"* ]] || return 1
|
|
301
|
+
|
|
302
|
+
# The standalone re-run — the exact path the ordering guarantee exists to
|
|
303
|
+
# make safe.
|
|
304
|
+
run librarian_lesson_promote "$PROJECT_KEY" "decl01"
|
|
305
|
+
[ "$status" -eq 0 ]
|
|
306
|
+
[ "$(grep -c 'art-decl01' "$(_dir)/declined.jsonl")" -eq 1 ]
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
@test "the pool entry guard leaves an existing entry untouched when the proposal isn't yet stamped" {
|
|
310
|
+
# Mirrors test 14's shape for the approved path. The ordering guarantee
|
|
311
|
+
# means the pool write can succeed and the stamp fail afterward, so a
|
|
312
|
+
# standalone re-run must find that pool entry already present and skip
|
|
313
|
+
# rewriting it. Nothing before this pins that: "promoting twice..." above
|
|
314
|
+
# never reaches the pool_path guard at all — the earlier already-promoted
|
|
315
|
+
# check (promoted_at present on the proposal) short-circuits first, since
|
|
316
|
+
# a plain double-promote always stamps successfully on the first call.
|
|
317
|
+
_seed_judged "pool01" "org" "approved" "$(_two_passing)"
|
|
318
|
+
|
|
319
|
+
mkdir -p "$(_dir)/approved"
|
|
320
|
+
printf '{"sentinel":"pre-existing pool entry"}' > "$(_dir)/approved/pool01.json"
|
|
321
|
+
|
|
322
|
+
run librarian_lesson_promote "$PROJECT_KEY" "pool01"
|
|
323
|
+
[ "$status" -eq 0 ]
|
|
324
|
+
[ "$(jq -r '.sentinel // empty' "$(_dir)/approved/pool01.json")" = "pre-existing pool entry" ]
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
@test "promotion self-heals a missing approved/ directory" {
|
|
328
|
+
# Empty directories do not survive git, or tar/rsync without -d, so
|
|
329
|
+
# approved/ can be legitimately absent even though proposals/ —
|
|
330
|
+
# non-empty, holding this very lesson — is right there. Unlike
|
|
331
|
+
# librarian_lesson_append_declined, promote did not call
|
|
332
|
+
# librarian_lesson_storage_init; without it this lesson stayed stuck at
|
|
333
|
+
# "cannot write the pool entry" forever.
|
|
334
|
+
_seed_judged "heal01" "org" "approved" "$(_two_passing)"
|
|
335
|
+
rm -rf "$(_dir)/approved"
|
|
336
|
+
|
|
337
|
+
run librarian_lesson_promote "$PROJECT_KEY" "heal01"
|
|
338
|
+
[ "$status" -eq 0 ]
|
|
339
|
+
[ -f "$(_dir)/approved/heal01.json" ]
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
@test "a refusal before the approved branch creates no directory scaffolding" {
|
|
343
|
+
# The self-heal above must not run ahead of every refusal path. A pure
|
|
344
|
+
# refusal ("not judged yet," "not found," wrong status) has to write
|
|
345
|
+
# NOTHING, per this function's own doc comment and the spec — and
|
|
346
|
+
# mkdir -p is a write, even though it's idempotent and inert once made.
|
|
347
|
+
# setup() already ran storage_init once, so start from zero prior
|
|
348
|
+
# activity by removing approved/ again before seeding.
|
|
349
|
+
_seed_judged "refuse01" "org" "confirmed" '[]'
|
|
350
|
+
rm -rf "$(_dir)/approved"
|
|
351
|
+
|
|
352
|
+
run librarian_lesson_promote "$PROJECT_KEY" "refuse01"
|
|
353
|
+
[ "$status" -ne 0 ]
|
|
354
|
+
[ ! -d "$(_dir)/approved" ]
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
@test "a missing lesson is refused" {
|
|
358
|
+
run --separate-stderr librarian_lesson_promote "$PROJECT_KEY" "nope01"
|
|
359
|
+
[ "$status" -ne 0 ]
|
|
360
|
+
[[ "$stderr" == *"not found"* ]] || return 1
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
@test "lessons promote lands a pool entry through the CLI" {
|
|
364
|
+
_seed_judged "cli01" "org" "approved" "$(_two_passing)"
|
|
365
|
+
run librarian_cli lessons promote "cli01" "$PROJECT_REPO"
|
|
366
|
+
[ "$status" -eq 0 ]
|
|
367
|
+
[ -f "$(_dir)/approved/cli01.json" ]
|
|
368
|
+
[[ "$output" == *"cli01"* ]] || return 1
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
@test "lessons promote requires a lesson id" {
|
|
372
|
+
run librarian_cli lessons promote
|
|
373
|
+
[ "$status" -ne 0 ]
|
|
374
|
+
[[ "$output" == *"usage:"* && "$output" == *"promote"* ]] || return 1
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
@test "lessons promote rejects an unknown flag" {
|
|
378
|
+
_seed_judged "cli02" "org" "approved" "$(_two_passing)"
|
|
379
|
+
run librarian_cli lessons promote "cli02" --force
|
|
380
|
+
[ "$status" -ne 0 ]
|
|
381
|
+
[[ "$output" == *"unknown option"* && "$output" == *"--force"* ]] || return 1
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
@test "lessons judge promotes automatically after recording a verdict" {
|
|
385
|
+
# The ordinary path is one command: judge, record, promote.
|
|
386
|
+
_seed_judged "auto01" "org" "confirmed" '[]'
|
|
387
|
+
run librarian_cli lessons judge "auto01" "$(_two_passing)" "$PROJECT_REPO"
|
|
388
|
+
[ "$status" -eq 0 ]
|
|
389
|
+
[ "$(jq -r '.status' "$(_dir)/proposals/auto01.json")" = "approved" ]
|
|
390
|
+
[ -f "$(_dir)/approved/auto01.json" ]
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
@test "an unjudged candidate does not even attempt promotion" {
|
|
394
|
+
# The judge returns 2 for UNJUDGED and writes nothing — but that alone
|
|
395
|
+
# does not pin the wiring. librarian_lesson_promote independently refuses
|
|
396
|
+
# a still-`confirmed` proposal ("has not been judged yet"), so a `2)` arm
|
|
397
|
+
# that mistakenly calls it would land on the exact same empty state as a
|
|
398
|
+
# `2)` arm that never calls it at all: same status, same missing pool
|
|
399
|
+
# file. Shadow librarian_lesson_promote with a spy that records whether
|
|
400
|
+
# it was invoked and delegates to the real implementation, so a call
|
|
401
|
+
# wired into the `2)` arm is caught even though the call itself is a
|
|
402
|
+
# no-op.
|
|
403
|
+
_seed_judged "auto02" "org" "confirmed" '[]'
|
|
404
|
+
|
|
405
|
+
local marker="${BATS_TEST_TMPDIR}/promote-invoked"
|
|
406
|
+
eval "$(declare -f librarian_lesson_promote | sed '1s/^librarian_lesson_promote/_real_librarian_lesson_promote/')"
|
|
407
|
+
librarian_lesson_promote() {
|
|
408
|
+
: > "$marker"
|
|
409
|
+
_real_librarian_lesson_promote "$@"
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
run librarian_cli lessons judge "auto02" '[{"judge_type":"standard","score":"bad","passed":true}]' "$PROJECT_REPO"
|
|
413
|
+
[ "$status" -eq 2 ]
|
|
414
|
+
[ "$(jq -r '.status' "$(_dir)/proposals/auto02.json")" = "confirmed" ]
|
|
415
|
+
[ ! -f "$(_dir)/approved/auto02.json" ]
|
|
416
|
+
[ ! -f "$marker" ]
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
@test "a judge-walk promotion failure reports, does not undo the verdict, and reconciles" {
|
|
420
|
+
# The other half of the reconcile property, exercised through the CLI's
|
|
421
|
+
# judge verb rather than promote directly. A promotion failure inside the
|
|
422
|
+
# automatic call must not undo the verdict the jury just recorded — the
|
|
423
|
+
# lesson stays `approved`, unstamped, and the standalone `lessons
|
|
424
|
+
# promote` verb is how it gets picked back up once the failure is fixed.
|
|
425
|
+
# Same corruption Task 1's `ak01` test uses to fail librarian_author_key.
|
|
426
|
+
_seed_judged "jwfail01" "org" "confirmed" '[]'
|
|
427
|
+
|
|
428
|
+
local secret_path
|
|
429
|
+
secret_path=$(librarian_author_secret_path)
|
|
430
|
+
mkdir -p "$(dirname "$secret_path")"
|
|
431
|
+
printf 'not-a-valid-secret\n' > "$secret_path"
|
|
432
|
+
chmod 0600 "$secret_path"
|
|
433
|
+
|
|
434
|
+
run librarian_cli lessons judge "jwfail01" "$(_two_passing)" "$PROJECT_REPO"
|
|
435
|
+
[ "$status" -eq 0 ]
|
|
436
|
+
[[ "$output" == *"Lesson jwfail01 is now approved."* ]] || return 1
|
|
437
|
+
[[ "$output" == *"Lesson jwfail01 was judged but not promoted; run \`lessons promote jwfail01\` to retry."* ]] || return 1
|
|
438
|
+
[ ! -f "$(_dir)/approved/jwfail01.json" ]
|
|
439
|
+
[ "$(jq -r '.status' "$(_dir)/proposals/jwfail01.json")" = "approved" ]
|
|
440
|
+
[ "$(jq -r 'has("promoted_at")' "$(_dir)/proposals/jwfail01.json")" = "false" ]
|
|
441
|
+
|
|
442
|
+
# Repair the secret and reconcile through the standalone verb — the
|
|
443
|
+
# point of the whole design.
|
|
444
|
+
openssl rand -hex 32 > "$secret_path"
|
|
445
|
+
chmod 0600 "$secret_path"
|
|
446
|
+
run librarian_cli lessons promote "jwfail01" "$PROJECT_REPO"
|
|
447
|
+
[ "$status" -eq 0 ]
|
|
448
|
+
[ -f "$(_dir)/approved/jwfail01.json" ]
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
_promote_refuses_shape() {
|
|
452
|
+
# $1 = id, $2 = jq program mutating the seeded proposal, $3 = field name
|
|
453
|
+
# the guard's stderr message must name — pins WHY the refusal happened,
|
|
454
|
+
# not just THAT it happened, so a guard that fires on the right shape but
|
|
455
|
+
# mislabels the field (or a check that's silently missing) still fails
|
|
456
|
+
# this test. Matches the file's existing refusal-test pattern (empty
|
|
457
|
+
# stdout + a specific stderr substring) rather than status/absence alone.
|
|
458
|
+
#
|
|
459
|
+
# Whole-token match, not substring: "id" is a substring of "candidate"
|
|
460
|
+
# (c-a-n-d-id-a-t-e), so a bare *"$field"* glob would accept a message
|
|
461
|
+
# naming candidate.claim for a null .id. Pull the guard's comma-joined
|
|
462
|
+
# field list out of the message, strip spaces, and check exact
|
|
463
|
+
# membership by delimiting both sides with commas.
|
|
464
|
+
local id="$1" mutate="$2" field="$3"
|
|
465
|
+
_seed_judged "$id" "org" "approved" "$(_two_passing)"
|
|
466
|
+
local p
|
|
467
|
+
p="$(_dir)/proposals/${id}.json"
|
|
468
|
+
local tmp="${BATS_TEST_TMPDIR}/${id}.json"
|
|
469
|
+
jq "$mutate" "$p" > "$tmp" && mv "$tmp" "$p"
|
|
470
|
+
|
|
471
|
+
run --separate-stderr librarian_lesson_promote "$PROJECT_KEY" "$id"
|
|
472
|
+
[ "$status" -ne 0 ]
|
|
473
|
+
[ "$output" = "" ]
|
|
474
|
+
[ ! -f "$(_dir)/approved/${id}.json" ]
|
|
475
|
+
[ "$(jq -r 'has("promoted_at")' "$p")" = "false" ]
|
|
476
|
+
|
|
477
|
+
local list
|
|
478
|
+
list=$(printf '%s' "$stderr" | sed -n 's/.*bad or missing: \([^)]*\)).*/\1/p')
|
|
479
|
+
[ -n "$list" ] || return 1
|
|
480
|
+
printf ',%s,' "$(printf '%s' "$list" | tr -d ' ')" | grep -Fq ",${field}," || return 1
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
@test "a null judges array is refused, not read as a jury-less lesson" {
|
|
484
|
+
# The worst shape: consensus {judges:0, agreed:0} is byte-identical to a
|
|
485
|
+
# legitimate private entry, but carries source "org" — so unlike a private
|
|
486
|
+
# entry it WILL sync, and then fail ingest on ZConsensus.judges >= 1.
|
|
487
|
+
_promote_refuses_shape "mal01" '.verdict.judges = null' "verdict.judges"
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
@test "a judges object rather than an array is refused" {
|
|
491
|
+
_promote_refuses_shape "mal02" '.verdict.judges = {"a":{"passed":true}}' "verdict.judges"
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
@test "a proposal missing its candidate is refused" {
|
|
495
|
+
# Currently produces the exact 13 ZLesson keys with null claim/rationale/
|
|
496
|
+
# evidence/applies_to — it passes the key-set test while being empty.
|
|
497
|
+
_promote_refuses_shape "mal03" 'del(.candidate)' "candidate.claim"
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
@test "a proposal missing judged_at is refused" {
|
|
501
|
+
_promote_refuses_shape "mal04" 'del(.judged_at)' "judged_at"
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
@test "a proposal with a null id is refused" {
|
|
505
|
+
# .id is read by the entry mapping (id: $p.id) same as candidate/judged_at/
|
|
506
|
+
# verdict.judges — a null id promotes to a pool entry with "id": null,
|
|
507
|
+
# passing the key-set check while being exactly the kind of well-formed-
|
|
508
|
+
# but-wrong entry this guard exists to prevent.
|
|
509
|
+
_promote_refuses_shape "mal05" '.id = null' "id"
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
@test "a well-formed proposal still promotes" {
|
|
513
|
+
# The guard must not reject anything the pipeline actually produces.
|
|
514
|
+
_seed_judged "ok01" "org" "approved" "$(_two_passing)"
|
|
515
|
+
run librarian_lesson_promote "$PROJECT_KEY" "ok01"
|
|
516
|
+
[ "$status" -eq 0 ]
|
|
517
|
+
[ -f "$(_dir)/approved/ok01.json" ]
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
@test "an approved promotion succeeds when sourced through exactly SKILL.md's source list" {
|
|
521
|
+
# The runtime's ONLY entry point for the lessons CLI is SKILL.md's source
|
|
522
|
+
# block — not this file's own setup(), which sources librarian-author-key.sh
|
|
523
|
+
# itself and so could never catch a missing dependency in the shipped list.
|
|
524
|
+
# Parse the `source` lines straight out of SKILL.md (never hand-copy them)
|
|
525
|
+
# and drive one promotion in a subshell seeded with only those, so a future
|
|
526
|
+
# lib gaining an undeclared dependency fails THIS test instead of shipping
|
|
527
|
+
# with an inert runtime.
|
|
528
|
+
local skill="${PLUGIN_ROOT}/skills/librarian/SKILL.md"
|
|
529
|
+
[ -f "$skill" ]
|
|
530
|
+
|
|
531
|
+
_seed_judged "skill01" "org" "approved" "$(_two_passing)"
|
|
532
|
+
|
|
533
|
+
local script="${BATS_TEST_TMPDIR}/drive-skill-sources.sh"
|
|
534
|
+
{
|
|
535
|
+
printf '#!/usr/bin/env bash\n'
|
|
536
|
+
printf 'set -uo pipefail\n'
|
|
537
|
+
# Mirrors SKILL.md's own two-line preamble (unchanged by this branch):
|
|
538
|
+
# librarian-config.sh sources a sibling by the bare $PLUGIN_ROOT
|
|
539
|
+
# variable, not $CLAUDE_PLUGIN_ROOT, so both must be set the same way
|
|
540
|
+
# the skill's bash block sets them, in the same shell as the sources
|
|
541
|
+
# below. This is fixed boilerplate, not "the source list" itself —
|
|
542
|
+
# only the `source` lines that follow are parsed out of SKILL.md.
|
|
543
|
+
printf 'PLUGIN_ROOT=%q\n' "$PLUGIN_ROOT"
|
|
544
|
+
printf 'export CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT"\n'
|
|
545
|
+
grep -E '^source "\$CLAUDE_PLUGIN_ROOT/' "$skill"
|
|
546
|
+
printf 'librarian_lesson_promote %q %q\n' "$PROJECT_KEY" "skill01"
|
|
547
|
+
} > "$script"
|
|
548
|
+
|
|
549
|
+
run bash "$script"
|
|
550
|
+
[ "$status" -eq 0 ]
|
|
551
|
+
[ -f "$(_dir)/approved/skill01.json" ]
|
|
552
|
+
}
|