Skip to content

Commit cb53761

Browse files
copeusclaude
andcommitted
release: v3.1.1 — a question no longer burns the session's one triage nudge
The UserPromptSubmit nudge fires at most once per session. The native-mechanism audit named the consequence precisely and it sat there unfixed: a session whose first prompt is "what does this do?" spent the reminder on a question, and the real change request that followed got nothing. A prompt of at most 200 characters ending in `?` now returns BEFORE the marker is written — the session stays armed, at the ~9 ms early exit rather than the ~89 ms eligibility path. The test is deliberately narrow, because "is this a change request?" is not decidable in a hook: only a SHORT prompt ending in `?` counts, and a long one that happens to end in `?` is a description with a question attached and still nudges. The direction of the error is chosen — a missed nudge costs a reminder, a spent one costs the session's only reminder. Six regression tests, and the guard was watched failing: removed from a copy of the hook, exactly three checks go red. The audit row stays ⚠ and the reason is unchanged: a model that ignores the line still leaves no triage record, and the Stop gate that would catch that is still off by default. The reminder got more accurate; it did not become a mechanism. Also fixes the auto-generated release title, which was the CHANGELOG's date — the first two releases the repaired gate published on its own both shipped as "vX.Y.Z — 2026-09-02". Both retitled. Verified: 8 shell tests (65 native-points checks), shellcheck, lint, lockstep. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 1e50d85 commit cb53761

6 files changed

Lines changed: 81 additions & 6 deletions

File tree

.claude-plugin/marketplace.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
{
1010
"name": "superpowers-v",
1111
"description": "Compound V for Superpowers: triple parallel pre-flight (code archaeology + domain-expert + Context7 library validator), disjoint partitioning, manifest-driven multi-backend dispatch (Claude + Codex + Antigravity + Cursor), git-diff scope enforcement, crash-resumable runs, adaptive tier-based routing, epic mode, research-grounded brainstorming (gated pre-brainstorm recon + batched elicitation), V-memory local-first semantic+lexical recall over docs/superpowers (opt-in pure-python embeddings + a deterministic recall→action bridge), and batched parallel dispatch (Opus default, narrow Sonnet exception)",
12-
"version": "3.1.0",
12+
"version": "3.1.1",
1313
"source": "./",
1414
"author": {
1515
"name": "Oleg",

.claude-plugin/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "superpowers-v",
33
"description": "Compound V for Superpowers: triple parallel pre-flight (code archaeology + domain-expert advisor + library/doc validator via Context7), disjoint file partitioning, manifest-driven multi-backend dispatch (Claude + headless Codex + Antigravity + Cursor workers), git-diff scope enforcement, crash-resumable runs, adaptive tier-based routing, epic mode for multi-feature builds, V-memory local-first semantic+lexical recall over docs/superpowers (opt-in pure-python embeddings + a deterministic recall\u2192action bridge), and batched parallel dispatch (Opus default, Sonnet for narrow junior-task carve-out), plus /v:onboard — a project-onboarding command that builds a citation-verified knowledge base + AGENTS.md/CLAUDE.md bridge behind a human gate. Auto-intercepts four transitions: pre-brainstorm recon \u2192 brainstorming \u2192 writing-plans \u2192 execution.",
4-
"version": "3.1.0",
4+
"version": "3.1.1",
55
"author": {
66
"name": "Oleg",
77
"email": "copeus@gmail.com"

CHANGELOG.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,24 @@ All notable changes to **superpowers-v (Compound V)** are documented here.
44

55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project uses semantic versioning.
66

7+
## [3.1.1] - 2026-09-02
8+
9+
### Fixed — a question no longer burns the session's one triage nudge
10+
11+
The `UserPromptSubmit` nudge fires **at most once per session**. The native-mechanism audit named the consequence precisely and it sat there unfixed: a session whose first prompt is *"what does this do?"* spent the reminder on a question, and the real change request that followed got nothing.
12+
13+
A prompt of at most 200 characters ending in `?` now returns **before** the marker is written — the session stays armed, and the cost is the ~9 ms early exit rather than the ~89 ms eligibility path.
14+
15+
The test is deliberately narrow, because "is this a change request?" is not decidable in a hook. Only a *short* prompt ending in `?` counts as a question; a long prompt that happens to end in `?` is a description with a question attached and still nudges. The direction of the error is chosen: a missed nudge costs a reminder, a spent one costs the session's **only** reminder.
16+
17+
Six regression tests, and the guard was watched failing — removed from a copy of the hook, exactly three checks go red. A guard nobody has seen fail is a guard nobody should trust.
18+
19+
**The audit row stays ⚠, and the reason is unchanged:** a model that ignores the line still leaves no triage record at all, and the Stop gate that would catch that is still off by default (`enforcement.triage_gate // false`). The reminder got more accurate. It did not become a mechanism.
20+
21+
### Fixed — the auto-generated release title was a date
22+
23+
The CHANGELOG heading is `## [X.Y.Z] - YYYY-MM-DD`, so the text after the dash is the date. v3.0.6 and v3.1.0 — the first two releases the repaired gate published on its own, and the first two where nobody passed `--title` by hand — both shipped as *"vX.Y.Z — 2026-09-02"*. The title now comes from the entry's first `###` heading, where a release actually names itself, and falls back to no suffix rather than to a date. Both releases retitled.
24+
725
## [3.1.0] - 2026-09-02
826

927
Three maintainer requirements, set 2026-09-02. Each one turned out to be a mechanism that already existed and was defaulted, named, or gated wrong.

docs/superpowers/architecture/native-mechanisms.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
| Гарантия | Нативный механизм | Есть? | Исп. 2.x | Исп. 3.0 | Почему своё |
1717
|---|---|---|---|---|---|
18-
| Триаж на приходе запроса | `UserPromptSubmit` | ✅ | ❌ | ⚠ | Не смотрели; описательные триггеры срабатывают уже после момента решения. **Понижено с ✅ до ⚠ проверкой по коду 2026-09-01.** Событие занято, но триаж им не обеспечивается: `hooks/triage-prompt-nudge.sh` — это НАПОМИНАНИЕ, и его собственный заголовок говорит это заглавными («никогда не пишет запись, не коммитит и не запускает `/v:triage`», `:27-28`, повторено `:36`). Оно срабатывает **не более одного раза за сессию** (маркер, `:286-309`), так что сессия, чей первый промпт — вопрос, расходует напоминание впустую. Модель, проигнорировавшая строку, не оставляет записи триажа вообще, а Stop-гейт, который поймал бы это, **выключен по умолчанию**: `epic-goal-stop.sh:566` читает `.enforcement.triage_gate // false`. Механически закрыт только путь диспатча — `--require-triage` у валидатора манифеста, — а он срабатывает на `/v:dispatch`, то есть снова ПОСЛЕ момента решения о размере. Ровно тот разрыв, ради которого строка существует |
18+
| Триаж на приходе запроса | `UserPromptSubmit` | ✅ | ❌ | ⚠ | Не смотрели; описательные триггеры срабатывают уже после момента решения. **Понижено с ✅ до ⚠ проверкой по коду 2026-09-01.** Событие занято, но триаж им не обеспечивается: `hooks/triage-prompt-nudge.sh` — это НАПОМИНАНИЕ, и его собственный заголовок говорит это заглавными («никогда не пишет запись, не коммитит и не запускает `/v:triage`», `:27-28`, повторено `:36`). Оно срабатывает **не более одного раза за сессию** (маркер, `:286-309`), так что сессия, чей первый промпт — вопрос, расходует напоминание впустую. Модель, проигнорировавшая строку, не оставляет записи триажа вообще, а Stop-гейт, который поймал бы это, **выключен по умолчанию**: `epic-goal-stop.sh:566` читает `.enforcement.triage_gate // false`. Механически закрыт только путь диспатча — `--require-triage` у валидатора манифеста, — а он срабатывает на `/v:dispatch`, то есть снова ПОСЛЕ момента решения о размере. Ровно тот разрыв, ради которого строка существует **3.1.1 закрыло одну названную здесь дыру, но не строку целиком.** Напоминание больше не тратится на вопрос: промпт до 200 символов, оканчивающийся на `?`, выходит ДО записи маркера, и сессия остаётся взведённой — тест снимает guard и показывает, что три проверки краснеют, то есть он не декоративен. Оставшаяся причина ⚠ прежняя и механическая: модель, проигнорировавшая строку, не оставляет записи триажа вообще, а Stop-гейт, который поймал бы это, по-прежнему выключен по умолчанию (`enforcement.triage_gate // false`). Напоминание стало точнее; механизмом оно не стало |
1919
| Решение о размере записано ||||| Движок на 7 883 строки, **артефактов ноль за всю историю** |
2020
| Полосы файлов объявлены ||||| Понятия «полоса» в рантайме нет |
2121
| Воркер не пишет вне полосы | `PreToolUse` + `permissionDecision: deny` |||| Проба (`0982ce0`): хук **срабатывает** для агентов воркфлоу и несёт `agent_id`, `agent_type`, `cwd` — вызов атрибутируется конкретному воркеру. Но матчер `Write\|Edit` обходится записью через шелл, поэтому deny **дополняет** git-вердикт, а не заменяет |

hooks/triage-prompt-nudge.sh

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@
4040
# 1. the payload parses and is a UserPromptSubmit event
4141
# 2. the prompt does not begin with `/` (a slash command is an invocation, not
4242
# a change request — and `/v:triage` itself is one)
43+
# 2b. the prompt is not a SHORT QUESTION (<=200 chars ending in `?`). A question
44+
# is not a change request, and spending the session's one nudge on it is the
45+
# hole the audit named. Skipping here leaves the session ARMED.
4346
# 3. a `session_id` is present (without it there is nothing to deduplicate on,
4447
# and a nudge on every prompt is worse than no nudge)
4548
# 4. the project looks Compound-V-enabled: `docs/superpowers/` or
@@ -104,7 +107,7 @@
104107
#
105108
# COST, measured on the development machine (macOS, /usr/bin/python3, mean of
106109
# 10 runs against this repository):
107-
# early exit (slash command, or the marker already set) ~9 ms
110+
# early exit (slash command, short question, or marker set) ~9 ms
108111
# full eligibility path (records scan + resume query) ~89 ms
109112
# The full path runs at most once per session — except while a run is active,
110113
# when no marker is written and conditions 6-7 are re-evaluated on each prompt.
@@ -246,16 +249,21 @@ hook_main() {
246249
((.hook_event_name // "") | tostring | gsub("[^A-Za-z]"; "")),
247250
(((.prompt // "") | tostring)[0:1] | gsub("[^/]"; "")),
248251
(((.session_id // "") | tostring) | gsub("[^A-Za-z0-9._:-]"; "")),
249-
(((.cwd // "") | tostring) | gsub("[\n\r]"; ""))
252+
(((.cwd // "") | tostring) | gsub("[\n\r]"; "")),
253+
(((.prompt // "") | tostring) | ascii_downcase
254+
| gsub("^[[:space:]]+"; "") | gsub("[[:space:]]+$"; "")
255+
| if (length > 0 and length <= 200 and (.[-1:] == "?"))
256+
then "q" else "" end)
250257
' 2>/dev/null)" || return 1
251258
[ -n "$fields" ] || return 1
252259

253-
local ev slash sid cwdv
260+
local ev slash sid cwdv question
254261
{
255262
read -r ev
256263
read -r slash
257264
read -r sid
258265
read -r cwdv
266+
read -r question
259267
} <<EOF
260268
${fields}
261269
EOF
@@ -270,6 +278,26 @@ EOF
270278
# `/v:status` and `/clear` all arrive here.
271279
[ -z "${slash:-}" ] || return 1
272280

281+
# A SHORT QUESTION IS NOT A CHANGE REQUEST, AND MUST NOT SPEND THE NUDGE.
282+
#
283+
# The nudge is once per session, and the audit named the hole precisely: a
284+
# session whose first prompt is "what does this do?" burns the reminder on a
285+
# question and the real change request that follows gets nothing. Returning
286+
# here — BEFORE the marker is written and before the ~89 ms eligibility path —
287+
# leaves the session armed and costs the ~9 ms early exit.
288+
#
289+
# The test is deliberately narrow, because the general problem (is this a
290+
# change request?) is not decidable in a hook: a prompt of at most 200
291+
# characters that ENDS IN A QUESTION MARK. Nothing else is treated as a
292+
# question. "Rename getUser to fetchUser, ok?" is 33 characters and ends in
293+
# `?` and will be skipped — that is the deliberate direction of the error: a
294+
# missed nudge costs a reminder, a spent one costs the session's only reminder.
295+
# A long prompt that happens to end in `?` is a description with a question
296+
# attached, and still nudges.
297+
if [ "${question:-}" = "q" ]; then
298+
return 1
299+
fi
300+
273301
# No session id ⇒ nothing to deduplicate on ⇒ stay silent.
274302
[ -n "${sid:-}" ] || return 1
275303

tests/test-native-points.sh

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,35 @@ check "a second prompt in the SAME session is silent (idempotent)" \
169169
out3="$(run_nudge sess-B 'rename the config key')"
170170
check "a different session fires again" "$([ -n "$out3" ] && echo 1 || echo 0)"
171171

172+
# --- 3.1.0: a QUESTION must not spend the session's one nudge -----------------
173+
# The audit named this hole precisely: the nudge fires at most once per session,
174+
# so a session whose first prompt is "what does this do?" burned the reminder and
175+
# the real change request that followed got nothing. A short question now returns
176+
# BEFORE the marker is written, leaving the session armed.
177+
q_out="$(run_nudge sess-Q 'what does this do?')"
178+
check "a short question does not nudge" "$([ -z "$q_out" ] && echo 1 || echo 0)"
179+
q_after="$(run_nudge sess-Q 'add a retry to the uploader')"
180+
check "the question did NOT burn the session's nudge" \
181+
"$([ -n "$q_after" ] && echo 1 || echo 0)"
182+
q_third="$(run_nudge sess-Q 'and bump the timeout too')"
183+
check "the nudge is still once-per-session after a question" \
184+
"$([ -z "$q_third" ] && echo 1 || echo 0)"
185+
markers_before="$(find "$TMPDIR" -name 'nudged-*' 2>/dev/null | wc -l | tr -d ' ')"
186+
_="$(run_nudge sess-Q2 'why is this failing?')"
187+
markers_after="$(find "$TMPDIR" -name 'nudged-*' 2>/dev/null | wc -l | tr -d ' ')"
188+
check "a question leaves NO marker behind (the session stays armed)" \
189+
"$([ "$markers_before" = "$markers_after" ] && echo 1 || echo 0)"
190+
# The test is narrow on purpose: only a SHORT prompt ending in `?` is a question.
191+
long_q="refactor the pricing module and propagate the new type to every caller, \
192+
then re-check the callers in the billing package and the reporting package, and \
193+
make sure the VAT rounding still matches the fixtures, right?"
194+
lq_out="$(run_nudge sess-LQ "$long_q")"
195+
check "a long prompt that merely ends in '?' still nudges" \
196+
"$([ -n "$lq_out" ] && echo 1 || echo 0)"
197+
nq_out="$(run_nudge sess-NQ 'rename getUser to fetchUser')"
198+
check "a plain change request is unaffected" \
199+
"$([ -n "$nq_out" ] && echo 1 || echo 0)"
200+
172201
# --------------------------------------------------------------------------- #
173202
# 2. Exemptions.
174203
# --------------------------------------------------------------------------- #

0 commit comments

Comments
 (0)