@onlooker-community/ecosystem 0.23.1 → 0.25.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.
Files changed (42) hide show
  1. package/.claude-plugin/marketplace.json +13 -0
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.github/workflows/autofix.yml +65 -0
  4. package/.release-please-manifest.json +4 -3
  5. package/CHANGELOG.md +14 -0
  6. package/CLAUDE.md +1 -0
  7. package/package.json +3 -3
  8. package/plugins/assayer/.claude-plugin/plugin.json +14 -0
  9. package/plugins/assayer/CHANGELOG.md +10 -0
  10. package/plugins/assayer/README.md +114 -0
  11. package/plugins/assayer/config.json +14 -0
  12. package/plugins/assayer/docs/adr/001-verify-claims-against-transcript-evidence.md +57 -0
  13. package/plugins/assayer/docs/design.md +72 -0
  14. package/plugins/assayer/hooks/hooks.json +15 -0
  15. package/plugins/assayer/scripts/hooks/assayer-stop.sh +249 -0
  16. package/plugins/assayer/scripts/lib/assayer-config.sh +88 -0
  17. package/plugins/assayer/scripts/lib/assayer-events.sh +85 -0
  18. package/plugins/assayer/scripts/lib/assayer-extract.sh +87 -0
  19. package/plugins/assayer/scripts/lib/assayer-project-key.sh +69 -0
  20. package/plugins/assayer/scripts/lib/assayer-transcript.sh +99 -0
  21. package/plugins/assayer/scripts/lib/assayer-ulid.sh +46 -0
  22. package/plugins/assayer/scripts/lib/assayer-verify.sh +95 -0
  23. package/plugins/compass/README.md +173 -0
  24. package/plugins/counsel/README.md +98 -0
  25. package/plugins/governor/README.md +127 -0
  26. package/plugins/librarian/.claude-plugin/plugin.json +2 -2
  27. package/plugins/librarian/CHANGELOG.md +7 -0
  28. package/plugins/librarian/scripts/lib/librarian-cli.sh +339 -0
  29. package/plugins/librarian/skills/librarian/SKILL.md +63 -0
  30. package/plugins/scribe/.claude-plugin/plugin.json +1 -3
  31. package/plugins/scribe/README.md +118 -0
  32. package/plugins/warden/README.md +185 -0
  33. package/release-please-config.json +16 -0
  34. package/test/bats/assayer-config.bats +60 -0
  35. package/test/bats/assayer-events.bats +99 -0
  36. package/test/bats/assayer-extract.bats +76 -0
  37. package/test/bats/assayer-project-key.bats +58 -0
  38. package/test/bats/assayer-stop-hook.bats +81 -0
  39. package/test/bats/assayer-transcript.bats +72 -0
  40. package/test/bats/assayer-ulid.bats +31 -0
  41. package/test/bats/assayer-verify.bats +89 -0
  42. package/test/bats/librarian-cli.bats +305 -0
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env bats
2
+
3
+ # Exercises the Assayer ULID generator.
4
+
5
+ setup() {
6
+ source "${BATS_TEST_DIRNAME}/../helpers/setup.bash"
7
+ setup_test_env
8
+ PLUGIN_ROOT="${REPO_ROOT}/plugins/assayer"
9
+ # shellcheck disable=SC1091
10
+ source "${PLUGIN_ROOT}/scripts/lib/assayer-ulid.sh"
11
+ }
12
+
13
+ @test "ulid is 26 chars of Crockford Base32" {
14
+ run assayer_ulid
15
+ [ "$status" -eq 0 ]
16
+ [ "${#output}" -eq 26 ]
17
+ [[ "$output" =~ ^[0-9A-HJKMNP-TV-Z]{26}$ ]]
18
+ }
19
+
20
+ @test "ulids are unique across calls" {
21
+ a=$(assayer_ulid)
22
+ b=$(assayer_ulid)
23
+ [ "$a" != "$b" ]
24
+ }
25
+
26
+ @test "ulids are lexicographically time-ordered" {
27
+ a=$(assayer_ulid)
28
+ sleep 0.01
29
+ b=$(assayer_ulid)
30
+ [[ "$a" < "$b" || "$a" == "$b" ]]
31
+ }
@@ -0,0 +1,89 @@
1
+ #!/usr/bin/env bats
2
+
3
+ # Exercises the deterministic claim verifier: assayer_classify_claim and
4
+ # assayer_audit_verdict. Pure logic — no LLM, no schema, no filesystem.
5
+
6
+ setup() {
7
+ source "${BATS_TEST_DIRNAME}/../helpers/setup.bash"
8
+ setup_test_env
9
+ PLUGIN_ROOT="${REPO_ROOT}/plugins/assayer"
10
+ # shellcheck disable=SC1091
11
+ source "${PLUGIN_ROOT}/scripts/lib/assayer-verify.sh"
12
+ }
13
+
14
+ CMDS_FAILING_TEST='[{"command":"npm test","is_error":true,"excerpt":"1 failed, 32 passed"},{"command":"git status","is_error":false,"excerpt":""}]'
15
+
16
+ @test "tests_pass claim is contradicted by a failing test command" {
17
+ run assayer_classify_claim '{"text":"tests pass","type":"tests_pass","command_keyword":"test","confidence":0.9}' "$CMDS_FAILING_TEST"
18
+ [ "$status" -eq 0 ]
19
+ [ "$(printf '%s' "$output" | jq -r '.verdict')" = "contradicted" ]
20
+ [ "$(printf '%s' "$output" | jq -r '.evidence_command')" = "npm test" ]
21
+ [ "$(printf '%s' "$output" | jq -r '.excerpt')" = "1 failed, 32 passed" ]
22
+ }
23
+
24
+ @test "build_succeeds claim is corroborated by a passing build" {
25
+ run assayer_classify_claim '{"text":"build is green","type":"build_succeeds","command_keyword":"build","confidence":0.9}' \
26
+ '[{"command":"npm run build","is_error":false,"excerpt":"done"}]'
27
+ [ "$status" -eq 0 ]
28
+ [ "$(printf '%s' "$output" | jq -r '.verdict')" = "corroborated" ]
29
+ }
30
+
31
+ @test "claim with no matching command is unverified (no_matching_command)" {
32
+ run assayer_classify_claim '{"text":"lint clean","type":"lint_clean","command_keyword":"lint","confidence":0.9}' \
33
+ '[{"command":"npm test","is_error":false,"excerpt":""}]'
34
+ [ "$status" -eq 0 ]
35
+ [ "$(printf '%s' "$output" | jq -r '.verdict')" = "unverified" ]
36
+ [ "$(printf '%s' "$output" | jq -r '.reason')" = "no_matching_command" ]
37
+ }
38
+
39
+ @test "generic claim with no keyword is unverified (ambiguous)" {
40
+ run assayer_classify_claim '{"text":"deploy healthy","type":"generic","command_keyword":"","confidence":0.9}' \
41
+ "$CMDS_FAILING_TEST"
42
+ [ "$status" -eq 0 ]
43
+ [ "$(printf '%s' "$output" | jq -r '.verdict')" = "unverified" ]
44
+ [ "$(printf '%s' "$output" | jq -r '.reason')" = "ambiguous" ]
45
+ }
46
+
47
+ @test "most recent matching command wins (fix-and-rerun)" {
48
+ # Failing test first, passing test after a fix — the later run is authoritative.
49
+ run assayer_classify_claim '{"text":"tests pass now","type":"tests_pass","command_keyword":"test","confidence":0.9}' \
50
+ '[{"command":"npm test","is_error":true,"excerpt":"fail"},{"command":"npm test","is_error":false,"excerpt":"pass"}]'
51
+ [ "$status" -eq 0 ]
52
+ [ "$(printf '%s' "$output" | jq -r '.verdict')" = "corroborated" ]
53
+ }
54
+
55
+ @test "types_check claim matches a tsc command" {
56
+ run assayer_classify_claim '{"text":"types check out","type":"types_check","command_keyword":"","confidence":0.9}' \
57
+ '[{"command":"npx tsc --noEmit","is_error":true,"excerpt":"TS2345"}]'
58
+ [ "$status" -eq 0 ]
59
+ [ "$(printf '%s' "$output" | jq -r '.verdict')" = "contradicted" ]
60
+ }
61
+
62
+ @test "command_keyword matches when type is generic" {
63
+ run assayer_classify_claim '{"text":"migration ran","type":"generic","command_keyword":"migrate","confidence":0.9}' \
64
+ '[{"command":"rails db:migrate","is_error":false,"excerpt":"migrated"}]'
65
+ [ "$status" -eq 0 ]
66
+ [ "$(printf '%s' "$output" | jq -r '.verdict')" = "corroborated" ]
67
+ }
68
+
69
+ @test "empty claim defaults to unverified ambiguous" {
70
+ run assayer_classify_claim "" "$CMDS_FAILING_TEST"
71
+ [ "$status" -eq 0 ]
72
+ [ "$(printf '%s' "$output" | jq -r '.verdict')" = "unverified" ]
73
+ }
74
+
75
+ @test "audit verdict is contradictions_found when any contradiction" {
76
+ [ "$(assayer_audit_verdict 1 3 0)" = "contradictions_found" ]
77
+ }
78
+
79
+ @test "audit verdict is clean with corroborations and no contradictions" {
80
+ [ "$(assayer_audit_verdict 0 2 1)" = "clean" ]
81
+ }
82
+
83
+ @test "audit verdict is clean when only unverified claims" {
84
+ [ "$(assayer_audit_verdict 0 0 2)" = "clean" ]
85
+ }
86
+
87
+ @test "audit verdict is nothing_to_verify when all counts zero" {
88
+ [ "$(assayer_audit_verdict 0 0 0)" = "nothing_to_verify" ]
89
+ }
@@ -0,0 +1,305 @@
1
+ #!/usr/bin/env bats
2
+ #
3
+ # Exercises the librarian-cli surface that the /librarian review skill
4
+ # drives. Each test seeds one or more proposals directly into the
5
+ # librarian storage layer (skipping the SessionEnd scan pipeline) and
6
+ # verifies that list/show/accept/reject/defer/status behave as the
7
+ # skill expects:
8
+ #
9
+ # - list → returns a count + table, sized to pending proposals only
10
+ # - show → renders provenance + body, fails clean on unknown id
11
+ # - accept → writes the typed memory file with provenance frontmatter,
12
+ # appends to MEMORY.md, sets status=accepted, emits
13
+ # librarian.proposal.accepted
14
+ # - reject → writes a body-hash tombstone, sets status=rejected,
15
+ # emits librarian.proposal.rejected AND
16
+ # librarian.tombstone.created
17
+ # - defer → leaves status pending but stamps the proposal so a
18
+ # reviewer can tell it was visited
19
+ # - status → reports pending/accepted/rejected counts
20
+ #
21
+ # The CLI is sourced into the bats shell directly (it's a library, not
22
+ # a hook), so assertions can read both stdout and side-effects on disk.
23
+
24
+ setup() {
25
+ source "${BATS_TEST_DIRNAME}/../helpers/setup.bash"
26
+ setup_test_env
27
+
28
+ PLUGIN_ROOT="${REPO_ROOT}/plugins/librarian"
29
+ export CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT"
30
+ export ONLOOKER_ECOSYSTEM_ROOT="$REPO_ROOT"
31
+
32
+ PROJECT_REPO="${BATS_TEST_TMPDIR}/repo"
33
+ mkdir -p "$PROJECT_REPO"
34
+ git -C "$PROJECT_REPO" init -q
35
+ git -C "$PROJECT_REPO" config user.email t@example.com
36
+ git -C "$PROJECT_REPO" config user.name "Test"
37
+ git -C "$PROJECT_REPO" remote add origin git@github.com:org/librarian-cli-test.git
38
+
39
+ # Source the five libs the skill loads, in the same order the SKILL.md
40
+ # walkthrough sources them. librarian-cli depends on storage + emit +
41
+ # project-key (and indirectly config).
42
+ # shellcheck disable=SC1091
43
+ source "${PLUGIN_ROOT}/scripts/lib/librarian-config.sh"
44
+ # shellcheck disable=SC1091
45
+ source "${PLUGIN_ROOT}/scripts/lib/librarian-project-key.sh"
46
+ # shellcheck disable=SC1091
47
+ source "${PLUGIN_ROOT}/scripts/lib/librarian-storage.sh"
48
+ # shellcheck disable=SC1091
49
+ source "${PLUGIN_ROOT}/scripts/lib/librarian-emit.sh"
50
+ # shellcheck disable=SC1091
51
+ source "${PLUGIN_ROOT}/scripts/lib/librarian-cli.sh"
52
+
53
+ PROJECT_KEY=$(librarian_project_key "$PROJECT_REPO")
54
+ [ -n "$PROJECT_KEY" ]
55
+
56
+ LIBRARIAN_DIR="${ONLOOKER_DIR}/librarian/${PROJECT_KEY}"
57
+ ONLOOKER_EVENTS_LOG="${ONLOOKER_DIR}/logs/onlooker-events.jsonl"
58
+ export ONLOOKER_EVENTS_LOG
59
+
60
+ # Where librarian_cli_accept writes typed memory. The CLI derives the
61
+ # encoded path from cwd when CLAUDE_PROJECT_ENCODED is unset; mirror
62
+ # that derivation so tests can assert against the resulting file.
63
+ ABS_CWD=$(cd "$PROJECT_REPO" && pwd -P)
64
+ ENCODED=$(printf '%s' "$ABS_CWD" | sed -E 's#/#-#g')
65
+ MEM_DIR="${TEST_HOME}/.claude/projects/${ENCODED}/memory"
66
+
67
+ librarian_storage_init "$PROJECT_KEY"
68
+ }
69
+
70
+ # Seed a single proposal JSON file directly into the storage layer.
71
+ # Usage: _seed_proposal <id> <type> <title> <filename> <body> [confidence] [status]
72
+ _seed_proposal() {
73
+ local id="$1" type="$2" title="$3" filename="$4" body="$5"
74
+ local confidence="${6:-0.82}" status="${7:-pending}"
75
+ local now json
76
+ now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
77
+ json=$(jq -cn \
78
+ --arg id "$id" \
79
+ --arg type "$type" \
80
+ --arg title "$title" \
81
+ --arg filename "$filename" \
82
+ --arg body "$body" \
83
+ --argjson conf "$confidence" \
84
+ --arg status "$status" \
85
+ --arg now "$now" \
86
+ --arg project_key "$PROJECT_KEY" \
87
+ '{
88
+ id: $id,
89
+ project_key: $project_key,
90
+ status: $status,
91
+ conflict_state: "none",
92
+ created_at: $now,
93
+ updated_at: $now,
94
+ source_session_ids: ["sess-seeded"],
95
+ source_artifact_ids: ["01ARTIFACT00000000000000"],
96
+ proposed: {
97
+ type: $type,
98
+ title: $title,
99
+ filename: $filename,
100
+ body: $body,
101
+ classifier_confidence: $conf
102
+ }
103
+ }')
104
+ librarian_storage_write_proposal "$PROJECT_KEY" "$id" "$json" >/dev/null
105
+ }
106
+
107
+ @test "list reports no-pending when the queue is empty" {
108
+ run librarian_cli_list "$PROJECT_REPO"
109
+ [ "$status" -eq 0 ]
110
+ [[ "$output" == *"No pending proposals."* ]]
111
+ }
112
+
113
+ @test "list summarizes pending proposals and ignores resolved ones" {
114
+ _seed_proposal "01LISTPENDINGA000000000000" \
115
+ "feedback" "Prefer functional patterns" "feedback_functional.md" \
116
+ "Body A."
117
+ _seed_proposal "01LISTPENDINGB000000000000" \
118
+ "project" "Auth rewrite is compliance" "project_auth_compliance.md" \
119
+ "Body B." "0.91"
120
+ _seed_proposal "01LISTACCEPTED0000000000000" \
121
+ "user" "Already accepted" "user_old.md" \
122
+ "Body C." "0.7" "accepted"
123
+
124
+ run librarian_cli_list "$PROJECT_REPO"
125
+ [ "$status" -eq 0 ]
126
+ # Header counts only pending entries (2 of 3).
127
+ [[ "$output" == *"2 pending proposal"* ]]
128
+ # Both pending titles surface.
129
+ [[ "$output" == *"Prefer functional patterns"* ]]
130
+ [[ "$output" == *"Auth rewrite is compliance"* ]]
131
+ # Pending rows include full IDs that can be used with show/accept/reject/defer.
132
+ [[ "$output" == *"01LISTPENDINGA000000000000"* ]]
133
+ [[ "$output" == *"01LISTPENDINGB000000000000"* ]]
134
+ # Accepted entry does NOT appear in the list output.
135
+ [[ "$output" != *"Already accepted"* ]]
136
+ }
137
+
138
+ @test "show renders provenance + body for an existing proposal" {
139
+ _seed_proposal "01SHOWPROPOSAL0000000000000" \
140
+ "feedback" "Some title" "feedback_x.md" \
141
+ "Body of the memory."
142
+
143
+ run librarian_cli_show "01SHOWPROPOSAL0000000000000" "$PROJECT_REPO"
144
+ [ "$status" -eq 0 ]
145
+ [[ "$output" == *"01SHOWPROPOSAL0000000000000"* ]]
146
+ [[ "$output" == *"type: feedback"* ]]
147
+ [[ "$output" == *"filename: feedback_x.md"* ]]
148
+ [[ "$output" == *"classifier_confidence: 0.82"* ]]
149
+ [[ "$output" == *"Body of the memory."* ]]
150
+ }
151
+
152
+ @test "show fails clean on an unknown proposal id" {
153
+ run librarian_cli_show "01NOSUCHPROPOSAL00000000000" "$PROJECT_REPO"
154
+ [ "$status" -eq 1 ]
155
+ [[ "$output" == *"not found"* ]]
156
+ }
157
+
158
+ @test "accept writes a memory file with provenance frontmatter" {
159
+ _seed_proposal "01ACCEPTPROPOSAL000000000000" \
160
+ "feedback" "Prefer functional patterns" "feedback_functional.md" \
161
+ "User prefers functional patterns.
162
+
163
+ **Why:** Stated explicitly.
164
+ **How to apply:** Default to plain functions."
165
+
166
+ run librarian_cli_accept "01ACCEPTPROPOSAL000000000000" "$PROJECT_REPO"
167
+ [ "$status" -eq 0 ]
168
+ [[ "$output" == *"Accepted."* ]]
169
+
170
+ local out_file="${MEM_DIR}/feedback_functional.md"
171
+ [ -f "$out_file" ]
172
+
173
+ # Frontmatter records who promoted it, when, and from where.
174
+ grep -q "^source: librarian$" "$out_file"
175
+ grep -q "^type: feedback$" "$out_file"
176
+ grep -q "^name: Prefer functional patterns$" "$out_file"
177
+ grep -q "^classifier_confidence: 0.82$" "$out_file"
178
+ grep -q "^promoted_at: " "$out_file"
179
+ # Body survives in full.
180
+ grep -q "Default to plain functions" "$out_file"
181
+ }
182
+
183
+ @test "accept appends to MEMORY.md and creates it if missing" {
184
+ _seed_proposal "01ACCEPTINDEX000000000000000" \
185
+ "project" "Auth rewrite is compliance" "project_auth_compliance.md" \
186
+ "Compliance-driven."
187
+
188
+ [ ! -f "${MEM_DIR}/MEMORY.md" ]
189
+
190
+ run librarian_cli_accept "01ACCEPTINDEX000000000000000" "$PROJECT_REPO"
191
+ [ "$status" -eq 0 ]
192
+
193
+ [ -f "${MEM_DIR}/MEMORY.md" ]
194
+ grep -F -q "(project_auth_compliance.md)" "${MEM_DIR}/MEMORY.md"
195
+ grep -F -q "Auth rewrite is compliance" "${MEM_DIR}/MEMORY.md"
196
+ }
197
+
198
+ @test "accept marks the proposal accepted and emits librarian.proposal.accepted" {
199
+ _seed_proposal "01ACCEPTEVENT000000000000000" \
200
+ "user" "User role" "user_role.md" "Body."
201
+
202
+ run librarian_cli_accept "01ACCEPTEVENT000000000000000" "$PROJECT_REPO"
203
+ [ "$status" -eq 0 ]
204
+
205
+ # Proposal file flipped to accepted.
206
+ local proposal_path="${LIBRARIAN_DIR}/proposals/01ACCEPTEVENT000000000000000.json"
207
+ jq -e '.status == "accepted"' "$proposal_path" >/dev/null
208
+ jq -e '.final_filename == "user_role.md"' "$proposal_path" >/dev/null
209
+
210
+ # Event landed in the canonical events log.
211
+ [ -f "$ONLOOKER_EVENTS_LOG" ]
212
+ grep -q '"event_type":"librarian.proposal.accepted"' "$ONLOOKER_EVENTS_LOG"
213
+ grep '"event_type":"librarian.proposal.accepted"' "$ONLOOKER_EVENTS_LOG" \
214
+ | jq -e '.payload.proposal_id == "01ACCEPTEVENT000000000000000" and .payload.final_filename == "user_role.md"' >/dev/null
215
+ }
216
+
217
+ @test "accept refuses path-traversal filenames and writes nothing" {
218
+ _seed_proposal "01ACCEPTUNSAFE00000000000000" \
219
+ "user" "Bad filename" "../escape.md" "Body."
220
+
221
+ run librarian_cli_accept "01ACCEPTUNSAFE00000000000000" "$PROJECT_REPO"
222
+ [ "$status" -eq 1 ]
223
+ [[ "$output" == *"Failed to write memory file."* ]]
224
+
225
+ # No memory file written anywhere under MEM_DIR or its parent.
226
+ [ ! -f "${MEM_DIR}/../escape.md" ]
227
+ [ ! -f "${MEM_DIR}/escape.md" ]
228
+
229
+ # Proposal stays pending so a reviewer can resolve it manually.
230
+ local proposal_path="${LIBRARIAN_DIR}/proposals/01ACCEPTUNSAFE00000000000000.json"
231
+ jq -e '.status == "pending"' "$proposal_path" >/dev/null
232
+ }
233
+
234
+ @test "reject writes a tombstone and marks the proposal rejected" {
235
+ local body="This is the body whose hash anchors the tombstone."
236
+ _seed_proposal "01REJECTPROPOSAL00000000000" \
237
+ "feedback" "Some idea" "feedback_some_idea.md" "$body"
238
+
239
+ run librarian_cli_reject "01REJECTPROPOSAL00000000000" "stale guidance" "$PROJECT_REPO"
240
+ [ "$status" -eq 0 ]
241
+ [[ "$output" == *"Rejected"* ]]
242
+ [[ "$output" == *"stale guidance"* ]]
243
+
244
+ # Proposal status now rejected with the reason captured.
245
+ local proposal_path="${LIBRARIAN_DIR}/proposals/01REJECTPROPOSAL00000000000.json"
246
+ jq -e '.status == "rejected"' "$proposal_path" >/dev/null
247
+ jq -e '.reason == "stale guidance"' "$proposal_path" >/dev/null
248
+
249
+ # Tombstone file present, keyed on the body hash.
250
+ local expected_hash
251
+ expected_hash=$(librarian_body_hash "$body")
252
+ [ -n "$expected_hash" ]
253
+ [ -f "${LIBRARIAN_DIR}/tombstones/${expected_hash}.json" ]
254
+ librarian_storage_has_tombstone "$PROJECT_KEY" "$expected_hash"
255
+
256
+ # Both events fired.
257
+ grep -q '"event_type":"librarian.proposal.rejected"' "$ONLOOKER_EVENTS_LOG"
258
+ grep -q '"event_type":"librarian.tombstone.created"' "$ONLOOKER_EVENTS_LOG"
259
+ grep '"event_type":"librarian.proposal.rejected"' "$ONLOOKER_EVENTS_LOG" \
260
+ | jq -e '.payload.reason == "stale guidance"' >/dev/null
261
+ grep '"event_type":"librarian.tombstone.created"' "$ONLOOKER_EVENTS_LOG" \
262
+ | jq -e --arg h "$expected_hash" '.payload.body_hash == $h' >/dev/null
263
+ }
264
+
265
+ @test "defer stamps the proposal but leaves it pending" {
266
+ _seed_proposal "01DEFERPROPOSAL000000000000" \
267
+ "user" "Maybe later" "user_maybe.md" "Body."
268
+
269
+ run librarian_cli_defer "01DEFERPROPOSAL000000000000" "$PROJECT_REPO"
270
+ [ "$status" -eq 0 ]
271
+ [[ "$output" == *"Deferred"* ]]
272
+
273
+ local proposal_path="${LIBRARIAN_DIR}/proposals/01DEFERPROPOSAL000000000000.json"
274
+ jq -e '.status == "pending"' "$proposal_path" >/dev/null
275
+ jq -e '.deferred == true' "$proposal_path" >/dev/null
276
+
277
+ # Defer should NOT touch the memory store.
278
+ [ ! -d "$MEM_DIR" ] || [ -z "$(ls -A "$MEM_DIR" 2>/dev/null)" ]
279
+ }
280
+
281
+ @test "status reports counts across pending, accepted, and rejected" {
282
+ _seed_proposal "01STATUSPENDING0000000000000" "user" "P" "user_p.md" "p" "0.7" "pending"
283
+ _seed_proposal "01STATUSACCEPTED000000000000" "user" "A" "user_a.md" "a" "0.7" "accepted"
284
+ _seed_proposal "01STATUSREJECTED000000000000" "user" "R" "user_r.md" "r" "0.7" "rejected"
285
+
286
+ run librarian_cli_status "$PROJECT_REPO"
287
+ [ "$status" -eq 0 ]
288
+ [[ "$output" == *"pending: 1"* ]]
289
+ [[ "$output" == *"accepted: 1"* ]]
290
+ [[ "$output" == *"rejected: 1"* ]]
291
+ }
292
+
293
+ @test "librarian_cli dispatch routes to the right subcommand" {
294
+ _seed_proposal "01DISPATCH00000000000000000" \
295
+ "user" "Dispatch test" "user_dispatch.md" "Body."
296
+
297
+ # Unknown action returns exit 2.
298
+ run librarian_cli "explode"
299
+ [ "$status" -eq 2 ]
300
+
301
+ # Known action delegates correctly.
302
+ run librarian_cli "show" "01DISPATCH00000000000000000" "$PROJECT_REPO"
303
+ [ "$status" -eq 0 ]
304
+ [[ "$output" == *"Dispatch test"* ]]
305
+ }