@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,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.
|