Skip to content

Commit 23a29f0

Browse files
committed
feat: refresh the coaching notebook only every 10 lessons
Calling update_notebook after every lesson churned the notebook. The lesson-ready hook now reads the total lesson count from the DB (the source of truth) and makes step 6 of the directive conditional: at an every-10th-lesson checkpoint it asks the model to call update_notebook (recording '(updated after N lessons)'); otherwise it explicitly tells the model NOT to touch the notebook and names the next checkpoint. The model never counts — the hook does.
1 parent 3c9cb43 commit 23a29f0

4 files changed

Lines changed: 77 additions & 22 deletions

File tree

assets/SKILL.md

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -446,13 +446,16 @@ Did that land?
446446

447447
---
448448

449-
### Step 1c — Update the coaching notebook
450-
451-
After feedback is known, fold it into the notebook: call `update_notebook` with the full
452-
revised `learning-state.md` markdown — note what the user absorbed (`know`) or should
453-
revisit (`dont_know`), any new recurring pattern, and updated open hypotheses, keeping the
454-
prior notes. This is what makes the notebook (read from `devcoach://notebook`) drive future
455-
lesson selection.
449+
### Step 1c — Update the coaching notebook (only at every-10-lesson checkpoints)
450+
451+
The notebook's observations are refreshed only at **checkpoints — every 10 delivered
452+
lessons**. The `lesson-ready` hook owns the count and tells you whether the current lesson
453+
is a checkpoint; do not count yourself. **At a checkpoint**, after feedback is known, call
454+
`update_notebook` with the full revised `learning-state.md` markdown — note what the user
455+
absorbed (`know`) or should revisit (`dont_know`), any new recurring pattern, and updated
456+
open hypotheses, keep the prior notes, and record `(updated after N lessons)`. **Between
457+
checkpoints, do not touch the notebook.** This keeps `devcoach://notebook` driving future
458+
lesson selection without churning it on every lesson.
456459

457460
---
458461

plugin/skills/devcoach/SKILL.md

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -446,13 +446,16 @@ Did that land?
446446

447447
---
448448

449-
### Step 1c — Update the coaching notebook
450-
451-
After feedback is known, fold it into the notebook: call `update_notebook` with the full
452-
revised `learning-state.md` markdown — note what the user absorbed (`know`) or should
453-
revisit (`dont_know`), any new recurring pattern, and updated open hypotheses, keeping the
454-
prior notes. This is what makes the notebook (read from `devcoach://notebook`) drive future
455-
lesson selection.
449+
### Step 1c — Update the coaching notebook (only at every-10-lesson checkpoints)
450+
451+
The notebook's observations are refreshed only at **checkpoints — every 10 delivered
452+
lessons**. The `lesson-ready` hook owns the count and tells you whether the current lesson
453+
is a checkpoint; do not count yourself. **At a checkpoint**, after feedback is known, call
454+
`update_notebook` with the full revised `learning-state.md` markdown — note what the user
455+
absorbed (`know`) or should revisit (`dont_know`), any new recurring pattern, and updated
456+
open hypotheses, keep the prior notes, and record `(updated after N lessons)`. **Between
457+
checkpoints, do not touch the notebook.** This keeps `devcoach://notebook` driving future
458+
lesson selection without churning it on every lesson.
456459

457460
---
458461

src/cli/commands.ts

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -787,20 +787,43 @@ function cmdOnboardHook(): void {
787787
);
788788
}
789789

790+
// The notebook's observations are refreshed only every Nth delivered lesson, not after
791+
// each one. The hook owns the count (the DB is the source of truth) so the model never
792+
// has to count.
793+
const NOTEBOOK_UPDATE_EVERY = 10;
794+
790795
function cmdLessonReady(): void {
791796
if (stopHookContinuation()) process.exit(0);
792797
// No DB → no profile yet. Stay silent without creating coaching.db.
793798
if (!existsSync(db.DB_PATH)) process.exit(0);
794-
let allowed: boolean;
799+
let result: { allowed: boolean; nextLessonNumber: number };
795800
try {
796-
allowed = db.withConnection((conn) => {
797-
if (!db.isOnboardingComplete(conn).knowledge_ready) return false;
798-
return coach.checkRateLimit(conn).allowed;
801+
result = db.withConnection((conn) => {
802+
if (!db.isOnboardingComplete(conn).knowledge_ready) {
803+
return { allowed: false, nextLessonNumber: 0 };
804+
}
805+
return {
806+
allowed: coach.checkRateLimit(conn).allowed,
807+
nextLessonNumber: Number(coach.getStats(conn).total_lessons ?? 0) + 1,
808+
};
799809
});
800810
} catch {
801811
process.exit(0);
802812
}
803-
if (!allowed) process.exit(0);
813+
if (!result.allowed) process.exit(0);
814+
815+
const { nextLessonNumber } = result;
816+
const updateDue = nextLessonNumber % NOTEBOOK_UPDATE_EVERY === 0;
817+
const nextCheckpoint =
818+
Math.ceil(nextLessonNumber / NOTEBOOK_UPDATE_EVERY) * NOTEBOOK_UPDATE_EVERY;
819+
const notebookStep = updateDue
820+
? `6. This is lesson #${nextLessonNumber} — a notebook checkpoint (every ${NOTEBOOK_UPDATE_EVERY} ` +
821+
"lessons). After log_lesson returns the feedback, call update_notebook with the revised " +
822+
"notebook markdown: fold in what the user absorbed or struggled with and any new pattern, " +
823+
`keep the prior notes, and record "(updated after ${nextLessonNumber} lessons)".`
824+
: `6. Do NOT call update_notebook this time — the notebook is refreshed only every ` +
825+
`${NOTEBOOK_UPDATE_EVERY} lessons (next checkpoint at lesson #${nextCheckpoint}). Just ` +
826+
"deliver the card and call log_lesson.";
804827

805828
emitBlock(
806829
"devcoach: a lesson is due for the technical work just completed. Do it now — do not " +
@@ -825,9 +848,7 @@ function cmdLessonReady(): void {
825848
"summary in their own fields; body = ONLY the prose + the 💡 tip as CLEAN markdown (no " +
826849
"bands, no '>' quote, no title or 'Category · Level' line). log_lesson then asks the user " +
827850
"'Did that land?', so the card MUST be printed first.\n" +
828-
"6. After log_lesson returns the feedback, call update_notebook with the revised notebook " +
829-
"markdown: fold in what the user absorbed or struggled with and any new pattern, keeping " +
830-
"the existing notes.\n\n" +
851+
`${notebookStep}\n\n` +
831852
"The rate-limit check is already done by this hook — skip it. Output only the lesson " +
832853
"card (or nothing). No preamble, no meta-commentary.",
833854
);

tests/cli.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,34 @@ describe("cli", () => {
8888
expect(lr.out).toContain("update_notebook");
8989
});
9090

91+
it("lesson-ready gates the notebook update to every 10 lessons", async () => {
92+
const seed = (n: number) =>
93+
db.withConnection((c) => {
94+
c.exec("DELETE FROM lessons");
95+
for (let i = 0; i < n; i++) {
96+
db.insertLesson(
97+
c,
98+
parseLesson({
99+
id: `seed-${i}`,
100+
timestamp: "2026-01-01T00:00:00Z",
101+
topic_id: "python",
102+
categories: [],
103+
title: "T",
104+
level: "mid",
105+
summary: "s",
106+
}),
107+
);
108+
}
109+
db.upsertKnowledge(c, "python", 4);
110+
});
111+
seed(5); // next lesson is #6 → not a checkpoint → skip the notebook update
112+
expect((await run(["lesson-ready"])).out).toContain("Do NOT call update_notebook");
113+
seed(9); // next lesson is #10 → checkpoint → update the notebook
114+
const due = await run(["lesson-ready"]);
115+
expect(due.out).toContain("notebook checkpoint");
116+
expect(due.out).toContain("call update_notebook");
117+
});
118+
91119
it("parseStopHookActive: only a payload with stop_hook_active=true short-circuits", () => {
92120
expect(parseStopHookActive(JSON.stringify({ stop_hook_active: true }))).toBe(true);
93121
expect(parseStopHookActive(JSON.stringify({ stop_hook_active: false }))).toBe(false);

0 commit comments

Comments
 (0)