@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.
Files changed (112) hide show
  1. package/.claude/skills/writing-tests/SKILL.md +27 -0
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.github/workflows/release.yml +14 -14
  4. package/.release-please-manifest.json +7 -7
  5. package/AGENTS.md +116 -0
  6. package/CHANGELOG.md +175 -0
  7. package/CLAUDE.md +5 -0
  8. package/docs/lesson-promotion-pipeline.md +6 -6
  9. package/docs/superpowers/plans/2026-08-10-lesson-confirmation.md +1273 -0
  10. package/docs/superpowers/plans/2026-08-11-lesson-judging.md +1232 -0
  11. package/docs/superpowers/plans/2026-08-11-lesson-unconfirm.md +458 -0
  12. package/docs/superpowers/plans/2026-08-12-author-key.md +567 -0
  13. package/docs/superpowers/plans/2026-08-13-approved-pool.md +747 -0
  14. package/docs/superpowers/plans/2026-08-13-librarian-cluster.md +392 -0
  15. package/docs/superpowers/plans/2026-08-14-criterion-scores-consumers.md +1374 -0
  16. package/docs/superpowers/plans/2026-08-14-criterion-scores-schema.md +277 -0
  17. package/docs/superpowers/plans/2026-08-16-cartographer-undocumented-entity.md +741 -0
  18. package/docs/superpowers/specs/2026-08-10-lesson-confirmation-design.md +213 -0
  19. package/docs/superpowers/specs/2026-08-11-lesson-judging-design.md +267 -0
  20. package/docs/superpowers/specs/2026-08-11-lesson-unconfirm-design.md +152 -0
  21. package/docs/superpowers/specs/2026-08-12-author-key-design.md +317 -0
  22. package/docs/superpowers/specs/2026-08-13-approved-pool-design.md +232 -0
  23. package/docs/superpowers/specs/2026-08-14-criterion-scores-design.md +254 -0
  24. package/docs/superpowers/specs/2026-08-16-cartographer-undocumented-entity-design.md +249 -0
  25. package/package.json +2 -2
  26. package/plugins/assayer/.claude-plugin/plugin.json +1 -1
  27. package/plugins/assayer/CHANGELOG.md +7 -0
  28. package/plugins/assayer/scripts/lib/assayer-config.sh +6 -0
  29. package/plugins/cartographer/.claude-plugin/plugin.json +1 -1
  30. package/plugins/cartographer/CHANGELOG.md +54 -0
  31. package/plugins/cartographer/README.md +56 -1
  32. package/plugins/cartographer/config.json +7 -1
  33. package/plugins/cartographer/scripts/hooks/cartographer-post-write.sh +4 -4
  34. package/plugins/cartographer/scripts/hooks/cartographer-session-start.sh +7 -4
  35. package/plugins/cartographer/scripts/lib/cartographer-collect.sh +8 -1
  36. package/plugins/cartographer/scripts/lib/cartographer-config.sh +26 -0
  37. package/plugins/cartographer/scripts/lib/cartographer-events.sh +90 -0
  38. package/plugins/cartographer/scripts/lib/cartographer-filter.sh +105 -0
  39. package/plugins/cartographer/scripts/lib/cartographer-omission.sh +130 -0
  40. package/plugins/cartographer/scripts/lib/cartographer-resolve.sh +132 -0
  41. package/plugins/cartographer/scripts/run-audit.sh +136 -53
  42. package/plugins/cartographer/skills/cartographer/SKILL.md +33 -9
  43. package/plugins/curator/.claude-plugin/plugin.json +1 -1
  44. package/plugins/curator/CHANGELOG.md +7 -0
  45. package/plugins/curator/scripts/lib/curator-emit.sh +2 -1
  46. package/plugins/historian/.claude-plugin/plugin.json +1 -1
  47. package/plugins/historian/CHANGELOG.md +7 -0
  48. package/plugins/historian/scripts/lib/historian-emit.sh +2 -1
  49. package/plugins/librarian/.claude-plugin/plugin.json +1 -1
  50. package/plugins/librarian/CHANGELOG.md +71 -0
  51. package/plugins/librarian/config.json +30 -1
  52. package/plugins/librarian/docs/adr/002-agent-definitions-are-shared-assets.md +82 -0
  53. package/plugins/librarian/scripts/hooks/librarian-session-end.sh +28 -0
  54. package/plugins/librarian/scripts/hooks/librarian-session-start.sh +42 -12
  55. package/plugins/librarian/scripts/lib/librarian-author-key.sh +279 -0
  56. package/plugins/librarian/scripts/lib/librarian-cli.sh +346 -2
  57. package/plugins/librarian/scripts/lib/librarian-emit.sh +2 -1
  58. package/plugins/librarian/scripts/lib/librarian-lesson-judge.sh +349 -0
  59. package/plugins/librarian/scripts/lib/librarian-lesson-promote.sh +225 -0
  60. package/plugins/librarian/scripts/lib/librarian-lesson-review.sh +312 -0
  61. package/plugins/librarian/scripts/lib/librarian-lesson-rubric.sh +56 -0
  62. package/plugins/librarian/scripts/lib/librarian-lesson-storage.sh +82 -13
  63. package/plugins/librarian/scripts/lib/librarian-lesson-transform.sh +6 -1
  64. package/plugins/librarian/scripts/lib/librarian-lesson-validate.sh +131 -62
  65. package/plugins/librarian/skills/librarian/SKILL.md +98 -1
  66. package/plugins/tribunal/.claude-plugin/plugin.json +1 -1
  67. package/plugins/tribunal/CHANGELOG.md +63 -0
  68. package/plugins/tribunal/agents/tribunal-judge-adversarial.md +34 -0
  69. package/plugins/tribunal/agents/tribunal-judge-security.md +33 -0
  70. package/plugins/tribunal/agents/tribunal-judge-standard.md +12 -0
  71. package/plugins/tribunal/config.json +1 -0
  72. package/plugins/tribunal/scripts/lib/tribunal-aggregate.sh +154 -9
  73. package/plugins/tribunal/scripts/lib/tribunal-gate.sh +195 -16
  74. package/plugins/tribunal/skills/tribunal/SKILL.md +5 -3
  75. package/scripts/lib/prompt-rules.sh +6 -1
  76. package/test/bats/archivist-inject.bats +1 -1
  77. package/test/bats/assayer-extract.bats +2 -2
  78. package/test/bats/bursar-session-start.bats +3 -3
  79. package/test/bats/cartographer-config.bats +43 -0
  80. package/test/bats/cartographer-events.bats +267 -0
  81. package/test/bats/cartographer-filter.bats +169 -0
  82. package/test/bats/cartographer-lock.bats +3 -3
  83. package/test/bats/cartographer-omission.bats +202 -0
  84. package/test/bats/cartographer-resolve.bats +287 -0
  85. package/test/bats/cartographer-run-audit.bats +325 -0
  86. package/test/bats/compass-sanitizer.bats +11 -11
  87. package/test/bats/compass-transcript.bats +2 -2
  88. package/test/bats/config.bats +15 -15
  89. package/test/bats/curator-session-start.bats +10 -3
  90. package/test/bats/emit-payload-default.bats +52 -0
  91. package/test/bats/governor-ledger.bats +1 -1
  92. package/test/bats/historian-prompt-submit.bats +1 -1
  93. package/test/bats/inspector-post-write-hook.bats +4 -4
  94. package/test/bats/librarian-author-key.bats +477 -0
  95. package/test/bats/librarian-cli.bats +16 -16
  96. package/test/bats/librarian-lesson-judge.bats +884 -0
  97. package/test/bats/librarian-lesson-promote.bats +552 -0
  98. package/test/bats/librarian-lesson-review.bats +900 -0
  99. package/test/bats/librarian-lesson-transform.bats +17 -3
  100. package/test/bats/librarian-session-end.bats +161 -0
  101. package/test/bats/librarian-session-start.bats +36 -2
  102. package/test/bats/lineage-config.bats +1 -1
  103. package/test/bats/lineage-redact.bats +5 -5
  104. package/test/bats/session-tracker.bats +4 -4
  105. package/test/bats/tribunal-aggregate.bats +354 -1
  106. package/test/bats/tribunal-gate.bats +631 -0
  107. package/test/bats/tribunal-judge-agents.bats +119 -0
  108. package/test/bats/tribunal-jury.bats +1 -1
  109. package/test/bats/turn-tracker.bats +1 -1
  110. package/test/bats/warden-sanitizer.bats +3 -3
  111. package/test/bats/worktree-tracker.bats +2 -2
  112. package/test/node/lesson-validate-agreement.test.mjs +35 -6
@@ -0,0 +1,458 @@
1
+ # Lesson Unconfirm Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** Give a human a way back from a confirmed lesson, before the jury has seen it.
6
+
7
+ **Architecture:** Two tasks. The first adds the snapshot to `librarian_lesson_confirm` and the `librarian_lesson_unconfirm` state transition beside it, in the same lib — they are one mechanism and a reviewer cannot sensibly accept one without the other. The second surfaces it as a CLI verb and a skill route.
8
+
9
+ **Tech Stack:** bash, `jq`, `bats`.
10
+
11
+ **Spec:** `docs/superpowers/specs/2026-08-11-lesson-unconfirm-design.md`
12
+
13
+ ## Global Constraints
14
+
15
+ - Bash only. `jq` for JSON. TAB-indented. `shellcheck -S error -x` must be clean.
16
+ - Always `${ONLOOKER_DIR:-$HOME/.onlooker}` — never a literal `~/.onlooker`.
17
+ - **Never inline braces in a parameter-expansion default.** `${N:-{\}}` and `${N:-{}}` are both wrong. Default in a separate statement: `local p="${3:-}"` then `[ -z "$p" ] && p='{}'`. Guarded by `test/bats/emit-payload-default.bats`.
18
+ - bats runs under macOS bash 3.2, where a failing **non-final** `[[ ]]` does not fail the test. Use `[ ]`, or append `|| return 1`. Break each new assertion once to confirm it discriminates.
19
+ - **This stage must never invoke a model.** No `claude`, no network.
20
+ - **No event emission.** `librarian.lesson.*` is unregistered in `@onlooker-community/schema` 2.11.0; the emitter exits 1 on an unknown `event_type`.
21
+ - `unconfirm` proceeds only from `confirmed`. Every other status is refused, naming it.
22
+ - American English. Commit style `<type>(<scope>): <subject> :emoji:` with a why-focused body.
23
+
24
+ ## File Structure
25
+
26
+ | File | Responsibility |
27
+ |---|---|
28
+ | `plugins/librarian/scripts/lib/librarian-lesson-review.sh` | Modified: snapshot on confirm; new `librarian_lesson_unconfirm` |
29
+ | `plugins/librarian/scripts/lib/librarian-cli.sh` | Modified: `lessons unconfirm` verb + dispatch arm |
30
+ | `plugins/librarian/skills/librarian/SKILL.md` | Modified: verb table and walk |
31
+ | `test/bats/librarian-lesson-review.bats` | Modified: append coverage for both tasks |
32
+
33
+ **Why the snapshot and the verb are one task.** The snapshot exists only to serve `unconfirm`; shipping it alone adds a field nothing reads, and shipping `unconfirm` alone leaves the justification case half-broken. A reviewer would reject either in isolation.
34
+
35
+ ---
36
+
37
+ ### Task 1: The snapshot and the unconfirm transition
38
+
39
+ **Files:**
40
+ - Modify: `plugins/librarian/scripts/lib/librarian-lesson-review.sh`
41
+ - Test: `test/bats/librarian-lesson-review.bats` (append)
42
+
43
+ **Interfaces:**
44
+ - Consumes: `librarian_lessons_dir <key>` (existing).
45
+ - Produces: `librarian_lesson_unconfirm <key> <lesson_id>` → exit 0 on success or when already `pending`; exit 1 on `passed`, an unrecognized status, or a missing lesson. On success from `confirmed`: sets `status: "pending"`, removes `visibility`, `confirmed_at`, and `candidate_before_confirm`, and restores `.candidate` from the snapshot when one exists.
46
+ - Also produces: `librarian_lesson_confirm` now writes `candidate_before_confirm` **only** when it rewrites scope.
47
+
48
+ - [ ] **Step 1: Write the failing tests**
49
+
50
+ Append to `test/bats/librarian-lesson-review.bats`. The file already defines `_review_setup`, `_seed_pending`, `_evidence`, `_candidate`, `_versioned`, `_indep` — reuse them.
51
+
52
+ ```bash
53
+ @test "confirm without a justification writes no snapshot" {
54
+ _review_setup
55
+ id=$(_seed_pending)
56
+ librarian_lesson_confirm "$PROJECT_KEY" "$id" "org"
57
+ run jq -e 'has("candidate_before_confirm")' "${LESSONS_DIR}/proposals/${id}.json"
58
+ [ "$status" -ne 0 ]
59
+ }
60
+
61
+ @test "confirm with a justification snapshots the pre-rewrite candidate" {
62
+ _review_setup
63
+ id=$(_seed_pending)
64
+ librarian_lesson_confirm "$PROJECT_KEY" "$id" "org" "git aborts on a dirty tree regardless of version"
65
+ run jq -e '.candidate_before_confirm.applies_to.scope.kind == "versioned"' \
66
+ "${LESSONS_DIR}/proposals/${id}.json"
67
+ [ "$status" -eq 0 ]
68
+ }
69
+
70
+ @test "unconfirm returns a confirmed lesson to pending and clears the decision" {
71
+ _review_setup
72
+ id=$(_seed_pending)
73
+ librarian_lesson_confirm "$PROJECT_KEY" "$id" "public"
74
+ run librarian_lesson_unconfirm "$PROJECT_KEY" "$id"
75
+ [ "$status" -eq 0 ]
76
+ run jq -e '.status == "pending" and (has("visibility") | not) and (has("confirmed_at") | not)' \
77
+ "${LESSONS_DIR}/proposals/${id}.json"
78
+ [ "$status" -eq 0 ]
79
+ }
80
+
81
+ @test "the round trip leaves the proposal byte-identical to its pre-confirm state" {
82
+ _review_setup
83
+ id=$(_seed_pending)
84
+ before="${BATS_TEST_TMPDIR}/before.json"
85
+ cp "${LESSONS_DIR}/proposals/${id}.json" "$before"
86
+
87
+ librarian_lesson_confirm "$PROJECT_KEY" "$id" "public" "git behavior is stable across versions"
88
+ librarian_lesson_unconfirm "$PROJECT_KEY" "$id"
89
+
90
+ run diff <(jq -S . "$before") <(jq -S . "${LESSONS_DIR}/proposals/${id}.json")
91
+ [ "$status" -eq 0 ]
92
+ }
93
+
94
+ @test "unconfirm restores versioned scope after a justification confirm" {
95
+ _review_setup
96
+ id=$(_seed_pending)
97
+ librarian_lesson_confirm "$PROJECT_KEY" "$id" "org" "stable across versions"
98
+ librarian_lesson_unconfirm "$PROJECT_KEY" "$id"
99
+ run jq -e '.candidate.applies_to.scope.kind == "versioned"
100
+ and (has("candidate_before_confirm") | not)' \
101
+ "${LESSONS_DIR}/proposals/${id}.json"
102
+ [ "$status" -eq 0 ]
103
+ }
104
+
105
+ @test "after unconfirm a fresh confirm at a different visibility succeeds" {
106
+ _review_setup
107
+ id=$(_seed_pending)
108
+ librarian_lesson_confirm "$PROJECT_KEY" "$id" "public"
109
+ librarian_lesson_unconfirm "$PROJECT_KEY" "$id"
110
+ run librarian_lesson_confirm "$PROJECT_KEY" "$id" "private"
111
+ [ "$status" -eq 0 ]
112
+ run jq -e '.status == "confirmed" and .visibility == "private"' \
113
+ "${LESSONS_DIR}/proposals/${id}.json"
114
+ [ "$status" -eq 0 ]
115
+ }
116
+
117
+ @test "unconfirm from pending is a no-op success" {
118
+ _review_setup
119
+ id=$(_seed_pending)
120
+ run librarian_lesson_unconfirm "$PROJECT_KEY" "$id"
121
+ [ "$status" -eq 0 ]
122
+ run jq -e '.status == "pending"' "${LESSONS_DIR}/proposals/${id}.json"
123
+ [ "$status" -eq 0 ]
124
+ }
125
+
126
+ @test "unconfirm refuses a passed lesson and leaves the ledger untouched" {
127
+ _review_setup
128
+ id=$(_seed_pending)
129
+ librarian_lesson_pass "$PROJECT_KEY" "$id" "not worth sharing"
130
+ before_lines=$(wc -l < "${LESSONS_DIR}/passed.jsonl")
131
+
132
+ run librarian_lesson_unconfirm "$PROJECT_KEY" "$id"
133
+ [ "$status" -ne 0 ]
134
+ run jq -e '.status == "passed"' "${LESSONS_DIR}/proposals/${id}.json"
135
+ [ "$status" -eq 0 ]
136
+ [ "$(wc -l < "${LESSONS_DIR}/passed.jsonl")" -eq "$before_lines" ]
137
+ }
138
+
139
+ @test "unconfirm refuses an unrecognized status and names it" {
140
+ _review_setup
141
+ id=$(_seed_pending)
142
+ tmp="${BATS_TEST_TMPDIR}/mut.json"
143
+ jq '.status = "judging"' "${LESSONS_DIR}/proposals/${id}.json" > "$tmp"
144
+ mv "$tmp" "${LESSONS_DIR}/proposals/${id}.json"
145
+
146
+ run librarian_lesson_unconfirm "$PROJECT_KEY" "$id"
147
+ [ "$status" -ne 0 ]
148
+ [[ "$output" == *"judging"* ]] || return 1
149
+ }
150
+
151
+ @test "unconfirm refuses a lesson that does not exist" {
152
+ _review_setup
153
+ run librarian_lesson_unconfirm "$PROJECT_KEY" "01KZNOSUCHLESSON0000000000"
154
+ [ "$status" -ne 0 ]
155
+ }
156
+
157
+ @test "unconfirm never invokes a model" {
158
+ _review_setup
159
+ stub="${BATS_TEST_TMPDIR}/bin"
160
+ mkdir -p "$stub"
161
+ printf '#!/usr/bin/env bash\necho "MODEL WAS INVOKED" >&2\nexit 42\n' > "${stub}/claude"
162
+ chmod +x "${stub}/claude"
163
+ PATH="${stub}:${PATH}"
164
+
165
+ id=$(_seed_pending)
166
+ librarian_lesson_confirm "$PROJECT_KEY" "$id" "org"
167
+ run librarian_lesson_unconfirm "$PROJECT_KEY" "$id"
168
+ [ "$status" -eq 0 ]
169
+ [[ "$output" != *"MODEL WAS INVOKED"* ]] || return 1
170
+ }
171
+ ```
172
+
173
+ - [ ] **Step 2: Run them and watch them fail**
174
+
175
+ Run: `bats test/bats/librarian-lesson-review.bats`
176
+ Expected: the snapshot tests fail on a missing `candidate_before_confirm`; the rest fail with `librarian_lesson_unconfirm: command not found`. The 50 existing tests still pass.
177
+
178
+ - [ ] **Step 3: Snapshot the pre-rewrite candidate in `librarian_lesson_confirm`**
179
+
180
+ The rewrite block currently reads:
181
+
182
+ ```bash
183
+ if [[ -n "$justification" ]]; then
184
+ candidate=$(printf '%s' "$candidate" | jq -c \
185
+ --arg j "$justification" \
186
+ '.applies_to.scope = {kind: "version_independent", justification: $j}' 2>/dev/null) || return 1
187
+ fi
188
+ ```
189
+
190
+ Capture the original before overwriting it:
191
+
192
+ ```bash
193
+ # Snapshot the candidate before the rewrite, so unconfirm can put it back.
194
+ # Only when we actually rewrite: a plain confirm never touches .candidate,
195
+ # so a snapshot there would be dead weight that unconfirm has to reason
196
+ # about. Absence of the field means "nothing was mutated".
197
+ local candidate_before=""
198
+ if [[ -n "$justification" ]]; then
199
+ candidate_before="$candidate"
200
+ candidate=$(printf '%s' "$candidate" | jq -c \
201
+ --arg j "$justification" \
202
+ '.applies_to.scope = {kind: "version_independent", justification: $j}' 2>/dev/null) || return 1
203
+ fi
204
+ ```
205
+
206
+ Then extend the write. It currently reads:
207
+
208
+ ```bash
209
+ updated=$(printf '%s' "$proposal" | jq \
210
+ --arg v "$visibility" --arg t "$now" --argjson c "$candidate" \
211
+ '. * {status: "confirmed", visibility: $v, confirmed_at: $t} | .candidate = $c' 2>/dev/null) || return 1
212
+ ```
213
+
214
+ Add the snapshot only when one was taken, in two branches rather than one jq
215
+ program with an internal conditional:
216
+
217
+ ```bash
218
+ if [[ -n "$candidate_before" ]]; then
219
+ updated=$(printf '%s' "$proposal" | jq \
220
+ --arg v "$visibility" --arg t "$now" \
221
+ --argjson c "$candidate" --argjson cb "$candidate_before" \
222
+ '. * {status: "confirmed", visibility: $v, confirmed_at: $t}
223
+ | .candidate = $c
224
+ | .candidate_before_confirm = $cb' 2>/dev/null) || return 1
225
+ else
226
+ updated=$(printf '%s' "$proposal" | jq \
227
+ --arg v "$visibility" --arg t "$now" --argjson c "$candidate" \
228
+ '. * {status: "confirmed", visibility: $v, confirmed_at: $t}
229
+ | .candidate = $c' 2>/dev/null) || return 1
230
+ fi
231
+ ```
232
+
233
+ The duplication is deliberate and worth its cost. Collapsing it into one program
234
+ means passing the snapshot as `--arg` (a string) so the empty case is
235
+ representable, then recovering it with `fromjson` — and a `fromjson` failure is
236
+ then indistinguishable from a legitimately absent snapshot, because both leave
237
+ the field unset. Two branches keep "empty" a *structural* condition rather than
238
+ a value, so `--argjson` never receives anything but valid JSON, and `unconfirm`
239
+ tests absence with `has()` rather than string-emptiness.
240
+
241
+ Keep `.candidate = $c` as a plain assignment in both branches. It must not be folded into the `*` merge — `*` is recursive, so merging a `version_independent` scope over a stored `versioned` one leaves the old `versions` key behind and produces a candidate that fails its own validator. That is a bug this pipeline already shipped once.
242
+
243
+ - [ ] **Step 4: Add `librarian_lesson_unconfirm`**
244
+
245
+ Place it immediately after `librarian_lesson_confirm`:
246
+
247
+ ```bash
248
+ # Take back a confirmation, before the jury has seen the lesson.
249
+ #
250
+ # `confirmed` is otherwise terminal: confirm refuses a differing repeat and
251
+ # pass refuses a confirmed lesson. Those guards are correct — they are what
252
+ # keeps passed.jsonl from contradicting the proposal it describes — but they
253
+ # left no way back from confirming at the wrong visibility, and `public` is
254
+ # the tier that leaves this machine.
255
+ #
256
+ # Proceeds ONLY from `confirmed`. Every other status is refused, which is also
257
+ # what makes this forward-safe: when the jury stage introduces a status of its
258
+ # own, this verb refuses it through the catch-all with no change here.
259
+ #
260
+ # Usage: librarian_lesson_unconfirm <key> <lesson_id>
261
+ librarian_lesson_unconfirm() {
262
+ local key="$1"
263
+ local lesson_id="$2"
264
+ [[ -z "$key" || -z "$lesson_id" ]] && return 1
265
+
266
+ local path
267
+ path="$(librarian_lessons_dir "$key")/proposals/${lesson_id}.json"
268
+ [[ -f "$path" ]] || { printf 'Lesson %s not found.\n' "$lesson_id" >&2; return 1; }
269
+
270
+ local current_status
271
+ current_status=$(jq -r '.status // ""' "$path" 2>/dev/null)
272
+
273
+ case "$current_status" in
274
+ confirmed) ;;
275
+ pending) return 0 ;;
276
+ passed)
277
+ # Passing is a different decision with its own durable record.
278
+ # Silently moving it back to pending would leave passed.jsonl
279
+ # asserting a decision the proposal contradicts.
280
+ printf 'Lesson %s was passed on, not confirmed; unconfirm does not undo that.\n' "$lesson_id" >&2
281
+ return 1
282
+ ;;
283
+ *)
284
+ printf 'Lesson %s has an unrecognized status: %s\n' "$lesson_id" "$current_status" >&2
285
+ return 1
286
+ ;;
287
+ esac
288
+
289
+ # Restore the pre-confirm candidate when one was snapshotted, and delete
290
+ # the snapshot either way. A stale snapshot left on a pending proposal is
291
+ # indistinguishable from a live one at the next confirm, and would
292
+ # silently revert a later legitimate rewrite.
293
+ local updated
294
+ updated=$(jq '
295
+ (if has("candidate_before_confirm") then .candidate = .candidate_before_confirm else . end)
296
+ | del(.candidate_before_confirm, .visibility, .confirmed_at)
297
+ | .status = "pending"
298
+ ' "$path" 2>/dev/null) || return 1
299
+ [[ -z "$updated" || "$updated" == "null" ]] && return 1
300
+ printf '%s\n' "$updated" > "$path"
301
+ }
302
+ ```
303
+
304
+ - [ ] **Step 5: Run the tests**
305
+
306
+ Run: `bats test/bats/librarian-lesson-review.bats`
307
+ Expected: all pass — 50 existing plus 11 new.
308
+
309
+ - [ ] **Step 6: Fault-inject the three guarantees**
310
+
311
+ Each is a guarantee rather than a behavior. For each: make the change, run the named test, confirm it FAILS, revert, confirm it passes. Report each result.
312
+
313
+ 1. Remove `del(.candidate_before_confirm, ...)`'s `candidate_before_confirm` term → "unconfirm restores versioned scope after a justification confirm" must fail.
314
+ 2. Change the `passed)` arm to `return 0` → "unconfirm refuses a passed lesson and leaves the ledger untouched" must fail.
315
+ 3. Remove the `candidate_before` capture in `confirm` → the round-trip test must fail.
316
+
317
+ If any does NOT fail when injected, say so rather than adjusting the test — that means the guarantee is unverified.
318
+
319
+ - [ ] **Step 7: Verify and commit**
320
+
321
+ Run: `shellcheck -S error -x plugins/librarian/scripts/lib/librarian-lesson-review.sh && bats test/bats/librarian-lesson-transform.bats && npm run lint:check`
322
+ Expected: shellcheck silent, transform suite unaffected, lint exit 0.
323
+
324
+ ```bash
325
+ git add plugins/librarian/scripts/lib/librarian-lesson-review.sh \
326
+ test/bats/librarian-lesson-review.bats
327
+ git commit -m "feat(librarian): let a human take back a confirmation :leftwards_arrow_with_hook:"
328
+ ```
329
+
330
+ ---
331
+
332
+ ### Task 2: The CLI verb and the skill route
333
+
334
+ **Files:**
335
+ - Modify: `plugins/librarian/scripts/lib/librarian-cli.sh`
336
+ - Modify: `plugins/librarian/skills/librarian/SKILL.md`
337
+ - Test: `test/bats/librarian-lesson-review.bats` (append)
338
+
339
+ **Interfaces:**
340
+ - Consumes: `librarian_lesson_unconfirm <key> <lesson_id>` (Task 1); `_librarian_cli_project_key <cwd>` (existing).
341
+ - Produces: `librarian_cli lessons unconfirm <lesson_id> [cwd]`.
342
+
343
+ - [ ] **Step 1: Write the failing tests**
344
+
345
+ Append. The file already defines `_cli_setup`.
346
+
347
+ ```bash
348
+ @test "lessons unconfirm returns a confirmed lesson to pending" {
349
+ _cli_setup
350
+ id=$(_seed_pending)
351
+ librarian_cli lessons confirm "$id" public "$PROJECT_REPO" >/dev/null
352
+ run librarian_cli lessons unconfirm "$id" "$PROJECT_REPO"
353
+ [ "$status" -eq 0 ]
354
+ run jq -e '.status == "pending"' "${LESSONS_DIR}/proposals/${id}.json"
355
+ [ "$status" -eq 0 ]
356
+ }
357
+
358
+ @test "lessons unconfirm requires a lesson id" {
359
+ _cli_setup
360
+ run librarian_cli lessons unconfirm
361
+ [ "$status" -ne 0 ]
362
+ }
363
+
364
+ @test "lessons unconfirm rejects an unknown flag" {
365
+ _cli_setup
366
+ id=$(_seed_pending)
367
+ librarian_cli lessons confirm "$id" public "$PROJECT_REPO" >/dev/null
368
+ run librarian_cli lessons unconfirm "$id" --force
369
+ [ "$status" -ne 0 ]
370
+ run jq -e '.status == "confirmed"' "${LESSONS_DIR}/proposals/${id}.json"
371
+ [ "$status" -eq 0 ]
372
+ }
373
+
374
+ @test "an unknown lessons verb is still rejected" {
375
+ _cli_setup
376
+ run librarian_cli lessons frobnicate
377
+ [ "$status" -ne 0 ]
378
+ }
379
+ ```
380
+
381
+ - [ ] **Step 2: Run and watch them fail**
382
+
383
+ Run: `bats test/bats/librarian-lesson-review.bats`
384
+ Expected: the new tests fail with `unknown lessons action: unconfirm`.
385
+
386
+ - [ ] **Step 3: Add the verb**
387
+
388
+ Place it after `librarian_cli_lessons_pass`, mirroring that function's flag rejection:
389
+
390
+ ```bash
391
+ librarian_cli_lessons_unconfirm() {
392
+ local lesson_id="${1:-}"
393
+ local cwd="${2:-}"
394
+ [[ -z "$lesson_id" ]] && { printf 'usage: librarian_cli lessons unconfirm <lesson_id>\n'; return 1; }
395
+
396
+ # Reject a flag-shaped token rather than treating it as cwd. The sibling
397
+ # verbs already do this; a stray flag absorbed as a path resolves to the
398
+ # wrong project key and the verb then reports success against a lesson it
399
+ # never touched.
400
+ case "$cwd" in
401
+ --*) printf 'unknown option: %s\n' "$cwd" >&2; return 1 ;;
402
+ esac
403
+
404
+ local key
405
+ key=$(_librarian_cli_project_key "$cwd")
406
+ [[ -z "$key" ]] && { printf 'No project key resolvable from this directory.\n'; return 1; }
407
+
408
+ librarian_lesson_unconfirm "$key" "$lesson_id" || return 1
409
+ printf 'Unconfirmed %s; it is pending again.\n' "$lesson_id"
410
+ }
411
+ ```
412
+
413
+ Then add one arm to `librarian_cli_lessons`'s `case`, before the `*)` catch-all:
414
+
415
+ ```bash
416
+ unconfirm) librarian_cli_lessons_unconfirm "$@" ;;
417
+ ```
418
+
419
+ - [ ] **Step 4: Run the tests**
420
+
421
+ Run: `bats test/bats/librarian-lesson-review.bats`
422
+ Expected: all pass.
423
+
424
+ - [ ] **Step 5: Update the skill**
425
+
426
+ In `plugins/librarian/skills/librarian/SKILL.md`, add `unconfirm` to the lessons verb table and to the usage comment line, matching the existing entries' shape:
427
+
428
+ ```
429
+ librarian_cli lessons unconfirm <id> take back a confirmation, before the jury sees it
430
+ ```
431
+
432
+ In the walk section, describe it as the way to correct a confirmation made at the wrong visibility, and state that it works only on a `confirmed` lesson — a passed one stays passed, because that decision has its own record.
433
+
434
+ Read the surrounding text first and match its voice rather than pasting the sentence above verbatim.
435
+
436
+ - [ ] **Step 6: Verify and commit**
437
+
438
+ Run: `npm run test:ci` and capture the exit code directly — not through a pipe, since a pipe reports the last command's status rather than npm's.
439
+ Expected: exit 0.
440
+
441
+ ```bash
442
+ git add plugins/librarian/scripts/lib/librarian-cli.sh \
443
+ plugins/librarian/skills/librarian/SKILL.md \
444
+ test/bats/librarian-lesson-review.bats
445
+ git commit -m "feat(librarian): surface unconfirm in the CLI and the review walk :leftwards_arrow_with_hook:"
446
+ ```
447
+
448
+ ---
449
+
450
+ ## Self-review notes
451
+
452
+ **Spec coverage.** State rule → Task 1 Step 4; snapshot and its deletion → Task 1 Steps 3–4; surfaces → Task 2; events (none) → nothing emits, enforced by the Global Constraints; the seam to the jury → Task 1's `*)` arm, tested by the `judging` case; testing → each task's Step 1.
453
+
454
+ **On the `judging` test.** It mutates a proposal's status to a value nothing sets today, purely to prove the catch-all refuses an unrecognized status by name. That is the forward-safety promise to `4z8.3` made executable — without it, the promise is a comment.
455
+
456
+ **Deliberately absent.** No `judging` status is introduced. No undo for `pass`. No event emission.
457
+
458
+ **Known risk.** Task 1 modifies `librarian_lesson_confirm`, which took two fix rounds to harden in PR #137 — including the `*`-merge bug that persisted candidates failing their own validator. The round-trip test is the guard: it compares the post-unconfirm proposal against a byte-for-byte copy taken before the confirm, so any field the confirm adds and the unconfirm fails to remove shows up as a diff.