yt-briefing 0.14.1 → 0.15.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/yt/SKILL.md +1 -1
- package/README.md +12 -4
- package/dist/lib/skill-install.js +7 -5
- package/dist/lib/summary-gate.js +12 -9
- package/dist/yt-summary-gate.js +27 -13
- package/package.json +1 -1
|
@@ -34,7 +34,7 @@ while true:
|
|
|
34
34
|
**On `rating_needed` — literally:**
|
|
35
35
|
|
|
36
36
|
- **A.** Take `out.summary` (markdown) and `out.pending` (metadata).
|
|
37
|
-
- **B.** In the same turn, as **your chat text** (NOT command output — the UI does not show it), paste `summary` **verbatim**: no paraphrase, no shortening, no comment, no "see above". The user must see it before the popup. B is **unconditional** — it does not depend on there being anything else to write, so an iteration that returns no `skipped` line still opens with the pasted summary, never straight with the tool call. _(If the user says "I don't see the summary" — you skipped B. In Claude Code a PreToolUse gate
|
|
37
|
+
- **B.** In the same turn, as **your chat text** (NOT command output — the UI does not show it), paste `summary` **verbatim**: no paraphrase, no shortening, no comment, no "see above". The user must see it before the popup. B is **unconditional** — it does not depend on there being anything else to write, so an iteration that returns no `skipped` line still opens with the pasted summary, never straight with the tool call. _(If the user says "I don't see the summary" — you skipped B. In Claude Code a PreToolUse gate refuses to record the rating in step D when the pending video's id is missing from your chat text; if it fires, paste the summary, ask for the rating again, then record it.)_
|
|
38
38
|
- **C.** In the same message call `AskUserQuestion` — **1 call, 1 question** (everything in one step), phrased in `output_lang`:
|
|
39
39
|
- The question (e.g. "Rating?") with three options whose descriptions explain: **OK** = neutral (no effect on the filter), **Weak** = worthless (teach the filter to skip such titles), **Research** = break the loop and dig into this video's content together. The digits never appear in the popup — internally map to `--rating`: **OK → 1, Weak → 0**; Research maps to no digit, it exits the loop (see Research mode). There is **no positive rating** — keeping the channel is the implicit positive; you only down-rate noise (`0`) or steer with a comment.
|
|
40
40
|
- **Other** is the comment / stop / research channel (no second question): the user types free text. If it equals `stop` (case-insensitive, trimmed) — or the popup is dismissed (✕) — **end the loop**. If it **starts with `?`**, the text after the `?` is a **research question** → Research mode. Otherwise it is a **comment**: **distill** the user's raw text into a clean, generalizable rule, and infer the rating — clearly negative → `0`, otherwise → `1`.
|
package/README.md
CHANGED
|
@@ -139,21 +139,29 @@ session. To install the skills again for another tool or project, run
|
|
|
139
139
|
The loop only works if you see the summary *before* you rate it, and that is the one step an
|
|
140
140
|
agent can silently drop — the popup still appears, you still answer, and the rating is recorded
|
|
141
141
|
against a summary nobody read. On Claude Code the installer wires a `PreToolUse` hook into your
|
|
142
|
-
project's `.claude/settings.json` that refuses
|
|
143
|
-
the chat. It merges with your existing hooks and updates itself on reinstall. If that file
|
|
144
|
-
valid JSON the installer leaves it alone and says so — add the entry yourself:
|
|
142
|
+
project's `.claude/settings.json` that refuses to *record* a rating unless the video's summary is
|
|
143
|
+
in the chat. It merges with your existing hooks and updates itself on reinstall. If that file
|
|
144
|
+
isn't valid JSON the installer leaves it alone and says so — add the entry yourself:
|
|
145
145
|
|
|
146
146
|
```json
|
|
147
147
|
{
|
|
148
148
|
"hooks": {
|
|
149
149
|
"PreToolUse": [
|
|
150
|
-
{ "matcher": "
|
|
150
|
+
{ "matcher": "Bash",
|
|
151
151
|
"hooks": [ { "type": "command", "command": "node \"${CLAUDE_PROJECT_DIR}/node_modules/yt-briefing/dist/yt-summary-gate.js\"" } ] }
|
|
152
152
|
]
|
|
153
153
|
}
|
|
154
154
|
}
|
|
155
155
|
```
|
|
156
156
|
|
|
157
|
+
It watches the rating write rather than the popup, because the popup cannot be gated: the agent
|
|
158
|
+
pastes the summary and opens the popup in one message, and the harness only writes a message to
|
|
159
|
+
the transcript once that message is complete — so the evidence does not exist yet at popup time.
|
|
160
|
+
The rating write is a separate call in the next message, where it does. The matcher is `Bash`, so
|
|
161
|
+
the hook is invoked on ordinary shell commands too; it reads the command line and exits before
|
|
162
|
+
touching anything. The tradeoff: a genuinely skipped paste is caught after you have answered, so
|
|
163
|
+
that one answer is wasted — but nothing reaches the channel profile unseen.
|
|
164
|
+
|
|
157
165
|
Other agents have no equivalent hook, so there the instruction in `SKILL.md` is what holds.
|
|
158
166
|
|
|
159
167
|
## Providers
|
|
@@ -111,7 +111,7 @@ const GATE_ID = 'yt-summary-gate';
|
|
|
111
111
|
* How settings.json must invoke the gate. Like the skill's engine commands it bakes nothing
|
|
112
112
|
* machine-absolute, but a hook may NOT assume its cwd — Claude Code's hooks reference states the
|
|
113
113
|
* working directory can vary, so a bare relative path would fail open, silently and invisibly
|
|
114
|
-
* (the
|
|
114
|
+
* (the rating is written ungated, which is exactly the bug the gate exists to catch). Hence the
|
|
115
115
|
* `$CLAUDE_PROJECT_DIR` prefix: still just a placeholder string in the committed JSON, resolved
|
|
116
116
|
* to the project root at hook time.
|
|
117
117
|
*/
|
|
@@ -119,14 +119,16 @@ export const gateCommand = (dist = false) => dist
|
|
|
119
119
|
? `${isBun ? 'bun' : 'node'} "\${CLAUDE_PROJECT_DIR}/${toProjectRel(join(DIST_DIR, GATE_ID + '.js'))}"`
|
|
120
120
|
: `bun run src/${GATE_ID}.ts`;
|
|
121
121
|
/**
|
|
122
|
-
* Put the gate into a settings object: a PreToolUse hook on
|
|
123
|
-
*
|
|
124
|
-
*
|
|
122
|
+
* Put the gate into a settings object: a PreToolUse hook on Bash, which is where the rating is
|
|
123
|
+
* written (gating the popup instead cannot work — see src/yt-summary-gate.ts). Merges — every
|
|
124
|
+
* other setting and hook is left as found, and our own entry is updated in place, so
|
|
125
|
+
* reinstalling (or upgrading from the pre-0.15.0 AskUserQuestion matcher) never duplicates or
|
|
126
|
+
* clobbers anything.
|
|
125
127
|
*/
|
|
126
128
|
export function withGateHook(settings, command) {
|
|
127
129
|
const preToolUse = ((settings.hooks ??= {}).PreToolUse ??= []);
|
|
128
130
|
const mine = preToolUse.find((e) => e.hooks?.some((h) => h.command?.includes(GATE_ID)));
|
|
129
|
-
const entry = { matcher: '
|
|
131
|
+
const entry = { matcher: 'Bash', hooks: [{ type: 'command', command }] };
|
|
130
132
|
if (mine)
|
|
131
133
|
Object.assign(mine, entry);
|
|
132
134
|
else
|
package/dist/lib/summary-gate.js
CHANGED
|
@@ -2,18 +2,21 @@
|
|
|
2
2
|
* Decision logic for the /yt summary gate — kept separate from the hook script so it is
|
|
3
3
|
* runtime-agnostic and unit-testable: pure functions over a payload and a transcript.
|
|
4
4
|
*
|
|
5
|
-
* See `src/yt-summary-gate.ts` for why the gate exists and
|
|
5
|
+
* See `src/yt-summary-gate.ts` for why the gate exists and where it had to move to work at all.
|
|
6
6
|
*/
|
|
7
|
+
/** Substring identifying the rating writer; the skill always invokes it by this script name. */
|
|
8
|
+
const RATING_SCRIPT = 'yt-rating';
|
|
7
9
|
/**
|
|
8
|
-
* Is this the
|
|
9
|
-
*
|
|
10
|
-
*
|
|
10
|
+
* Is this the call that writes a rating? The skill records every rating by running
|
|
11
|
+
* `yt-rating(.ts|.js) --rating <1|0>` through Bash, so the command line is the signal. Anything
|
|
12
|
+
* else — a sweep, a transcript pull, unrelated shell work — is none of the gate's business, and
|
|
13
|
+
* is waved through before this module does any I/O at all.
|
|
11
14
|
*/
|
|
12
|
-
export function
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
return
|
|
15
|
+
export function isRatingWrite(payload) {
|
|
16
|
+
if (payload.tool_name !== 'Bash')
|
|
17
|
+
return false;
|
|
18
|
+
const command = payload.tool_input?.command ?? '';
|
|
19
|
+
return command.includes(RATING_SCRIPT) && command.includes('--rating');
|
|
17
20
|
}
|
|
18
21
|
/**
|
|
19
22
|
* Did the agent actually paste the summary? The summary carries the video's watch URL, so its id
|
package/dist/yt-summary-gate.js
CHANGED
|
@@ -1,25 +1,38 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* yt-summary-gate — PreToolUse gate for Claude Code: refuses
|
|
3
|
+
* yt-summary-gate — PreToolUse gate for Claude Code: refuses to RECORD a rating until the
|
|
4
4
|
* video's summary has actually been pasted into the chat.
|
|
5
5
|
*
|
|
6
6
|
* Step B of the rating loop (paste `out.summary` verbatim, THEN ask) is the one instruction the
|
|
7
7
|
* agent can drop with no visible symptom: the popup still appears, the user still answers, and
|
|
8
|
-
* the rating is recorded against a summary nobody saw.
|
|
9
|
-
*
|
|
10
|
-
* dropped in iterations that return none, where the turn can open straight with a tool call. A
|
|
11
|
-
* blind rating is worse than no rating: it teaches the title filter from noise.
|
|
8
|
+
* the rating is recorded against a summary nobody saw. A blind rating is worse than no rating:
|
|
9
|
+
* it teaches the title filter from noise.
|
|
12
10
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
11
|
+
* WHY IT GATES THE WRITE AND NOT THE POPUP. Until 0.15.0 this hook ran on `AskUserQuestion` and
|
|
12
|
+
* blocked the popup. That could never work: the harness writes an assistant message's entries to
|
|
13
|
+
* the transcript only once the message is COMPLETE — i.e. after its tool calls have returned —
|
|
14
|
+
* and the skill pastes the summary and opens the popup in a single message. So the gate asked
|
|
15
|
+
* the transcript for text that could not be there yet, and the write it was waiting on could not
|
|
16
|
+
* happen until the gate returned. It blocked every video and cleared on a blind retry (measured
|
|
17
|
+
* 2026-08-30: a marker in a long text block was absent from the transcript during its own
|
|
18
|
+
* message's tool call, and present in the next message immediately). 0.14.2's 5s poll treated
|
|
19
|
+
* that as a timing lag and so only made the block slower.
|
|
20
|
+
*
|
|
21
|
+
* The rating write is a separate Bash call in the FOLLOWING message, by which point the message
|
|
22
|
+
* carrying the summary is closed and on disk — the evidence exists exactly when this hook needs
|
|
23
|
+
* it. The tradeoff is honest: a genuinely skipped paste is now caught after the user has already
|
|
24
|
+
* answered the popup, so that one answer is wasted and the agent has to paste and re-ask. That
|
|
25
|
+
* costs nothing on the happy path, and it guards the thing that actually matters — what gets
|
|
26
|
+
* written into the channel profile.
|
|
16
27
|
*
|
|
17
28
|
* Reads the PreToolUse payload on stdin; exit 0 allows, exit 2 blocks and feeds stderr back to
|
|
18
29
|
* the agent. Anything unexpected — foreign tool, unreadable transcript, no pending video —
|
|
19
|
-
* allows: a gate that misfires on unrelated work would be worse than the bug it guards.
|
|
30
|
+
* allows: a gate that misfires on unrelated work would be worse than the bug it guards. This
|
|
31
|
+
* hook is matched on Bash, so it runs on ordinary shell work too: it must decide from the
|
|
32
|
+
* command string and exit before touching the disk.
|
|
20
33
|
*/
|
|
21
34
|
import { readFileSync, existsSync } from 'node:fs';
|
|
22
|
-
import {
|
|
35
|
+
import { isRatingWrite, summaryWasPasted } from "./lib/summary-gate.js";
|
|
23
36
|
const ALLOW = 0;
|
|
24
37
|
const BLOCK = 2;
|
|
25
38
|
const allow = () => process.exit(ALLOW);
|
|
@@ -30,7 +43,7 @@ try {
|
|
|
30
43
|
catch {
|
|
31
44
|
allow();
|
|
32
45
|
}
|
|
33
|
-
if (
|
|
46
|
+
if (!isRatingWrite(payload))
|
|
34
47
|
allow();
|
|
35
48
|
// paths.ts derives the data dir from the cwd, which for a hook is the agent's, not ours — so the
|
|
36
49
|
// import has to wait until we've moved there.
|
|
@@ -58,6 +71,7 @@ catch {
|
|
|
58
71
|
}
|
|
59
72
|
if (seen)
|
|
60
73
|
allow();
|
|
61
|
-
process.stderr.write(`/yt step B not done: the summary for ${pending.videoId} ("${pending.title ?? '?'}")
|
|
62
|
-
`
|
|
74
|
+
process.stderr.write(`/yt step B not done: the summary for ${pending.videoId} ("${pending.title ?? '?'}") never appeared in your chat text, ` +
|
|
75
|
+
`so this rating would be recorded against a summary the user never saw.\n` +
|
|
76
|
+
`Paste out.summary verbatim as your reply, ask for the rating again, then record it.\n`);
|
|
63
77
|
process.exit(BLOCK);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yt-briefing",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "A self-learning YouTube briefing engine: it sweeps the channels you follow, filters noise in two stages (title, then transcript), summarizes the rest in your language, and adapts to your ratings — one video at a time.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|