@kaydash9999/banker-plugins 0.18.0 → 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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +52 -0
- package/README.md +18 -7
- package/codex/manifest.json +1 -0
- package/codex/transform-matrix.md +1 -0
- package/hooks/graceful-pause.mjs +4 -2
- package/hooks/obsidize-hook.mjs +38 -28
- package/hooks/progress-plan.mjs +14 -8
- package/hooks/progress.mjs +174 -47
- package/hooks/register.mjs +14 -7
- package/hooks/update-checkin.mjs +29 -27
- package/package.json +1 -1
- package/skills/anti-drm/SKILL.md +36 -12
- package/skills/anti-drm/scripts/anti-drm.mjs +121 -30
- package/skills/anti-drm/scripts/instruction-toggle.mjs +241 -35
- package/skills/lineage/SKILL.md +1 -1
- package/skills/lineage/lineage.py +29 -2
- package/skills/omc-patch/SKILL.md +18 -6
- package/skills/omc-patch/scripts/omc-patch.mjs +400 -87
- package/skills/theme-selection/SKILL.md +165 -0
- package/skills/theme-selection/assets/LICENSE +21 -0
- package/skills/theme-selection/assets/NOTICE.md +30 -0
- package/skills/theme-selection/assets/palettes.json +81 -0
- package/skills/theme-selection/references/contrast.md +138 -0
- package/skills/theme-selection/references/integrate.md +161 -0
- package/skills/theme-selection/scripts/themes.mjs +1067 -0
- package/skills/tone-compact/SKILL.md +8 -4
- package/skills/tone-compact/scripts/instruction-toggle.mjs +241 -35
- package/skills/tone-compact/scripts/tone-compact.mjs +2 -3
- package/skills/update-banker/SKILL.md +1 -1
|
@@ -184,7 +184,7 @@ argument-hint: "on | off | status"
|
|
|
184
184
|
- 블록이 남음: `omx setup --merge-agents`, 기본 `omx setup`(team 모드 켜짐, 모델 표만 갱신)
|
|
185
185
|
- 블록이 사라짐: `omx setup --force`, 덮어쓰기 확인, team 모드를 끈 `omx setup`. 그 뒤 `on` 다시 실행
|
|
186
186
|
- UTF-8 이 아닌 지침 파일(CP949, UTF-16): 손대지 않고 거부
|
|
187
|
-
- Codex 파일 변경 전 내용: `<파일>.tone-compact.bak` (이 계정만 읽기 가능)
|
|
187
|
+
- Codex 파일 변경 전 내용: `<파일>.tone-compact.bak` (이 계정만 읽기 가능). `off` 결과에 그 경로를 알림
|
|
188
188
|
- 같은 파일에 있는 다른 도구의 블록은 건드리지 않음
|
|
189
189
|
- `status`: 두 런타임 상태를 모두 표시
|
|
190
190
|
- 한 런타임에서 켜도 다른 런타임은 그대로. 필요하면 그쪽에서도 실행
|
|
@@ -193,11 +193,15 @@ argument-hint: "on | off | status"
|
|
|
193
193
|
|
|
194
194
|
| 메시지 | 조치 |
|
|
195
195
|
|---|---|
|
|
196
|
-
| 같은 이름의 다른 파일이 있음 | `rules/banker-tone-compact.md` 가 banker 파일이 아님.
|
|
197
|
-
| 표시의 짝이 맞지 않음 | 지침 파일에 tone-compact 표시 줄이 한쪽만 있거나 문장 속에 들어 있음. 또는 블록 안에 다른 표시 줄이 있음. 짝을 새로 맞추지
|
|
196
|
+
| 같은 이름의 다른 파일이 있음 | `rules/banker-tone-compact.md` 가 banker 파일이 아님. 지우거나 이름을 바꾸지 않음. 내용을 보이고 어떻게 할지 물은 뒤 다시 실행 |
|
|
197
|
+
| 표시의 짝이 맞지 않음 | 지침 파일에 tone-compact 표시 줄이 한쪽만 있거나 문장 속에 들어 있음. 또는 블록 안에 다른 표시 줄이 있음. 짝을 새로 맞추지 않음. 남은 표시 줄을 보이고 지울지 물은 뒤 다시 실행 |
|
|
198
198
|
| 설정 폴더가 없음 | 해당 런타임 미설치. 또는 설정 폴더 환경 변수 값 확인 |
|
|
199
199
|
| 규칙이 이 버전과 다름 | `on` 한 번 실행 |
|
|
200
200
|
| 블록이 Codex 가 읽지 않는 AGENTS.md 에 있음 | `AGENTS.override.md` 가 새로 생김. `on` 실행 시 옮김 |
|
|
201
201
|
| AGENTS.override.md 에 블록만 있어 AGENTS.md 가 가려짐 | override 의 다른 내용이 지워짐. `on` 실행 시 AGENTS.md 로 옮기고 빈 override 삭제 |
|
|
202
|
-
|
|
|
202
|
+
| 다른 banker 블록만 있어 Codex 가 AGENTS.md 를 읽지 않음 | 메시지가 말한 스킬에서 `on` 실행 |
|
|
203
|
+
| UTF-8 이 아닌 파일 | 지침 파일을 바꾸지 않음. 인코딩을 보이고, 백업을 만든 뒤 UTF-8 로 바꿀지 물은 뒤 다시 실행 |
|
|
204
|
+
| 끊어진 링크라 그대로 두었습니다 | AGENTS.md 가 가리키는 파일이 없음. 링크를 지우거나 바꾸지 않음. 링크와 대상을 보이고 어떻게 할지 물은 뒤 다시 실행 |
|
|
205
|
+
| 일반 파일이 아님 | 지침 파일 자리(AGENTS.md, AGENTS.override.md)의 폴더나 특수 파일은 지우지 않음. 무엇인지 사용자에게 보이고, 옮기거나 이름을 바꿀지 물은 뒤 다시 실행 |
|
|
206
|
+
| 다른 banker 지침 작업이 끝나지 않아 그대로 두었습니다 | 다른 banker 스킬의 `on`, `off` 가 도는 중. 잠시 뒤 다시 실행. 끝나지 않은 작업이 남긴 잠금은 1분 뒤 저절로 넘어감. 1분이 지나도 같거나 잠금 자리가 폴더면 잠금 경로를 보이고 어떻게 할지 물은 뒤 다시 실행. 잠금을 임의로 지우지 않음 |
|
|
203
207
|
| 쓸 권한이 없음 | 해당 파일과 폴더 권한 확인 |
|
|
@@ -33,17 +33,28 @@
|
|
|
33
33
|
* open and close in turn; a marker quoted inside a sentence, or an end before a
|
|
34
34
|
* start, makes turnOn and turnOff refuse rather than guess where the block ends.
|
|
35
35
|
* So does a file that is not UTF-8 (CP949, UTF-16): written back as text it
|
|
36
|
-
* would lose its bytes.
|
|
36
|
+
* would lose its bytes. A folder or FIFO at a file's name is never read: a
|
|
37
|
+
* FIFO would make the read wait forever. Nor is a socket or a link loop; and an
|
|
38
|
+
* AGENTS.md that is a link to nothing is refused by turnOn, which would
|
|
39
|
+
* otherwise create the file the link points at.
|
|
40
|
+
*
|
|
41
|
+
* Codex's files are read and written under a lock file (.banker-agents.lock in
|
|
42
|
+
* the Codex folder), so both skills turned on at once keep both blocks; a lock
|
|
43
|
+
* older than a minute is taken over.
|
|
37
44
|
*
|
|
38
45
|
* Results are { ok, message }; ok false means refused with nothing changed.
|
|
39
46
|
* Anything else that goes wrong throws.
|
|
40
47
|
*/
|
|
41
48
|
import {
|
|
42
49
|
accessSync,
|
|
50
|
+
closeSync,
|
|
43
51
|
constants,
|
|
52
|
+
fstatSync,
|
|
44
53
|
lstatSync,
|
|
54
|
+
openSync,
|
|
45
55
|
mkdirSync,
|
|
46
56
|
readFileSync,
|
|
57
|
+
realpathSync,
|
|
47
58
|
renameSync,
|
|
48
59
|
rmSync,
|
|
49
60
|
statSync,
|
|
@@ -60,8 +71,12 @@ export const KEEP_CLOSE = "<!-- USER:OMX:POLICY:END -->";
|
|
|
60
71
|
|
|
61
72
|
// The byte order mark an editor may put in front of a file.
|
|
62
73
|
const BOM = String.fromCharCode(0xfeff);
|
|
63
|
-
|
|
64
|
-
const
|
|
74
|
+
/** The text without the byte order mark an editor may put in front of it. */
|
|
75
|
+
export const noBom = (text) => (text.startsWith(BOM) ? text.slice(BOM.length) : text);
|
|
76
|
+
/** The text with CRLF line ends turned into LF. */
|
|
77
|
+
export const lf = (text) => text.replace(/\r\n/g, "\n");
|
|
78
|
+
/** How many times needle occurs in text. */
|
|
79
|
+
export const count = (text, needle) => text.split(needle).length - 1;
|
|
65
80
|
const eolOf = (text) => (text.includes("\r\n") ? "\r\n" : "\n");
|
|
66
81
|
const isDir = (path) => {
|
|
67
82
|
try {
|
|
@@ -80,27 +95,61 @@ const isLink = (path) => {
|
|
|
80
95
|
|
|
81
96
|
const UTF8 = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
|
|
82
97
|
|
|
98
|
+
// What sits at a name that is not a regular file: a folder, a FIFO or a device.
|
|
99
|
+
// It is never read; reading a FIFO waits for a writer.
|
|
100
|
+
const SPECIAL = Object.freeze({ bytes: null, text: null, special: true });
|
|
101
|
+
// What sits at a name that is a link to nothing: Codex finds no file there, and writing would follow the link.
|
|
102
|
+
const DANGLING = Object.freeze({ bytes: null, text: null, dangling: true });
|
|
103
|
+
// What opening a name fails with when no regular file can be there: links that loop (ELOOP) or a socket (ENXIO,
|
|
104
|
+
// EOPNOTSUPP). macOS answers a socket with errno 102, which Node 22 does not name ("Unknown system error -102"), so
|
|
105
|
+
// the kind of what is there decides too.
|
|
106
|
+
const NOT_A_FILE = new Set(["ELOOP", "ENXIO", "EOPNOTSUPP"]);
|
|
107
|
+
// Whether path leads to something that is there and is no regular file: a folder, a socket, a FIFO or a device.
|
|
108
|
+
const notAFile = (path) => {
|
|
109
|
+
try {
|
|
110
|
+
return !statSync(path).isFile();
|
|
111
|
+
} catch {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
// What a failed open of path means: null for nothing there, DANGLING for a link to nothing, SPECIAL for a folder,
|
|
117
|
+
// a socket or a link loop. Any other failure (permissions) is not "missing" and propagates.
|
|
118
|
+
function openFailure(path, e) {
|
|
119
|
+
if (e.code === "ENOENT" || e.code === "ENOTDIR") return isLink(path) ? DANGLING : null;
|
|
120
|
+
if (e.code === "EISDIR" || NOT_A_FILE.has(e.code) || notAFile(path)) return SPECIAL;
|
|
121
|
+
throw e;
|
|
122
|
+
}
|
|
123
|
+
|
|
83
124
|
// A file's bytes and text, null when there is no such file. text is null when
|
|
84
125
|
// the bytes are not UTF-8, which callers refuse: decoding them would replace
|
|
85
|
-
// them with U+FFFD for good.
|
|
86
|
-
//
|
|
126
|
+
// them with U+FFFD for good. A folder, a socket, a link loop or anything but a
|
|
127
|
+
// regular file is SPECIAL, and a link to nothing is DANGLING: the name is
|
|
128
|
+
// opened without blocking and not read.
|
|
87
129
|
function readFile(path) {
|
|
88
|
-
let
|
|
130
|
+
let fd;
|
|
89
131
|
try {
|
|
90
|
-
|
|
132
|
+
fd = openSync(path, constants.O_RDONLY | (constants.O_NONBLOCK ?? 0));
|
|
91
133
|
} catch (e) {
|
|
92
|
-
|
|
93
|
-
throw e;
|
|
134
|
+
return openFailure(path, e);
|
|
94
135
|
}
|
|
95
136
|
try {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
137
|
+
if (!fstatSync(fd).isFile()) return SPECIAL;
|
|
138
|
+
const bytes = readFileSync(fd);
|
|
139
|
+
try {
|
|
140
|
+
return { bytes, text: UTF8.decode(bytes) };
|
|
141
|
+
} catch {
|
|
142
|
+
return { bytes, text: null };
|
|
143
|
+
}
|
|
144
|
+
} finally {
|
|
145
|
+
closeSync(fd);
|
|
99
146
|
}
|
|
100
147
|
}
|
|
101
148
|
|
|
102
|
-
|
|
103
|
-
const
|
|
149
|
+
/** A result that did, or found already done, what was asked. */
|
|
150
|
+
export const done = (message) => ({ ok: true, message });
|
|
151
|
+
/** A result that refused and changed nothing. */
|
|
152
|
+
export const refuse = (message) => ({ ok: false, message });
|
|
104
153
|
|
|
105
154
|
/** A skill's marks: the first line of its rules file, and the lines around its Codex block. */
|
|
106
155
|
export function markersOf(slug) {
|
|
@@ -144,9 +193,14 @@ export const denied = (e) => e.code === "EACCES" || e.code === "EPERM" || e.code
|
|
|
144
193
|
* paragraph separator, private-use, unassigned and variation selector
|
|
145
194
|
* characters become "?": a path from the environment cannot start a line of
|
|
146
195
|
* its own, reorder the text around it, or hide text after a visible letter.
|
|
196
|
+
* So do letters that print as nothing (default-ignorable ones such as U+3164),
|
|
197
|
+
* the braille blank and spaces other than the ASCII one, which hide text as blanks.
|
|
147
198
|
*/
|
|
148
199
|
export const where = (value) =>
|
|
149
|
-
String(value).replace(
|
|
200
|
+
String(value).replace(
|
|
201
|
+
/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}\p{Co}\p{Cn}\p{Variation_Selector}\p{Default_Ignorable_Code_Point}\u{2800}\u{a0}\u{1680}\u{2000}-\u{200a}\u{202f}\u{205f}\u{3000}]/gu,
|
|
202
|
+
"?",
|
|
203
|
+
);
|
|
150
204
|
|
|
151
205
|
// ---- Claude Code: one rules file that is banker's alone ----
|
|
152
206
|
|
|
@@ -156,7 +210,7 @@ const claudeText = (t, rules) => `${t.header}\n${rules}\n`;
|
|
|
156
210
|
// "off", "foreign" (not banker's file), or, when rules are given, "on" or
|
|
157
211
|
// "stale" (other rules); without rules a banker file is "ours".
|
|
158
212
|
function claudeState(t, file, rules) {
|
|
159
|
-
if (file === null) return "off";
|
|
213
|
+
if (file === null || file.dangling) return "off";
|
|
160
214
|
if (file.text === null) return "foreign";
|
|
161
215
|
const body = lf(noBom(file.text));
|
|
162
216
|
if (!body.startsWith(t.ownerMark)) return "foreign";
|
|
@@ -317,25 +371,48 @@ const otherBlocks = (t, text) =>
|
|
|
317
371
|
return spans !== null && spans.length > 0;
|
|
318
372
|
});
|
|
319
373
|
|
|
374
|
+
const special = (f) => f.special === true;
|
|
320
375
|
const binary = (f) => f.bytes !== null && f.text === null;
|
|
321
376
|
const unpaired = (f) => f.spans === null;
|
|
322
377
|
const hasBlock = (f) => f.spans !== null && f.spans.length > 0;
|
|
323
378
|
const stripped = (f) => (f.text === null ? "" : strip(f.text, f.spans));
|
|
324
379
|
|
|
380
|
+
/** Whether two bigint stats name one file: one device and one inode, where the file system gives inodes at all. */
|
|
381
|
+
export const sameInode = (x, y) => x.ino !== 0n && x.dev === y.dev && x.ino === y.ino;
|
|
382
|
+
|
|
383
|
+
// Whether two paths name one file: a link (both resolve to one real path) or a hard link (one inode). A file system
|
|
384
|
+
// that gives no inode numbers (0) cannot say so for hard links, so those count as two files.
|
|
385
|
+
function sameFile(a, b) {
|
|
386
|
+
try {
|
|
387
|
+
if (realpathSync(a) === realpathSync(b)) return true;
|
|
388
|
+
return sameInode(statSync(a, { bigint: true }), statSync(b, { bigint: true }));
|
|
389
|
+
} catch {
|
|
390
|
+
return false;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
325
394
|
// Both global instruction files with their bytes and text (null when absent;
|
|
326
395
|
// text null when not UTF-8) and blocks (null when the markers do not pair up).
|
|
327
396
|
// The block's place (target) is the override when the override has text of its
|
|
328
397
|
// own, AGENTS.md otherwise. shadowers names the other banker skills whose
|
|
329
398
|
// blocks alone fill the override: Codex then reads the override, not AGENTS.md.
|
|
399
|
+
// An override that is a link or a hard link to AGENTS.md is one file with it.
|
|
330
400
|
function codexFiles(t, o) {
|
|
331
401
|
const read = (name) => {
|
|
332
402
|
const path = join(o.codexDir, name);
|
|
333
403
|
const file = readFile(path);
|
|
334
|
-
if (file === null
|
|
404
|
+
if (file === null || file.special || file.dangling) {
|
|
405
|
+
return { path, bytes: null, text: null, spans: [], special: file?.special === true, dangling: file?.dangling === true };
|
|
406
|
+
}
|
|
335
407
|
return { path, ...file, spans: file.text === null ? null : blockSpansOf(file.text, t.open, t.close) };
|
|
336
408
|
};
|
|
337
409
|
const override = read("AGENTS.override.md");
|
|
338
410
|
const main = read("AGENTS.md");
|
|
411
|
+
if (override.bytes !== null && main.bytes !== null && sameFile(override.path, main.path)) {
|
|
412
|
+
// One file under both names: Codex reads it either way, so it is AGENTS.md alone.
|
|
413
|
+
const none = { path: override.path, bytes: null, text: null, spans: [] };
|
|
414
|
+
return { target: main, other: none, all: [none, main], shadowers: [] };
|
|
415
|
+
}
|
|
339
416
|
const ownContent = binary(override) || (override.text !== null && hasContent(withoutBankerBlocks(override.text)));
|
|
340
417
|
const target = ownContent ? override : main;
|
|
341
418
|
const shadowers = ownContent || override.text === null ? [] : otherBlocks(t, override.text);
|
|
@@ -357,18 +434,26 @@ function withBlock(t, text, rules) {
|
|
|
357
434
|
return text === "" ? blockFor(t, rules, eol) : text + eol + blockFor(t, rules, eol);
|
|
358
435
|
}
|
|
359
436
|
|
|
437
|
+
// The state when the block is not in its place: "hidden" (only in an override with no text of its own),
|
|
438
|
+
// "misplaced" (only in AGENTS.md while Codex reads the override), "dangling" (nowhere, and AGENTS.md is a link to
|
|
439
|
+
// nothing that on must not write through) or "off".
|
|
440
|
+
function placeState(files) {
|
|
441
|
+
if (hasBlock(files.other)) return files.other === files.all[0] ? "hidden" : "misplaced";
|
|
442
|
+
return files.target.dangling ? "dangling" : "off";
|
|
443
|
+
}
|
|
444
|
+
|
|
360
445
|
// "off", "on", "stale", "misplaced" (only in AGENTS.md while Codex reads the
|
|
361
446
|
// override, so not applied), "hidden" (in the override with no text of its own,
|
|
362
|
-
// applied but hiding AGENTS.md), "
|
|
363
|
-
//
|
|
447
|
+
// applied but hiding AGENTS.md), "dangling" (nowhere, and AGENTS.md is a link to
|
|
448
|
+
// nothing), "unpaired", "notutf8" or "notregular" (a folder, FIFO, socket or
|
|
449
|
+
// link loop at a file's name). Without rules any block in its place counts as "on".
|
|
364
450
|
function codexState(t, files, rules) {
|
|
451
|
+
if (files.all.some(special)) return "notregular";
|
|
365
452
|
if (files.all.some(binary)) return "notutf8";
|
|
366
453
|
if (files.all.some(unpaired)) return "unpaired";
|
|
367
|
-
|
|
368
|
-
const inOther = hasBlock(files.other);
|
|
369
|
-
if (!inTarget) return inOther ? (files.other === files.all[0] ? "hidden" : "misplaced") : "off";
|
|
454
|
+
if (!hasBlock(files.target)) return placeState(files);
|
|
370
455
|
if (rules === undefined) return "on";
|
|
371
|
-
return blockCurrent(t, files.target, rules) && !
|
|
456
|
+
return blockCurrent(t, files.target, rules) && !hasBlock(files.other) ? "on" : "stale";
|
|
372
457
|
}
|
|
373
458
|
|
|
374
459
|
// Why these files cannot be written, or "" when they can. The folder counts
|
|
@@ -390,15 +475,103 @@ function writeBlocked(changes) {
|
|
|
390
475
|
// target changes, after keeping the old bytes in a .bak only this account can
|
|
391
476
|
// read (the file may be a link into a private folder). A new file gets 0644
|
|
392
477
|
// whatever the umask allows more. A file left empty is removed, unless it is a
|
|
393
|
-
// link
|
|
478
|
+
// link or has other names (a hard link), whose other names would keep the old
|
|
479
|
+
// text: it is emptied instead. Codex skips an empty file anyway.
|
|
394
480
|
function apply(t, changes) {
|
|
395
481
|
for (const { file, after } of changes) {
|
|
396
482
|
if (file.bytes !== null) writeFresh(`${file.path}.${t.slug}.bak`, file.bytes, 0o600);
|
|
397
|
-
if (after === "" && !isLink(file.path)) rmSync(file.path, { force: true });
|
|
483
|
+
if (after === "" && !isLink(file.path) && !hasOtherNames(file.path)) rmSync(file.path, { force: true });
|
|
398
484
|
else writeFileSync(file.path, after, { mode: 0o644 });
|
|
399
485
|
}
|
|
400
486
|
}
|
|
401
487
|
|
|
488
|
+
// Whether the file at path is also reachable under another name: a hard link.
|
|
489
|
+
function hasOtherNames(path) {
|
|
490
|
+
try {
|
|
491
|
+
return lstatSync(path).nlink > 1;
|
|
492
|
+
} catch {
|
|
493
|
+
return false;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// The lock both banker skills take while they read and write Codex's instructions files: two on's at once would
|
|
498
|
+
// each write the file back with only their own block. A lock is held for milliseconds; one older than a minute was
|
|
499
|
+
// left by a run that died.
|
|
500
|
+
const LOCK = ".banker-agents.lock";
|
|
501
|
+
const LOCK_STALE_MS = 60_000;
|
|
502
|
+
const LOCK_STEP_MS = 50;
|
|
503
|
+
const SLEEP = new Int32Array(new SharedArrayBuffer(4));
|
|
504
|
+
|
|
505
|
+
// Makes the lock file; false when one is there already. Anything else (permissions) throws.
|
|
506
|
+
function tryLock(path, token) {
|
|
507
|
+
try {
|
|
508
|
+
writeFileSync(path, token, { flag: "wx", mode: 0o600 });
|
|
509
|
+
return true;
|
|
510
|
+
} catch (e) {
|
|
511
|
+
if (e.code === "EEXIST") return false;
|
|
512
|
+
throw e;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Removes a lock a dead run left; true when there is none now, so the next try may take it. The name itself is
|
|
517
|
+
// looked at, not where it leads: a link to nothing or a link loop there is a lock like any other. A lock dated more
|
|
518
|
+
// than a minute away either way (a clock set back) is stale too. A folder there is none of this module's: it stays,
|
|
519
|
+
// and the wait ends in a refusal.
|
|
520
|
+
function dropStale(path) {
|
|
521
|
+
let st;
|
|
522
|
+
try {
|
|
523
|
+
st = lstatSync(path);
|
|
524
|
+
} catch (e) {
|
|
525
|
+
if (e.code === "ENOENT") return true; /* gone in between */
|
|
526
|
+
throw e;
|
|
527
|
+
}
|
|
528
|
+
if (st.isDirectory() || Math.abs(Date.now() - st.mtimeMs) <= LOCK_STALE_MS) return false;
|
|
529
|
+
rmSync(path, { force: true });
|
|
530
|
+
return true;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// Takes the lock: { path, token }, or null when another run held it for waitMs. The deadline is looked at every
|
|
534
|
+
// round, so no lock spot, whatever is there, keeps the wait going past it.
|
|
535
|
+
function takeLock(dir, waitMs) {
|
|
536
|
+
const path = join(dir, LOCK);
|
|
537
|
+
const token = `${process.pid} ${Date.now()} ${Math.random()}`;
|
|
538
|
+
const until = Date.now() + waitMs;
|
|
539
|
+
for (;;) {
|
|
540
|
+
if (tryLock(path, token)) return { path, token };
|
|
541
|
+
if (Date.now() >= until) return null;
|
|
542
|
+
if (!dropStale(path)) Atomics.wait(SLEEP, 0, 0, LOCK_STEP_MS);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// Gives the lock back while it is still this run's. The name is opened without blocking: a pipe put there is not
|
|
547
|
+
// this run's lock and stays.
|
|
548
|
+
function dropLock(lock) {
|
|
549
|
+
try {
|
|
550
|
+
if (readFile(lock.path)?.text === lock.token) rmSync(lock.path, { force: true });
|
|
551
|
+
} catch {
|
|
552
|
+
/* already gone */
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// Runs fn holding the lock; a lock another run keeps past the wait is a refusal. A folder this account cannot write
|
|
557
|
+
// takes no lock, and nothing in it can be written either: fn then only reads, and writeBlocked refuses any change,
|
|
558
|
+
// so "already on" and "already off" answer as before.
|
|
559
|
+
function locked(o, fn) {
|
|
560
|
+
let lock;
|
|
561
|
+
try {
|
|
562
|
+
lock = takeLock(o.codexDir, o.lockWaitMs ?? 5000);
|
|
563
|
+
} catch (e) {
|
|
564
|
+
if (denied(e)) return fn();
|
|
565
|
+
throw e;
|
|
566
|
+
}
|
|
567
|
+
if (!lock) return refuse(`다른 banker 지침 작업이 끝나지 않아 그대로 두었습니다: ${where(join(o.codexDir, LOCK))}`);
|
|
568
|
+
try {
|
|
569
|
+
return fn();
|
|
570
|
+
} finally {
|
|
571
|
+
dropLock(lock);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
402
575
|
const OMX_NOTE =
|
|
403
576
|
"참고: omx setup 이 AGENTS.md 를 새로 만들면(--force, 덮어쓰기 확인, team 모드를 끈 상태의 omx setup) 이 블록이 사라집니다. 그 뒤에는 on 을 다시 실행하세요. --merge-agents 와 기본 omx setup 은 블록을 남깁니다.";
|
|
404
577
|
|
|
@@ -408,35 +581,64 @@ const shadowNote = (files) =>
|
|
|
408
581
|
? ""
|
|
409
582
|
: `\n주의: AGENTS.override.md 에 다른 banker 블록(${files.shadowers.join(", ")})만 있어 Codex 가 AGENTS.md 를 읽지 않습니다. 그 스킬에서 on 을 실행해 블록을 옮기세요.`;
|
|
410
583
|
|
|
584
|
+
// The file a refused state is about: the folder or FIFO, the file that is not UTF-8, or the one whose markers do
|
|
585
|
+
// not pair up; null in any other state.
|
|
586
|
+
function problemFile(files, state) {
|
|
587
|
+
const which = { notregular: special, notutf8: binary, unpaired }[state];
|
|
588
|
+
return which ? files.all.find(which) : null;
|
|
589
|
+
}
|
|
590
|
+
|
|
411
591
|
// The refusal for files on and off must not touch, or null.
|
|
412
592
|
function codexRefusal(t, files, state) {
|
|
413
|
-
|
|
414
|
-
if (
|
|
415
|
-
|
|
593
|
+
const file = problemFile(files, state);
|
|
594
|
+
if (!file) return null;
|
|
595
|
+
const why = {
|
|
596
|
+
notregular: "일반 파일이 아니라 그대로 두었습니다",
|
|
597
|
+
notutf8: "UTF-8 이 아닌 파일이라 그대로 두었습니다",
|
|
598
|
+
unpaired: `${t.slug} 표시의 짝이 맞지 않아 그대로 두었습니다`,
|
|
599
|
+
}[state];
|
|
600
|
+
return refuse(`${why}: ${where(file.path)}`);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// What codexOn says it did. A block Codex does not read, because other banker
|
|
604
|
+
// blocks alone fill the override, says so in its first words.
|
|
605
|
+
function onWords(t, state, unread) {
|
|
606
|
+
if (state === "on") return unread ? "이미 AGENTS.md 에 있지만 지금은 적용되지 않습니다 (변경 없음)" : "이미 켜져 있습니다 (변경 없음)";
|
|
607
|
+
if (unread) return "AGENTS.md 에 넣었지만 지금은 적용되지 않습니다";
|
|
608
|
+
const moved = "블록을 맞는 파일로 옮겼습니다";
|
|
609
|
+
return { stale: t.updatedText, misplaced: moved, hidden: moved }[state] ?? "켰습니다. 다음 세션부터 자동 적용됩니다";
|
|
416
610
|
}
|
|
417
611
|
|
|
418
612
|
function codexOn(t, o, getRules) {
|
|
419
613
|
if (!isDir(o.codexDir)) return refuse(`Codex 설정 폴더가 없습니다: ${where(o.codexDir)}`);
|
|
614
|
+
return locked(o, () => codexOnHeld(t, o, getRules));
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function codexOnHeld(t, o, getRules) {
|
|
420
618
|
const rules = getRules();
|
|
421
619
|
const files = codexFiles(t, o);
|
|
422
620
|
const state = codexState(t, files, rules);
|
|
423
621
|
const refusal = codexRefusal(t, files, state);
|
|
424
622
|
if (refusal) return refusal;
|
|
623
|
+
if (files.target.dangling) return refuse(`끊어진 링크라 그대로 두었습니다: ${where(files.target.path)}`);
|
|
624
|
+
const words = onWords(t, state, files.shadowers.length > 0);
|
|
425
625
|
const tail = `${where(files.target.path)}${shadowNote(files)}`;
|
|
426
|
-
if (state === "on") return done(`Codex:
|
|
626
|
+
if (state === "on") return done(`Codex: ${words}. ${tail}`);
|
|
427
627
|
const changes = [{ file: files.target, after: withBlock(t, stripped(files.target), rules) }];
|
|
428
628
|
if (hasBlock(files.other)) changes.push({ file: files.other, after: stripped(files.other) });
|
|
429
629
|
const why = writeBlocked(changes);
|
|
430
630
|
if (why) return refuse(why);
|
|
431
631
|
apply(t, changes);
|
|
432
|
-
const moved = "블록을 맞는 파일로 옮겼습니다";
|
|
433
|
-
const what = { stale: t.updatedText, misplaced: moved, hidden: moved }[state];
|
|
434
632
|
const note = files.target === files.all[1] ? `\n${OMX_NOTE}` : "";
|
|
435
|
-
return done(`Codex: ${
|
|
633
|
+
return done(`Codex: ${words}. ${tail}${note}`);
|
|
436
634
|
}
|
|
437
635
|
|
|
438
636
|
function codexOff(t, o) {
|
|
439
637
|
if (!isDir(o.codexDir)) return done(`Codex: 설정 폴더가 없어 끌 것이 없습니다 (변경 없음). ${where(o.codexDir)}`);
|
|
638
|
+
return locked(o, () => codexOffHeld(t, o));
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function codexOffHeld(t, o) {
|
|
440
642
|
const files = codexFiles(t, o);
|
|
441
643
|
const refusal = codexRefusal(t, files, codexState(t, files));
|
|
442
644
|
if (refusal) return refusal;
|
|
@@ -445,7 +647,9 @@ function codexOff(t, o) {
|
|
|
445
647
|
const why = writeBlocked(changes);
|
|
446
648
|
if (why) return refuse(why);
|
|
447
649
|
apply(t, changes);
|
|
448
|
-
|
|
650
|
+
const paths = changes.map((c) => where(c.file.path)).join(", ");
|
|
651
|
+
const baks = changes.map((c) => where(`${c.file.path}.${t.slug}.bak`)).join(", ");
|
|
652
|
+
return done(`Codex: 껐습니다. 다음 세션부터 적용되지 않습니다. ${paths}\n지우기 전 파일: ${baks}`);
|
|
449
653
|
}
|
|
450
654
|
|
|
451
655
|
// ---- status ----
|
|
@@ -458,7 +662,9 @@ const STATE_TEXT = {
|
|
|
458
662
|
misplaced: "꺼짐, 단 블록이 Codex 가 읽지 않는 AGENTS.md 에 있음 (on 으로 옮김)",
|
|
459
663
|
hidden: "켜짐, 단 AGENTS.override.md 에 블록만 있어 AGENTS.md 가 가려짐 (on 으로 옮김)",
|
|
460
664
|
notutf8: "UTF-8 이 아닌 지침 파일이 있어 손대지 않음 (on, off 모두 거부)",
|
|
665
|
+
notregular: "일반 파일이 아닌 지침 파일이 있어 손대지 않음 (on, off 모두 거부)",
|
|
461
666
|
unpaired: "표시의 짝이 맞지 않음 (on, off 모두 거부. 남은 표시 줄 정리 필요)",
|
|
667
|
+
dangling: "꺼짐, 단 AGENTS.md 가 끊어진 링크라 on 이 거부함 (링크 확인 필요)",
|
|
462
668
|
};
|
|
463
669
|
|
|
464
670
|
// A state as the status line says it.
|
|
@@ -486,7 +692,7 @@ function codexLook(t, o, rules) {
|
|
|
486
692
|
const files = codexFiles(t, o);
|
|
487
693
|
const state = codexState(t, files, rules);
|
|
488
694
|
const shadowed = files.shadowers.length > 0 && (state === "on" || state === "stale");
|
|
489
|
-
return { state: shadowed ? "shadowed" : state, path: files.target.path, who: files.shadowers };
|
|
695
|
+
return { state: shadowed ? "shadowed" : state, path: (problemFile(files, state) ?? files.target).path, who: files.shadowers };
|
|
490
696
|
}
|
|
491
697
|
|
|
492
698
|
/** One status line per runtime: "- Claude Code: ..." and "- Codex: ...". rules may be undefined. */
|
|
@@ -507,7 +713,7 @@ export function stateLines(t, o, rules) {
|
|
|
507
713
|
*/
|
|
508
714
|
export function present(t, runtime, o) {
|
|
509
715
|
if (runtime === "claude") return isDir(o.claudeDir) && claudeState(t, readFile(claudeFile(t, o))) === "ours";
|
|
510
|
-
return isDir(o.codexDir) && !["off", "notutf8"].includes(codexState(t, codexFiles(t, o)));
|
|
716
|
+
return isDir(o.codexDir) && !["off", "notutf8", "notregular", "dangling"].includes(codexState(t, codexFiles(t, o)));
|
|
511
717
|
}
|
|
512
718
|
|
|
513
719
|
const knownRuntime = (runtime) => {
|
|
@@ -27,7 +27,9 @@ import {
|
|
|
27
27
|
KEEP_OPEN,
|
|
28
28
|
RUNTIMES,
|
|
29
29
|
blockSpansOf,
|
|
30
|
+
count,
|
|
30
31
|
hasMarker,
|
|
32
|
+
lf,
|
|
31
33
|
stateLines,
|
|
32
34
|
toggleSpec,
|
|
33
35
|
turnOff as toggleOff,
|
|
@@ -50,9 +52,6 @@ const DEFAULTS = {
|
|
|
50
52
|
skillFile: fileURLToPath(new URL("../SKILL.md", import.meta.url)),
|
|
51
53
|
};
|
|
52
54
|
|
|
53
|
-
const count = (text, needle) => text.split(needle).length - 1;
|
|
54
|
-
const lf = (text) => text.replace(/\r\n/g, "\n");
|
|
55
|
-
|
|
56
55
|
/** The rules: the text between the markers in SKILL.md, without the line breaks around it. */
|
|
57
56
|
export function rulesOf(skillText) {
|
|
58
57
|
const text = lf(skillText);
|
|
@@ -61,7 +61,7 @@ banker 는 서로 다른 메커니즘의 채널 3개로 설치되고, 그 채널
|
|
|
61
61
|
- 반대 방향의 거짓말도 하지 마라: banker 자체는 dependencies 0 이라 컴파일이 없다. **빌드 도구·툴체인 준비를 요구하는 절차를 넣으면 그건 거짓이다.**
|
|
62
62
|
|
|
63
63
|
**4. `~/.claude/plugins/cache/` 를 `rm -rf` 하지 마라.**
|
|
64
|
-
- 캐시에 구버전이 공존하는 것은 **관측 대상이지 조치 대상이 아니다.** 갱신 의미론
|
|
64
|
+
- 캐시에 구버전이 공존하는 것은 **관측 대상이지 조치 대상이 아니다.** 갱신 의미론 밖이다. Claude Code 는 교체된 판 폴더에 `.orphaned_at` 을 쓰고, 14일이 지나고 그 판을 쓰는 세션이 없으면 그 폴더를 스스로 지운다(2.1.299 에서 확인). 링크로 된 판 폴더(OMC 의 옛 판 경로 등)는 표시하지도 지우지도 않는다.
|
|
65
65
|
- 발견하면 보고만 한다. 손대지 않는다.
|
|
66
66
|
|
|
67
67
|
## 워크플로
|