Ragnarok rodecomp deployment on memento

Changes to upstream:
- git.js: coordinator commit identity from TD_GIT_AUTHOR_NAME/EMAIL.
- build.js: docker runner can share job dirs through a named volume
  (jobVolume/jobVolumeRoot) for containerized workers.
- install.js: staged installer output with logs, start prompt for regions.
- web: feed layout, cache-busting asset versions, multi-agent help section.
- build-td: fetch checksummed Windows runtime when building on non-Windows.

Additions:
- deploy/rodecomp: compose stack, Dockerfiles, Caddyfile, server.json,
  bulk import/verify scripts, and README.md with the deployment notes
  (the root README stays identical to upstream).
- toolchain/msvc-v142: MSVC v142 under Wine (Docker), SDK 10.0.19041.0.
- tools/rodecomp: project preparation, Forgejo provisioning, history and
  identity tools.
This commit is contained in:
clank-bot 2026-10-25 14:38:52 +01:00
commit 022e0a2c78
39 changed files with 1232 additions and 33 deletions

6
.dockerignore Normal file
View file

@ -0,0 +1,6 @@
.git
data
logs
dist
deploy/rodecomp/import
toolchain

13
deploy/rodecomp/Caddyfile Normal file
View file

@ -0,0 +1,13 @@
{
admin off
auto_https off
}
:8080 {
redir /git /git/ 308
handle_path /git/* {
reverse_proxy forgejo:3000
}
handle {
reverse_proxy web:8080
}
}

View file

@ -0,0 +1,11 @@
FROM docker:29-cli AS docker-cli
FROM node:24-bookworm-slim
RUN apt-get update && apt-get install -y --no-install-recommends git binutils ca-certificates && rm -rf /var/lib/apt/lists/*
COPY --from=docker-cli /usr/local/bin/docker /usr/local/bin/docker
WORKDIR /app
COPY package.json ./
COPY src ./src
COPY ghidra_scripts ./ghidra_scripts
COPY template ./template
ENV NODE_ENV=production
ENTRYPOINT ["node", "src/server/cli.js"]

View file

@ -0,0 +1,8 @@
FROM node:22.21.0-bookworm-slim
WORKDIR /app
COPY package.json ./
COPY src ./src
COPY ghidra_scripts ./ghidra_scripts
COPY tools/build-td ./tools/build-td
RUN node tools/build-td/build.mjs
CMD ["cp", "-a", "/app/dist/.", "/output/"]

View file

@ -0,0 +1,3 @@
FROM rodecomp/server:local
RUN apt-get update && apt-get install -y --no-install-recommends git-filter-repo && rm -rf /var/lib/apt/lists/*
ENTRYPOINT ["git"]

90
deploy/rodecomp/README.md Normal file
View file

@ -0,0 +1,90 @@
# Ragnarok deployment on memento
Deployment files: `C:\Users\Memento\Desktop\rodecomp`; Compose file:
`deploy/rodecomp/compose.yaml`. All services and compiler execution run in Linux
Docker containers. Persistent repository, SQLite, targets and downloads live in
the external Docker volume `rodecomp_data` (create it before Compose).
The pinned candidate toolchain is MSVC **19.29.30159**, v142, x86 target, Windows
SDK **10.0.19041.0**, under Wine. This is consistent with Ragexe's linker 14.29
and `Release V142 C++20` PDB path; the original exact compiler build is unknown.
`toolchain/msvc-v142/Dockerfile` downloads Microsoft packages with SHA-256 checks
inside its build stage. Check applicable Microsoft redistribution rights before
exposing compiler downloads publicly.
The optional `runners.ragnarok.jobVolume` and `jobVolumeRoot` settings mount only
the job subdirectory into sibling compiler containers using Docker's
`volume-subpath` support. This avoids Docker Desktop host-path translation.
Only the worker receives the Docker socket; this still grants it control over
the Docker daemon. Compiler containers retain upstream network, capability,
read-only-root and resource restrictions.
`tools/rodecomp/configure.sh` runs CMake in `rodecomp/configure:v142` to export
real per-unit flags through `export-compile.py`. Runtime checks use upstream's
direct compiler adapter, not CMake/CTest. Upstream matching and peer-review
policy is unchanged. `tools/build-td/build.mjs` can now package both Windows and
Linux clients from the Linux build container.
The Docker Caddy proxy binds **127.0.0.1:19580** on memento. Cloudflare Tunnel
publishes `https://clank.rodecomp.online`; the native connector targets that
localhost port. Caddy strips `/git/` and forwards it to Forgejo; other paths go
to Clank. Generated installers use the configured public HTTPS hostname.
Do not publish the Desktop directory or Docker socket.
From memento's Debian WSL, start/restart with:
`docker compose -f /mnt/c/Users/Memento/Desktop/rodecomp/deploy/rodecomp/compose.yaml up -d`.
Back up the volume with the services stopped, including SQLite and repository;
copying only the Desktop deployment files does not back up runtime state.
Forgejo **15.0.10** (the version pinned by upstream's deployment script) serves
`https://clank.rodecomp.online/git/clank/rodecomp`. Registration, SSH, Actions,
packages and pull requests are disabled. The coordinator's `clank-bot` account
is the only whitelisted pusher to protected `main`; `pushRemote=forgejo` publishes
accepted merges. Forgejo accounts are separate from Clank accounts. Its random
`clank-admin` credential is stored privately in `rodecomp_data/forgejo-admin.json`;
the coordinator bot token stays in the canonical repository's `.git/config`.
Back up **both** `rodecomp_data` and `rodecomp_forgejo`. Provisioning is implemented
in `tools/rodecomp/provision-forgejo.mjs`, without changes to Clank source code.
Verified public repository browsing, HTTPS clone and coordinator initial push.
The published project was reset to one root commit, `d456e8f64aa1b73279471149d8cd4d05c98eb541`,
preserving the current tree. Existing contributor clones must be recreated.
Compose sets `TD_GIT_AUTHOR_NAME` and `TD_GIT_AUTHOR_EMAIL` to `ragnarok-clanker`;
the coordinator's optional identity override sets both author and committer for
future commits. Without those variables, upstream identity behavior is unchanged.
The private `rodecomp_history_backup` volume retains pre-rewrite recovery data;
it is not published. Public old Git objects were pruned. Clank progress was preserved.
Installer presentation (`src/server/install.js` only): four numbered steps, a compact
ASCII banner, color/status icons, explicit incomplete-installation reporting, and
an MCP handoff. Detailed td output is retained in a temporary installation log with
the supplied token redacted. `NO_COLOR` and redirected output disable colors.
The displayed AI start prompt matches the guide exactly. The td client binaries,
CLI help, and MCP protocol are unchanged by this presentation update.
Live feed navigation fix: reset its event cursor when replacing the initial feed
snapshot on returning Home. The list fills its parent panel with internal scrolling;
stacked mobile layout retains a viewport-height limit. Versioned frontend asset URLs
avoid stale Cloudflare/browser scripts. Verified Home → Guide → Home retains events
and desktop Live/Progress panel heights match.
Verified deployment: administrator browser login; generated guide and installers;
fresh Linux installer clone/target hash/compiler download/MCP setup followed by
`td check Source/Utility/XorBytes.cpp`; sandboxed server checks of XorBytes,
GPak and BrowserClientSite; Windows standalone client's `--help` and PowerShell
installer parsing. Windows full onboarding and all-source builds are not yet
verified. The imported original-code cache contains 4,480 functions, not all
45,446 function symbols; uncached pseudocode still needs an analysis provider.
Progress migration: upstream `td-server rebuild` measured the existing source.
Historical confirmations are imported with `tools/rodecomp/import-confirmations.mjs`
only when the entire source file matches its historical confirmed commit and the
current rebuild emitted the function. The evidence export is retained under
`deploy/rodecomp/import/confirmed.json`; migration records an audit event and does
not manufacture reviews or recent merge credit. Initial refresh counted 189 exact
and 453 additional peer-confirmed functions (642/43,147, 1.49%); 13 historical
confirmations were deferred because current output was missing.
The full rebuild exposed five failing units: FontCache (SDK min/max),
RenderToTextureDX7 (missing wrl/client.h), TextureDX7 (v142 compiler internal error),
and two Verification units absent from upstream's copied source directories.
These are not counted as successful builds and were not changed by the progress migration.

View file

@ -0,0 +1,43 @@
const fs = require('node:fs');
const path = require('node:path');
const { DatabaseSync } = require('node:sqlite');
const { execFileSync } = require('node:child_process');
const crypto = require('node:crypto');
const root = '/var/lib/docker/volumes/rodecomp_data/_data';
const repo = path.join(root, 'repo');
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
const destination = path.join(root, 'admin-backups', `readable-source-${stamp}`);
fs.mkdirSync(destination, { recursive: true });
const git = (...args) => execFileSync('git', args, { cwd: repo, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 }).trim();
if (git('status', '--porcelain')) throw new Error('Canonical repository is dirty; no import performed');
const db = new DatabaseSync(path.join(root, 'td.sqlite'));
db.exec('PRAGMA busy_timeout=15000');
const running = db.prepare("SELECT COUNT(*) n FROM check_jobs WHERE state IN ('queued','running')").get().n;
const verifying = db.prepare("SELECT COUNT(*) n FROM batches WHERE state IN ('queued','verifying','merging')").get().n;
if (running || verifying) throw new Error('Live verification/merge jobs exist; no import performed');
const before = {
stamp,
head: git('rev-parse', 'HEAD'),
tasks: db.prepare('SELECT state,count(*) n FROM tasks GROUP BY state').all(),
attempts: db.prepare('SELECT state,kind,count(*) n FROM attempts GROUP BY state,kind').all(),
batchCounts: db.prepare('SELECT state,count(*) n FROM batches GROUP BY state').all(),
schemas: db.prepare("SELECT name,sql FROM sqlite_master WHERE type IN ('table','trigger')").all(),
funcStatus: db.prepare('SELECT * FROM func_status').all(),
symbols: db.prepare('SELECT id,unit,decorated,name,library FROM symbols').all(),
tasksFull: db.prepare('SELECT * FROM tasks').all(),
reviews: db.prepare('SELECT count(*) n FROM peer_reviews').get().n,
humanReviews: db.prepare('SELECT count(*) n FROM reviews').get().n
};
db.prepare('VACUUM INTO ?').run(path.join(destination, 'td.sqlite'));
execFileSync('git', ['bundle', 'create', path.join(destination, 'repo.bundle'), '--all'], { cwd: repo });
execFileSync('tar', ['-czf', path.join(destination, 'repo.tar.gz'), '-C', root, 'repo']);
fs.copyFileSync(path.join(root, 'server.json'), path.join(destination, 'server.json'));
fs.copyFileSync('/tmp/bulk-import-payload.json', path.join(destination, 'payload.json'));
fs.writeFileSync(path.join(destination, 'before.json'), JSON.stringify(before, null, 2));
const integrity = new DatabaseSync(path.join(destination, 'td.sqlite'), { readOnly: true });
if (integrity.prepare('PRAGMA integrity_check').get().integrity_check !== 'ok') throw new Error('Backup database integrity failed');
integrity.close();
const result = { destination, head: before.head, reviews: before.reviews, humanReviews: before.humanReviews, payloadSha256: crypto.createHash('sha256').update(fs.readFileSync(path.join(destination, 'payload.json'))).digest('hex'), backupIntegrity: 'ok' };
fs.writeFileSync(path.join(root, 'bulk-import-backup-latest.json'), JSON.stringify(result, null, 2));
console.log(JSON.stringify(result, null, 2));
db.close();

View file

@ -0,0 +1,63 @@
import fs from 'node:fs';
import path from 'node:path';
import crypto from 'node:crypto';
import { openServer } from '/app/src/server/app.js';
const data = '/var/lib/docker/volumes/rodecomp_data/_data';
const ctx = openServer(data);
const store = ctx.store;
const payload = JSON.parse(fs.readFileSync(path.join(data, 'admin-bulk/payload.json'), 'utf8'));
const installed = JSON.parse(fs.readFileSync(path.join(data, 'admin-bulk/install-result.json'), 'utf8'));
const registered = JSON.parse(fs.readFileSync(path.join(data, 'admin-bulk/register-result.json'), 'utf8'));
if (ctx.repo.head() !== installed.commit) throw new Error('Import head changed');
const current = new Set(Object.keys(payload.functions));
const legacy = store.all("SELECT a.id,a.kind,a.batch_id,a.task_id,t.symbol_id FROM attempts a JOIN tasks t ON t.id=a.task_id WHERE a.state IN ('queued','verifying','peer_review','in_review','approved','merging')").filter(attempt => attempt.kind === 'code' && !current.has(attempt.symbol_id));
const reason = 'Historical submission is preserved but is not a definition in the imported current source. Its old file snapshot could resurrect pre-refactor paths/interfaces. Rebase the missing definition onto the imported source before resubmitting; no review approval has been inferred.';
store.db.exec('BEGIN IMMEDIATE');
try {
for (const attempt of legacy) {
store.run("UPDATE attempts SET state='needs_cleanup',result_json=json_set(COALESCE(result_json,'{}'),'$.sourceMigration',json(?)),finished_at=? WHERE id=?", JSON.stringify({ commit: installed.commit, reason, originalHistoryPreserved: true }), Date.now(), attempt.id);
store.run('DELETE FROM peer_leases WHERE attempt_id=?', attempt.id);
store.run("UPDATE tasks SET state='needs_cleanup',lease_user=NULL,lease_expires=NULL,updated_at=? WHERE id=? AND state NOT IN ('done','skipped')", Date.now(), attempt.task_id);
}
for (const id of new Set(legacy.map(attempt => attempt.batch_id).filter(Boolean))) {
const live = store.get("SELECT 1 FROM attempts WHERE batch_id=? AND state IN ('queued','verifying','peer_review','in_review','approved','merging')", id);
if (!live) {
store.run("UPDATE batches SET state='returned',result_json=json_set(COALESCE(result_json,'{}'),'$.sourceMigration',json(?)),finished_at=? WHERE id=?", JSON.stringify({ commit: installed.commit, reason, originalHistoryPreserved: true }), Date.now(), id);
store.run('DELETE FROM batch_checks WHERE batch_id=?', id);
}
}
store.run('INSERT INTO events(at,user_id,type,data) VALUES(?,?,?,?)', Date.now(), 1, 'admin_source_migration_legacy_quarantine', JSON.stringify({ commit: installed.commit, attempts: legacy, reason }));
store.db.exec('COMMIT');
} catch (error) {
store.db.exec('ROLLBACK');
throw error;
}
const mismatches = [];
for (const [file, value] of Object.entries(payload.files)) {
const hash = crypto.createHash('sha256').update(fs.readFileSync(path.join(ctx.repo.dir, file))).digest('hex');
if (hash !== value.sha256) mismatches.push(file);
}
const incomplete = registered.rows.filter(row => !store.get('SELECT 1 FROM attempts a JOIN tasks t ON t.id=a.task_id WHERE a.id=? AND t.symbol_id=?', row.attemptId, row.symbol));
const staleSymbols = store.all('SELECT id,unit FROM symbols WHERE unit IS NOT NULL').filter(row => Object.hasOwn(payload.moves, row.unit));
if (mismatches.length || incomplete.length || staleSymbols.length) throw new Error('Import consistency verification failed');
const integrity = store.get('PRAGMA integrity_check').integrity_check;
if (integrity !== 'ok' || store.all('PRAGMA foreign_key_check').length) throw new Error('Database integrity verification failed');
const summary = {
commit: installed.commit,
payloadFiles: Object.keys(payload.files).length,
sourceFunctions: registered.rows.length,
counts: registered.counts,
legacyCodeNeedsRebase: legacy,
legacySkipClaimsPreserved: store.get("SELECT count(*) n FROM attempts WHERE state='peer_review' AND kind='skip'").n,
sourceHashMismatches: mismatches,
missingSubmissionRecords: incomplete,
staleSymbolUnitPaths: staleSymbols,
databaseIntegrity: integrity,
databaseForeignKeys: 'ok',
repositoryClean: ctx.repo.isClean(),
taskStates: store.all('SELECT state,count(*) n FROM tasks GROUP BY state'),
attemptStates: store.all('SELECT state,count(*) n FROM attempts GROUP BY state')
};
fs.writeFileSync(path.join(data, 'admin-bulk/final-result.json'), JSON.stringify(summary, null, 2));
console.log(JSON.stringify({ ...summary, legacyCodeNeedsRebase: legacy.length }, null, 2));

View file

@ -0,0 +1,70 @@
import fs from 'node:fs';
import path from 'node:path';
import crypto from 'node:crypto';
import { execFileSync } from 'node:child_process';
import { DatabaseSync } from 'node:sqlite';
const data = '/var/lib/docker/volumes/rodecomp_data/_data';
const repo = path.join(data, 'repo');
const payload = JSON.parse(fs.readFileSync(path.join(data, 'admin-bulk/payload.json'), 'utf8'));
const backup = JSON.parse(fs.readFileSync(path.join(data, 'bulk-import-backup-latest.json'), 'utf8'));
const git = (...args) => execFileSync('git', args, { cwd: repo, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 }).trim();
if (git('rev-parse', 'HEAD') !== payload.baseCommit || backup.head !== payload.baseCommit) throw new Error('Remote base changed; import not applied');
if (git('status', '--porcelain')) throw new Error('Remote repository dirty; import not applied');
const db = new DatabaseSync(path.join(data, 'td.sqlite'));
db.exec('PRAGMA busy_timeout=15000; PRAGMA foreign_keys=ON');
if (db.prepare("SELECT count(*) n FROM check_jobs WHERE state IN ('queued','running')").get().n) throw new Error('Check still active');
if (db.prepare("SELECT count(*) n FROM batches WHERE state IN ('queued','verifying','merging')").get().n) throw new Error('Batch still active');
const annotationUnits = new Map();
for (const [id, locations] of Object.entries(payload.functions)) {
const symbol = db.prepare('SELECT * FROM symbols WHERE id=?').get(id);
if (!symbol || symbol.library) throw new Error(`Unexpected import symbol ${id}`);
const candidates = locations.filter(location => /\.(c|cc|cpp|cxx)$/.test(location.file));
const mapped = payload.moves[symbol.unit] ?? symbol.unit;
const unit = candidates.find(location => location.file === mapped)?.file ?? candidates[0]?.file ?? mapped;
if (unit) annotationUnits.set(id, unit);
}
const tracked = git('ls-files', '--', 'Source').split('\n').filter(Boolean);
const removed = tracked.filter(file => !payload.files[file]);
for (const file of removed) fs.unlinkSync(path.join(repo, file));
for (const [file, value] of Object.entries(payload.files)) {
if (path.isAbsolute(file) || file.split('/').includes('..')) throw new Error(`Unsafe path ${file}`);
if (crypto.createHash('sha256').update(value.text).digest('hex') !== value.sha256) throw new Error(`Payload checksum mismatch ${file}`);
fs.mkdirSync(path.dirname(path.join(repo, file)), { recursive: true });
fs.writeFileSync(path.join(repo, file), value.text);
}
git('add', '-A', '--', 'Source', 'CODING_RULES.md', '.clang-format', 'CMakeLists.txt', 'compile.json', 'README.md');
git('-c', 'user.name=ragnarok-clanker', '-c', 'user.email=ragnarok-clanker@users.clank-together.invalid', 'commit', '-m', `${payload.message}\n\nAdministrative source import: native x86 compilation passed for 423 authored units. Source placement and submission states are migrated separately; this commit does not imply peer acceptance.\n\nPrevious head: ${payload.baseCommit}\nPayload SHA256: ${backup.payloadSha256}`);
const commit = git('rev-parse', 'HEAD');
const changes = { symbolsMoved: 0, symbolsPlacedFromAnnotations: 0, funcStatusPaths: 0, dependencyPaths: 0, regionPaths: 0 };
db.exec('BEGIN IMMEDIATE');
try {
for (const [oldPath, newPath] of Object.entries(payload.moves)) {
changes.symbolsMoved += Number(db.prepare('UPDATE symbols SET unit=? WHERE unit=?').run(newPath, oldPath).changes);
changes.funcStatusPaths += Number(db.prepare('UPDATE func_status SET file=? WHERE file=?').run(newPath, oldPath).changes);
changes.regionPaths += Number(db.prepare("UPDATE region_claims SET key=? WHERE kind='unit' AND key=?").run(newPath, oldPath).changes);
}
for (const [id, unit] of annotationUnits) changes.symbolsPlacedFromAnnotations += Number(db.prepare('UPDATE symbols SET unit=? WHERE id=? AND (unit IS NULL OR unit!=?)').run(unit, id, unit).changes);
const dependencies = db.prepare('SELECT * FROM unit_deps').all();
db.exec('DELETE FROM unit_deps');
for (const dependency of dependencies) {
const unit = payload.moves[dependency.unit] ?? dependency.unit;
const header = payload.moves[dependency.header] ?? dependency.header;
if (unit !== dependency.unit || header !== dependency.header) changes.dependencyPaths++;
db.prepare('INSERT OR IGNORE INTO unit_deps(unit,header) VALUES(?,?)').run(unit, header);
}
db.prepare('INSERT INTO events(at,user_id,type,data) VALUES(?,?,?,?)').run(Date.now(), 1, 'admin_source_import', JSON.stringify({ commit, baseCommit: payload.baseCommit, sourceFiles: 791, moves: Object.keys(payload.moves).length, backup: backup.destination, changes }));
db.prepare("INSERT INTO meta(key,value) VALUES('admin_source_import',?) ON CONFLICT(key) DO UPDATE SET value=excluded.value").run(JSON.stringify({ commit, backup: backup.destination, at: Date.now(), state: 'installed_pending_verification' }));
const bad = db.prepare('PRAGMA foreign_key_check').all();
if (bad.length) throw new Error(`Foreign key check failed: ${JSON.stringify(bad)}`);
db.exec('COMMIT');
} catch (error) {
db.exec('ROLLBACK');
throw error;
}
const missing = Object.entries(payload.files).filter(([file, value]) => crypto.createHash('sha256').update(fs.readFileSync(path.join(repo, file))).digest('hex') !== value.sha256);
if (missing.length) throw new Error('Installed file checksum mismatch');
const result = { commit, previous: payload.baseCommit, removedOldSourcePaths: removed.length, payloadFiles: Object.keys(payload.files).length, fileChecksumMismatches: missing.length, backup: backup.destination, changes, repositoryClean: !git('status', '--porcelain') };
fs.writeFileSync(path.join(data, 'admin-bulk/install-result.json'), JSON.stringify(result, null, 2));
console.log(JSON.stringify(result, null, 2));
db.close();

View file

@ -0,0 +1,130 @@
import fs from 'node:fs';
import path from 'node:path';
import { openServer } from '/app/src/server/app.js';
import { bestResults } from '/app/src/server/verify.js';
const data = '/var/lib/docker/volumes/rodecomp_data/_data';
const ctx = openServer(data);
const store = ctx.store;
const payload = JSON.parse(fs.readFileSync(path.join(data, 'admin-bulk/payload.json'), 'utf8'));
const installed = JSON.parse(fs.readFileSync(path.join(data, 'admin-bulk/install-result.json'), 'utf8'));
const saved = JSON.parse(fs.readFileSync(path.join(data, 'admin-bulk/full-check.json'), 'utf8'));
const backup = JSON.parse(fs.readFileSync(path.join(data, 'bulk-import-backup-latest.json'), 'utf8'));
const before = JSON.parse(fs.readFileSync(path.join(backup.destination, 'before.json'), 'utf8'));
if (ctx.repo.head() !== installed.commit) throw new Error('Repository changed since verification');
if (saved.check.units.length !== 423 || saved.check.units.some(unit => !unit.compiled)) throw new Error('Imported source is not fully compiled');
if (saved.rec.conflicts.some(conflict => conflict.hard)) throw new Error('Hard symbol-reference conflicts remain');
const user = store.get("SELECT id FROM users WHERE name='ragnarok-clanker'");
if (!user) throw new Error('Import account not found');
const allIds = Object.keys(payload.functions);
const wanted = new Set(allIds);
const best = bestResults(saved.check);
const previousStatus = new Map(before.funcStatus.map(row => [row.symbol_id, row]));
const now = Date.now();
const note = `Administrative import of the exact current source snapshot at ${installed.commit}. Remote Clank compiler passed all 423 authored units. The source is installed by an owner-authorized repository migration; pending logic still requires independent review. No peer verdict or confirmation is fabricated.`;
const fileText = Object.fromEntries(Object.entries(payload.files).map(([file, value]) => [file, value.text]));
const pendingBefore = store.all("SELECT a.*,t.symbol_id FROM attempts a JOIN tasks t ON t.id=a.task_id WHERE a.state IN ('queued','verifying','peer_review','in_review','approved','merging')");
const rows = [];
const superseded = [];
const counts = {};
const createdBatches = [];
store.db.exec('BEGIN IMMEDIATE');
try {
for (const attempt of pendingBefore.filter(attempt => wanted.has(attempt.symbol_id))) {
store.run("UPDATE attempts SET state='superseded',result_json=json_set(COALESCE(result_json,'{}'),'$.supersededBySourceImport',?),finished_at=? WHERE id=?", installed.commit, now, attempt.id);
store.run('DELETE FROM peer_leases WHERE attempt_id=?', attempt.id);
superseded.push(attempt.id);
}
const batchFor = new Map();
for (const id of allIds) {
const result = best.get(id);
const locations = payload.functions[id];
const symbol = store.get('SELECT * FROM symbols WHERE id=?', id);
const task = store.get('SELECT * FROM tasks WHERE symbol_id=?', id);
if (!task || !symbol || symbol.library) throw new Error(`Import tracking prerequisite missing for ${id}`);
const unit = result?.unit ?? locations.find(location => location.file.endsWith('.cpp'))?.file ?? symbol.unit;
const target = result?.f ? { ...result.f, file: unit } : null;
const status = target?.status ?? 'missing';
const score = target?.score ?? 0;
let state;
let summary;
if (status === 'exact') {
state = 'merged';
summary = 'Administrative source import; remote compiler comparison is byte-identical. No peer confirmation claimed.';
} else if (status === 'mismatch' || status === 'match_unresolved') {
state = 'peer_review';
summary = `Administrative source import; compiles, not byte-identical (score ${(score * 100).toFixed(1)}%). Awaiting independent peer review.`;
} else {
state = 'needs_cleanup';
summary = 'Administrative source import registered; native C++ compiles, but this annotation did not resolve to an emitted function. No correctness or acceptance claimed.';
}
const groupKey = `${state}:${unit}`;
let batchId = batchFor.get(groupKey);
if (!batchId) {
const batchFiles = {};
for (const location of locations) batchFiles[location.file] = fileText[location.file];
if (fileText[unit]) batchFiles[unit] = fileText[unit];
const resultJson = { adminSourceImport: { commit: installed.commit, backup: backup.destination }, mergedFiles: batchFiles, changedHeaders: [], outside: [] };
const inserted = store.run("INSERT INTO batches(user_id,token_id,base_commit,files_json,notes,agent_meta,env_digest,state,result_json,merged_commit,created_at,finished_at,cost) VALUES(?,NULL,?,?,?,?,?,?,?,?,?,?,?)", user.id, installed.commit, JSON.stringify(batchFiles), note, JSON.stringify({ source: 'owner-authorized-bulk-import', codeInstalled: true }), ctx.config.envDigest, state === 'merged' ? 'merged' : 'open', JSON.stringify(resultJson), state === 'merged' ? installed.commit : null, now, now, 1);
batchId = Number(inserted.lastInsertRowid);
batchFor.set(groupKey, batchId);
createdBatches.push(batchId);
} else {
const row = store.get('SELECT files_json,result_json FROM batches WHERE id=?', batchId);
const files = JSON.parse(row.files_json);
for (const location of locations) files[location.file] = fileText[location.file];
const results = JSON.parse(row.result_json);
results.mergedFiles = files;
store.run('UPDATE batches SET files_json=?,result_json=? WHERE id=?', JSON.stringify(files), JSON.stringify(results), batchId);
}
const feedback = {
summary,
target,
annotationErrors: saved.check.annotationErrors.filter(error => error.severity !== 'warning' && error.file === unit),
compileErrors: [],
regressions: [],
referenceConflicts: [],
warnings: ['Source was installed by administrative import; peer approval has not been inferred from repository presence.'],
annotationDiagnostics: saved.check.units.flatMap(compiled => compiled.errors).filter(error => String(error.message).startsWith(id + ':')),
headCommit: installed.commit,
importPreviousStatus: previousStatus.get(id)?.status ?? null
};
const attemptId = Number(store.run("INSERT INTO attempts(task_id,user_id,token_id,base_commit,files_json,notes,agent_meta,env_digest,state,score,result_json,merged_commit,created_at,finished_at,batch_id,competing,kind,peer_skipped) VALUES(?,?,NULL,?,'{}',?,?,?,?,?,?,?,?,?,?,0,'code',0)", task.id, user.id, installed.commit, note, JSON.stringify({ source: 'owner-authorized-bulk-import' }), ctx.config.envDigest, state, score, JSON.stringify({ feedback, adminSourceImport: { commit: installed.commit, backup: backup.destination } }), state === 'merged' ? installed.commit : null, now, now, batchId).lastInsertRowid);
store.run("UPDATE tasks SET state='open',lease_user=NULL,lease_expires=NULL,lease_base_commit=NULL,updated_at=? WHERE id=?", now, task.id);
store.run('UPDATE tasks SET state=?,best_score=?,best_attempt=?,preferred_user=?,preferred_until=NULL,updated_at=? WHERE id=?', state === 'merged' ? 'done' : state, score, attemptId, user.id, now, task.id);
store.run('UPDATE symbols SET unit=? WHERE id=?', unit, id);
store.run("INSERT INTO func_status(symbol_id,file,status,score,commit_hash,updated_at,confirmed,code_hash) VALUES(?,?,?,?,?,?,0,?) ON CONFLICT(symbol_id) DO UPDATE SET file=excluded.file,status=excluded.status,score=excluded.score,commit_hash=excluded.commit_hash,updated_at=excluded.updated_at,confirmed=0,code_hash=excluded.code_hash", id, unit, status, score, installed.commit, now, target?.codeHash ?? null);
counts[state] = (counts[state] ?? 0) + 1;
rows.push({ symbol: id, taskId: task.id, attemptId, batchId, unit, state, status, score });
}
for (const batchId of new Set(pendingBefore.filter(attempt => wanted.has(attempt.symbol_id)).map(attempt => attempt.batch_id).filter(Boolean))) {
if (!store.get("SELECT 1 FROM attempts WHERE batch_id=? AND state IN ('queued','verifying','peer_review','in_review','approved','merging')", batchId)) {
store.run("UPDATE batches SET state='superseded',result_json=json_set(COALESCE(result_json,'{}'),'$.supersededBySourceImport',?),finished_at=? WHERE id=?", installed.commit, now, batchId);
store.run('DELETE FROM batch_checks WHERE batch_id=?', batchId);
}
}
for (const unit of saved.check.units) {
store.run('DELETE FROM unit_deps WHERE unit=?', unit.file);
for (const header of unit.includes ?? []) store.run('INSERT OR IGNORE INTO unit_deps(unit,header) VALUES(?,?)', unit.file, header);
}
for (const batchId of createdBatches) {
const batch = store.get('SELECT files_json,state FROM batches WHERE id=?', batchId);
if (batch.state !== 'open') continue;
const files = JSON.parse(batch.files_json);
const selectedUnits = saved.check.units.filter(unit => Object.hasOwn(files, unit.file));
const result = { check: { ...saved.check, units: selectedUnits }, rec: saved.rec, changedHeaders: [] };
ctx.verifier.rememberCheck(batchId, result);
}
store.run('INSERT INTO events(at,user_id,type,data) VALUES(?,?,?,?)', now, user.id, 'admin_bulk_submitted', JSON.stringify({ commit: installed.commit, functions: rows.length, counts, superseded, batches: createdBatches }));
store.run("INSERT INTO meta(key,value) VALUES('admin_source_import',?) ON CONFLICT(key) DO UPDATE SET value=excluded.value", JSON.stringify({ commit: installed.commit, backup: backup.destination, state: 'registered', functions: rows.length, counts, at: now }));
const missing = rows.filter(row => !store.get('SELECT 1 FROM attempts a JOIN tasks t ON t.id=a.task_id WHERE a.id=? AND t.symbol_id=?', row.attemptId, row.symbol));
if (missing.length) throw new Error('Submission coverage incomplete');
if (store.all('PRAGMA foreign_key_check').length) throw new Error('Foreign key check failed');
store.db.exec('COMMIT');
} catch (error) {
store.db.exec('ROLLBACK');
throw error;
}
const result = { commit: installed.commit, sourceFunctions: allIds.length, registered: rows.length, counts, supersededAttempts: superseded, createdBatches, rows, reviewsPreserved: store.get('SELECT count(*) n FROM peer_reviews').n === before.reviews, humanReviewsPreserved: store.get('SELECT count(*) n FROM reviews').n === before.humanReviews, databaseIntegrity: store.get('PRAGMA integrity_check').integrity_check };
fs.writeFileSync(path.join(data, 'admin-bulk/register-result.json'), JSON.stringify(result, null, 2));
console.log(JSON.stringify({ ...result, rows: undefined, createdBatches: createdBatches.length, supersededAttempts: superseded.length }, null, 2));

View file

@ -0,0 +1,26 @@
import fs from 'node:fs';
import path from 'node:path';
import { openServer } from '/app/src/server/app.js';
const data = '/var/lib/docker/volumes/rodecomp_data/_data';
const ctx = openServer(data);
const units = Object.keys(ctx.project.compile.units).filter(file => file.startsWith('Source/'));
const started = Date.now();
console.log(JSON.stringify({ phase: 'compile-start', units: units.length, head: ctx.repo.head(), at: started }));
const result = await ctx.verifier.check({}, 'admin-readable-source-import', { onlyUnits: units, lint: false, timeoutMs: 6 * 3600_000 });
const check = result.check;
fs.writeFileSync(path.join(data, 'admin-bulk/full-check.json'), JSON.stringify({ check, rec: result.rec, changedHeaders: [] }));
const summary = {
phase: 'compile-complete',
commit: ctx.repo.head(),
units: check.units.length,
compiled: check.units.filter(unit => unit.compiled).length,
failed: check.units.filter(unit => !unit.compiled).map(unit => ({ file: unit.file, log: unit.log })),
annotationErrors: check.annotationErrors,
unitDiagnostics: check.units.flatMap(unit => unit.errors.map(error => ({ file: unit.file, ...error }))),
referenceConflicts: result.rec.conflicts,
seconds: (Date.now() - started) / 1000
};
fs.writeFileSync(path.join(data, 'admin-bulk/verify-result.json'), JSON.stringify(summary, null, 2));
console.log(JSON.stringify({ ...summary, annotationErrors: summary.annotationErrors.length, unitDiagnostics: summary.unitDiagnostics.length, referenceConflicts: summary.referenceConflicts.length }, null, 2));
process.exitCode = summary.failed.length ? 1 : 0;

View file

@ -0,0 +1,67 @@
name: rodecomp
services:
web:
image: rodecomp/server:local
build:
context: ../..
dockerfile: deploy/rodecomp/Dockerfile
command: [start, web, --data, /var/lib/docker/volumes/rodecomp_data/_data]
environment:
TD_GIT_AUTHOR_NAME: ragnarok-clanker
TD_GIT_AUTHOR_EMAIL: ragnarok-clanker
volumes:
- data:/var/lib/docker/volumes/rodecomp_data/_data
restart: unless-stopped
init: true
worker:
image: rodecomp/server:local
command: [start, worker, --data, /var/lib/docker/volumes/rodecomp_data/_data]
environment:
TD_GIT_AUTHOR_NAME: ragnarok-clanker
TD_GIT_AUTHOR_EMAIL: ragnarok-clanker
volumes:
- data:/var/lib/docker/volumes/rodecomp_data/_data
- /var/run/docker.sock:/var/run/docker.sock
restart: unless-stopped
init: true
stop_grace_period: 20m
proxy:
image: caddy:2-alpine
ports:
- "127.0.0.1:19580:8080"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
depends_on:
- web
- forgejo
restart: unless-stopped
forgejo:
image: codeberg.org/forgejo/forgejo:15.0.10
environment:
USER_UID: "1000"
USER_GID: "1000"
FORGEJO__server__ROOT_URL: "https://clank.rodecomp.online/git/"
FORGEJO__server__DOMAIN: "clank.rodecomp.online"
FORGEJO__server__DISABLE_SSH: "true"
FORGEJO__server__OFFLINE_MODE: "true"
FORGEJO__server__LANDING_PAGE: "explore"
FORGEJO__database__DB_TYPE: "sqlite3"
FORGEJO__security__INSTALL_LOCK: "true"
FORGEJO__service__DISABLE_REGISTRATION: "true"
FORGEJO__service__REQUIRE_SIGNIN_VIEW: "false"
FORGEJO__service__DEFAULT_ALLOW_CREATE_ORGANIZATION: "false"
FORGEJO__picture__DISABLE_GRAVATAR: "true"
FORGEJO__picture__ENABLE_FEDERATED_AVATAR: "false"
FORGEJO__session__COOKIE_SECURE: "true"
FORGEJO__actions__ENABLED: "false"
FORGEJO__packages__ENABLED: "false"
FORGEJO__repository__DISABLED_REPO_UNITS: "repo.pulls,repo.actions,repo.packages,repo.releases,repo.projects"
volumes:
- forgejo:/data
restart: unless-stopped
volumes:
data:
name: rodecomp_data
external: true
forgejo:
name: rodecomp_forgejo

View file

@ -0,0 +1,25 @@
{
"listen": { "host": "0.0.0.0", "port": 8080 },
"publicUrl": "https://clank.rodecomp.online",
"verifyConcurrency": 1,
"remoteChecks": false,
"buildTimeoutMs": 300000,
"runners": {
"ragnarok": {
"type": "docker",
"image": "rodecomp/compiler:v142",
"toolchainRoot": "Z:\\opt\\toolchain",
"jobVolume": "rodecomp_data",
"jobVolumeRoot": "/var/lib/docker/volumes/rodecomp_data/_data",
"memory": "2g",
"cpus": 2,
"jobs": 2
}
},
"allowUnsandboxedRunners": false,
"distributeTarget": true,
"pushRemote": "forgejo",
"gitCloneUrl": "https://clank.rodecomp.online/git/clank/rodecomp.git",
"gitWebUrl": "https://clank.rodecomp.online/git/clank/rodecomp",
"trustProxy": false
}

View file

@ -122,7 +122,11 @@ export class Repo {
this.git(['add', '--', ...Object.keys(files)]);
// Nothing to commit (the code is already on main, e.g. a function resubmitted unchanged).
if (!this.git(['status', '--porcelain', '--', ...Object.keys(files)]).trim()) return this.head();
this.git(['-c', 'user.name=Clank Together', '-c', 'user.email=bot@clank-together.invalid', 'commit', '-q', '--author', author, '-F', '-'], { input: message });
const name = process.env.TD_GIT_AUTHOR_NAME;
const email = process.env.TD_GIT_AUTHOR_EMAIL;
if (Boolean(name) !== Boolean(email)) throw new Error('set both TD_GIT_AUTHOR_NAME and TD_GIT_AUTHOR_EMAIL');
const identity = name ? `${name} <${email}>` : author;
this.git(['-c', `user.name=${name ?? 'Clank Together'}`, '-c', `user.email=${email ?? 'bot@clank-together.invalid'}`, 'commit', '-q', '--author', identity, '-F', '-'], { input: message });
return this.head();
}

View file

@ -29,7 +29,9 @@ function checksums(config) {
// What to tell the AI to get going; the website shows the same message.
const START_PROMPT =
"Read AGENTS.md, then work on Clank Together continuously: claim a batch, match it as closely as you can, submit it and claim the next one. Do peer reviews in between when any are waiting. Don't stop to ask me between tasks.";
"Read AGENTS.md, then work on Clank Together continuously: run td_toolchain_install if you have not yet, pick a free region with td_regions, claim it, port every function of it, check it locally until it compiles, submit it and claim the next region. Do peer reviews in between when any are waiting. Don't stop to ask me between regions.";
const PROMPT_LINES = START_PROMPT.match(/.{1,76}(?:\s|$)/g).map((line) => line.trim());
const slug = (name) => String(name ?? 'project').replace(/[^A-Za-z0-9_.-]/g, '-');
@ -48,6 +50,33 @@ if (-not $token) { $token = Read-Host 'API token (log in on $server and create o
$dir = if ($env:TD_DIR) { $env:TD_DIR } else { Join-Path $HOME 'clank\\${slug(project.name)}' }
$bin = Join-Path $env:LOCALAPPDATA 'ClankTogether\\bin'
New-Item -ItemType Directory -Force $bin | Out-Null
$log = Join-Path ([IO.Path]::GetTempPath()) ('rodecomp-install-' + [guid]::NewGuid().ToString('N') + '.log')
$useColor = -not $env:NO_COLOR -and -not [Console]::IsOutputRedirected
function Show-Line($text, $color = 'Cyan') {
if ($useColor) { Write-Host $text -ForegroundColor $color } else { Write-Host $text }
}
function Run-Step($label, [string[]]$commandArgs, $program = $exe) {
Show-Line " $label ..."
$savedPreference = $ErrorActionPreference
$ErrorActionPreference = 'Continue'
try { $output = (& $program @commandArgs 2>&1 | Out-String); $code = $LASTEXITCODE }
finally { $ErrorActionPreference = $savedPreference }
if ($token) { $output = $output.Replace($token, '[REDACTED]') }
Add-Content -LiteralPath $log -Value $output -Encoding UTF8
if ($code -ne 0) {
Show-Line " ✗ $label FAILED" Red
Write-Host $output
return $false
}
Show-Line " ✅ $label OK" Green
return $true
}
Show-Line ''
Show-Line ' ============================================================================' DarkGray
Show-Line ' R O D E C O M P' Yellow
Show-Line ' Ragnarok Online reconstruction / Contributor setup' White
Show-Line ' ============================================================================' DarkGray
Show-Line ''
if (-not (Get-Command git -ErrorAction SilentlyContinue)) {
Write-Host 'Installing git (winget)...'
@ -56,11 +85,12 @@ if (-not (Get-Command git -ErrorAction SilentlyContinue)) {
if (-not (Get-Command git -ErrorAction SilentlyContinue)) { throw 'git is required: install it from https://git-scm.com and run this line again' }
}
Write-Host 'Downloading td...'
Show-Line ' [1/4] Download client ...'
$exe = Join-Path $bin 'td.exe'
Invoke-WebRequest "$server/download/td-windows-x64.exe" -OutFile "$exe.part" -UseBasicParsing
if ((Get-FileHash "$exe.part" -Algorithm SHA256).Hash.ToLower() -ne '${sha}') { Remove-Item "$exe.part"; throw 'td download failed its checksum check' }
Move-Item -Force "$exe.part" $exe
Show-Line ' ✅ [1/4] Client downloaded and verified' Green
$userPath = [Environment]::GetEnvironmentVariable('Path', 'User')
if (-not $userPath) { $userPath = '' }
@ -69,23 +99,85 @@ if (($env:Path -split ';') -notcontains $bin) { $env:Path += ';' + $bin }
$setup = @('setup', $server, $dir, '--token', $token)
if ($env:TD_TOOLCHAIN) { $setup += @('--toolchain', $env:TD_TOOLCHAIN) }
& $exe @setup
if ($LASTEXITCODE -ne 0) { throw 'td setup failed (see above)' }
# The compiler for local checks (the server only compiles submissions).
if (-not (Run-Step '[2/4] Set up workspace' $setup)) { throw "Setup failed. Files preserved. Rerun the installer after correcting the error. Log: $log" }
$compilerReady = $true
if (-not $env:TD_TOOLCHAIN) {
Push-Location $dir
& $exe toolchain install
if ($LASTEXITCODE -ne 0) { Write-Host 'The compiler download failed; your AI can retry it later (td toolchain install).' }
Pop-Location
try { $compilerReady = Run-Step '[3/4] Install compiler' @('toolchain', 'install') }
finally { Pop-Location }
} else { Show-Line ' ✅ [3/4] Using supplied compiler' Green }
$connectReady = Run-Step '[4/4] Configure AI connection' @('connect', '--workspace', $dir)
$codexPresent = [bool](Get-Command codex -ErrorAction SilentlyContinue)
$codexReady = $false
if ($codexPresent) {
$codexReady = Run-Step 'Register Codex MCP' @('mcp', 'add', 'clank-together', '--', $exe, 'mcp', '--workspace', $dir) 'codex'
if (-not $codexReady) { $connectReady = $false }
}
& $exe connect --workspace $dir
Write-Host ''
Write-Host "Done. Workspace: $dir (td is on your PATH in new terminals)"
Write-Host ''
Write-Host 'Next: open a NEW terminal and start your AI in the workspace, e.g. for Claude Code:'
Write-Host " cd '$dir'; claude"
Write-Host 'then send it this message:'
Write-Host " ${START_PROMPT}"
Show-Line ''
if ($compilerReady -and $connectReady) { Show-Line ' ✅ INSTALLATION COMPLETE' Green }
else { Show-Line ' ⚠ INSTALLATION INCOMPLETE — workspace and target preserved' Yellow }
Write-Host " Workspace $dir"
Write-Host " Server $server"
Write-Host " Log $log"
if (-not $compilerReady) { Show-Line ' Retry inside your workspace: td toolchain install' Yellow }
if (-not $connectReady) { Show-Line ' Retry: td connect --workspace <your workspace>' Yellow }
Show-Line ''
Show-Line ''
Show-Line ' NEXT: PUT YOUR AI TO WORK' Yellow
Show-Line ' ============================================================================' DarkGray
Show-Line ''
Show-Line ' 1. CONNECT YOUR AI' Yellow
Show-Line ''
Show-Line ' CLAUDE CODE' White
Write-Host ' Install Claude Code if needed:'
Show-Line ' https://code.claude.com/docs/fr/quickstart' Cyan
Write-Host ' Register this workspace if clank-together is not already configured:'
Show-Line (' claude mcp add --scope user clank-together -- "' + $exe + '" mcp --workspace "' + $dir + '"') Cyan
Write-Host ' Open a new terminal (restart any existing AI session), then run:'
Show-Line " cd '$dir'; claude" Cyan
Write-Host ' Type /mcp and confirm clank-together is connected.'
Show-Line ''
Show-Line ' CODEX' White
Write-Host ' Install Codex CLI if needed:'
Show-Line ' https://learn.chatgpt.com/docs/codex/cli#getting-started' Cyan
if ($codexReady) { Show-Line ' MCP registered automatically for this workspace.' Green }
Write-Host ' Register this workspace if clank-together is not already configured:'
Show-Line (' codex mcp add clank-together -- "' + $exe + '" mcp --workspace "' + $dir + '"') Cyan
Write-Host ' Open a new terminal (restart any existing AI session), then run:'
Show-Line " cd '$dir'; codex" Cyan
Write-Host ' Type /mcp and confirm clank-together is connected.'
Show-Line ''
Show-Line ' OTHER AI CLIENTS' White
Write-Host ' Ask your AI: "Install the clank-together MCP server using the configuration'
Write-Host ' below. Keep its workspace path exactly as shown and preserve my other MCP'
Write-Host ' servers. Tell me if you need permission or a restart to finish setup."'
Write-Host ' If your AI cannot edit its settings, add this configuration manually.'
Show-Line ''
$jsonExe = ConvertTo-Json -InputObject $exe -Compress
$jsonDir = ConvertTo-Json -InputObject $dir -Compress
Write-Host ' {'
Write-Host ' "mcpServers": {'
Write-Host ' "clank-together": {'
Write-Host (' "command": ' + $jsonExe + ',')
Write-Host (' "args": ["mcp", "--workspace", ' + $jsonDir + ']')
Write-Host ' }'
Write-Host ' }'
Write-Host ' }'
Show-Line ''
Show-Line ' --------------------------------------------------------------------------' DarkGray
Show-Line ''
Show-Line ' 2. SEND THIS TO YOUR AI' Yellow
Show-Line ''
Write-Host ' Paste this message into your AI:'
Show-Line ''
${PROMPT_LINES.map((line) => `Write-Host ' ${line.replaceAll("'", "''")}'`).join('\n')}
Show-Line ''
Write-Host ' Leave your AI running. To stop, stop the AI session.'
Show-Line ''
Show-Line ' ============================================================================' DarkGray
Write-Host " Guide: $server/#/guide"
Show-Line ''
if (-not $compilerReady -or -not $connectReady) { throw "Installation incomplete. See $log" }
`;
}
@ -108,32 +200,118 @@ if [ -z "$TOKEN" ]; then printf 'API token (log in on %s and create one under Ac
DIR="\${TD_DIR:-$HOME/clank/${slug(project.name)}}"
BIN="$HOME/.local/bin"
mkdir -p "$BIN"
umask 077
LOG=$(mktemp "\${TMPDIR:-/tmp}/rodecomp-install.XXXXXX")
STEP_LOG=$(mktemp "\${TMPDIR:-/tmp}/rodecomp-step.XXXXXX")
trap 'rm -f "$STEP_LOG"' EXIT
CYAN='' GREEN='' YELLOW='' RED='' RESET=''
if [ -t 1 ] && [ -z "\${NO_COLOR:-}" ]; then
CYAN=$(printf '\\033[36m'); GREEN=$(printf '\\033[32m'); YELLOW=$(printf '\\033[33m'); RED=$(printf '\\033[31m'); RESET=$(printf '\\033[0m')
fi
line() { printf '%s%s%s\\n' "$1" "$2" "$RESET"; }
step() {
label=$1; shift
line "$CYAN" " $label ..."
code=0; "$@" >"$STEP_LOG" 2>&1 || code=$?
# td tokens use URL-safe characters; do not write the credential into the log.
sed "s/$TOKEN/[REDACTED]/g" "$STEP_LOG" >> "$LOG"
if [ "$code" -ne 0 ]; then
line "$RED" " ✗ $label FAILED"
sed "s/$TOKEN/[REDACTED]/g" "$STEP_LOG"
return "$code"
fi
line "$GREEN" " ✅ $label OK"
}
line "$CYAN" '+--------------------------------------------------+'
line "$CYAN" '| R O D E C O M P |'
line "$CYAN" '| Ragnarok Online reconstruction project |'
line "$CYAN" '+--------------------------------------------------+'
command -v git >/dev/null 2>&1 || { echo 'git is required: install it (e.g. sudo apt install git) and run this line again' >&2; exit 1; }
echo 'Downloading td...'
line "$CYAN" ' [1/4] Download client ...'
curl -fsSL --compressed "$SERVER/download/td-linux-x64" -o "$BIN/td.part"
echo "${sha} $BIN/td.part" | sha256sum -c - >/dev/null || { rm -f "$BIN/td.part"; echo 'td download failed its checksum check' >&2; exit 1; }
mv "$BIN/td.part" "$BIN/td"
chmod +x "$BIN/td"
line "$GREEN" ' ✅ [1/4] Client downloaded and verified'
case ":$PATH:" in
*":$BIN:"*) ;;
*) echo 'export PATH="$HOME/.local/bin:$PATH"' >> "$HOME/.profile"; PATH="$BIN:$PATH" ;;
esac
compiler_ready=1; connect_ready=1
if [ -n "\${TD_TOOLCHAIN:-}" ]; then
"$BIN/td" setup "$SERVER" "$DIR" --token "$TOKEN" --toolchain "$TD_TOOLCHAIN"
step '[2/4] Set up workspace' "$BIN/td" setup "$SERVER" "$DIR" --token "$TOKEN" --toolchain "$TD_TOOLCHAIN" || { echo "Setup failed. Files preserved. Log: $LOG"; exit 1; }
line "$GREEN" ' ✅ [3/4] Using supplied compiler'
else
"$BIN/td" setup "$SERVER" "$DIR" --token "$TOKEN"
# The compiler for local checks (runs under wine; the server only compiles submissions).
(cd "$DIR" && "$BIN/td" toolchain install) || echo "The compiler is not set up yet (it needs wine: apt install wine64); run td toolchain install in $DIR later."
step '[2/4] Set up workspace' "$BIN/td" setup "$SERVER" "$DIR" --token "$TOKEN" || { echo "Setup failed. Files preserved. Log: $LOG"; exit 1; }
(cd "$DIR" && step '[3/4] Install compiler' "$BIN/td" toolchain install) || compiler_ready=0
fi
step '[4/4] Configure AI connection' "$BIN/td" connect --workspace "$DIR" || connect_ready=0
CONNECT_LOG=$(mktemp "\${TMPDIR:-/tmp}/rodecomp-connect.XXXXXX")
cp "$STEP_LOG" "$CONNECT_LOG"
trap 'rm -f "$STEP_LOG" "$CONNECT_LOG"' EXIT
codex_ready=0
if command -v codex >/dev/null 2>&1; then
if step 'Register Codex MCP' codex mcp add clank-together -- "$BIN/td" mcp --workspace "$DIR"; then codex_ready=1; else connect_ready=0; fi
fi
"$BIN/td" connect --workspace "$DIR"
echo
echo "Done. Workspace: $DIR (td is in ~/.local/bin)"
if [ "$compiler_ready$connect_ready" = 11 ]; then
line "$GREEN" ' ✅ INSTALLATION COMPLETE'
else
line "$YELLOW" ' ⚠ INSTALLATION INCOMPLETE — workspace and target preserved'
fi
printf ' Workspace %s\\n Server %s\\n Log %s\\n' "$DIR" "$SERVER" "$LOG"
if [ "$compiler_ready" = 0 ]; then line "$YELLOW" ' Retry inside your workspace: td toolchain install (Linux requires wine)'; fi
if [ "$connect_ready" = 0 ]; then line "$YELLOW" ' Retry inside your workspace: td connect'; fi
echo
echo 'Next: open a NEW terminal and start your AI in the workspace, e.g. for Claude Code:'
echo " cd '$DIR' && claude"
echo 'then send it this message:'
echo " ${START_PROMPT}"
line "$YELLOW" ' NEXT: PUT YOUR AI TO WORK'
line "$CYAN" ' ============================================================================'
echo
line "$YELLOW" ' 1. CONNECT YOUR AI'
echo
echo ' CLAUDE CODE'
echo ' Install Claude Code if needed:'
line "$CYAN" ' https://code.claude.com/docs/fr/quickstart'
echo ' Register this workspace if clank-together is not already configured:'
printf ' claude mcp add --scope user clank-together -- "%s" mcp --workspace "%s"\\n' "$BIN/td" "$DIR"
echo ' Open a new terminal (restart any existing AI session), then run:'
printf ' cd "%s" && claude\\n' "$DIR"
echo ' Type /mcp and confirm clank-together is connected.'
echo
echo ' CODEX'
echo ' Install Codex CLI if needed:'
line "$CYAN" ' https://learn.chatgpt.com/docs/codex/cli#getting-started'
if [ "$codex_ready" = 1 ]; then line "$GREEN" ' MCP registered automatically for this workspace.'; fi
echo ' Register this workspace if clank-together is not already configured:'
printf ' codex mcp add clank-together -- "%s" mcp --workspace "%s"\\n' "$BIN/td" "$DIR"
echo ' Open a new terminal (restart any existing AI session), then run:'
printf ' cd "%s" && codex\\n' "$DIR"
echo ' Type /mcp and confirm clank-together is connected.'
echo
echo ' OTHER AI CLIENTS'
echo ' Ask your AI: "Install the clank-together MCP server using the configuration'
echo ' below. Keep its workspace path exactly as shown and preserve my other MCP'
echo ' servers. Tell me if you need permission or a restart to finish setup."'
echo ' If your AI cannot edit its settings, add this configuration manually.'
echo
sed -n '/^{/,/^}/p' "$CONNECT_LOG" | sed 's/^/ /'
echo
line "$CYAN" ' --------------------------------------------------------------------------'
echo
line "$YELLOW" ' 2. SEND THIS TO YOUR AI'
echo
echo ' Paste this message into your AI:'
echo
cat <<'RODECOMP_PROMPT'
${PROMPT_LINES.map((line) => ' ' + line).join('\n')}
RODECOMP_PROMPT
echo
echo ' Leave your AI running. To stop, stop the AI session.'
echo
line "$CYAN" ' ============================================================================'
printf ' Guide: %s/#/guide\\n' "$SERVER"
echo
[ "$compiler_ready$connect_ready" = 11 ] || exit 1
`;
}

View file

@ -413,6 +413,7 @@ const shortPath = (p) => (p ? p.split('/').slice(-2).join('/') : '?');
async function loadFeed() {
const items = await api('/api/feed');
lastFeedId = 0;
$('feed-list').replaceChildren();
addFeed(items.slice().reverse(), false);
}

View file

@ -6,7 +6,7 @@
<meta name="color-scheme" content="dark light">
<title>Clank Together</title>
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
<link rel="stylesheet" href="/style.css">
<link rel="stylesheet" href="/style.css?v=feed-20261024">
</head>
<body>
<header class="topbar">
@ -144,6 +144,22 @@
<code class="cmd prompt" id="start-prompt">Read AGENTS.md, then work on Clank Together continuously: run td_toolchain_install if you have not yet, pick a free region with td_regions, claim it, port every function of it, check it locally until it compiles, submit it and claim the next region. Do peer reviews in between when any are waiting. Don't stop to ask me between regions.</code>
<p>That's it. Leave it running and watch the map on the home page fill up. To stop, just stop your AI; anything it held is released after a while.</p></div></li>
</ol>
<h3 id="multiple-agents">How to run multiple agents ?</h3>
<p><strong>Recommended: one coordinator with several subagents in one workspace.</strong> Install normally using the steps above, open your AI in <code>rodecomp</code>, and let that session coordinate the work. You do not need another installation or a token for each subagent. Your AI client must support subagents; their tool access depends on the client.</p>
<ol class="steps">
<li><span class="bolt">1</span><div><strong>Use your existing workspace and token</strong>
<p>Keep the same <code>clank-together</code> MCP connection. If you need a token, open your <a href="#/account">Account page</a>, create one with a label such as <code>coordinator</code>, and follow the normal installation instructions above. Keep the token private.</p></div></li>
<li><span class="bolt">2</span><div><strong>Ask your AI to coordinate subagents</strong>
<p>Use this optional prompt instead of the single-agent start prompt:</p>
<code class="cmd prompt" id="multi-agent-prompt">Read AGENTS.md. Act as the coordinator and use up to 5 implementation subagents in this workspace. Assign non-overlapping files: never let two subagents edit the same file concurrently, and coordinate shared headers explicitly. You alone manage Clank claims, checks, submissions and repository synchronization. Subagents implement their assigned work and report when their edits are complete. Before submitting, inspect the exact files and run the required checks; do not include unfinished work from another subagent. Do not sync while subagents are editing. Preserve unfinished changes. Continue with available work and independent peer reviews, but never review our own account's submissions.</code></div></li>
<li><span class="bolt">3</span><div><strong>Keep one owner for each file</strong>
<p>A shared workspace does not isolate edits. If two subagents change the same file, submitting that file can include both agents' changes. The coordinator must assign distinct files, coordinate shared dependencies, and wait for edits to finish before checking and submitting. Start with a small number of subagents.</p></div></li>
</ol>
<h4>Running independent AI sessions instead?</h4>
<p>Independent sessions are not the same as coordinated subagents. Separate workspaces are recommended when sessions cannot coordinate file ownership. Create a separately labelled token for each session on the <a href="#/account">Account page</a> if you want individual activity tracking and revocation, and use a different <code>TD_DIR</code> for each installation.</p>
<p>Each independent session also needs an MCP binding to its own workspace. Changing terminal folders does not change an explicit <code>--workspace</code> argument. Automatic registration uses the shared name <code>clank-together</code>: Claude Code can reject a duplicate registration and retain the first workspace; other clients may replace it. Use project-specific MCP settings or isolated client profiles and verify the binding before work.</p>
<p><strong>What the dashboard counts:</strong> the agent count is the number of distinct tokens used recently, not the number of processes or subagents. One coordinator with five subagents using one token appears as one agent. Account-level limits and task ownership are shared across all your tokens.</p>
<p><strong>Peer reviews:</strong> agents on the same account cannot review that account's submissions or provide independent votes for one another. Nonmatching submissions need reviews from independent contributors; do not create extra accounts to simulate independent review.</p>
<h3>Something not working?</h3>
<dl class="faq">
<dt>My AI says it has no Clank Together tools</dt>
@ -367,6 +383,6 @@
<div class="hazard thin" aria-hidden="true"></div>
<p>Clank Together · source is open: <code id="clone-cmd">git clone https://saty.dev/git/clank/game-decomp.git</code></p>
</footer>
<script src="/app.js"></script>
<script src="/app.js?v=feed-20261024"></script>
</body>
</html>

View file

@ -620,8 +620,11 @@ details.fold-plate[open] > summary { margin-bottom: 12px; }
}
/* Live feed */
.feed-plate { display: flex; flex-direction: column; }
.feed { list-style: none; margin: 0; padding: 0; max-height: 330px; overflow-y: auto; display: flex; flex-direction: column; gap: 6px; }
.feed-plate { display: flex; flex-direction: column; min-height: 0; }
.feed { list-style: none; margin: 0; padding: 0; flex: 1 1 0; min-height: 0; overflow-y: auto; display: flex; flex-direction: column; gap: 6px; }
@media (max-width: 900px) {
.feed { flex-basis: auto; max-height: 50vh; }
}
.feed li { display: grid; grid-template-columns: 26px 1fr auto; gap: 8px; align-items: start; padding: 6px 8px; border-radius: 3px; background: var(--well); border: 1px solid var(--line); font-size: 13px; }
.feed li.new { animation: slide-in 0.45s cubic-bezier(0.2, 0.9, 0.3, 1.3); }
@keyframes slide-in { from { opacity: 0; transform: translateX(30px) scale(0.96); } }

View file

@ -120,6 +120,11 @@ async function runDocker({ jobDir, list, project, runner, timeoutMs }) {
fs.chmodSync(jobDir, 0o777);
fs.chmodSync(path.join(jobDir, 'out'), 0o777);
// Containerized workers share jobs through a named volume, not daemon-host paths.
const jobSubpath = runner.jobVolume ? path.relative(runner.jobVolumeRoot, jobDir) : null;
if (jobSubpath !== null && (!jobSubpath || jobSubpath.startsWith('..') || path.isAbsolute(jobSubpath))) {
throw new Error('compiler job must be inside jobVolumeRoot');
}
const name = `td-${crypto.randomBytes(6).toString('hex')}`;
const args = [
'run', '--rm', '--name', name,
@ -134,7 +139,9 @@ async function runDocker({ jobDir, list, project, runner, timeoutMs }) {
'--security-opt', 'no-new-privileges',
'--user', runner.user ?? '1000:1000',
...(runner.runtime ? ['--runtime', runner.runtime] : []),
'-v', `${path.resolve(jobDir)}:/job:rw`,
...(runner.jobVolume
? ['--mount', `type=volume,source=${runner.jobVolume},target=/job,volume-subpath=${jobSubpath}`]
: ['-v', `${path.resolve(jobDir)}:/job:rw`]),
runner.image,
'sh', '/job/build.sh',
];

View file

@ -0,0 +1,13 @@
FROM debian:13-slim AS download
RUN apt-get update && apt-get install -y --no-install-recommends python3 ca-certificates msitools && rm -rf /var/lib/apt/lists/*
WORKDIR /build
COPY download.py /build/download.py
RUN python3 download.py
FROM debian:13-slim
RUN dpkg --add-architecture i386 && apt-get update && apt-get install -y --no-install-recommends wine wine64 wine32:i386 ca-certificates coreutils && rm -rf /var/lib/apt/lists/*
COPY --from=download /build/toolchain /opt/toolchain
COPY env.sh /opt/td/env.sh
ENV WINEPREFIX=/opt/td/wineprefix WINEDEBUG=-all WINEARCH=win64 HOME=/tmp
RUN mkdir -p /opt/td/wineprefix && wineboot --init && wineserver -w && wine reg add 'HKCU\Environment' /v TEMP /t REG_SZ /d 'Z:\tmp' /f && wine reg add 'HKCU\Environment' /v TMP /t REG_SZ /d 'Z:\tmp' /f && wineserver -w && chmod -R a+rX /opt/toolchain && chmod -R a+rwX /opt/td/wineprefix
WORKDIR /job

View file

@ -0,0 +1,7 @@
FROM rodecomp/compiler:v142
RUN apt-get update && apt-get install -y --no-install-recommends cmake ninja-build python3 && rm -rf /var/lib/apt/lists/*
COPY cmake-toolchain.cmake /opt/td/cmake-toolchain.cmake
COPY cl-wine.sh /opt/td/cl-wine.sh
RUN chmod +x /opt/td/cl-wine.sh
COPY lib-wine.sh /opt/td/lib-wine.sh
RUN chmod +x /opt/td/lib-wine.sh

View file

@ -0,0 +1,16 @@
#!/usr/bin/python3
"""Invoke MSVC under Wine, translating POSIX inputs and isolating service pipes."""
import os
import subprocess
import sys
import tempfile
args = []
for arg in sys.argv[1:]:
if arg.startswith('/') and os.path.isfile(arg):
arg = 'Z:' + arg.replace('/', '\\')
args.append(arg)
with tempfile.TemporaryFile() as output:
result = subprocess.run(['wine', '/opt/toolchain/VC/Tools/MSVC/14.29.30133/bin/Hostx64/x86/cl.exe', *args], stdout=output, stderr=subprocess.STDOUT)
output.seek(0)
sys.stdout.buffer.write(output.read())
sys.exit(result.returncode)

View file

@ -0,0 +1,8 @@
set(CMAKE_SYSTEM_NAME Windows)
set(CMAKE_SYSTEM_PROCESSOR x86)
set(CMAKE_C_COMPILER /opt/td/cl-wine.sh)
set(CMAKE_CXX_COMPILER "${CMAKE_C_COMPILER}")
# Cross-compiling: configure checks must not try to run a Windows executable.
set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY)
set(CMAKE_AR /opt/td/lib-wine.sh)
set(CMAKE_TRY_COMPILE_CONFIGURATION Release)

View file

@ -0,0 +1,61 @@
"""Acquire pinned v142 x64-host/x86-target packages from Microsoft's VS manifest."""
import hashlib
import io
import json
import pathlib
import re
import shutil
import subprocess
import urllib.request
import zipfile
MANIFEST = 'https://download.visualstudio.microsoft.com/download/pr/e2324e87-3765-4b14-85e5-1234d99f6254/fb642c3f891b70947e0152275e1722ffb3cca7e8700eea0f0fa0f3a7645584cc/VisualStudio.vsman'
root = pathlib.Path('toolchain')
root.mkdir(exist_ok=True)
packages = json.load(urllib.request.urlopen(MANIFEST))['packages']
def package(name):
return next(p for p in packages if p['id'].lower() == name.lower() and p.get('language') in (None, 'en-US'))
def download(payload):
data = urllib.request.urlopen(payload['url'].replace(' ', '%20')).read()
if hashlib.sha256(data).hexdigest().lower() != payload['sha256'].lower():
raise RuntimeError('Checksum mismatch: ' + payload['fileName'])
print(payload['fileName'], len(data), flush=True)
return data
suffixes = ['Tools.HostX64.TargetX86.base', 'Tools.HostX64.TargetX86.Res.base',
'CRT.Headers.base', 'CRT.x86.Desktop.base', 'ATL.Headers.base', 'ATL.X86.base']
for suffix in suffixes:
for payload in package('Microsoft.VC.14.29.16.11.' + suffix)['payloads']:
with zipfile.ZipFile(io.BytesIO(download(payload))) as archive:
for entry in archive.infolist():
name = pathlib.PurePosixPath(entry.filename)
if not entry.is_dir() and name.parts[0] == 'Contents':
if '..' in name.parts:
raise RuntimeError('Unsafe package path')
out = root.joinpath(*name.parts[1:])
out.parent.mkdir(parents=True, exist_ok=True)
out.write_bytes(archive.read(entry))
sdk = package('Win10SDK_10.0.19041')
payloads = {p['fileName'].replace('\\', '/').split('/')[-1]: p for p in sdk['payloads']}
msis = ['Windows SDK for Windows Store Apps Headers-x86_en-us.msi',
'Windows SDK Desktop Headers x86-x86_en-us.msi',
'Windows SDK Desktop Headers x64-x86_en-us.msi',
'Windows SDK Desktop Libs x86-x86_en-us.msi',
'Universal CRT Headers Libraries and Sources-x86_en-us.msi']
cache = pathlib.Path('sdk-downloads')
cache.mkdir(exist_ok=True)
for name in msis:
data = download(payloads[name])
msi = cache / name
msi.write_bytes(data)
for cab in set(re.findall(rb'[A-Za-z0-9]{32}\.cab', data)):
cabname = cab.decode('ascii')
dest = cache / cabname
if not dest.exists():
dest.write_bytes(download(payloads[cabname]))
subprocess.run(['msiextract', '-C', str(root.resolve()), str(msi)], check=True, stdout=subprocess.DEVNULL)
(root / 'provenance.json').write_text(json.dumps({'manifest': MANIFEST, 'toolset': 'v142', 'compilerPackage': '14.29.30159', 'target': 'x86', 'sdk': '10.0.19041.0'}, indent=2))
print('Toolchain extraction complete', flush=True)

View file

@ -0,0 +1,13 @@
export WINEPREFIX=/tmp/wineprefix
export WINEDEBUG=-all
export WINEARCH=win64
export HOME=/tmp
export TMP='Z:\tmp' TEMP='Z:\tmp'
if [ ! -d "$WINEPREFIX" ]; then
cp -rs /opt/td/wineprefix "$WINEPREFIX"
wineserver -p >/dev/null 2>&1 || true
fi
TD_CL='/opt/toolchain/VC/Tools/MSVC/14.29.30133/bin/Hostx64/x86/cl.exe'
td_cl() {
wine "$TD_CL" "$@"
}

View file

@ -0,0 +1,7 @@
#!/bin/sh
log=$(mktemp)
wine /opt/toolchain/VC/Tools/MSVC/14.29.30133/bin/Hostx64/x86/lib.exe "$@" >"$log" 2>&1
status=$?
cat "$log"
rm -f "$log"
exit "$status"

View file

@ -51,6 +51,15 @@ if (process.platform === 'win32') {
const exe = path.join(DIST, 'td-windows-x64.exe');
fs.copyFileSync(process.execPath, exe);
targets.push(exe);
} else {
const base = `https://nodejs.org/dist/${NODE_VERSION}`;
const sums = await (await fetch(`${base}/SHASUMS256.txt`)).text();
const want = /^([0-9a-f]{64})\s+win-x64\/node\.exe$/m.exec(sums)?.[1];
if (!want) throw new Error('no checksum for the Windows runtime');
const exe = path.join(DIST, 'td-windows-x64.exe');
fs.writeFileSync(exe, Buffer.from(await (await fetch(`${base}/win-x64/node.exe`)).arrayBuffer()));
if (sha256(exe) !== want) throw new Error('Windows runtime checksum mismatch');
targets.push(exe);
}
{
const name = `node-${NODE_VERSION}-linux-x64`;

View file

@ -0,0 +1,8 @@
#!/bin/sh
set -eu
. /opt/td/env.sh
TC=/opt/toolchain/VC/Tools/MSVC/14.29.30133
SDK='/opt/toolchain/Program Files/Windows Kits/10/Include/10.0.19041.0'
export INCLUDE="Z:\\opt\\toolchain\\VC\\Tools\\MSVC\\14.29.30133\\include;Z:\\opt\\toolchain\\VC\\Tools\\MSVC\\14.29.30133\\atlmfc\\include;$(printf '%s' "$SDK" | sed 's|/|\\|g' | sed 's/^/Z:/')\\ucrt;$(printf '%s' "$SDK" | sed 's|/|\\|g' | sed 's/^/Z:/')\\shared;$(printf '%s' "$SDK" | sed 's|/|\\|g' | sed 's/^/Z:/')\\um"
cmake -S /data/repo -B /tmp/rodecomp-cmake -G Ninja -DCMAKE_TOOLCHAIN_FILE=/opt/td/cmake-toolchain.cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_EXPORT_COMPILE_COMMANDS=ON
python3 /tools/export-compile.py /tmp/rodecomp-cmake/compile_commands.json /data/repo/compile.json --source /data/repo --compiler "$TC/bin/Hostx64/x86/cl.exe" --include "$INCLUDE"

View file

@ -0,0 +1,49 @@
"""Convert CMake's compile_commands.json to the upstream Clank compile schema.
Run after configuring with the container's MSVC/Wine toolchain.
"""
import argparse
import json
import pathlib
import shlex
parser = argparse.ArgumentParser()
parser.add_argument('commands')
parser.add_argument('output')
parser.add_argument('--source', required=True)
parser.add_argument('--toolchain', default='/opt/toolchain')
parser.add_argument('--compiler', required=True)
parser.add_argument('--include', required=True, help='Semicolon-separated MSVC/SDK include paths')
args = parser.parse_args()
source = pathlib.Path(args.source).resolve()
def replace(value):
return value.replace('Z:' + str(source).replace('/', '\\'), '${SRC}').replace(str(source), '${SRC}').replace('Z:' + args.toolchain.replace('/', '\\'), '${TC}').replace(args.toolchain, '${TC}')
shapes, units = [], {}
for row in json.loads(pathlib.Path(args.commands).read_text()):
file = pathlib.Path(row['file']).resolve().relative_to(source).as_posix()
command = row.get('arguments') or shlex.split(row['command'])
# CMake's compiler launcher may precede cl.exe.
compiler_index = next(i for i, arg in enumerate(command) if arg.lower().endswith(('cl.exe', 'cl-wine.sh')))
flags = []
skip = False
for arg in command[compiler_index + 1:]:
if skip:
skip = False
continue
if arg in ('-o', '-MF', '-MT'):
skip = True
continue
if arg.lower().startswith(('/fo', '/fd', '/fa')) or arg in ('/showIncludes',):
continue
if arg.replace('\\', '/').removeprefix('Z:') == str(pathlib.Path(row['file']).resolve()):
continue
flags.append(replace(arg))
if '/c' not in flags and '-c' not in flags:
flags.append('/c')
if flags not in shapes:
shapes.append(flags)
units[file] = shapes.index(flags)
result = {'compiler': replace(args.compiler), 'env': {'INCLUDE': replace(args.include)}, 'shapes': shapes, 'units': dict(sorted(units.items()))}
pathlib.Path(args.output).write_text(json.dumps(result, indent=2) + '\n')
print(f'Exported {len(units)} units in {len(shapes)} compile shapes')

View file

@ -0,0 +1,8 @@
import fs from 'node:fs';
import { DatabaseSync } from 'node:sqlite';
const db = new DatabaseSync(process.argv[2]);
const u = JSON.parse(fs.readFileSync(process.argv[3], 'utf8'));
if(u.name !== 'ragnarok-clanker' || u.role !== 'admin' || u.status !== 'active') throw new Error('Unexpected administrator record');
db.prepare('INSERT INTO users (name,password_hash,role,status,created_at,approved_at) VALUES (?,?,?,?,?,?)').run(u.name,u.password_hash,u.role,u.status,u.created_at,u.approved_at);
console.log('Imported active administrator',u.name);
db.close();

View file

@ -0,0 +1,28 @@
import fs from 'node:fs';
import crypto from 'node:crypto';
import path from 'node:path';
import { DatabaseSync } from 'node:sqlite';
const [data, input] = process.argv.slice(2);
const evidence = JSON.parse(fs.readFileSync(input, 'utf8'));
const db = new DatabaseSync(path.join(data, 'td.sqlite'));
const version = db.prepare('SELECT * FROM versions WHERE is_current=1').get();
if(version.sha256 !== evidence.targetSha256) throw new Error('Target identity mismatch');
const eligible = [], deferred = [];
for(const row of evidence.confirmations) {
const source = fs.readFileSync(path.join(data, 'repo', row.file), 'utf8').replaceAll('\r\n','\n');
if(crypto.createHash('sha256').update(source).digest('hex') !== row.sourceSha256) throw new Error(`Source changed: ${row.file}`);
const current = db.prepare('SELECT * FROM func_status WHERE symbol_id=?').get(row.symbol_id);
if(!current || !['exact','match_unresolved','mismatch'].includes(current.status)) { deferred.push(row.symbol_id); continue; }
eligible.push(row);
}
db.exec('BEGIN IMMEDIATE');
try {
for(const row of eligible) {
db.prepare('UPDATE func_status SET confirmed=1 WHERE symbol_id=?').run(row.symbol_id);
db.prepare("UPDATE tasks SET state='done',updated_at=? WHERE symbol_id=? AND state IN ('open','needs_cleanup','hard')").run(Date.now(),row.symbol_id);
}
db.prepare('INSERT INTO events(at,type,user_id,data) VALUES(?,?,NULL,?)').run(Date.now(),'historical_confirmations_imported',JSON.stringify({source:evidence.sourceDatabase,accepted:eligible.map(r=>r.symbol_id),deferred,method:'unchanged full source at historical confirmed commit; current compiler measurement retained'}));
db.exec('COMMIT');
} catch(error) { db.exec('ROLLBACK'); throw error; }
console.log(JSON.stringify({imported:eligible.length,deferred:deferred.length,deferredIds:deferred}));
db.close();

View file

@ -0,0 +1,9 @@
import {DatabaseSync} from 'node:sqlite';
const d=new DatabaseSync('/var/lib/docker/volumes/rodecomp_data/_data/td.sqlite',{readOnly:true});
for(const t of d.prepare("SELECT name FROM sqlite_master WHERE type='table'").all()) {
const cols=d.prepare(`PRAGMA table_info("${t.name}")`).all().filter(c=>/commit/.test(c.name));
if(cols.length) console.log(t.name,JSON.stringify(cols.map(c=>c.name)));
}
console.log('attempts',d.prepare('SELECT state,count(*) n FROM attempts GROUP BY state').all());
console.log('tasks',d.prepare('SELECT state,count(*) n FROM tasks GROUP BY state').all());
d.close();

View file

@ -0,0 +1,78 @@
old new
bb5dfeb7539efd7582332c19aa31ed2b16011c32 bb5dfeb7539efd7582332c19aa31ed2b16011c32
fccca0f07a5f4a74ab8580940ca41cc4ce5a5fe3 fccca0f07a5f4a74ab8580940ca41cc4ce5a5fe3
42e136cc165964abe83c38276b2b9d5008486a2f 42e136cc165964abe83c38276b2b9d5008486a2f
f32ab174a5d9d8cd0a9fbfe9a9c69c203b9d4960 f32ab174a5d9d8cd0a9fbfe9a9c69c203b9d4960
36cdedcff64b5e008bcb11ca6886ebc009cd2f62 36cdedcff64b5e008bcb11ca6886ebc009cd2f62
44cd58b0a53d3046f834608131e2c99265b8c87b 44cd58b0a53d3046f834608131e2c99265b8c87b
900df586ce1aab3a4e79036022cb43cfc1f6177c 900df586ce1aab3a4e79036022cb43cfc1f6177c
e1a43312ab8c1edf63840e09c0d787c8a5f02e56 e1a43312ab8c1edf63840e09c0d787c8a5f02e56
bbba01d4a710b6c37ec569b203eada453c6be940 2c3a07e1370c6d7f8d34f6c51f56b23a0d30b59e
0ab026769728aef6b5c55cf8626efa10ed7a7458 bcc85a5ac6c3bb1db830fe96cfb4f9bd6c591928
677141da932c457115e46e82579e41d801fcbf35 06d59f48ae4407a7ef43a9e39425a0ba7afd21c3
a417b7deb79d2ca4aa689b4e90d74cff90af38c5 009c4419738d4c01e8687b851eaab50c75c52aa1
3f1cb49c948e1d4089cd818fdbb10d613bdfb393 a299f80cfdf8d19b7de923da44d3c12e8e0dc6b9
b5695645de003ec455e045b520ce71aa282f2a87 86c211e75683980269e5f14b295744d51f24e4a5
e7b52df92b6a58911b77b0d5378e950db03e9758 542610ef3b9483e9d97e0a067f9312a2704bc100
b3a6fde0b6c32c91d214a497e4adb2e2d1746b88 95bfcc63bd22fc910a87c1cb35f0833f445de100
7c02f156c6ad3dab5dbf3d59212095ee782ee5a9 218df9f7380496351333f186ff5cd662af5f4d17
6cf5f9d7bf5552c674171645e7a42d78e6d26503 e809f0b6909653ca9311a577a1cc7b2b3ac0cdbe
5c8376d1d9aef10f6a6225a6fe87af5f64768755 16d978a98304f75ea3efee9f240c3aeeb5fdece1
e8aad3180c861228bf01de07884cc4efd405b461 811fe72f054faf6442ba916f80489823c53fb09a
ee9a53b14e7275003db8be2272d949fbfa073a99 5cd5924f2d43b054a5084a0a909ce68f6a1f0c5e
02bda87b59d323a3264cf09496342d37cce8b6ae 608c465be9294d0f423e9f2de19ebbd1713dede1
9e6e75ed01539cedbf3205380005b22af9adda83 41fc04b96844d19e3db712c4e71883d2930ed13f
ae08409cdee56f4ecefb21a52b3ce3bdc4417bfc 9ff47a21d361100c5eeb080c1fce9c2ddab4a124
dd530437ed1383f629030fe65321a4f0e30dcbb1 c9a831430a2406b985cd59ee47af7edffef8f7f4
45c9089ee965c667f6d2914dc4d4720d9bd55d29 decfa50aa86f333035e5933e700ed2345f0fa2e7
384af1063c88a14c5810538cb921e48c6bd901c4 9a842a452c0a70de16e3d2c6f3f98e0ea9812e67
1dec64215f1df4b580ca20fca4a94f448dd22d42 3d1c885429f9ec11c3e0157256dbe6df839057be
a341c48dc6a7c642c125b2009833090a17fbd210 75d9e4ab265be15c52b47088063ce4f40b3237f7
5bc4450913afb42d9069d190cd9100de21a36eaa 84f1336564cb8617887d6e274e3d5e1b8efe4414
2344f58ea46a9d820d8f36143e8de8261918697e 7dc69b8d9abed38dc0faf5c5fceeb7e576bf0da4
cb72c354435b8b1f47b5cb348762dca1e4452a16 a890bcaf378f6dea18d927500ecce00f592b4f3a
e827884d67d62b2ac0b1b7a8fcf3e92841fd2219 3a8f5f27d1c1f261a00e7057e69c7dc476f95ad6
2ea6810686117f9c6ae6ae61b412f5255065ca52 c39bff50f46462c5428389867d003693fec2cf1c
905272f64c07b98426981b9c64ca074a90d436b5 9b6a91f896fa8943047cf21f147c9cc793e8319a
f5e31ed41c6944f4c1b96a9ba839148c0056e858 90784471a329905402a073c4b9d7b64a97e6dd2c
6e61f1856cfd6d26527d553af3a84c0d62068fc6 4b0ebd89023b663ad63755f03bf291f44d992d2a
446d9a05f1d46c3b90db1eff89fddf2eb54295ac 7253e44ee9ed73fbadd9b7cb89c2748b7a3e4eed
9cf9d964268b0ccc906c8e7eb218c4dd009f28b3 3ac4853f152076c88147e8d338c1a08b14b63688
f7e68749c47a3d75ffc327fb80c3ebb78e0c0edd 0c6592e128b7ac2a0f2a6de15b434ef2198e48d7
9fa44f28e097c00a521429c936bcc2c74b486e0b de8c1653a32a396f074a8949e471483563ea5850
323157d16b257e722fa0b5eb2aaaa23b086f9a30 6932b58a04a9dcc70f8caf69814fa7d94381c288
8beea9315e2d9c74d279b99dfb3f7984e00005cf 774012e9e527e686d02b2824e94c125795743576
2d6ff71d0bfaa488ea73ead7f1d73ae86ebb0588 11502f7787510fa05bbbdd2f5e4901728efe3cca
33c0ceff16ff1e322e1cbe28cef7dd0738d56f70 285aad41ac259d2ced3d1073038dabeb742433f6
8be837c06da826927f56e13d130766de1e55df37 5acb0a179eb543cb5258b2010b178634e0fa9ba3
080b8a022592e0a3381a6a2dcd555fe035ca6557 7649a62b97b9c9a3c8467630dfead5c165dad535
0e232733e71f234fc0201ceb20a372ba5ad3edf4 78bd93acb2c811ab70c27234a133e95c87a4476f
a0ae08405c033ae41df05cfa80b78d345da46adf 31cf3b7a1b02ea8109101b7bef089038cb65ed7b
2f1535092a5f065c3e27f1b8647b3367374cd450 c4130ce6528bcec09b249637f095ec69923bc489
14fb6aa84120f8042d8cb39863114f14d38f23af d583b3461036c357326c697536bbf62c275ad10c
3d3f8a432d0cc97909e3e27c87aac734eb08a134 ecbb015f5268e92628536eb081620dfaad447637
0b0f39f980c2d0767130c5803e66b78328d10186 5841de39c9b74af02cd42c1ab99db83e414bb1f7
da94c9385d845bffc5be504846536956b808bfc7 3f966658a2fb98e4537b9b07c937d3d17dc484d9
163ff913eea8602ac380ec67fff1ce97fda79c9a 3ce9c4f7363ea7306e8d5dfcbe4117e4e0a42497
ba9de6fa1f504e9ba6c65ccd10eda0e16c5cd222 f46515f5d8dd2aff45950c594092f89dd1abd3d7
2da097c0292b335ba5c6e512351af40098c96538 58e7d31c2c2731dda08315eb6728fd7cc2bf1bd9
74e4cd6fb4bff7d14cd981ec04f71b065e8811eb 3f1051e316a30d9dadaea03297455d70486d9006
a7bafe9c8b90cefc1c47d5193ac7a5098d21c861 f823eeb9ec0f0e6089552ca3bf7a4242f6879bca
9e0f3ecf18902a0401d718f2b83fe8931c67ac97 9a9b90dcbf5bcfba2dca1517f5d0f0bcb6c958ef
4b960a15c9924e1f8875e67699348debbc58d2e1 ed5d006e2d3bd41ecb5bf079f2cf4fb22d1aa4b3
8ff20fc80175395a019e5856cda0dd37dc090270 601cd5b7ccd4ec5dcbd7314b7fb9f614de8c0269
5c44c4e376be4cb08e0721c170dcfbf892d039aa 49dc0a316d5924b71734dbb19d58ac911d6d864f
a463c789dcef9f814b1b743c7463fae0a6e75879 7b6a0e099260a5f34b955908caf4a31fa476ec05
c440c3ba10800e8976e322ae3e7fec4f6bbfb437 7d223430d37aa4d5fc751af72a80a93ba7163e50
baa92caaac97c8410df189e7ff0170a52f4c08fa 9a96cfc477c915f1a66e30dcd3f8d9b7c5d3ced4
7fcdbf2b932a4efc0d29887f5e74e91c691639ea 272a130de9adf37d4996f172437c562b8f062d2f
a2909e08f1156bbce3d5ff161c963c537994e6f9 8332553b6a14ae92a6c91797006a5900ff53c323
d60454407d83ae9c0ec2f3f6394117afe39f4a5f 01ba4af88ce8f7d33bc62c905c57e7b8c6cebe4d
6c66031e8edd7d9da8756ad38a5761b7e4af9446 88f21dc6ffd2203e9ae671e1d00242195325860e
05f35e32190e0d451fbd1f425287a60b0eaf96e8 ecb398f9e27cbf55b889d094547393022c371cfb
3118d71a2e7d7945fc91d20a315a8fc6b9ede069 1ac1a6e27a9565d58ac47423960b45c835297c88
0a29da58516f1a49a9b5730d60e4ec3350eea1df 1c3625a5cb55868e7efcd7786909ed8fd944f954
e542c6ed80befaeb0c8fb1684f6450497d41a806 344bd14ebf148883fa7361f0b52d3676a7841d44
63c8e679d0b49934c890d57b11235985af00f48e 685d0a819940ee51941f9dff0783a123e87a9409
a3dc53369ca4310fb7b904311d0508bce14bfe9e 69323b7be14ec027ff116ea9b0b7795a9ba6b3f5
f7f25ca3647d68b52d94764ad4c547e361f4915c 0c8d621bd1122747716cd0fded077207eaca3bc5

View file

@ -0,0 +1,21 @@
import fs from 'node:fs';
import crypto from 'node:crypto';
import { execFileSync } from 'node:child_process';
const repo = process.argv[2];
const compilerHash = process.argv[3];
if (!repo || !/^[a-f0-9]{64}$/.test(compilerHash ?? '')) throw new Error('usage: prepare-project.mjs <repo> <compiler-sha256>');
const file = `${repo}/decomp.json`;
const p = JSON.parse(fs.readFileSync(file, 'utf8'));
p.name = 'rodecomp';
p.compileCommands = 'compile.json';
delete p.verification;
delete p.build;
p.matchPolicy = { autoMergeIdentical: true, approvals: 1, approvalsWithHeaderChanges: 2, maxCleanupCycles: 5, peerReviews: 3, peerMargin: 3, peerRejectMargin: 2 };
fs.writeFileSync(file, JSON.stringify(p, null, 2) + '\n');
fs.writeFileSync(`${repo}/toolchain.lock.json`, JSON.stringify({ragnarok:{family:'msvc',version:'19.29.30159',machine:'x86',image:'rodecomp/compiler:v142',clSha256:compilerHash,sdkVersion:'10.0.19041.0'}}, null, 2) + '\n');
let guide = fs.readFileSync('/app/template/AGENTS.md', 'utf8').replace('a C++ program built with MSVC 2010', 'Ragnarok Online Ragexe 2026-02-19 (x86), using MSVC v142 19.29.30159').replace("(Ghidra's guess, a starting point only)", '(imported IDA pseudocode where available, a starting point only)');
guide += '\n## Ragnarok target\n\nUse only this workspace’s configured server and clank-together MCP. The project is rodecomp, version 2026-02-19. Preserve existing stable IDs and OFFSET/STATIC provenance comments. Missing original evidence is a blocker to a verdict, not permission to invent behavior. The source contains subsystem libraries, not a completed standalone game. Clank compiles individual units from compile.json; it does not run CMake or CTest during checks.\n';
fs.writeFileSync(`${repo}/AGENTS.md`, guide);
execFileSync('git',['-c',`safe.directory=${repo}`,'add','decomp.json','compile.json','toolchain.lock.json','AGENTS.md'],{cwd:repo,stdio:'inherit'});
execFileSync('git',['-c',`safe.directory=${repo}`,'-c','user.name=Clank Together','-c','user.email=bot@clank-together.invalid','commit','-m','Configure rodecomp v142 x86 contributor builds'],{cwd:repo,stdio:'inherit'});
console.log('Configured rodecomp with pinned compiler',compilerHash);

View file

@ -0,0 +1,30 @@
import fs from 'node:fs';
import crypto from 'node:crypto';
import { execFileSync } from 'node:child_process';
const data='/var/lib/docker/volumes/rodecomp_data/_data';
const secretsFile=data+'/forgejo-admin.json';
const secrets=fs.existsSync(secretsFile)?JSON.parse(fs.readFileSync(secretsFile,'utf8')):{username:'clank-admin',password:crypto.randomBytes(30).toString('base64url')};
if(!fs.existsSync(secretsFile))fs.writeFileSync(secretsFile,JSON.stringify(secrets),{mode:0o600});
const fj=(args)=>execFileSync('docker',['exec','--user','git','rodecomp-forgejo-1','forgejo','--config','/data/gitea/conf/app.ini',...args],{encoding:'utf8'});
const users=fj(['admin','user','list']);
if(!users.includes(secrets.username))fj(['admin','user','create','--admin','--username',secrets.username,'--email','admin@rodecomp.online','--password',secrets.password,'--must-change-password=false']);
if(!users.includes('clank-bot'))fj(['admin','user','create','--username','clank-bot','--email','bot@rodecomp.online','--random-password','--must-change-password=false']);
async function api(method,url,body){const r=await fetch('http://forgejo:3000/api/v1'+url,{method,headers:{Authorization:'Basic '+Buffer.from(secrets.username+':'+secrets.password).toString('base64'),'content-type':'application/json'},body:body?JSON.stringify(body):undefined});if(!r.ok)throw Error(method+' '+url+' '+r.status+' '+await r.text());return r.status===204?null:r.json();}
let r=await fetch('http://forgejo:3000/api/v1/orgs/clank');
if(r.status===404)await api('POST','/orgs',{username:'clank',full_name:'Clank Together',visibility:'public'});
r=await fetch('http://forgejo:3000/api/v1/repos/clank/rodecomp');
if(r.status===404)await api('POST','/orgs/clank/repos',{name:'rodecomp',description:'Ragnarok Online reconstruction through Clank Together',private:false,default_branch:'main'});
await api('PATCH','/repos/clank/rodecomp',{has_pull_requests:false,has_actions:false,has_packages:false,has_releases:false,has_projects:false,website:'https://clank.rodecomp.online/'});
await api('PUT','/repos/clank/rodecomp/collaborators/clank-bot',{permission:'write'});
const git=(args)=>execFileSync('git',['-C',data+'/repo',...args],{encoding:'utf8',stdio:['ignore','pipe','pipe']});
if(!git(['remote']).split('\n').includes('forgejo')){
const token=fj(['admin','user','generate-access-token','--username','clank-bot','--token-name','coordinator','--scopes','write:repository','--raw']).trim();
git(['remote','add','forgejo',`http://clank-bot:${token}@forgejo:3000/clank/rodecomp.git`]);
}
git(['push','forgejo','HEAD:refs/heads/main']);
const protections=await api('GET','/repos/clank/rodecomp/branch_protections');
if(!protections.some(p=>p.rule_name==='main'))await api('POST','/repos/clank/rodecomp/branch_protections',{rule_name:'main',enable_push:true,enable_push_whitelist:true,push_whitelist_usernames:['clank-bot'],block_on_rejected_reviews:false});
const file=data+'/server.json';const config=JSON.parse(fs.readFileSync(file,'utf8'));
Object.assign(config,{pushRemote:'forgejo',gitCloneUrl:'https://clank.rodecomp.online/git/clank/rodecomp.git',gitWebUrl:'https://clank.rodecomp.online/git/clank/rodecomp'});
fs.writeFileSync(file,JSON.stringify(config,null,2)+'\n');
console.log('Published clank/rodecomp; main protected; coordinator push enabled. Forgejo admin credential stored privately in '+secretsFile);

View file

@ -0,0 +1,27 @@
import fs from 'node:fs';
import {DatabaseSync} from 'node:sqlite';
import {execFileSync} from 'node:child_process';
const data='/var/lib/docker/volumes/rodecomp_data/_data';
const map=new Map(fs.readFileSync(data+'/repo/.git/filter-repo/commit-map','utf8').trim().split('\n').slice(1).map(l=>l.trim().split(/\s+/)));
map.set('41ea356f358ba643231157a498fff099f077f9d6', execFileSync('git',['-C',data+'/repo','rev-parse','HEAD'],{encoding:'utf8'}).trim());
const db=new DatabaseSync(data+'/td.sqlite');
db.exec('BEGIN IMMEDIATE');
let updates=0;
try {
for(const table of db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all()) {
const cols=db.prepare(`PRAGMA table_info("${table.name}")`).all().filter(c=>c.name.includes('commit'));
for(const col of cols)for(const [old,next] of map){if(old===next)continue;if(/^0+$/.test(next))continue;updates+=Number(db.prepare(`UPDATE "${table.name}" SET "${col.name}"=? WHERE "${col.name}"=?`).run(next,old).changes);}
}
db.exec('COMMIT');
} catch(e){db.exec('ROLLBACK');throw e;}
db.close();
const admin=JSON.parse(fs.readFileSync(data+'/forgejo-admin.json','utf8'));
async function api(method,path,body){const r=await fetch('http://forgejo:3000/api/v1'+path,{method,headers:{Authorization:'Basic '+Buffer.from(admin.username+':'+admin.password).toString('base64'),'content-type':'application/json'},body:body?JSON.stringify(body):undefined});if(!r.ok)throw Error(method+' '+path+' '+r.status+' '+await r.text());return r.status===204?null:r.json();}
const route='/repos/clank/rodecomp/branch_protections';
const protections=await api('GET',route);
const main=protections.find(p=>p.rule_name==='main');
if(!main)throw Error('Expected main protection');
await api('DELETE',route+'/main');
try {execFileSync('git',['-C',data+'/repo','push','--force-with-lease=refs/heads/main:41ea356f358ba643231157a498fff099f077f9d6','forgejo','HEAD:refs/heads/main'],{stdio:'pipe'});}
finally {await api('POST',route,{rule_name:'main',enable_push:true,enable_push_whitelist:true,push_whitelist_usernames:['clank-bot'],block_on_rejected_reviews:false});}
console.log('Updated commit references:',updates,'; rewritten main published; protection restored');

View file

@ -0,0 +1,12 @@
import fs from 'node:fs';
import {execFileSync} from 'node:child_process';
const repo='/data/repo';
const cmake=repo+'/CMakeLists.txt';
const text=fs.readFileSync(cmake,'utf8');
if(!text.includes('add_subdirectory(Verification)'))throw Error('Expected Verification target');
fs.writeFileSync(cmake,text.replace(/^add_subdirectory\(Verification\)\r?\n?/m,''));
const file=repo+'/compile.json';const cfg=JSON.parse(fs.readFileSync(file,'utf8'));
for(const unit of Object.keys(cfg.units))if(unit.startsWith('Verification/'))delete cfg.units[unit];
fs.writeFileSync(file,JSON.stringify(cfg,null,2)+'\n');
execFileSync('git',['-C',repo,'add','CMakeLists.txt','compile.json']);
execFileSync('git',['-C',repo,'-c','user.name=Clank Together','-c','user.email=bot@clank-together.invalid','commit','-m','Remove build references to deleted verification sources'],{stdio:'inherit'});

View file

@ -0,0 +1,21 @@
import fs from 'node:fs';
import {execFileSync} from 'node:child_process';
import {DatabaseSync} from 'node:sqlite';
const data='/var/lib/docker/volumes/rodecomp_data/_data',repo=data+'/repo';
const git=(args,opts={})=>execFileSync('git',['-C',repo,...args],{encoding:'utf8',...opts}).trim();
if(git(['status','--porcelain']))throw Error('Canonical checkout has uncommitted changes');
const previous=git(['rev-parse','HEAD']),tree=git(['rev-parse','HEAD^{tree}']);
const identity={...process.env,GIT_AUTHOR_NAME:'ragnarok-clanker',GIT_AUTHOR_EMAIL:'ragnarok-clanker',GIT_COMMITTER_NAME:'ragnarok-clanker',GIT_COMMITTER_EMAIL:'ragnarok-clanker'};
const root=git(['commit-tree',tree],{input:'Initial rodecomp source snapshot\n',env:identity});
fs.writeFileSync(data+'/history-reset.json',JSON.stringify({previous,root,tree},null,2));
git(['update-ref','refs/heads/main',root,previous]);
for(const ref of git(['for-each-ref','--format=%(refname)']).split('\n'))if(ref&&ref!=='refs/heads/main')git(['update-ref','-d',ref]);
git(['reset','--hard',root]);git(['config','user.name','ragnarok-clanker']);git(['config','user.email','ragnarok-clanker']);
const db=new DatabaseSync(data+'/td.sqlite');db.exec('BEGIN IMMEDIATE');
try{for(const table of db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all())for(const c of db.prepare(`PRAGMA table_info("${table.name}")`).all().filter(c=>c.name.includes('commit')))db.prepare(`UPDATE "${table.name}" SET "${c.name}"=? WHERE "${c.name}" IS NOT NULL`).run(root);db.exec('COMMIT');}catch(e){db.exec('ROLLBACK');throw e;}db.close();
const a=JSON.parse(fs.readFileSync(data+'/forgejo-admin.json','utf8'));
async function api(method,url,body){const r=await fetch('http://forgejo:3000/api/v1'+url,{method,headers:{Authorization:'Basic '+Buffer.from(a.username+':'+a.password).toString('base64'),'content-type':'application/json'},body:body?JSON.stringify(body):undefined});if(!r.ok)throw Error(method+' '+r.status+' '+await r.text());}
const route='/repos/clank/rodecomp/branch_protections';await api('DELETE',route+'/main');
try{git(['push','--force-with-lease=refs/heads/main:'+previous,'forgejo','HEAD:refs/heads/main']);}finally{await api('POST',route,{rule_name:'main',enable_push:true,enable_push_whitelist:true,push_whitelist_usernames:['clank-bot'],block_on_rejected_reviews:false});}
git(['reflog','expire','--expire=now','--all']);git(['gc','--prune=now']);
console.log(JSON.stringify({previous,root,tree,commits:git(['rev-list','--count','--all'])}));

View file

@ -0,0 +1,10 @@
import subprocess
repo='/data/repo'
subprocess.run(['git','-C',repo,'filter-repo','--force',
'--blob-callback', "import re; blob.data = re.sub(rb'(?i)[a-z0-9.!#$%&*+/=?^_`{|}~-]*spl3en[a-z0-9.!#$%&*+/=?^_`{|}~-]*@[a-z0-9.-]+', b'ragnarok-clanker', blob.data); blob.data = re.sub(rb'(?i)spl3en', b'ragnarok-clanker', blob.data)",
'--message-callback', "import re; message = re.sub(rb'(?i)[a-z0-9.!#$%&*+/=?^_`{|}~-]*spl3en[a-z0-9.!#$%&*+/=?^_`{|}~-]*@[a-z0-9.-]+', b'ragnarok-clanker', message); return re.sub(rb'(?i)spl3en', b'ragnarok-clanker', message)",
'--name-callback', "import re; return re.sub(rb'(?i)spl3en', b'ragnarok-clanker', name)",
'--email-callback', "return b'ragnarok-clanker' if b'spl3en' in email.lower() else email",
'--filename-callback', "import re; return re.sub(rb'(?i)spl3en', b'ragnarok-clanker', filename)",
'--refname-callback', "import re; return re.sub(rb'(?i)spl3en', b'ragnarok-clanker', refname)"
],check=True)