@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,213 @@
1
+ # Lesson Confirmation — Design
2
+
3
+ **Status:** Approved, not started.
4
+ **Tracked by:** `ecosystem-4z8.2`, under epic `ecosystem-4z8`.
5
+ **Upstream:** `docs/superpowers/specs/2026-08-09-lesson-transform-design.md` (stage 5,
6
+ shipped in `librarian-v0.7.1`).
7
+ **Parent design:** `docs/superpowers/specs/2026-08-08-promotion-pipeline-design.md` in the
8
+ [onlooker](https://github.com/onlooker-community/onlooker) repo, Sections 2–3.
9
+
10
+ ---
11
+
12
+ ## What this stage is
13
+
14
+ The human picks which lesson candidates go to the jury, and their intended
15
+ visibility, before any Opus tokens are spent.
16
+
17
+ This splits the pipeline's two filters along their natural lines. The human
18
+ judges **intent** — "do I want to share anything about this?" — which is cheap
19
+ and which only they can do. The jury judges **quality**, which is expensive and
20
+ which only it can do. Cost then scales with intent rather than artifact volume.
21
+
22
+ It also keeps promotion propose-only, consistent with librarian's ADR-001.
23
+
24
+ ## Where it lives
25
+
26
+ `/librarian lessons`, dispatched through a nested `librarian_cli lessons <verb>`.
27
+
28
+ Librarian already has a review surface for memory promotions: a SessionStart
29
+ surfacer that injects a one-line pointer, a `/librarian` skill that walks the
30
+ queue one entry at a time, and a CLI with `list` / `show` / `accept` / `reject`
31
+ / `defer`. Lessons reuse that plumbing but keep a separate queue, separate
32
+ verbs, and a separate walk.
33
+
34
+ Sharing the *machinery* while separating the *surface* is the point. The two
35
+ decisions look alike and are not: accepting a memory writes a file on your
36
+ machine, while confirming a lesson commits it toward being shared beyond your
37
+ machine, irreversibly once synced. ADR-001's argument — that a wrong promotion
38
+ is silent, slow to detect, and keeps acting on you afterward — applies harder
39
+ here, not softer. A lesson confirmation should never sit one keystroke away
40
+ from a memory accept in the same list.
41
+
42
+ The verb sets differ anyway. Memory review is a binary plus a punt. Every
43
+ lesson confirmation carries a third axis, visibility, that has no counterpart
44
+ on the memory side.
45
+
46
+ ### Verbs
47
+
48
+ | Command | Effect |
49
+ |---|---|
50
+ | `librarian_cli lessons list` | pending lessons, one row each |
51
+ | `librarian_cli lessons show <id>` | full candidate for one proposal |
52
+ | `librarian_cli lessons confirm <id> --visibility <v>` | `status: "confirmed"`, records `visibility` |
53
+ | `librarian_cli lessons pass <id> [reason]` | `status: "passed"`, appends to `passed.jsonl` |
54
+ | `librarian_cli lessons defer <id>` | no-op on state; leaves it `pending` |
55
+ | `librarian_cli lessons status` | one-line counts |
56
+
57
+ `confirm` also accepts `--version-independent <justification>`, which rewrites
58
+ `applies_to.scope` to that branch and is subject to constraint 2 below.
59
+
60
+ The walk shows, per candidate: `claim`, `rationale`, `evidence.resolution`, the
61
+ inferred `applies_to.stack` and `scope.versions`, and the source `artifact_id`.
62
+ Resolution is shown because it is the field that distinguishes a lesson from a
63
+ warning, and it is the one a human is best placed to sanity-check.
64
+
65
+ ## State model
66
+
67
+ All three states live on the proposal file the transform already writes at
68
+ `~/.onlooker/librarian/<project-key>/lessons/proposals/<ulid>.json`.
69
+
70
+ | `status` | Meaning | Written by |
71
+ |---|---|---|
72
+ | `pending` | transform wrote it; nobody has looked | stage 5 (ships today) |
73
+ | `confirmed` + `visibility` | human wants it shared; awaiting the jury | this stage |
74
+ | `passed` | human declined to share it | this stage |
75
+
76
+ The file already carries `status: "pending"` and nothing reads it yet, so this
77
+ stage gives an existing field its purpose rather than introducing a parallel
78
+ one.
79
+
80
+ **A passed candidate keeps its file.** `librarian_lesson_seen` scans
81
+ `proposals/` by `artifact_id`, so leaving the file in place is what stops a
82
+ declined artifact being re-proposed on the next scan. Deleting it would make
83
+ the pipeline re-transform and re-ask every session, re-paying Haiku each time
84
+ for an answer the human already gave.
85
+
86
+ ### Two ledgers, deliberately not one
87
+
88
+ ```
89
+ lessons/proposals/<ulid>.json pending | confirmed | passed
90
+ lessons/declined.jsonl machine verdicts (stage 5)
91
+ lessons/passed.jsonl human intent (this stage)
92
+ ```
93
+
94
+ `declined.jsonl` exists to make expensive drops auditable and to produce the
95
+ data that tunes the rubric later. A human declining to share is a statement
96
+ about intent, not about quality. Mixing "I didn't want to share this" into the
97
+ same file as "the model emitted malformed JSON" would corrupt exactly the
98
+ signal that file exists to carry.
99
+
100
+ Keeping them apart also makes each readable on its own: what does the model get
101
+ wrong, versus what do I not want to share.
102
+
103
+ `passed.jsonl` records `artifact_id`, `lesson_id`, `passed_at`, and an optional
104
+ free-text reason. Append-only, like its sibling.
105
+
106
+ ## The three constraints
107
+
108
+ These are the acceptance criteria expressed as properties of the code rather
109
+ than as promises.
110
+
111
+ **1. `confirm` requires an explicit visibility.** There is no default and no
112
+ bare `confirm`. A candidate cannot reach `confirmed` without someone naming
113
+ `private`, `org`, or `public`.
114
+
115
+ **2. `version_independent` forces `org` or `public`.** Combining it with
116
+ `private` is refused, with the reason stated.
117
+
118
+ The transform can only emit `versioned` scope — that is what stops it minting
119
+ lessons that never expire. This stage hands that branch to a human, who has the
120
+ context to write a real justification. But private lessons run no jury, so a
121
+ private `version_independent` claim would reach the pool with nothing checking
122
+ it: the same hole, reached by a different door, with a person instead of Haiku
123
+ writing the unchecked claim.
124
+
125
+ Requiring org or public means the claim gets judged. `scope_accuracy` is the
126
+ criterion that asks whether the justification is actually true, and the
127
+ disclosure lens applies on top at the public tier. The schema stops the
128
+ accident; the jury stops the lazy excuse; this constraint is what guarantees
129
+ the jury is present to do so.
130
+
131
+ **3. This stage never calls a model.** No Haiku, no Opus, no network. That is
132
+ what makes "no tokens spent on unselected candidates" checkable rather than
133
+ merely intended.
134
+
135
+ ## Two validators, on purpose
136
+
137
+ `librarian_lesson_validate_candidate` hard-requires
138
+ `.applies_to.scope.kind == "versioned"`. A human-converted `version_independent`
139
+ candidate therefore fails it.
140
+
141
+ This stage gets its own: `librarian_lesson_validate_confirmed`, which permits
142
+ either branch, requires a non-empty `justification` when the branch is
143
+ `version_independent`, and otherwise enforces exactly what the transform's
144
+ validator enforces — including the provenance rules and the version-range
145
+ pattern.
146
+
147
+ The two are not redundant and the difference is not stylistic. They encode
148
+ different trust: one bounds what a model may assert unsupervised, the other
149
+ bounds what a human may assert *and a jury will then check*. The versioned-only
150
+ rule stays on the transform path, where nothing downstream would catch a bad
151
+ claim.
152
+
153
+ State this in the code. A future reader who finds two validators and assumes
154
+ one is dead will delete the wrong one.
155
+
156
+ ## Surfacing
157
+
158
+ The SessionStart surfacer gains a second count: a distinct one-line pointer when
159
+ lesson proposals are pending, alongside the existing memory-proposal line.
160
+
161
+ Bodies stay out of SessionStart. That follows the surfacer's own stated reason —
162
+ session-start context is precious, and a queue of unreviewed entries is not
163
+ where it should go. The walk is where candidates get read.
164
+
165
+ ## Events
166
+
167
+ Deferred, for the same reason stage 5 deferred its own.
168
+
169
+ `librarian.lesson.confirmed` and `librarian.lesson.passed` would be as
170
+ unregistered in `@onlooker-community/schema` as `.proposed` and `.declined`
171
+ still are — verified against 2.11.0, which carries no `librarian.lesson.*`
172
+ types at all. With a validator resolvable, the emitter rejects an unknown
173
+ `event_type` and exits 1.
174
+
175
+ Nothing is lost by waiting: `4z8.3` reads the proposal files, not the bus. When
176
+ the schema package publishes all four types, one follow-up wires them together.
177
+
178
+ ## Testing
179
+
180
+ bats, isolated temp home, per the repo's `writing-tests` skill — including its
181
+ rule that non-final `[[ ]]` assertions need `|| return 1`, and its habit of
182
+ breaking each new assertion once to confirm it discriminates.
183
+
184
+ The cases that matter are the three constraints, because each is a guarantee:
185
+
186
+ - `confirm` with no visibility is refused and leaves `status: "pending"`
187
+ - `version_independent` + `private` is refused, with the reason
188
+ - confirming invokes no model — asserted with a `claude` stub on `PATH` that
189
+ fails loudly if called, the same technique that proved the transform's
190
+ `unavailable` path
191
+
192
+ Plus:
193
+
194
+ - a passed candidate keeps its file, and `librarian_lesson_seen` still reports
195
+ its artifact handled
196
+ - this stage never writes to `declined.jsonl`
197
+ - a confirmed candidate carries both `status: "confirmed"` and its `visibility`
198
+ - `librarian_lesson_validate_confirmed` accepts a well-formed
199
+ `version_independent` candidate and rejects one whose justification is empty,
200
+ while still rejecting everything the transform's validator rejects
201
+
202
+ ## Boundary to the next stage
203
+
204
+ `4z8.3` selects proposals where `status == "confirmed"` and reads `visibility`
205
+ from the same file to choose its gate: no jury for `private`, the
206
+ `lesson-promotion` rubric for `org`, rubric plus disclosure lens for `public`.
207
+
208
+ Nothing else crosses between the stages.
209
+
210
+ ## Out of scope
211
+
212
+ The jury itself, the approved pool, `author_key` derivation, and any sync. This
213
+ stage ends when a candidate is marked confirmed.
@@ -0,0 +1,267 @@
1
+ # Lesson Judging — Design
2
+
3
+ **Status:** Approved, not started.
4
+ **Tracked by:** `ecosystem-4z8.3`, under epic `ecosystem-4z8`.
5
+ **Upstream:** `docs/superpowers/specs/2026-08-10-lesson-confirmation-design.md` (stage 6,
6
+ shipped in PR #137) and `docs/superpowers/specs/2026-08-11-lesson-unconfirm-design.md`
7
+ (`si6`, shipped in PR #139).
8
+ **Blocks:** `ecosystem-4z8.4` (approved pool and declined ledger).
9
+
10
+ ---
11
+
12
+ ## What this stage is
13
+
14
+ The jury. A human has already said which lesson candidates they want shared and
15
+ at what visibility; this stage decides whether each one is good enough to go.
16
+
17
+ It is the first stage in the pipeline that spends Opus tokens, which shapes
18
+ nearly every decision below.
19
+
20
+ ## Where it lives
21
+
22
+ `/librarian lessons judge`, a new route on the existing librarian skill.
23
+ Human-invoked — never a hook.
24
+
25
+ The agent orchestrates and bash does file I/O, the same split stage 6
26
+ established. That split is what lets the jury be real judge subagents rather
27
+ than headless `claude -p` calls: tribunal's actual jury runs in its skill via
28
+ the Task tool, and only its Stop hook uses the single-prompt shape.
29
+
30
+ Per candidate, the skill reads the confirmed proposal, spawns the judge
31
+ subagents with the candidate and the rubric, parses their verdicts, aggregates
32
+ and gates, and **writes the verdict back before moving to the next lesson**.
33
+ A new lib function `librarian_lesson_record_verdict <key> <lesson_id>
34
+ <verdict_json>` owns that write, surfaced as `librarian_cli lessons
35
+ record-verdict <id> <verdict-json> [cwd]`.
36
+
37
+ The batch comes from `librarian_cli lessons list --confirmed`, which `si6`
38
+ shipped precisely because a confirmed lesson was otherwise unfindable.
39
+
40
+ Before spending anything, the skill reports that batch — how many candidates are
41
+ confirmed and how many are public — and asks. A jury is the most expensive thing
42
+ in the pipeline, and stage 6 established that the human sees what is about to
43
+ happen before it happens. Declining the prompt spends nothing and writes
44
+ nothing; every candidate stays `confirmed`.
45
+
46
+ ### There is no Actor
47
+
48
+ Tribunal's loop is Actor → Jury → Meta-Judge → Gate, built around a producer
49
+ that gets retried with critique. A lesson candidate already exists and nothing
50
+ rewrites it, so this stage uses only Jury → aggregate → gate.
51
+
52
+ That makes `max_iterations` vestigial. The bead specified `max_iterations: 1` to
53
+ mean *drop, don't repair* — but with no producer, that is simply what happens.
54
+ **The knob is omitted rather than carried**, because a setting that reads as if
55
+ retry were possible invites someone to raise it later and wonder why nothing
56
+ changes.
57
+
58
+ ## Why librarian owns this
59
+
60
+ CLAUDE.md states that plugins do not call each other directly. This stage
61
+ couples librarian and tribunal in some direction no matter how it is arranged:
62
+ either librarian reaches for tribunal's rubric and judges, or tribunal reaches
63
+ into librarian's project-keyed proposal files and writes verdicts back.
64
+
65
+ **Librarian owns the verb, the rubrics, and the gate decision, and dispatches
66
+ tribunal's judge agents by name.** The invariant is read as forbidding
67
+ hook-to-hook runtime calls — the coupling that would make one plugin's failure
68
+ another's — not reuse of published agent definitions, which are declarative
69
+ assets with no runtime surface. An ADR records that reading.
70
+
71
+ Keeping it in librarian also keeps the whole lesson lifecycle in one plugin, so
72
+ `4z8.4` does not have to split its pool and ledger across two.
73
+
74
+ ## The rubrics
75
+
76
+ Two builtins in **librarian's** `config.json`, mirroring tribunal's
77
+ `rubric.builtins` shape so they stay legible to anyone who knows tribunal, and
78
+ so the two could merge later.
79
+
80
+ | criterion | `lesson-promotion` | `lesson-promotion-public` | asks |
81
+ |---|---|---|---|
82
+ | grounding | 0.45 / 0.7 | 0.32 / 0.7 | does the claim follow from evidence and resolution? |
83
+ | scope_accuracy | 0.35 / 0.7 | 0.24 / 0.7 | does `applies_to` correctly bound the claim? |
84
+ | generality | 0.20 / 0.6 | 0.14 / 0.6 | is this a lesson, or a session-scoped fact? |
85
+ | disclosure | — | 0.30 / 0.9 | does the text leak a credential, internal name, or proprietary detail? |
86
+
87
+ *(weight / min_pass.)* Both sum to 1.00. The public rubric is the org rubric
88
+ scaled to 70%, with disclosure taking the remaining 30%.
89
+
90
+ The public weights are rounded **to preserve that sum**, not by rounding each
91
+ value independently: `0.35 × 0.7 = 0.245` is written as `0.24`, because rounding
92
+ it up to `0.25` totals 1.01. Use the values in the table verbatim rather than
93
+ recomputing them.
94
+
95
+ The sum matters even though nothing reads it today: tribunal validates each
96
+ weight in `[0,1]` but never checks their total, so an unnormalized 1.30 would
97
+ silently mis-score the moment real `weighted_mean` lands.
98
+
99
+ `score_threshold` 0.75 and `judge_types` `[standard, adversarial]` for both.
100
+
101
+ ### scope_accuracy is the point of the whole pipeline
102
+
103
+ Stage 5 was deliberately restricted to emitting `versioned` scope, because a
104
+ model minting `version_independent` lessons produces claims that never expire.
105
+ Stage 6 handed that branch to a human, who has the context to write a real
106
+ justification, and refused it at `private` because that tier runs no jury.
107
+
108
+ `scope_accuracy` is where the loop closes. The schema guarantees a
109
+ `version_independent` lesson **carries** a justification; this criterion asks
110
+ whether it is **true**. Schema stops the accident; the jury stops the lazy
111
+ excuse.
112
+
113
+ ### The weights and floors are inert today
114
+
115
+ `tribunal_aggregate` takes the rubric as a parameter and explicitly discards it;
116
+ `weighted_mean` falls through to the same plain average as `mean`. And
117
+ `min_pass` is enforced nowhere — `TribunalVerdictPayload` carries one scalar
118
+ `score`, one boolean `passed`, and `criteria_evaluated` as a list of *names*
119
+ with no scores, so the orchestrator never learns what a judge scored on any
120
+ individual criterion. Per the judge contract, `passed` is the judge's own
121
+ self-assessment against the thresholds: prompt-level, not machine-enforced.
122
+
123
+ The weights and floors are declared anyway, because they are the honest
124
+ statement of intent and they go live unchanged the moment `ecosystem-pht`
125
+ implements threading. But no part of this design may depend on them.
126
+
127
+ ## Gating by visibility
128
+
129
+ | Visibility | Jury | Rubric | Gate policy |
130
+ |---|---|---|---|
131
+ | `private` | none — no model call at all | — | — |
132
+ | `org` | standard + adversarial | `lesson-promotion` | majority |
133
+ | `public` | standard + adversarial | `lesson-promotion-public` | unanimous |
134
+
135
+ `private` skipping the jury entirely is what makes "cost scales with intent
136
+ rather than artifact volume" true end to end, and it is why stage 6 refuses
137
+ `version_independent` at `private` — that is the tier with nothing checking the
138
+ claim.
139
+
140
+ `org` gets the rubric alone because the org boundary already implies trust.
141
+
142
+ `public` gets **`unanimous` instead of `majority`**, and this is a deliberate
143
+ substitution for a mechanism that does not exist. The intent was a disclosure
144
+ floor at `min_pass` 0.9 — a near-veto, on the reasoning that correctness rots
145
+ and `applies_to` retires it, but harm does not; a leaked credential never
146
+ expires on its own. Since `min_pass` cannot be enforced, `unanimous` delivers
147
+ the property that actually mattered: **a single judge's objection cannot be
148
+ outvoted.**
149
+
150
+ The trade-off is stated plainly: it is not disclosure-specific. A judge unhappy
151
+ about `generality` also blocks a public lesson. That is accepted for the tier
152
+ that leaves the machine and draws the disclosure lens. When `pht` lands, this
153
+ can narrow to a true per-criterion floor.
154
+
155
+ **Correction (2026-08-14).** This section described `unanimous` as delivering
156
+ "a single judge's objection cannot be outvoted." It never did. Both rubrics
157
+ declare `judge_types: ["standard", "adversarial"]` — a panel of two — and at
158
+ panel size two `unanimous` (`passed == count`) and `majority`
159
+ (`passed * 2 > count`) return the same answer for all three possible pass
160
+ counts. They diverge only at three judges or more, and `librarian_lesson_judge`
161
+ refuses any panel whose judge-type multiset does not match the rubric's, so a
162
+ third judge never reaches the gate.
163
+
164
+ The public tier was never stricter than the org tier. `ecosystem-pht` replaced
165
+ the stand-in with `disclosure`'s real `min_pass` floor and the policy is now
166
+ `majority`. Tracked as `ecosystem-j74`.
167
+
168
+ ### No new judge agent
169
+
170
+ The bead proposed reusing `tribunal-judge-security` as the disclosure lens. On
171
+ inspection that does not work: it is built for code review — injection, auth
172
+ bypass, path traversal, SSRF, TOCTOU — and is instructed to *read the changed
173
+ files*, with `Read, Grep, Glob` as its tools. A lesson candidate is prose and
174
+ has no files.
175
+
176
+ Disclosure is therefore a **criterion** scored by the judges already empaneled,
177
+ not a judge type. If generalist judges prove weak at it, a purpose-built
178
+ disclosure judge is the follow-up — but writing one now is machinery ahead of
179
+ evidence.
180
+
181
+ ## Verdict is not the same as "could not judge"
182
+
183
+ `4z8.4` draws this distinction; this stage has to produce it.
184
+
185
+ Tribunal's own skill tells the orchestrator that an unparseable judge response
186
+ becomes `score: 0, passed: false` and lets the gate decide. **For lessons that is
187
+ inverted deliberately.** A flaky judge would permanently decline a good lesson,
188
+ and the watermark has already advanced past its artifact — so one transient
189
+ outage would bury it for good.
190
+
191
+ **Every empaneled judge must return a parseable verdict, or the candidate is
192
+ unjudged.** Same for `claude` being unavailable, a judge timing out, or an empty
193
+ panel. There is no quorum rule to tune: with a two-judge panel under `unanimous`
194
+ or `majority`, losing one verdict means the gate cannot be decided at all, so
195
+ "all of them" is the only honest threshold.
196
+
197
+ **"Unjudged" is expressed as the absence of a write.** The proposal stays
198
+ `confirmed`, nothing lands on disk, and the next run retries it. There is no
199
+ failure state to clean up. The skill reports which candidates it skipped so the
200
+ human knows to re-run.
201
+
202
+ ## State model
203
+
204
+ `confirmed` → `approved` | `rejected`, written atomically per lesson.
205
+
206
+ Committing per lesson rather than per batch is what makes a crash cheap: each
207
+ candidate transitions the moment its own verdict lands, so an interrupted run
208
+ costs at most one re-judgment and strands nothing.
209
+
210
+ **No `judging` status.** `si6` deferred that decision here, and the answer is no.
211
+ A mid-flight status would recreate exactly the trap `si6` was filed to fix — a
212
+ lesson stuck in a state with no verb to free it, needing its own recovery
213
+ escape hatch. Per-lesson commit gets crash-safety without one.
214
+
215
+ Alongside `status`, the write records `judged_at` and a `verdict` object: rubric
216
+ id, gate policy, aggregate score, threshold, the per-judge verdicts, and a
217
+ reason when blocked.
218
+
219
+ **A rejected proposal keeps its file**, exactly as a passed one does. That is
220
+ what stops the artifact being re-proposed on the next scan and re-paying tokens
221
+ for an answer already given.
222
+
223
+ ### `unconfirm` needs no change
224
+
225
+ Its catch-all refuses any status it does not recognize, naming it — so
226
+ `approved` and `rejected` are refused automatically, with no coordination
227
+ between the stages. This is the forward-safety promise `si6` made to this stage,
228
+ and collecting it costs nothing. It is asserted with a test rather than trusted.
229
+
230
+ Undoing a rejection is **out of scope**, for the same reason undoing a `pass`
231
+ was: it is a real decision with its own record, and inventing a reversal verb
232
+ for it now would repeat the mistake `si6` avoided.
233
+
234
+ ## Events
235
+
236
+ None, consistent with the two stages before it. `@onlooker-community/schema`
237
+ 2.11.0 registers only `meridian.lesson.curated` — no `librarian.lesson.*` and no
238
+ `tribunal.lesson.judged` — and the runtime emitter exits 1 on an unknown
239
+ `event_type`. `4z8.4` reads proposal files, not the bus.
240
+
241
+ ## Testing
242
+
243
+ bats, isolated temp home, per the repo's `writing-tests` skill — single-bracket
244
+ assertions or `|| return 1` on non-final ones, and every new assertion broken
245
+ once to confirm it discriminates.
246
+
247
+ - **A `private` candidate reaches `approved` with no model call**, asserted with
248
+ a `claude` stub on `PATH` that fails loudly if invoked — the technique that
249
+ already proved stage 5's `unavailable` path and stage 6's no-model guarantee.
250
+ - `org` uses `lesson-promotion` with majority; `public` uses
251
+ `lesson-promotion-public` with unanimous.
252
+ - **A public candidate that one judge blocks is rejected even when the aggregate
253
+ clears `score_threshold`** — the unanimous property, and the reason this tier
254
+ differs at all.
255
+ - One judge returning unparseable output leaves the candidate `confirmed` with
256
+ nothing written, and the skipped id is reported.
257
+ - A below-threshold candidate is rejected and keeps its file.
258
+ - `unconfirm` refuses `approved` and refuses `rejected`, naming the status.
259
+ - Re-running skips candidates that are not `confirmed`.
260
+ - Both rubrics' weights sum to 1.00 — a guard for when `pht` makes them live.
261
+
262
+ ## Out of scope
263
+
264
+ The approved pool and `declined.jsonl` writes (both `4z8.4`), `author_key`
265
+ derivation (`4z8.5`), any sync, and re-judging a rejection. Per-criterion score
266
+ threading and real `min_pass` enforcement are `ecosystem-pht`, filed against
267
+ tribunal.
@@ -0,0 +1,152 @@
1
+ # Lesson Unconfirm — Design
2
+
3
+ **Status:** Approved, not started.
4
+ **Tracked by:** `ecosystem-si6`, wired as a blocker on `ecosystem-4z8.3`.
5
+ **Upstream:** `docs/superpowers/specs/2026-08-10-lesson-confirmation-design.md`
6
+ (shipped in PR #137).
7
+
8
+ ---
9
+
10
+ ## What this is
11
+
12
+ A way back from a confirmed lesson, before the jury has seen it.
13
+
14
+ `confirmed` is currently terminal. `librarian_lesson_confirm` treats a repeat as
15
+ idempotent only when visibility and candidate match exactly and refuses anything
16
+ else; `librarian_lesson_pass` refuses a confirmed lesson outright; nothing else
17
+ writes the proposal file. So a human who confirms at `public` meaning `private`
18
+ has no path back — and that is the direction most worth correcting, since
19
+ `public` is the tier that leaves the machine and draws the disclosure lens.
20
+
21
+ Those guards are not the problem and should not be loosened. They are what keeps
22
+ `lessons/passed.jsonl` from contradicting the proposal it describes, and they
23
+ exist because pass-then-confirm once left the ledger asserting a human had
24
+ declined something the proposal said was confirmed and bound for the jury. The
25
+ guards closed the write path correctly. They just left no recovery path.
26
+
27
+ ## Why a verb rather than a relaxed guard
28
+
29
+ The alternative was letting `confirm` overwrite a prior confirm with a different
30
+ visibility. Rejected on two grounds.
31
+
32
+ It would delete the guard that makes repeats safe — the same guard added in
33
+ response to a real contradiction bug. And it would make a typo'd repeat silently
34
+ change visibility on the verb that commits a lesson toward leaving the machine.
35
+
36
+ An explicit "I take that back" matches how this design has treated every other
37
+ irreversibility: make the consequential thing deliberate rather than incidental.
38
+
39
+ ## The state rule
40
+
41
+ `librarian_lesson_unconfirm <key> <lesson_id>`, surfaced as
42
+ `librarian_cli lessons unconfirm <id> [cwd]`.
43
+
44
+ | Current status | Behavior |
45
+ |---|---|
46
+ | `confirmed` | → `pending`; clear `visibility` and `confirmed_at`; restore the candidate snapshot |
47
+ | `pending` | no-op success — already there |
48
+ | `passed` | refuse |
49
+ | anything else | refuse, naming the status |
50
+
51
+ `pending` as an idempotent no-op mirrors how `pass` treats an already-passed
52
+ lesson, so repeat invocations are safe.
53
+
54
+ Refusing `passed` keeps the ledger authoritative. Silently moving a passed
55
+ lesson back to `pending` would leave `passed.jsonl` asserting a decision the
56
+ proposal contradicts — precisely the bug the status guards were added for.
57
+ Passing is a different decision from confirming, and it has its own durable
58
+ record; undoing it is not this verb's job.
59
+
60
+ ## The candidate snapshot
61
+
62
+ `confirm` writes four things: `status`, `visibility`, `confirmed_at`, and
63
+ `.candidate`. That last one is why returning to `pending` is not enough on its
64
+ own — a `--justification` rewrites the candidate's scope to
65
+ `version_independent`, and nothing records what the transform originally
66
+ inferred.
67
+
68
+ Without a snapshot, unconfirming a justification-confirm leaves the proposal
69
+ carrying `version_independent` scope with no route back to its `versioned`
70
+ form. The confirmation stage's own guard would then correctly refuse it at
71
+ `private` — so the user escapes one dead end into another, for exactly the
72
+ mistake most worth undoing.
73
+
74
+ **`confirm` stores the pre-rewrite candidate as `candidate_before_confirm`, and
75
+ only when it actually rewrites scope.** `unconfirm` restores it and deletes the
76
+ field.
77
+
78
+ Deleting it is load-bearing. A stale snapshot left on a `pending` proposal is
79
+ indistinguishable from a live one at the next confirm, and would silently revert
80
+ a later legitimate rewrite. Absence of the field means the candidate was never
81
+ mutated and there is nothing to restore — which is the common case, since a
82
+ plain confirm does not touch it.
83
+
84
+ ## Surfaces
85
+
86
+ `librarian_cli lessons unconfirm <id> [cwd]`, carrying the same `--*` flag
87
+ rejection the other lesson verbs have. An unrecognized flag must not be absorbed
88
+ as a `cwd` candidate; that hazard is already fixed twice in this pipeline and
89
+ the contract should be consistent across the verb set.
90
+
91
+ The `/librarian lessons` skill gains it in the verb table and in the walk,
92
+ described as taking back a confirmation before the jury sees it.
93
+
94
+ `librarian_cli lessons list` gains a `--confirmed` flag, and
95
+ `lessons show` gains a `visibility` line. Neither was in the original design,
96
+ and without them the verb has no discovery path: `list` prints only pending
97
+ lessons, `unconfirm` operates only on `confirmed` ones, and nothing rendered
98
+ the visibility a lesson sits at. The user story is inherently cross-session — a
99
+ human who confirms at `public` meaning `private` comes back later with the claim
100
+ in mind and no id — so a recovery verb with no way to find its subject is a
101
+ verb nobody can reach. Bare `list` still shows pending; the flag carries the
102
+ same `unknown option:` rejection as the sibling verbs.
103
+
104
+ ## Events
105
+
106
+ None, consistent with the rest of this stage. `librarian.lesson.*` is
107
+ unregistered in `@onlooker-community/schema` 2.11.0 and the emitter exits 1 on
108
+ an unknown `event_type`. The next stage reads proposal files, not the bus.
109
+
110
+ ## The seam to the jury
111
+
112
+ This is a promise to code that does not exist yet, so state it plainly.
113
+
114
+ `unconfirm` proceeds **only** from `confirmed`. Every other status falls to the
115
+ catch-all and is refused with the status named. When `4z8.3` introduces a status
116
+ of its own — `judging`, `judged`, whatever it picks — `unconfirm` refuses it
117
+ automatically, with no change here and no coordination between the two stages.
118
+
119
+ That is what "must not preclude a jury-consumption state" means in practice: the
120
+ existing convention of refusing unrecognized statuses loudly already handles a
121
+ consumer that has not been written.
122
+
123
+ **This design deliberately does not add a `judging` status now.** Inventing a
124
+ state machine for an unwritten consumer produces a status nothing sets and every
125
+ verb must handle. `4z8.3` owns that decision; this verb is forward-safe without
126
+ it.
127
+
128
+ ## Testing
129
+
130
+ bats, isolated temp home, per the repo's `writing-tests` skill — single-bracket
131
+ assertions or `|| return 1` on non-final ones, and every new assertion broken
132
+ once to confirm it discriminates.
133
+
134
+ - **Round trip.** Confirm at `public` with a justification, then unconfirm: the
135
+ proposal is byte-identical to its pre-confirm state, scope back to
136
+ `versioned`, and no `candidate_before_confirm` left behind.
137
+ - Unconfirm from `pending` is a no-op success.
138
+ - Unconfirm from `passed` is refused, and `passed.jsonl` is untouched.
139
+ - An unrecognized status is refused with the status named.
140
+ - **After unconfirm, a fresh confirm at a different visibility succeeds** — the
141
+ user story this verb exists for.
142
+ - Unconfirm invokes no model, asserted with a `claude` stub that fails loudly if
143
+ called.
144
+ - A plain confirm (no justification) writes no snapshot, and unconfirming it
145
+ still returns the proposal to `pending` cleanly.
146
+
147
+ ## Out of scope
148
+
149
+ The jury, the approved pool, `author_key`, and any sync. Undoing a `pass` is
150
+ also out of scope — that decision has its own ledger and its own reasoning, and
151
+ conflating the two undos is how the ledger starts disagreeing with the proposal
152
+ again.