@kaydash9999/banker-plugins 0.17.1 → 0.19.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 (38) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +72 -0
  4. package/README.md +26 -8
  5. package/codex/manifest.json +3 -1
  6. package/codex/transform-matrix.md +5 -2
  7. package/commands/front-qa.md +3 -3
  8. package/hooks/graceful-pause.mjs +4 -2
  9. package/hooks/obsidize-hook.mjs +38 -28
  10. package/hooks/progress-plan.mjs +14 -8
  11. package/hooks/progress.mjs +174 -47
  12. package/hooks/register.mjs +14 -7
  13. package/hooks/update-checkin.mjs +29 -27
  14. package/package.json +1 -1
  15. package/skills/3d-intro-build/SKILL.md +1 -1
  16. package/skills/3d-intro-build/references/assemble.mjs +3 -3
  17. package/skills/3d-intro-setup/SKILL.md +3 -3
  18. package/skills/anti-drm/SKILL.md +254 -0
  19. package/skills/anti-drm/scripts/anti-drm.mjs +643 -0
  20. package/skills/anti-drm/scripts/instruction-toggle.mjs +733 -0
  21. package/skills/lineage/SKILL.md +3 -3
  22. package/skills/lineage/lineage.py +43 -7
  23. package/skills/omc-patch/SKILL.md +18 -6
  24. package/skills/omc-patch/scripts/omc-patch.mjs +400 -87
  25. package/skills/omc-reference/SKILL.md +1 -1
  26. package/skills/ralph-qa/SKILL.md +19 -19
  27. package/skills/theme-selection/SKILL.md +165 -0
  28. package/skills/theme-selection/assets/LICENSE +21 -0
  29. package/skills/theme-selection/assets/NOTICE.md +30 -0
  30. package/skills/theme-selection/assets/palettes.json +81 -0
  31. package/skills/theme-selection/references/contrast.md +138 -0
  32. package/skills/theme-selection/references/integrate.md +161 -0
  33. package/skills/theme-selection/scripts/themes.mjs +1067 -0
  34. package/skills/tone-compact/SKILL.md +8 -4
  35. package/skills/tone-compact/scripts/instruction-toggle.mjs +733 -0
  36. package/skills/tone-compact/scripts/tone-compact.mjs +48 -397
  37. package/skills/ultra-chain/SKILL.md +7 -6
  38. package/skills/update-banker/SKILL.md +1 -1
@@ -7,23 +7,29 @@
7
7
  * step for the work at hand. Under each step are the tool calls made while it was the step in
8
8
  * progress (a subagent's or a background agent's included), one level deep. A press on a step or
9
9
  * a call shows a short description under the list; a press on a step's arrow folds or unfolds its
10
- * calls.
10
+ * calls. Of two calls on the task list that overlap, the one begun later stands, and a list or a
11
+ * read begun before a change keeps what changed after it began; the engine's own order of
12
+ * overlapping calls is not something this module can see.
11
13
  *
12
- * Claude 5 models get no task list tool in Claude Code 2.1.296 (only older models, a background
13
- * job, or CLAUDE_CODE_ENABLE_TODO_TOOLS do). Opening the pane offers the model one of its own
14
- * (`mcp__banker__todo`, TodoWrite's shape) where it has none, and the list it sends is the steps.
14
+ * Claude 5 models get no task list tool in Claude Code 2.1.296 and 2.1.300 unless
15
+ * CLAUDE_CODE_ENABLE_TODO_TOOLS is set; older models and a background job get one. (Seen in a
16
+ * live 2.1.300 session with Haiku 5.5: with the variable TaskCreate, TaskUpdate, TaskList and
17
+ * TaskGet came and the pane showed their list; without it, none.) Opening the pane offers the
18
+ * model one of its own (`mcp__banker__todo`, TodoWrite's shape) where it has none, and the list it
19
+ * sends is the steps.
15
20
  * Above them the pane shows the plan file the session touched last (progress-plan.mjs reads it):
16
21
  * the whole plan as a tree that folds like a toggle list, its sections folded at first.
17
22
  * A prompt's harness tags leave its title, and a background task's notice (or a teammate's message)
18
23
  * goes on with the last prompt's step instead of becoming one: the step runs again while it does,
19
24
  * and keeps its own end and time.
20
25
  *
21
- * `/progress` and `/progress show` toggle the pane, `on` and `off` set it. The command is
22
- * registered `immediate`, so it answers while a turn runs. An engine without mods never loads
23
- * this file: commands/progress.md and hooks/progress-fallback.mjs answer there instead.
26
+ * `/progress` and `/progress show` toggle the pane, `on` and `off` set it. From Claude Code 2.1.289
27
+ * on the command is registered `immediate`, so it answers while a turn runs; an older engine gets it
28
+ * as a plain command. An engine without mods never loads this file: commands/progress.md and
29
+ * hooks/progress-fallback.mjs answer there instead.
24
30
  */
25
31
 
26
- import { cells, clip, fit, isPlanPath, parsePlan, pick, readPrompt } from './progress-plan.mjs';
32
+ import { cells, clip, fit, parsePlan, pick, planKind, readPrompt } from './progress-plan.mjs';
27
33
 
28
34
  const COMMAND = 'progress';
29
35
  const PANE = 'banker-progress';
@@ -91,6 +97,8 @@ const NOTICE = {
91
97
  completed: '백그라운드 작업 끝남', failed: '백그라운드 작업 실패', stopped: '백그라운드 작업 중지됨',
92
98
  killed: '백그라운드 작업 중지됨', message: '팀원 메시지',
93
99
  };
100
+ // How a notice's turn ended other than with an answer, as the row under its step says it.
101
+ const NOTICE_END = { aborted: '알림 처리 중단됨', refusal: '알림 처리 거절됨', error: '알림 처리 오류로 끝남' };
94
102
  // How a prompt's turn ended (turn.complete's reason), as its step's state.
95
103
  const ENDED = { answer: 'done', aborted: 'stopped', refusal: 'refused', error: 'failed' };
96
104
  // Why a hook failed (the kind of the engine's HookFailure), for a failure that brings no message.
@@ -108,12 +116,17 @@ const tally = () => ({ calls: [], count: 0, failed: 0 });
108
116
  // The calls made while the task list has no task in progress.
109
117
  const looseStep = () => ({ key: 'loose', title: '작업 목록 밖의 작업', ...tally() });
110
118
 
111
- // `rev` counts the task changes (TaskCreate, TaskUpdate) taken in, to tell a stale TaskList or TaskGet.
119
+ // `rev` counts the task changes (TaskCreate, TaskUpdate) taken in, and `order` the calls begun: a TaskList or
120
+ // TaskGet begun before a change keeps what changed after it began, and of two calls that overlap the one begun later
121
+ // stands. `marks` holds, per task id, the orders that decide it (markOf). `listAt` is the order of the TaskList the
122
+ // list last came from.
112
123
  // `plan` is the plan last read from `planPath`, `planShut` whether the whole plan is folded and
113
124
  // `planOpen` the ids of its nodes unfolded. `touch` counts the calls that touched a plan file and
114
- // `taken` is the touch the plan came from, to tell a read that a newer one overtook.
125
+ // `taken` is the touch the plan came from, to tell a read that a newer one overtook. `followed` is the step in
126
+ // progress the pane last asked to bring into view.
115
127
  export const freshState = () => ({ turns: [], tasks: new Map(), todos: null, loose: looseStep(), open: new Set(), selected: null, seq: 0, rev: 0,
116
- plan: null, planPath: null, planShut: false, planOpen: new Set(), touch: 0, taken: 0 });
128
+ order: 0, marks: new Map(), listAt: 0, plan: null, planPath: null, planShut: false, planOpen: new Set(), touch: 0, taken: 0,
129
+ followed: null });
117
130
 
118
131
  const tail = (path) => String(path).split(/[\\/]/).filter(Boolean).slice(-2).join('/');
119
132
  const seconds = (ms) => {
@@ -179,7 +192,10 @@ function dropFinished(s) {
179
192
  s.loose = looseStep();
180
193
  }
181
194
 
195
+ // A main turn's start. A subagent's, a fork's or a teammate's run (it carries an agentId; Claude Code 2.1.300
196
+ // raises turn.start for them too) makes no step: its calls go under the step in progress.
182
197
  export function startTurn(s, e, now) {
198
+ if (e.agentId) return;
183
199
  const said = readPrompt(e.text);
184
200
  if (said.kind === 'notice') return noticeTurn(s, e, said, now);
185
201
  dropFinished(s);
@@ -187,13 +203,16 @@ export function startTurn(s, e, now) {
187
203
  }
188
204
 
189
205
  // A main turn's end; a subagent's (it carries an agentId) ends no step, and a notice's leaves its
190
- // step's own end as it was. A turn the module never saw start (it loaded mid-turn) ends the step for
191
- // the work at hand.
206
+ // step's own end as it was. A notice's turn that did not end with an answer (stopped, refused, an
207
+ // error) adds a row saying so under the step. A turn the module never saw start (it loaded mid-turn)
208
+ // ends the step for the work at hand.
192
209
  export function endTurn(s, e, now) {
193
210
  if (e.agentId) return;
194
211
  const host = s.turns.find((x) => x.notices.has(e.turnId));
195
212
  if (host) {
196
213
  host.notices.delete(e.turnId);
214
+ const how = pick(NOTICE_END, e.reason);
215
+ if (how) note(s, host, how, false, now);
197
216
  return;
198
217
  }
199
218
  const t = s.turns.find((x) => x.id === e.turnId) ?? s.turns.find((x) => x.id === AT_HAND && x.status === 'running');
@@ -275,17 +294,46 @@ function putTask(s, t, now) {
275
294
  while (s.tasks.size > MAX_STEPS) s.tasks.delete(overflow([...s.tasks.values()]).id);
276
295
  }
277
296
 
278
- function createTask(s, e, r, now) {
297
+ // The marks of a task id, made on first use. They live in the state, not on the task, so a task deleted, or not
298
+ // created yet, keeps them: `changedRev` the change count after its last TaskCreate or TaskUpdate (a delete
299
+ // included), `statusAt` and `status` the order and the status of the last status update begun, `seenAt` the order
300
+ // of the last read taken in.
301
+ function markOf(s, id) {
302
+ if (!s.marks.has(id)) s.marks.set(id, {});
303
+ return s.marks.get(id);
304
+ }
305
+
306
+ // Whether a list or a read begun at (since, order) is older than what the pane holds of a task: the task changed
307
+ // after it began (a delete too), or a later-begun read already brought it.
308
+ function stale(s, id, since, order) {
309
+ const m = s.marks.get(id);
310
+ return Boolean(m) && ((m.changedRev ?? 0) > since || order < (m.seenAt ?? 0));
311
+ }
312
+
313
+ // An update begun after the create may end first (the ids are predictable): its status stands, and a delete wins.
314
+ function createTask(s, e, r, now, { order }) {
279
315
  const id = r?.result?.task?.id;
280
316
  if (id === undefined) return;
281
- putTask(s, { id: String(id), subject: e.subject, description: e.description, activeForm: e.activeForm, status: 'pending' }, now);
317
+ const m = s.marks.get(String(id));
318
+ const status = m && order < (m.statusAt ?? 0) ? m.status : 'pending';
319
+ if (status === 'deleted') return;
320
+ putTask(s, { id: String(id), subject: e.subject, description: e.description, activeForm: e.activeForm, status }, now);
282
321
  }
283
322
 
284
- // A task this module never saw (a list from before it loaded, or one another session shares) joins
285
- // the list while it is not done.
286
- function updateTask(s, e, r, now) {
323
+ // The status a TaskUpdate left: the change it answers with, else the one it asked for.
324
+ const updatedStatus = (e, r) => r?.result?.statusChange?.to ?? e.status;
325
+
326
+ // A task this module never saw (a list from before it loaded, or one another session shares) joins the list while
327
+ // it is not done. Of two status updates to one task, the one begun later stands; an update without a status (an
328
+ // owner, a description) brings its words only.
329
+ function updateTask(s, e, r, now, { order }) {
287
330
  const id = String(e.taskId);
288
- const status = r?.result?.statusChange?.to ?? e.status;
331
+ const status = updatedStatus(e, r);
332
+ if (status !== undefined) {
333
+ const m = markOf(s, id);
334
+ if (order < (m.statusAt ?? 0)) return;
335
+ Object.assign(m, { statusAt: order, status });
336
+ }
289
337
  if (status === 'deleted') {
290
338
  s.tasks.delete(id);
291
339
  return;
@@ -294,28 +342,55 @@ function updateTask(s, e, r, now) {
294
342
  putTask(s, { id, subject: e.subject, description: e.description, activeForm: e.activeForm, status }, now);
295
343
  }
296
344
 
297
- // TaskList names the whole list, in its order: a task it leaves out is gone. A list whose every task
298
- // is completed is over, and only brings up to date the tasks still on the pane.
299
- function listTasks(s, _e, r, now, since) {
345
+ // Whether the pane changed a task (a delete too) after a list of it began.
346
+ const changedAfter = (s, id, since) => (s.marks.get(id)?.changedRev ?? 0) > since;
347
+ // The status a list or a read begun at order shows for a task: a status update begun after it stands, also for a
348
+ // task the pane did not hold then.
349
+ const readStatus = (s, id, order, status) => {
350
+ const m = s.marks.get(id);
351
+ return m && order < (m.statusAt ?? 0) ? m.status : status;
352
+ };
353
+
354
+ // One task a TaskList names: as the pane holds it where it changed since the list began or a later-begun read
355
+ // brought it, else as listed.
356
+ function takeListed(s, before, t, { over, since, order, now }) {
357
+ const id = String(t.id);
358
+ const held = before.get(id);
359
+ if (held) s.tasks.set(id, held);
360
+ if (stale(s, id, since, order)) return;
361
+ const status = readStatus(s, id, order, t.status);
362
+ if (status === 'deleted') return;
363
+ if (held || !over) putTask(s, { id, subject: t.subject, status }, now);
364
+ markOf(s, id).seenAt = order;
365
+ }
366
+
367
+ // TaskList names the whole list, in its order: a task it leaves out is gone, unless it changed after the list
368
+ // began. A list whose every task is completed is over, and only brings up to date the tasks still on the pane. A
369
+ // list begun before one already taken in changes nothing.
370
+ function listTasks(s, _e, r, now, { since, order }) {
300
371
  const listed = r?.result?.tasks;
301
- if (since !== s.rev || !Array.isArray(listed)) return;
372
+ if (!Array.isArray(listed) || order < s.listAt) return;
373
+ s.listAt = order;
302
374
  const over = listed.every((t) => t.status === 'completed');
303
375
  const before = s.tasks;
376
+ const named = new Set(listed.map((t) => String(t.id)));
304
377
  s.tasks = new Map();
305
- for (const t of listed) {
306
- const id = String(t.id);
307
- if (before.has(id)) s.tasks.set(id, before.get(id));
308
- if (before.has(id) || !over) putTask(s, { id, subject: t.subject, status: t.status }, now);
309
- }
378
+ for (const t of listed) takeListed(s, before, t, { over, since, order, now });
379
+ for (const [id, held] of before) if (!named.has(id) && changedAfter(s, id, since)) s.tasks.set(id, held);
310
380
  }
311
381
 
312
- // TaskGet reads one task: one it cannot find is gone, and a completed one off the pane stays off.
313
- function getTask(s, e, r, now, since) {
314
- if (since !== s.rev) return;
382
+ // TaskGet reads one task: one it cannot find is gone, and a completed one off the pane stays off. A read older than
383
+ // what the pane holds of the task changes nothing.
384
+ function getTask(s, e, r, now, { since, order }) {
385
+ if (stale(s, String(e.taskId), since, order)) return;
315
386
  const t = r?.result?.task;
316
387
  if (t === null) s.tasks.delete(String(e.taskId));
317
- if (!t || (t.status === 'completed' && !s.tasks.has(String(t.id)))) return;
318
- putTask(s, { id: String(t.id), subject: t.subject, description: t.description, status: t.status }, now);
388
+ if (!t) return;
389
+ const id = String(t.id);
390
+ const status = readStatus(s, id, order, t.status);
391
+ if (status === 'deleted' || (status === 'completed' && !s.tasks.has(id))) return;
392
+ putTask(s, { id, subject: t.subject, description: t.description, status }, now);
393
+ markOf(s, id).seenAt = order;
319
394
  }
320
395
 
321
396
  const TASK_NOTES = {
@@ -329,15 +404,22 @@ const TASK_NOTES = {
329
404
  // A refused or failed call changes nothing: the task list stays as Claude Code holds it. A TaskUpdate
330
405
  // that changed nothing (an id it cannot find, a TaskCompleted hook that blocks) answers
331
406
  // `success: false` without isError.
332
- // TaskList and TaskGet run beside other calls: one that read the list before a change landed (`since`,
333
- // the count when it started, is behind) is stale and changes nothing.
407
+ // TaskList and TaskGet run beside other calls (`since` is the count of changes when one began, `order` its place
408
+ // among the calls begun): what changed after it began, or what a later-begun call already brought, stays.
334
409
  const refused = (r) => Boolean(r?.deny || r?.isError || r?.result?.success === false);
335
410
  const CHANGES = new Set(['TaskCreate', 'TaskUpdate']);
411
+ // The task a TaskCreate or TaskUpdate changed, or undefined where the answer names none.
412
+ const changedId = (e, r) => (e.tool === 'TaskCreate' ? r?.result?.task?.id : e.taskId);
336
413
 
337
- export function noteTaskTool(s, e, r, now, since) {
414
+ export function noteTaskTool(s, e, r, now, since = s.rev, order = ++s.order) {
338
415
  if (refused(r)) return;
339
- TASK_NOTES[e.tool]?.(s, e, r, now, since ?? s.rev);
340
- if (CHANGES.has(e.tool)) s.rev += 1;
416
+ TASK_NOTES[e.tool]?.(s, e, r, now, { since, order });
417
+ if (!CHANGES.has(e.tool)) return;
418
+ s.rev += 1;
419
+ // Only a change the pane took in marks the task: one it holds after the change, or a delete. An update of a task the
420
+ // pane does not hold leaves the lists alone (its status still counts, through readStatus).
421
+ const id = changedId(e, r);
422
+ if (id !== undefined && (s.tasks.has(String(id)) || updatedStatus(e, r) === 'deleted')) markOf(s, String(id)).changedRev = s.rev;
341
423
  }
342
424
 
343
425
  // The list a to-do call sent, each item's words cleaned, or what is wrong with it.
@@ -369,12 +451,13 @@ export function answerTodo(s, e, now, paneOpen = true) {
369
451
 
370
452
  // The plan file a call touched, with the touch's count, when it is one and the call went through:
371
453
  // a write by anyone, a read by the session itself (an agent reviewing a plan does not make it the plan).
372
- // `own` is a write by the session itself.
454
+ // `own` is a write by the session itself; `weak` a review round or open questions beside a plan.
373
455
  export function planTouched(s, e, call) {
374
456
  const path = e.file_path ?? e.notebook_path;
375
- if (!call?.ok || !PLAN_TOUCH.has(e.tool) || !isPlanPath(path) || (e.tool === 'Read' && e.agentId)) return null;
457
+ const kind = planKind(path);
458
+ if (!call?.ok || !PLAN_TOUCH.has(e.tool) || !kind || (e.tool === 'Read' && e.agentId)) return null;
376
459
  s.touch += 1;
377
- return { path, touch: s.touch, own: !e.agentId && e.tool !== 'Read' };
460
+ return { path, touch: s.touch, own: !e.agentId && e.tool !== 'Read', weak: kind === 'weak' };
378
461
  }
379
462
 
380
463
  function parsed(path, text) {
@@ -393,10 +476,13 @@ function current(s, path, { touch, since }) {
393
476
  return path === s.planPath && s.taken <= since;
394
477
  }
395
478
 
396
- // Whether what was read stays off the pane: no plan, or a file with no steps in place of a plan that
397
- // has some, unless the session wrote it itself (an agent's review written beside a plan, or a note the
398
- // session only read, does not push the plan away; a plan the session writes is the plan, steps or not).
399
- const keepsOff = (s, path, plan, read) => !plan || (path !== s.planPath && !plan.total && Boolean(s.plan?.total) && !read.own);
479
+ // Whether what was read stays off the pane: no plan; a review round or open questions while the pane shows another
480
+ // plan; or a file with no steps in place of a plan that has some, unless the session wrote it itself (an agent's
481
+ // review written beside a plan, or a note the session only read, does not push the plan away; a plan the session
482
+ // writes is the plan, steps or not).
483
+ const keepsOff = (s, path, plan, read) => !plan
484
+ || (path !== s.planPath && Boolean(s.plan) && read.weak === true)
485
+ || (path !== s.planPath && !plan.total && Boolean(s.plan?.total) && !read.own);
400
486
 
401
487
  // The ids in a tree, to keep the unfolded ones that are still there when a plan is read again.
402
488
  function treeIds(nodes, out = new Set()) {
@@ -442,6 +528,9 @@ export function steps(s) {
442
528
  return s.loose.calls.length ? [...items, { ...s.loose, status: looseStatus(s, items) }] : items;
443
529
  }
444
530
 
531
+ // The key of the step in progress (a task or a to-do item in progress, or the prompt that runs), or null.
532
+ const runningKey = (s) => [...steps(s)].reverse().find((t) => ['in_progress', 'running'].includes(statusOf(t)))?.key ?? null;
533
+
445
534
  function callCount(step) {
446
535
  return `도구 호출 ${step.count}회${step.failed ? `(실패 ${step.failed}회)` : ''}`;
447
536
  }
@@ -655,6 +744,7 @@ export async function runProgress($, args) {
655
744
  if (pane) await $.ui.close({ id: PANE });
656
745
  return {};
657
746
  }
747
+ ref.s.followed = null; // before the open: the engine may draw the pane before the open answers
658
748
  await showPane($, pane);
659
749
  await offerTodo($);
660
750
  return {};
@@ -676,6 +766,7 @@ export const SPEC = { name: COMMAND, description: '진행 상황 패널을 켜
676
766
  async function watchCall($, e, next) {
677
767
  const s = ref.s;
678
768
  const since = s.rev;
769
+ const order = ++s.order;
679
770
  const call = startCall(s, e, Date.now());
680
771
  let r = { isError: true };
681
772
  try {
@@ -683,9 +774,9 @@ async function watchCall($, e, next) {
683
774
  return r;
684
775
  } finally {
685
776
  endCall(call, r, Date.now());
686
- if (TASK_TOOLS.has(e.tool)) noteTaskTool(s, e, r, Date.now(), since);
777
+ if (TASK_TOOLS.has(e.tool)) noteTaskTool(s, e, r, Date.now(), since, order);
687
778
  const touched = planTouched(s, e, call);
688
- if (touched) takePlan(s, touched.path, await readText($, touched.path), { touch: touched.touch, own: touched.own });
779
+ if (touched) takePlan(s, touched.path, await readText($, touched.path), { touch: touched.touch, own: touched.own, weak: touched.weak });
689
780
  $.ui.invalidate('ui.render');
690
781
  }
691
782
  }
@@ -708,6 +799,41 @@ async function readText($, path) {
708
799
  // The session's progress, one per loaded module.
709
800
  const ref = { s: freshState() };
710
801
 
802
+ // How long a move waits, and how many times it asks. The row a change brings is drawn after the invalidate that came
803
+ // with it; a move asked before that is refused ("no element of its own is drawn under that key" in 2.1.300).
804
+ const FOLLOW_WAIT_MS = 100;
805
+ const FOLLOW_TRIES = 4;
806
+
807
+ // Brings the step in progress into the pane's window as the pane draws, when that step changed (a task started, a
808
+ // new prompt) or the pane was opened again: in a long list it can sit below the window. Only a drawn pane asks, so a
809
+ // closed one or a tab behind another asks nothing. `nearest` moves nothing while its row shows, and the person's own
810
+ // scrolling stands until the next change. An engine without the move changes nothing.
811
+ function follow($) {
812
+ const key = runningKey(ref.s);
813
+ if (!key || key === ref.s.followed) return;
814
+ ref.s.followed = key;
815
+ followSoon($, key, FOLLOW_TRIES);
816
+ }
817
+
818
+ // Asks for the move after a wait, and again while the row is not drawn yet. A later change of the step in progress,
819
+ // or any other refusal (no surface, the person moved the window meanwhile), ends it.
820
+ function followSoon($, key, tries) {
821
+ try {
822
+ $.clock.after(FOLLOW_WAIT_MS, async () => {
823
+ if (ref.s.followed !== key) return;
824
+ let answer;
825
+ try {
826
+ answer = await $.ui.scroll({ to: { key: `row-${key}` }, in: PANE, block: 'nearest' });
827
+ } catch {
828
+ return; /* an engine without $.ui.scroll */
829
+ }
830
+ if (tries > 1 && /\bdrawn\b/.test(answer?.deny ?? '')) followSoon($, key, tries - 1);
831
+ });
832
+ } catch {
833
+ /* an engine without $.clock */
834
+ }
835
+ }
836
+
711
837
  // The command and the events only this feature hooks. register.mjs hooks the events every banker
712
838
  // feature shares (the engine takes one hook per event from the module) and calls the functions
713
839
  // below for them. The fallback's name is hooked too: where mods run, `/banker:progress` toggles
@@ -735,6 +861,7 @@ export function registerProgress(on) {
735
861
  on('ui.render', { component: 'Pane' }, async ($, e, next) => {
736
862
  if (e.requestId !== PANE) return next(e);
737
863
  const redraw = () => $.ui.invalidate('ui.render');
864
+ follow($);
738
865
  const cols = Number.isFinite(e.props?.bodyColumns) ? e.props.bodyColumns : Infinity;
739
866
  const act = {
740
867
  cols,
@@ -5,9 +5,11 @@
5
5
  *
6
6
  * The engine reads the module's source before it runs it: each `on("<event>", hook)` names its
7
7
  * event as a string literal, an event without a matcher is hooked once in the whole module (its
8
- * imported files included), and `$` goes only to functions declared in the file that holds it.
9
- * So each feature (graceful-pause.mjs, progress.mjs) hooks the events only it needs, and this
10
- * file hooks the four they share: it calls the features' plain functions for them and makes the
8
+ * imported files included), and `$` goes only to functions declared in the file that holds it
9
+ * (2.1.300 also takes a function imported from the module's own files; an older engine refuses
10
+ * the whole module, so this file keeps to the older rule, and a test holds it). So each feature
11
+ * (graceful-pause.mjs, progress.mjs) hooks the events only it needs, and this file hooks the
12
+ * four they share: it calls the features' plain functions for them and makes the
11
13
  * engine calls itself, the module's command registrations among them. The engine shows a
12
14
  * `$.ui.log` line under the plugin's name, so the lines here do not repeat it.
13
15
  */
@@ -37,13 +39,18 @@ async function registerOne($, spec) {
37
39
  }
38
40
  }
39
41
 
40
- // /graceful-pause from 2.1.289 on (an older engine gets one line saying why), /progress wherever
41
- // mods run. A name the engine refuses is logged and the other still registers.
42
+ // A command spec without `immediate`, for an engine where answering at once was not seen to work.
43
+ const withoutImmediate = ({ immediate: _immediate, ...spec }) => spec;
44
+
45
+ // /graceful-pause from 2.1.289 on (an older engine gets one line saying why). /progress wherever mods run, as a
46
+ // command that answers at once (immediate) only from 2.1.289 on, where that was seen to work; below it, or with no
47
+ // version, it answers when the turn ends. A name the engine refuses is logged and the other still registers.
42
48
  async function registerCommands($) {
43
49
  const version = await engineVersion($);
44
- if (atLeast(version, MIN_ENGINE)) await registerOne($, PAUSE_SPEC);
50
+ const recent = atLeast(version, MIN_ENGINE);
51
+ if (recent) await registerOne($, PAUSE_SPEC);
45
52
  else $.ui.log(`/${PAUSE} 는 Claude Code ${MIN_ENGINE.join('.')} 이상에서만 켭니다 (이 엔진: ${version || '버전 미상'})`);
46
- await registerOne($, PROGRESS_SPEC);
53
+ await registerOne($, recent ? PROGRESS_SPEC : withoutImmediate(PROGRESS_SPEC));
47
54
  }
48
55
 
49
56
  // /progress's plan file, read again at a turn's start and end: a ralph PRD or a plan's checkboxes
@@ -158,42 +158,44 @@ function fetchSkillChangesMap(url) {
158
158
  });
159
159
  }
160
160
 
161
- async function main() {
162
- if (!countingActive()) return; // 미활성 - 무동작(로그 미변경).
163
- const url = endpoint();
164
- if (!url) return; // 엔드포인트 없음 - 무동작(로그 미변경).
165
-
166
- // 로그 없음도 유효한 체크인(설치 펄스 + 알림 fetch 겸함): 빈 counts 로 진행한다.
161
+ // 보낼 본문. PII/IP/지속ID 없음: version + os(플랫폼 계열) + counts(스킬별 x 시간대)뿐.
162
+ // 로그 없음도 유효한 체크인(설치 펄스 + 알림 fetch 겸함): 빈 counts 로 진행한다.
163
+ function checkinBody() {
167
164
  let raw = '';
168
165
  try { raw = readFileSync(USAGE_LOG_PATH, 'utf8'); } catch { raw = ''; }
166
+ return JSON.stringify({ version: installedVersion() || 'unknown', os: process.platform, counts: aggregate(raw) });
167
+ }
169
168
 
170
- // PII/IP/지속ID 없음: version + os(플랫폼 계열) + counts(스킬별 x 시간대)뿐.
171
- const payload = JSON.stringify({
172
- version: installedVersion() || 'unknown',
173
- os: process.platform,
174
- counts: aggregate(raw),
175
- });
176
- const responseBody = await post(url, payload);
177
-
178
- // 응답의 latest 로 공유 update-check 캐시를 갱신(알림 겸함). 없으면 캐시 미변경.
179
- // update-fetch 처럼 notified:null 재무장을 하지 않아도 등가다: dedupe 판정이 notified===latest
180
- // (update-notify)라 전진 버전에서 구 notified 는 항상 신 latest 와 달라 재고지가 정상 발화하고,
181
- // count-default-on 은 이 훅만·npm 배포는 update-fetch 만 써 같은 캐시를 겹쳐 쓰지 않는다.
169
+ // 응답의 latest 로 공유 update-check 캐시를 갱신(알림 겸함). 없으면 캐시 미변경.
170
+ // update-fetch 처럼 notified:null 재무장을 하지 않아도 등가다: dedupe 판정이 notified===latest
171
+ // (update-notify)라 전진 버전에서 구 notified 는 항상 신 latest 와 달라 재고지가 정상 발화하고,
172
+ // count-default-on 은 이 훅만·npm 배포는 update-fetch 만 써 같은 캐시를 겹쳐 쓰지 않는다.
173
+ async function refreshUpdateCache(responseBody) {
182
174
  const latest = extractLatest(responseBody);
183
- if (latest) {
184
- // 개인화용: 변경-스킬 매니페스트를 best-effort GET(실패 시 changedSkills 생략, 기존 값 보존).
185
- const changesUrl = process.env.BANKER_SKILL_CHANGES_URL || SKILL_CHANGES_URL; // 테스트 주입용 override.
186
- const map = await fetchSkillChangesMap(changesUrl);
187
- const patch = { latest, checkedAt: Date.now() };
188
- if (map) patch.changedSkills = changedSkillsBetween(map, installedVersion(), latest);
189
- try { writeUpdateCache(patch); } catch { /* best-effort */ }
190
- }
175
+ if (!latest) return;
176
+ // 개인화용: 변경-스킬 매니페스트를 best-effort GET(실패 시 changedSkills 생략, 기존 값 보존).
177
+ const changesUrl = process.env.BANKER_SKILL_CHANGES_URL || SKILL_CHANGES_URL; // 테스트 주입용 override.
178
+ const map = await fetchSkillChangesMap(changesUrl);
179
+ const patch = { latest, checkedAt: Date.now() };
180
+ if (map) patch.changedSkills = changedSkillsBetween(map, installedVersion(), latest);
181
+ try { writeUpdateCache(patch); } catch { /* best-effort */ }
182
+ }
191
183
 
192
- // 전송 시도 후(성공/실패 무관) 로그를 비워 24h 윈도우로 상한(배치 유실 허용).
184
+ // 전송 시도 후(성공/실패 무관) 로그를 비워 24h 윈도우로 상한(배치 유실 허용).
185
+ function resetLog() {
193
186
  try { writeFileSync(USAGE_LOG_PATH, ''); } catch { /* best-effort */ }
194
187
  try { writeFileSync(LAST_FLUSH_PATH, new Date().toISOString()); } catch { /* best-effort */ }
195
188
  }
196
189
 
190
+ async function main() {
191
+ if (!countingActive()) return; // 미활성 - 무동작(로그 미변경).
192
+ const url = endpoint();
193
+ if (!url) return; // 엔드포인트 없음 - 무동작(로그 미변경).
194
+ const responseBody = await post(url, checkinBody());
195
+ await refreshUpdateCache(responseBody);
196
+ resetLog();
197
+ }
198
+
197
199
  main()
198
200
  .catch(() => {}) // 어떤 예외도 삼킨다.
199
201
  .finally(() => process.exit(0));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kaydash9999/banker-plugins",
3
- "version": "0.17.1",
3
+ "version": "0.19.0",
4
4
  "description": "banker — Claude Code + Codex CLI plugin (QA, audit, docs, architecture, wiki skills). Install via Claude Code marketplace, or `npm i -g @kaydash9999/banker-plugins` then `banker setup`.",
5
5
  "bin": {
6
6
  "banker": "bin/banker.js"
@@ -296,7 +296,7 @@ node references/verify-intro.mjs http://localhost:<port>/ --out <projectDir>/ver
296
296
 
297
297
  - 백업을 먼저 한다. 백업은 프로젝트의 `backup/` 아래에 둔다. `/tmp` 는 쓰지 않는다.
298
298
  - `site/` 의 파일을 사이트의 정적 폴더로 옮긴다. `#world` 를 본문 위에 두고, `stepNav.end` 는 본문 시작 요소를 가리킨다.
299
- - 사이트 CSP 에 `connect-src 'self'`, `media-src 'self' blob:`, `style-src` 의 `styleHash` 를 더한다. 인라인을 허용하지 않아도 된다. `scrub-engine.LICENSE.txt`(엔진의 MIT 고지)도 함께 옮긴다.
299
+ - 사이트 CSP 에 `connect-src 'self'`, `media-src 'self' blob:`, `style-src` 의 `styleHash` 를 더한다. 인라인을 허용하지 않아도 된다. `scrub-engine.LICENSE.md`(엔진의 MIT 고지)도 함께 옮긴다.
300
300
  - 배포 뒤 운영 주소에 `verify-intro.mjs` 를 다시 돌려 결과를 대조해 보고한다.
301
301
 
302
302
  ---
@@ -8,7 +8,7 @@
8
8
  * index.html (head of index-template.html; no inline script or style)
9
9
  * scrub-engine.js (copied verbatim from references/)
10
10
  * scrub-engine.css (the CSS the engine injects, taken from scrub-engine.js unchanged)
11
- * scrub-engine.LICENSE.txt (scroll-world's MIT notice, which ships with the engine files)
11
+ * scrub-engine.LICENSE.md (scroll-world's MIT notice, which ships with the engine files)
12
12
  * theme.css (the template's --sw-* tokens, set from the manifest theme)
13
13
  * intro-fixes.css (layout, button text and overflow fixes for the engine, every page)
14
14
  * intro.js (the mountScrollWorld config and call)
@@ -231,12 +231,12 @@ const escAttr = (s) => String(s).replace(/[&<>"]/g, (c) => ({ '&': '&amp;', '<':
231
231
 
232
232
  // Heads the copied engine rules: they are scroll-world's (MIT), whose notice ships beside them.
233
233
  const ENGINE_NOTICE = '/* scroll-world (https://github.com/oso95/scroll-world), MIT License, (c) 2026 cyw.\n'
234
- + ' The rules scrub-engine.js injects, copied unchanged. Notice: scrub-engine.LICENSE.txt */\n';
234
+ + ' The rules scrub-engine.js injects, copied unchanged. Notice: scrub-engine.LICENSE.md */\n';
235
235
 
236
236
  function writeStatic(out, manifest, template) {
237
237
  const engineSource = fs.readFileSync(path.join(HERE, 'scrub-engine.js'), 'utf8');
238
238
  fs.copyFileSync(path.join(HERE, 'scrub-engine.js'), path.join(out, 'scrub-engine.js'));
239
- fs.copyFileSync(path.join(HERE, 'LICENSE'), path.join(out, 'scrub-engine.LICENSE.txt'));
239
+ fs.copyFileSync(path.join(HERE, 'LICENSE'), path.join(out, 'scrub-engine.LICENSE.md'));
240
240
  const css = engineCss(engineSource);
241
241
  fs.writeFileSync(path.join(out, 'scrub-engine.css'), ENGINE_NOTICE + css);
242
242
  const style = /<style>([\s\S]*?)<\/style>/.exec(template);
@@ -152,7 +152,7 @@ AZURE_IMAGE_API_KEY=<Image 리소스 키>
152
152
  ### 모드 (b) — 콘솔 샘플 파일
153
153
  Azure AI Foundry/AI Hub 콘솔의 "Sample code"/"View code" 패널이 주는 curl 또는 Python 스니펫에는 원문 키가 들어 있다. 사람이 이것을 **채팅에 붙이지 않고** 비공개 파일에 저장한다:
154
154
  ```bash
155
- mkdir -p ~/.config/banker/3d-intro && (umask 077 && touch ~/.config/banker/3d-intro/sample.txt) # 사람이 이 파일에 샘플을 붙여 저장
155
+ mkdir -p ~/.config/banker/3d-intro && (umask 077 && touch ~/.config/banker/3d-intro/sample.md) # 사람이 이 파일에 샘플을 붙여 저장
156
156
  ```
157
157
  에이전트는 그 파일을 읽어 키를 화면에 내지 않고 키 파일에 바로 합친 뒤 샘플 파일을 지운다. 리소스마다 마지막 인자를 `sora`, `image`, `wan:<n>` 중 하나로 준다:
158
158
  ```bash
@@ -178,8 +178,8 @@ node -e "
178
178
  const replaced = old && add[names.apiKey] && old !== add[names.apiKey] ? redact(old) : null;
179
179
  console.log(JSON.stringify({ file: target, endpoint: p.endpoint, deployment: p.deployment, apiVersion: p.apiVersion, apiKey: redact(p.apiKey), replaced }));
180
180
  })();
181
- " "$HOME/.config/banker/3d-intro/sample.txt" sora
182
- rm -f "$HOME/.config/banker/3d-intro/sample.txt"
181
+ " "$HOME/.config/banker/3d-intro/sample.md" sora
182
+ rm -f "$HOME/.config/banker/3d-intro/sample.md"
183
183
  ```
184
184
  파싱 결과(저장한 파일, 엔드포인트, 배포명, apiVersion, 길이만 보이는 key)를 사람에게 보여 주고 확인받는다.
185
185
  `replaced` 에 길이가 보이면 파일에 있던 다른 키를 이 샘플의 키로 바꾼 것이다. 교체(로테이션)가 아니었다면 사람이 키 파일의 키 줄을 다시 적는다.