@kaydash9999/banker-plugins 0.21.0 → 0.23.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 +69 -0
- package/PRIVACY.md +8 -3
- package/README.md +78 -15
- package/bin/banker.js +154 -32
- package/bin/lib/codex-hooks.mjs +123 -20
- package/bin/lib/codex-names.mjs +4 -0
- package/bin/lib/codex-routes.mjs +204 -0
- package/bin/lib/command-skill.cjs +50 -0
- package/bin/lib/telemetry-config.mjs +2 -1
- package/codex/command-skills/front-qa/SKILL.md +56 -0
- package/codex/command-skills/setup/SKILL.md +87 -0
- package/codex/hooks.json +29 -0
- package/codex/manifest.json +18 -15
- package/codex/skills/graceful-pause/SKILL.md +2 -2
- package/codex/skills/progress/scripts/progress.mjs +44 -14
- package/codex/skills/teamwork/SKILL.md +137 -0
- package/codex/skills/teamwork/scripts/teamwork.mjs +957 -0
- package/codex/transform-matrix.md +5 -3
- package/commands/setup.md +2 -2
- package/hooks/obsidize-hook.mjs +75 -32
- package/hooks/telemetry-count-codex.mjs +303 -35
- package/hooks/update-fetch.mjs +2 -1
- package/hooks/update-notify.mjs +146 -13
- package/package.json +3 -1
- package/skills/3d-intro-build/SKILL.md +101 -20
- package/skills/3d-intro-build/references/NOTICE.md +9 -2
- package/skills/3d-intro-build/references/assemble.mjs +113 -22
- package/skills/3d-intro-build/references/azure-adapter.mjs +110 -24
- package/skills/3d-intro-build/references/clip-post.mjs +443 -0
- package/skills/3d-intro-build/references/dive-mode.mjs +299 -0
- package/skills/3d-intro-build/references/intro-fixes.css +11 -0
- package/skills/3d-intro-build/references/prompts.md +51 -4
- package/skills/3d-intro-build/references/scrub-engine.js +1 -0
- package/skills/3d-intro-build/references/step-nav.css +29 -0
- package/skills/3d-intro-build/references/step-nav.js +277 -106
- package/skills/3d-intro-build/references/verify-intro.mjs +387 -32
- package/skills/3d-intro-build/references/verify.md +6 -0
- package/skills/3d-intro-build/references/video-pool.mjs +142 -12
- package/skills/3d-intro-setup/SKILL.md +20 -7
- package/skills/3d-intro-setup/references/azure-adapter.mjs +110 -24
- package/skills/3d-intro-setup/references/video-pool.mjs +142 -12
- package/skills/all-in-one/SKILL.md +125 -86
- package/skills/anti-drm/SKILL.md +1 -1
- package/skills/anti-drm/scripts/instruction-toggle.mjs +57 -4
- package/skills/arch-diagram/build_pptx_template.py +47 -28
- package/skills/audit-web-page/SKILL.md +1 -1
- package/skills/audit-web-page/harness.py +0 -1
- package/skills/lineage/lineage.py +1435 -533
- package/skills/motion-graphic-make/SKILL.md +1 -1
- package/skills/obsidizer/SKILL.md +20 -5
- package/skills/omc-patch/SKILL.md +1 -1
- package/skills/payload-mon/SKILL.md +2 -2
- package/skills/play-qa/SKILL.md +7 -5
- package/skills/progress-mon/README.md +162 -0
- package/skills/progress-mon/SKILL.md +165 -0
- package/skills/progress-mon/assets/fixture/README.md +1 -0
- package/skills/progress-mon/assets/fixture/arch.json +33 -0
- package/skills/progress-mon/assets/fixture/commits.json +7 -0
- package/skills/progress-mon/assets/fixture/config.json +30 -0
- package/skills/progress-mon/assets/fixture/events.jsonl +7 -0
- package/skills/progress-mon/assets/fixture/flow.json +44 -0
- package/skills/progress-mon/assets/fixture/items.json +14 -0
- package/skills/progress-mon/assets/fixture/req-spec.json +31 -0
- package/skills/progress-mon/assets/fixture/reqs.json +11 -0
- package/skills/progress-mon/assets/fixture/variants/arch-auto/arch.json +58 -0
- package/skills/progress-mon/assets/fixture/variants/check-auto/arch.json +15 -0
- package/skills/progress-mon/assets/fixture/variants/check-overlap/arch.json +14 -0
- package/skills/progress-mon/assets/fixture/variants/history-manual/config.json +13 -0
- package/skills/progress-mon/assets/fixture/variants/history-manual/items.json +7 -0
- package/skills/progress-mon/assets/fixture/variants/workflow-wide/flow.json +35 -0
- package/skills/progress-mon/assets/fixture/workflows/wf_ex1/agent-a01.jsonl +1 -0
- package/skills/progress-mon/assets/fixture/workflows/wf_ex1/agent-a02.jsonl +1 -0
- package/skills/progress-mon/assets/fixture/workflows/wf_ex1/agent-a03.jsonl +1 -0
- package/skills/progress-mon/assets/fixture/workflows/wf_ex1/agent-a04.jsonl +1 -0
- package/skills/progress-mon/assets/fixture/workflows/wf_ex1/journal.jsonl +7 -0
- package/skills/progress-mon/references/arch.md +280 -0
- package/skills/progress-mon/references/authoring.md +199 -0
- package/skills/progress-mon/references/config.md +115 -0
- package/skills/progress-mon/references/flow.md +75 -0
- package/skills/progress-mon/references/inventory.md +58 -0
- package/skills/progress-mon/references/items.md +74 -0
- package/skills/progress-mon/references/req-spec.md +74 -0
- package/skills/progress-mon/references/reqs.md +30 -0
- package/skills/progress-mon/scripts/index.js +31 -0
- package/skills/progress-mon/scripts/pm-check.mjs +526 -0
- package/skills/progress-mon/scripts/pm-config.mjs +341 -0
- package/skills/progress-mon/scripts/pm-data.mjs +394 -0
- package/skills/progress-mon/scripts/pm-html.mjs +77 -0
- package/skills/progress-mon/scripts/pm-memory.mjs +308 -0
- package/skills/progress-mon/scripts/pm-render.mjs +1644 -0
- package/skills/progress-mon/scripts/pm-server.mjs +735 -0
- package/skills/progress-mon/scripts/pm-sources.mjs +501 -0
- package/skills/progress-mon/scripts/progress-mon.mjs +167 -0
- package/skills/ralph-qa/SKILL.md +105 -16
- package/skills/ralph-qa/references/gate-evidence.mjs +457 -0
- package/skills/ralph-qa/references/payload-scan.mjs +19 -2
- package/skills/ralph-qa/references/run-folder.mjs +299 -0
- package/skills/ralph-qa/references/seat-payload.mjs +60 -13
- package/skills/ralph-qa/references/seat-run.mjs +159 -155
- package/skills/remains/SKILL.md +2 -1
- package/skills/remains/scripts/boxes.mjs +6 -2
- package/skills/rfp-author/scripts/md2docx.py +63 -34
- package/skills/setup-bypass-permissions/SKILL.md +2 -2
- package/skills/setup-bypass-permissions/scripts/codex-config.mjs +19 -8
- package/skills/setup-bypass-permissions/scripts/fallback/bypass-permissions.py +66 -15
- package/skills/setup-mcp/SKILL.md +1 -1
- package/skills/setup-omc-hud/SKILL.md +2 -1
- package/skills/setup-omc-hud/assets/codex-config.mjs +19 -8
- package/skills/smart-compact/SKILL.md +1 -1
- package/skills/smart-compact/scripts/codex-gate.mjs +2 -1
- package/skills/theme-selection/SKILL.md +1 -1
- package/skills/tone-compact/SKILL.md +1 -1
- package/skills/tone-compact/scripts/instruction-toggle.mjs +57 -4
- package/skills/ultra-chain/SKILL.md +34 -16
- package/skills/ultra-chain/references/chain-ledger.mjs +111 -3
- package/skills/ultra-init/SKILL.md +30 -22
- package/skills/ultra-ui-qa/SKILL.md +1 -1
- package/skills/ultraqa/NOTICE.md +34 -0
- package/skills/ultraqa/SKILL.md +196 -0
- package/skills/ultraqa/references/gate-evidence.mjs +457 -0
- package/skills/ultraqa/references/run-folder.mjs +299 -0
- package/skills/ultraqa/references/tmux-drive.md +52 -0
- package/skills/ultraqa/references/uqa-run.mjs +339 -0
- package/skills/update-banker/SKILL.md +32 -7
- package/skills/vertical-pptx/SKILL.md +1 -1
- package/skills/vertical-pptx/build_a4p.js +20 -2
- package/skills/vertical-pptx/build_a4p.py +560 -359
|
@@ -19,10 +19,11 @@
|
|
|
19
19
|
* (the DRM of a closed network encrypts files by their extension, and .md is one it leaves readable).
|
|
20
20
|
*/
|
|
21
21
|
import { fork } from "node:child_process";
|
|
22
|
-
import {
|
|
23
|
-
import { constants as osConstants
|
|
24
|
-
import { basename, dirname,
|
|
22
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, realpathSync, rmSync, utimesSync, writeFileSync } from "node:fs";
|
|
23
|
+
import { constants as osConstants } from "node:os";
|
|
24
|
+
import { basename, dirname, join } from "node:path";
|
|
25
25
|
import { fileURLToPath } from "node:url";
|
|
26
|
+
import { Stop as RunStop, bases, isFile, listDir, lstatOr, makeRunFolder, norm, realOr, removeMarked, runFolder } from "./run-folder.mjs";
|
|
26
27
|
import { POLICY, cleanSeat, policyIntact, strayEnvFiles, sweepBase } from "./gemini-seat.mjs";
|
|
27
28
|
import { scan } from "./payload-scan.mjs";
|
|
28
29
|
import { MAX_BYTES, Stop, buildPayload, checkPayload, gitAt, markerCount, readRun, splitPayload, untracked } from "./seat-payload.mjs";
|
|
@@ -42,6 +43,8 @@ const RUN_NAME = /^ralph-qa\.[A-Za-z0-9]+$/;
|
|
|
42
43
|
const ANY_RUN = /^ralph-qa(-out)?\.[A-Za-z0-9]+$/;
|
|
43
44
|
// The lock of a send that runs: one name, made with "wx", holding the pid and the time.
|
|
44
45
|
const LOCK_NAME = ".running.md";
|
|
46
|
+
const TAKING_NAME = ".taking.md";
|
|
47
|
+
const STALE_TAKING_MS = 60 * 1000;
|
|
45
48
|
|
|
46
49
|
class Usage extends Error {}
|
|
47
50
|
const USAGE = "usage: node seat-run.mjs init | untracked --repo <repo> [--folder <f>] | prepare --dir <d> --repo <repo> --base <base|auto> --id <id>"
|
|
@@ -93,98 +96,30 @@ export function fromMsys(p, platform) {
|
|
|
93
96
|
return m ? `${m[1].toUpperCase()}:\\${(m[2] || "").replace(/\//g, "\\")}` : p;
|
|
94
97
|
}
|
|
95
98
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
}
|
|
100
|
-
return null;
|
|
101
|
-
}
|
|
102
|
-
};
|
|
103
|
-
const isFile = (p) => Boolean(lstatOr(p)?.isFile());
|
|
104
|
-
const listDir = (dir) => {
|
|
105
|
-
try {
|
|
106
|
-
return readdirSync(dir);
|
|
107
|
-
} catch {
|
|
108
|
-
return [];
|
|
109
|
-
}
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
// A folder init made: named ralph-qa.<letters and digits>, a folder and no link, with its marker. Its answer folder
|
|
113
|
-
// is the sibling ralph-qa-out.<the same>.
|
|
114
|
-
export function runFolder(dir) {
|
|
115
|
-
const d = resolve(String(dir));
|
|
116
|
-
if (!RUN_NAME.test(basename(d)) || !lstatOr(d)?.isDirectory() || !isFile(join(d, ".ralph-qa"))) {
|
|
117
|
-
throw new Stop("not-a-run-folder", `init 이 만든 폴더가 아니다: ${d}`);
|
|
118
|
-
}
|
|
99
|
+
// A folder init made (run-folder.mjs: its name, its marker, right under a known private base) and its answer folder,
|
|
100
|
+
// the sibling ralph-qa-out.<the same>.
|
|
101
|
+
function runPair(dir, ctx) {
|
|
102
|
+
const { d } = runFolder(dir, ctx, ["ralph-qa"]);
|
|
119
103
|
const o = join(dirname(d), basename(d).replace(/^ralph-qa\./, "ralph-qa-out."));
|
|
120
104
|
const answers = lstatOr(o);
|
|
121
105
|
return { d, o: answers?.isDirectory() && isFile(join(o, ".ralph-qa")) ? o : null };
|
|
122
106
|
}
|
|
123
107
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const realOr = (p) => {
|
|
127
|
-
try {
|
|
128
|
-
return realpathSync.native(p);
|
|
129
|
-
} catch {
|
|
130
|
-
return p;
|
|
131
|
-
}
|
|
132
|
-
};
|
|
133
|
-
|
|
134
|
-
// A folder only this account may write in (POSIX: its own, no group or other write bit).
|
|
135
|
-
function privateDir(p) {
|
|
136
|
-
try {
|
|
137
|
-
const st = statSync(p);
|
|
138
|
-
return st.isDirectory() && st.uid === process.getuid() && (st.mode & 0o022) === 0;
|
|
139
|
-
} catch {
|
|
140
|
-
return false;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
const insideHome = (ctx, p) => norm(ctx, realOr(p)).startsWith(norm(ctx, realOr(homeOf(ctx))) + sep);
|
|
144
|
-
|
|
145
|
-
// The base for this run's folders, and every base a run of any version may have used, for the day rule. Linux: an
|
|
146
|
-
// XDG_RUNTIME_DIR of this account's own, else ~/.cache; macOS: $TMPDIR (its own per account); Windows: %TEMP% when it
|
|
147
|
-
// is inside the home, else ~/.cache (the old Git Bash runs used ~/.cache). Never /tmp: gemini reads a .env it finds
|
|
148
|
-
// walking up from its folder, and another account may put one there.
|
|
149
|
-
export function bases(ctx) {
|
|
150
|
-
const cache = join(homeOf(ctx), ".cache");
|
|
151
|
-
const base = baseOn(ctx, cache);
|
|
152
|
-
const xdg = ctx.platform === "linux" && ctx.env.XDG_RUNTIME_DIR ? [resolve(ctx.env.XDG_RUNTIME_DIR)] : [];
|
|
153
|
-
return { base, known: [...new Set([base, cache, ...xdg])] };
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
function baseOn(ctx, cache) {
|
|
157
|
-
const { env } = ctx;
|
|
158
|
-
if (ctx.platform === "win32") return env.TEMP && insideHome(ctx, env.TEMP) ? resolve(env.TEMP) : cache;
|
|
159
|
-
if (ctx.platform === "darwin") return env.TMPDIR ? resolve(env.TMPDIR) : cache;
|
|
160
|
-
const xdg = env.XDG_RUNTIME_DIR;
|
|
161
|
-
return xdg && isAbsolute(xdg) && privateDir(xdg) ? resolve(xdg) : cache;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
function readyBase(ctx, base) {
|
|
165
|
-
try {
|
|
166
|
-
mkdirSync(base, { recursive: true, mode: 0o700 });
|
|
167
|
-
} catch (e) {
|
|
168
|
-
throw new Stop("base-not-writable", `기준 폴더를 만들 수 없다(${e.code}): ${base}`);
|
|
169
|
-
}
|
|
170
|
-
if (ctx.platform !== "win32" && !privateDir(base)) throw new Stop("base-not-private", `기준 폴더를 다른 계정도 쓸 수 있다: ${base}`);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// Two folders with the same six characters: the payload one by mkdtemp, its answer folder beside it.
|
|
174
|
-
function makeFolders(base) {
|
|
108
|
+
// Two folders with the same six characters: the payload one by run-folder.mjs, its answer folder beside it.
|
|
109
|
+
function makeFolders(ctx) {
|
|
175
110
|
for (let tries = 0; tries < 20; tries += 1) {
|
|
176
|
-
const d =
|
|
111
|
+
const { dir: d, base } = makeRunFolder(ctx, "ralph-qa");
|
|
177
112
|
const o = join(base, basename(d).replace(/^ralph-qa\./, "ralph-qa-out."));
|
|
178
113
|
try {
|
|
179
114
|
mkdirSync(o, { mode: 0o700 });
|
|
180
|
-
|
|
181
|
-
return { d, o };
|
|
115
|
+
writeFileSync(join(o, ".ralph-qa"), "", { mode: 0o600 });
|
|
116
|
+
return { d, o, base };
|
|
182
117
|
} catch (e) {
|
|
183
118
|
rmSync(d, { recursive: true, force: true });
|
|
184
|
-
if (e.code !== "EEXIST") throw e;
|
|
119
|
+
if (e.code !== "EEXIST") throw new Stop("base-not-writable", `기준 폴더에 쓸 수 없다(${e.code}): ${base}`);
|
|
185
120
|
}
|
|
186
121
|
}
|
|
187
|
-
throw new Stop("base-not-writable", `기준 폴더에 새 폴더 이름을 정하지 못했다: ${base}`);
|
|
122
|
+
throw new Stop("base-not-writable", `기준 폴더에 새 폴더 이름을 정하지 못했다: ${bases(ctx).base}`);
|
|
188
123
|
}
|
|
189
124
|
|
|
190
125
|
// Whether `dir` is inside a git work tree: a seat would then read the repo through its tools.
|
|
@@ -197,20 +132,12 @@ function insideRepo(ctx, dir) {
|
|
|
197
132
|
}
|
|
198
133
|
|
|
199
134
|
function init(_o, ctx) {
|
|
200
|
-
const
|
|
201
|
-
readyBase(ctx, base);
|
|
202
|
-
let made;
|
|
203
|
-
try {
|
|
204
|
-
made = makeFolders(base);
|
|
205
|
-
} catch (e) {
|
|
206
|
-
if (e instanceof Stop) throw e;
|
|
207
|
-
throw new Stop("base-not-writable", `기준 폴더에 쓸 수 없다(${e.code}): ${base}`);
|
|
208
|
-
}
|
|
135
|
+
const made = makeFolders(ctx);
|
|
209
136
|
if (insideRepo(ctx, made.d)) {
|
|
210
137
|
for (const p of [made.d, made.o]) rmSync(p, { recursive: true, force: true });
|
|
211
|
-
throw new Stop("inside-repo", `기준 폴더가 git 저장소 안이다: ${base}`);
|
|
138
|
+
throw new Stop("inside-repo", `기준 폴더가 git 저장소 안이다: ${made.base}`);
|
|
212
139
|
}
|
|
213
|
-
return [{ ok: true, dir: made.d, out: made.o, base }, 0, made.d];
|
|
140
|
+
return [{ ok: true, dir: made.d, out: made.o, base: made.base }, 0, made.d];
|
|
214
141
|
}
|
|
215
142
|
|
|
216
143
|
function listUntracked(o, ctx) {
|
|
@@ -235,15 +162,15 @@ function verdict(result) {
|
|
|
235
162
|
}
|
|
236
163
|
|
|
237
164
|
function prepare(o, ctx) {
|
|
238
|
-
const { d } =
|
|
165
|
+
const { d } = runPair(o.dir, ctx);
|
|
239
166
|
refuseSent(d);
|
|
240
167
|
const built = buildPayload({ dir: d, repo: o.repo, base: o.base, id: o.id, env: ctx.env });
|
|
241
168
|
const [result, code] = verdict({ dir: d, prompt: join(d, "prompt.md"), raw: join(d, "raw.md"), unsentFile: join(d, "unsent.md"), ...built });
|
|
242
169
|
return [result, code, d];
|
|
243
170
|
}
|
|
244
171
|
|
|
245
|
-
function check(o) {
|
|
246
|
-
const { d } =
|
|
172
|
+
function check(o, ctx) {
|
|
173
|
+
const { d } = runPair(o.dir, ctx);
|
|
247
174
|
const id = readRun(d).id;
|
|
248
175
|
const c = checkPayload({ dir: d, id });
|
|
249
176
|
if (c.markers !== 2) throw new Stop("markers", `표지가 두 줄이 아니다(${c.markers}줄). 가리다 표지를 지웠으면 prepare 를 다시 한다`);
|
|
@@ -262,7 +189,7 @@ const readStdin = (ctx) => {
|
|
|
262
189
|
|
|
263
190
|
function put(o, ctx) {
|
|
264
191
|
if (!["short.md", "spec.md"].includes(o.name)) throw new Usage(`--name 은 short.md 나 spec.md 다: ${String(o.name).slice(0, 80)}`);
|
|
265
|
-
const { d } =
|
|
192
|
+
const { d } = runPair(o.dir, ctx);
|
|
266
193
|
refuseSent(d);
|
|
267
194
|
const data = readStdin(ctx);
|
|
268
195
|
writeFileSync(join(d, o.name), data, { mode: 0o600 });
|
|
@@ -294,22 +221,71 @@ export function liveLock(d, now = Date.now()) {
|
|
|
294
221
|
return st && st.isFile() && pid !== process.pid && now - st.mtimeMs < STALE_LOCK_MS && alive(pid) ? pid : null;
|
|
295
222
|
}
|
|
296
223
|
|
|
297
|
-
//
|
|
298
|
-
//
|
|
299
|
-
|
|
300
|
-
const
|
|
301
|
-
|
|
224
|
+
// A lock with no number to read (empty: another send made it and has not written its number yet; or broken) touched
|
|
225
|
+
// in the last 15 minutes: taken over, it would let two sends hold the folder.
|
|
226
|
+
function freshUnread(d, now) {
|
|
227
|
+
const st = lockPid(d) ? null : lstatOr(join(d, LOCK_NAME));
|
|
228
|
+
return Boolean(st && st.isFile() && now - st.mtimeMs < STALE_LOCK_MS);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const justStarted = () => new Stop("busy", "이 폴더의 send 가 방금 시작했다");
|
|
232
|
+
|
|
233
|
+
// Windows makes "wx" fail EPERM (or EACCES, EBUSY), not EEXIST, where another process is deleting the file (delete
|
|
234
|
+
// pending: a lock given back that moment). Such a code is tried again for up to a second; then it is the lock there
|
|
235
|
+
// when the file is seen, else this send's error. Any other error fails at once.
|
|
236
|
+
const PENDING = new Set(["EPERM", "EACCES", "EBUSY"]);
|
|
237
|
+
const PENDING_TRIES = 40;
|
|
238
|
+
const PENDING_WAIT_MS = 25;
|
|
239
|
+
const pause = (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
240
|
+
|
|
241
|
+
// The lock (or the take-over mark) made with "wx": one of two sends that make it together has it.
|
|
242
|
+
function makeLock(file, now) {
|
|
243
|
+
for (let tries = 1; ; tries += 1) {
|
|
302
244
|
try {
|
|
303
|
-
writeFileSync(
|
|
304
|
-
return
|
|
245
|
+
writeFileSync(file, `${process.pid} ${new Date(now).toISOString()}\n`, { flag: "wx", mode: 0o600 });
|
|
246
|
+
return true;
|
|
305
247
|
} catch (e) {
|
|
306
|
-
if (e.code
|
|
248
|
+
if (e.code === "EEXIST") return false;
|
|
249
|
+
if (!PENDING.has(e.code)) throw e;
|
|
250
|
+
if (tries >= PENDING_TRIES) {
|
|
251
|
+
if (existsSync(file)) return false;
|
|
252
|
+
throw e;
|
|
253
|
+
}
|
|
307
254
|
}
|
|
308
|
-
|
|
309
|
-
|
|
255
|
+
pause(PENDING_WAIT_MS);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// A stale lock is taken over only under the take-over mark .taking.md, and judged again under it: of two sends that
|
|
260
|
+
// both found it stale, the second finds the first's new lock live (or with no number yet) and is busy; neither removes
|
|
261
|
+
// a lock that is not stale. A mark left by a send that died taking over is cleared once it is a minute old (that send
|
|
262
|
+
// is busy; the next one goes on), so it holds the folder no longer than that.
|
|
263
|
+
export function takeOver(d, lock, now = Date.now()) {
|
|
264
|
+
const taking = join(d, TAKING_NAME);
|
|
265
|
+
if (!makeLock(taking, now)) {
|
|
266
|
+
const st = lstatOr(taking);
|
|
267
|
+
if (st?.isFile() && now - st.mtimeMs >= STALE_TAKING_MS) rmSync(taking, { force: true });
|
|
268
|
+
throw new Stop("busy", "다른 send 가 이 폴더의 낡은 잠금을 넘겨받는 중이다");
|
|
269
|
+
}
|
|
270
|
+
try {
|
|
271
|
+
if (liveLock(d, now) || freshUnread(d, now)) throw justStarted();
|
|
310
272
|
rmSync(lock, { force: true });
|
|
273
|
+
if (!makeLock(lock, now)) throw justStarted();
|
|
274
|
+
return lock;
|
|
275
|
+
} finally {
|
|
276
|
+
rmSync(taking, { force: true });
|
|
311
277
|
}
|
|
312
|
-
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Takes the folder's lock: "wx" lets one of two sends that start together have it; a stale one is taken over under
|
|
281
|
+
// the take-over mark. Gives back the lock's path.
|
|
282
|
+
export function takeLock(d, now = Date.now()) {
|
|
283
|
+
const lock = join(d, LOCK_NAME);
|
|
284
|
+
if (makeLock(lock, now)) return lock;
|
|
285
|
+
const pid = liveLock(d, now);
|
|
286
|
+
if (pid) throw new Stop("busy", `이 폴더의 send 가 아직 돈다(pid ${pid})`);
|
|
287
|
+
if (freshUnread(d, now)) throw justStarted();
|
|
288
|
+
return takeOver(d, lock, now);
|
|
313
289
|
}
|
|
314
290
|
|
|
315
291
|
// Touches the lock every `every` ms while a send runs: a send past 15 minutes (split parts) stays live to cleanup.
|
|
@@ -330,31 +306,22 @@ function giveBack(lock) {
|
|
|
330
306
|
if (lockPid(dirname(lock)) === process.pid) rmSync(lock, { force: true });
|
|
331
307
|
}
|
|
332
308
|
|
|
333
|
-
// The
|
|
334
|
-
//
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
function removeMarked(p) {
|
|
338
|
-
const was = lstatOr(p);
|
|
309
|
+
// The lock, and only then the last look for result-send.md: a send that checked before another gave the lock back
|
|
310
|
+
// finds the folder sent here, not after it sent again. A stop here gives the lock back.
|
|
311
|
+
export function holdFolder(d, now = Date.now()) {
|
|
312
|
+
const lock = takeLock(d, now);
|
|
339
313
|
try {
|
|
340
|
-
|
|
314
|
+
refuseSent(d);
|
|
341
315
|
} catch (e) {
|
|
342
|
-
|
|
316
|
+
giveBack(lock);
|
|
343
317
|
throw e;
|
|
344
318
|
}
|
|
319
|
+
return lock;
|
|
345
320
|
}
|
|
346
321
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
if (!isFile(join(p, ".ralph-qa"))) writeFileSync(join(p, ".ralph-qa"), "", { flag: "wx", mode: 0o600 });
|
|
351
|
-
if (was) utimesSync(p, was.atime, was.mtime);
|
|
352
|
-
} catch {
|
|
353
|
-
/* the folder takes no marker or times either */
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
// One that cannot be removed (a held handle, a folder this account cannot empty) goes to `left` with why; the rest go on.
|
|
322
|
+
// The day rule: folders of an earlier run, marked, past a day, never a link or a folder without the marker (removed
|
|
323
|
+
// by run-folder.mjs, the marker last). One that cannot be removed (a held handle, a folder this account cannot
|
|
324
|
+
// empty) goes to `left` with why; the rest go on.
|
|
358
325
|
function dayRule(base, now, left) {
|
|
359
326
|
const removed = [];
|
|
360
327
|
for (const name of listDir(base)) {
|
|
@@ -362,7 +329,7 @@ function dayRule(base, now, left) {
|
|
|
362
329
|
const st = ANY_RUN.test(name) ? lstatOr(p) : null;
|
|
363
330
|
if (!st?.isDirectory() || now - st.mtimeMs <= DAY_MS || !isFile(join(p, ".ralph-qa"))) continue;
|
|
364
331
|
try {
|
|
365
|
-
removeMarked(p);
|
|
332
|
+
removeMarked(p, ".ralph-qa");
|
|
366
333
|
removed.push(p);
|
|
367
334
|
} catch (e) {
|
|
368
335
|
left.push({ path: p, why: e.code || e.message });
|
|
@@ -376,7 +343,7 @@ function removeOwn(folders) {
|
|
|
376
343
|
const stayed = [];
|
|
377
344
|
for (const p of folders.filter(Boolean)) {
|
|
378
345
|
try {
|
|
379
|
-
removeMarked(p);
|
|
346
|
+
removeMarked(p, ".ralph-qa");
|
|
380
347
|
} catch (e) {
|
|
381
348
|
stayed.push(`${p} (${e.code || e.message})`);
|
|
382
349
|
}
|
|
@@ -430,7 +397,7 @@ const readIdOr = (d) => {
|
|
|
430
397
|
};
|
|
431
398
|
|
|
432
399
|
async function cleanup(o, ctx) {
|
|
433
|
-
const { d, o: out } =
|
|
400
|
+
const { d, o: out } = runPair(o.dir, ctx);
|
|
434
401
|
const pid = o.force ? null : liveLock(d, ctx.now());
|
|
435
402
|
if (pid) throw new Stop("busy", `이 폴더의 send 가 아직 돈다(pid ${pid}). 끝난 뒤 다시 한다`);
|
|
436
403
|
const { known } = bases(ctx);
|
|
@@ -586,8 +553,8 @@ async function cliChecks(ctx, seat, plan, exe) {
|
|
|
586
553
|
}
|
|
587
554
|
|
|
588
555
|
// The parts a seat gets: the payload, or its split, each with its two markers and no secret-like line.
|
|
589
|
-
function partsOf(payload, n, id, short) {
|
|
590
|
-
const parts = n ? splitPayload(payload, n, id, short) : [payload];
|
|
556
|
+
function partsOf(payload, n, { id, short, shortSha }) {
|
|
557
|
+
const parts = n ? splitPayload(payload, n, id, short, shortSha) : [payload];
|
|
591
558
|
parts.forEach((part, k) => {
|
|
592
559
|
if (markerCount(part, id) !== 2 || scan(part.toString("utf8")).count) throw new Stop("split", `부분 ${k + 1}/${parts.length} 의 표지나 검사가 맞지 않는다`);
|
|
593
560
|
});
|
|
@@ -699,7 +666,7 @@ async function seatWork(ctx, run, seat, g, payload, base) {
|
|
|
699
666
|
if (why) return { ...base, why };
|
|
700
667
|
let parts;
|
|
701
668
|
try {
|
|
702
|
-
parts = partsOf(payload, run.split[seat.cli], run
|
|
669
|
+
parts = partsOf(payload, run.split[seat.cli], run);
|
|
703
670
|
} catch (e) {
|
|
704
671
|
if (e instanceof Stop) return { ...base, why: "split", notes: [e.message] };
|
|
705
672
|
throw e;
|
|
@@ -708,10 +675,37 @@ async function seatWork(ctx, run, seat, g, payload, base) {
|
|
|
708
675
|
return runParts(ctx, run, seat, g, parts, base);
|
|
709
676
|
}
|
|
710
677
|
|
|
678
|
+
// The seat CLI's version, read without starting gemini or opencode: codex's from the probe's `codex --version`, the
|
|
679
|
+
// others' from the package.json of their npm package (beside the entry point, above its real path, or under the
|
|
680
|
+
// prefix's lib/node_modules), opencode's own installer folder holding the plugin package of its version.
|
|
681
|
+
const PACKAGES = { codex: "@openai/codex", gemini: "@google/gemini-cli", opencode: "opencode-ai" };
|
|
682
|
+
const readJson = (p) => {
|
|
683
|
+
try {
|
|
684
|
+
return JSON.parse(readFileSync(p, "utf8"));
|
|
685
|
+
} catch {
|
|
686
|
+
return null;
|
|
687
|
+
}
|
|
688
|
+
};
|
|
689
|
+
function installedVersion(cli, exe) {
|
|
690
|
+
const real = realOr(exe);
|
|
691
|
+
const pkg = PACKAGES[cli].split("/");
|
|
692
|
+
const tries = [join(dirname(exe), "node_modules", ...pkg, "package.json"), join(dirname(dirname(exe)), "lib", "node_modules", ...pkg, "package.json")];
|
|
693
|
+
for (let dir = dirname(real), k = 0; k < 5; k += 1, dir = dirname(dir)) tries.push(join(dir, "package.json"));
|
|
694
|
+
const found = tries.map(readJson).find((j) => j?.name === PACKAGES[cli] && typeof j.version === "string");
|
|
695
|
+
if (found) return found.version;
|
|
696
|
+
const own = cli === "opencode" ? readJson(join(dirname(dirname(real)), "package.json"))?.dependencies?.["@opencode-ai/plugin"] : null;
|
|
697
|
+
return typeof own === "string" ? `${own} (설치 폴더의 @opencode-ai/plugin)` : null;
|
|
698
|
+
}
|
|
699
|
+
export function cliVersion(cli, exe, probed) {
|
|
700
|
+
if (cli === "codex" && probed?.version) return String(probed.version).slice(0, 80);
|
|
701
|
+
return installedVersion(cli, exe) || "알 수 없음";
|
|
702
|
+
}
|
|
703
|
+
|
|
711
704
|
async function sendSeat(ctx, run, seat, payload) {
|
|
712
|
-
const
|
|
705
|
+
const none = { seat: seat.cli, model: seat.model, status: "not-sent", why: null, exit: null, signal: null, secs: 0, answer: null, errors: null, facts: {}, notes: [] };
|
|
713
706
|
const g = guards(ctx, seat, run);
|
|
714
|
-
if (g.why) return { ...
|
|
707
|
+
if (g.why) return { ...none, why: g.why, notes: g.notes || [] };
|
|
708
|
+
const base = { ...none, cliVersion: cliVersion(seat.cli, g.exe, g.probed) };
|
|
715
709
|
let result;
|
|
716
710
|
try {
|
|
717
711
|
mkdirSync(join(run.d, seat.cli), { recursive: true, mode: 0o700 });
|
|
@@ -719,7 +713,7 @@ async function sendSeat(ctx, run, seat, payload) {
|
|
|
719
713
|
} catch (e) {
|
|
720
714
|
// the runner's own error in this seat's work (its copy cannot be written: ENOSPC, EACCES, a Windows lock) is this
|
|
721
715
|
// seat's failure: the other seats go on, and the lock and the stop handlers hold until they end
|
|
722
|
-
if (e instanceof
|
|
716
|
+
if (e instanceof RunStop) throw e;
|
|
723
717
|
result = { ...base, status: "failed", why: "spawn", notes: [`좌석을 준비하지 못했다(${e.code || e.message})`] };
|
|
724
718
|
} finally {
|
|
725
719
|
const notes = await afterSeat(ctx, run, seat);
|
|
@@ -775,29 +769,42 @@ function declared(run, bytes, seats, d) {
|
|
|
775
769
|
}
|
|
776
770
|
|
|
777
771
|
function sendStart(o, ctx, a) {
|
|
778
|
-
const { d, o: out } =
|
|
772
|
+
const { d, o: out } = runPair(o.dir, ctx);
|
|
779
773
|
if (!out) throw new Stop("not-a-run-folder", `답 폴더(ralph-qa-out.*)가 없다: ${d}`);
|
|
780
774
|
refuseSent(d);
|
|
781
775
|
if (ctx.env.CODEX_SANDBOX_NETWORK_DISABLED === "1" && !a.dryRun) throw new Stop("sandboxed", "Codex sandbox 가 네트워크를 막는다. 승인을 받아 sandbox 밖에서 send 를 다시 한다");
|
|
782
|
-
const { id, short } = readRun(d);
|
|
776
|
+
const { id, short, shortsha } = readRun(d);
|
|
783
777
|
const payload = payloadToSend(d, id, Boolean(o["allow-large"]));
|
|
784
|
-
return { d, out, id, payload, short: Number(short) };
|
|
778
|
+
return { d, out, id, payload, short: Number(short), shortSha: shortsha || null };
|
|
785
779
|
}
|
|
786
780
|
|
|
781
|
+
const resultJson = (command, result) => `${JSON.stringify({ command, ...result }, null, 2)}\n`;
|
|
782
|
+
const writeResult = (dir, name, json) => {
|
|
783
|
+
try {
|
|
784
|
+
writeFileSync(join(dir, `result-${name}.md`), json, { mode: 0o600 });
|
|
785
|
+
} catch {
|
|
786
|
+
/* the JSON on stdout is the answer */
|
|
787
|
+
}
|
|
788
|
+
};
|
|
789
|
+
|
|
790
|
+
// A send that went out writes result-send.md before the lock goes back, so the next send to take the lock finds it
|
|
791
|
+
// (report writes it again with the same JSON).
|
|
787
792
|
async function send(o, ctx) {
|
|
788
793
|
const a = sendArgs(o);
|
|
789
|
-
const { d, out, id, payload, short } = sendStart(o, ctx, a);
|
|
790
|
-
const lock =
|
|
794
|
+
const { d, out, id, payload, short, shortSha } = sendStart(o, ctx, a);
|
|
795
|
+
const lock = holdFolder(d, ctx.now());
|
|
791
796
|
const touch = keepFresh(lock);
|
|
792
797
|
const signals = watchSignals();
|
|
793
|
-
const run = { ...a, d, o: out, id, short, signalled: () => signals.code() !== 0 };
|
|
798
|
+
const run = { ...a, d, o: out, id, short, shortSha, signalled: () => signals.code() !== 0 };
|
|
794
799
|
try {
|
|
795
800
|
// every seat settles before the lock goes back and the stop handlers go away
|
|
796
801
|
const settled = await Promise.allSettled(a.seats.map((x) => sendSeat(ctx, run, x, payload)));
|
|
797
802
|
const thrown = settled.find((x) => x.status === "rejected");
|
|
798
803
|
if (thrown) throw thrown.reason;
|
|
799
804
|
const seats = settled.map((x) => x.value);
|
|
805
|
+
if (!a.dryRun) appendFileSync(join(d, "run.md"), seats.filter((x) => x.cliVersion).map((x) => `cli-${x.seat} ${x.cliVersion}\n`).join(""), { mode: 0o600 });
|
|
800
806
|
const result = { ok: true, ...(a.dryRun ? { dryRun: true } : {}), dir: d, out, bytes: payload.length, scan: 0, unsent: unsentCount(d), seats, declared: declared(run, payload.length, seats, d) };
|
|
807
|
+
if (!a.dryRun) writeResult(d, "send", resultJson("send", result));
|
|
801
808
|
return [result, signals.code(), a.dryRun ? null : d];
|
|
802
809
|
} finally {
|
|
803
810
|
clearInterval(touch);
|
|
@@ -810,23 +817,17 @@ const COMMANDS = { init, untracked: listUntracked, prepare, check, send, cleanup
|
|
|
810
817
|
|
|
811
818
|
// The JSON on stdout and in <d>/result-<command>.md (not for cleanup: the folder is gone).
|
|
812
819
|
function report(ctx, command, dir, result, code, name = command) {
|
|
813
|
-
const json =
|
|
820
|
+
const json = resultJson(command, result);
|
|
814
821
|
ctx.stdout.write(json);
|
|
815
822
|
if (result.message) ctx.stderr.write(`${result.message}\n`);
|
|
816
|
-
if (dir)
|
|
817
|
-
try {
|
|
818
|
-
writeFileSync(join(dir, `result-${name}.md`), json, { mode: 0o600 });
|
|
819
|
-
} catch {
|
|
820
|
-
/* the JSON on stdout is the answer */
|
|
821
|
-
}
|
|
822
|
-
}
|
|
823
|
+
if (dir) writeResult(dir, name, json);
|
|
823
824
|
return code;
|
|
824
825
|
}
|
|
825
826
|
|
|
826
827
|
// A stop in the folder it concerns, when that is a run folder.
|
|
827
|
-
const folderOf = (o) => {
|
|
828
|
+
const folderOf = (o, ctx) => {
|
|
828
829
|
try {
|
|
829
|
-
return o.dir ?
|
|
830
|
+
return o.dir ? runPair(o.dir, ctx).d : null;
|
|
830
831
|
} catch {
|
|
831
832
|
return null;
|
|
832
833
|
}
|
|
@@ -835,17 +836,20 @@ const folderOf = (o) => {
|
|
|
835
836
|
// A send that stopped goes to result-send-stop.md: result-send.md says the folder was sent, and a stopped send sent nothing.
|
|
836
837
|
const stopName = (command) => (command === "send" ? "send-stop" : command);
|
|
837
838
|
|
|
839
|
+
// A stop of run-folder.mjs carries no name: this runner's goes before it.
|
|
840
|
+
const named = (message) => (message.startsWith("ralph-qa: ") ? message : `ralph-qa: ${message}`);
|
|
841
|
+
|
|
838
842
|
function failed(ctx, { command, o }, e) {
|
|
839
843
|
if (e instanceof Usage) {
|
|
840
844
|
ctx.stderr.write(`ralph-qa: ${e.message}\n${USAGE}`);
|
|
841
845
|
return 2;
|
|
842
846
|
}
|
|
843
|
-
if (!(e instanceof
|
|
847
|
+
if (!(e instanceof RunStop)) {
|
|
844
848
|
ctx.stderr.write(`ralph-qa: 실행기 내부 오류(${e?.code || e?.name || "error"}): ${String(e?.message || e).split("\n")[0]}\n`);
|
|
845
849
|
return 2;
|
|
846
850
|
}
|
|
847
|
-
const result = { ok: false, stop: e.code, message: e.message, detail: e.detail };
|
|
848
|
-
return report(ctx, command, folderOf(o), result, EXIT_OF[e.code] || 3, stopName(command));
|
|
851
|
+
const result = { ok: false, stop: e.code, message: named(e.message), detail: e.detail };
|
|
852
|
+
return report(ctx, command, folderOf(o, ctx), result, EXIT_OF[e.code] || 3, stopName(command));
|
|
849
853
|
}
|
|
850
854
|
|
|
851
855
|
// ---- the send's own node ----
|
package/skills/remains/SKILL.md
CHANGED
|
@@ -41,7 +41,7 @@ argument-hint: "[기준 커밋] [--no-boxes]"
|
|
|
41
41
|
|
|
42
42
|
## 4. 테스트박스 (등록돼 있으면)
|
|
43
43
|
|
|
44
|
-
박스 작업은 이 스킬 폴더의 `scripts/boxes.mjs` 가 한다(Codex
|
|
44
|
+
박스 작업은 이 스킬 폴더의 `scripts/boxes.mjs` 가 한다(Codex 의 npm 설치는 `~/.codex/skills/banker-remains/scripts/boxes.mjs`, 플러그인 설치는 `~/.codex/plugins/cache/<마켓플레이스>/banker/<판>/skills/remains/scripts/boxes.mjs`). Node 만 쓰고, 이 머신에 `ssh`, `scp`, `git`, 박스에 `tar` 가 있어야 한다. `--no-boxes` 를 받으면 이 단계를 건너뛰고 확인 범위에 그렇게 적는다.
|
|
45
45
|
|
|
46
46
|
1. `node <스킬 폴더>/scripts/boxes.mjs list` 로 등록을 본다. 등록 파일이 없으면 확인 범위에 "테스트박스 미등록" 이라 적고 넘어간다.
|
|
47
47
|
2. `node <스킬 폴더>/scripts/boxes.mjs probe` 로 접속을 확인한다.
|
|
@@ -54,6 +54,7 @@ argument-hint: "[기준 커밋] [--no-boxes]"
|
|
|
54
54
|
- `--node-test` 는 `*.test.mjs` 만 돌린다. 다른 종류의 시험(Python, `.test.ts` 등)은 `--cmd` 로 따로 돌리거나 그 OS 에서 미검증으로 적는다.
|
|
55
55
|
- 보내는 것은 추적하는 파일의 커밋된 내용 그대로다(줄 끝 변환 없음). `--ref worktree` 는 커밋하지 않은 추적 파일의 변경까지 보낸다. 추적하지 않는 새 파일은 보내지 않는다(출력의 `untracked`). 그 파일이 시험에 필요하면 미검증으로 적는다.
|
|
56
56
|
- Windows 박스에서는 명령이 `.cmd` 파일 안에서 돈다. 명령이 OS 마다 달라야 하면 `--box <이름>` 으로 나눠 돌린다.
|
|
57
|
+
- 그 `.cmd` 파일은 UTF-8 코드 페이지로 돌아 한글 인자도 그대로 간다. `for` 의 변수는 배치 파일 규칙대로 `%%i` 로 쓰고, `for` 나 `if` 안에서 npm 같은 배치 명령을 부르면 `call npm ...` 으로 쓴다.
|
|
57
58
|
- 오래 걸리면 `--timeout <분>`(기본 20)을 늘린다.
|
|
58
59
|
4. 출력 JSON 의 박스별 `summary`(tests, pass, fail, skipped, failures)와 `log` 파일을 근거로 쓴다. 종료 코드는 다음과 같다.
|
|
59
60
|
- 0: 모든 박스가 통과
|
|
@@ -117,10 +117,14 @@ export const nodeTestCommand = (os, files) =>
|
|
|
117
117
|
|
|
118
118
|
// The script that runs on the box from the top of the sent tree. On Linux and macOS it first leaves its process
|
|
119
119
|
// id beside itself, so a stopped run can end everything it started. On Windows it is a .cmd file: `call` returns
|
|
120
|
-
// from a batch command such as npm, and the command stays out of the ssh and cmd /c quoting.
|
|
120
|
+
// from a batch command such as npm, and the command stays out of the ssh and cmd /c quoting. The file is UTF-8, so
|
|
121
|
+
// it first sets that code page: cmd reads each line in the console's (949 on a Korean Windows), and a Korean
|
|
122
|
+
// argument came to node broken. `call` cannot run for, if or a ( block, so those go as they are.
|
|
123
|
+
const NO_CALL = /^\s*(?:(?:for|if)(?=[\s/]|$)|\()/i;
|
|
121
124
|
export function runScript(b, prefix, command) {
|
|
122
125
|
if (b.os === "win32") {
|
|
123
|
-
const
|
|
126
|
+
const run = NO_CALL.test(command) ? command : `call ${command}`;
|
|
127
|
+
const lines = ["@echo off", "chcp 65001 >nul 2>&1", `cd /d "%~dp0${prefix}" || exit /b 97`, run, "exit /b %ERRORLEVEL%"];
|
|
124
128
|
return { ext: ".cmd", text: lines.map((l) => `${l}\r\n`).join("") };
|
|
125
129
|
}
|
|
126
130
|
const path = b.path?.length ? [`${pathPrefix(b)}export PATH`] : [];
|