@xbghc/warden 0.14.0 → 0.16.0

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/dist/cli.js CHANGED
@@ -1,14 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // bin/cli.ts
4
- import path10 from "path";
4
+ import path11 from "path";
5
5
  import { spawn } from "child_process";
6
6
  import { existsSync as existsSync2 } from "fs";
7
7
  import { fileURLToPath } from "url";
8
8
 
9
9
  // packages/server/src/index.ts
10
10
  import net from "net";
11
- import { randomUUID as randomUUID2 } from "crypto";
11
+ import { randomUUID as randomUUID4 } from "crypto";
12
12
 
13
13
  // node_modules/.pnpm/@hono+node-server@1.19.17_hono@4.13.7/node_modules/@hono/node-server/dist/index.mjs
14
14
  import { createServer as createServerHTTP } from "http";
@@ -652,8 +652,9 @@ var serve = (options, listeningListener) => {
652
652
  };
653
653
 
654
654
  // packages/server/src/app.ts
655
- import path8 from "path";
656
- import { randomUUID } from "crypto";
655
+ import path9 from "path";
656
+ import { stat as stat7 } from "fs/promises";
657
+ import { randomUUID as randomUUID3 } from "crypto";
657
658
 
658
659
  // node_modules/.pnpm/hono@4.13.7/node_modules/hono/dist/compose.js
659
660
  var compose = (middleware, onError, onNotFound) => {
@@ -812,26 +813,26 @@ var throwNestingLimitExceeded = () => {
812
813
  };
813
814
 
814
815
  // node_modules/.pnpm/hono@4.13.7/node_modules/hono/dist/utils/url.js
815
- var splitPath = (path11) => {
816
- const paths = path11.split("/");
816
+ var splitPath = (path12) => {
817
+ const paths = path12.split("/");
817
818
  if (paths[0] === "") {
818
819
  paths.shift();
819
820
  }
820
821
  return paths;
821
822
  };
822
823
  var splitRoutingPath = (routePath) => {
823
- const { groups, path: path11 } = extractGroupsFromPath(routePath);
824
- const paths = splitPath(path11);
824
+ const { groups, path: path12 } = extractGroupsFromPath(routePath);
825
+ const paths = splitPath(path12);
825
826
  return replaceGroupMarks(paths, groups);
826
827
  };
827
- var extractGroupsFromPath = (path11) => {
828
+ var extractGroupsFromPath = (path12) => {
828
829
  const groups = [];
829
- path11 = path11.replace(/\{[^}]+\}/g, (match2, index) => {
830
+ path12 = path12.replace(/\{[^}]+\}/g, (match2, index) => {
830
831
  const mark = `@${index}`;
831
832
  groups.push([mark, match2]);
832
833
  return mark;
833
834
  });
834
- return { groups, path: path11 };
835
+ return { groups, path: path12 };
835
836
  };
836
837
  var replaceGroupMarks = (paths, groups) => {
837
838
  for (let i = groups.length - 1; i >= 0; i--) {
@@ -888,8 +889,8 @@ var getPath = (request) => {
888
889
  const queryIndex = url.indexOf("?", i);
889
890
  const hashIndex = url.indexOf("#", i);
890
891
  const end = queryIndex === -1 ? hashIndex === -1 ? void 0 : hashIndex : hashIndex === -1 ? queryIndex : Math.min(queryIndex, hashIndex);
891
- const path11 = url.slice(start, end);
892
- return tryDecodeURI(path11.includes("%25") ? path11.replace(/%25/g, "%2525") : path11);
892
+ const path12 = url.slice(start, end);
893
+ return tryDecodeURI(path12.includes("%25") ? path12.replace(/%25/g, "%2525") : path12);
893
894
  } else if (charCode === 63 || charCode === 35) {
894
895
  break;
895
896
  }
@@ -906,11 +907,11 @@ var mergePath = (base, sub, ...rest) => {
906
907
  }
907
908
  return `${base?.[0] === "/" ? "" : "/"}${base}${sub === "/" ? "" : `${base?.at(-1) === "/" ? "" : "/"}${sub?.[0] === "/" ? sub.slice(1) : sub}`}`;
908
909
  };
909
- var checkOptionalParameter = (path11) => {
910
- if (path11.charCodeAt(path11.length - 1) !== 63 || !path11.includes(":")) {
910
+ var checkOptionalParameter = (path12) => {
911
+ if (path12.charCodeAt(path12.length - 1) !== 63 || !path12.includes(":")) {
911
912
  return null;
912
913
  }
913
- const segments = path11.split("/");
914
+ const segments = path12.split("/");
914
915
  const results = [];
915
916
  let basePath = "";
916
917
  segments.forEach((segment) => {
@@ -1052,9 +1053,9 @@ var HonoRequest = class {
1052
1053
  */
1053
1054
  path;
1054
1055
  bodyCache = {};
1055
- constructor(request, path11 = "/", matchResult = [[]]) {
1056
+ constructor(request, path12 = "/", matchResult = [[]]) {
1056
1057
  this.raw = request;
1057
- this.path = path11;
1058
+ this.path = path12;
1058
1059
  this.#matchResult = matchResult;
1059
1060
  }
1060
1061
  param(key) {
@@ -1826,8 +1827,8 @@ var Hono = class _Hono {
1826
1827
  return this;
1827
1828
  };
1828
1829
  });
1829
- this.on = (method, path11, ...handlers) => {
1830
- for (const p of [path11].flat()) {
1830
+ this.on = (method, path12, ...handlers) => {
1831
+ for (const p of [path12].flat()) {
1831
1832
  this.#path = p;
1832
1833
  for (const m of [method].flat()) {
1833
1834
  const methodName = m.toUpperCase();
@@ -1885,8 +1886,8 @@ var Hono = class _Hono {
1885
1886
  * app.route("/api", app2) // GET /api/user
1886
1887
  * ```
1887
1888
  */
1888
- route(path11, app) {
1889
- const subApp = this.basePath(path11);
1889
+ route(path12, app) {
1890
+ const subApp = this.basePath(path12);
1890
1891
  app.routes.map((r) => {
1891
1892
  let handler;
1892
1893
  if (app.errorHandler === errorHandler) {
@@ -1912,9 +1913,9 @@ var Hono = class _Hono {
1912
1913
  * const api = new Hono().basePath('/api')
1913
1914
  * ```
1914
1915
  */
1915
- basePath(path11) {
1916
+ basePath(path12) {
1916
1917
  const subApp = this.#clone();
1917
- subApp._basePath = mergePath(this._basePath, path11);
1918
+ subApp._basePath = mergePath(this._basePath, path12);
1918
1919
  return subApp;
1919
1920
  }
1920
1921
  /**
@@ -1988,7 +1989,7 @@ var Hono = class _Hono {
1988
1989
  * })
1989
1990
  * ```
1990
1991
  */
1991
- mount(path11, applicationHandler, options) {
1992
+ mount(path12, applicationHandler, options) {
1992
1993
  let replaceRequest;
1993
1994
  let optionHandler;
1994
1995
  if (options) {
@@ -2015,7 +2016,7 @@ var Hono = class _Hono {
2015
2016
  return [c.env, executionContext];
2016
2017
  };
2017
2018
  replaceRequest ||= (() => {
2018
- const mergedPath = mergePath(this._basePath, path11);
2019
+ const mergedPath = mergePath(this._basePath, path12);
2019
2020
  const pathPrefixLength = mergedPath === "/" ? 0 : mergedPath.length;
2020
2021
  return (request) => {
2021
2022
  const url = new URL(request.url);
@@ -2030,18 +2031,18 @@ var Hono = class _Hono {
2030
2031
  }
2031
2032
  await next();
2032
2033
  };
2033
- this.#addRoute(METHOD_NAME_ALL, mergePath(path11, "*"), handler);
2034
+ this.#addRoute(METHOD_NAME_ALL, mergePath(path12, "*"), handler);
2034
2035
  return this;
2035
2036
  }
2036
- #addRoute(method, path11, handler, baseRoutePath) {
2037
- path11 = mergePath(this._basePath, path11);
2037
+ #addRoute(method, path12, handler, baseRoutePath) {
2038
+ path12 = mergePath(this._basePath, path12);
2038
2039
  const r = {
2039
2040
  basePath: baseRoutePath !== void 0 ? mergePath(this._basePath, baseRoutePath) : this._basePath,
2040
- path: path11,
2041
+ path: path12,
2041
2042
  method,
2042
2043
  handler
2043
2044
  };
2044
- this.router.add(method, path11, [handler, r]);
2045
+ this.router.add(method, path12, [handler, r]);
2045
2046
  this.routes.push(r);
2046
2047
  }
2047
2048
  #handleError(err, c) {
@@ -2054,10 +2055,10 @@ var Hono = class _Hono {
2054
2055
  if (method === "HEAD") {
2055
2056
  return (async () => new Response(null, await this.#dispatch(request, executionCtx, env, "GET")))();
2056
2057
  }
2057
- const path11 = this.getPath(request, { env });
2058
- const matchResult = this.router.match(method, path11);
2058
+ const path12 = this.getPath(request, { env });
2059
+ const matchResult = this.router.match(method, path12);
2059
2060
  const c = new Context(request, {
2060
- path: path11,
2061
+ path: path12,
2061
2062
  matchResult,
2062
2063
  env,
2063
2064
  executionCtx,
@@ -2160,7 +2161,7 @@ var createNullObject = () => /* @__PURE__ */ Object.create(null);
2160
2161
 
2161
2162
  // node_modules/.pnpm/hono@4.13.7/node_modules/hono/dist/router/reg-exp-router/matcher.js
2162
2163
  var emptyParam = [];
2163
- function match(method, path11) {
2164
+ function match(method, path12) {
2164
2165
  const matchers = this.buildAllMatchers();
2165
2166
  const match2 = ((method2, path22) => {
2166
2167
  const matcher = matchers[method2] || matchers[METHOD_NAME_ALL];
@@ -2176,7 +2177,7 @@ function match(method, path11) {
2176
2177
  return [matcher[1][index], match3];
2177
2178
  });
2178
2179
  this.match = match2;
2179
- return match2(method, path11);
2180
+ return match2(method, path12);
2180
2181
  }
2181
2182
 
2182
2183
  // node_modules/.pnpm/hono@4.13.7/node_modules/hono/dist/router/reg-exp-router/node.js
@@ -2293,14 +2294,14 @@ var Trie = class {
2293
2294
  #index = 0;
2294
2295
  // dynamic path -> [handler index, param assoc]; static paths are not registered
2295
2296
  paths = createNullObject();
2296
- insert(path11, isStatic) {
2297
+ insert(path12, isStatic) {
2297
2298
  if (isStatic) {
2298
- this.#root.insert(path11.split(""), 0, [], this.#context, true);
2299
+ this.#root.insert(path12.split(""), 0, [], this.#context, true);
2299
2300
  return;
2300
2301
  }
2301
2302
  const paramAssoc = [];
2302
2303
  const groups = [];
2303
- let markedPath = path11;
2304
+ let markedPath = path12;
2304
2305
  for (let i = 0; ; ) {
2305
2306
  let replaced = false;
2306
2307
  markedPath = markedPath.replace(/\{[^}]+\}/g, (m) => {
@@ -2325,7 +2326,7 @@ var Trie = class {
2325
2326
  }
2326
2327
  }
2327
2328
  this.#root.insert(tokens, this.#index, paramAssoc, this.#context, false);
2328
- this.paths[path11] = [this.#index++, paramAssoc];
2329
+ this.paths[path12] = [this.#index++, paramAssoc];
2329
2330
  }
2330
2331
  buildRegExp() {
2331
2332
  let regexp = this.#root.buildRegExpStr();
@@ -2352,17 +2353,17 @@ var Trie = class {
2352
2353
 
2353
2354
  // node_modules/.pnpm/hono@4.13.7/node_modules/hono/dist/router/reg-exp-router/router.js
2354
2355
  var wildcardRegExpCache = createNullObject();
2355
- function buildWildcardRegExp(path11) {
2356
- return wildcardRegExpCache[path11] ??= new RegExp(
2357
- `^${path11.replace(
2356
+ function buildWildcardRegExp(path12) {
2357
+ return wildcardRegExpCache[path12] ??= new RegExp(
2358
+ `^${path12.replace(
2358
2359
  /\/:[^/{}]+(?:\{\[\^\/]\+})?(?=[/{]|$)|\/?\*$|([.\\+*[^\]$()?{}|])/g,
2359
2360
  (match2, metaChar) => metaChar ? `\\${metaChar}` : match2 === "/*" ? TAIL_WILDCARD_REG_EXP_STR : match2 === "*" ? ONLY_WILDCARD_REG_EXP_STR : `/:${LABEL_REG_EXP_STR}`
2360
2361
  )}$`
2361
2362
  );
2362
2363
  }
2363
- function findMiddleware(middleware, path11) {
2364
+ function findMiddleware(middleware, path12) {
2364
2365
  for (const k of Object.keys(middleware).sort((a, b) => b.length - a.length)) {
2365
- if (buildWildcardRegExp(k).test(path11)) {
2366
+ if (buildWildcardRegExp(k).test(path12)) {
2366
2367
  return [...middleware[k]];
2367
2368
  }
2368
2369
  }
@@ -2378,14 +2379,14 @@ var RegExpRouter = class {
2378
2379
  this.#routes = { [METHOD_NAME_ALL]: createNullObject() };
2379
2380
  this.#tries = { [METHOD_NAME_ALL]: new Trie() };
2380
2381
  }
2381
- #insertPath(method, path11) {
2382
+ #insertPath(method, path12) {
2382
2383
  try {
2383
- this.#tries[method].insert(path11, !/\*|\/:/.test(path11));
2384
+ this.#tries[method].insert(path12, !/\*|\/:/.test(path12));
2384
2385
  } catch (e) {
2385
- throw e === PATH_ERROR ? new UnsupportedPathError(path11) : e;
2386
+ throw e === PATH_ERROR ? new UnsupportedPathError(path12) : e;
2386
2387
  }
2387
2388
  }
2388
- add(method, path11, handler) {
2389
+ add(method, path12, handler) {
2389
2390
  const middleware = this.#middleware;
2390
2391
  const routes = this.#routes;
2391
2392
  if (!middleware) {
@@ -2401,28 +2402,28 @@ var RegExpRouter = class {
2401
2402
  }
2402
2403
  }
2403
2404
  }
2404
- if (path11 === "/*") {
2405
- path11 = "*";
2405
+ if (path12 === "/*") {
2406
+ path12 = "*";
2406
2407
  }
2407
2408
  const methods = method === METHOD_NAME_ALL ? Object.keys(middleware) : [method];
2408
- if (/\*$/.test(path11)) {
2409
- const re = buildWildcardRegExp(path11);
2409
+ if (/\*$/.test(path12)) {
2410
+ const re = buildWildcardRegExp(path12);
2410
2411
  for (const m of methods) {
2411
- if (!middleware[m][path11]) {
2412
- this.#insertPath(m, path11);
2413
- middleware[m][path11] = findMiddleware(middleware[m], path11) || findMiddleware(middleware[METHOD_NAME_ALL], path11) || [];
2412
+ if (!middleware[m][path12]) {
2413
+ this.#insertPath(m, path12);
2414
+ middleware[m][path12] = findMiddleware(middleware[m], path12) || findMiddleware(middleware[METHOD_NAME_ALL], path12) || [];
2414
2415
  }
2415
2416
  }
2416
2417
  for (const handlerMap of [middleware, routes]) {
2417
2418
  for (const m of methods) {
2418
2419
  for (const p in handlerMap[m]) {
2419
- re.test(p) && handlerMap[m][p].push([handler, path11]);
2420
+ re.test(p) && handlerMap[m][p].push([handler, path12]);
2420
2421
  }
2421
2422
  }
2422
2423
  }
2423
2424
  return;
2424
2425
  }
2425
- const paths = checkOptionalParameter(path11) || [path11];
2426
+ const paths = checkOptionalParameter(path12) || [path12];
2426
2427
  for (const path22 of paths) {
2427
2428
  for (const m of methods) {
2428
2429
  if (!routes[m][path22]) {
@@ -2451,11 +2452,11 @@ var RegExpRouter = class {
2451
2452
  const handlerData = [];
2452
2453
  const [regexp, indexReplacementMap, paramReplacementMap] = trie.buildRegExp();
2453
2454
  for (const r of [middleware, routes]) {
2454
- for (const path11 in r) {
2455
- const handlers = r[path11];
2456
- const pathData = trie.paths[path11];
2455
+ for (const path12 in r) {
2456
+ const handlers = r[path12];
2457
+ const pathData = trie.paths[path12];
2457
2458
  if (!pathData) {
2458
- staticMap[path11] = [handlers.map(([h]) => [h, createNullObject()]), emptyParam];
2459
+ staticMap[path12] = [handlers.map(([h]) => [h, createNullObject()]), emptyParam];
2459
2460
  continue;
2460
2461
  }
2461
2462
  handlerData[pathData[0]] = handlers.map(([h, handlerPath]) => [
@@ -2479,13 +2480,13 @@ var SmartRouter = class {
2479
2480
  constructor(init) {
2480
2481
  this.#routers = init.routers;
2481
2482
  }
2482
- add(method, path11, handler) {
2483
+ add(method, path12, handler) {
2483
2484
  if (!this.#routes) {
2484
2485
  throw new Error(MESSAGE_MATCHER_IS_ALREADY_BUILT);
2485
2486
  }
2486
- this.#routes.push([method, path11, handler]);
2487
+ this.#routes.push([method, path12, handler]);
2487
2488
  }
2488
- match(method, path11) {
2489
+ match(method, path12) {
2489
2490
  if (!this.#routes) {
2490
2491
  throw new Error("Fatal error");
2491
2492
  }
@@ -2500,7 +2501,7 @@ var SmartRouter = class {
2500
2501
  for (let i2 = 0, len2 = routes.length; i2 < len2; i2++) {
2501
2502
  router.add(...routes[i2]);
2502
2503
  }
2503
- res = router.match(method, path11);
2504
+ res = router.match(method, path12);
2504
2505
  } catch (e) {
2505
2506
  if (e instanceof UnsupportedPathError) {
2506
2507
  continue;
@@ -2535,9 +2536,9 @@ var Node2 = class _Node2 {
2535
2536
  #patterns = [];
2536
2537
  #pattern;
2537
2538
  #params = emptyParams;
2538
- insert(method, path11, handler) {
2539
+ insert(method, path12, handler) {
2539
2540
  let curNode = this;
2540
- const parts = splitRoutingPath(path11);
2541
+ const parts = splitRoutingPath(path12);
2541
2542
  const possibleKeys = /* @__PURE__ */ new Set();
2542
2543
  let i = 0;
2543
2544
  for (const p of parts) {
@@ -2577,12 +2578,12 @@ var Node2 = class _Node2 {
2577
2578
  }
2578
2579
  }
2579
2580
  }
2580
- search(method, path11) {
2581
+ search(method, path12) {
2581
2582
  const handlerSets = [];
2582
2583
  this.#params = emptyParams;
2583
2584
  const curNode = this;
2584
2585
  let curNodes = [curNode];
2585
- const parts = splitPath(path11);
2586
+ const parts = splitPath(path12);
2586
2587
  const curNodesQueue = [];
2587
2588
  const len = parts.length;
2588
2589
  let partOffsets = null;
@@ -2624,13 +2625,13 @@ var Node2 = class _Node2 {
2624
2625
  if (matcher !== true) {
2625
2626
  if (!partOffsets) {
2626
2627
  partOffsets = [];
2627
- let offset = path11[0] === "/" ? 1 : 0;
2628
+ let offset = path12[0] === "/" ? 1 : 0;
2628
2629
  for (let p = 0; p < len; p++) {
2629
2630
  partOffsets[p] = offset;
2630
2631
  offset += parts[p].length + 1;
2631
2632
  }
2632
2633
  }
2633
- const restPathString = path11.slice(partOffsets[i]);
2634
+ const restPathString = path12.slice(partOffsets[i]);
2634
2635
  const m = matcher.exec(restPathString);
2635
2636
  if (m) {
2636
2637
  params[name] = m[0];
@@ -2690,13 +2691,13 @@ var Node2 = class _Node2 {
2690
2691
  var TrieRouter = class {
2691
2692
  name = "TrieRouter";
2692
2693
  #node = new Node2();
2693
- add(method, path11, handler) {
2694
- for (const result of checkOptionalParameter(path11) || [path11]) {
2694
+ add(method, path12, handler) {
2695
+ for (const result of checkOptionalParameter(path12) || [path12]) {
2695
2696
  this.#node.insert(method, result, handler);
2696
2697
  }
2697
2698
  }
2698
- match(method, path11) {
2699
- return this.#node.search(method, path11);
2699
+ match(method, path12) {
2700
+ return this.#node.search(method, path12);
2700
2701
  }
2701
2702
  };
2702
2703
 
@@ -2902,6 +2903,11 @@ function parseSuffix(rest) {
2902
2903
  if (!isValidRef(ref)) throw new TargetKeyError(`invalid base ref: ${ref}`);
2903
2904
  return { kind: "base", ref };
2904
2905
  }
2906
+ if (rest.startsWith("checkpoint:")) {
2907
+ const n = rest.slice("checkpoint:".length);
2908
+ if (!/^[1-9][0-9]{0,8}$/.test(n)) throw new TargetKeyError(`invalid checkpoint: ${n}`);
2909
+ return { kind: "checkpoint", id: Number(n) };
2910
+ }
2905
2911
  if (rest.startsWith("range:")) {
2906
2912
  const spec = rest.slice("range:".length);
2907
2913
  const idx = spec.indexOf("..");
@@ -2917,7 +2923,7 @@ function parseTargetKey(key) {
2917
2923
  if (typeof key !== "string" || !key) throw new TargetKeyError("empty target key");
2918
2924
  if (key.startsWith("worktree:")) {
2919
2925
  const rest = key.slice("worktree:".length);
2920
- const m = /^(.+):(working|staged|all|commit:.+|range:.+|base:.+)$/.exec(rest);
2926
+ const m = /^(.+):(working|staged|all|commit:.+|range:.+|base:.+|checkpoint:[0-9]+)$/.exec(rest);
2921
2927
  if (!m?.[1] || !m[2]) throw new TargetKeyError(`invalid worktree target key: ${key}`);
2922
2928
  const worktree = m[1];
2923
2929
  if (!worktree.startsWith("/")) throw new TargetKeyError(`worktree path must be absolute: ${worktree}`);
@@ -2942,6 +2948,9 @@ function formatTargetKey(t) {
2942
2948
  case "base":
2943
2949
  suffix = `base:${t.ref}`;
2944
2950
  break;
2951
+ case "checkpoint":
2952
+ suffix = `checkpoint:${t.id}`;
2953
+ break;
2945
2954
  }
2946
2955
  return t.worktree ? `worktree:${t.worktree}:${suffix}` : suffix;
2947
2956
  }
@@ -2992,6 +3001,17 @@ function moveBefore(list, id, before) {
2992
3001
  return rest;
2993
3002
  }
2994
3003
 
3004
+ // packages/shared/src/comments.ts
3005
+ function commentWorktree(c) {
3006
+ return tryParseTargetKey(c.targetKey)?.worktree;
3007
+ }
3008
+ function awaitsAgent(c) {
3009
+ return !c.exportedAt;
3010
+ }
3011
+ function awaitsReviewer(c) {
3012
+ return c.replies?.at(-1)?.author === "agent";
3013
+ }
3014
+
2995
3015
  // packages/server/src/errors.ts
2996
3016
  var HttpError = class extends Error {
2997
3017
  constructor(status, message, code) {
@@ -3075,6 +3095,14 @@ var GitError = class extends HttpError {
3075
3095
  exitCode;
3076
3096
  stderr;
3077
3097
  };
3098
+ function snapshotVars(snapshot) {
3099
+ if (!snapshot) return {};
3100
+ return {
3101
+ GIT_OBJECT_DIRECTORY: snapshot.objects,
3102
+ GIT_ALTERNATE_OBJECT_DIRECTORIES: snapshot.alternates,
3103
+ ...snapshot.index ? { GIT_INDEX_FILE: snapshot.index } : {}
3104
+ };
3105
+ }
3078
3106
  function assertAllowedGitArgs(args) {
3079
3107
  const sub = args[0];
3080
3108
  if (!sub || !ALLOWED_SUBCOMMANDS.has(sub)) {
@@ -3120,7 +3148,8 @@ async function runGit(args, opts) {
3120
3148
  LC_ALL: "C",
3121
3149
  GIT_PAGER: "cat",
3122
3150
  PAGER: "cat",
3123
- GIT_TERMINAL_PROMPT: "0"
3151
+ GIT_TERMINAL_PROMPT: "0",
3152
+ ...snapshotVars(opts.snapshot)
3124
3153
  },
3125
3154
  windowsHide: true
3126
3155
  },
@@ -3212,6 +3241,14 @@ async function applyOnce(cwd, patch, opts) {
3212
3241
  });
3213
3242
  }
3214
3243
  async function runGitWrite(args, opts) {
3244
+ return execWrite(args, opts, {});
3245
+ }
3246
+ async function runGitSnapshot(args, opts) {
3247
+ const allowed = args[0] === "add" && args[1] === "--all" && args.slice(2).every((a) => a === "--intent-to-add") || args[0] === "write-tree" && args[1] === "--missing-ok" && args.length === 2;
3248
+ if (!allowed) throw new GitError(`not a snapshot command: git ${args.join(" ")}`, null, "", 400, "git_subcommand_forbidden");
3249
+ return execWrite(args, opts, { GIT_OBJECT_DIRECTORY: opts.objects, GIT_INDEX_FILE: opts.index });
3250
+ }
3251
+ function execWrite(args, opts, extraEnv) {
3215
3252
  for (const arg of args) if (arg.includes("\0")) throw new GitError("NUL byte in git argument", null, "", 400, "git_bad_argument");
3216
3253
  return new Promise((resolve, reject) => {
3217
3254
  execFile(
@@ -3222,7 +3259,7 @@ async function runGitWrite(args, opts) {
3222
3259
  timeout: opts.timeoutMs ?? DEFAULT_GIT_TIMEOUT_MS,
3223
3260
  maxBuffer: 16 * 1024 * 1024,
3224
3261
  encoding: "utf8",
3225
- env: { ...process.env, LC_ALL: "C", GIT_PAGER: "cat", PAGER: "cat", GIT_TERMINAL_PROMPT: "0" },
3262
+ env: { ...process.env, LC_ALL: "C", GIT_PAGER: "cat", PAGER: "cat", GIT_TERMINAL_PROMPT: "0", ...extraEnv },
3226
3263
  windowsHide: true
3227
3264
  },
3228
3265
  (error, stdout, stderr) => {
@@ -3374,8 +3411,18 @@ async function getRepoInfo(ctx, defaultTarget) {
3374
3411
  }
3375
3412
 
3376
3413
  // packages/server/src/targets.ts
3414
+ import path4 from "path";
3415
+ import { readFile as readFile2 } from "fs/promises";
3416
+
3417
+ // packages/server/src/checkpoints.ts
3418
+ import path3 from "path";
3419
+ import { randomUUID } from "crypto";
3420
+ import { copyFile, mkdir as mkdir2, rm, stat as stat2, utimes } from "fs/promises";
3421
+
3422
+ // packages/server/src/state.ts
3423
+ import os from "os";
3377
3424
  import path2 from "path";
3378
- import { readFile } from "fs/promises";
3425
+ import { mkdir, open, readFile, rename, stat, unlink, writeFile } from "fs/promises";
3379
3426
 
3380
3427
  // packages/server/src/hash.ts
3381
3428
  import { createHash } from "crypto";
@@ -3391,6 +3438,278 @@ function hunkHash(lineContents) {
3391
3438
  );
3392
3439
  }
3393
3440
 
3441
+ // packages/server/src/state.ts
3442
+ function dataDir() {
3443
+ const xdg = process.env.XDG_DATA_HOME;
3444
+ const base = xdg?.trim() ? xdg : path2.join(os.homedir(), ".local", "share");
3445
+ return path2.join(base, "warden");
3446
+ }
3447
+ function repoHash(repoRoot) {
3448
+ return sha1(repoRoot).slice(0, 12);
3449
+ }
3450
+ function stateFilePath(repoRoot, baseDir = dataDir()) {
3451
+ return path2.join(baseDir, repoHash(repoRoot), "state.json");
3452
+ }
3453
+ function defaultPrefs() {
3454
+ return { viewMode: "unified", nvimSocketByRoot: {}, autoRefresh: true, railOpen: false, ignoreDebug: true };
3455
+ }
3456
+ function defaultState(repoRoot) {
3457
+ return { schemaVersion: 1, repoRoot, targets: {}, todos: [], checkpoints: [], prefs: defaultPrefs() };
3458
+ }
3459
+ function ensureTarget(state, key) {
3460
+ let t = state.targets[key];
3461
+ if (!t) {
3462
+ t = { viewed: {}, comments: [] };
3463
+ state.targets[key] = t;
3464
+ }
3465
+ return t;
3466
+ }
3467
+ var usable = (v) => !!v && typeof v === "object" && typeof v.id === "string";
3468
+ var usableCheckpoint = (v) => {
3469
+ const c = v;
3470
+ return !!c && typeof c === "object" && Number.isInteger(c.id) && typeof c.tree === "string" && (c.worktree === void 0 || typeof c.worktree === "string");
3471
+ };
3472
+ var usableComment = (v) => usable(v) && typeof v.anchor === "object" && !!v.anchor;
3473
+ function normalise(raw2, repoRoot) {
3474
+ const base = defaultState(repoRoot);
3475
+ if (!raw2 || typeof raw2 !== "object") return base;
3476
+ const r = raw2;
3477
+ if (r.schemaVersion !== 1) return base;
3478
+ const targets = {};
3479
+ for (const [k, v] of Object.entries(r.targets ?? {})) {
3480
+ if (!v || typeof v !== "object") continue;
3481
+ targets[k] = {
3482
+ viewed: typeof v.viewed === "object" && v.viewed ? v.viewed : {},
3483
+ comments: Array.isArray(v.comments) ? v.comments.filter(usableComment) : [],
3484
+ ...typeof v.head === "string" ? { head: v.head } : {}
3485
+ };
3486
+ }
3487
+ migrateLocalViews(targets);
3488
+ return {
3489
+ schemaVersion: 1,
3490
+ repoRoot: r.repoRoot ?? repoRoot,
3491
+ targets,
3492
+ // Issues were folded into todos (a todo links comments now); the few there were are let go,
3493
+ // and the next write leaves them out of the file.
3494
+ todos: Array.isArray(r.todos) ? r.todos.filter(usable) : [],
3495
+ checkpoints: Array.isArray(r.checkpoints) ? r.checkpoints.filter(usableCheckpoint) : [],
3496
+ prefs: {
3497
+ ...defaultPrefs(),
3498
+ ...r.prefs ?? {},
3499
+ nvimSocketByRoot: r.prefs?.nvimSocketByRoot ?? {},
3500
+ autoRefresh: typeof r.prefs?.autoRefresh === "boolean" ? r.prefs.autoRefresh : true,
3501
+ railOpen: typeof r.prefs?.railOpen === "boolean" ? r.prefs.railOpen : false,
3502
+ ignoreDebug: typeof r.prefs?.ignoreDebug === "boolean" ? r.prefs.ignoreDebug : true
3503
+ }
3504
+ };
3505
+ }
3506
+ function migrateLocalViews(targets) {
3507
+ for (const key of Object.keys(targets).sort()) {
3508
+ const scope = commentScopeKey(key);
3509
+ if (scope === key) continue;
3510
+ const src = targets[key];
3511
+ delete targets[key];
3512
+ if (src.comments.length === 0) continue;
3513
+ let dst = targets[scope];
3514
+ if (!dst) {
3515
+ dst = { viewed: {}, comments: [] };
3516
+ targets[scope] = dst;
3517
+ }
3518
+ dst.comments.push(...src.comments);
3519
+ }
3520
+ }
3521
+ var LOCK_STALE_MS = 5e3;
3522
+ var LOCK_WAIT_MS = 5e3;
3523
+ async function sleep(ms) {
3524
+ await new Promise((r) => setTimeout(r, ms));
3525
+ }
3526
+ var StateStore = class {
3527
+ constructor(file, repoRoot) {
3528
+ this.file = file;
3529
+ this.repoRoot = repoRoot;
3530
+ }
3531
+ file;
3532
+ repoRoot;
3533
+ queue = Promise.resolve();
3534
+ async load() {
3535
+ try {
3536
+ const text = await readFile(this.file, "utf8");
3537
+ return normalise(JSON.parse(text), this.repoRoot);
3538
+ } catch (e) {
3539
+ const err = e;
3540
+ if (err.code === "ENOENT") return defaultState(this.repoRoot);
3541
+ if (e instanceof SyntaxError) {
3542
+ try {
3543
+ await rename(this.file, `${this.file}.corrupt-${Date.now()}`);
3544
+ } catch {
3545
+ }
3546
+ return defaultState(this.repoRoot);
3547
+ }
3548
+ throw e;
3549
+ }
3550
+ }
3551
+ /** Apply a mutation under lock. The callback receives the freshest on-disk state. */
3552
+ update(fn) {
3553
+ const run2 = async () => {
3554
+ await mkdir(path2.dirname(this.file), { recursive: true });
3555
+ const release = await this.acquireLock();
3556
+ try {
3557
+ const state = await this.load();
3558
+ const result = await fn(state);
3559
+ await this.writeAtomic(state);
3560
+ return result;
3561
+ } finally {
3562
+ await release();
3563
+ }
3564
+ };
3565
+ const p = this.queue.then(run2, run2);
3566
+ this.queue = p.catch(() => void 0);
3567
+ return p;
3568
+ }
3569
+ async writeAtomic(state) {
3570
+ const tmp = `${this.file}.${process.pid}.${Date.now()}.tmp`;
3571
+ await writeFile(tmp, JSON.stringify(state, null, 2) + "\n", "utf8");
3572
+ await rename(tmp, this.file);
3573
+ }
3574
+ async acquireLock() {
3575
+ const lock = `${this.file}.lock`;
3576
+ const started = Date.now();
3577
+ for (; ; ) {
3578
+ try {
3579
+ const fh = await open(lock, "wx");
3580
+ await fh.writeFile(String(process.pid));
3581
+ await fh.close();
3582
+ return async () => {
3583
+ try {
3584
+ await unlink(lock);
3585
+ } catch {
3586
+ }
3587
+ };
3588
+ } catch (e) {
3589
+ const err = e;
3590
+ if (err.code !== "EEXIST") throw e;
3591
+ try {
3592
+ const st = await stat(lock);
3593
+ if (Date.now() - st.mtimeMs > LOCK_STALE_MS) {
3594
+ await unlink(lock).catch(() => void 0);
3595
+ continue;
3596
+ }
3597
+ } catch {
3598
+ continue;
3599
+ }
3600
+ if (Date.now() - started > LOCK_WAIT_MS) {
3601
+ throw new Error(`timed out waiting for state lock ${lock}`);
3602
+ }
3603
+ await sleep(20);
3604
+ }
3605
+ }
3606
+ }
3607
+ };
3608
+ function forgetWorktreeTargets(state, worktreePath) {
3609
+ const prefix = `worktree:${worktreePath}:`;
3610
+ const gone = /* @__PURE__ */ new Set();
3611
+ for (const key of Object.keys(state.targets)) {
3612
+ if (!key.startsWith(prefix)) continue;
3613
+ for (const c of state.targets[key].comments) gone.add(c.id);
3614
+ delete state.targets[key];
3615
+ }
3616
+ unlinkComments(state, gone);
3617
+ state.checkpoints = state.checkpoints.filter((c) => c.worktree !== worktreePath);
3618
+ }
3619
+ function unlinkComments(state, ids) {
3620
+ const gone = new Set(ids);
3621
+ if (gone.size === 0) return;
3622
+ for (const todo of state.todos) {
3623
+ if (!todo.commentIds?.some((id) => gone.has(id))) continue;
3624
+ const kept = todo.commentIds.filter((id) => !gone.has(id));
3625
+ if (kept.length) todo.commentIds = kept;
3626
+ else delete todo.commentIds;
3627
+ }
3628
+ }
3629
+
3630
+ // packages/server/src/checkpoints.ts
3631
+ function checkpointStore(stateFile) {
3632
+ return path3.join(path3.dirname(stateFile), "checkpoints");
3633
+ }
3634
+ async function repoObjects(cwd) {
3635
+ const r = await runGit(["rev-parse", "--git-path", "objects"], { cwd });
3636
+ return path3.resolve(cwd, r.stdout.trim());
3637
+ }
3638
+ async function repoIndex(cwd) {
3639
+ const r = await runGit(["rev-parse", "--git-path", "index"], { cwd });
3640
+ return path3.resolve(cwd, r.stdout.trim());
3641
+ }
3642
+ async function checkpointObjects(cwd, store) {
3643
+ return { objects: path3.join(store, "objects"), alternates: await repoObjects(cwd) };
3644
+ }
3645
+ async function withIndexCopy(cwd, store, fn) {
3646
+ const tmp = path3.join(store, "tmp");
3647
+ await mkdir2(path3.join(store, "objects"), { recursive: true });
3648
+ await mkdir2(tmp, { recursive: true });
3649
+ const [objects, index] = await Promise.all([checkpointObjects(cwd, store), repoIndex(cwd)]);
3650
+ const copy = path3.join(tmp, `index-${randomUUID()}`);
3651
+ try {
3652
+ const st = await stat2(index).catch(() => void 0);
3653
+ if (st) {
3654
+ await copyFile(index, copy);
3655
+ const earlier = Math.floor(st.mtimeMs / 1e3) - 1;
3656
+ await utimes(copy, earlier, earlier);
3657
+ }
3658
+ return await fn({ ...objects, index: copy });
3659
+ } finally {
3660
+ await rm(copy, { force: true });
3661
+ await rm(`${copy}.lock`, { force: true });
3662
+ }
3663
+ }
3664
+ async function snapshotWorktree(cwd, store) {
3665
+ return withIndexCopy(cwd, store, async (snapshot) => {
3666
+ await runGitSnapshot(["add", "--all"], { cwd, ...snapshot });
3667
+ const r = await runGitSnapshot(["write-tree", "--missing-ok"], { cwd, ...snapshot });
3668
+ return r.stdout.trim();
3669
+ });
3670
+ }
3671
+ async function withCheckpointIndex(cwd, store, fn) {
3672
+ return withIndexCopy(cwd, store, async (snapshot) => {
3673
+ await runGitSnapshot(["add", "--all", "--intent-to-add"], { cwd, ...snapshot });
3674
+ return fn(snapshot);
3675
+ });
3676
+ }
3677
+ var MAX_CHECKPOINTS = 20;
3678
+ var sameWorktree = (c, worktree) => c.worktree === worktree;
3679
+ function checkpointsOf(state, worktree) {
3680
+ return state.checkpoints.filter((c) => sameWorktree(c, worktree));
3681
+ }
3682
+ function findCheckpoint(state, worktree, id) {
3683
+ return state.checkpoints.find((c) => sameWorktree(c, worktree) && c.id === id);
3684
+ }
3685
+ function checkpointKey(c) {
3686
+ return formatTargetKey({ kind: "checkpoint", id: c.id, ...c.worktree ? { worktree: c.worktree } : {} });
3687
+ }
3688
+ function forgetCheckpoint(state, c) {
3689
+ state.checkpoints = state.checkpoints.filter((x) => x !== c);
3690
+ const key = checkpointKey(c);
3691
+ const gone = new Set((state.targets[key]?.comments ?? []).map((x) => x.id));
3692
+ delete state.targets[key];
3693
+ unlinkComments(state, gone);
3694
+ }
3695
+ function addCheckpoint(state, worktree, tree, head, handoff = false) {
3696
+ const mine = checkpointsOf(state, worktree);
3697
+ const id = mine.reduce((n, c) => Math.max(n, c.id), 0) + 1;
3698
+ const checkpoint = { id, ...worktree ? { worktree } : {}, tree, head, createdAt: (/* @__PURE__ */ new Date()).toISOString(), ...handoff ? { handoff } : {} };
3699
+ state.checkpoints.push(checkpoint);
3700
+ for (const old of mine.slice(0, Math.max(0, mine.length + 1 - MAX_CHECKPOINTS))) forgetCheckpoint(state, old);
3701
+ return checkpoint;
3702
+ }
3703
+ async function takeCheckpoint(store, cwd, worktree, opts = {}) {
3704
+ const tree = await snapshotWorktree(cwd, checkpointStore(store.file));
3705
+ const head = await revParse(cwd, "HEAD") ?? "";
3706
+ return store.update((s) => {
3707
+ const newest = checkpointsOf(s, worktree).at(-1);
3708
+ if (newest?.tree === tree) return { checkpoint: newest, unchanged: true };
3709
+ return { checkpoint: addCheckpoint(s, worktree, tree, head, opts.handoff), unchanged: false };
3710
+ });
3711
+ }
3712
+
3394
3713
  // packages/server/src/diffparse.ts
3395
3714
  function unquotePath(p) {
3396
3715
  if (!(p.length >= 2 && p.startsWith('"') && p.endsWith('"'))) return p;
@@ -3632,6 +3951,17 @@ function markDebugLines(diff, newSide, oldSide) {
3632
3951
  }
3633
3952
 
3634
3953
  // packages/server/src/targets.ts
3954
+ function checkpointOf(ctx) {
3955
+ if (!ctx.checkpoint) throw new Error(`checkpoint of ${ctx.key} was not looked up`);
3956
+ return ctx.checkpoint;
3957
+ }
3958
+ async function objectsFor(ctx) {
3959
+ return ctx.target.kind === "checkpoint" ? { snapshot: await checkpointObjects(ctx.cwd, checkpointOf(ctx).store) } : {};
3960
+ }
3961
+ async function runDiff(ctx, args) {
3962
+ if (ctx.target.kind !== "checkpoint") return (await runGit(args, { cwd: ctx.cwd })).stdout;
3963
+ return withCheckpointIndex(ctx.cwd, checkpointOf(ctx).store, async (snapshot) => (await runGit(args, { cwd: ctx.cwd, snapshot })).stdout);
3964
+ }
3635
3965
  var DIFF_BASE_ARGS = ["diff", "--no-color", "--no-ext-diff", "-U3", "-M", "--find-renames"];
3636
3966
  function resolveTargetContext(repo, worktrees, key) {
3637
3967
  let target;
@@ -3685,6 +4015,8 @@ async function diffArgs(ctx) {
3685
4015
  }
3686
4016
  case "base":
3687
4017
  return [...DIFF_BASE_ARGS, await baseSha(ctx, t.ref)];
4018
+ case "checkpoint":
4019
+ return [...DIFF_BASE_ARGS, checkpointOf(ctx).tree];
3688
4020
  }
3689
4021
  }
3690
4022
  function includesUntracked(t) {
@@ -3726,8 +4058,7 @@ async function mapLimit(items, limit, fn) {
3726
4058
  }
3727
4059
  async function listTargetDiffs(ctx) {
3728
4060
  const args = await diffArgs(ctx);
3729
- const r = await runGit(args, { cwd: ctx.cwd });
3730
- const files = parseUnifiedDiff(r.stdout);
4061
+ const files = parseUnifiedDiff(await runDiff(ctx, args));
3731
4062
  if (includesUntracked(ctx.target)) {
3732
4063
  const untracked = await listUntracked(ctx.cwd);
3733
4064
  const extra = await mapLimit(untracked, 8, (f) => untrackedDiff(ctx.cwd, f).catch(() => void 0));
@@ -3749,8 +4080,7 @@ async function getFileDiff(ctx, filePath, hints = {}) {
3749
4080
  const args = await diffArgs(ctx);
3750
4081
  const pathspec = [literal(filePath)];
3751
4082
  if (hints.oldPath && hints.oldPath !== filePath) pathspec.push(literal(hints.oldPath));
3752
- const r = await runGit([...args, "--", ...pathspec], { cwd: ctx.cwd });
3753
- const files = parseUnifiedDiff(r.stdout);
4083
+ const files = parseUnifiedDiff(await runDiff(ctx, [...args, "--", ...pathspec]));
3754
4084
  return files.find((f) => f.path === filePath) ?? files[0];
3755
4085
  }
3756
4086
  async function refForSide(ctx, side) {
@@ -3770,6 +4100,8 @@ async function refForSide(ctx, side) {
3770
4100
  return await mergeBase(ctx.cwd, t.base, t.head) ?? t.base;
3771
4101
  case "base":
3772
4102
  return side === "new" ? void 0 : baseSha(ctx, t.ref);
4103
+ case "checkpoint":
4104
+ return side === "new" ? void 0 : checkpointOf(ctx).tree;
3773
4105
  }
3774
4106
  }
3775
4107
  async function getFullFile(ctx, filePath, side) {
@@ -3777,14 +4109,14 @@ async function getFullFile(ctx, filePath, side) {
3777
4109
  const ref = await refForSide(ctx, side);
3778
4110
  if (ref === void 0) {
3779
4111
  try {
3780
- const buf = await readFile(path2.join(ctx.cwd, filePath));
4112
+ const buf = await readFile2(path4.join(ctx.cwd, filePath));
3781
4113
  return buf.toString("utf8");
3782
4114
  } catch {
3783
4115
  return null;
3784
4116
  }
3785
4117
  }
3786
4118
  try {
3787
- const r = await runGit(["show", `${ref}:${filePath}`], { cwd: ctx.cwd });
4119
+ const r = await runGit(["show", `${ref}:${filePath}`], { cwd: ctx.cwd, ...await objectsFor(ctx) });
3788
4120
  return r.stdout;
3789
4121
  } catch (e) {
3790
4122
  if (e instanceof HttpError && e.code === "git_failed") return null;
@@ -3801,9 +4133,10 @@ async function pathsWithMarkers(ctx, side, paths) {
3801
4133
  else if (ref === ":0") args.push("--cached");
3802
4134
  else args.push(ref);
3803
4135
  const prefix = ref === void 0 || ref === ":0" ? "" : `${ref}:`;
4136
+ const objects = await objectsFor(ctx);
3804
4137
  for (let i = 0; i < paths.length; i += GREP_CHUNK) {
3805
4138
  const chunk = paths.slice(i, i + GREP_CHUNK).map(literal);
3806
- const r = await runGit([...args, "--", ...chunk], { cwd: ctx.cwd, okCodes: [0, 1] });
4139
+ const r = await runGit([...args, "--", ...chunk], { cwd: ctx.cwd, okCodes: [0, 1], ...objects });
3807
4140
  for (const p of r.stdout.split("\0")) if (p) found.add(p.startsWith(prefix) ? p.slice(prefix.length) : p);
3808
4141
  }
3809
4142
  return found;
@@ -4076,15 +4409,15 @@ function buildStagePatch(diff, mode, selection, opts = {}) {
4076
4409
  }
4077
4410
 
4078
4411
  // packages/server/src/worktrees.ts
4079
- import path3 from "path";
4080
- import { readdir, realpath as realpath2, stat } from "fs/promises";
4412
+ import path5 from "path";
4413
+ import { readdir, realpath as realpath2, stat as stat3 } from "fs/promises";
4081
4414
  function slotPath(ctx, slot) {
4082
- return path3.join(path3.dirname(ctx.commonRoot), `${path3.basename(ctx.commonRoot)}-${slot}`);
4415
+ return path5.join(path5.dirname(ctx.commonRoot), `${path5.basename(ctx.commonRoot)}-${slot}`);
4083
4416
  }
4084
4417
  function slotOf(ctx, p) {
4085
- if (path3.dirname(p) !== path3.dirname(ctx.commonRoot)) return void 0;
4086
- const prefix = `${path3.basename(ctx.commonRoot)}-`;
4087
- const name = path3.basename(p);
4418
+ if (path5.dirname(p) !== path5.dirname(ctx.commonRoot)) return void 0;
4419
+ const prefix = `${path5.basename(ctx.commonRoot)}-`;
4420
+ const name = path5.basename(p);
4088
4421
  const n = name.startsWith(prefix) ? name.slice(prefix.length) : "";
4089
4422
  return /^[1-9]\d*$/.test(n) ? Number(n) : void 0;
4090
4423
  }
@@ -4169,7 +4502,7 @@ async function assertBranchName(ctx, name) {
4169
4502
  }
4170
4503
  }
4171
4504
  async function dirUsable(p) {
4172
- const st = await stat(p).catch((e) => {
4505
+ const st = await stat3(p).catch((e) => {
4173
4506
  if (e.code === "ENOENT") return void 0;
4174
4507
  throw e;
4175
4508
  });
@@ -4247,8 +4580,8 @@ async function checkoutWorktree(ctx, req) {
4247
4580
  if (e instanceof GitError && /already (checked out|used by worktree)/.test(e.stderr)) throw new HttpError(409, e.message, "branch_in_use");
4248
4581
  throw e;
4249
4582
  }
4250
- const real = await realpath2(target.path).catch(() => target.path);
4251
- const worktree = (await listWorktrees(ctx)).find((w) => w.path === real);
4583
+ const real2 = await realpath2(target.path).catch(() => target.path);
4584
+ const worktree = (await listWorktrees(ctx)).find((w) => w.path === real2);
4252
4585
  if (!worktree) throw new HttpError(500, `git checked ${branch} out at ${target.path} but does not list it`, "git_failed");
4253
4586
  return { worktree, slot: target.slot, reused: target.reuse };
4254
4587
  }
@@ -4266,7 +4599,7 @@ async function dropBranch(ctx, wt, asked) {
4266
4599
  }
4267
4600
  async function releaseWorktree(ctx, req) {
4268
4601
  const p = req.path.trim();
4269
- const wt = (await listWorktreesAll(ctx)).find((w) => w.path === p || p && w.path === path3.resolve(p));
4602
+ const wt = (await listWorktreesAll(ctx)).find((w) => w.path === p || p && w.path === path5.resolve(p));
4270
4603
  if (!wt) throw badRequest(`unknown worktree: ${p}`, "unknown_worktree");
4271
4604
  if (wt.isMain) throw badRequest("the main worktree cannot be released", "main_worktree");
4272
4605
  if (wt.bare || slotOf(ctx, wt.path) === void 0) throw badRequest(`${wt.path} is not a slot; remove it instead`, "not_a_slot");
@@ -4282,7 +4615,7 @@ async function releaseWorktree(ctx, req) {
4282
4615
  }
4283
4616
  async function removeWorktree(ctx, req) {
4284
4617
  const p = req.path.trim();
4285
- const wt = (await listWorktreesAll(ctx)).find((w) => w.path === p || p && w.path === path3.resolve(p));
4618
+ const wt = (await listWorktreesAll(ctx)).find((w) => w.path === p || p && w.path === path5.resolve(p));
4286
4619
  if (!wt) throw badRequest(`unknown worktree: ${p}`, "unknown_worktree");
4287
4620
  if (wt.isMain) throw badRequest("the main worktree cannot be removed", "main_worktree");
4288
4621
  const args = ["worktree", "remove"];
@@ -4302,181 +4635,6 @@ async function removeWorktree(ctx, req) {
4302
4635
  return dropBranch(ctx, wt, req.deleteBranch);
4303
4636
  }
4304
4637
 
4305
- // packages/server/src/state.ts
4306
- import os from "os";
4307
- import path4 from "path";
4308
- import { mkdir, open, readFile as readFile2, rename, stat as stat2, unlink, writeFile } from "fs/promises";
4309
- function dataDir() {
4310
- const xdg = process.env.XDG_DATA_HOME;
4311
- const base = xdg?.trim() ? xdg : path4.join(os.homedir(), ".local", "share");
4312
- return path4.join(base, "warden");
4313
- }
4314
- function repoHash(repoRoot) {
4315
- return sha1(repoRoot).slice(0, 12);
4316
- }
4317
- function stateFilePath(repoRoot, baseDir = dataDir()) {
4318
- return path4.join(baseDir, repoHash(repoRoot), "state.json");
4319
- }
4320
- function defaultPrefs() {
4321
- return { viewMode: "unified", nvimSocketByRoot: {}, autoRefresh: true, railOpen: false, ignoreDebug: true };
4322
- }
4323
- function defaultState(repoRoot) {
4324
- return { schemaVersion: 1, repoRoot, targets: {}, issues: [], todos: [], prefs: defaultPrefs() };
4325
- }
4326
- function ensureTarget(state, key) {
4327
- let t = state.targets[key];
4328
- if (!t) {
4329
- t = { viewed: {}, comments: [] };
4330
- state.targets[key] = t;
4331
- }
4332
- return t;
4333
- }
4334
- var usable = (v) => !!v && typeof v === "object" && typeof v.id === "string";
4335
- var usableComment = (v) => usable(v) && typeof v.anchor === "object" && !!v.anchor;
4336
- function normalise(raw2, repoRoot) {
4337
- const base = defaultState(repoRoot);
4338
- if (!raw2 || typeof raw2 !== "object") return base;
4339
- const r = raw2;
4340
- if (r.schemaVersion !== 1) return base;
4341
- const targets = {};
4342
- for (const [k, v] of Object.entries(r.targets ?? {})) {
4343
- if (!v || typeof v !== "object") continue;
4344
- targets[k] = {
4345
- viewed: typeof v.viewed === "object" && v.viewed ? v.viewed : {},
4346
- comments: Array.isArray(v.comments) ? v.comments.filter(usableComment) : [],
4347
- ...typeof v.head === "string" ? { head: v.head } : {}
4348
- };
4349
- }
4350
- migrateLocalViews(targets);
4351
- return {
4352
- schemaVersion: 1,
4353
- repoRoot: r.repoRoot ?? repoRoot,
4354
- targets,
4355
- issues: Array.isArray(r.issues) ? r.issues.filter(usable) : [],
4356
- todos: Array.isArray(r.todos) ? r.todos.filter(usable) : [],
4357
- prefs: {
4358
- ...defaultPrefs(),
4359
- ...r.prefs ?? {},
4360
- nvimSocketByRoot: r.prefs?.nvimSocketByRoot ?? {},
4361
- autoRefresh: typeof r.prefs?.autoRefresh === "boolean" ? r.prefs.autoRefresh : true,
4362
- railOpen: typeof r.prefs?.railOpen === "boolean" ? r.prefs.railOpen : false,
4363
- ignoreDebug: typeof r.prefs?.ignoreDebug === "boolean" ? r.prefs.ignoreDebug : true
4364
- }
4365
- };
4366
- }
4367
- function migrateLocalViews(targets) {
4368
- for (const key of Object.keys(targets).sort()) {
4369
- const scope = commentScopeKey(key);
4370
- if (scope === key) continue;
4371
- const src = targets[key];
4372
- delete targets[key];
4373
- if (src.comments.length === 0) continue;
4374
- let dst = targets[scope];
4375
- if (!dst) {
4376
- dst = { viewed: {}, comments: [] };
4377
- targets[scope] = dst;
4378
- }
4379
- dst.comments.push(...src.comments);
4380
- }
4381
- }
4382
- var LOCK_STALE_MS = 5e3;
4383
- var LOCK_WAIT_MS = 5e3;
4384
- async function sleep(ms) {
4385
- await new Promise((r) => setTimeout(r, ms));
4386
- }
4387
- var StateStore = class {
4388
- constructor(file, repoRoot) {
4389
- this.file = file;
4390
- this.repoRoot = repoRoot;
4391
- }
4392
- file;
4393
- repoRoot;
4394
- queue = Promise.resolve();
4395
- async load() {
4396
- try {
4397
- const text = await readFile2(this.file, "utf8");
4398
- return normalise(JSON.parse(text), this.repoRoot);
4399
- } catch (e) {
4400
- const err = e;
4401
- if (err.code === "ENOENT") return defaultState(this.repoRoot);
4402
- if (e instanceof SyntaxError) {
4403
- try {
4404
- await rename(this.file, `${this.file}.corrupt-${Date.now()}`);
4405
- } catch {
4406
- }
4407
- return defaultState(this.repoRoot);
4408
- }
4409
- throw e;
4410
- }
4411
- }
4412
- /** Apply a mutation under lock. The callback receives the freshest on-disk state. */
4413
- update(fn) {
4414
- const run2 = async () => {
4415
- await mkdir(path4.dirname(this.file), { recursive: true });
4416
- const release = await this.acquireLock();
4417
- try {
4418
- const state = await this.load();
4419
- const result = await fn(state);
4420
- await this.writeAtomic(state);
4421
- return result;
4422
- } finally {
4423
- await release();
4424
- }
4425
- };
4426
- const p = this.queue.then(run2, run2);
4427
- this.queue = p.catch(() => void 0);
4428
- return p;
4429
- }
4430
- async writeAtomic(state) {
4431
- const tmp = `${this.file}.${process.pid}.${Date.now()}.tmp`;
4432
- await writeFile(tmp, JSON.stringify(state, null, 2) + "\n", "utf8");
4433
- await rename(tmp, this.file);
4434
- }
4435
- async acquireLock() {
4436
- const lock = `${this.file}.lock`;
4437
- const started = Date.now();
4438
- for (; ; ) {
4439
- try {
4440
- const fh = await open(lock, "wx");
4441
- await fh.writeFile(String(process.pid));
4442
- await fh.close();
4443
- return async () => {
4444
- try {
4445
- await unlink(lock);
4446
- } catch {
4447
- }
4448
- };
4449
- } catch (e) {
4450
- const err = e;
4451
- if (err.code !== "EEXIST") throw e;
4452
- try {
4453
- const st = await stat2(lock);
4454
- if (Date.now() - st.mtimeMs > LOCK_STALE_MS) {
4455
- await unlink(lock).catch(() => void 0);
4456
- continue;
4457
- }
4458
- } catch {
4459
- continue;
4460
- }
4461
- if (Date.now() - started > LOCK_WAIT_MS) {
4462
- throw new Error(`timed out waiting for state lock ${lock}`);
4463
- }
4464
- await sleep(20);
4465
- }
4466
- }
4467
- }
4468
- };
4469
- function forgetWorktreeTargets(state, worktreePath) {
4470
- const prefix = `worktree:${worktreePath}:`;
4471
- const gone = /* @__PURE__ */ new Set();
4472
- for (const key of Object.keys(state.targets)) {
4473
- if (!key.startsWith(prefix)) continue;
4474
- for (const c of state.targets[key].comments) gone.add(c.id);
4475
- delete state.targets[key];
4476
- }
4477
- if (gone.size) for (const issue of state.issues) issue.commentIds = issue.commentIds.filter((id) => !gone.has(id));
4478
- }
4479
-
4480
4638
  // packages/server/src/export.ts
4481
4639
  var LANG_BY_EXT = {
4482
4640
  ts: "ts",
@@ -4524,36 +4682,123 @@ function langForPath(filePath) {
4524
4682
  if (!m?.[1]) return "";
4525
4683
  return LANG_BY_EXT[m[1].toLowerCase()] ?? m[1].toLowerCase();
4526
4684
  }
4685
+ function shortId(id) {
4686
+ return id.slice(0, 8);
4687
+ }
4688
+ var quote = (text) => text.replace(/\r\n/g, "\n").split("\n").map((l) => l ? `> ${l}` : ">").join("\n");
4527
4689
  function formatCommentSection(c) {
4528
4690
  const range = c.startLine === c.endLine ? `${c.startLine}` : `${c.startLine}-${c.endLine}`;
4529
4691
  const width = String(c.endLine).length;
4530
4692
  const code = c.codeSnippet.map((line, i) => `${String(c.startLine + i).padStart(width, " ")} | ${line}`).join("\n");
4531
4693
  const lang = langForPath(c.filePath);
4532
- const body = c.body.replace(/\r\n/g, "\n").split("\n").map((l) => l ? `> ${l}` : ">").join("\n");
4533
4694
  const fence = code.includes("```") ? "````" : "```";
4534
4695
  const orphanNote = c.status === "orphaned" ? " [orphaned: original location no longer exists]" : "";
4535
- return [`## ${c.filePath}:${range} (${c.side})${orphanNote}`, `${fence}${lang}`, code, fence, body].join("\n");
4696
+ const parts = [`## ${c.filePath}:${range} (${c.side}) [id: ${shortId(c.id)}]${orphanNote}`, `${fence}${lang}`, code, fence, quote(c.body)];
4697
+ for (const r of c.replies ?? []) parts.push(`${r.author === "agent" ? "Agent" : "Reviewer"} replied:
4698
+ ${quote(r.body)}`);
4699
+ return parts.join("\n");
4536
4700
  }
4537
- function formatCommentsExport({ repoRoot, comments }) {
4701
+ function replyFooter(replyCommand) {
4702
+ return `When you have dealt with a comment, answer it with \`${replyCommand} reply <id> "<what you changed, or why you did not>"\` so the reviewer sees your answer beside it.`;
4703
+ }
4704
+ function formatCommentsExport({ repoRoot, comments, replyCommand }) {
4538
4705
  const targets = [...new Set(comments.map((c) => c.targetKey))];
4539
4706
  const head = ["# Review comments", `Target: ${targets.join(", ") || "-"}`, `Repo: ${repoRoot}`, `Count: ${comments.length}`];
4540
4707
  const sections = comments.map(formatCommentSection);
4541
- return [head.join("\n"), ...sections].join("\n\n") + "\n";
4708
+ const tail = replyCommand && comments.length > 0 ? [replyFooter(replyCommand)] : [];
4709
+ return [head.join("\n"), ...sections, ...tail].join("\n\n") + "\n";
4542
4710
  }
4543
- function formatIssueExport({ repoRoot, issue, comments }) {
4544
- const targets = [...new Set(comments.map((c) => c.targetKey))];
4545
- const head = [`# Issue: ${issue.title}`, `Status: ${issue.status}`, `Target: ${targets.join(", ") || "-"}`, `Repo: ${repoRoot}`, `Count: ${comments.length}`];
4546
- const parts = [head.join("\n")];
4547
- if (issue.body.trim()) parts.push(issue.body.trim());
4548
- parts.push(...comments.map(formatCommentSection));
4549
- return parts.join("\n\n") + "\n";
4711
+ function formatTodoExport({ todo, comments, replyCommand }) {
4712
+ const body = todo.body.replace(/\r\n/g, "\n").trim();
4713
+ const parts = [todo.title, ...body ? [body] : [], ...comments.map(formatCommentSection)];
4714
+ if (replyCommand && comments.length > 0) parts.push(replyFooter(replyCommand));
4715
+ return parts.join("\n\n");
4716
+ }
4717
+
4718
+ // packages/server/src/feedback.ts
4719
+ import { realpath as realpath3 } from "fs/promises";
4720
+ import { randomUUID as randomUUID2 } from "crypto";
4721
+ async function real(p) {
4722
+ try {
4723
+ return await realpath3(p);
4724
+ } catch {
4725
+ return p;
4726
+ }
4727
+ }
4728
+ function worktreeOf(c, mainRoot) {
4729
+ return commentWorktree(c) ?? mainRoot;
4730
+ }
4731
+ async function commentsIn(state, worktreeRoot, mainRoot) {
4732
+ const want = await real(worktreeRoot);
4733
+ const resolved = /* @__PURE__ */ new Map();
4734
+ const out = [];
4735
+ for (const t of Object.values(state.targets)) {
4736
+ for (const c of t.comments) {
4737
+ const wt = worktreeOf(c, mainRoot);
4738
+ let p = resolved.get(wt);
4739
+ if (!p) {
4740
+ p = real(wt);
4741
+ resolved.set(wt, p);
4742
+ }
4743
+ if (await p === want) out.push(c);
4744
+ }
4745
+ }
4746
+ return out;
4747
+ }
4748
+ async function takeFeedback(store, opts) {
4749
+ if (opts.peek) return (await commentsIn(await store.load(), opts.worktreeRoot, opts.mainRoot)).filter(awaitsAgent);
4750
+ return store.update(async (s) => {
4751
+ const ids = new Set((await commentsIn(s, opts.worktreeRoot, opts.mainRoot)).filter(awaitsAgent).map((c) => c.id));
4752
+ const now = (/* @__PURE__ */ new Date()).toISOString();
4753
+ const taken = [];
4754
+ for (const t of Object.values(s.targets)) {
4755
+ t.comments = t.comments.map((c) => {
4756
+ if (!ids.has(c.id)) return c;
4757
+ const next = { ...c, exportedAt: now, updatedAt: now, status: c.status === "active" ? "exported" : c.status };
4758
+ taken.push(next);
4759
+ return next;
4760
+ });
4761
+ }
4762
+ return taken;
4763
+ });
4764
+ }
4765
+ function locate(state, ref) {
4766
+ const needle = ref.trim().toLowerCase();
4767
+ if (!needle) throw badRequest("a comment id is required", "bad_comment_id");
4768
+ const hits = [];
4769
+ for (const t of Object.values(state.targets)) {
4770
+ t.comments.forEach((c, index) => {
4771
+ if (c.id === needle) hits.unshift({ list: t.comments, index });
4772
+ else if (c.id.startsWith(needle)) hits.push({ list: t.comments, index });
4773
+ });
4774
+ }
4775
+ const exact = hits[0] && hits[0].list[hits[0].index].id === needle;
4776
+ if (hits.length === 0) throw notFound(`no comment with id ${ref}; it may have been resolved or deleted`, "unknown_comment");
4777
+ if (!exact && hits.length > 1) throw badRequest(`id ${ref} matches ${hits.length} comments; give more of it`, "ambiguous_comment");
4778
+ return hits[0];
4779
+ }
4780
+ async function addReply(store, ref, author, body) {
4781
+ if (!body.trim()) throw badRequest("reply must not be empty", "empty_reply");
4782
+ return store.update((s) => {
4783
+ const { list, index } = locate(s, ref);
4784
+ const now = (/* @__PURE__ */ new Date()).toISOString();
4785
+ const reply2 = { id: randomUUID2(), author, body: body.trim(), at: now };
4786
+ const prev = list[index];
4787
+ const next = { ...prev, replies: [...prev.replies ?? [], reply2], updatedAt: now };
4788
+ if (author === "reviewer") {
4789
+ delete next.exportedAt;
4790
+ if (next.status === "exported") next.status = "active";
4791
+ }
4792
+ list[index] = next;
4793
+ return next;
4794
+ });
4550
4795
  }
4551
4796
 
4552
4797
  // packages/server/src/nvim.ts
4553
4798
  import os2 from "os";
4554
- import path5 from "path";
4799
+ import path6 from "path";
4555
4800
  import { execFile as execFile2 } from "child_process";
4556
- import { readdir as readdir2, stat as stat3 } from "fs/promises";
4801
+ import { readdir as readdir2, stat as stat4 } from "fs/promises";
4557
4802
  var QUERY_TIMEOUT_MS = 500;
4558
4803
  var CACHE_TTL_MS = 1e4;
4559
4804
  function execNvim(args, timeoutMs) {
@@ -4582,14 +4827,14 @@ function socketDirs(env = process.env) {
4582
4827
  }
4583
4828
  }
4584
4829
  const tmp = env.TMPDIR || "/tmp";
4585
- if (user) dirs.push(path5.join(tmp, `nvim.${user}`));
4586
- if (env.NVIM_LISTEN_ADDRESS) dirs.push(path5.dirname(env.NVIM_LISTEN_ADDRESS));
4830
+ if (user) dirs.push(path6.join(tmp, `nvim.${user}`));
4831
+ if (env.NVIM_LISTEN_ADDRESS) dirs.push(path6.dirname(env.NVIM_LISTEN_ADDRESS));
4587
4832
  return [...new Set(dirs)];
4588
4833
  }
4589
4834
  var SOCKET_NAME = /^nvim\.(\d+)\.\d+$/;
4590
4835
  async function isSocket(p) {
4591
4836
  try {
4592
- return (await stat3(p)).isSocket();
4837
+ return (await stat4(p)).isSocket();
4593
4838
  } catch {
4594
4839
  return false;
4595
4840
  }
@@ -4604,7 +4849,7 @@ async function findSockets(dirs = socketDirs()) {
4604
4849
  return;
4605
4850
  }
4606
4851
  for (const ent of entries) {
4607
- const full = path5.join(dir, ent.name);
4852
+ const full = path6.join(dir, ent.name);
4608
4853
  const m = SOCKET_NAME.exec(ent.name);
4609
4854
  if (m && (ent.isSocket() || await isSocket(full))) {
4610
4855
  found.set(full, Number(m[1]));
@@ -4670,7 +4915,7 @@ var NvimService = class {
4670
4915
 
4671
4916
  // packages/server/src/tmux.ts
4672
4917
  import { execFile as execFile3 } from "child_process";
4673
- import { realpath as realpath3 } from "fs/promises";
4918
+ import { realpath as realpath4 } from "fs/promises";
4674
4919
  var runTmux = (args) => new Promise((resolve, reject) => {
4675
4920
  execFile3("tmux", args, { encoding: "utf8", timeout: 5e3, maxBuffer: 1024 * 1024, windowsHide: true }, (error, stdout, stderr) => {
4676
4921
  if (!error) return resolve(stdout);
@@ -4686,29 +4931,29 @@ var TmuxService = class {
4686
4931
  }
4687
4932
  run;
4688
4933
  async sessions(mainRoot) {
4689
- const root = await realpath3(mainRoot);
4934
+ const root = await realpath4(mainRoot);
4690
4935
  const output = await this.run(["list-sessions", "-F", "#{session_id} #{session_name} #{session_path}"]);
4691
4936
  const sessions = [];
4692
4937
  for (const line of output.trimEnd().split("\n")) {
4693
4938
  const [id, name, ...parts] = line.split(" ");
4694
- const path11 = parts.join(" ");
4695
- if (!id || !/^\$\d+$/.test(id) || !name || !path11) continue;
4696
- if (await realpath3(path11).catch(() => null) === root) sessions.push({ id, name, path: path11 });
4939
+ const path12 = parts.join(" ");
4940
+ if (!id || !/^\$\d+$/.test(id) || !name || !path12) continue;
4941
+ if (await realpath4(path12).catch(() => null) === root) sessions.push({ id, name, path: path12 });
4697
4942
  }
4698
4943
  return sessions;
4699
4944
  }
4700
4945
  async open(mainRoot, worktreePath, sessionId) {
4701
4946
  const session = (await this.sessions(mainRoot)).find((s) => s.id === sessionId);
4702
4947
  if (!session) throw new HttpError(409, "\u4E3B\u4ED3\u5E93\u5BF9\u5E94\u7684 tmux session \u5DF2\u4E0D\u5B58\u5728\uFF0C\u8BF7\u5237\u65B0\u540E\u91CD\u8BD5", "tmux_session_missing");
4703
- const directory = await realpath3(worktreePath);
4948
+ const directory = await realpath4(worktreePath);
4704
4949
  const window = (await this.run(["new-window", "-d", "-P", "-F", "#{window_id}", "-t", `${session.id}:`, "-c", directory.replaceAll("#", "##")])).trim();
4705
4950
  return { session: session.name, window };
4706
4951
  }
4707
4952
  };
4708
4953
 
4709
4954
  // packages/server/src/watcher.ts
4710
- import path6 from "path";
4711
- import { stat as stat4 } from "fs/promises";
4955
+ import path7 from "path";
4956
+ import { stat as stat5 } from "fs/promises";
4712
4957
  var DEFAULT_POLL_INTERVAL_MS = 1500;
4713
4958
  function statusPaths(out) {
4714
4959
  const parts = out.split("\0");
@@ -4804,7 +5049,7 @@ var RepoWatcher = class {
4804
5049
  const stats = await Promise.all(
4805
5050
  statusPaths(statusRes.stdout).map(async (rel) => {
4806
5051
  try {
4807
- const st = await stat4(path6.join(this.root, rel));
5052
+ const st = await stat5(path7.join(this.root, rel));
4808
5053
  return `${rel} ${st.mtimeMs} ${st.size}`;
4809
5054
  } catch {
4810
5055
  return `${rel} -`;
@@ -4816,8 +5061,8 @@ var RepoWatcher = class {
4816
5061
  };
4817
5062
 
4818
5063
  // packages/server/src/static.ts
4819
- import path7 from "path";
4820
- import { readFile as readFile3, stat as stat5 } from "fs/promises";
5064
+ import path8 from "path";
5065
+ import { readFile as readFile3, stat as stat6 } from "fs/promises";
4821
5066
  var MIME = {
4822
5067
  ".html": "text/html; charset=utf-8",
4823
5068
  ".js": "text/javascript; charset=utf-8",
@@ -4834,20 +5079,20 @@ var MIME = {
4834
5079
  ".txt": "text/plain; charset=utf-8"
4835
5080
  };
4836
5081
  async function serveStaticFile(c, webDir) {
4837
- const root = path7.resolve(webDir);
5082
+ const root = path8.resolve(webDir);
4838
5083
  let rel;
4839
5084
  try {
4840
5085
  rel = decodeURIComponent(c.req.path);
4841
5086
  } catch {
4842
5087
  return c.text("Bad Request", 400);
4843
5088
  }
4844
- let target = path7.resolve(root, "." + rel);
4845
- if (target !== root && !target.startsWith(root + path7.sep)) return c.text("Forbidden", 403);
5089
+ let target = path8.resolve(root, "." + rel);
5090
+ if (target !== root && !target.startsWith(root + path8.sep)) return c.text("Forbidden", 403);
4846
5091
  try {
4847
- const st = await stat5(target);
4848
- if (st.isDirectory()) target = path7.join(target, "index.html");
5092
+ const st = await stat6(target);
5093
+ if (st.isDirectory()) target = path8.join(target, "index.html");
4849
5094
  } catch {
4850
- target = path7.join(root, "index.html");
5095
+ target = path8.join(root, "index.html");
4851
5096
  }
4852
5097
  let body;
4853
5098
  try {
@@ -4855,7 +5100,7 @@ async function serveStaticFile(c, webDir) {
4855
5100
  } catch {
4856
5101
  return c.text("Not Found", 404);
4857
5102
  }
4858
- const ext = path7.extname(target).toLowerCase();
5103
+ const ext = path8.extname(target).toLowerCase();
4859
5104
  const isAsset = rel.startsWith("/assets/");
4860
5105
  return new Response(new Uint8Array(body), {
4861
5106
  status: 200,
@@ -4868,6 +5113,7 @@ async function serveStaticFile(c, webDir) {
4868
5113
 
4869
5114
  // packages/server/src/app.ts
4870
5115
  var SSE_HEARTBEAT_MS = 15e3;
5116
+ var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["127.0.0.1", "localhost"]);
4871
5117
  var LISTING_HINT_TTL_MS = 5 * 6e4;
4872
5118
  function createApp(opts) {
4873
5119
  const { repo, store } = opts;
@@ -4891,7 +5137,12 @@ function createApp(opts) {
4891
5137
  worktreeCache = { at: Date.now(), list };
4892
5138
  return list;
4893
5139
  };
4894
- const targetCtx = async (key) => {
5140
+ const checkpoints = checkpointStore(store.file);
5141
+ const stateStamp = () => stat7(store.file).then(
5142
+ (st) => st.mtimeMs,
5143
+ () => 0
5144
+ );
5145
+ const worktreeCtx = async (key) => {
4895
5146
  let list = await worktrees();
4896
5147
  try {
4897
5148
  return resolveTargetContext(repo, list, key);
@@ -4903,6 +5154,13 @@ function createApp(opts) {
4903
5154
  throw e;
4904
5155
  }
4905
5156
  };
5157
+ const targetCtx = async (key) => {
5158
+ const ctx = await worktreeCtx(key);
5159
+ if (ctx.target.kind !== "checkpoint") return ctx;
5160
+ const cp = findCheckpoint(await store.load(), ctx.target.worktree, ctx.target.id);
5161
+ if (!cp) throw notFound(`no checkpoint #${ctx.target.id} in this worktree`, "unknown_checkpoint");
5162
+ return { ...ctx, checkpoint: { tree: cp.tree, store: checkpoints } };
5163
+ };
4906
5164
  const fileDiffWithHints = async (ctx, filePath, explicit) => {
4907
5165
  const cached = listings.get(ctx.key);
4908
5166
  const hint = cached && Date.now() - cached.at < LISTING_HINT_TTL_MS ? cached.files.find((f) => f.path === filePath) : void 0;
@@ -4911,6 +5169,14 @@ function createApp(opts) {
4911
5169
  untracked: explicit?.untracked ?? hint?.untracked
4912
5170
  });
4913
5171
  };
5172
+ const checkpointHandoff = (comments) => {
5173
+ const worktrees2 = new Set(comments.map((x) => tryParseTargetKey(x.targetKey)?.worktree));
5174
+ for (const wt of worktrees2) {
5175
+ takeCheckpoint(store, wt ?? repo.root, wt, { handoff: true }).catch((e) => {
5176
+ console.error(`warden: no checkpoint for the comments handed over in ${wt ?? repo.root}: ${e instanceof Error ? e.message : e}`);
5177
+ });
5178
+ }
5179
+ };
4914
5180
  const findComment = (state, id) => {
4915
5181
  for (const [targetKey, t] of Object.entries(state.targets)) {
4916
5182
  const index = t.comments.findIndex((c) => c.id === id);
@@ -4918,6 +5184,13 @@ function createApp(opts) {
4918
5184
  }
4919
5185
  return void 0;
4920
5186
  };
5187
+ const linkable = (state, raw2) => {
5188
+ if (!Array.isArray(raw2) || raw2.some((x) => typeof x !== "string")) throw badRequest("commentIds must be a list of ids");
5189
+ const ids = [...new Set(raw2)];
5190
+ const missing = ids.find((x) => !findComment(state, x));
5191
+ if (missing) throw notFound(`no comment with id ${missing}`, "unknown_comment");
5192
+ return ids;
5193
+ };
4921
5194
  app.onError((err, c) => {
4922
5195
  if (err instanceof HttpError) {
4923
5196
  return c.json({ error: err.message, code: err.code }, err.status);
@@ -4925,10 +5198,20 @@ function createApp(opts) {
4925
5198
  console.error(err);
4926
5199
  return c.json({ error: err instanceof Error ? err.message : String(err), code: "internal" }, 500);
4927
5200
  });
5201
+ app.use("*", async (c, next) => {
5202
+ const host = new URL(c.req.url).hostname;
5203
+ if (!LOOPBACK_HOSTS.has(host)) {
5204
+ throw new HttpError(403, `requests for host ${host} are refused; open warden at http://127.0.0.1 or http://localhost`, "bad_host");
5205
+ }
5206
+ await next();
5207
+ });
4928
5208
  const api = new Hono2();
4929
5209
  api.use("*", async (c, next) => {
4930
- if (c.req.method !== "GET" && c.req.method !== "HEAD" && c.req.header("sec-fetch-site") === "cross-site") {
4931
- throw new HttpError(403, "cross-site requests are refused", "cross_site");
5210
+ if (c.req.method !== "GET" && c.req.method !== "HEAD") {
5211
+ const origin = c.req.header("origin");
5212
+ if (c.req.header("sec-fetch-site") === "cross-site" || origin && origin !== new URL(c.req.url).origin) {
5213
+ throw new HttpError(403, "cross-site requests are refused", "cross_site");
5214
+ }
4932
5215
  }
4933
5216
  await next();
4934
5217
  });
@@ -4939,7 +5222,7 @@ function createApp(opts) {
4939
5222
  worktreeCache = { at: Date.now(), list: info.worktrees };
4940
5223
  const last = state.prefs.lastTarget;
4941
5224
  const target = last ? tryParseTargetKey(last) : void 0;
4942
- const usable2 = !!target && (!target.worktree || info.worktrees.some((w) => w.path === target.worktree));
5225
+ const usable2 = !!target && (!target.worktree || info.worktrees.some((w) => w.path === target.worktree)) && (target.kind !== "checkpoint" || !!findCheckpoint(state, target.worktree, target.id));
4943
5226
  if (last && usable2) info.defaultTarget = last;
4944
5227
  else if (last) {
4945
5228
  await store.update((s) => {
@@ -5038,6 +5321,27 @@ function createApp(opts) {
5038
5321
  changedNotices.get(key)?.delete(body.path);
5039
5322
  return c.json({ viewed });
5040
5323
  });
5324
+ api.get("/targets/:key/checkpoints", async (c) => {
5325
+ const ctx = await worktreeCtx(c.req.param("key"));
5326
+ const res = { checkpoints: checkpointsOf(await store.load(), ctx.target.worktree) };
5327
+ return c.json(res);
5328
+ });
5329
+ api.post("/targets/:key/checkpoints", async (c) => {
5330
+ const ctx = await worktreeCtx(c.req.param("key"));
5331
+ const { checkpoint, unchanged } = await takeCheckpoint(store, ctx.cwd, ctx.target.worktree);
5332
+ const res = { checkpoint, unchanged, targetKey: checkpointKey(checkpoint) };
5333
+ return c.json(res, unchanged ? 200 : 201);
5334
+ });
5335
+ api.delete("/targets/:key/checkpoints/:id", async (c) => {
5336
+ const ctx = await worktreeCtx(c.req.param("key"));
5337
+ const id = Number(c.req.param("id"));
5338
+ await store.update((s) => {
5339
+ const cp = Number.isInteger(id) ? findCheckpoint(s, ctx.target.worktree, id) : void 0;
5340
+ if (!cp) throw notFound(`no checkpoint #${c.req.param("id")} in this worktree`, "unknown_checkpoint");
5341
+ forgetCheckpoint(s, cp);
5342
+ });
5343
+ return c.json({ ok: true });
5344
+ });
5041
5345
  api.post("/targets/:key/stage", async (c) => {
5042
5346
  const key = c.req.param("key");
5043
5347
  const ctx = await targetCtx(key);
@@ -5079,7 +5383,7 @@ function createApp(opts) {
5079
5383
  if (!anchored) throw badRequest("selection is not fully visible in the diff on that side (must be inside one hunk)", "bad_selection");
5080
5384
  const now = (/* @__PURE__ */ new Date()).toISOString();
5081
5385
  const comment = {
5082
- id: randomUUID(),
5386
+ id: randomUUID3(),
5083
5387
  targetKey: key,
5084
5388
  filePath: body.filePath,
5085
5389
  side: body.side,
@@ -5150,12 +5454,19 @@ function createApp(opts) {
5150
5454
  if (!t) return false;
5151
5455
  const before = t.comments.length;
5152
5456
  t.comments = t.comments.filter((x) => x.id !== id);
5153
- for (const issue of s.issues) issue.commentIds = issue.commentIds.filter((x) => x !== id);
5457
+ unlinkComments(s, [id]);
5154
5458
  return t.comments.length !== before;
5155
5459
  });
5156
5460
  if (!removed) throw notFound("comment not found");
5157
5461
  return c.json({ ok: true });
5158
5462
  });
5463
+ api.post("/targets/:key/comments/:id/replies", async (c) => {
5464
+ const body = await c.req.json();
5465
+ if (typeof body.body !== "string") throw badRequest("body is required");
5466
+ const found = findComment(await store.load(), c.req.param("id"));
5467
+ if (!found || found.targetKey !== commentScopeKey(c.req.param("key"))) throw notFound("comment not found");
5468
+ return c.json(await addReply(store, found.comment.id, "reviewer", body.body), 201);
5469
+ });
5159
5470
  api.post("/targets/:key/comments/reanchor", async (c) => {
5160
5471
  const key = c.req.param("key");
5161
5472
  const ctx = await targetCtx(key);
@@ -5227,7 +5538,7 @@ function createApp(opts) {
5227
5538
  const patch = located.get(cm.id);
5228
5539
  if (!patch) {
5229
5540
  if (!comments.some((x) => x.id === cm.id)) return [cm];
5230
- if (committed) {
5541
+ if (committed && !cm.replies?.length) {
5231
5542
  dropped.push(cm.id);
5232
5543
  return [];
5233
5544
  }
@@ -5237,9 +5548,7 @@ function createApp(opts) {
5237
5548
  const unchanged = cm.targetKey === next.targetKey && cm.status === next.status && cm.startLine === next.startLine && cm.endLine === next.endLine && cm.anchor.hunkHash === next.anchor.hunkHash;
5238
5549
  return [unchanged ? cm : { ...next, updatedAt: now }];
5239
5550
  });
5240
- for (const id of dropped) {
5241
- for (const issue of s.issues) issue.commentIds = issue.commentIds.filter((x) => x !== id);
5242
- }
5551
+ unlinkComments(s, dropped);
5243
5552
  t.head = head;
5244
5553
  return t.comments;
5245
5554
  });
@@ -5250,6 +5559,7 @@ function createApp(opts) {
5250
5559
  const body = await c.req.json();
5251
5560
  if (!Array.isArray(body.commentIds)) throw badRequest("commentIds required");
5252
5561
  const ids = body.commentIds.filter((x) => typeof x === "string");
5562
+ let handed = [];
5253
5563
  const res = await store.update((s) => {
5254
5564
  const now = (/* @__PURE__ */ new Date()).toISOString();
5255
5565
  const selected = [];
@@ -5261,94 +5571,14 @@ function createApp(opts) {
5261
5571
  s.targets[found.targetKey].comments[found.index] = next;
5262
5572
  selected.push(next);
5263
5573
  }
5574
+ handed = selected;
5264
5575
  return {
5265
- text: formatCommentsExport({ repoRoot: repo.root, comments: selected }),
5576
+ text: formatCommentsExport({ repoRoot: repo.root, comments: selected, replyCommand: opts.replyCommand }),
5266
5577
  count: selected.length,
5267
5578
  commentIds: selected.map((x) => x.id)
5268
5579
  };
5269
5580
  });
5270
- return c.json(res);
5271
- });
5272
- api.get("/issues", async (c) => {
5273
- const s = await store.load();
5274
- return c.json({ issues: s.issues });
5275
- });
5276
- api.post("/issues", async (c) => {
5277
- const body = await c.req.json();
5278
- if (!body.title?.trim()) throw badRequest("title is required");
5279
- const now = (/* @__PURE__ */ new Date()).toISOString();
5280
- const issue = {
5281
- id: randomUUID(),
5282
- title: body.title.trim(),
5283
- body: typeof body.body === "string" ? body.body : "",
5284
- status: body.status === "closed" ? "closed" : "open",
5285
- commentIds: Array.isArray(body.commentIds) ? body.commentIds.filter((x) => typeof x === "string") : [],
5286
- createdAt: now,
5287
- updatedAt: now
5288
- };
5289
- await store.update((s) => {
5290
- s.issues = insertAfter(s.issues, issue, typeof body.after === "string" ? body.after : void 0);
5291
- });
5292
- return c.json(issue, 201);
5293
- });
5294
- api.post("/issues/:id/move", async (c) => {
5295
- const id = c.req.param("id");
5296
- const body = await c.req.json();
5297
- if (body.before !== null && typeof body.before !== "string") throw badRequest("before must be an id or null");
5298
- const issues = await store.update((s) => {
5299
- const next = moveBefore(s.issues, id, body.before);
5300
- if (!next) throw notFound("issue not found");
5301
- s.issues = next;
5302
- return s.issues;
5303
- });
5304
- return c.json({ issues });
5305
- });
5306
- api.patch("/issues/:id", async (c) => {
5307
- const id = c.req.param("id");
5308
- const body = await c.req.json();
5309
- const updated = await store.update((s) => {
5310
- const issue = s.issues.find((i) => i.id === id);
5311
- if (!issue) throw notFound("issue not found");
5312
- if (typeof body.title === "string" && body.title.trim()) issue.title = body.title.trim();
5313
- if (typeof body.body === "string") issue.body = body.body;
5314
- if (body.status === "open" || body.status === "closed") issue.status = body.status;
5315
- if (Array.isArray(body.commentIds)) issue.commentIds = [...new Set(body.commentIds.filter((x) => typeof x === "string"))];
5316
- issue.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
5317
- return issue;
5318
- });
5319
- return c.json(updated);
5320
- });
5321
- api.delete("/issues/:id", async (c) => {
5322
- const id = c.req.param("id");
5323
- const removed = await store.update((s) => {
5324
- const before = s.issues.length;
5325
- s.issues = s.issues.filter((i) => i.id !== id);
5326
- return s.issues.length !== before;
5327
- });
5328
- if (!removed) throw notFound("issue not found");
5329
- return c.json({ ok: true });
5330
- });
5331
- api.post("/issues/:id/export", async (c) => {
5332
- const id = c.req.param("id");
5333
- const res = await store.update((s) => {
5334
- const issue = s.issues.find((i) => i.id === id);
5335
- if (!issue) throw notFound("issue not found");
5336
- const now = (/* @__PURE__ */ new Date()).toISOString();
5337
- const comments = [];
5338
- for (const cid of issue.commentIds) {
5339
- const found = findComment(s, cid);
5340
- if (!found) continue;
5341
- const next = { ...found.comment, exportedAt: now, updatedAt: now };
5342
- if (next.status === "active") next.status = "exported";
5343
- s.targets[found.targetKey].comments[found.index] = next;
5344
- comments.push(next);
5345
- }
5346
- return {
5347
- text: formatIssueExport({ repoRoot: repo.root, issue, comments }),
5348
- count: comments.length,
5349
- commentIds: comments.map((x) => x.id)
5350
- };
5351
- });
5581
+ checkpointHandoff(handed);
5352
5582
  return c.json(res);
5353
5583
  });
5354
5584
  const knownRoot = async (rootParam) => {
@@ -5372,7 +5602,7 @@ function createApp(opts) {
5372
5602
  const branch = body.branch?.trim() || await currentBranch(await knownRoot(body.root));
5373
5603
  const now = (/* @__PURE__ */ new Date()).toISOString();
5374
5604
  const todo = {
5375
- id: randomUUID(),
5605
+ id: randomUUID3(),
5376
5606
  branch,
5377
5607
  title: body.title.trim(),
5378
5608
  body: typeof body.body === "string" ? body.body : "",
@@ -5381,6 +5611,10 @@ function createApp(opts) {
5381
5611
  updatedAt: now
5382
5612
  };
5383
5613
  await store.update((s) => {
5614
+ if (body.commentIds !== void 0) {
5615
+ const ids = linkable(s, body.commentIds);
5616
+ if (ids.length) todo.commentIds = ids;
5617
+ }
5384
5618
  s.todos = insertAfter(s.todos, todo, typeof body.after === "string" ? body.after : void 0);
5385
5619
  });
5386
5620
  return c.json(todo, 201);
@@ -5407,11 +5641,40 @@ function createApp(opts) {
5407
5641
  if (typeof body.title === "string" && body.title.trim()) todo.title = body.title.trim();
5408
5642
  if (typeof body.body === "string") todo.body = body.body;
5409
5643
  if (body.status === "open" || body.status === "done") todo.status = body.status;
5644
+ if (body.commentIds !== void 0) {
5645
+ const ids = linkable(s, body.commentIds);
5646
+ if (ids.length) todo.commentIds = ids;
5647
+ else delete todo.commentIds;
5648
+ }
5410
5649
  todo.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
5411
5650
  return todo;
5412
5651
  });
5413
5652
  return c.json(updated);
5414
5653
  });
5654
+ api.post("/todos/:id/export", async (c) => {
5655
+ const id = c.req.param("id");
5656
+ const handed = [];
5657
+ const res = await store.update((s) => {
5658
+ const todo = s.todos.find((t) => t.id === id);
5659
+ if (!todo) throw notFound("todo not found");
5660
+ const now = (/* @__PURE__ */ new Date()).toISOString();
5661
+ for (const cid of todo.commentIds ?? []) {
5662
+ const found = findComment(s, cid);
5663
+ if (!found) continue;
5664
+ const next = { ...found.comment, exportedAt: now, updatedAt: now };
5665
+ if (next.status === "active") next.status = "exported";
5666
+ s.targets[found.targetKey].comments[found.index] = next;
5667
+ handed.push(next);
5668
+ }
5669
+ return {
5670
+ text: formatTodoExport({ todo, comments: handed, replyCommand: opts.replyCommand }),
5671
+ count: handed.length,
5672
+ commentIds: handed.map((x) => x.id)
5673
+ };
5674
+ });
5675
+ checkpointHandoff(handed);
5676
+ return c.json(res);
5677
+ });
5415
5678
  api.delete("/todos/:id", async (c) => {
5416
5679
  const id = c.req.param("id");
5417
5680
  const removed = await store.update((s) => {
@@ -5490,8 +5753,31 @@ function createApp(opts) {
5490
5753
  return c.json(await tmux.open(repo.commonRoot, wt.path, body.sessionId), 201);
5491
5754
  });
5492
5755
  api.get("/worktrees", async (c) => {
5493
- const list = await listWorktreesDetailed(repo);
5756
+ const [list, state] = await Promise.all([listWorktreesDetailed(repo), store.load()]);
5494
5757
  const [branches, newSlot] = await Promise.all([listBranches(repo, list), nextSlot(repo, list)]);
5758
+ const review = /* @__PURE__ */ new Map();
5759
+ const latest = /* @__PURE__ */ new Map();
5760
+ for (const t of Object.values(state.targets)) {
5761
+ for (const cm of t.comments) {
5762
+ const at = commentWorktree(cm) ?? repo.root;
5763
+ const r = review.get(at) ?? { toAgent: 0, toReviewer: 0 };
5764
+ const seen = latest.get(at) ?? { agent: "", reviewer: "" };
5765
+ if (awaitsAgent(cm)) {
5766
+ r.toAgent++;
5767
+ if (cm.updatedAt >= seen.agent) [seen.agent, r.toAgentTarget] = [cm.updatedAt, cm.targetKey];
5768
+ }
5769
+ if (awaitsReviewer(cm)) {
5770
+ r.toReviewer++;
5771
+ if (cm.updatedAt >= seen.reviewer) [seen.reviewer, r.toReviewerTarget] = [cm.updatedAt, cm.targetKey];
5772
+ }
5773
+ review.set(at, r);
5774
+ latest.set(at, seen);
5775
+ }
5776
+ }
5777
+ for (const wt of list) {
5778
+ const r = review.get(wt.path);
5779
+ if (r && (r.toAgent || r.toReviewer)) wt.review = r;
5780
+ }
5495
5781
  const res = { worktrees: list, branches, newSlot };
5496
5782
  return c.json(res);
5497
5783
  });
@@ -5538,7 +5824,19 @@ function createApp(opts) {
5538
5824
  const event = { type: "changed", ...change };
5539
5825
  void enqueue(() => stream2.writeSSE({ event: "changed", data: JSON.stringify(event) }));
5540
5826
  });
5541
- stream2.onAbort(release);
5827
+ let stamp = await stateStamp();
5828
+ const statePoll = setInterval(() => {
5829
+ void stateStamp().then((next) => {
5830
+ if (next === stamp) return;
5831
+ stamp = next;
5832
+ const event = { type: "state", at: (/* @__PURE__ */ new Date()).toISOString() };
5833
+ void enqueue(() => stream2.writeSSE({ event: "state", data: JSON.stringify(event) }));
5834
+ });
5835
+ }, opts.watchIntervalMs ?? DEFAULT_POLL_INTERVAL_MS);
5836
+ stream2.onAbort(() => {
5837
+ clearInterval(statePoll);
5838
+ release();
5839
+ });
5542
5840
  try {
5543
5841
  while (!stream2.aborted && !stream2.closed) {
5544
5842
  await stream2.sleep(SSE_HEARTBEAT_MS);
@@ -5546,6 +5844,7 @@ function createApp(opts) {
5546
5844
  await enqueue(() => stream2.write(": ping\n\n"));
5547
5845
  }
5548
5846
  } finally {
5847
+ clearInterval(statePoll);
5549
5848
  release();
5550
5849
  }
5551
5850
  });
@@ -5574,7 +5873,7 @@ function createApp(opts) {
5574
5873
  api.post("/nvim/open", async (c) => {
5575
5874
  const body = await c.req.json();
5576
5875
  if (!body.socket || !body.absPath) throw badRequest("socket and absPath are required");
5577
- if (!path8.isAbsolute(body.absPath)) throw badRequest("absPath must be absolute");
5876
+ if (!path9.isAbsolute(body.absPath)) throw badRequest("absPath must be absolute");
5578
5877
  const scan = await nvim.scan();
5579
5878
  if (!scan.instances.some((i) => i.socket === body.socket)) {
5580
5879
  const fresh = await nvim.scan(true);
@@ -5634,14 +5933,14 @@ function reachableFromWindows(port, token, timeoutMs = PROBE_TIMEOUT_MS) {
5634
5933
  }
5635
5934
 
5636
5935
  // packages/server/src/update.ts
5637
- import path9 from "path";
5638
- import { mkdir as mkdir2, readFile as readFile5, writeFile as writeFile2 } from "fs/promises";
5936
+ import path10 from "path";
5937
+ import { mkdir as mkdir3, readFile as readFile5, writeFile as writeFile2 } from "fs/promises";
5639
5938
  var PACKAGE = "@xbghc/warden";
5640
5939
  var LATEST_URL = "https://registry.npmjs.org/@xbghc%2fwarden/latest";
5641
5940
  var FETCH_TIMEOUT_MS = 3e3;
5642
5941
  var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
5643
5942
  function updateCacheFile(baseDir = dataDir()) {
5644
- return path9.join(baseDir, "update-check.json");
5943
+ return path10.join(baseDir, "update-check.json");
5645
5944
  }
5646
5945
  function updateCheckEnabled(env = process.env) {
5647
5946
  return !env.WARDEN_NO_UPDATE_CHECK && !env.NO_UPDATE_NOTIFIER && !env.CI;
@@ -5675,7 +5974,7 @@ async function readCache(file, now) {
5675
5974
  }
5676
5975
  async function writeCache(file, cache) {
5677
5976
  try {
5678
- await mkdir2(path9.dirname(file), { recursive: true });
5977
+ await mkdir3(path10.dirname(file), { recursive: true });
5679
5978
  await writeFile2(file, JSON.stringify(cache));
5680
5979
  } catch {
5681
5980
  }
@@ -5720,8 +6019,8 @@ async function startServer(opts) {
5720
6019
  const repo = await resolveRepo(opts.repoPath);
5721
6020
  const stateFile = opts.stateFile ?? stateFilePath(repo.commonRoot);
5722
6021
  const store = new StateStore(stateFile, repo.commonRoot);
5723
- const instanceToken = randomUUID2();
5724
- const app = createApp({ repo, store, nvim: new NvimService(), webDir: opts.webDir, instanceToken, update: opts.update });
6022
+ const instanceToken = randomUUID4();
6023
+ const app = createApp({ repo, store, nvim: new NvimService(), webDir: opts.webDir, instanceToken, update: opts.update, replyCommand: opts.replyCommand });
5725
6024
  const listen = (p) => new Promise((resolve, reject) => {
5726
6025
  const s = serve({ fetch: app.fetch, hostname: HOST, port: p }, () => resolve(s));
5727
6026
  s.once("error", reject);
@@ -5763,8 +6062,68 @@ async function startServer(opts) {
5763
6062
  };
5764
6063
  }
5765
6064
 
6065
+ // bin/agent.ts
6066
+ var AGENT_COMMANDS = ["feedback", "reply"];
6067
+ function agentUsage(cmd) {
6068
+ return `Commands for the agent, run inside the worktree under review:
6069
+ ${cmd} feedback [--peek] Print the review comments not yet handed over, and mark them handed
6070
+ over (--peek leaves them as they are)
6071
+ ${cmd} reply <id> <message> Answer a comment; <id> is the one in its heading. A message of "-",
6072
+ or none with stdin piped, is read from stdin
6073
+ `;
6074
+ }
6075
+ async function readStdin() {
6076
+ const chunks = [];
6077
+ for await (const chunk of process.stdin) chunks.push(chunk);
6078
+ return Buffer.concat(chunks).toString("utf8");
6079
+ }
6080
+ async function openStore() {
6081
+ const repo = await resolveRepo(process.cwd());
6082
+ return { store: new StateStore(stateFilePath(repo.commonRoot), repo.commonRoot), root: repo.root, commonRoot: repo.commonRoot };
6083
+ }
6084
+ async function feedback(args, replyCommand) {
6085
+ const unknown = args.find((a) => a !== "--peek");
6086
+ if (unknown) throw new Error(`unknown argument: ${unknown}`);
6087
+ const { store, root, commonRoot } = await openStore();
6088
+ const peek = args.includes("--peek");
6089
+ const comments = await takeFeedback(store, { worktreeRoot: root, mainRoot: commonRoot, peek });
6090
+ if (comments.length === 0) {
6091
+ console.log("No review comments are waiting for you.");
6092
+ return;
6093
+ }
6094
+ if (!peek) {
6095
+ await takeCheckpoint(store, root, root === commonRoot ? void 0 : root, { handoff: true }).catch((e) => {
6096
+ console.error(`warden feedback: no checkpoint taken: ${e instanceof Error ? e.message : e}`);
6097
+ });
6098
+ }
6099
+ process.stdout.write(formatCommentsExport({ repoRoot: root, comments, replyCommand }));
6100
+ }
6101
+ async function reply(args) {
6102
+ const [id, ...words] = args;
6103
+ if (!id) throw new Error("reply needs a comment id");
6104
+ let body = words.join(" ");
6105
+ if (body === "-" || !body && !process.stdin.isTTY) body = await readStdin();
6106
+ if (!body.trim()) throw new Error("reply needs a message");
6107
+ const { store } = await openStore();
6108
+ const c = await addReply(store, id, "agent", body);
6109
+ const range = c.startLine === c.endLine ? `${c.startLine}` : `${c.startLine}-${c.endLine}`;
6110
+ console.log(`Replied to ${shortId(c.id)} (${c.filePath}:${range}).`);
6111
+ }
6112
+ async function runAgentCommand(argv, replyCommand) {
6113
+ const [cmd, ...rest] = argv;
6114
+ if (!cmd || !AGENT_COMMANDS.includes(cmd)) return void 0;
6115
+ try {
6116
+ if (cmd === "feedback") await feedback(rest, replyCommand);
6117
+ else await reply(rest);
6118
+ return 0;
6119
+ } catch (e) {
6120
+ console.error(`warden ${cmd}: ${e instanceof HttpError || e instanceof Error ? e.message : String(e)}`);
6121
+ return 1;
6122
+ }
6123
+ }
6124
+
5766
6125
  // bin/cli.ts
5767
- var VERSION = true ? "0.14.0" : "dev";
6126
+ var VERSION = true ? "0.16.0" : "dev";
5768
6127
  function parseArgs(argv) {
5769
6128
  const args = { repoPath: process.cwd(), open: true, updateCheck: true, help: false, version: false };
5770
6129
  for (let i = 0; i < argv.length; i++) {
@@ -5783,14 +6142,17 @@ function parseArgs(argv) {
5783
6142
  } else if (a === "--help" || a === "-h") args.help = true;
5784
6143
  else if (a === "--version" || a === "-v") args.version = true;
5785
6144
  else if (a.startsWith("-")) throw new Error(`unknown option: ${a}`);
5786
- else args.repoPath = path10.resolve(a);
6145
+ else args.repoPath = path11.resolve(a);
5787
6146
  }
5788
6147
  return args;
5789
6148
  }
6149
+ var viaNpx = fileURLToPath(import.meta.url).includes(`${path11.sep}_npx${path11.sep}`);
6150
+ var selfCommand = viaNpx ? "npx @xbghc/warden" : "warden";
5790
6151
  function usage() {
5791
6152
  return `warden \u2014 local git diff review UI
5792
6153
 
5793
6154
  Usage: warden [repoPath] [options]
6155
+ warden <command> [args]
5794
6156
 
5795
6157
  Options:
5796
6158
  --port <n>, -p <n> Listen on a fixed port (default: first free port from 4100, and under WSL
@@ -5800,6 +6162,9 @@ Options:
5800
6162
  (also: WARDEN_NO_UPDATE_CHECK=1, NO_UPDATE_NOTIFIER=1, CI)
5801
6163
  -h, --help Show this help
5802
6164
  -v, --version Print version
6165
+
6166
+ ${agentUsage("warden")}
6167
+ A repository in a directory named after a command is given as ./<name>.
5803
6168
  `;
5804
6169
  }
5805
6170
  async function openBrowser(url) {
@@ -5828,20 +6193,22 @@ async function openBrowser(url) {
5828
6193
  return false;
5829
6194
  }
5830
6195
  function resolveWebDir() {
5831
- const here = path10.dirname(fileURLToPath(import.meta.url));
6196
+ const here = path11.dirname(fileURLToPath(import.meta.url));
5832
6197
  const candidates = [
5833
6198
  process.env.WARDEN_WEB_DIR,
5834
- path10.join(here, "web"),
6199
+ path11.join(here, "web"),
5835
6200
  // dist/cli.js -> dist/web
5836
- path10.join(here, "..", "dist", "web")
6201
+ path11.join(here, "..", "dist", "web")
5837
6202
  // bin/cli.ts (tsx dev) -> dist/web
5838
6203
  ].filter((p) => !!p);
5839
6204
  for (const dir of candidates) {
5840
- if (existsSync2(path10.join(dir, "index.html"))) return dir;
6205
+ if (existsSync2(path11.join(dir, "index.html"))) return dir;
5841
6206
  }
5842
6207
  return void 0;
5843
6208
  }
5844
6209
  async function main() {
6210
+ const code = await runAgentCommand(process.argv.slice(2), selfCommand);
6211
+ if (code !== void 0) process.exit(code);
5845
6212
  let args;
5846
6213
  try {
5847
6214
  args = parseArgs(process.argv.slice(2));
@@ -5862,12 +6229,11 @@ async function main() {
5862
6229
  if (!webDir) {
5863
6230
  console.error("warning: built web assets not found (dist/web). Only the /api endpoints will be served.");
5864
6231
  }
5865
- const viaNpx = fileURLToPath(import.meta.url).includes(`${path10.sep}_npx${path10.sep}`);
5866
6232
  const checking = args.updateCheck && VERSION !== "dev" && updateCheckEnabled();
5867
6233
  const update = checking ? checkForUpdate({ current: VERSION, npx: viaNpx }) : Promise.resolve(null);
5868
6234
  let server;
5869
6235
  try {
5870
- server = await startServer({ repoPath: args.repoPath, port: args.port, webDir, update });
6236
+ server = await startServer({ repoPath: args.repoPath, port: args.port, webDir, update, replyCommand: selfCommand });
5871
6237
  } catch (e) {
5872
6238
  console.error(`warden: ${e.message}`);
5873
6239
  process.exit(1);