@tritard/waterbrother 0.16.111 → 0.16.112
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/cli.js +128 -35
- package/src/gateway.js +138 -42
- package/src/shared-project.js +1 -0
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { execFile, spawn } from "node:child_process";
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import process from "node:process";
|
|
5
6
|
import readline from "node:readline";
|
|
@@ -3244,47 +3245,54 @@ function formatTelegramVerificationSummary(result = {}) {
|
|
|
3244
3245
|
|
|
3245
3246
|
async function runTelegramLocalVerification(cwd) {
|
|
3246
3247
|
const planned = await planTelegramVerificationCommands(cwd);
|
|
3248
|
+
const workspace = await resolveVerificationWorkspace(cwd);
|
|
3247
3249
|
const commands = [];
|
|
3248
3250
|
const startedAt = new Date().toISOString();
|
|
3249
3251
|
const logs = [];
|
|
3250
3252
|
let passedCount = 0;
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3253
|
+
try {
|
|
3254
|
+
for (const [bin, args, label] of planned) {
|
|
3255
|
+
commands.push(label);
|
|
3256
|
+
try {
|
|
3257
|
+
await execFileAsync(bin, args, {
|
|
3258
|
+
cwd: workspace.cwd,
|
|
3259
|
+
env: { ...process.env },
|
|
3260
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
3261
|
+
timeout: 10 * 60 * 1000
|
|
3262
|
+
});
|
|
3263
|
+
passedCount += 1;
|
|
3264
|
+
} catch (error) {
|
|
3265
|
+
const combined = `${String(error?.stderr || "")}\n${String(error?.stdout || "")}`.trim();
|
|
3266
|
+
const extracted = combined
|
|
3267
|
+
.split("\n")
|
|
3268
|
+
.map((line) => String(line || "").trim())
|
|
3269
|
+
.filter(Boolean)
|
|
3270
|
+
.slice(0, 6);
|
|
3271
|
+
logs.push(...extracted);
|
|
3272
|
+
return {
|
|
3273
|
+
outcome: error?.killed || error?.signal === "SIGTERM" ? "timeout" : (passedCount > 0 ? "partial" : "failed"),
|
|
3274
|
+
summary: `${label} failed${passedCount > 0 ? ` after ${passedCount} passing check${passedCount === 1 ? "" : "s"}` : ""}.`,
|
|
3275
|
+
failedCommand: label,
|
|
3276
|
+
isolation: workspace.isolation,
|
|
3277
|
+
commands,
|
|
3278
|
+
startedAt,
|
|
3279
|
+
completedAt: new Date().toISOString(),
|
|
3280
|
+
logs
|
|
3281
|
+
};
|
|
3282
|
+
}
|
|
3278
3283
|
}
|
|
3284
|
+
return {
|
|
3285
|
+
outcome: "passed",
|
|
3286
|
+
summary: `${commands.length} verification command${commands.length === 1 ? "" : "s"} passed.`,
|
|
3287
|
+
isolation: workspace.isolation,
|
|
3288
|
+
commands,
|
|
3289
|
+
startedAt,
|
|
3290
|
+
completedAt: new Date().toISOString(),
|
|
3291
|
+
logs
|
|
3292
|
+
};
|
|
3293
|
+
} finally {
|
|
3294
|
+
await workspace.cleanup();
|
|
3279
3295
|
}
|
|
3280
|
-
return {
|
|
3281
|
-
outcome: "passed",
|
|
3282
|
-
summary: `${commands.length} verification command${commands.length === 1 ? "" : "s"} passed.`,
|
|
3283
|
-
commands,
|
|
3284
|
-
startedAt,
|
|
3285
|
-
completedAt: new Date().toISOString(),
|
|
3286
|
-
logs
|
|
3287
|
-
};
|
|
3288
3296
|
}
|
|
3289
3297
|
|
|
3290
3298
|
async function chooseFromInteractiveMenu({ title, options, defaultIndex = 0 }) {
|
|
@@ -4843,6 +4851,91 @@ async function gitExec(args, { cwd, maxBuffer = 4 * 1024 * 1024, timeout = 30000
|
|
|
4843
4851
|
return pfy(execFileCb)("git", args, { cwd, maxBuffer, timeout });
|
|
4844
4852
|
}
|
|
4845
4853
|
|
|
4854
|
+
async function resolveVerificationWorkspace(cwd) {
|
|
4855
|
+
const cleanup = async () => {};
|
|
4856
|
+
let repoRoot = "";
|
|
4857
|
+
try {
|
|
4858
|
+
const { stdout } = await execFileAsync("git", ["rev-parse", "--show-toplevel"], {
|
|
4859
|
+
cwd,
|
|
4860
|
+
maxBuffer: 256 * 1024,
|
|
4861
|
+
timeout: 10000
|
|
4862
|
+
});
|
|
4863
|
+
repoRoot = String(stdout || "").trim();
|
|
4864
|
+
} catch {
|
|
4865
|
+
return { cwd, cleanup, isolation: "cwd" };
|
|
4866
|
+
}
|
|
4867
|
+
if (!repoRoot) {
|
|
4868
|
+
return { cwd, cleanup, isolation: "cwd" };
|
|
4869
|
+
}
|
|
4870
|
+
|
|
4871
|
+
const worktreeDir = await fs.mkdtemp(path.join(tmpdir(), "waterbrother-verify-"));
|
|
4872
|
+
let attached = false;
|
|
4873
|
+
const cleanupWorktree = async () => {
|
|
4874
|
+
if (attached) {
|
|
4875
|
+
try {
|
|
4876
|
+
await execFileAsync("git", ["worktree", "remove", "--force", worktreeDir], {
|
|
4877
|
+
cwd: repoRoot,
|
|
4878
|
+
maxBuffer: 1024 * 1024,
|
|
4879
|
+
timeout: 30000
|
|
4880
|
+
});
|
|
4881
|
+
} catch {}
|
|
4882
|
+
}
|
|
4883
|
+
await fs.rm(worktreeDir, { recursive: true, force: true }).catch(() => {});
|
|
4884
|
+
};
|
|
4885
|
+
|
|
4886
|
+
try {
|
|
4887
|
+
await execFileAsync("git", ["worktree", "add", "--detach", worktreeDir, "HEAD"], {
|
|
4888
|
+
cwd: repoRoot,
|
|
4889
|
+
maxBuffer: 1024 * 1024,
|
|
4890
|
+
timeout: 30000
|
|
4891
|
+
});
|
|
4892
|
+
attached = true;
|
|
4893
|
+
|
|
4894
|
+
const copyRelativePath = async (relativePath) => {
|
|
4895
|
+
const sourcePath = path.join(repoRoot, relativePath);
|
|
4896
|
+
const targetPath = path.join(worktreeDir, relativePath);
|
|
4897
|
+
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
|
4898
|
+
await fs.cp(sourcePath, targetPath, { recursive: true, force: true });
|
|
4899
|
+
};
|
|
4900
|
+
|
|
4901
|
+
const trackedChanged = await execFileAsync("git", ["diff", "--name-only", "-z", "HEAD"], {
|
|
4902
|
+
cwd: repoRoot,
|
|
4903
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
4904
|
+
timeout: 30000
|
|
4905
|
+
}).catch(() => ({ stdout: "" }));
|
|
4906
|
+
const deletedTracked = await execFileAsync("git", ["diff", "--name-only", "--diff-filter=D", "-z", "HEAD"], {
|
|
4907
|
+
cwd: repoRoot,
|
|
4908
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
4909
|
+
timeout: 30000
|
|
4910
|
+
}).catch(() => ({ stdout: "" }));
|
|
4911
|
+
const untracked = await execFileAsync("git", ["ls-files", "--others", "--exclude-standard", "-z"], {
|
|
4912
|
+
cwd: repoRoot,
|
|
4913
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
4914
|
+
timeout: 30000
|
|
4915
|
+
}).catch(() => ({ stdout: "" }));
|
|
4916
|
+
|
|
4917
|
+
const deleted = new Set(String(deletedTracked.stdout || "").split("\0").map((item) => item.trim()).filter(Boolean));
|
|
4918
|
+
const tracked = String(trackedChanged.stdout || "").split("\0").map((item) => item.trim()).filter(Boolean);
|
|
4919
|
+
const extras = String(untracked.stdout || "").split("\0").map((item) => item.trim()).filter(Boolean);
|
|
4920
|
+
|
|
4921
|
+
for (const relativePath of tracked) {
|
|
4922
|
+
if (deleted.has(relativePath)) continue;
|
|
4923
|
+
await copyRelativePath(relativePath);
|
|
4924
|
+
}
|
|
4925
|
+
for (const relativePath of extras) {
|
|
4926
|
+
await copyRelativePath(relativePath);
|
|
4927
|
+
}
|
|
4928
|
+
for (const relativePath of deleted) {
|
|
4929
|
+
await fs.rm(path.join(worktreeDir, relativePath), { recursive: true, force: true }).catch(() => {});
|
|
4930
|
+
}
|
|
4931
|
+
|
|
4932
|
+
return { cwd: worktreeDir, cleanup: cleanupWorktree, isolation: "worktree" };
|
|
4933
|
+
} catch {
|
|
4934
|
+
await cleanupWorktree();
|
|
4935
|
+
return { cwd, cleanup, isolation: "cwd" };
|
|
4936
|
+
}
|
|
4937
|
+
}
|
|
4938
|
+
|
|
4846
4939
|
async function ensureGitRepo(cwd) {
|
|
4847
4940
|
const check = await gitShell("git rev-parse --is-inside-work-tree", { cwd }).catch(() => null);
|
|
4848
4941
|
if (!check || check.stdout.trim() !== "true") {
|
package/src/gateway.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import process from "node:process";
|
|
5
6
|
import { fileURLToPath } from "node:url";
|
|
@@ -589,6 +590,9 @@ function formatVerificationResultMarkup(result = {}, verifier = null) {
|
|
|
589
590
|
if (result.failedCommand) {
|
|
590
591
|
lines.push(`failed step: <code>${escapeTelegramHtml(String(result.failedCommand || ""))}</code>`);
|
|
591
592
|
}
|
|
593
|
+
if (result.isolation) {
|
|
594
|
+
lines.push(`environment: <code>${escapeTelegramHtml(String(result.isolation || ""))}</code>`);
|
|
595
|
+
}
|
|
592
596
|
if (Array.isArray(result.commands) && result.commands.length) {
|
|
593
597
|
lines.push("commands:");
|
|
594
598
|
for (const command of result.commands) {
|
|
@@ -3564,56 +3568,148 @@ class TelegramGateway {
|
|
|
3564
3568
|
return commands;
|
|
3565
3569
|
}
|
|
3566
3570
|
|
|
3571
|
+
async resolveVerificationWorkspace(cwd) {
|
|
3572
|
+
const cleanup = async () => {};
|
|
3573
|
+
let repoRoot = "";
|
|
3574
|
+
try {
|
|
3575
|
+
const { stdout } = await execFileAsync("git", ["rev-parse", "--show-toplevel"], {
|
|
3576
|
+
cwd,
|
|
3577
|
+
maxBuffer: 256 * 1024,
|
|
3578
|
+
timeout: 10000
|
|
3579
|
+
});
|
|
3580
|
+
repoRoot = String(stdout || "").trim();
|
|
3581
|
+
} catch {
|
|
3582
|
+
return { cwd, cleanup, isolation: "cwd" };
|
|
3583
|
+
}
|
|
3584
|
+
if (!repoRoot) {
|
|
3585
|
+
return { cwd, cleanup, isolation: "cwd" };
|
|
3586
|
+
}
|
|
3587
|
+
|
|
3588
|
+
const worktreeDir = await fs.mkdtemp(path.join(tmpdir(), "waterbrother-verify-"));
|
|
3589
|
+
let attached = false;
|
|
3590
|
+
const cleanupWorktree = async () => {
|
|
3591
|
+
if (attached) {
|
|
3592
|
+
try {
|
|
3593
|
+
await execFileAsync("git", ["worktree", "remove", "--force", worktreeDir], {
|
|
3594
|
+
cwd: repoRoot,
|
|
3595
|
+
maxBuffer: 1024 * 1024,
|
|
3596
|
+
timeout: 30000
|
|
3597
|
+
});
|
|
3598
|
+
} catch {}
|
|
3599
|
+
}
|
|
3600
|
+
await fs.rm(worktreeDir, { recursive: true, force: true }).catch(() => {});
|
|
3601
|
+
};
|
|
3602
|
+
|
|
3603
|
+
try {
|
|
3604
|
+
await execFileAsync("git", ["worktree", "add", "--detach", worktreeDir, "HEAD"], {
|
|
3605
|
+
cwd: repoRoot,
|
|
3606
|
+
maxBuffer: 1024 * 1024,
|
|
3607
|
+
timeout: 30000
|
|
3608
|
+
});
|
|
3609
|
+
attached = true;
|
|
3610
|
+
|
|
3611
|
+
const copyRelativePath = async (relativePath) => {
|
|
3612
|
+
const sourcePath = path.join(repoRoot, relativePath);
|
|
3613
|
+
const targetPath = path.join(worktreeDir, relativePath);
|
|
3614
|
+
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
|
3615
|
+
await fs.cp(sourcePath, targetPath, { recursive: true, force: true });
|
|
3616
|
+
};
|
|
3617
|
+
|
|
3618
|
+
const trackedChanged = await execFileAsync("git", ["diff", "--name-only", "-z", "HEAD"], {
|
|
3619
|
+
cwd: repoRoot,
|
|
3620
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
3621
|
+
timeout: 30000
|
|
3622
|
+
}).catch(() => ({ stdout: "" }));
|
|
3623
|
+
const deletedTracked = await execFileAsync("git", ["diff", "--name-only", "--diff-filter=D", "-z", "HEAD"], {
|
|
3624
|
+
cwd: repoRoot,
|
|
3625
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
3626
|
+
timeout: 30000
|
|
3627
|
+
}).catch(() => ({ stdout: "" }));
|
|
3628
|
+
const untracked = await execFileAsync("git", ["ls-files", "--others", "--exclude-standard", "-z"], {
|
|
3629
|
+
cwd: repoRoot,
|
|
3630
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
3631
|
+
timeout: 30000
|
|
3632
|
+
}).catch(() => ({ stdout: "" }));
|
|
3633
|
+
|
|
3634
|
+
const deleted = new Set(String(deletedTracked.stdout || "").split("\0").map((item) => item.trim()).filter(Boolean));
|
|
3635
|
+
const tracked = String(trackedChanged.stdout || "").split("\0").map((item) => item.trim()).filter(Boolean);
|
|
3636
|
+
const extras = String(untracked.stdout || "").split("\0").map((item) => item.trim()).filter(Boolean);
|
|
3637
|
+
|
|
3638
|
+
for (const relativePath of tracked) {
|
|
3639
|
+
if (deleted.has(relativePath)) continue;
|
|
3640
|
+
await copyRelativePath(relativePath);
|
|
3641
|
+
}
|
|
3642
|
+
for (const relativePath of extras) {
|
|
3643
|
+
await copyRelativePath(relativePath);
|
|
3644
|
+
}
|
|
3645
|
+
for (const relativePath of deleted) {
|
|
3646
|
+
await fs.rm(path.join(worktreeDir, relativePath), { recursive: true, force: true }).catch(() => {});
|
|
3647
|
+
}
|
|
3648
|
+
|
|
3649
|
+
return { cwd: worktreeDir, cleanup: cleanupWorktree, isolation: "worktree" };
|
|
3650
|
+
} catch {
|
|
3651
|
+
await cleanupWorktree();
|
|
3652
|
+
return { cwd, cleanup, isolation: "cwd" };
|
|
3653
|
+
}
|
|
3654
|
+
}
|
|
3655
|
+
|
|
3567
3656
|
async runLocalVerification(cwd, verifier = null) {
|
|
3568
3657
|
const planned = await this.planVerificationCommands(cwd);
|
|
3658
|
+
const workspace = await this.resolveVerificationWorkspace(cwd);
|
|
3569
3659
|
const commands = [];
|
|
3570
3660
|
const startedAt = new Date().toISOString();
|
|
3571
3661
|
const logs = [];
|
|
3572
3662
|
let passedCount = 0;
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3663
|
+
try {
|
|
3664
|
+
for (const [bin, args, label] of planned) {
|
|
3665
|
+
commands.push(label);
|
|
3666
|
+
try {
|
|
3667
|
+
await execFileAsync(bin, args, {
|
|
3668
|
+
cwd: workspace.cwd,
|
|
3669
|
+
env: { ...process.env },
|
|
3670
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
3671
|
+
timeout: 10 * 60 * 1000
|
|
3672
|
+
});
|
|
3673
|
+
passedCount += 1;
|
|
3674
|
+
} catch (error) {
|
|
3675
|
+
const combined = `${String(error?.stderr || "")}\n${String(error?.stdout || "")}`.trim();
|
|
3676
|
+
const extracted = combined
|
|
3677
|
+
.split("\n")
|
|
3678
|
+
.map((line) => String(line || "").trim())
|
|
3679
|
+
.filter(Boolean)
|
|
3680
|
+
.slice(0, 6);
|
|
3681
|
+
logs.push(...extracted);
|
|
3682
|
+
const outcome = error?.killed || error?.signal === "SIGTERM" ? "timeout" : (passedCount > 0 ? "partial" : "failed");
|
|
3683
|
+
return {
|
|
3684
|
+
id: "",
|
|
3685
|
+
workItemId: "",
|
|
3686
|
+
verifierAgentId: String(verifier?.id || "").trim(),
|
|
3687
|
+
outcome,
|
|
3688
|
+
summary: `${label} failed${passedCount > 0 ? ` after ${passedCount} passing check${passedCount === 1 ? "" : "s"}` : ""}.`,
|
|
3689
|
+
failedCommand: label,
|
|
3690
|
+
isolation: workspace.isolation,
|
|
3691
|
+
commands,
|
|
3692
|
+
startedAt,
|
|
3693
|
+
completedAt: new Date().toISOString(),
|
|
3694
|
+
logs
|
|
3695
|
+
};
|
|
3696
|
+
}
|
|
3604
3697
|
}
|
|
3698
|
+
return {
|
|
3699
|
+
id: "",
|
|
3700
|
+
workItemId: "",
|
|
3701
|
+
verifierAgentId: String(verifier?.id || "").trim(),
|
|
3702
|
+
outcome: "passed",
|
|
3703
|
+
summary: `${commands.length} verification command${commands.length === 1 ? "" : "s"} passed.`,
|
|
3704
|
+
isolation: workspace.isolation,
|
|
3705
|
+
commands,
|
|
3706
|
+
startedAt,
|
|
3707
|
+
completedAt: new Date().toISOString(),
|
|
3708
|
+
logs: []
|
|
3709
|
+
};
|
|
3710
|
+
} finally {
|
|
3711
|
+
await workspace.cleanup();
|
|
3605
3712
|
}
|
|
3606
|
-
return {
|
|
3607
|
-
id: "",
|
|
3608
|
-
workItemId: "",
|
|
3609
|
-
verifierAgentId: String(verifier?.id || "").trim(),
|
|
3610
|
-
outcome: "passed",
|
|
3611
|
-
summary: `${commands.length} verification command${commands.length === 1 ? "" : "s"} passed.`,
|
|
3612
|
-
commands,
|
|
3613
|
-
startedAt,
|
|
3614
|
-
completedAt: new Date().toISOString(),
|
|
3615
|
-
logs: []
|
|
3616
|
-
};
|
|
3617
3713
|
}
|
|
3618
3714
|
|
|
3619
3715
|
async startTypingLoop(chatId) {
|
package/src/shared-project.js
CHANGED
|
@@ -99,6 +99,7 @@ function normalizeVerificationResult(result = {}) {
|
|
|
99
99
|
outcome: ["passed", "failed", "partial", "timeout"].includes(outcome) ? outcome : "failed",
|
|
100
100
|
summary: String(result.summary || "").trim(),
|
|
101
101
|
failedCommand: String(result.failedCommand || "").trim(),
|
|
102
|
+
isolation: String(result.isolation || "").trim(),
|
|
102
103
|
commands,
|
|
103
104
|
startedAt: String(result.startedAt || new Date().toISOString()).trim(),
|
|
104
105
|
completedAt: String(result.completedAt || result.startedAt || new Date().toISOString()).trim(),
|