@kaydash9999/banker-plugins 0.16.1 → 0.16.3
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-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +24 -0
- package/README.md +4 -2
- package/hooks/progress-plan.mjs +328 -0
- package/hooks/progress.mjs +224 -32
- package/hooks/register.mjs +18 -1
- package/package.json +1 -1
- package/skills/3d-intro-build/SKILL.md +4 -3
- package/skills/3d-intro-build/references/azure-adapter.mjs +83 -28
- package/skills/3d-intro-build/references/verify-intro.mjs +27 -15
- package/skills/3d-intro-build/references/verify.md +1 -1
- package/skills/3d-intro-setup/SKILL.md +13 -8
- package/skills/3d-intro-setup/references/azure-adapter.mjs +83 -28
package/hooks/progress.mjs
CHANGED
|
@@ -9,11 +9,20 @@
|
|
|
9
9
|
* a call shows a short description under the list; a press on a step's arrow folds or unfolds its
|
|
10
10
|
* calls.
|
|
11
11
|
*
|
|
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), so there prompts are the steps. Above them the pane
|
|
14
|
+
* shows the plan file the session touched last (progress-plan.mjs reads it), as steps of its own.
|
|
15
|
+
* A prompt's harness tags leave its title, and a background task's notice (or a teammate's message)
|
|
16
|
+
* goes on with the last prompt's step instead of becoming one: the step runs again while it does,
|
|
17
|
+
* and keeps its own end and time.
|
|
18
|
+
*
|
|
12
19
|
* `/progress` and `/progress show` toggle the pane, `on` and `off` set it. The command is
|
|
13
20
|
* registered `immediate`, so it answers while a turn runs. An engine without mods never loads
|
|
14
21
|
* this file: commands/progress.md and hooks/progress-fallback.mjs answer there instead.
|
|
15
22
|
*/
|
|
16
23
|
|
|
24
|
+
import { cells, clip, fit, isPlanPath, parsePlan, pick, readPrompt } from './progress-plan.mjs';
|
|
25
|
+
|
|
17
26
|
const COMMAND = 'progress';
|
|
18
27
|
const PANE = 'banker-progress';
|
|
19
28
|
const TITLE = '진행 상황';
|
|
@@ -22,6 +31,9 @@ const HINT = '단계나 하위 항목을 누르면 설명이 여기에 나옵니
|
|
|
22
31
|
const MAX_STEPS = 50;
|
|
23
32
|
const MAX_KEPT_CALLS = 200;
|
|
24
33
|
const MAX_SHOWN_CALLS = 30;
|
|
34
|
+
const MAX_SHOWN_PLAN = 20;
|
|
35
|
+
// The calls whose file, when it is a plan file, becomes the plan the pane shows.
|
|
36
|
+
const PLAN_TOUCH = new Set(['Write', 'Edit', 'MultiEdit', 'Read', 'NotebookEdit']);
|
|
25
37
|
// The id of the step made for work already under way when the module loaded mid-turn.
|
|
26
38
|
const AT_HAND = 'now';
|
|
27
39
|
// Bookkeeping calls: they become steps, not calls under a step.
|
|
@@ -30,6 +42,13 @@ const STATUS = {
|
|
|
30
42
|
running: '진행 중', in_progress: '진행 중', pending: '대기', done: '완료', completed: '완료',
|
|
31
43
|
stopped: '중단됨', refused: '거절됨', failed: '오류로 끝남',
|
|
32
44
|
};
|
|
45
|
+
// A plan step's state, as progress-plan.mjs names it.
|
|
46
|
+
const PLAN_STATUS = { completed: '완료', in_progress: '진행 중', pending: '대기', failed: '실패', blocked: '막힘' };
|
|
47
|
+
// A notice's row, by the status it came with.
|
|
48
|
+
const NOTICE = {
|
|
49
|
+
completed: '백그라운드 작업 끝남', failed: '백그라운드 작업 실패', stopped: '백그라운드 작업 중지됨',
|
|
50
|
+
killed: '백그라운드 작업 중지됨', message: '팀원 메시지',
|
|
51
|
+
};
|
|
33
52
|
// How a prompt's turn ended (turn.complete's reason), as its step's state.
|
|
34
53
|
const ENDED = { answer: 'done', aborted: 'stopped', refusal: 'refused', error: 'failed' };
|
|
35
54
|
// Why a hook failed (the kind of the engine's HookFailure), for a failure that brings no message.
|
|
@@ -48,15 +67,12 @@ const tally = () => ({ calls: [], count: 0, failed: 0 });
|
|
|
48
67
|
const looseStep = () => ({ key: 'loose', title: '작업 목록 밖의 작업', ...tally() });
|
|
49
68
|
|
|
50
69
|
// `rev` counts the task changes (TaskCreate, TaskUpdate) taken in, to tell a stale TaskList or TaskGet.
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
//
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const chars = [...s];
|
|
58
|
-
return chars.length > n ? `${chars.slice(0, n - 1).join('')}…` : s;
|
|
59
|
-
};
|
|
70
|
+
// `plan` is the plan last read from `planPath`, `planShut` whether its steps are folded. `touch` counts
|
|
71
|
+
// the calls that touched a plan file and `taken` is the touch the plan came from, to tell a read that
|
|
72
|
+
// a newer one overtook.
|
|
73
|
+
export const freshState = () => ({ turns: [], tasks: new Map(), todos: null, loose: looseStep(), open: new Set(), selected: null, seq: 0, rev: 0,
|
|
74
|
+
plan: null, planPath: null, planShut: false, touch: 0, taken: 0 });
|
|
75
|
+
|
|
60
76
|
const tail = (path) => String(path).split(/[\\/]/).filter(Boolean).slice(-2).join('/');
|
|
61
77
|
const seconds = (ms) => {
|
|
62
78
|
const all = Math.max(0, Math.round(ms / 1000));
|
|
@@ -72,14 +88,39 @@ export function callLabel(e) {
|
|
|
72
88
|
|
|
73
89
|
// ── what the module learns from the session ──────────────────────────────────
|
|
74
90
|
|
|
75
|
-
// A prompt as a step. A prompt whose end never came is done where the next one starts
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
91
|
+
// A prompt as a step. A prompt whose end never came is done where the next one starts, and so are
|
|
92
|
+
// the notices still running on a step. `notices` holds the turns of the notices running on it.
|
|
93
|
+
function addTurn(s, turnId, text, now, title) {
|
|
94
|
+
for (const t of s.turns) {
|
|
95
|
+
if (t.status === 'running') Object.assign(t, { status: 'done', endedAt: now });
|
|
96
|
+
t.notices.clear();
|
|
97
|
+
}
|
|
98
|
+
s.turns.push({ key: `t-${turnId}`, id: turnId, title: title ?? (clip(text, 60) || '(이어서 진행)'), text,
|
|
99
|
+
startedAt: now, endedAt: null, status: 'running', notices: new Set(), ...tally() });
|
|
80
100
|
if (s.turns.length > MAX_STEPS) s.turns.shift();
|
|
81
101
|
}
|
|
82
102
|
|
|
103
|
+
// A step's state on the pane: a prompt's step runs again while a notice runs on it.
|
|
104
|
+
const statusOf = (step) => (step.notices?.size ? 'running' : step.status);
|
|
105
|
+
|
|
106
|
+
// A row under a step that is no tool call: it counts as none. `ok` false shows it failed.
|
|
107
|
+
function note(s, step, label, ok, now) {
|
|
108
|
+
step.calls.push({ key: `c-${++s.seq}`, tool: '', label: clip(label, 60), detail: clip(label, 240), isNote: true,
|
|
109
|
+
startedAt: now, endedAt: now, ok, steps: [step] });
|
|
110
|
+
if (step.calls.length > MAX_KEPT_CALLS) step.calls.shift();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// A background task's end (or a teammate's message) opens a turn of its own: it runs on the last
|
|
114
|
+
// prompt's step, under which a row says so, and the list stays as it is. Before any prompt it is a
|
|
115
|
+
// step of its own.
|
|
116
|
+
function noticeTurn(s, e, said, now) {
|
|
117
|
+
const word = pick(NOTICE, said.status) ?? '백그라운드 작업 알림';
|
|
118
|
+
if (!s.turns.length) addTurn(s, e.turnId, '', now, said.status === 'message' ? word : '백그라운드 작업 알림');
|
|
119
|
+
const t = s.turns.at(-1);
|
|
120
|
+
if (t.id !== e.turnId) t.notices.add(e.turnId);
|
|
121
|
+
if (said.text) note(s, t, `${word}: ${said.text}`, said.status !== 'failed', now);
|
|
122
|
+
}
|
|
123
|
+
|
|
83
124
|
// The task list's items, which are the steps while there are any: TodoWrite's, else the Tasks list's.
|
|
84
125
|
function listItems(s) {
|
|
85
126
|
return s.todos?.length ? s.todos : [...s.tasks.values()];
|
|
@@ -97,14 +138,22 @@ function dropFinished(s) {
|
|
|
97
138
|
}
|
|
98
139
|
|
|
99
140
|
export function startTurn(s, e, now) {
|
|
141
|
+
const said = readPrompt(e.text);
|
|
142
|
+
if (said.kind === 'notice') return noticeTurn(s, e, said, now);
|
|
100
143
|
dropFinished(s);
|
|
101
|
-
addTurn(s, e, now);
|
|
144
|
+
addTurn(s, e.turnId, said.text, now);
|
|
102
145
|
}
|
|
103
146
|
|
|
104
|
-
// A main turn's end; a subagent's (it carries an agentId) ends no step
|
|
105
|
-
// saw start (it loaded mid-turn) ends the step for
|
|
147
|
+
// A main turn's end; a subagent's (it carries an agentId) ends no step, and a notice's leaves its
|
|
148
|
+
// step's own end as it was. A turn the module never saw start (it loaded mid-turn) ends the step for
|
|
149
|
+
// the work at hand.
|
|
106
150
|
export function endTurn(s, e, now) {
|
|
107
151
|
if (e.agentId) return;
|
|
152
|
+
const host = s.turns.find((x) => x.notices.has(e.turnId));
|
|
153
|
+
if (host) {
|
|
154
|
+
host.notices.delete(e.turnId);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
108
157
|
const t = s.turns.find((x) => x.id === e.turnId) ?? s.turns.find((x) => x.id === AT_HAND && x.status === 'running');
|
|
109
158
|
if (!t) return;
|
|
110
159
|
t.status = ENDED[e.reason] ?? 'failed';
|
|
@@ -118,7 +167,7 @@ function owners(s, now) {
|
|
|
118
167
|
const items = listItems(s);
|
|
119
168
|
const task = [...items].reverse().find((t) => t.status === 'in_progress');
|
|
120
169
|
const first = task ?? (items.length ? s.loose : null);
|
|
121
|
-
if (!s.turns.length) addTurn(s,
|
|
170
|
+
if (!s.turns.length) addTurn(s, AT_HAND, '지금 하는 작업', now);
|
|
122
171
|
return first ? [first, s.turns.at(-1)] : [s.turns.at(-1)];
|
|
123
172
|
}
|
|
124
173
|
|
|
@@ -249,6 +298,50 @@ export function noteTaskTool(s, e, r, now, since) {
|
|
|
249
298
|
if (CHANGES.has(e.tool)) s.rev += 1;
|
|
250
299
|
}
|
|
251
300
|
|
|
301
|
+
// The plan file a call touched, with the touch's count, when it is one and the call went through:
|
|
302
|
+
// a write by anyone, a read by the session itself (an agent reviewing a plan does not make it the plan).
|
|
303
|
+
export function planTouched(s, e, call) {
|
|
304
|
+
const path = e.file_path ?? e.notebook_path;
|
|
305
|
+
if (!call?.ok || !PLAN_TOUCH.has(e.tool) || !isPlanPath(path) || (e.tool === 'Read' && e.agentId)) return null;
|
|
306
|
+
s.touch += 1;
|
|
307
|
+
return { path, touch: s.touch };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function parsed(path, text) {
|
|
311
|
+
try {
|
|
312
|
+
return typeof text === 'string' ? parsePlan(path, text) : null;
|
|
313
|
+
} catch {
|
|
314
|
+
return null; /* a plan the parser trips on leaves the pane as it was */
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Whether a read may change the plan: a read for a touch newer than the one the plan came from, or a
|
|
319
|
+
// read again of the plan's own file (begun when `since` touches were counted) that no touch taken
|
|
320
|
+
// in the meantime overtook.
|
|
321
|
+
function current(s, path, { touch, since }) {
|
|
322
|
+
if (touch !== undefined) return touch > s.taken;
|
|
323
|
+
return path === s.planPath && s.taken <= since;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const isPlanKey = (key) => key === 'plan' || /^p-\d+$/.test(key ?? '');
|
|
327
|
+
|
|
328
|
+
// Whether what was read stays off the pane: no plan, or a file with no steps in place of a plan that
|
|
329
|
+
// has some (a review written beside it).
|
|
330
|
+
const keepsOff = (s, path, plan) => !plan || (path !== s.planPath && !plan.total && Boolean(s.plan?.total));
|
|
331
|
+
|
|
332
|
+
// What a read of a plan file found (`read` is `{ touch }` for a touch's read, `{ since }` for a read
|
|
333
|
+
// again). A file that cannot be read or is no plan leaves the plan as it was.
|
|
334
|
+
export function takePlan(s, path, text, read = {}) {
|
|
335
|
+
if (!current(s, path, read)) return;
|
|
336
|
+
const plan = parsed(path, text);
|
|
337
|
+
if (keepsOff(s, path, plan)) return;
|
|
338
|
+
const moving = path !== s.planPath;
|
|
339
|
+
if (read.touch !== undefined) s.taken = read.touch;
|
|
340
|
+
if (moving && isPlanKey(s.selected)) s.selected = null;
|
|
341
|
+
s.planPath = path;
|
|
342
|
+
s.plan = { ...plan, path, steps: plan.steps.map((st, i) => ({ ...st, key: `p-${i}`, planStep: true })) };
|
|
343
|
+
}
|
|
344
|
+
|
|
252
345
|
// ── what the pane shows ──────────────────────────────────────────────────────
|
|
253
346
|
|
|
254
347
|
// Nothing seen yet: one step that says so, with no state word beside it.
|
|
@@ -257,7 +350,8 @@ const IDLE = { key: 'idle', title: '진행 중인 작업 없음', status: null,
|
|
|
257
350
|
// The calls outside the list are in progress while calls still go there: no task is in progress
|
|
258
351
|
// and the prompt runs.
|
|
259
352
|
function looseStatus(s, items) {
|
|
260
|
-
const
|
|
353
|
+
const last = s.turns.at(-1);
|
|
354
|
+
const going = !items.some((t) => t.status === 'in_progress') && last !== undefined && statusOf(last) === 'running';
|
|
261
355
|
return going ? 'running' : 'done';
|
|
262
356
|
}
|
|
263
357
|
|
|
@@ -272,9 +366,10 @@ function callCount(step) {
|
|
|
272
366
|
}
|
|
273
367
|
|
|
274
368
|
function stepState(step, now) {
|
|
275
|
-
const
|
|
369
|
+
const status = statusOf(step);
|
|
370
|
+
const doing = status === 'in_progress' && step.activeForm ? `(${clip(step.activeForm, 40)})` : '';
|
|
276
371
|
const time = Number.isFinite(step.startedAt) ? `, ${seconds((step.endedAt ?? now) - step.startedAt)}` : '';
|
|
277
|
-
return `${STATUS[
|
|
372
|
+
return `${STATUS[status] ?? status}${doing}${time}`;
|
|
278
373
|
}
|
|
279
374
|
|
|
280
375
|
function stepWords(step, now) {
|
|
@@ -285,54 +380,127 @@ function stepWords(step, now) {
|
|
|
285
380
|
}
|
|
286
381
|
|
|
287
382
|
function callWords(call, now) {
|
|
383
|
+
if (call.isNote) return `${call.detail.replace(/[.!?。]+$/, '')}.`;
|
|
288
384
|
const state = call.ok === null ? '실행 중' : call.ok ? '성공' : '실패';
|
|
289
385
|
const who = call.isAgent ? '하위 에이전트의 ' : '';
|
|
290
386
|
return `${who}${call.tool}${call.detail ? `: ${call.detail}` : ''}. ${state}, ${seconds((call.endedAt ?? now) - call.startedAt)}.`;
|
|
291
387
|
}
|
|
292
388
|
|
|
293
|
-
|
|
389
|
+
function planWords(plan) {
|
|
390
|
+
const where = `계획 파일: ${clip(plan.path.split(/[\\/]/).filter(Boolean).slice(-3).join('/'), 120)}`;
|
|
391
|
+
const cut = plan.cut ? ' 파일이 길어 앞부분만 읽었습니다.' : '';
|
|
392
|
+
if (!plan.total) return `${where}. 단계를 찾지 못했습니다.${cut}`;
|
|
393
|
+
const done = plan.hasStates ? `, 완료 ${plan.done}개` : '';
|
|
394
|
+
return `${where}. 단계 ${plan.total}개${done}.${cut}`;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function planStepWords(st) {
|
|
398
|
+
const what = st.text.replace(/[.!?。]+$/, '');
|
|
399
|
+
return st.status ? `${what}. ${PLAN_STATUS[st.status]}.` : `${what}.`;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// What the person pressed last, while it is still on the pane: the plan or one of its steps, a
|
|
403
|
+
// step, or a call under one.
|
|
294
404
|
function pressed(s) {
|
|
405
|
+
if (s.plan && s.selected === 'plan') return { key: 'plan', isPlan: true };
|
|
406
|
+
const plan = s.plan?.steps.find((st) => st.key === s.selected);
|
|
407
|
+
if (plan) return plan;
|
|
295
408
|
const all = steps(s);
|
|
296
409
|
return all.find((x) => x.key === s.selected) ?? all.flatMap((x) => x.calls).find((c) => c.key === s.selected);
|
|
297
410
|
}
|
|
298
411
|
|
|
412
|
+
function itemWords(s, item, now) {
|
|
413
|
+
if (item.isPlan) return planWords(s.plan);
|
|
414
|
+
if (item.planStep) return planStepWords(item);
|
|
415
|
+
return item.tool !== undefined ? callWords(item, now) : stepWords(item, now);
|
|
416
|
+
}
|
|
417
|
+
|
|
299
418
|
// The description under the list: the pressed step's or call's, or how to get one.
|
|
300
419
|
export function describe(s, now) {
|
|
301
420
|
const item = pressed(s);
|
|
302
|
-
if (item) return
|
|
421
|
+
if (item) return itemWords(s, item, now);
|
|
303
422
|
const all = steps(s);
|
|
304
423
|
return all.length === 1 && all[0].key === 'idle' ? stepWords(IDLE, now) : HINT;
|
|
305
424
|
}
|
|
306
425
|
|
|
426
|
+
// A label cut so that it, the cells before it and the words after it fit on one row of the pane.
|
|
427
|
+
const room = (label, act, before, after) => fit(label, (act.cols ?? Infinity) - before - (after ? cells(after) + 1 : 0));
|
|
428
|
+
|
|
307
429
|
function callRows(ui, step, act) {
|
|
308
430
|
const { Box, Text, Button } = ui;
|
|
309
431
|
const shown = step.calls.slice(-MAX_SHOWN_CALLS);
|
|
310
|
-
const rows = shown.map((c) =>
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
432
|
+
const rows = shown.map((c) => {
|
|
433
|
+
const state = c.ok === null ? '실행 중' : c.ok ? '' : '실패';
|
|
434
|
+
return Box({ key: `row-${c.key}`, flexDirection: 'row', columnGap: 1, paddingLeft: 4, children: [
|
|
435
|
+
Button({ key: c.key, label: room(c.label, act, 4, state), plain: true, dimColor: c.ok !== false, onPress: () => act.select(c.key) }),
|
|
436
|
+
...stateWord(ui, state, false),
|
|
437
|
+
] });
|
|
438
|
+
});
|
|
314
439
|
const hidden = step.calls.length - shown.length;
|
|
315
440
|
return hidden ? [Box({ paddingLeft: 4, children: [Text({ children: [`앞의 ${hidden}개는 줄였습니다`], dimColor: true })] }), ...rows] : rows;
|
|
316
441
|
}
|
|
317
442
|
|
|
443
|
+
// A row's state word, bold while it runs; none where the state is empty, so no gap is kept for it.
|
|
444
|
+
function stateWord(ui, state, running) {
|
|
445
|
+
return state ? [ui.Text({ children: [state], dimColor: !running, bold: running })] : [];
|
|
446
|
+
}
|
|
447
|
+
|
|
318
448
|
function stepRow(ui, s, step, act) {
|
|
319
449
|
const { Box, Text, Button } = ui;
|
|
320
450
|
const isOpen = s.open.has(step.key);
|
|
321
451
|
const arrow = step.calls.length
|
|
322
452
|
? Button({ key: `arrow-${step.key}`, label: isOpen ? '▾' : '▸', plain: true, onPress: () => act.fold(step.key) })
|
|
323
453
|
: Text({ children: [' '] });
|
|
324
|
-
const
|
|
454
|
+
const status = statusOf(step);
|
|
455
|
+
const state = STATUS[status] ?? status ?? '';
|
|
325
456
|
return Box({ key: `row-${step.key}`, flexDirection: 'row', columnGap: 1, children: [
|
|
326
457
|
arrow,
|
|
327
|
-
Button({ key: step.key, label: step.title, plain: true, onPress: () => act.select(step.key) }),
|
|
328
|
-
|
|
458
|
+
Button({ key: step.key, label: room(step.title, act, 2, state), plain: true, onPress: () => act.select(step.key) }),
|
|
459
|
+
...stateWord(ui, state, status === 'running' || status === 'in_progress'),
|
|
460
|
+
] });
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// The plan's row: its title in bold (a Button draws its Text children in place of its label) and
|
|
464
|
+
// how far along it is.
|
|
465
|
+
function planHead(ui, s, act) {
|
|
466
|
+
const { Box, Text, Button } = ui;
|
|
467
|
+
const { plan } = s;
|
|
468
|
+
const count = !plan.total ? '단계 없음' : plan.hasStates ? `${plan.done}/${plan.total} 완료` : `${plan.total}단계`;
|
|
469
|
+
const arrow = plan.steps.length
|
|
470
|
+
? Button({ key: 'arrow-plan', label: s.planShut ? '▸' : '▾', plain: true, onPress: () => act.foldPlan() })
|
|
471
|
+
: Text({ children: [' '] });
|
|
472
|
+
const label = room(`계획: ${plan.title}`, act, 2, count);
|
|
473
|
+
return Box({ key: 'row-plan', flexDirection: 'row', columnGap: 1, children: [
|
|
474
|
+
arrow,
|
|
475
|
+
Button({ key: 'plan', label, plain: true, onPress: () => act.select('plan'), children: [Text({ bold: true, children: [label] })] }),
|
|
476
|
+
Text({ children: [count], dimColor: true }),
|
|
329
477
|
] });
|
|
330
478
|
}
|
|
331
479
|
|
|
332
|
-
// The
|
|
480
|
+
// The plan's rows: its title, then its first steps unless folded, then a line for the plan's rest.
|
|
481
|
+
function planRows(ui, s, act) {
|
|
482
|
+
const { Box, Text, Button } = ui;
|
|
483
|
+
if (!s.plan) return [];
|
|
484
|
+
const rows = [planHead(ui, s, act)];
|
|
485
|
+
if (s.planShut) return [...rows, Box({ key: 'plan-gap', marginBottom: 1 })];
|
|
486
|
+
const shown = s.plan.steps.slice(0, MAX_SHOWN_PLAN);
|
|
487
|
+
for (const st of shown) {
|
|
488
|
+
const state = PLAN_STATUS[st.status] ?? '';
|
|
489
|
+
rows.push(Box({ key: `row-${st.key}`, flexDirection: 'row', columnGap: 1, paddingLeft: 2, children: [
|
|
490
|
+
Button({ key: st.key, label: room(st.title, act, 2, state), plain: true, onPress: () => act.select(st.key) }),
|
|
491
|
+
...stateWord(ui, state, st.status === 'in_progress'),
|
|
492
|
+
] }));
|
|
493
|
+
}
|
|
494
|
+
const rest = s.plan.total - shown.length;
|
|
495
|
+
if (rest > 0) rows.push(Box({ key: 'plan-more', paddingLeft: 2, children: [Text({ children: [`외 ${rest}개`], dimColor: true })] }));
|
|
496
|
+
return [...rows, Box({ key: 'plan-gap', marginBottom: 1 })];
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// The pane's tree. `ui` is the surface's element table; `act` holds what a press does and `cols`,
|
|
500
|
+
// the cells across the pane's body (Infinity where the engine gives none).
|
|
333
501
|
export function draw(ui, s, now, act) {
|
|
334
502
|
const { Box, Text } = ui;
|
|
335
|
-
const rows =
|
|
503
|
+
const rows = planRows(ui, s, act);
|
|
336
504
|
for (const step of steps(s)) {
|
|
337
505
|
rows.push(stepRow(ui, s, step, act));
|
|
338
506
|
if (s.open.has(step.key)) rows.push(...callRows(ui, step, act));
|
|
@@ -385,10 +553,21 @@ async function watchCall($, e, next) {
|
|
|
385
553
|
} finally {
|
|
386
554
|
endCall(call, r, Date.now());
|
|
387
555
|
if (TASK_TOOLS.has(e.tool)) noteTaskTool(s, e, r, Date.now(), since);
|
|
556
|
+
const touched = planTouched(s, e, call);
|
|
557
|
+
if (touched) takePlan(s, touched.path, await readText($, touched.path), { touch: touched.touch });
|
|
388
558
|
$.ui.invalidate('ui.render');
|
|
389
559
|
}
|
|
390
560
|
}
|
|
391
561
|
|
|
562
|
+
// A file's text, or null where it cannot be read (missing, over the engine's 4 MiB, refused).
|
|
563
|
+
async function readText($, path) {
|
|
564
|
+
try {
|
|
565
|
+
return await $.fs.read(path);
|
|
566
|
+
} catch {
|
|
567
|
+
return null;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
392
571
|
// The session's progress, one per loaded module.
|
|
393
572
|
const ref = { s: freshState() };
|
|
394
573
|
|
|
@@ -405,9 +584,12 @@ export function registerProgress(on) {
|
|
|
405
584
|
on('ui.render', { component: 'Pane' }, async ($, e, next) => {
|
|
406
585
|
if (e.requestId !== PANE) return next(e);
|
|
407
586
|
const redraw = () => $.ui.invalidate('ui.render');
|
|
587
|
+
const cols = Number.isFinite(e.props?.bodyColumns) ? e.props.bodyColumns : Infinity;
|
|
408
588
|
const act = {
|
|
589
|
+
cols,
|
|
409
590
|
select: (key) => { ref.s.selected = ref.s.selected === key ? null : key; redraw(); },
|
|
410
591
|
fold: (key) => { if (!ref.s.open.delete(key)) ref.s.open.add(key); redraw(); },
|
|
592
|
+
foldPlan: () => { ref.s.planShut = !ref.s.planShut; redraw(); },
|
|
411
593
|
};
|
|
412
594
|
return draw($.ui.resolve(e), ref.s, Date.now(), act);
|
|
413
595
|
});
|
|
@@ -425,3 +607,13 @@ export function progressTurnStarted(e) {
|
|
|
425
607
|
export function progressTurnCompleted(e) {
|
|
426
608
|
endTurn(ref.s, e, Date.now());
|
|
427
609
|
}
|
|
610
|
+
|
|
611
|
+
// The plan file to read again as a turn starts and ends (register.mjs reads it), with the touches
|
|
612
|
+
// counted when the read begins; null without a plan.
|
|
613
|
+
export function progressPlanTarget() {
|
|
614
|
+
return ref.s.planPath ? { path: ref.s.planPath, since: ref.s.touch } : null;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
export function progressPlanRead(target, text) {
|
|
618
|
+
takePlan(ref.s, target.path, text, { since: target.since });
|
|
619
|
+
}
|
package/hooks/register.mjs
CHANGED
|
@@ -16,7 +16,8 @@ import {
|
|
|
16
16
|
registerGracefulPause,
|
|
17
17
|
} from './graceful-pause.mjs';
|
|
18
18
|
import {
|
|
19
|
-
SPEC as PROGRESS_SPEC, progressReset, progressTurnCompleted, progressTurnStarted,
|
|
19
|
+
SPEC as PROGRESS_SPEC, progressPlanRead, progressPlanTarget, progressReset, progressTurnCompleted, progressTurnStarted,
|
|
20
|
+
registerProgress,
|
|
20
21
|
} from './progress.mjs';
|
|
21
22
|
|
|
22
23
|
async function engineVersion($) {
|
|
@@ -45,6 +46,20 @@ async function registerCommands($) {
|
|
|
45
46
|
await registerOne($, PROGRESS_SPEC);
|
|
46
47
|
}
|
|
47
48
|
|
|
49
|
+
// /progress's plan file, read again at a turn's start and end: a ralph PRD or a plan's checkboxes
|
|
50
|
+
// change without a call this module sees. A read that fails keeps the plan as it was.
|
|
51
|
+
async function rereadPlan($) {
|
|
52
|
+
const target = progressPlanTarget();
|
|
53
|
+
if (!target) return;
|
|
54
|
+
let text = null;
|
|
55
|
+
try {
|
|
56
|
+
text = await $.fs.read(target.path);
|
|
57
|
+
} catch {
|
|
58
|
+
/* missing or refused: the pane keeps the plan it read last */
|
|
59
|
+
}
|
|
60
|
+
progressPlanRead(target, text);
|
|
61
|
+
}
|
|
62
|
+
|
|
48
63
|
// A /graceful-pause request that came after its turn's last model request: a note voids it.
|
|
49
64
|
async function voidLateRequest($, late) {
|
|
50
65
|
try {
|
|
@@ -75,6 +90,7 @@ export const register = (on) => {
|
|
|
75
90
|
on('turn.start', async ($, e, next) => {
|
|
76
91
|
pauseTurnStarted(e);
|
|
77
92
|
progressTurnStarted(e);
|
|
93
|
+
await rereadPlan($);
|
|
78
94
|
$.ui.invalidate('ui.render');
|
|
79
95
|
return next(e);
|
|
80
96
|
});
|
|
@@ -83,6 +99,7 @@ export const register = (on) => {
|
|
|
83
99
|
const late = pauseTurnCompleted(e);
|
|
84
100
|
if (late) await voidLateRequest($, late);
|
|
85
101
|
progressTurnCompleted(e);
|
|
102
|
+
await rereadPlan($);
|
|
86
103
|
$.ui.invalidate('ui.render');
|
|
87
104
|
return next(e);
|
|
88
105
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kaydash9999/banker-plugins",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.3",
|
|
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"
|
|
@@ -50,11 +50,12 @@ description: "스크롤 위치에 맞춰 3D 플라이스루 영상이 움직이
|
|
|
50
50
|
```js
|
|
51
51
|
import { resolveCreds, resolveFfmpeg } from './references/azure-adapter.mjs';
|
|
52
52
|
const creds = resolveCreds({ projectDir }); // _source === null 이면 미설정
|
|
53
|
-
//
|
|
54
|
-
if (creds.
|
|
53
|
+
// ffmpeg 경로는 resolveFfmpeg 가 셸의 FFMPEG_PATH 와 홈의 설정 파일, 키 파일에서만 찾는다. 프로젝트 키 파일의 값은 쓰지 않는다.
|
|
54
|
+
if (creds._ignored) console.log('프로젝트 키 파일에서 쓰지 않는 값:', Object.keys(creds._ignored).join(', ')); // 값은 출력하지 않는다
|
|
55
55
|
```
|
|
56
56
|
|
|
57
57
|
- `creds._source` 가 `null` 이면 크리덴셜이 없다. `3d-intro-setup` 으로 보낸다.
|
|
58
|
+
- `creds._ignored` 가 있으면 프로젝트 키 파일의 `FFMPEG_PATH` 를 쓰지 않은 것이다. 저장소와 함께 온 파일이 실행할 프로그램을 정하지 못하게 막은 것이다. 값은 출력하지 않으니 사람이 파일을 열어 확인하고, 그 경로가 필요하면 `3d-intro-setup` §2 로 홈 설정 파일에 남긴다.
|
|
58
59
|
Claude Code: `/banker:setup` 에서 3d-intro-setup / Codex: `banker-3d-intro-setup`.
|
|
59
60
|
- `resolveFfmpeg()` 가 throw 하면 ffmpeg 가 없다. 같은 `3d-intro-setup` 으로 보낸다.
|
|
60
61
|
- 둘 다 통과해야 다음 단계로 간다.
|
|
@@ -64,7 +65,7 @@ if (creds.FFMPEG_PATH && !process.env.FFMPEG_PATH) process.env.FFMPEG_PATH = cre
|
|
|
64
65
|
`malformed` 는 endpoint 가 https URL 이 아니거나(http 는 이 기기의 localhost 만 허용) 모델이 한 id 가 아닌 항목이다(`reason` 이 `endpoint` 또는 `model`). 영상 단계는 이 항목을 건너뛰고, 모두 `malformed` 면 영상 단계에서 멈춘다. 그러니 유료 스틸을 만들기 전에 사용자에게 알리고 `3d-intro-setup` 으로 고친다.
|
|
65
66
|
Azure 호출도 같다. 키는 https 로만 보내고, `http://` endpoint 는 보내기 전에 막는다(Sora 영상 단계는 `invalid_request`, 이미지 생성 같은 그 밖의 호출은 `HTTP 0 (EINSECURE)` 오류).
|
|
66
67
|
WAN 항목(`WAN_<n>_ENDPOINT`, `WAN_<n>_API_KEY`)이 하나도 없으면 영상은 처음부터 Sora 로 만든다. 이때 정지와 비행 모드(Step 4B)는 쓸 수 없다.
|
|
67
|
-
- 독립 검증(Step 6)에는 Node 용 playwright 패키지와 Chromium 이 필요하다. `npx playwright install chromium`(`setup-playwright`)은 브라우저만 받으므로, 패키지도 `npm i -g playwright`
|
|
68
|
+
- 독립 검증(Step 6)에는 Node 용 playwright 패키지와 Chromium 이 필요하다. `npx playwright install chromium`(`setup-playwright`)은 브라우저만 받으므로, 패키지도 `npm i -g playwright` 로 설치해 둔다. 사용자가 자기 프로젝트에 `npm i -D playwright` 로 설치했다고 확인해 준 때만 Step 6 에 `--playwright-from-project` 를 준다. 프로젝트에 `node_modules/playwright` 가 있다는 이유만으로는 주지 않는다. 이 옵션이 없으면 프로젝트의 `node_modules` 는 읽지 않는다(저장소가 함께 가져온 코드가 실행되지 않게).
|
|
68
69
|
|
|
69
70
|
---
|
|
70
71
|
|