@bondsmith/bondsmithd 0.1.8 → 0.1.9

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.
Files changed (3) hide show
  1. package/README.md +15 -0
  2. package/dist/index.js +92 -9
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -81,6 +81,21 @@ The daemon reads it on its 5-second tick and shows it on the session. Anything e
81
81
  a report dated in the future. A `waiting` or `done` report is dropped once the pane writes output more
82
82
  than 2 seconds after it, since that means the agent moved on; a `working` report is not.
83
83
 
84
+ An agent can also share the plan it is working against, which Bondsmith shows above the session's
85
+ changes. Run this from inside the session's pane once the plan is agreed, and again whenever it changes:
86
+
87
+ ```sh
88
+ tmux set-option -t "$TMUX_PANE" -q @bondsmith-plan "$(cat <<'EOF'
89
+ # What the session is doing
90
+ - step one
91
+ - step two
92
+ EOF
93
+ )"
94
+ ```
95
+
96
+ The value is markdown, cut at 32,768 characters. The daemon reads it only when the browser asks, so
97
+ unlike the state report it has no timestamp and is not dropped when the pane writes output.
98
+
84
99
  An agent with no hook can still get attention by ringing the terminal bell (`printf '\a'`). A bell
85
100
  rung in a session nobody has looked at marks it as waiting, whatever its last report said, until tmux
86
101
  clears the flag when someone attaches to the session. Agents that can ring the bell for approvals or
package/dist/index.js CHANGED
@@ -16,7 +16,7 @@ var REPLAYED_COMMAND_MESSAGE = "command already used (replay rejected)";
16
16
  var INVALID_MESSAGE_ERROR = "invalid message";
17
17
 
18
18
  // ../../packages/local-agent-protocol/src/protocol-version.ts
19
- var PROTOCOL_VERSION = 7;
19
+ var PROTOCOL_VERSION = 8;
20
20
 
21
21
  // ../../packages/local-agent-protocol/src/project-path.ts
22
22
  var PROJECT_PATH_INVALID_REASONS = [
@@ -62,6 +62,7 @@ var MAX_INPUT_CHARS = 16 * 1024;
62
62
  var MAX_WIRE_MESSAGE_BYTES = 256 * 1024;
63
63
  var MAX_DAEMON_REPLY_BYTES = 200 * 1024;
64
64
  var MAX_RANGE_LINES = 500;
65
+ var MAX_PLAN_CHARS = 32 * 1024;
65
66
  var MAX_TERMINAL_DIMENSION = 1e3;
66
67
  var terminalSizeSchema = z2.object({
67
68
  cols: z2.number().int().min(1).max(MAX_TERMINAL_DIMENSION),
@@ -202,6 +203,11 @@ var browserMessageSchema = z2.discriminatedUnion("type", [
202
203
  staged: z2.boolean(),
203
204
  requestId: z2.string().min(1).max(200)
204
205
  }),
206
+ z2.object({
207
+ type: z2.literal("workspace.getPlan"),
208
+ sessionId: z2.string(),
209
+ requestId: z2.string().min(1).max(200)
210
+ }),
205
211
  z2.object({
206
212
  type: z2.literal("project.validatePath"),
207
213
  path: z2.string().min(1).max(4096),
@@ -291,6 +297,12 @@ var daemonMessageSchema = z2.discriminatedUnion("type", [
291
297
  requestId: z2.string(),
292
298
  diff: workspaceFileDiffSchema
293
299
  }),
300
+ z2.object({
301
+ type: z2.literal("workspace.plan"),
302
+ sessionId: z2.string(),
303
+ requestId: z2.string(),
304
+ plan: z2.string().max(MAX_PLAN_CHARS).nullable()
305
+ }),
294
306
  z2.object({
295
307
  type: z2.literal("project.pathValidated"),
296
308
  requestId: z2.string(),
@@ -406,6 +418,7 @@ var SIGNABLE_ACTIONS = [
406
418
  "workspace.getStatus",
407
419
  "workspace.getDiff",
408
420
  "workspace.readFileRange",
421
+ "workspace.getPlan",
409
422
  "project.validatePath"
410
423
  ];
411
424
  var envelopeSchema = z3.object({
@@ -891,6 +904,9 @@ import { promisify } from "util";
891
904
  var execFile = promisify(execFileCb);
892
905
  var MAX_GIT_OUTPUT_BYTES = 64 * 1024 * 1024;
893
906
  var SAFE_DIFF_FLAGS = ["--no-ext-diff", "--no-textconv"];
907
+ function splitNulList(output) {
908
+ return output.split("\0").filter((entry) => entry.length > 0);
909
+ }
894
910
  var GitNotARepoError = class extends Error {
895
911
  constructor() {
896
912
  super("not a git repository");
@@ -990,7 +1006,7 @@ import { sign } from "crypto";
990
1006
  import WebSocket from "ws";
991
1007
 
992
1008
  // src/version.ts
993
- var DAEMON_VERSION = true ? "0.1.8" : "dev";
1009
+ var DAEMON_VERSION = true ? "0.1.9" : "dev";
994
1010
  var VERSION_FLAGS = ["--version", "-v"];
995
1011
  function versionLine() {
996
1012
  return `bondsmithd ${DAEMON_VERSION} (protocol ${PROTOCOL_VERSION})`;
@@ -1061,6 +1077,16 @@ async function getSessionActivity(tmuxSessionName) {
1061
1077
  return null;
1062
1078
  }
1063
1079
  }
1080
+ var PLAN_OPTION = "@bondsmith-plan";
1081
+ async function getSessionPlan(tmuxSessionName) {
1082
+ try {
1083
+ const plan = (await runTmux(["show-options", "-qv", "-t", tmuxSessionName, PLAN_OPTION])).trim();
1084
+ return plan ? plan.slice(0, MAX_PLAN_CHARS) : null;
1085
+ } catch (error2) {
1086
+ if (error2 instanceof TmuxNotFoundError) throw error2;
1087
+ return null;
1088
+ }
1089
+ }
1064
1090
  async function listTmuxSessions(formatString) {
1065
1091
  try {
1066
1092
  return await runTmux(["list-sessions", "-F", formatString]);
@@ -1709,6 +1735,22 @@ async function dispatch(conn, message, {
1709
1735
  }
1710
1736
  return;
1711
1737
  }
1738
+ case "workspace.getPlan": {
1739
+ const session = getSessionOrError(sessionManager, conn, message.sessionId, message.requestId);
1740
+ if (!session) return;
1741
+ try {
1742
+ const plan = await getSessionPlan(session.tmuxSessionName);
1743
+ conn.send({
1744
+ type: "workspace.plan",
1745
+ sessionId: message.sessionId,
1746
+ requestId: message.requestId,
1747
+ plan
1748
+ });
1749
+ } catch (err) {
1750
+ conn.send(error(errorMessageOf(err, "failed to read the session plan"), message.requestId));
1751
+ }
1752
+ return;
1753
+ }
1712
1754
  case "project.validatePath": {
1713
1755
  try {
1714
1756
  const result = await validateProjectPath2(message.path);
@@ -2796,7 +2838,7 @@ async function listChangedPaths(cwd, baseSha) {
2796
2838
  runGitDiff(["--relative", "--name-only", "-z", baseSha], cwd),
2797
2839
  runGit(["ls-files", "--others", "--exclude-standard", "-z"], cwd)
2798
2840
  ]);
2799
- const paths = [...tracked.split("\0"), ...untracked.split("\0")].filter((p) => p.length > 0);
2841
+ const paths = [...splitNulList(tracked), ...splitNulList(untracked)];
2800
2842
  return [...new Set(paths)];
2801
2843
  }
2802
2844
  async function captureBaseline(cwd, options) {
@@ -2908,7 +2950,7 @@ async function isGitRepo(cwd) {
2908
2950
  }
2909
2951
  function parseNumstat(output) {
2910
2952
  const map = /* @__PURE__ */ new Map();
2911
- const fields = output.split("\0").filter((f) => f.length > 0);
2953
+ const fields = splitNulList(output);
2912
2954
  let i = 0;
2913
2955
  while (i < fields.length) {
2914
2956
  const line = fields[i];
@@ -2949,7 +2991,7 @@ function isStatusLetter(letter) {
2949
2991
  return letter in STATUS_BY_LETTER;
2950
2992
  }
2951
2993
  function parseNameStatus(output) {
2952
- const fields = output.split("\0").filter((f) => f.length > 0);
2994
+ const fields = splitNulList(output);
2953
2995
  const changed = [];
2954
2996
  let i = 0;
2955
2997
  while (i < fields.length) {
@@ -2968,16 +3010,57 @@ function parseNameStatus(output) {
2968
3010
  }
2969
3011
  return changed;
2970
3012
  }
3013
+ var reportedFailures = /* @__PURE__ */ new Map();
3014
+ async function loadDefaultBranchView(cwd, baseSha) {
3015
+ const defaultRef = await findDefaultBranchRef(cwd);
3016
+ if (!defaultRef) {
3017
+ reportedFailures.delete(cwd);
3018
+ return null;
3019
+ }
3020
+ try {
3021
+ const defaultSha = (await runGit(["rev-parse", "--verify", "--quiet", `${defaultRef}^{commit}`], cwd)).trim();
3022
+ const forkSha = (await runGit(["merge-base", baseSha, defaultSha], cwd)).trim();
3023
+ const [differing, deletedSinceFork] = await Promise.all([
3024
+ runGitDiff(["--relative", "--name-only", "-z", defaultSha], cwd),
3025
+ runGitDiff(["--relative", "--name-only", "--diff-filter=D", "-z", forkSha, defaultSha], cwd)
3026
+ ]);
3027
+ reportedFailures.delete(cwd);
3028
+ return {
3029
+ differing: new Set(splitNulList(differing)),
3030
+ deletedSinceFork: new Set(splitNulList(deletedSinceFork))
3031
+ };
3032
+ } catch (error2) {
3033
+ const signature = `${defaultRef}: ${error2 instanceof Error ? error2.message : String(error2)}`;
3034
+ if (reportedFailures.get(cwd) !== signature) {
3035
+ reportedFailures.set(cwd, signature);
3036
+ console.error("Failed to compare with the default branch; listing files unfiltered", {
3037
+ cwd,
3038
+ defaultRef,
3039
+ error: error2
3040
+ });
3041
+ }
3042
+ return null;
3043
+ }
3044
+ }
3045
+ function dropAlreadyOnDefaultBranch(files, view) {
3046
+ if (!view) return files;
3047
+ return files.filter((file) => {
3048
+ if (file.status === "untracked") return true;
3049
+ if (file.status === "deleted") return !view.deletedSinceFork.has(file.path);
3050
+ return view.differing.has(file.path) || file.previousPath !== void 0 && view.differing.has(file.previousPath);
3051
+ });
3052
+ }
2971
3053
  async function getWorkspaceStatus(cwd, baseline) {
2972
3054
  const branch = await getCurrentBranch(cwd);
2973
3055
  if (branch === null && !await isGitRepo(cwd)) {
2974
3056
  return { isGitRepo: false, branch: null, files: [] };
2975
3057
  }
2976
3058
  const base = await resolveBaseFor(cwd, baseline);
2977
- const [nameStatusRaw, numstatRaw, untrackedRaw] = await Promise.all([
3059
+ const [nameStatusRaw, numstatRaw, untrackedRaw, defaultBranchView] = await Promise.all([
2978
3060
  runGitDiff(["--relative", "--name-status", "-z", "-M", base.sha], cwd),
2979
3061
  runGitDiff(["--relative", "--numstat", "-z", "-M", base.sha], cwd),
2980
- runGit(["ls-files", "--others", "--exclude-standard", "-z"], cwd)
3062
+ runGit(["ls-files", "--others", "--exclude-standard", "-z"], cwd),
3063
+ loadDefaultBranchView(cwd, base.sha)
2981
3064
  ]);
2982
3065
  const numstat = parseNumstat(numstatRaw);
2983
3066
  const tracked = parseNameStatus(nameStatusRaw).map((changed) => {
@@ -2992,7 +3075,7 @@ async function getWorkspaceStatus(cwd, baseline) {
2992
3075
  binary: counts?.binary ?? false
2993
3076
  };
2994
3077
  });
2995
- const untracked = untrackedRaw.split("\0").filter((path) => path.length > 0).map((path) => ({
3078
+ const untracked = splitNulList(untrackedRaw).map((path) => ({
2996
3079
  path,
2997
3080
  status: "untracked",
2998
3081
  staged: false,
@@ -3000,7 +3083,7 @@ async function getWorkspaceStatus(cwd, baseline) {
3000
3083
  deletions: null,
3001
3084
  binary: false
3002
3085
  }));
3003
- const files = [...tracked, ...untracked];
3086
+ const files = dropAlreadyOnDefaultBranch([...tracked, ...untracked], defaultBranchView);
3004
3087
  return {
3005
3088
  isGitRepo: true,
3006
3089
  branch,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bondsmith/bondsmithd",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "private": false,
5
5
  "description": "Local daemon for Bondsmith agent sessions — runs on the user's own machine, pairs with a Bondsmith account, and exposes tmux-backed terminal sessions to the Bondsmith web app over a signed WebSocket protocol.",
6
6
  "homepage": "https://bondsmith.dev",