fix(paired): auto-provision paired workspace so tribunal rooms get a reviewer on registration

Rooms registered as tribunal had no work_dir and no paired_projects row, so
ensurePairedProject() returned null, no paired task was ever created, and only
the owner ran — the reviewer/arbiter never fired (web-vstock and 4 other rooms).

Add ensurePairedWorkspaceProvisioned(): defaults canonical work_dir to
groups/<folder>, guarantees it is a standalone git repo with an initial commit
(detected via a LOCAL .git so we never walk up into the EJClaw checkout and
create a stray worktree), and upserts the paired_projects row. Wire it into
setup/register.ts so every newly registered room is immediately usable by the
full owner→reviewer→arbiter flow. Adds a regression test.
This commit is contained in:
Codex
2026-07-30 01:45:28 +09:00
parent 6036430c60
commit 44c54a759c
3 changed files with 108 additions and 1 deletions

View File

@@ -3,11 +3,12 @@ import crypto from 'crypto';
import fs from 'fs';
import path from 'path';
import { DATA_DIR } from './config.js';
import { DATA_DIR, GROUPS_DIR } from './config.js';
import {
getPairedProject,
getPairedTaskById,
getPairedWorkspace,
upsertPairedProject,
upsertPairedWorkspace,
} from './db.js';
import { resolvePairedTaskWorkspacePath } from './group-folder.js';
@@ -610,6 +611,62 @@ function getTaskAndProject(taskId: string): {
return { task, canonicalWorkDir: project.canonical_work_dir };
}
/**
* Ensure a room's canonical paired workspace exists so the tribunal flow can
* provision an owner worktree and run the reviewer/arbiter.
*
* Root cause this guards against: a room registered as tribunal with no
* `work_dir` → `ensurePairedProject` returns null → no paired task is created →
* reviewer/arbiter never run (the symptom seen in web-vstock and other rooms).
*
* This defaults the canonical work dir to `groups/<folder>`, guarantees it is a
* standalone git repo with at least one commit (so `git worktree add` can
* resolve HEAD during owner-workspace provisioning), and upserts the
* `paired_projects` row. Idempotent — safe to call on every registration.
*/
export function ensurePairedWorkspaceProvisioned(args: {
chatJid: string;
groupFolder: string;
workDir?: string | null;
}): string {
const canonicalWorkDir =
args.workDir && args.workDir.trim().length > 0
? args.workDir
: path.join(GROUPS_DIR, args.groupFolder);
fs.mkdirSync(canonicalWorkDir, { recursive: true });
// Detect a standalone repo by its OWN `.git` only. A bare `git rev-parse`
// would walk up to a parent repo (e.g. the EJClaw checkout that contains
// groups/<folder>) and falsely report the dir as a repo — the exact bug that
// produced stray EJClaw worktrees. So init a fresh repo when no local .git.
if (!fs.existsSync(path.join(canonicalWorkDir, '.git'))) {
runGit(['init', '-b', 'main'], canonicalWorkDir);
}
if (!resolveCommit(canonicalWorkDir, 'HEAD')) {
// Fresh repo with no commits — owner-workspace provisioning needs a
// resolvable HEAD, so seed an empty initial commit with a local identity.
runGit(['config', 'user.email', 'claude-bot@ejclaw'], canonicalWorkDir);
runGit(['config', 'user.name', 'EJClaw'], canonicalWorkDir);
runGit(
['commit', '--allow-empty', '-m', 'chore: initialize paired workspace'],
canonicalWorkDir,
);
}
const now = new Date().toISOString();
upsertPairedProject({
chat_jid: args.chatJid,
group_folder: args.groupFolder,
canonical_work_dir: canonicalWorkDir,
created_at: now,
updated_at: now,
});
return canonicalWorkDir;
}
function makeWorkspaceRecord(args: {
taskId: string;
role: PairedWorkspace['role'];