@kaydash9999/banker-plugins 0.25.0 → 0.26.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 +29 -0
- package/README.md +4 -3
- package/codex/manifest.json +1 -0
- package/codex/transform-matrix.md +1 -0
- package/package.json +1 -1
- package/skills/3d-intro-build/SKILL.md +1 -0
- package/skills/3d-intro-build/references/azure-adapter.mjs +54 -27
- package/skills/3d-intro-build/references/video-pool.mjs +11 -6
- package/skills/3d-intro-setup/references/azure-adapter.mjs +54 -27
- package/skills/3d-intro-setup/references/video-pool.mjs +11 -6
- package/skills/make-evidence/SKILL.md +182 -0
- package/skills/motion-sites/SKILL.md +9 -1
- package/skills/motion-sites/scripts/motion-sites.mjs +20 -3
- package/skills/omc-patch/scripts/omc-patch.mjs +74 -16
- package/skills/ralph-qa/SKILL.md +1 -0
- package/skills/ralph-qa/references/seat-run.mjs +21 -4
- package/skills/ralph-qa/references/verifier-probe.mjs +35 -13
- package/skills/remains/SKILL.md +1 -1
- package/skills/remains/scripts/boxes.mjs +31 -12
|
@@ -188,6 +188,8 @@ export const DECISION_REASONS = [
|
|
|
188
188
|
|
|
189
189
|
export const CLI_DEFAULT = "@cli-default";
|
|
190
190
|
|
|
191
|
+
// How a CLI call ended, for a note.
|
|
192
|
+
const callWhy = (res) => (res?.why ? res.why : res?.status === 0 ? "exit 0 with output the probe could not use" : `exit ${res?.status ?? "?"}`);
|
|
191
193
|
const firstLine = (res) => (res?.status === 0 ? String(res.stdout).trim().split("\n")[0] || null : null);
|
|
192
194
|
const absent = (cli) => ({ cli, present: false, decision: "unseated", reason: "cli-absent" });
|
|
193
195
|
const adopt = (seat, model, reason) => ({ ...seat, decision: "adopt", model, reason });
|
|
@@ -382,6 +384,8 @@ const rawMcpNames = (lines) => lines.filter((l) => !/^\s*#/.test(l))
|
|
|
382
384
|
// or the line that opened what is still open), and the strings of each top-level array
|
|
383
385
|
// (`arrays`). Enough for those keys; not a TOML parser.
|
|
384
386
|
export function readToml(text) {
|
|
387
|
+
// a file there that could not be read (permissions, a folder): nothing it says is known, so it reads as broken
|
|
388
|
+
if (text?.unreadable) return { values: {}, roots: [], arrays: {}, mcpServers: [], mcpUnread: true, modelKeys: false, broken: true, brokenAt: 0, unreadable: text.unreadable };
|
|
385
389
|
const state = tomlState();
|
|
386
390
|
const lines = String(text ?? "").split(/\r?\n/);
|
|
387
391
|
const isOpen = () => state.open !== null || state.depth > 0;
|
|
@@ -457,6 +461,7 @@ export function stripJsonc(text) {
|
|
|
457
461
|
// { value } for a readable JSONC text, { unreadable: true } for a broken one, {} when absent.
|
|
458
462
|
function readJsonc(text) {
|
|
459
463
|
if (text == null) return {};
|
|
464
|
+
if (text.unreadable) return { unreadable: true };
|
|
460
465
|
try {
|
|
461
466
|
const value = JSON.parse(stripJsonc(String(text)));
|
|
462
467
|
return value && typeof value === "object" && !Array.isArray(value) ? { value } : { unreadable: true };
|
|
@@ -530,7 +535,7 @@ function codexConfig(world) {
|
|
|
530
535
|
mcpUnread: layers.some((l) => l.mcpUnread),
|
|
531
536
|
modelKeys: layers.some((l) => l.modelKeys),
|
|
532
537
|
broken: layers.some((l) => l.broken),
|
|
533
|
-
brokenAt: layers.map((l, k) => (l.broken ? `${files[k]}
|
|
538
|
+
brokenAt: layers.map((l, k) => (!l.broken ? null : l.unreadable ? `${files[k]} (not readable: ${l.unreadable})` : `${files[k]} line ${l.brokenAt}`)).filter(Boolean),
|
|
534
539
|
};
|
|
535
540
|
}
|
|
536
541
|
|
|
@@ -555,6 +560,8 @@ function mcpBlockedSeat(seat, conf, notes) {
|
|
|
555
560
|
const SEAT_KEYS = ["notify", "otel", "mcp_servers", "web_search", "tools", "analytics", "features", "hooks",
|
|
556
561
|
"sandbox_mode", "sandbox_workspace_write", "approval_policy"];
|
|
557
562
|
|
|
563
|
+
const unreadAt = (read) => (read.unreadable ? `could not be read (${read.unreadable})` : `could not be read to its end (line ${read.brokenAt})`);
|
|
564
|
+
|
|
558
565
|
// The unseated seat when the managed file sets one of those keys, or could not be read to its
|
|
559
566
|
// end (a line read short may set one); null otherwise.
|
|
560
567
|
function managedBlockedSeat(seat, world, notes) {
|
|
@@ -562,7 +569,7 @@ function managedBlockedSeat(seat, world, notes) {
|
|
|
562
569
|
const read = readToml(world.readFile(CODEX_MANAGED));
|
|
563
570
|
const keys = read.roots.filter((k) => SEAT_KEYS.includes(k));
|
|
564
571
|
if (!keys.length && !read.broken) return null;
|
|
565
|
-
notes.push(`${CODEX_MANAGED} ${keys.length ? `sets ${keys.join(", ")}` :
|
|
572
|
+
notes.push(`${CODEX_MANAGED} ${keys.length ? `sets ${keys.join(", ")}` : unreadAt(read)}, which codex applies over the seat's -c flags: the seat cannot hold its guards`);
|
|
566
573
|
return { ...seat, decision: "unseated", reason: "cli-call-failed" };
|
|
567
574
|
}
|
|
568
575
|
|
|
@@ -583,7 +590,7 @@ function requirementsBlockedSeat(seat, world, notes) {
|
|
|
583
590
|
const allows = (k) => SEAT_ALLOWS[k] !== undefined && (read.arrays[k] ?? []).includes(SEAT_ALLOWS[k]);
|
|
584
591
|
const keys = read.roots.filter((k) => !GUARD_FREE.includes(k) && !allows(k));
|
|
585
592
|
if (!keys.length && !read.broken) return null;
|
|
586
|
-
notes.push(`${CODEX_REQUIREMENTS} ${keys.length ? `sets ${keys.join(", ")}` :
|
|
593
|
+
notes.push(`${CODEX_REQUIREMENTS} ${keys.length ? `sets ${keys.join(", ")}` : unreadAt(read)}, which codex holds over the seat's -c, --disable and -s flags: the seat cannot hold its guards`);
|
|
587
594
|
return { ...seat, decision: "unseated", reason: "cli-call-failed" };
|
|
588
595
|
}
|
|
589
596
|
|
|
@@ -653,10 +660,18 @@ function codexSeat(flag, { world, runtime, authorFamilies }) {
|
|
|
653
660
|
const notes = [];
|
|
654
661
|
const conf = codexConfig(world);
|
|
655
662
|
const configured = vetConfigured(safeOnly([conf.cfg.model], "config.toml", notes)[0], authorFamilies, notes);
|
|
656
|
-
const
|
|
663
|
+
const listed = world.run("codex", ["debug", "models", "--bundled"]);
|
|
664
|
+
const read = readCatalog(listed, notes);
|
|
665
|
+
const catalog = read ?? [];
|
|
657
666
|
const sorted = sortModels([...catalog, configured], authorFamilies);
|
|
658
|
-
const
|
|
667
|
+
const ran = world.run("codex", ["--version"]);
|
|
668
|
+
// a version line has a version number in it; an error text on stdout with exit 0 is not one
|
|
669
|
+
const version = /\d+\.\d+/.test(firstLine(ran) ?? "") ? firstLine(ran) : null;
|
|
670
|
+
if (!read) notes.push(`codex debug models --bundled gave no catalog (${callWhy(listed)})`);
|
|
671
|
+
if (!version) notes.push(`codex --version gave no version (${callWhy(ran)})`);
|
|
659
672
|
const seat = { cli: "codex", present: true, version, configured, candidates: sorted.independent, unclassified: sorted.unclassified, mcpServers: conf.mcpServers };
|
|
673
|
+
// both calls failed: this codex does not run here (not logged in, broken install, killed), so it is not a seat
|
|
674
|
+
if (!read && !version) return withNotes({ ...seat, decision: "unseated", reason: "cli-call-failed" }, notes);
|
|
660
675
|
const refused = legacyProfileSeat(seat, conf.cfg, notes) ?? managedBlockedSeat(seat, world, notes)
|
|
661
676
|
?? requirementsBlockedSeat(seat, world, notes) ?? mcpBlockedSeat(seat, conf, notes);
|
|
662
677
|
if (refused) return withNotes(refused, notes);
|
|
@@ -710,8 +725,10 @@ function geminiPoliciesDir(world) {
|
|
|
710
725
|
// seat's read-only tools rest on that flag: such a seat is not seated.
|
|
711
726
|
function geminiPolicySeat(seat, world, notes) {
|
|
712
727
|
const dir = geminiPoliciesDir(world);
|
|
713
|
-
|
|
714
|
-
|
|
728
|
+
const list = world.listDir(dir);
|
|
729
|
+
if (list && !list.some((f) => f.endsWith(".toml"))) return null;
|
|
730
|
+
notes.push(list ? `gemini ignores --admin-policy while ${dir} holds policies: the seat cannot be held to its read-only tools`
|
|
731
|
+
: `the probe could not list ${dir}: gemini ignores --admin-policy while it holds policies, so the seat cannot be shown to keep to its read-only tools`);
|
|
715
732
|
return { ...seat, decision: "unseated", reason: "cli-call-failed" };
|
|
716
733
|
}
|
|
717
734
|
|
|
@@ -835,7 +852,8 @@ const opencodeCache = (world) => join(world.env.XDG_CACHE_HOME || join(world.hom
|
|
|
835
852
|
function catalogProviders(world) {
|
|
836
853
|
const file = join(opencodeCache(world), "models.json");
|
|
837
854
|
const read = readJsonc(world.readFile(file));
|
|
838
|
-
|
|
855
|
+
// an emptied copy ({}) lists no provider: it says nothing about which ones models.dev knows
|
|
856
|
+
return read.value && Object.keys(read.value).length ? new Set(Object.keys(read.value)) : null;
|
|
839
857
|
}
|
|
840
858
|
|
|
841
859
|
// The models the config lists are all opencode can run only when enabled_providers names
|
|
@@ -953,18 +971,20 @@ function realWorld() {
|
|
|
953
971
|
platform: process.platform,
|
|
954
972
|
env: process.env,
|
|
955
973
|
which: onPath,
|
|
974
|
+
// null when the file is not there; { unreadable } when it is but cannot be read, which is not the same as absent
|
|
956
975
|
readFile: (path) => {
|
|
957
976
|
try {
|
|
958
977
|
return readFileSync(path, "utf8");
|
|
959
|
-
} catch {
|
|
960
|
-
return null;
|
|
978
|
+
} catch (e) {
|
|
979
|
+
return e.code === "ENOENT" || e.code === "ENOTDIR" ? null : { unreadable: e.code || "error" };
|
|
961
980
|
}
|
|
962
981
|
},
|
|
982
|
+
// [] when the folder is not there; null when it is but cannot be listed
|
|
963
983
|
listDir: (path) => {
|
|
964
984
|
try {
|
|
965
985
|
return readdirSync(path);
|
|
966
|
-
} catch {
|
|
967
|
-
return [];
|
|
986
|
+
} catch (e) {
|
|
987
|
+
return e.code === "ENOENT" || e.code === "ENOTDIR" ? [] : null;
|
|
968
988
|
}
|
|
969
989
|
},
|
|
970
990
|
run: (cmd, args, env = {}) => {
|
|
@@ -978,11 +998,13 @@ function realWorld() {
|
|
|
978
998
|
const r = spawnSync(viaCmd ? `"${exe}"` : exe, args, {
|
|
979
999
|
encoding: "utf8",
|
|
980
1000
|
timeout: 15000,
|
|
1001
|
+
// a CLI that ignores SIGTERM would hold spawnSync past its time limit
|
|
1002
|
+
killSignal: "SIGKILL",
|
|
981
1003
|
maxBuffer: 64 * 1024 * 1024,
|
|
982
1004
|
shell,
|
|
983
1005
|
env: { ...process.env, ...(viaCmd ? { NoDefaultCurrentDirectoryInExePath: "1" } : {}), ...env },
|
|
984
1006
|
});
|
|
985
|
-
return { status: r.status, stdout: r.stdout ?? "" };
|
|
1007
|
+
return { status: r.status, stdout: r.stdout ?? "", why: r.error ? (r.error.code === "ETIMEDOUT" ? "timed out" : r.error.code) : r.signal ? `killed by ${r.signal}` : null };
|
|
986
1008
|
},
|
|
987
1009
|
};
|
|
988
1010
|
}
|
package/skills/remains/SKILL.md
CHANGED
|
@@ -92,7 +92,7 @@ argument-hint: "[기준 커밋] [--no-boxes]"
|
|
|
92
92
|
- `noTests` 는 시험이 하나도 돌지 않았다는 뜻이라 통과가 아니라 미검증이다.
|
|
93
93
|
- 한 OS 에서만 실패하면, 제품의 결함은 버그로, 시험이 실행 환경을 잘못 가정한 것은 하자로 적는다.
|
|
94
94
|
|
|
95
|
-
박스에 둔 것은 `run` 이 끝날 때 지운다. 시간 초과, 연결 끊김, 중단(SIGINT, SIGTERM) 때는 박스에서 시작한 프로세스를 먼저 끝낸다. Linux, macOS 는 `mktemp -d` 로 만든 개인 폴더를 쓴다. 지우지 못하면 결과의 `cleaned` 가 `false` 이고, 그 박스를 하자로 적는다.
|
|
95
|
+
박스에 둔 것은 `run` 이 끝날 때 지운다. 시간 초과, 연결 끊김, 중단(SIGINT, SIGTERM) 때는 박스에서 시작한 프로세스를 먼저 끝낸다. Linux, macOS 는 `mktemp -d` 로 만든 개인 폴더를 쓴다. 보통은 `/tmp` 아래이고, `runAs` 박스는 `/var/lib` 아래다. `runAs` 계정의 HOME 도 그 안에 있어서, 임시 폴더를 거부하는 도구(teamwork 등)의 시험도 그대로 돈다. 지우지 못하면 결과의 `cleaned` 가 `false` 이고, 그 박스를 하자로 적는다.
|
|
96
96
|
알려진 한계: 이중 fork 로 프로세스 트리를 벗어난 프로세스는 끝나지 않는다. `pgrep` 이 없는 박스에서는 기록된 프로세스 하나만 끝난다.
|
|
97
97
|
|
|
98
98
|
### 등록 파일
|
|
@@ -27,7 +27,11 @@ const FIELDS = new Set(["name", "os", "ssh", "port", "identity", "path", "runAs"
|
|
|
27
27
|
const SECRETISH = /pass|secret|token|credential/i;
|
|
28
28
|
const NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
29
29
|
const TARGET = /^[A-Za-z0-9][A-Za-z0-9._-]*@[A-Za-z0-9][A-Za-z0-9.-]*$/;
|
|
30
|
-
|
|
30
|
+
// Where a box's private folder goes. A runAs box gets one outside the temporary folders, so that the copy and the
|
|
31
|
+
// run account's HOME inside it are not under /tmp: tools that refuse temporary folders (teamwork's Codex home check)
|
|
32
|
+
// would refuse the tests' fixtures there. /var/lib is root's, so only a root login (runAs) makes one there.
|
|
33
|
+
const dirBase = (b) => (b.runAs ? "/var/lib" : "/tmp");
|
|
34
|
+
const remoteDir = (b) => new RegExp(`^${dirBase(b).replace(/\//g, "\\/")}\\/banker-remains\\.[A-Za-z0-9]+$`);
|
|
31
35
|
// What ssh and scp print about the connection itself, as opposed to what the remote command prints.
|
|
32
36
|
const SSH_ERR = /^(ssh|scp): |Connection (closed|refused|reset|timed out)|Host key verification failed|Permission denied \(|Could not resolve hostname|No route to host|Broken pipe/m;
|
|
33
37
|
const UPLOAD_MS = 10 * 60 * 1000;
|
|
@@ -153,8 +157,8 @@ function winSteps(b, name) {
|
|
|
153
157
|
};
|
|
154
158
|
}
|
|
155
159
|
|
|
156
|
-
// Linux and macOS work in the private folder `mktemp -d` made
|
|
157
|
-
// HOME of its own. Ending a run takes the process tree under the recorded id first, then the folder.
|
|
160
|
+
// Linux and macOS work in the private folder `mktemp -d` made (under /tmp, or /var/lib for runAs). A root login
|
|
161
|
+
// hands the run to runAs, with a HOME of its own inside that folder. Ending a run takes the process tree under the recorded id first, then the folder.
|
|
158
162
|
function posixSteps(b, name, dir) {
|
|
159
163
|
const go = b.runAs
|
|
160
164
|
? `mkdir -p "$D/home" && chown -R ${b.runAs} "$D" && runuser -u ${b.runAs} -- env HOME="$D/home" sh "$D/run.sh"`
|
|
@@ -227,10 +231,10 @@ const sshFailed = (r) => (r.status === null && !r.timedOut) || (r.status === 255
|
|
|
227
231
|
|
|
228
232
|
async function makeDir(b, exec) {
|
|
229
233
|
if (b.os === "win32") return { dir: null };
|
|
230
|
-
const r = await exec("ssh", [...sshBase(b), b.ssh,
|
|
234
|
+
const r = await exec("ssh", [...sshBase(b), b.ssh, `mktemp -d ${dirBase(b)}/banker-remains.XXXXXXXX`], { timeoutMs: PROBE_MS });
|
|
231
235
|
const dir = String(r.stdout || "").trim();
|
|
232
236
|
if (r.status !== 0) return { error: lastLine(r.stderr) || "ssh 실패" };
|
|
233
|
-
return
|
|
237
|
+
return remoteDir(b).test(dir) ? { dir } : { error: `mktemp -d 가 예상과 다른 경로를 냈습니다: ${dir.slice(0, 80)}` };
|
|
234
238
|
}
|
|
235
239
|
|
|
236
240
|
async function attempt(b, plan, exec, files, steps) {
|
|
@@ -265,22 +269,37 @@ export async function runOnBox(b, plan, exec) {
|
|
|
265
269
|
|
|
266
270
|
// Started from the home folder with git -C: Windows looks for a program in the working folder first, so a git.exe
|
|
267
271
|
// inside the folder worked on never runs.
|
|
268
|
-
function git(cwd, args) {
|
|
269
|
-
const r = spawnSync("git", ["-C", resolve(cwd), ...args], { cwd: runDir(), encoding: "utf8", maxBuffer: 64 * 1024 * 1024 });
|
|
270
|
-
if (r.status !== 0) throw new Error(`git ${args
|
|
272
|
+
function git(cwd, args, env = process.env) {
|
|
273
|
+
const r = spawnSync("git", ["-C", resolve(cwd), ...args], { cwd: runDir(), env, encoding: "utf8", maxBuffer: 64 * 1024 * 1024 });
|
|
274
|
+
if (r.status !== 0) throw new Error(`git ${gitWords(args)} 실패: ${lastLine(r.stderr) || r.error?.message || ""}`);
|
|
271
275
|
return r.stdout;
|
|
272
276
|
}
|
|
273
277
|
|
|
278
|
+
// The command words for a message: global options (-c key=value, --no-optional-locks) are left out.
|
|
279
|
+
export const gitWords = (args) => args.filter((a, i) => a !== "-c" && args[i - 1] !== "-c" && !a.startsWith("--no-optional-locks")).join(" ");
|
|
280
|
+
|
|
274
281
|
// The commit to send. `worktree` is a commit object made from the tracked files as they are now
|
|
275
282
|
// (git stash create: no stash entry, no change to the work tree). Untracked files are never sent.
|
|
276
|
-
|
|
277
|
-
|
|
283
|
+
// Neither step writes the repository's index: status takes no optional lock, and stash create works on a copy of
|
|
284
|
+
// the index (GIT_INDEX_FILE), so two runs at once, or a git command of the user's, do not meet on index.lock.
|
|
285
|
+
export function resolveTree(repo, ref) {
|
|
286
|
+
const status = git(repo, ["--no-optional-locks", "status", "--porcelain", "--untracked-files=all"]).split("\n").filter(Boolean);
|
|
278
287
|
const untracked = status.filter((l) => l.startsWith("??")).length;
|
|
279
288
|
if (ref !== "worktree") {
|
|
280
289
|
return { sha: git(repo, ["rev-parse", "--verify", `${ref}^{commit}`]).trim(), untracked, modified: status.length - untracked };
|
|
281
290
|
}
|
|
282
|
-
const
|
|
283
|
-
|
|
291
|
+
const tmp = join(tmpdir(), `banker-remains-index-${randomBytes(8).toString("hex")}`);
|
|
292
|
+
mkdirSync(tmp, { mode: 0o700 });
|
|
293
|
+
try {
|
|
294
|
+
const index = resolve(repo, git(repo, ["rev-parse", "--git-path", "index"]).trim());
|
|
295
|
+
const copy = join(tmp, "index");
|
|
296
|
+
if (existsSync(index)) copyFileSync(index, copy);
|
|
297
|
+
const env = { ...process.env, GIT_INDEX_FILE: copy };
|
|
298
|
+
const made = git(repo, ["-c", "user.name=banker-remains", "-c", "user.email=banker-remains@localhost", "stash", "create"], env).trim();
|
|
299
|
+
return { sha: made || git(repo, ["rev-parse", "HEAD"]).trim(), untracked, modified: 0 };
|
|
300
|
+
} finally {
|
|
301
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
302
|
+
}
|
|
284
303
|
}
|
|
285
304
|
|
|
286
305
|
function testFiles(repo, sha) {
|