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