@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +72 -0
- package/README.md +26 -8
- package/codex/manifest.json +3 -1
- package/codex/transform-matrix.md +5 -2
- package/commands/front-qa.md +3 -3
- 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/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 +254 -0
- package/skills/anti-drm/scripts/anti-drm.mjs +643 -0
- package/skills/anti-drm/scripts/instruction-toggle.mjs +733 -0
- package/skills/lineage/SKILL.md +3 -3
- package/skills/lineage/lineage.py +43 -7
- package/skills/omc-patch/SKILL.md +18 -6
- package/skills/omc-patch/scripts/omc-patch.mjs +400 -87
- package/skills/omc-reference/SKILL.md +1 -1
- package/skills/ralph-qa/SKILL.md +19 -19
- 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 +733 -0
- package/skills/tone-compact/scripts/tone-compact.mjs +48 -397
- package/skills/ultra-chain/SKILL.md +7 -6
- package/skills/update-banker/SKILL.md +1 -1
|
@@ -8,62 +8,43 @@
|
|
|
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
|
+
count,
|
|
31
|
+
hasMarker,
|
|
32
|
+
lf,
|
|
33
|
+
stateLines,
|
|
34
|
+
toggleSpec,
|
|
35
|
+
turnOff as toggleOff,
|
|
36
|
+
turnOn as toggleOn,
|
|
37
|
+
where,
|
|
38
|
+
} from "./instruction-toggle.mjs";
|
|
55
39
|
|
|
56
40
|
const RULES_START = "<!-- tone-compact:rules:start -->";
|
|
57
41
|
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];
|
|
42
|
+
const TOGGLE = toggleSpec({
|
|
43
|
+
slug: "tone-compact",
|
|
44
|
+
staleText: "켜짐, 단 규칙이 이 버전과 다름 (on 으로 갱신)",
|
|
45
|
+
updatedText: "규칙을 이 버전으로 바꿨습니다",
|
|
46
|
+
});
|
|
47
|
+
const MARKERS = [TOGGLE.open, TOGGLE.close, KEEP_OPEN, KEEP_CLOSE, RULES_START, RULES_END];
|
|
67
48
|
|
|
68
49
|
const DEFAULTS = {
|
|
69
50
|
claudeDir: process.env.CLAUDE_CONFIG_DIR || join(homedir(), ".claude"),
|
|
@@ -71,48 +52,6 @@ const DEFAULTS = {
|
|
|
71
52
|
skillFile: fileURLToPath(new URL("../SKILL.md", import.meta.url)),
|
|
72
53
|
};
|
|
73
54
|
|
|
74
|
-
const count = (text, needle) => text.split(needle).length - 1;
|
|
75
|
-
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
|
-
|
|
116
55
|
/** The rules: the text between the markers in SKILL.md, without the line breaks around it. */
|
|
117
56
|
export function rulesOf(skillText) {
|
|
118
57
|
const text = lf(skillText);
|
|
@@ -122,7 +61,7 @@ export function rulesOf(skillText) {
|
|
|
122
61
|
throw new Error("SKILL.md 에서 규칙 구간(tone-compact:rules 표시)을 찾지 못했습니다");
|
|
123
62
|
}
|
|
124
63
|
const rules = text.slice(start + RULES_START.length, end).replace(/^\n+|\n+$/g, "");
|
|
125
|
-
if (rules === "" || MARKERS.some((m) => rules.includes(m))) {
|
|
64
|
+
if (rules === "" || MARKERS.some((m) => rules.includes(m)) || hasMarker(rules)) {
|
|
126
65
|
throw new Error("SKILL.md 의 규칙 구간이 비었거나 관리 표시를 담고 있습니다");
|
|
127
66
|
}
|
|
128
67
|
return rules;
|
|
@@ -130,291 +69,8 @@ export function rulesOf(skillText) {
|
|
|
130
69
|
|
|
131
70
|
const loadRules = (o) => rulesOf(readFileSync(o.skillFile, "utf8"));
|
|
132
71
|
|
|
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
|
-
}
|
|
72
|
+
/** Where the tone-compact blocks sit in text, or null when the markers do not pair up. */
|
|
73
|
+
export const blockSpans = (text) => blockSpansOf(text, TOGGLE.open, TOGGLE.close);
|
|
418
74
|
|
|
419
75
|
export function status(opts = {}) {
|
|
420
76
|
const o = { ...DEFAULTS, ...opts };
|
|
@@ -423,29 +79,17 @@ export function status(opts = {}) {
|
|
|
423
79
|
try {
|
|
424
80
|
rules = loadRules(o);
|
|
425
81
|
} catch (e) {
|
|
426
|
-
lines.push(`- 규칙 원문을 읽지 못해 버전은 비교하지 않음 (${e.code || e.message})`);
|
|
82
|
+
lines.push(`- 규칙 원문을 읽지 못해 버전은 비교하지 않음 (${where(e.code || e.message)})`);
|
|
427
83
|
}
|
|
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"));
|
|
84
|
+
lines.push(...stateLines(TOGGLE, o, rules));
|
|
85
|
+
return { ok: true, message: lines.join("\n") };
|
|
440
86
|
}
|
|
441
87
|
|
|
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 });
|
|
88
|
+
export const turnOn = (runtime, opts = {}) => {
|
|
89
|
+
const o = { ...DEFAULTS, ...opts };
|
|
90
|
+
return toggleOn(TOGGLE, runtime, o, () => loadRules(o));
|
|
91
|
+
};
|
|
92
|
+
export const turnOff = (runtime, opts = {}) => toggleOff(TOGGLE, runtime, { ...DEFAULTS, ...opts });
|
|
449
93
|
|
|
450
94
|
// { command, runtime, extra } from the command line; --runtime takes "x" or "=x".
|
|
451
95
|
export function parseArgs(argv) {
|
|
@@ -465,14 +109,21 @@ const USAGE = "사용법: tone-compact on | off | status [--runtime claude|codex
|
|
|
465
109
|
// The runtime on and off act on: the flag, else Claude Code when it runs this.
|
|
466
110
|
const runtimeOf = (flag, env) => flag ?? (env.CLAUDECODE === "1" ? "claude" : null);
|
|
467
111
|
|
|
112
|
+
// Why the command line cannot run as given, or "" when it can.
|
|
113
|
+
function usageError(command, flag, extra) {
|
|
114
|
+
if (extra.length > 0 || !["on", "off", "status"].includes(command)) return USAGE;
|
|
115
|
+
return flag !== null && !RUNTIMES.includes(flag) ? USAGE : "";
|
|
116
|
+
}
|
|
117
|
+
|
|
468
118
|
function main(argv, env = process.env) {
|
|
469
119
|
const { command, runtime: flag, extra } = parseArgs(argv);
|
|
470
|
-
|
|
471
|
-
|
|
120
|
+
const why = usageError(command, flag, extra);
|
|
121
|
+
if (why) {
|
|
122
|
+
process.stdout.write(`${why}\n`);
|
|
472
123
|
return 1;
|
|
473
124
|
}
|
|
474
125
|
const runtime = runtimeOf(flag, env);
|
|
475
|
-
if (command !== "status" && !
|
|
126
|
+
if (command !== "status" && !RUNTIMES.includes(runtime)) {
|
|
476
127
|
process.stdout.write(`런타임을 정할 수 없습니다. --runtime claude 또는 --runtime codex 를 붙이세요.\n${USAGE}\n`);
|
|
477
128
|
return 1;
|
|
478
129
|
}
|
|
@@ -481,7 +132,7 @@ function main(argv, env = process.env) {
|
|
|
481
132
|
process.stdout.write(`${message}\n`);
|
|
482
133
|
return ok ? 0 : 1;
|
|
483
134
|
} catch (e) {
|
|
484
|
-
process.stdout.write(`예기치 못한 오류로 중단했습니다: ${e.message}\nstatus 로 상태를 확인하세요.\n`);
|
|
135
|
+
process.stdout.write(`예기치 못한 오류로 중단했습니다: ${where(e.message)}\nstatus 로 상태를 확인하세요.\n`);
|
|
485
136
|
return 2;
|
|
486
137
|
}
|
|
487
138
|
}
|
|
@@ -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
|
|
|
@@ -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
|
## 워크플로
|