@kaydash9999/banker-plugins 0.17.0 → 0.18.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 +44 -0
- package/README.md +10 -3
- package/codex/manifest.json +2 -1
- package/codex/transform-matrix.md +4 -2
- package/commands/front-qa.md +3 -3
- package/package.json +1 -1
- package/skills/3d-intro-build/SKILL.md +1 -1
- package/skills/3d-intro-build/references/assemble.mjs +3 -3
- package/skills/3d-intro-setup/SKILL.md +3 -3
- package/skills/anti-drm/SKILL.md +230 -0
- package/skills/anti-drm/scripts/anti-drm.mjs +552 -0
- package/skills/anti-drm/scripts/instruction-toggle.mjs +527 -0
- package/skills/lineage/SKILL.md +3 -3
- package/skills/lineage/lineage.py +16 -7
- package/skills/omc-patch/SKILL.md +12 -4
- package/skills/omc-patch/scripts/omc-patch.mjs +477 -225
- package/skills/omc-reference/SKILL.md +1 -1
- package/skills/ralph-qa/SKILL.md +19 -19
- package/skills/tone-compact/SKILL.md +1 -1
- package/skills/tone-compact/scripts/instruction-toggle.mjs +527 -0
- package/skills/tone-compact/scripts/tone-compact.mjs +46 -394
- package/skills/ultra-chain/SKILL.md +7 -6
- package/skills/update-banker/SKILL.md +2 -1
|
@@ -8,62 +8,41 @@
|
|
|
8
8
|
*
|
|
9
9
|
* The rules are the part of ../SKILL.md between the tone-compact:rules markers,
|
|
10
10
|
* so the text the skill applies in the session and the text later sessions load
|
|
11
|
-
* are the same.
|
|
12
|
-
*
|
|
13
|
-
* Claude Code
|
|
14
|
-
*
|
|
15
|
-
* `on` writes banker-tone-compact.md there and `off` deletes it. A file at that
|
|
16
|
-
* name that does not start with banker's mark belongs to someone else and is
|
|
17
|
-
* left alone.
|
|
18
|
-
*
|
|
19
|
-
* Codex reads $CODEX_HOME/AGENTS.override.md when it holds any non-whitespace
|
|
20
|
-
* text and $CODEX_HOME/AGENTS.md otherwise (CODEX_HOME defaults to ~/.codex).
|
|
21
|
-
* `on` adds one marked block at the end of AGENTS.md, or of the override when
|
|
22
|
-
* the override has text of its own (a block alone there would hide AGENTS.md);
|
|
23
|
-
* `off` takes it out and gives back the bytes from before. The block sits inside
|
|
24
|
-
* OMX's USER:OMX:POLICY markers, the only part of a generated AGENTS.md that
|
|
25
|
-
* `omx setup --merge-agents` carries over; a plain `omx setup` with team mode on
|
|
26
|
-
* only refreshes the model table. `omx setup --force`, confirming its overwrite
|
|
27
|
-
* prompt, or a plain `omx setup` with team mode off replaces the file block and
|
|
28
|
-
* all; `on` then has to run again. Text other tools keep in these files is left
|
|
29
|
-
* alone, and the bytes from before each change are kept in
|
|
30
|
-
* <file>.tone-compact.bak. Markers count only as whole lines that open and close
|
|
31
|
-
* in turn; a marker quoted inside a sentence, or an end before a start, makes on
|
|
32
|
-
* and off refuse rather than guess where the block ends. So does a file that is
|
|
33
|
-
* not UTF-8 (CP949, UTF-16): written back as text it would lose its bytes.
|
|
11
|
+
* are the same. Where they go, and how they come out again byte for byte, is
|
|
12
|
+
* instruction-toggle.mjs beside this file, shared with anti-drm: a rules file
|
|
13
|
+
* under Claude Code's <config>/rules/, and one marked block in the instructions
|
|
14
|
+
* file Codex reads.
|
|
34
15
|
*
|
|
35
16
|
* `off` never needs the rules, so it works even when SKILL.md cannot be read.
|
|
36
17
|
* Exit status 0 means done or already so; 1 means refused or a usage error, with
|
|
37
18
|
* nothing changed; 2 means an unexpected error stopped the command partway, so
|
|
38
19
|
* check `status`.
|
|
39
20
|
*/
|
|
40
|
-
import {
|
|
41
|
-
accessSync,
|
|
42
|
-
constants,
|
|
43
|
-
lstatSync,
|
|
44
|
-
mkdirSync,
|
|
45
|
-
readFileSync,
|
|
46
|
-
realpathSync,
|
|
47
|
-
renameSync,
|
|
48
|
-
rmSync,
|
|
49
|
-
statSync,
|
|
50
|
-
writeFileSync,
|
|
51
|
-
} from "node:fs";
|
|
21
|
+
import { readFileSync, realpathSync } from "node:fs";
|
|
52
22
|
import { homedir } from "node:os";
|
|
53
|
-
import {
|
|
23
|
+
import { join } from "node:path";
|
|
54
24
|
import { fileURLToPath } from "node:url";
|
|
25
|
+
import {
|
|
26
|
+
KEEP_CLOSE,
|
|
27
|
+
KEEP_OPEN,
|
|
28
|
+
RUNTIMES,
|
|
29
|
+
blockSpansOf,
|
|
30
|
+
hasMarker,
|
|
31
|
+
stateLines,
|
|
32
|
+
toggleSpec,
|
|
33
|
+
turnOff as toggleOff,
|
|
34
|
+
turnOn as toggleOn,
|
|
35
|
+
where,
|
|
36
|
+
} from "./instruction-toggle.mjs";
|
|
55
37
|
|
|
56
38
|
const RULES_START = "<!-- tone-compact:rules:start -->";
|
|
57
39
|
const RULES_END = "<!-- tone-compact:rules:end -->";
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const KEEP_OPEN = "<!-- USER:OMX:POLICY:START -->";
|
|
65
|
-
const KEEP_CLOSE = "<!-- USER:OMX:POLICY:END -->";
|
|
66
|
-
const MARKERS = [OPEN, CLOSE, KEEP_OPEN, KEEP_CLOSE, RULES_START, RULES_END];
|
|
40
|
+
const TOGGLE = toggleSpec({
|
|
41
|
+
slug: "tone-compact",
|
|
42
|
+
staleText: "켜짐, 단 규칙이 이 버전과 다름 (on 으로 갱신)",
|
|
43
|
+
updatedText: "규칙을 이 버전으로 바꿨습니다",
|
|
44
|
+
});
|
|
45
|
+
const MARKERS = [TOGGLE.open, TOGGLE.close, KEEP_OPEN, KEEP_CLOSE, RULES_START, RULES_END];
|
|
67
46
|
|
|
68
47
|
const DEFAULTS = {
|
|
69
48
|
claudeDir: process.env.CLAUDE_CONFIG_DIR || join(homedir(), ".claude"),
|
|
@@ -73,45 +52,6 @@ const DEFAULTS = {
|
|
|
73
52
|
|
|
74
53
|
const count = (text, needle) => text.split(needle).length - 1;
|
|
75
54
|
const lf = (text) => text.replace(/\r\n/g, "\n");
|
|
76
|
-
const eolOf = (text) => (text.includes("\r\n") ? "\r\n" : "\n");
|
|
77
|
-
const isDir = (path) => {
|
|
78
|
-
try {
|
|
79
|
-
return statSync(path).isDirectory();
|
|
80
|
-
} catch {
|
|
81
|
-
return false;
|
|
82
|
-
}
|
|
83
|
-
};
|
|
84
|
-
const isLink = (path) => {
|
|
85
|
-
try {
|
|
86
|
-
return lstatSync(path).isSymbolicLink();
|
|
87
|
-
} catch {
|
|
88
|
-
return false;
|
|
89
|
-
}
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
const UTF8 = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
|
|
93
|
-
|
|
94
|
-
// A file's bytes and text, null when there is no such file. text is null when
|
|
95
|
-
// the bytes are not UTF-8, which callers refuse: decoding them would replace
|
|
96
|
-
// them with U+FFFD for good. Any other read failure (permissions, a folder at
|
|
97
|
-
// that name) is not "missing" and propagates.
|
|
98
|
-
function readFile(path) {
|
|
99
|
-
let bytes;
|
|
100
|
-
try {
|
|
101
|
-
bytes = readFileSync(path);
|
|
102
|
-
} catch (e) {
|
|
103
|
-
if (e.code === "ENOENT" || e.code === "ENOTDIR") return null;
|
|
104
|
-
throw e;
|
|
105
|
-
}
|
|
106
|
-
try {
|
|
107
|
-
return { bytes, text: UTF8.decode(bytes) };
|
|
108
|
-
} catch {
|
|
109
|
-
return { bytes, text: null };
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
const done = (message) => ({ ok: true, message });
|
|
114
|
-
const refuse = (message) => ({ ok: false, message });
|
|
115
55
|
|
|
116
56
|
/** The rules: the text between the markers in SKILL.md, without the line breaks around it. */
|
|
117
57
|
export function rulesOf(skillText) {
|
|
@@ -122,7 +62,7 @@ export function rulesOf(skillText) {
|
|
|
122
62
|
throw new Error("SKILL.md 에서 규칙 구간(tone-compact:rules 표시)을 찾지 못했습니다");
|
|
123
63
|
}
|
|
124
64
|
const rules = text.slice(start + RULES_START.length, end).replace(/^\n+|\n+$/g, "");
|
|
125
|
-
if (rules === "" || MARKERS.some((m) => rules.includes(m))) {
|
|
65
|
+
if (rules === "" || MARKERS.some((m) => rules.includes(m)) || hasMarker(rules)) {
|
|
126
66
|
throw new Error("SKILL.md 의 규칙 구간이 비었거나 관리 표시를 담고 있습니다");
|
|
127
67
|
}
|
|
128
68
|
return rules;
|
|
@@ -130,291 +70,8 @@ export function rulesOf(skillText) {
|
|
|
130
70
|
|
|
131
71
|
const loadRules = (o) => rulesOf(readFileSync(o.skillFile, "utf8"));
|
|
132
72
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
// leaves the old file as it was.
|
|
136
|
-
function writeFresh(file, data, mode = 0o644) {
|
|
137
|
-
const tmp = `${file}.${process.pid}.tmp`;
|
|
138
|
-
rmSync(tmp, { force: true });
|
|
139
|
-
try {
|
|
140
|
-
writeFileSync(tmp, data, { flag: "wx", mode });
|
|
141
|
-
renameSync(tmp, file);
|
|
142
|
-
} catch (e) {
|
|
143
|
-
rmSync(tmp, { force: true });
|
|
144
|
-
throw e;
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const denied = (e) => e.code === "EACCES" || e.code === "EPERM" || e.code === "EROFS";
|
|
149
|
-
|
|
150
|
-
// ---- Claude Code: one rules file that is banker's alone ----
|
|
151
|
-
|
|
152
|
-
const claudeFile = (o) => join(o.claudeDir, "rules", "banker-tone-compact.md");
|
|
153
|
-
const claudeText = (rules) => `${HEADER}\n${rules}\n`;
|
|
154
|
-
|
|
155
|
-
// "off", "foreign" (not banker's file), or, when rules are given, "on" or
|
|
156
|
-
// "stale" (an earlier version's rules); without rules a banker file is "ours".
|
|
157
|
-
function claudeState(file, rules) {
|
|
158
|
-
if (file === null) return "off";
|
|
159
|
-
if (file.text === null) return "foreign";
|
|
160
|
-
const body = lf(file.text.replace(/^\uFEFF/, ""));
|
|
161
|
-
if (!body.startsWith(OWNER_MARK)) return "foreign";
|
|
162
|
-
if (rules === undefined) return "ours";
|
|
163
|
-
return body === claudeText(rules) ? "on" : "stale";
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function claudeOn(o) {
|
|
167
|
-
const file = claudeFile(o);
|
|
168
|
-
if (!isDir(o.claudeDir)) return refuse(`Claude Code 설정 폴더가 없습니다: ${o.claudeDir}`);
|
|
169
|
-
const rules = loadRules(o);
|
|
170
|
-
const state = claudeState(readFile(file), rules);
|
|
171
|
-
if (state === "foreign") return refuse(`같은 이름의 다른 파일이 있어 그대로 두었습니다: ${file}`);
|
|
172
|
-
if (state === "on") return done(`Claude Code: 이미 켜져 있습니다 (변경 없음). ${file}`);
|
|
173
|
-
try {
|
|
174
|
-
mkdirSync(dirname(file), { recursive: true, mode: 0o755 });
|
|
175
|
-
writeFresh(file, claudeText(rules));
|
|
176
|
-
} catch (e) {
|
|
177
|
-
if (denied(e)) return refuse(`쓸 권한이 없어 켜지 못했습니다 (${e.code}): ${file}`);
|
|
178
|
-
throw e;
|
|
179
|
-
}
|
|
180
|
-
return done(
|
|
181
|
-
state === "stale"
|
|
182
|
-
? `Claude Code: 규칙을 이 버전으로 바꿨습니다. ${file}`
|
|
183
|
-
: `Claude Code: 켰습니다. 다음 세션부터 자동 적용됩니다. ${file}`,
|
|
184
|
-
);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
function claudeOff(o) {
|
|
188
|
-
const file = claudeFile(o);
|
|
189
|
-
const state = claudeState(readFile(file));
|
|
190
|
-
if (state === "off") return done("Claude Code: 이미 꺼져 있습니다 (변경 없음).");
|
|
191
|
-
if (state === "foreign") return refuse(`같은 이름의 다른 파일이 있어 그대로 두었습니다: ${file}`);
|
|
192
|
-
try {
|
|
193
|
-
rmSync(file);
|
|
194
|
-
} catch (e) {
|
|
195
|
-
if (denied(e)) return refuse(`지울 권한이 없어 끄지 못했습니다 (${e.code}): ${file}`);
|
|
196
|
-
throw e;
|
|
197
|
-
}
|
|
198
|
-
return done(`Claude Code: 껐습니다. 다음 세션부터 적용되지 않습니다. ${file}`);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// ---- Codex: one block in the global instructions file Codex reads ----
|
|
202
|
-
|
|
203
|
-
const lineBare = (line) => line.replace(/\r?\n$/, "");
|
|
204
|
-
// A line without its line break, and the first one without the byte order mark
|
|
205
|
-
// an editor may put in front of it.
|
|
206
|
-
const bareAt = (lines, i) => (i === 0 ? lineBare(lines[i]).replace(/^\uFEFF/, "") : lineBare(lines[i]));
|
|
207
|
-
|
|
208
|
-
// One block's place in the text: [from, to) runs from its first line (the keep
|
|
209
|
-
// marker when one wraps it directly) through the end of its last line; cut is
|
|
210
|
-
// from minus the line break `on` puts before the block, and blankBefore says
|
|
211
|
-
// whether that break makes a blank line of its own.
|
|
212
|
-
function spanOf(lines, offsets, first, last) {
|
|
213
|
-
const wrapped =
|
|
214
|
-
first > 0 &&
|
|
215
|
-
last + 1 < lines.length &&
|
|
216
|
-
bareAt(lines, first - 1) === KEEP_OPEN &&
|
|
217
|
-
bareAt(lines, last + 1) === KEEP_CLOSE;
|
|
218
|
-
const top = wrapped ? first - 1 : first;
|
|
219
|
-
const bottom = wrapped ? last + 1 : last;
|
|
220
|
-
const before = top > 0 ? lines[top - 1] : "";
|
|
221
|
-
const joint = before.endsWith("\r\n") ? 2 : before.endsWith("\n") ? 1 : 0;
|
|
222
|
-
const blankBefore = before !== "" && lineBare(before) === "";
|
|
223
|
-
return { cut: offsets[top] - joint, from: offsets[top], to: offsets[bottom] + lines[bottom].length, blankBefore };
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* Where the tone-compact blocks sit in text, or null when the markers are not
|
|
228
|
-
* whole lines that open and close in turn. on and off refuse null, so a marker
|
|
229
|
-
* quoted inside a sentence can never make them cut the user's own text.
|
|
230
|
-
*/
|
|
231
|
-
export function blockSpans(text) {
|
|
232
|
-
const lines = text.split(/(?<=\n)/);
|
|
233
|
-
const offsets = [];
|
|
234
|
-
let at = 0;
|
|
235
|
-
for (const line of lines) {
|
|
236
|
-
offsets.push(at);
|
|
237
|
-
at += line.length;
|
|
238
|
-
}
|
|
239
|
-
const spans = [];
|
|
240
|
-
let open = -1;
|
|
241
|
-
for (let i = 0; i < lines.length; i++) {
|
|
242
|
-
const bare = bareAt(lines, i);
|
|
243
|
-
if (bare === OPEN && open === -1) open = i;
|
|
244
|
-
else if (bare === CLOSE && open !== -1) {
|
|
245
|
-
spans.push(spanOf(lines, offsets, open, i));
|
|
246
|
-
open = -1;
|
|
247
|
-
} else if (lines[i].includes(OPEN) || lines[i].includes(CLOSE)) return null;
|
|
248
|
-
}
|
|
249
|
-
return open === -1 ? spans : null;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
// The text without its blocks: exactly what it was before `on` added them. The
|
|
253
|
-
// line break `on` put in front goes with the block when the block ends the file
|
|
254
|
-
// or has a blank line of its own in front; with other text after it, the break
|
|
255
|
-
// stays, so the lines on either side never run together.
|
|
256
|
-
function strip(text, spans) {
|
|
257
|
-
let out = "";
|
|
258
|
-
let at = 0;
|
|
259
|
-
for (const s of spans) {
|
|
260
|
-
const keepBreak = s.to < text.length && !s.blankBefore;
|
|
261
|
-
out += text.slice(at, keepBreak ? s.from : s.cut);
|
|
262
|
-
at = s.to;
|
|
263
|
-
}
|
|
264
|
-
return out + text.slice(at);
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
// Whether text holds anything but whitespace, the way Codex judges a file empty
|
|
268
|
-
// (Rust's trim, which unlike JavaScript's keeps a byte order mark).
|
|
269
|
-
const hasContent = (text) => /[^\t\n\v\f\r \u0085\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000]/.test(text);
|
|
270
|
-
|
|
271
|
-
// Both global instruction files with their bytes and text (null when absent;
|
|
272
|
-
// text null when not UTF-8) and blocks (null when the markers do not pair up).
|
|
273
|
-
// The block's place (target) is the override when the override has text of its
|
|
274
|
-
// own, AGENTS.md otherwise: an override holding only the block would hide
|
|
275
|
-
// AGENTS.md from Codex, which reads any override with non-whitespace text.
|
|
276
|
-
function codexFiles(o) {
|
|
277
|
-
const read = (name) => {
|
|
278
|
-
const path = join(o.codexDir, name);
|
|
279
|
-
const file = readFile(path);
|
|
280
|
-
if (file === null) return { path, bytes: null, text: null, spans: [] };
|
|
281
|
-
return { path, ...file, spans: file.text === null ? null : blockSpans(file.text) };
|
|
282
|
-
};
|
|
283
|
-
const override = read("AGENTS.override.md");
|
|
284
|
-
const main = read("AGENTS.md");
|
|
285
|
-
const ownContent =
|
|
286
|
-
binary(override) ||
|
|
287
|
-
(override.text !== null && hasContent(override.spans === null ? override.text : strip(override.text, override.spans)));
|
|
288
|
-
const target = ownContent ? override : main;
|
|
289
|
-
return { target, other: target === override ? main : override, all: [override, main] };
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
const blockFor = (rules, eol) => [KEEP_OPEN, OPEN, ...rules.split("\n"), CLOSE, KEEP_CLOSE, ""].join(eol);
|
|
293
|
-
const binary = (f) => f.bytes !== null && f.text === null;
|
|
294
|
-
const unpaired = (f) => f.spans === null;
|
|
295
|
-
const hasBlock = (f) => f.spans !== null && f.spans.length > 0;
|
|
296
|
-
const stripped = (f) => (f.text === null ? "" : strip(f.text, f.spans));
|
|
297
|
-
// A block's text compared without line-break style, a leading byte order mark
|
|
298
|
-
// or the final line break, which editors add and drop.
|
|
299
|
-
const normBlock = (text) => lf(text).replace(/^\uFEFF/, "").replace(/\n$/, "");
|
|
300
|
-
// The block exactly as `on` writes it, and only once.
|
|
301
|
-
const blockCurrent = (f, rules) =>
|
|
302
|
-
f.spans.length === 1 && normBlock(f.text.slice(f.spans[0].from, f.spans[0].to)) === normBlock(blockFor(rules, "\n"));
|
|
303
|
-
|
|
304
|
-
// The text with the block at the end, one line break between them; an empty
|
|
305
|
-
// file gets the block alone. strip() undoes exactly this.
|
|
306
|
-
function withBlock(text, rules) {
|
|
307
|
-
const eol = eolOf(text);
|
|
308
|
-
return text === "" ? blockFor(rules, eol) : text + eol + blockFor(rules, eol);
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
// "off", "on", "stale", "misplaced" (only in AGENTS.md while Codex reads the
|
|
312
|
-
// override, so not applied), "hidden" (alone in the override, applied but hiding
|
|
313
|
-
// AGENTS.md), "unpaired" or "notutf8". Without rules any block in its place
|
|
314
|
-
// counts as "on".
|
|
315
|
-
function codexState(files, rules) {
|
|
316
|
-
if (files.all.some(binary)) return "notutf8";
|
|
317
|
-
if (files.all.some(unpaired)) return "unpaired";
|
|
318
|
-
const inTarget = hasBlock(files.target);
|
|
319
|
-
const inOther = hasBlock(files.other);
|
|
320
|
-
if (!inTarget) return inOther ? (files.other === files.all[0] ? "hidden" : "misplaced") : "off";
|
|
321
|
-
if (rules === undefined) return "on";
|
|
322
|
-
return blockCurrent(files.target, rules) && !inOther ? "on" : "stale";
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
// Why these files cannot be written, or "" when they can. The folder counts
|
|
326
|
-
// too: the .bak is written through a temp file beside the file.
|
|
327
|
-
function writeBlocked(changes) {
|
|
328
|
-
for (const { file } of changes) {
|
|
329
|
-
for (const path of file.bytes === null ? [dirname(file.path)] : [file.path, dirname(file.path)]) {
|
|
330
|
-
try {
|
|
331
|
-
accessSync(path, constants.W_OK);
|
|
332
|
-
} catch (e) {
|
|
333
|
-
return `쓸 권한이 없어 그대로 두었습니다 (${e.code || e.message}): ${path}`;
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
return "";
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
// Writes each file in place, so a symlinked AGENTS.md stays a link and its
|
|
341
|
-
// target changes, after keeping the old bytes in a .bak only this account can
|
|
342
|
-
// read (the file may be a link into a private folder). A new file gets 0644
|
|
343
|
-
// whatever the umask allows more. A file left empty is removed, unless it is a
|
|
344
|
-
// link: Codex skips an empty file anyway.
|
|
345
|
-
function apply(changes) {
|
|
346
|
-
for (const { file, after } of changes) {
|
|
347
|
-
if (file.bytes !== null) writeFresh(`${file.path}.tone-compact.bak`, file.bytes, 0o600);
|
|
348
|
-
if (after === "" && !isLink(file.path)) rmSync(file.path, { force: true });
|
|
349
|
-
else writeFileSync(file.path, after, { mode: 0o644 });
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
const OMX_NOTE =
|
|
354
|
-
"참고: omx setup 이 AGENTS.md 를 새로 만들면(--force, 덮어쓰기 확인, team 모드를 끈 상태의 omx setup) 이 블록이 사라집니다. 그 뒤에는 on 을 다시 실행하세요. --merge-agents 와 기본 omx setup 은 블록을 남깁니다.";
|
|
355
|
-
|
|
356
|
-
const notUtf8Refusal = (files) => refuse(`UTF-8 이 아닌 파일이라 그대로 두었습니다: ${files.all.find(binary).path}`);
|
|
357
|
-
|
|
358
|
-
const unpairedRefusal = (files) =>
|
|
359
|
-
refuse(`tone-compact 표시의 짝이 맞지 않아 그대로 두었습니다: ${files.all.find(unpaired).path}`);
|
|
360
|
-
|
|
361
|
-
function codexOn(o) {
|
|
362
|
-
if (!isDir(o.codexDir)) return refuse(`Codex 설정 폴더가 없습니다: ${o.codexDir}`);
|
|
363
|
-
const rules = loadRules(o);
|
|
364
|
-
const files = codexFiles(o);
|
|
365
|
-
const state = codexState(files, rules);
|
|
366
|
-
if (state === "notutf8") return notUtf8Refusal(files);
|
|
367
|
-
if (state === "unpaired") return unpairedRefusal(files);
|
|
368
|
-
if (state === "on") return done(`Codex: 이미 켜져 있습니다 (변경 없음). ${files.target.path}`);
|
|
369
|
-
const changes = [{ file: files.target, after: withBlock(stripped(files.target), rules) }];
|
|
370
|
-
if (hasBlock(files.other)) changes.push({ file: files.other, after: stripped(files.other) });
|
|
371
|
-
const why = writeBlocked(changes);
|
|
372
|
-
if (why) return refuse(why);
|
|
373
|
-
apply(changes);
|
|
374
|
-
const moved = "블록을 맞는 파일로 옮겼습니다";
|
|
375
|
-
const what = { stale: "규칙을 이 버전으로 바꿨습니다", misplaced: moved, hidden: moved }[state];
|
|
376
|
-
const note = files.target === files.all[1] ? `\n${OMX_NOTE}` : "";
|
|
377
|
-
return done(`Codex: ${what ?? "켰습니다. 다음 세션부터 자동 적용됩니다"}. ${files.target.path}${note}`);
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
function codexOff(o) {
|
|
381
|
-
if (!isDir(o.codexDir)) return done(`Codex: 설정 폴더가 없어 끌 것이 없습니다 (변경 없음). ${o.codexDir}`);
|
|
382
|
-
const files = codexFiles(o);
|
|
383
|
-
const state = codexState(files);
|
|
384
|
-
if (state === "notutf8") return notUtf8Refusal(files);
|
|
385
|
-
if (state === "unpaired") return unpairedRefusal(files);
|
|
386
|
-
const changes = files.all.filter(hasBlock).map((f) => ({ file: f, after: stripped(f) }));
|
|
387
|
-
if (changes.length === 0) return done("Codex: 이미 꺼져 있습니다 (변경 없음).");
|
|
388
|
-
const why = writeBlocked(changes);
|
|
389
|
-
if (why) return refuse(why);
|
|
390
|
-
apply(changes);
|
|
391
|
-
return done(`Codex: 껐습니다. 다음 세션부터 적용되지 않습니다. ${changes.map((c) => c.file.path).join(", ")}`);
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
// ---- status ----
|
|
395
|
-
|
|
396
|
-
const STATE_TEXT = {
|
|
397
|
-
on: "켜짐",
|
|
398
|
-
ours: "켜짐 (규칙 버전 확인 불가)",
|
|
399
|
-
stale: "켜짐, 단 규칙이 이 버전과 다름 (on 으로 갱신)",
|
|
400
|
-
off: "꺼짐",
|
|
401
|
-
foreign: "꺼짐, 같은 이름의 다른 파일이 있음 (손대지 않음)",
|
|
402
|
-
misplaced: "꺼짐, 단 블록이 Codex 가 읽지 않는 AGENTS.md 에 있음 (on 으로 옮김)",
|
|
403
|
-
hidden: "켜짐, 단 AGENTS.override.md 에 블록만 있어 AGENTS.md 가 가려짐 (on 으로 옮김)",
|
|
404
|
-
notutf8: "UTF-8 이 아닌 지침 파일이 있어 손대지 않음 (on, off 모두 거부)",
|
|
405
|
-
unpaired: "표시의 짝이 맞지 않음 (on, off 모두 거부. 남은 표시 줄 정리 필요)",
|
|
406
|
-
};
|
|
407
|
-
|
|
408
|
-
// One status line per runtime. Read failures are reported, not thrown: status
|
|
409
|
-
// is where an exit 2 sends the user to look.
|
|
410
|
-
function describe(name, look) {
|
|
411
|
-
try {
|
|
412
|
-
const { state, path } = look();
|
|
413
|
-
return `- ${name}: ${STATE_TEXT[state] ?? state} (${path})`;
|
|
414
|
-
} catch (e) {
|
|
415
|
-
return `- ${name}: 읽을 수 없음 (${e.code || e.message})`;
|
|
416
|
-
}
|
|
417
|
-
}
|
|
73
|
+
/** Where the tone-compact blocks sit in text, or null when the markers do not pair up. */
|
|
74
|
+
export const blockSpans = (text) => blockSpansOf(text, TOGGLE.open, TOGGLE.close);
|
|
418
75
|
|
|
419
76
|
export function status(opts = {}) {
|
|
420
77
|
const o = { ...DEFAULTS, ...opts };
|
|
@@ -423,29 +80,17 @@ export function status(opts = {}) {
|
|
|
423
80
|
try {
|
|
424
81
|
rules = loadRules(o);
|
|
425
82
|
} catch (e) {
|
|
426
|
-
lines.push(`- 규칙 원문을 읽지 못해 버전은 비교하지 않음 (${e.code || e.message})`);
|
|
83
|
+
lines.push(`- 규칙 원문을 읽지 못해 버전은 비교하지 않음 (${where(e.code || e.message)})`);
|
|
427
84
|
}
|
|
428
|
-
lines.push(
|
|
429
|
-
|
|
430
|
-
if (!isDir(o.claudeDir)) return { state: "설정 폴더 없음", path: o.claudeDir };
|
|
431
|
-
return { state: claudeState(readFile(claudeFile(o)), rules), path: claudeFile(o) };
|
|
432
|
-
}),
|
|
433
|
-
describe("Codex", () => {
|
|
434
|
-
if (!isDir(o.codexDir)) return { state: "설정 폴더 없음", path: o.codexDir };
|
|
435
|
-
const files = codexFiles(o);
|
|
436
|
-
return { state: codexState(files, rules), path: files.target.path };
|
|
437
|
-
}),
|
|
438
|
-
);
|
|
439
|
-
return done(lines.join("\n"));
|
|
85
|
+
lines.push(...stateLines(TOGGLE, o, rules));
|
|
86
|
+
return { ok: true, message: lines.join("\n") };
|
|
440
87
|
}
|
|
441
88
|
|
|
442
|
-
const
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
export const turnOn = (runtime, opts = {}) => ACTIONS.get(runtime).on({ ...DEFAULTS, ...opts });
|
|
448
|
-
export const turnOff = (runtime, opts = {}) => ACTIONS.get(runtime).off({ ...DEFAULTS, ...opts });
|
|
89
|
+
export const turnOn = (runtime, opts = {}) => {
|
|
90
|
+
const o = { ...DEFAULTS, ...opts };
|
|
91
|
+
return toggleOn(TOGGLE, runtime, o, () => loadRules(o));
|
|
92
|
+
};
|
|
93
|
+
export const turnOff = (runtime, opts = {}) => toggleOff(TOGGLE, runtime, { ...DEFAULTS, ...opts });
|
|
449
94
|
|
|
450
95
|
// { command, runtime, extra } from the command line; --runtime takes "x" or "=x".
|
|
451
96
|
export function parseArgs(argv) {
|
|
@@ -465,14 +110,21 @@ const USAGE = "사용법: tone-compact on | off | status [--runtime claude|codex
|
|
|
465
110
|
// The runtime on and off act on: the flag, else Claude Code when it runs this.
|
|
466
111
|
const runtimeOf = (flag, env) => flag ?? (env.CLAUDECODE === "1" ? "claude" : null);
|
|
467
112
|
|
|
113
|
+
// Why the command line cannot run as given, or "" when it can.
|
|
114
|
+
function usageError(command, flag, extra) {
|
|
115
|
+
if (extra.length > 0 || !["on", "off", "status"].includes(command)) return USAGE;
|
|
116
|
+
return flag !== null && !RUNTIMES.includes(flag) ? USAGE : "";
|
|
117
|
+
}
|
|
118
|
+
|
|
468
119
|
function main(argv, env = process.env) {
|
|
469
120
|
const { command, runtime: flag, extra } = parseArgs(argv);
|
|
470
|
-
|
|
471
|
-
|
|
121
|
+
const why = usageError(command, flag, extra);
|
|
122
|
+
if (why) {
|
|
123
|
+
process.stdout.write(`${why}\n`);
|
|
472
124
|
return 1;
|
|
473
125
|
}
|
|
474
126
|
const runtime = runtimeOf(flag, env);
|
|
475
|
-
if (command !== "status" && !
|
|
127
|
+
if (command !== "status" && !RUNTIMES.includes(runtime)) {
|
|
476
128
|
process.stdout.write(`런타임을 정할 수 없습니다. --runtime claude 또는 --runtime codex 를 붙이세요.\n${USAGE}\n`);
|
|
477
129
|
return 1;
|
|
478
130
|
}
|
|
@@ -481,7 +133,7 @@ function main(argv, env = process.env) {
|
|
|
481
133
|
process.stdout.write(`${message}\n`);
|
|
482
134
|
return ok ? 0 : 1;
|
|
483
135
|
} catch (e) {
|
|
484
|
-
process.stdout.write(`예기치 못한 오류로 중단했습니다: ${e.message}\nstatus 로 상태를 확인하세요.\n`);
|
|
136
|
+
process.stdout.write(`예기치 못한 오류로 중단했습니다: ${where(e.message)}\nstatus 로 상태를 확인하세요.\n`);
|
|
485
137
|
return 2;
|
|
486
138
|
}
|
|
487
139
|
}
|
|
@@ -283,7 +283,7 @@ ralplan 은 계획 작성자 외에 Architect 와 Critic 두 검토자로 합의
|
|
|
283
283
|
|
|
284
284
|
## 5. 구현
|
|
285
285
|
|
|
286
|
-
1. 기준선: 바꾸기 전에 시험 명령을 한 번 돌려 이미 실패하는 항목을 `<run>/05-baseline.
|
|
286
|
+
1. 기준선: 바꾸기 전에 시험 명령을 한 번 돌려 이미 실패하는 항목을 `<run>/05-baseline.md` 에 남긴다. 0단계에서 기존 변경을 두고 진행했으면 지금 트리를 기준선으로 쓴다.
|
|
287
287
|
- 깨끗한 사본이 필요하면 저장소 밖 임시 폴더에 `git worktree add --detach <임시> HEAD` 로 만든다. 의존성을 링크하거나 설치한 뒤 돌리고, 명령이 실제로 돌았는지 확인한다. "명령 없음" 오류뿐인 기록은 기준선이 아니다. 끝나면 `git worktree remove --force <임시>` 로 지운다.
|
|
288
288
|
- `git stash`, `git reset` 은 쓰지 않는다.
|
|
289
289
|
2. Claude Code: ralph 를 `--critic=critic` 으로 부른다. 인자에 아래를 넘긴다.
|
|
@@ -322,7 +322,7 @@ ralph-qa 의 검토자는 시험을 직접 돌리지 않는다. 연쇄가 이
|
|
|
322
322
|
1. 이번 변경의 새 파일을 스테이징한다(`git add -- <파일>...`, 커밋은 하지 않음). 박스로 보내는 트리는 추적 파일만 담는다. 스테이징할 목록을 먼저 보인다.
|
|
323
323
|
- `.omc/`, `.omx/` 아래, `.env` 류, 키와 자격 증명 파일은 넣지 않는다.
|
|
324
324
|
- 넣을 텍스트 파일은 `<scan>` 으로 검사한다. 걸린 파일은 넣지 않고 사용자에게 알린다. 그 파일이 필요한 시험은 박스에서 미검증이다.
|
|
325
|
-
2. 이 머신에서 시험, 빌드, 린트를 돌려 원문 출력을 `<run>/qa/local-r<k>.
|
|
325
|
+
2. 이 머신에서 시험, 빌드, 린트를 돌려 원문 출력을 `<run>/qa/local-r<k>.md` 에 남긴다. 결과를 `tests` 에 기록한다(`where: "local"`, `os`, `status`, 셀 수 있으면 `tests`, `pass`, `fail`, `skipped`, `log: "qa/local-r<k>.md"`).
|
|
326
326
|
3. 박스에서 같은 시험을 돌린다. 고른 박스마다 `--box <이름>` 을 붙인다. 오래 도는 명령으로 돌리고, 끝난 뒤에 기록한다.
|
|
327
327
|
|
|
328
328
|
```bash
|
|
@@ -341,7 +341,7 @@ ralph-qa 의 검토자는 시험을 직접 돌리지 않는다. 연쇄가 이
|
|
|
341
341
|
- 한 OS 에서만 실패하면 원인을 나눈다. 제품 결함은 고치고, 시험이 실행 환경을 잘못 가정한 것도 고친다.
|
|
342
342
|
- `--boxes=none` 이면 이 3을 건너뛴다.
|
|
343
343
|
4. ralph-qa 를 `--lens=both --agents=<agents> --max=<max>` 로 부른다. 외부 좌석은 3단계에서 고른 모델을 값으로 넘긴다.
|
|
344
|
-
- 입력: 요청 원문, `02-spec.md` 의 수용 기준과 사용자 답변 원문, `03-plan.md`, 승인된 목업과 기대 출력, 2의 원문 출력, 3의 박스 JSON 과 실패한 박스의 로그 발췌, `05-baseline.
|
|
344
|
+
- 입력: 요청 원문, `02-spec.md` 의 수용 기준과 사용자 답변 원문, `03-plan.md`, 승인된 목업과 기대 출력, 2의 원문 출력, 3의 박스 JSON 과 실패한 박스의 로그 발췌, `05-baseline.md`. diff 의 기준은 `<base>` 다
|
|
345
345
|
- 3단계와 같이 모든 좌석의 지시에 적대적 검토 문장을 넣는다
|
|
346
346
|
- 회차마다 `note` 로 기록하고, 끝나면 보고 전문을 `<run>/qa/ralph-qa-r<k>.md` 로 저장한다.
|
|
347
347
|
- `ITERATE` 면 고친 뒤 1부터 3까지 다시 하고(새 `<k>`), 같은 실행에서 다시 판정받는다. 끈끈한 반대는 그 검토자의 재판정으로만 풀린다.
|
|
@@ -355,7 +355,7 @@ ralph-qa 의 검토자는 시험을 직접 돌리지 않는다. 연쇄가 이
|
|
|
355
355
|
- 임시 데이터 폴더나 임시 HOME 에서 만든다. 사용자의 실제 데이터에 쓰지 않는다.
|
|
356
356
|
- 색을 끈다(`NO_COLOR=1`). 종료 코드도 대조 대상이다.
|
|
357
357
|
- `mock/expected/` 와 대조한다. JSON 은 파싱해 깊게 비교하고, 텍스트는 바이트로 비교한다(줄 끝 포함). `.ignore.json` 에 적힌 칸은 뺀다.
|
|
358
|
-
- 결과를 보기마다 `tests` 하나로 `where: "compare:<보기>"` 에 기록한다. `tests` 는 그 보기에서 대조한 파일 수, `pass` 는 같은 파일 수, `fail` 은 다른 파일 수다. `log` 는 파일마다 `same <이름>` 이나 `DIFF <이름>` 한 줄을 남긴 대조 출력(`visual/compare-r<k>.
|
|
358
|
+
- 결과를 보기마다 `tests` 하나로 `where: "compare:<보기>"` 에 기록한다. `tests` 는 그 보기에서 대조한 파일 수, `pass` 는 같은 파일 수, `fail` 은 다른 파일 수다. `log` 는 파일마다 `same <이름>` 이나 `DIFF <이름>` 한 줄을 남긴 대조 출력(`visual/compare-r<k>.md`, `<k>` 는 마지막 `qa` 회차)이다. 로그만 보고도 수를 다시 셀 수 있어야 한다.
|
|
359
359
|
2. 캡처: 목업 보기와 실제 결과를 같은 크기로 찍는다. CLI 와 라이브러리는 실제 출력을 escape 해 목업과 같은 HTML 틀에 담아 `<run>/actual/<view>.html` 로 만든 뒤 찍는다. 앱 서버가 필요하면 시험 데이터와 임시 HOME 으로(실제 `.env`, 실제 DB 없이) 오래 도는 명령으로 띄우고, 캡처가 끝나면 그 작업을 멈춘다.
|
|
360
360
|
|
|
361
361
|
```bash
|
|
@@ -429,9 +429,10 @@ node "<S>/references/chain-report.mjs" "<run>"
|
|
|
429
429
|
5. 회차는 `maxRound` 다음 번호로 이어 센다. 앞 회차 파일을 덮어쓰지 않는다. `--max` 는 남은 횟수만 쓴다.
|
|
430
430
|
6. 판정이 없는 회차(그 번호의 시험이나 박스 기록은 있는데 같은 번호의 판정 기록이 없음)는 새 번호로, 지금 트리로 시험부터 다시 한다.
|
|
431
431
|
7. 5단계에서 이을 때
|
|
432
|
-
- `05-baseline.
|
|
432
|
+
- `05-baseline.md` 는 다시 쓰지 않는다. 이미 바뀐 트리로 쓰면 이번 변경이 만든 실패가 기존 실패로 바뀐다.
|
|
433
|
+
- 이 판보다 먼저 시작한 실행 폴더에는 기준선이 `05-baseline.txt` 로 있다. `05-baseline.md` 가 없으면 그 파일을 기준선으로 쓴다.
|
|
433
434
|
- 같은 세션에서 ralph 가 아직 켜져 있으면 다시 부르지 않고 끝나기를 기다린다.
|
|
434
|
-
- 새 세션에서 ralph 를 다시 부르면 ralph 는 지금 트리로 기준선을 다시 기록한다. 이 사실을 `build` 의 `note` 에 적고, 새 실패의 판정은 `05-baseline.
|
|
435
|
+
- 새 세션에서 ralph 를 다시 부르면 ralph 는 지금 트리로 기준선을 다시 기록한다. 이 사실을 `build` 의 `note` 에 적고, 새 실패의 판정은 `05-baseline.md` 기준으로 한다.
|
|
435
436
|
|
|
436
437
|
## 플래그
|
|
437
438
|
|
|
@@ -116,7 +116,8 @@ PATH 의 banker : 0.2.0 (구버전)
|
|
|
116
116
|
claude plugin update banker@banker-plugins
|
|
117
117
|
```
|
|
118
118
|
|
|
119
|
-
- 옵션은 `-s/--scope`(`user`|`project`|`local`|`managed`, 기본 `
|
|
119
|
+
- Claude Code 2.1.296 의 옵션은 `-s/--scope`(`user`|`project`|`local`|`managed`, 기본 auto-detect), `--json`(사람용 문구 대신 기계용 결과 한 줄), `-y/--yes`, `--accept-command <sha256>`, `-h/--help` 다. help 문구는 "Update a plugin to the latest version (restart required to apply)".
|
|
120
|
+
- `-y` 와 `--accept-command` 는 마켓플레이스가 선언한 설치 명령(command 원천이나 headersHelper)을 승인할 때만 쓴다. banker 마켓플레이스는 원천이 저장소 폴더(`"source": "."`)라 선언한 명령이 없어 둘 다 필요 없다.
|
|
120
121
|
- **옵션은 문서가 아니라 live `--help` 가 정본이다.** 공식문서의 옵션표는 실제 CLI 보다 뒤처져 있다(`plugin install` 의 `--config` 누락 확인). 옵션을 쓰기 전에 `claude plugin update --help` 로 확인하라.
|
|
121
122
|
- `<plugin>` 은 bare name(`banker`) 또는 `plugin@marketplace`(`banker@banker-plugins`) 둘 다 받는다. 마켓플레이스가 여럿이면 후자로 특정한다.
|
|
122
123
|
- **반영에는 재시작이 필요하다**(help 가 명시). 재시작 전 관측이 옛 값인 것은 실패가 아니다.
|