@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tritard/waterbrother",
3
- "version": "0.16.111",
3
+ "version": "0.16.112",
4
4
  "description": "Waterbrother: bring-your-own-model coding CLI with local tools, sessions, operator modes, and approval controls",
5
5
  "type": "module",
6
6
  "bin": {
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
- for (const [bin, args, label] of planned) {
3252
- commands.push(label);
3253
- try {
3254
- await execFileAsync(bin, args, {
3255
- cwd,
3256
- env: { ...process.env },
3257
- maxBuffer: 8 * 1024 * 1024,
3258
- timeout: 10 * 60 * 1000
3259
- });
3260
- passedCount += 1;
3261
- } catch (error) {
3262
- const combined = `${String(error?.stderr || "")}\n${String(error?.stdout || "")}`.trim();
3263
- const extracted = combined
3264
- .split("\n")
3265
- .map((line) => String(line || "").trim())
3266
- .filter(Boolean)
3267
- .slice(0, 6);
3268
- logs.push(...extracted);
3269
- return {
3270
- outcome: error?.killed || error?.signal === "SIGTERM" ? "timeout" : (passedCount > 0 ? "partial" : "failed"),
3271
- summary: `${label} failed${passedCount > 0 ? ` after ${passedCount} passing check${passedCount === 1 ? "" : "s"}` : ""}.`,
3272
- failedCommand: label,
3273
- commands,
3274
- startedAt,
3275
- completedAt: new Date().toISOString(),
3276
- logs
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
- for (const [bin, args, label] of planned) {
3574
- commands.push(label);
3575
- try {
3576
- await execFileAsync(bin, args, {
3577
- cwd,
3578
- env: { ...process.env },
3579
- maxBuffer: 8 * 1024 * 1024,
3580
- timeout: 10 * 60 * 1000
3581
- });
3582
- passedCount += 1;
3583
- } catch (error) {
3584
- const combined = `${String(error?.stderr || "")}\n${String(error?.stdout || "")}`.trim();
3585
- const extracted = combined
3586
- .split("\n")
3587
- .map((line) => String(line || "").trim())
3588
- .filter(Boolean)
3589
- .slice(0, 6);
3590
- logs.push(...extracted);
3591
- const outcome = error?.killed || error?.signal === "SIGTERM" ? "timeout" : (passedCount > 0 ? "partial" : "failed");
3592
- return {
3593
- id: "",
3594
- workItemId: "",
3595
- verifierAgentId: String(verifier?.id || "").trim(),
3596
- outcome,
3597
- summary: `${label} failed${passedCount > 0 ? ` after ${passedCount} passing check${passedCount === 1 ? "" : "s"}` : ""}.`,
3598
- failedCommand: label,
3599
- commands,
3600
- startedAt,
3601
- completedAt: new Date().toISOString(),
3602
- logs
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) {
@@ -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(),