harlay 0.1.2 → 0.1.3

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/app/dist/main.cjs CHANGED
@@ -29,20 +29,112 @@ var LaunchError = class extends HarlayError {
29
29
  written;
30
30
  };
31
31
 
32
+ // ../../packages/core/src/launch/launchers.ts
33
+ var import_node_child_process = require("node:child_process");
34
+ var import_promises = require("node:fs/promises");
35
+ var import_node_path = require("node:path");
36
+ var import_node_util = require("node:util");
37
+ var pexec = (0, import_node_util.promisify)(import_node_child_process.execFile);
38
+ var HANDOFF_PROMPT = "Read .harlay/HANDOFF.md and continue the task.";
39
+ var DEFAULT_MODES = { "claude-code": "app", codex: "app", cursor: "app" };
40
+ var PROMPT_LIMIT = {
41
+ "claude-code": { app: 6e3, terminal: 1e5 },
42
+ codex: { app: 6e3, terminal: 1e5 },
43
+ cursor: { app: 9900, terminal: 1e5 }
44
+ };
45
+ function promptFits(target, mode, prompt) {
46
+ const length = mode === "app" ? encodeURIComponent(prompt).length : prompt.length;
47
+ return length <= PROMPT_LIMIT[target][mode];
48
+ }
49
+ var defaultRunner = {
50
+ async open(args) {
51
+ await pexec("/usr/bin/open", args);
52
+ },
53
+ async writeExecutable(path, content) {
54
+ await (0, import_promises.mkdir)((0, import_node_path.dirname)(path), { recursive: true });
55
+ await (0, import_promises.writeFile)(path, content, { mode: 493 });
56
+ await (0, import_promises.chmod)(path, 493);
57
+ },
58
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms))
59
+ };
60
+ function shq(s) {
61
+ return `'${s.replace(/'/g, `'\\''`)}'`;
62
+ }
63
+ var enc = encodeURIComponent;
64
+ function planLaunch(o) {
65
+ if (o.mode === "app") {
66
+ switch (o.target) {
67
+ case "claude-code":
68
+ return { target: o.target, mode: o.mode, steps: [{ kind: "open", args: [`claude://code/new?folder=${enc(o.cwd)}&q=${enc(o.prompt)}`] }] };
69
+ case "codex":
70
+ return { target: o.target, mode: o.mode, steps: [{ kind: "open", args: [`codex://threads/new?path=${enc(o.cwd)}&prompt=${enc(o.prompt)}`] }] };
71
+ case "cursor":
72
+ return {
73
+ target: o.target,
74
+ mode: o.mode,
75
+ steps: [
76
+ { kind: "open", args: ["-a", "Cursor", o.cwd] },
77
+ { kind: "wait", ms: 1500 },
78
+ { kind: "open", args: [`cursor://anysphere.cursor-deeplink/prompt?text=${enc(o.prompt)}`] }
79
+ ]
80
+ };
81
+ }
82
+ }
83
+ const bin = o.binaries[o.target];
84
+ if (!bin) throw new HarlayError(`${TOOL_LABEL[o.target]} CLI not found. Install it or switch ${TOOL_LABEL[o.target]} to app mode.`);
85
+ const argv = o.target === "codex" ? [bin, "-C", o.cwd, o.prompt] : o.target === "cursor" ? [bin, "--workspace", o.cwd, o.prompt] : [bin, o.prompt];
86
+ const path = (0, import_node_path.join)(o.scriptDir, `continue-${o.target}.command`);
87
+ const content = `#!/bin/zsh -l
88
+ cd -- ${shq(o.cwd)} || exit 1
89
+ exec ${argv.map(shq).join(" ")}
90
+ `;
91
+ return {
92
+ target: o.target,
93
+ mode: o.mode,
94
+ steps: [
95
+ { kind: "script", path, content },
96
+ { kind: "open", args: ["-a", o.terminalApp, path] }
97
+ ]
98
+ };
99
+ }
100
+ async function executeLaunch(plan, runner = defaultRunner) {
101
+ for (const step of plan.steps) {
102
+ if (step.kind === "open") await runner.open(step.args);
103
+ else if (step.kind === "script") await runner.writeExecutable(step.path, step.content);
104
+ else await runner.sleep(step.ms);
105
+ }
106
+ }
107
+
32
108
  // ../../packages/core/src/util/text.ts
33
109
  var CHARS_PER_TOKEN = 4;
34
- function clip(s, maxTokens, pointer) {
35
- const max = maxTokens * CHARS_PER_TOKEN;
36
- if (s.length <= max) return s;
37
- const note = pointer ? ` \u2026 (truncated, see ${pointer})` : " \u2026 (truncated)";
38
- return s.slice(0, Math.max(0, max - note.length)).trimEnd() + note;
39
- }
110
+ var isHigh = (c) => c >= 55296 && c <= 56319;
111
+ var isLow = (c) => c >= 56320 && c <= 57343;
40
112
  function tail(s, maxChars) {
41
- return s.length <= maxChars ? s : "\u2026" + s.slice(s.length - maxChars + 1);
113
+ if (s.length <= maxChars) return s;
114
+ let start = s.length - maxChars + 1;
115
+ if (start < s.length && isLow(s.charCodeAt(start))) start++;
116
+ return "\u2026" + s.slice(start);
117
+ }
118
+ var LONE_SURROGATE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g;
119
+ function wellFormed(s) {
120
+ return s.replace(LONE_SURROGATE, "\uFFFD");
42
121
  }
43
122
  function oneLine(s, max = 120) {
44
123
  const t = s.replace(/\s+/g, " ").trim();
45
- return t.length <= max ? t : t.slice(0, max - 1) + "\u2026";
124
+ if (t.length <= max) return t;
125
+ let end = max - 1;
126
+ if (end > 0 && isHigh(t.charCodeAt(end - 1))) end--;
127
+ return t.slice(0, end) + "\u2026";
128
+ }
129
+ function wordCount(s) {
130
+ return s.split(/\s+/).filter(Boolean).length;
131
+ }
132
+ function clipWords(s, max) {
133
+ const w = s.split(/\s+/).filter(Boolean);
134
+ return w.length <= max ? w.join(" ") : w.slice(0, max).join(" ") + " \u2026";
135
+ }
136
+ function splitSentences(s) {
137
+ return s.split(/(?<=[.!?])\s+|\n+/).map((x) => x.trim()).filter(Boolean);
46
138
  }
47
139
  var CONTEXT_BLOCK_RE = /<(ide_opened_file|ide_selection|artifact-view-context|artifact-content-authored-by-others)\b[^>]*?(?:\/>|>[\s\S]*?<\/\1\s*>)/g;
48
140
  var MACHINE_TAG_RE = /^<(command-name|command-message|command-args|local-command-stdout|local-command-stderr|local-command-caveat|task-notification|bash-input|bash-stdout|bash-stderr|environment_context|user_instructions)\b/;
@@ -53,8 +145,27 @@ function cleanPrompt(raw) {
53
145
  if (MACHINE_TAG_RE.test(s)) return null;
54
146
  return s ? s : null;
55
147
  }
56
- function isHandoffPrompt(p) {
57
- return p.length <= 300 && p.includes(".harlay/HANDOFF.md");
148
+ var HANDOFF_TAG = '<handoff from="';
149
+ var HANDOFF_END = "</handoff>";
150
+ var CLOSING_RE = /^\s*Continue from (?:the most recent user message|where it stopped)\. Start with the next concrete step\./;
151
+ var HANDOFF_ID_RE = /Only if you need more: \.harlay\/handoffs\/([^/\s]+)\//g;
152
+ function splitHandoffPrompt(p) {
153
+ const start = p.indexOf(HANDOFF_TAG);
154
+ const end = p.lastIndexOf(HANDOFF_END);
155
+ if (start !== -1 && end > start) {
156
+ const before = p.slice(0, start).trim();
157
+ const after = p.slice(end + HANDOFF_END.length).replace(CLOSING_RE, "").trim();
158
+ const id = [...p.slice(start, end).matchAll(HANDOFF_ID_RE)].at(-1)?.[1] ?? null;
159
+ return { handoff: true, rest: [before, after].filter(Boolean).join("\n\n"), id };
160
+ }
161
+ if (p.trimStart().startsWith(HANDOFF_TAG) || p.trim() === HANDOFF_PROMPT) return { handoff: true, rest: "", id: null };
162
+ return { handoff: false, rest: p, id: null };
163
+ }
164
+ var OSC_RE = /\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g;
165
+ var CSI_RE = /\x1b\[[0-?]*[ -/]*[@-~]/g;
166
+ var CONTROL_RE = /[\x00-\x08\x0b-\x1f\x7f-\x9f]/g;
167
+ function stripControl(s) {
168
+ return s.replace(OSC_RE, "").replace(CSI_RE, "").replace(CONTROL_RE, "");
58
169
  }
59
170
  function slugify(s, max = 40) {
60
171
  const slug = s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, max).replace(/-+$/, "");
@@ -62,25 +173,25 @@ function slugify(s, max = 40) {
62
173
  }
63
174
 
64
175
  // ../../packages/core/src/sources/claude.ts
65
- var import_promises2 = require("node:fs/promises");
66
- var import_node_path2 = require("node:path");
176
+ var import_promises3 = require("node:fs/promises");
177
+ var import_node_path3 = require("node:path");
67
178
 
68
179
  // ../../packages/core/src/util/files.ts
69
- var import_promises = require("node:fs/promises");
70
- var import_node_path = require("node:path");
180
+ var import_promises2 = require("node:fs/promises");
181
+ var import_node_path2 = require("node:path");
71
182
  async function listSessionFiles(root, accept) {
72
183
  let names;
73
184
  try {
74
- names = await (0, import_promises.readdir)(root, { recursive: true });
185
+ names = await (0, import_promises2.readdir)(root, { recursive: true });
75
186
  } catch {
76
187
  return [];
77
188
  }
78
189
  const out = [];
79
190
  for (const name of names) {
80
- const path = (0, import_node_path.join)(root, name);
191
+ const path = (0, import_node_path2.join)(root, name);
81
192
  if (!accept(path)) continue;
82
193
  try {
83
- const s = await (0, import_promises.stat)(path);
194
+ const s = await (0, import_promises2.stat)(path);
84
195
  if (s.isFile()) out.push({ path, mtime: s.mtime });
85
196
  } catch {
86
197
  }
@@ -88,7 +199,7 @@ async function listSessionFiles(root, accept) {
88
199
  return out.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
89
200
  }
90
201
  async function readFirstLine(file, maxBytes = 8 * 1024 * 1024) {
91
- const fh = await (0, import_promises.open)(file, "r");
202
+ const fh = await (0, import_promises2.open)(file, "r");
92
203
  try {
93
204
  const chunks = [];
94
205
  const buf = Buffer.alloc(65536);
@@ -272,10 +383,10 @@ function normalizeClaude(records, ref, invalid = 0) {
272
383
  }
273
384
  async function loadDesktopTitles(dir) {
274
385
  const out = /* @__PURE__ */ new Map();
275
- const files = await listSessionFiles(dir, (f) => (0, import_node_path2.basename)(f).startsWith("local_") && f.endsWith(".json"));
386
+ const files = await listSessionFiles(dir, (f) => (0, import_node_path3.basename)(f).startsWith("local_") && f.endsWith(".json"));
276
387
  for (const f of files) {
277
388
  try {
278
- const d = JSON.parse(await (0, import_promises2.readFile)(f.path, "utf8"));
389
+ const d = JSON.parse(await (0, import_promises3.readFile)(f.path, "utf8"));
279
390
  if (isObj(d) && typeof d.cliSessionId === "string" && typeof d.title === "string" && d.title && !out.has(d.cliSessionId)) {
280
391
  out.set(d.cliSessionId, d.title);
281
392
  }
@@ -285,14 +396,14 @@ async function loadDesktopTitles(dir) {
285
396
  return out;
286
397
  }
287
398
  function createClaudeReader(o) {
288
- const root = (0, import_node_path2.join)(o.home, ".claude", "projects");
289
- const indexDir = (0, import_node_path2.join)(o.home, "Library", "Application Support", "Claude", "claude-code-sessions");
290
- const isSessionFile = (file) => file.startsWith(root + import_node_path2.sep) && file.endsWith(".jsonl") && (0, import_node_path2.relative)(root, file).split(import_node_path2.sep).length === 2;
399
+ const root = (0, import_node_path3.join)(o.home, ".claude", "projects");
400
+ const indexDir = (0, import_node_path3.join)(o.home, "Library", "Application Support", "Claude", "claude-code-sessions");
401
+ const isSessionFile = (file) => file.startsWith(root + import_node_path3.sep) && file.endsWith(".jsonl") && (0, import_node_path3.relative)(root, file).split(import_node_path3.sep).length === 2;
291
402
  const cache = /* @__PURE__ */ new Map();
292
403
  async function refForFile(file, titles) {
293
404
  let st;
294
405
  try {
295
- st = await (0, import_promises2.stat)(file);
406
+ st = await (0, import_promises3.stat)(file);
296
407
  } catch {
297
408
  return null;
298
409
  }
@@ -300,7 +411,7 @@ function createClaudeReader(o) {
300
411
  if (!entry || entry.size !== st.size || entry.mtimeMs !== st.mtimeMs) {
301
412
  let text;
302
413
  try {
303
- text = await (0, import_promises2.readFile)(file, "utf8");
414
+ text = await (0, import_promises3.readFile)(file, "utf8");
304
415
  } catch {
305
416
  return null;
306
417
  }
@@ -325,7 +436,7 @@ function createClaudeReader(o) {
325
436
  entry = { size: st.size, mtimeMs: st.mtimeMs, cwd, fileTitle: custom ?? ai, ref: null };
326
437
  cache.set(file, entry);
327
438
  }
328
- const id = (0, import_node_path2.basename)(file, ".jsonl");
439
+ const id = (0, import_node_path3.basename)(file, ".jsonl");
329
440
  const title = entry.fileTitle ?? (titles ?? await loadDesktopTitles(indexDir)).get(id) ?? null;
330
441
  if (!entry.ref || entry.ref.title !== title) entry.ref = { tool: "claude-code", id, file, cwd: entry.cwd, title, lastActivityAt: st.mtime };
331
442
  return entry.ref;
@@ -334,7 +445,7 @@ function createClaudeReader(o) {
334
445
  tool: "claude-code",
335
446
  root,
336
447
  isSessionFile,
337
- idFromPath: (file) => isSessionFile(file) ? (0, import_node_path2.basename)(file, ".jsonl") : null,
448
+ idFromPath: (file) => isSessionFile(file) ? (0, import_node_path3.basename)(file, ".jsonl") : null,
338
449
  refForFile: (file) => refForFile(file),
339
450
  async listSessions(limit = 20) {
340
451
  const files = await listSessionFiles(root, isSessionFile);
@@ -348,7 +459,7 @@ function createClaudeReader(o) {
348
459
  return refs;
349
460
  },
350
461
  async readSession(ref) {
351
- const { records, invalid } = parseJsonLines(await (0, import_promises2.readFile)(ref.file, "utf8"));
462
+ const { records, invalid } = parseJsonLines(await (0, import_promises3.readFile)(ref.file, "utf8"));
352
463
  return normalizeClaude(records, ref, invalid);
353
464
  },
354
465
  detect(record) {
@@ -361,8 +472,8 @@ function createClaudeReader(o) {
361
472
  }
362
473
 
363
474
  // ../../packages/core/src/sources/codex.ts
364
- var import_promises3 = require("node:fs/promises");
365
- var import_node_path3 = require("node:path");
475
+ var import_promises4 = require("node:fs/promises");
476
+ var import_node_path4 = require("node:path");
366
477
  var OUTPUT_TAIL2 = 2e4;
367
478
  var ROLLOUT_ID_RE = /-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/i;
368
479
  var KIND = { add: "create", update: "modify", delete: "delete" };
@@ -468,7 +579,7 @@ async function loadTitles(file) {
468
579
  const out = /* @__PURE__ */ new Map();
469
580
  let text;
470
581
  try {
471
- text = await (0, import_promises3.readFile)(file, "utf8");
582
+ text = await (0, import_promises4.readFile)(file, "utf8");
472
583
  } catch {
473
584
  return out;
474
585
  }
@@ -478,14 +589,14 @@ async function loadTitles(file) {
478
589
  return out;
479
590
  }
480
591
  function createCodexReader(o) {
481
- const root = (0, import_node_path3.join)(o.home, ".codex", "sessions");
482
- const indexFile = (0, import_node_path3.join)(o.home, ".codex", "session_index.jsonl");
483
- const isSessionFile = (file) => file.startsWith(root + import_node_path3.sep) && (0, import_node_path3.basename)(file).startsWith("rollout-") && file.endsWith(".jsonl");
592
+ const root = (0, import_node_path4.join)(o.home, ".codex", "sessions");
593
+ const indexFile = (0, import_node_path4.join)(o.home, ".codex", "session_index.jsonl");
594
+ const isSessionFile = (file) => file.startsWith(root + import_node_path4.sep) && (0, import_node_path4.basename)(file).startsWith("rollout-") && file.endsWith(".jsonl");
484
595
  const cache = /* @__PURE__ */ new Map();
485
596
  async function refForFile(file, titles) {
486
597
  let st;
487
598
  try {
488
- st = await (0, import_promises3.stat)(file);
599
+ st = await (0, import_promises4.stat)(file);
489
600
  } catch {
490
601
  return null;
491
602
  }
@@ -510,7 +621,7 @@ function createCodexReader(o) {
510
621
  tool: "codex",
511
622
  root,
512
623
  isSessionFile,
513
- idFromPath: (file) => isSessionFile(file) ? ROLLOUT_ID_RE.exec((0, import_node_path3.basename)(file))?.[1] ?? null : null,
624
+ idFromPath: (file) => isSessionFile(file) ? ROLLOUT_ID_RE.exec((0, import_node_path4.basename)(file))?.[1] ?? null : null,
514
625
  refForFile: (file) => refForFile(file),
515
626
  async listSessions(limit = 20) {
516
627
  const files = await listSessionFiles(root, isSessionFile);
@@ -524,7 +635,7 @@ function createCodexReader(o) {
524
635
  return refs;
525
636
  },
526
637
  async readSession(ref) {
527
- const { records, invalid } = parseJsonLines(await (0, import_promises3.readFile)(ref.file, "utf8"));
638
+ const { records, invalid } = parseJsonLines(await (0, import_promises4.readFile)(ref.file, "utf8"));
528
639
  return normalizeCodex(records, ref, invalid);
529
640
  },
530
641
  detect(record) {
@@ -549,21 +660,21 @@ function createCodexReader(o) {
549
660
  }
550
661
 
551
662
  // ../../packages/core/src/sources/cursor.ts
552
- var import_promises4 = require("node:fs/promises");
553
- var import_node_path4 = require("node:path");
663
+ var import_promises5 = require("node:fs/promises");
664
+ var import_node_path5 = require("node:path");
554
665
  var norm = (name) => name.replace(/[^A-Za-z0-9]/g, "-");
555
666
  async function resolveCursorSlug(slug, rootDir = "/") {
556
667
  if (!slug || /^\d+$/.test(slug)) return null;
557
668
  async function walk(dir, rest, depth) {
558
669
  if (depth > 16) return null;
559
- const entries = await (0, import_promises4.readdir)(dir, { withFileTypes: true }).catch(() => null);
670
+ const entries = await (0, import_promises5.readdir)(dir, { withFileTypes: true }).catch(() => null);
560
671
  if (!entries) return null;
561
672
  for (const e of entries) {
562
673
  if (!e.isDirectory()) continue;
563
674
  const n = norm(e.name);
564
- if (rest === n) return (0, import_node_path4.join)(dir, e.name);
675
+ if (rest === n) return (0, import_node_path5.join)(dir, e.name);
565
676
  if (rest.startsWith(n + "-")) {
566
- const found = await walk((0, import_node_path4.join)(dir, e.name), rest.slice(n.length + 1), depth + 1);
677
+ const found = await walk((0, import_node_path5.join)(dir, e.name), rest.slice(n.length + 1), depth + 1);
567
678
  if (found) return found;
568
679
  }
569
680
  }
@@ -623,7 +734,7 @@ function normalizeCursor(records, ref, invalid = 0) {
623
734
  return { ref, model: null, timeline, todos: todos ? [...todos.values()] : null, halt, usage: null, unknownRecords: unknown };
624
735
  }
625
736
  function createCursorReader(o) {
626
- const root = (0, import_node_path4.join)(o.home, ".cursor", "projects");
737
+ const root = (0, import_node_path5.join)(o.home, ".cursor", "projects");
627
738
  const slugCache = /* @__PURE__ */ new Map();
628
739
  const cwdFor = (slug) => {
629
740
  let p = slugCache.get(slug);
@@ -634,17 +745,17 @@ function createCursorReader(o) {
634
745
  return p;
635
746
  };
636
747
  const isSessionFile = (file) => {
637
- if (!file.startsWith(root + import_node_path4.sep) || !file.endsWith(".jsonl")) return false;
638
- const parts = (0, import_node_path4.relative)(root, file).split(import_node_path4.sep);
748
+ if (!file.startsWith(root + import_node_path5.sep) || !file.endsWith(".jsonl")) return false;
749
+ const parts = (0, import_node_path5.relative)(root, file).split(import_node_path5.sep);
639
750
  return parts.length === 4 && parts[1] === "agent-transcripts" && parts[3] === `${parts[2]}.jsonl`;
640
751
  };
641
752
  const cache = /* @__PURE__ */ new Map();
642
753
  async function refForFile(file) {
643
754
  if (!isSessionFile(file)) return null;
644
- const parts = (0, import_node_path4.relative)(root, file).split(import_node_path4.sep);
755
+ const parts = (0, import_node_path5.relative)(root, file).split(import_node_path5.sep);
645
756
  let st;
646
757
  try {
647
- st = await (0, import_promises4.stat)(file);
758
+ st = await (0, import_promises5.stat)(file);
648
759
  } catch {
649
760
  return null;
650
761
  }
@@ -652,7 +763,7 @@ function createCursorReader(o) {
652
763
  if (hit && hit.size === st.size && hit.mtimeMs === st.mtimeMs) return hit.ref;
653
764
  let text;
654
765
  try {
655
- text = await (0, import_promises4.readFile)(file, "utf8");
766
+ text = await (0, import_promises5.readFile)(file, "utf8");
656
767
  } catch {
657
768
  return null;
658
769
  }
@@ -660,8 +771,9 @@ function createCursorReader(o) {
660
771
  for (const r of parseJsonLines(text).records) {
661
772
  if (isObj(r) && r.role === "user" && isObj(r.message)) {
662
773
  const p = cleanPrompt(blockText(r.message.content));
663
- if (p && !isHandoffPrompt(p)) {
664
- title = oneLine(p, 60);
774
+ const rest = p ? splitHandoffPrompt(p).rest : "";
775
+ if (rest) {
776
+ title = oneLine(rest, 60);
665
777
  break;
666
778
  }
667
779
  }
@@ -674,7 +786,7 @@ function createCursorReader(o) {
674
786
  tool: "cursor",
675
787
  root,
676
788
  isSessionFile,
677
- idFromPath: (file) => isSessionFile(file) ? (0, import_node_path4.relative)(root, file).split(import_node_path4.sep)[2] : null,
789
+ idFromPath: (file) => isSessionFile(file) ? (0, import_node_path5.relative)(root, file).split(import_node_path5.sep)[2] : null,
678
790
  refForFile,
679
791
  async listSessions(limit = 20) {
680
792
  const files = await listSessionFiles(root, isSessionFile);
@@ -687,7 +799,7 @@ function createCursorReader(o) {
687
799
  return refs;
688
800
  },
689
801
  async readSession(ref) {
690
- const { records, invalid } = parseJsonLines(await (0, import_promises4.readFile)(ref.file, "utf8"));
802
+ const { records, invalid } = parseJsonLines(await (0, import_promises5.readFile)(ref.file, "utf8"));
691
803
  return normalizeCursor(records, ref, invalid);
692
804
  },
693
805
  detect(record) {
@@ -700,14 +812,14 @@ function createCursorReader(o) {
700
812
  }
701
813
 
702
814
  // ../../packages/core/src/git/snapshot.ts
703
- var import_node_child_process = require("node:child_process");
815
+ var import_node_child_process2 = require("node:child_process");
704
816
  var import_node_crypto = require("node:crypto");
705
- var import_node_util = require("node:util");
706
- var pexec = (0, import_node_util.promisify)(import_node_child_process.execFile);
817
+ var import_node_util2 = require("node:util");
818
+ var pexec2 = (0, import_node_util2.promisify)(import_node_child_process2.execFile);
707
819
  var MAX_DIFF_CHARS = 2e6;
708
820
  var MAX_FILES = 500;
709
821
  async function git(cwd, args) {
710
- const { stdout } = await pexec("git", args, {
822
+ const { stdout } = await pexec2("git", args, {
711
823
  cwd,
712
824
  maxBuffer: 64 * 1024 * 1024,
713
825
  env: { ...process.env, GIT_OPTIONAL_LOCKS: "0" }
@@ -861,12 +973,76 @@ var Redactor = class {
861
973
  };
862
974
 
863
975
  // ../../packages/core/src/extract/extract.ts
864
- var import_node_path5 = require("node:path");
865
- var DECISION_RE = /\b(don['’]t|do not|never|must|always|keep|avoid|make sure|without|instead of)\b/i;
976
+ var import_node_path6 = require("node:path");
977
+
978
+ // ../../packages/core/src/extract/recent.ts
979
+ var RECENT = { budgetTokens: 450, min: 2, max: 6, words: 120 };
980
+ function pickRecentMessages(prompts) {
981
+ const out = [];
982
+ let used = 0;
983
+ for (let i = prompts.length - 1; i >= 0 && out.length < RECENT.max; i--) {
984
+ const m = clipWords(prompts[i], RECENT.words);
985
+ if (!m) continue;
986
+ const cost = Math.ceil(m.length / CHARS_PER_TOKEN);
987
+ if (out.length >= RECENT.min && used + cost > RECENT.budgetTokens) break;
988
+ out.unshift(m);
989
+ used += cost;
990
+ }
991
+ return out;
992
+ }
993
+
994
+ // ../../packages/core/src/extract/rules.ts
995
+ var RULES2 = { maxRules: 10, budgetTokens: 300, words: 40, pastedWords: 400 };
996
+ var DIRECTIVE_RE = /\b(never|always|don['’]?t|do not|must|make sure|prefer|avoid|no need to|instead of|from now on|going forward|remember|stop|only)\b/i;
997
+ var norm2 = (s) => s.toLowerCase().replace(/\s+/g, " ").trim();
998
+ function findPinnedRules(prompts, recent) {
999
+ const recentText = norm2(recent.join("\n"));
1000
+ const found = [];
1001
+ prompts.forEach((p, message) => {
1002
+ if (wordCount(p) > RULES2.pastedWords) return;
1003
+ for (const sentence of splitSentences(p)) {
1004
+ if (sentence.endsWith("?") || sentence.endsWith(":") || !DIRECTIVE_RE.test(sentence)) continue;
1005
+ if (recentText.includes(norm2(sentence))) continue;
1006
+ found.push({ text: clipWords(sentence, RULES2.words), message });
1007
+ }
1008
+ });
1009
+ return capRules(latestCopies(found));
1010
+ }
1011
+ function mergePinnedRules(older, newer, recent) {
1012
+ const recentText = norm2(recent.join("\n"));
1013
+ const all = [...older.map((r) => ({ text: r.text, message: -1 })), ...newer].filter((r) => !recentText.includes(norm2(r.text)));
1014
+ return capRules(latestCopies(all));
1015
+ }
1016
+ function latestCopies(rules) {
1017
+ const byKey = /* @__PURE__ */ new Map();
1018
+ for (const r of rules) {
1019
+ const key = norm2(r.text);
1020
+ byKey.delete(key);
1021
+ byKey.set(key, r);
1022
+ }
1023
+ return [...byKey.values()];
1024
+ }
1025
+ function capRules(all) {
1026
+ const kept = [];
1027
+ let used = 0;
1028
+ for (let i = all.length - 1; i >= 0 && kept.length < RULES2.maxRules; i--) {
1029
+ const cost = Math.ceil(all[i].text.length / CHARS_PER_TOKEN);
1030
+ if (used + cost > RULES2.budgetTokens) break;
1031
+ kept.unshift(all[i]);
1032
+ used += cost;
1033
+ }
1034
+ return { rules: kept, more: all.length - kept.length };
1035
+ }
1036
+
1037
+ // ../../packages/core/src/extract/extract.ts
866
1038
  var TRANSCRIPT_MAX = 2e5;
1039
+ var SLASH_COMMAND_RE = /^\/[a-z][\w-]*(\s|$)/i;
1040
+ function isSlashCommand(p) {
1041
+ return SLASH_COMMAND_RE.test(p.trim());
1042
+ }
867
1043
  function isSubstantive(p) {
868
1044
  const t = p.trim();
869
- return !t.startsWith("/") && t.split(/\s+/).length >= 3;
1045
+ return !isSlashCommand(t) && t.split(/\s+/).length >= 3;
870
1046
  }
871
1047
  var PROBES = /* @__PURE__ */ new Set([
872
1048
  "rg",
@@ -901,95 +1077,96 @@ function isProbeCommand(cmd) {
901
1077
  let s = cmd;
902
1078
  for (let m = LEADING_CD_RE.exec(s); m; m = LEADING_CD_RE.exec(s)) s = s.slice(m[0].length);
903
1079
  const words = s.trim().split(/\s+/);
904
- const first = (0, import_node_path5.basename)(words[0] ?? "");
1080
+ const first = (0, import_node_path6.basename)(words[0] ?? "");
905
1081
  if (first !== "git") return PROBES.has(first);
906
1082
  let i = 1;
907
1083
  while (i < words.length && words[i].startsWith("-")) i += words[i] === "-C" || words[i] === "-c" ? 2 : 1;
908
1084
  return GIT_PROBES.has(words[i] ?? "");
909
1085
  }
910
- function findBlocker(cmds) {
911
- for (let i = cmds.length - 1; i >= 0; i--) {
912
- const c = cmds[i];
913
- if (c.exitCode === null || c.exitCode === 0 || isProbeCommand(c.cmd)) continue;
914
- if (!cmds.slice(i + 1).some((d) => d.cmd === c.cmd && d.exitCode === 0)) return c;
1086
+ var FAILURE_WINDOW = 5;
1087
+ function findLastFailure(cmds) {
1088
+ const real = cmds.filter((c) => !isProbeCommand(c.cmd));
1089
+ for (let i = real.length - 1; i >= Math.max(0, real.length - FAILURE_WINDOW); i--) {
1090
+ const c = real[i];
1091
+ if (c.exitCode === null || c.exitCode === 0) continue;
1092
+ if (real.slice(i + 1).some((d) => d.cmd === c.cmd && d.exitCode === 0)) continue;
1093
+ return { cmd: c.cmd, exitCode: c.exitCode, output: dropPartialLine(c.output) };
915
1094
  }
916
1095
  return null;
917
1096
  }
918
- function findDeadEnds(cmds, blocker) {
919
- const seen = /* @__PURE__ */ new Set();
920
- const out = [];
921
- cmds.forEach((c, i) => {
922
- if (c.exitCode === null || c.exitCode === 0 || seen.has(c.cmd) || isProbeCommand(c.cmd)) return;
923
- if (blocker && c.cmd === blocker.cmd) return;
924
- if (cmds.slice(i + 1).some((d) => d.cmd === c.cmd && d.exitCode === 0)) return;
925
- seen.add(c.cmd);
926
- out.push({ cmd: c.cmd, exitCode: c.exitCode });
927
- });
928
- return out.slice(-5);
1097
+ function dropPartialLine(output) {
1098
+ if (!output.startsWith("\u2026")) return output;
1099
+ const nl = output.indexOf("\n");
1100
+ return nl === -1 ? "" : output.slice(nl + 1);
929
1101
  }
930
- function findDecisions(prompts) {
931
- const out = [];
932
- for (const p of prompts) {
933
- for (const s of p.split(/(?<=[.!?])\s+|\n+/)) {
934
- const t = s.trim();
935
- if (t.length >= 8 && t.length <= 300 && DECISION_RE.test(t) && !out.includes(t)) out.push(t);
936
- }
937
- }
938
- return out.slice(-8);
939
- }
940
- function buildTranscript(s, title, rel) {
1102
+ function buildTranscript(s, title, rel, redactor) {
1103
+ const r = (x) => redactor.apply(x);
941
1104
  const lines = [`# Condensed transcript: ${title}`, "", `Source: ${TOOL_LABEL[s.ref.tool]} session \`${s.ref.id}\``, ""];
942
1105
  for (const t of s.timeline) {
943
- if (t.kind === "user") lines.push("**User:**", t.text, "");
944
- else if (t.kind === "assistant") lines.push("**Agent:**", t.text.length > 600 ? t.text.slice(0, 599) + "\u2026" : t.text, "");
945
- else if (t.kind === "command") lines.push(`\`$ ${oneLine(t.run.cmd, 200)}\`${t.run.exitCode === null ? "" : ` \u2192 exit ${t.run.exitCode}`}`, "");
946
- else lines.push(`Edited \`${rel(t.edit.path)}\` (${t.edit.kind})`, "");
1106
+ if (t.kind === "user") {
1107
+ const { handoff: handoff2, rest } = splitHandoffPrompt(t.text);
1108
+ if (handoff2) lines.push("**Harlay handoff prompt** (from the previous agent; context is in the brief)", "");
1109
+ if (rest) lines.push("**User:**", r(rest), "");
1110
+ } else if (t.kind === "assistant") {
1111
+ const text = r(t.text);
1112
+ lines.push("**Agent:**", text.length > 600 ? text.slice(0, 599) + "\u2026" : text, "");
1113
+ } else if (t.kind === "command") lines.push(`\`$ ${oneLine(r(t.run.cmd), 200)}\`${t.run.exitCode === null ? "" : ` \u2192 exit ${t.run.exitCode}`}`, "");
1114
+ else lines.push(`Edited \`${r(rel(t.edit.path))}\` (${t.edit.kind})`, "");
947
1115
  }
948
1116
  const out = lines.join("\n");
949
- return out.length > TRANSCRIPT_MAX ? "\u2026(earlier transcript truncated)\n" + out.slice(out.length - TRANSCRIPT_MAX) : out;
1117
+ if (out.length <= TRANSCRIPT_MAX) return out;
1118
+ const cut = out.slice(out.length - TRANSCRIPT_MAX);
1119
+ const nl = cut.indexOf("\n");
1120
+ return "\u2026(earlier transcript truncated)\n" + (nl === -1 ? cut : cut.slice(nl + 1));
950
1121
  }
951
1122
  function extractHandoff(s, git2, o) {
952
1123
  const cwd = s.ref.cwd;
953
1124
  if (!cwd) throw new HarlayError("This session has no known working directory, so Harlay cannot hand it off.");
954
1125
  const workdir = git2?.root ?? cwd;
955
1126
  const rel = (p) => {
956
- if (!(0, import_node_path5.isAbsolute)(p)) return p;
957
- const r = (0, import_node_path5.relative)(workdir, p);
958
- return r.startsWith("..") || (0, import_node_path5.isAbsolute)(r) ? p : r;
1127
+ if (!(0, import_node_path6.isAbsolute)(p)) return p;
1128
+ const r = (0, import_node_path6.relative)(workdir, p);
1129
+ return r.startsWith("..") || (0, import_node_path6.isAbsolute)(r) ? p : r;
959
1130
  };
960
- const allPrompts = s.timeline.flatMap((t) => t.kind === "user" ? [t.text] : []);
961
- const prompts = allPrompts.filter((p) => !isHandoffPrompt(p));
1131
+ const split = s.timeline.flatMap((t) => t.kind === "user" ? [splitHandoffPrompt(t.text)] : []);
1132
+ const prompts = split.flatMap((p) => p.rest ? [p.rest] : []);
962
1133
  const agentTexts = s.timeline.flatMap((t) => t.kind === "assistant" ? [t.text] : []);
963
1134
  const cmds = s.timeline.flatMap((t) => t.kind === "command" ? [t.run] : []);
964
1135
  const edits = s.timeline.flatMap((t) => t.kind === "edit" ? [t.edit] : []);
965
- const carried = o.previous && o.previous.goal !== null && prompts.length < allPrompts.length ? o.previous : null;
966
- const goal = carried ? carried.goal : prompts.find(isSubstantive) ?? prompts[0] ?? null;
967
- const last = carried ? [...prompts].reverse().find(isSubstantive) ?? null : prompts.length ? prompts[prompts.length - 1] : null;
968
- const latestInstruction = last !== null && last !== goal && isSubstantive(last) ? last : null;
1136
+ const redactor = new Redactor();
1137
+ const realPrompts = prompts.filter((p) => !isSlashCommand(p)).map((p) => redactor.apply(p));
1138
+ const prev = o.previous;
1139
+ const fromPrev = prev ? split.some((p) => p.handoff && (p.id === null || p.id === prev.id)) : false;
1140
+ const carried = prev && prev.goal !== null && fromPrev ? prev : null;
1141
+ const carriedRecent = carried ? carried.recentUserMessages.map((m) => redactor.apply(m)) : [];
1142
+ const recentUserMessages = pickRecentMessages([...carriedRecent, ...realPrompts]);
1143
+ let { rules: pinnedRules, more: moreRules } = findPinnedRules(realPrompts, recentUserMessages);
1144
+ if (carried) {
1145
+ const older = [...carried.pinnedRules.map((r) => ({ ...r, text: redactor.apply(r.text) })), ...findPinnedRules(carriedRecent, []).rules];
1146
+ const merged = mergePinnedRules(older, pinnedRules, recentUserMessages);
1147
+ moreRules += merged.more + carried.moreRules;
1148
+ pinnedRules = merged.rules;
1149
+ }
1150
+ const originalGoal = carried ? carried.goal === null ? null : redactor.apply(carried.goal) : realPrompts.find(isSubstantive) ?? realPrompts[0] ?? null;
969
1151
  const todos = (s.todos ?? []).filter((t) => t.status !== "cancelled");
970
- const nextTodo = todos.find((t) => t.status === "in_progress") ?? todos.find((t) => t.status === "pending");
971
1152
  const lastAgent = agentTexts.length ? agentTexts[agentTexts.length - 1] : null;
972
- const nextStep = nextTodo ? { text: nextTodo.text, source: "todo" } : lastAgent ? { text: lastAgent, source: "agent" } : null;
973
- const blocker = findBlocker(cmds);
974
- const editedFiles = [];
975
- const counts = /* @__PURE__ */ new Map();
976
- for (const e of edits) {
1153
+ const whereItStopped = lastAgent === null ? null : clipWords(redactor.apply(lastAgent), 150);
1154
+ const openTodos = todos.flatMap((t) => t.status === "in_progress" || t.status === "pending" ? [{ text: redactor.apply(t.text).replace(/\s+/g, " ").trim(), status: t.status }] : []).slice(0, 8);
1155
+ const failure = findLastFailure(cmds);
1156
+ const lastFailure = failure ? { ...failure, cmd: redactor.apply(failure.cmd), output: redactor.apply(failure.output) } : null;
1157
+ const filesTouched = [];
1158
+ for (const e of [...edits].reverse()) {
977
1159
  const r = rel(e.path);
978
- if (!editedFiles.includes(r)) editedFiles.push(r);
979
- counts.set(r, (counts.get(r) ?? 0) + 1);
1160
+ if (!(0, import_node_path6.isAbsolute)(r) && !filesTouched.includes(r)) filesTouched.push(r);
1161
+ if (filesTouched.length === 12) break;
980
1162
  }
981
- const readFirst = [...editedFiles].sort((a, b) => (counts.get(b) ?? 0) - (counts.get(a) ?? 0)).slice(0, 3);
982
- const progress = todos.length ? todos.map((t) => ({ text: t.text, done: t.status === "completed" })) : editedFiles.map((f) => ({ text: `\`${f}\``, done: true }));
983
- const progressSource = todos.length ? "todos" : editedFiles.length ? "edits" : "none";
984
- const redactor = new Redactor();
985
- const title = carried ? carried.title : s.ref.title ?? (goal ? oneLine(redactor.apply(goal), 60) : "Untitled task");
1163
+ const title = carried ? carried.title : s.ref.title ?? (originalGoal ? oneLine(redactor.apply(originalGoal), 60) : "Untitled task");
986
1164
  const pad = (n2) => String(n2).padStart(2, "0");
987
1165
  const n = o.now;
988
1166
  const baseId = `${pad(n.getMonth() + 1)}${pad(n.getDate())}-${pad(n.getHours())}${pad(n.getMinutes())}-${slugify(redactor.apply(title))}`;
989
1167
  return {
990
1168
  baseId,
991
1169
  title,
992
- status: "in_progress",
993
1170
  createdAt: o.now,
994
1171
  from: {
995
1172
  tool: s.ref.tool,
@@ -1001,142 +1178,155 @@ function extractHandoff(s, git2, o) {
1001
1178
  },
1002
1179
  workdir,
1003
1180
  git: git2,
1004
- goal,
1005
- latestInstruction,
1006
- nextStep,
1007
- blocker: blocker && blocker.exitCode !== null ? { cmd: blocker.cmd, exitCode: blocker.exitCode, output: blocker.output } : null,
1008
- progress,
1009
- progressSource,
1010
- editedFiles,
1011
- decisions: findDecisions(prompts),
1012
- deadEnds: findDeadEnds(cmds, blocker),
1013
- readFirst,
1014
- transcript: buildTranscript(s, title, rel),
1015
- unknownRecords: s.unknownRecords
1181
+ originalGoal,
1182
+ recentUserMessages,
1183
+ pinnedRules,
1184
+ moreRules,
1185
+ whereItStopped,
1186
+ todos: openTodos,
1187
+ filesTouched,
1188
+ lastFailure,
1189
+ transcript: buildTranscript(s, title, rel, redactor),
1190
+ unknownRecords: s.unknownRecords,
1191
+ redactions: { ...redactor.counts },
1192
+ carriedFrom: carried ? carried.id : null
1016
1193
  };
1017
1194
  }
1018
1195
 
1019
- // ../../packages/core/src/brief/render.ts
1020
- var BUDGET = { goal: 150, latest: 100, next: 200, blocker: 250, progress: 300, files: 300, decisions: 250, deadEnds: 200 };
1196
+ // ../../packages/core/src/brief/write.ts
1197
+ var import_promises6 = require("node:fs/promises");
1198
+ var import_node_path7 = require("node:path");
1199
+
1200
+ // ../../packages/core/src/brief/prompt.ts
1201
+ var TIER_MAX = { full: 6e3, compact: 3e3 };
1021
1202
  var REASON_TEXT = {
1022
1203
  usage_limit: "it hit a usage limit",
1023
1204
  crash: "it stopped with an error",
1024
1205
  manual: "the user handed it off"
1025
1206
  };
1026
- var j = (v) => JSON.stringify(v ?? null);
1027
- function quote(s) {
1028
- return s.split("\n").map((l) => l.trim() ? `> ${l}` : ">").join("\n");
1207
+ var OUR_TAGS = /<(\/?)(handoff|original_goal|rules_from_user|recent_user_messages|where_it_stopped|files|last_failure)\b/gi;
1208
+ function safe(s) {
1209
+ return s.replace(OUR_TAGS, "\u2039$1$2");
1210
+ }
1211
+ var attr = (s) => safe(s).replace(/"/g, "'").replace(/\s+/g, " ").trim();
1212
+ var norm3 = (s) => s.toLowerCase().replace(/\s+/g, " ").trim();
1213
+ function goalIsRecent(goal, recent) {
1214
+ const g = norm3(goal.replace(/\s…$/, ""));
1215
+ return recent.some((m) => norm3(m).startsWith(g));
1216
+ }
1217
+ function renderPrompt(rec, tier) {
1218
+ const full = tier === "full";
1219
+ const from = TOOL_LABEL[rec.from.tool];
1220
+ const dir = `.harlay/handoffs/${rec.id}`;
1221
+ const out = [
1222
+ `<handoff from="${from}" reason="${rec.from.reason}" task="${attr(rec.task.title)}">`,
1223
+ `You are taking over a coding session that ${from} could not finish (${REASON_TEXT[rec.from.reason]}). What you need to start is below; don't re-read the repo to rebuild it.`
1224
+ ];
1225
+ const goal = rec.task.originalGoal;
1226
+ const noRecent = rec.task.recentUserMessages.length === 0;
1227
+ if (goal && (full ? !goalIsRecent(goal, rec.task.recentUserMessages) : noRecent)) out.push("", "<original_goal>", safe(goal), "</original_goal>");
1228
+ if (rec.task.pinnedRules.length) {
1229
+ out.push("", "<rules_from_user>", ...rec.task.pinnedRules.map((r) => `- ${safe(r.text)}`));
1230
+ if (rec.task.moreRules) out.push(`(${rec.task.moreRules} more in the transcript)`);
1231
+ out.push("</rules_from_user>");
1232
+ }
1233
+ const recent = full ? rec.task.recentUserMessages : rec.task.recentUserMessages.slice(-2);
1234
+ if (recent.length) out.push("", '<recent_user_messages oldest_first="true">', ...recent.map((m) => `- ${safe(m)}`), "</recent_user_messages>");
1235
+ const where = rec.state.whereItStopped === null ? null : full ? rec.state.whereItStopped : clipWords(rec.state.whereItStopped, 60);
1236
+ const todos = rec.state.todos.map((t) => `[${t.status === "in_progress" ? "in progress" : "pending"}] ${safe(t.text)}`);
1237
+ if (where || todos.length) {
1238
+ out.push("", "<where_it_stopped>");
1239
+ if (where) out.push(safe(where));
1240
+ if (todos.length) out.push(`Open todos: ${todos.join("; ")}`);
1241
+ out.push("</where_it_stopped>");
1242
+ }
1243
+ if (full && (rec.files.touched.length || rec.files.uncommitted.length)) {
1244
+ out.push("", "<files>");
1245
+ if (rec.files.touched.length) out.push(`Touched: ${rec.files.touched.map(safe).join(", ")}`);
1246
+ if (rec.files.uncommitted.length) {
1247
+ const list = rec.files.uncommitted.map((f) => `${safe(f.path)} (${f.status}${f.added !== null && f.removed !== null ? `, +${f.added} -${f.removed}` : ""})`);
1248
+ out.push(`Uncommitted: ${list.join(", ")}${rec.files.moreUncommitted ? `, and ${rec.files.moreUncommitted} more` : ""}`);
1249
+ }
1250
+ out.push("</files>");
1251
+ }
1252
+ if (rec.lastFailure) {
1253
+ out.push("", "<last_failure>", `$ ${safe(rec.lastFailure.cmd)} (exit ${rec.lastFailure.exitCode})`);
1254
+ if (full) out.push(safe(rec.lastFailure.outputTail));
1255
+ out.push("</last_failure>");
1256
+ }
1257
+ const more = [`${dir}/transcript.md (search it, don't read it all)`];
1258
+ if (rec.details.diff) more.push(`${dir}/diff.patch`);
1259
+ const next = noRecent ? "Continue from where it stopped." : "Continue from the most recent user message.";
1260
+ out.push("", `Only if you need more: ${more.join(" and ")}.`, "</handoff>", "", `${next} Start with the next concrete step.`);
1261
+ return wellFormed(out.join("\n"));
1262
+ }
1263
+ function choosePrompt(rec, fits) {
1264
+ for (const tier of ["full", "compact"]) {
1265
+ const text = renderPrompt(rec, tier);
1266
+ if (text.length <= TIER_MAX[tier] && fits(text)) return { tier, text };
1267
+ }
1268
+ return { tier: "pointer", text: HANDOFF_PROMPT };
1029
1269
  }
1030
- function listWithin(lines, maxTokens, pointer) {
1031
- const max = maxTokens * CHARS_PER_TOKEN;
1032
- const out = [];
1033
- let used = 0;
1034
- for (let i = 0; i < lines.length; i++) {
1035
- const line = lines[i];
1036
- if (out.length > 0 && used + line.length + 1 > max) {
1037
- out.push(`- \u2026 ${lines.length - i} more (see \`${pointer}\`)`);
1038
- break;
1039
- }
1040
- out.push(line);
1041
- used += line.length + 1;
1042
- }
1043
- return out;
1270
+
1271
+ // ../../packages/core/src/brief/record.ts
1272
+ var RECORD_SCHEMA = "harlay/handoff@2";
1273
+ var MAX_UNCOMMITTED = 15;
1274
+ function buildRecord(d, id, previousId, r) {
1275
+ const a = (s) => r.apply(stripControl(s));
1276
+ const files = d.git?.files ?? [];
1277
+ return {
1278
+ schema: RECORD_SCHEMA,
1279
+ id,
1280
+ createdAt: d.createdAt.toISOString(),
1281
+ from: {
1282
+ tool: d.from.tool,
1283
+ model: d.from.model,
1284
+ session: d.from.session,
1285
+ reason: d.from.reason,
1286
+ resetsAt: d.from.resetsAt ? d.from.resetsAt.toISOString() : null,
1287
+ message: d.from.message === null ? null : a(d.from.message)
1288
+ },
1289
+ repo: { root: d.workdir, git: d.git !== null, branch: d.git?.branch ?? null, head: d.git?.head ?? null, dirtyFiles: files.length },
1290
+ task: {
1291
+ title: a(d.title),
1292
+ originalGoal: d.originalGoal === null ? null : clipWords(a(d.originalGoal), 60),
1293
+ recentUserMessages: d.recentUserMessages.map(a),
1294
+ pinnedRules: d.pinnedRules.map((p) => ({ text: a(p.text), message: p.message })),
1295
+ moreRules: d.moreRules
1296
+ },
1297
+ state: {
1298
+ whereItStopped: d.whereItStopped === null ? null : a(d.whereItStopped),
1299
+ todos: d.todos.map((t) => ({ text: oneLine(a(t.text), 200), status: t.status }))
1300
+ },
1301
+ files: {
1302
+ touched: d.filesTouched.map(a),
1303
+ uncommitted: files.slice(0, MAX_UNCOMMITTED).map((f) => ({ path: a(f.path), status: f.status, added: f.added, removed: f.removed })),
1304
+ moreUncommitted: Math.max(0, files.length - MAX_UNCOMMITTED)
1305
+ },
1306
+ lastFailure: d.lastFailure ? { cmd: clipWords(a(d.lastFailure.cmd), 30), exitCode: d.lastFailure.exitCode, outputTail: a(d.lastFailure.output).split("\n").slice(-8).join("\n") } : null,
1307
+ details: { transcript: "transcript.md", diff: d.git ? "diff.patch" : null, lastError: d.lastFailure ? "last-error.txt" : null },
1308
+ previous: previousId ? { id: previousId } : null,
1309
+ unknownRecords: d.unknownRecords
1310
+ };
1044
1311
  }
1045
- function tailWithin(s, maxTokens) {
1046
- const max = maxTokens * CHARS_PER_TOKEN;
1047
- const t = s.trimEnd();
1048
- if (t.length <= max) return t;
1049
- const cut = t.slice(t.length - max);
1050
- const nl = cut.indexOf("\n");
1051
- return "\u2026\n" + (nl >= 0 ? cut.slice(nl + 1) : cut);
1052
- }
1053
- function fenceFor(body) {
1054
- const longest = Math.max(0, ...(body.match(/`+/g) ?? []).map((r) => r.length));
1055
- return "`".repeat(Math.max(3, longest + 1));
1056
- }
1057
- function fileLine(f) {
1058
- const counts = f.added !== null && f.removed !== null ? `, +${f.added} -${f.removed}` : "";
1059
- return `- \`${f.path}\` (${f.status}${counts})`;
1060
- }
1061
- function renderBrief(d, id, opts = {}) {
1062
- const dir = `.harlay/handoffs/${id}`;
1063
- const transcript = `${dir}/transcript.md`;
1064
- const front = [
1065
- "---",
1066
- "harlay: 1",
1067
- `id: ${j(id)}`,
1068
- `status: ${j(d.status)}`,
1069
- `created: ${j(d.createdAt.toISOString())}`,
1070
- "from:",
1071
- ` tool: ${j(d.from.tool)}`,
1072
- ` model: ${j(d.from.model)}`,
1073
- ` session: ${j(d.from.session)}`,
1074
- ` reason: ${j(d.from.reason)}`,
1075
- ` resets_at: ${j(d.from.resetsAt ? d.from.resetsAt.toISOString() : null)}`,
1076
- "repo:",
1077
- ` path: ${j(d.workdir)}`,
1078
- ...d.git ? [` branch: ${j(d.git.branch)}`, ` base_commit: ${j(d.git.head)}`, ` dirty_files: ${d.git.files.length}`, ` diff_sha: ${j(d.git.diffSha.slice(0, 12))}`] : [" git: false"],
1079
- "---"
1080
- ];
1081
- const header = [
1082
- `# Continue: ${d.title}`,
1083
- "",
1084
- `> You are continuing a coding task that another AI agent (${TOOL_LABEL[d.from.tool]}) started but could not finish (${REASON_TEXT[d.from.reason]}).`,
1085
- "> This brief was extracted from its session log and the repo.",
1086
- d.git ? '> First run `git status` and check it matches "Changed files". Do not redo finished work.' : "> Check the files listed below before editing. Do not redo finished work."
1087
- ];
1088
- const goal = ["## 1. Goal (user's words)", d.goal ? quote(clip(d.goal, BUDGET.goal, transcript)) : "Not recorded. See the transcript."];
1089
- if (d.latestInstruction) goal.push("", "Latest instruction:", quote(clip(d.latestInstruction, BUDGET.latest, transcript)));
1090
- const next = ["## 2. Next step"];
1091
- if (!d.nextStep) next.push("Not recorded. Re-read the goal and the changed files, then decide.");
1092
- else if (d.nextStep.source === "todo") next.push(clip(oneLine(d.nextStep.text, 200), BUDGET.next));
1093
- else next.push("From the agent's last message:", quote(clip(d.nextStep.text, BUDGET.next)));
1094
- const blocker = ["## 3. Blocker"];
1095
- if (d.blocker) {
1096
- const body = tailWithin(d.blocker.output, BUDGET.blocker);
1097
- const fence = fenceFor(body);
1098
- blocker.push(`\`${oneLine(d.blocker.cmd, 200)}\` failed (exit ${d.blocker.exitCode}):`, `${fence}text`, body, fence, `Full output: \`${dir}/last-error.txt\``);
1099
- } else {
1100
- blocker.push("No failing command recorded.");
1101
- }
1102
- const progress = ["## 4. Progress"];
1103
- if (d.progressSource === "none") progress.push("Nothing recorded yet.");
1104
- else {
1105
- if (d.progressSource === "edits") progress.push("No task list was recorded. Files the agent edited:");
1106
- progress.push(...listWithin(d.progress.map((p) => `- [${p.done ? "x" : " "}] ${oneLine(p.text, 200)}`), BUDGET.progress, transcript));
1107
- }
1108
- const files = [];
1109
- if (d.git) {
1110
- files.push("## 5. Changed files (uncommitted)");
1111
- if (d.git.files.length === 0) files.push("Working tree is clean.");
1112
- else files.push(...listWithin(d.git.files.map(fileLine), BUDGET.files, `${dir}/diff.patch`), `Full diff: \`${dir}/diff.patch\``);
1113
- } else {
1114
- files.push("## 5. Changed files", "Not a git repository." + (d.editedFiles.length ? " Files the agent edited:" : ""));
1115
- files.push(...listWithin(d.editedFiles.map((f) => `- \`${f}\``), BUDGET.files, transcript));
1116
- }
1117
- const decisions = [
1118
- "## 6. Decisions and constraints",
1119
- ...d.decisions.length ? listWithin(d.decisions.map((s) => `- "${oneLine(s, 300)}"`), BUDGET.decisions, transcript) : ["None recorded."]
1120
- ];
1121
- const deadEnds = [
1122
- "## 7. Dead ends (don't retry)",
1123
- ...d.deadEnds.length ? listWithin(d.deadEnds.map((e) => `- \`${oneLine(e.cmd, 160)}\` failed (exit ${e.exitCode})`), BUDGET.deadEnds, transcript) : ["None recorded."]
1124
- ];
1125
- const more = ["## 8. Read more", `- Condensed transcript: \`${transcript}\``];
1126
- if (opts.previousBrief) more.push(`- Previous brief (as last updated by the previous agent): \`${dir}/previous-HANDOFF.md\``);
1127
- if (d.readFirst.length) more.push(`- Read these first: ${d.readFirst.map((f) => `\`${f}\``).join(", ")}`);
1128
- const stop = ["## 9. When you stop", "Before ending your turn, update sections 2\u20134 and `status:` in this file so the next agent can continue."];
1129
- return [front, header, goal, next, blocker, progress, files, decisions, deadEnds, more, stop].map((s) => s.join("\n")).join("\n\n") + "\n";
1312
+ var isStr = (v) => typeof v === "string";
1313
+ var isObj2 = (v) => typeof v === "object" && v !== null;
1314
+ function validateRecord(raw) {
1315
+ if (!isObj2(raw) || raw.schema !== RECORD_SCHEMA || !isStr(raw.id) || !isStr(raw.createdAt)) return null;
1316
+ const { task, from } = raw;
1317
+ if (!isObj2(from) || !isTool(from.tool)) return null;
1318
+ if (!isObj2(task) || !isStr(task.title) || !(task.originalGoal === null || isStr(task.originalGoal))) return null;
1319
+ if (!Array.isArray(task.recentUserMessages) || !task.recentUserMessages.every(isStr)) return null;
1320
+ if (!Array.isArray(task.pinnedRules) || !task.pinnedRules.every((r) => isObj2(r) && isStr(r.text) && typeof r.message === "number")) return null;
1321
+ return raw;
1130
1322
  }
1131
1323
 
1132
1324
  // ../../packages/core/src/brief/write.ts
1133
- var import_promises5 = require("node:fs/promises");
1134
- var import_node_path6 = require("node:path");
1135
1325
  async function allocateHandoffId(handoffsDir, baseId) {
1136
1326
  for (let i = 1; i < 1e3; i++) {
1137
1327
  const id = i === 1 ? baseId : `${baseId}-${i}`;
1138
1328
  try {
1139
- await (0, import_promises5.access)((0, import_node_path6.join)(handoffsDir, id));
1329
+ await (0, import_promises6.access)((0, import_node_path7.join)(handoffsDir, id));
1140
1330
  } catch {
1141
1331
  return id;
1142
1332
  }
@@ -1145,22 +1335,22 @@ async function allocateHandoffId(handoffsDir, baseId) {
1145
1335
  }
1146
1336
  async function ensureGitExclude(workdir) {
1147
1337
  const p = (await git(workdir, ["rev-parse", "--git-path", "info/exclude"])).trim();
1148
- const file = (0, import_node_path6.isAbsolute)(p) ? p : (0, import_node_path6.join)(workdir, p);
1338
+ const file = (0, import_node_path7.isAbsolute)(p) ? p : (0, import_node_path7.join)(workdir, p);
1149
1339
  let current2 = "";
1150
1340
  try {
1151
- current2 = await (0, import_promises5.readFile)(file, "utf8");
1341
+ current2 = await (0, import_promises6.readFile)(file, "utf8");
1152
1342
  } catch {
1153
1343
  current2 = "";
1154
1344
  }
1155
1345
  if (current2.split("\n").some((l) => l.trim() === ".harlay/" || l.trim() === ".harlay")) return;
1156
- await (0, import_promises5.mkdir)((0, import_node_path6.dirname)(file), { recursive: true });
1157
- await (0, import_promises5.appendFile)(file, `${current2 && !current2.endsWith("\n") ? "\n" : ""}.harlay/
1346
+ await (0, import_promises6.mkdir)((0, import_node_path7.dirname)(file), { recursive: true });
1347
+ await (0, import_promises6.appendFile)(file, `${current2 && !current2.endsWith("\n") ? "\n" : ""}.harlay/
1158
1348
  `);
1159
1349
  }
1160
1350
  async function readHandoffState(workdir) {
1161
1351
  let raw;
1162
1352
  try {
1163
- raw = JSON.parse(await (0, import_promises5.readFile)((0, import_node_path6.join)(workdir, ".harlay", "state.json"), "utf8"));
1353
+ raw = JSON.parse(await (0, import_promises6.readFile)((0, import_node_path7.join)(workdir, ".harlay", "state.json"), "utf8"));
1164
1354
  } catch {
1165
1355
  return null;
1166
1356
  }
@@ -1172,118 +1362,61 @@ async function readHandoffState(workdir) {
1172
1362
  }
1173
1363
  async function readIfExists(file) {
1174
1364
  try {
1175
- return await (0, import_promises5.readFile)(file, "utf8");
1365
+ return await (0, import_promises6.readFile)(file, "utf8");
1176
1366
  } catch {
1177
1367
  return null;
1178
1368
  }
1179
1369
  }
1180
- async function writeHandoff(d) {
1181
- const harlayDir = (0, import_node_path6.join)(d.workdir, ".harlay");
1182
- const latestPath = (0, import_node_path6.join)(harlayDir, "HANDOFF.md");
1183
- const id = await allocateHandoffId((0, import_node_path6.join)(harlayDir, "handoffs"), d.baseId);
1184
- const dir = (0, import_node_path6.join)(harlayDir, "handoffs", id);
1185
- await (0, import_promises5.mkdir)(dir, { recursive: true });
1186
- const r = new Redactor();
1187
- const previous = await readIfExists(latestPath);
1188
- if (previous !== null) await (0, import_promises5.writeFile)((0, import_node_path6.join)(dir, "previous-HANDOFF.md"), r.apply(previous));
1189
- const brief = r.apply(renderBrief(d, id, { previousBrief: previous !== null }));
1190
- const briefPath = (0, import_node_path6.join)(dir, "HANDOFF.md");
1191
- await (0, import_promises5.writeFile)(briefPath, brief);
1192
- await (0, import_promises5.writeFile)((0, import_node_path6.join)(dir, "transcript.md"), r.apply(d.transcript));
1193
- if (d.git) await (0, import_promises5.writeFile)((0, import_node_path6.join)(dir, "diff.patch"), r.apply(d.git.diff));
1194
- if (d.blocker) await (0, import_promises5.writeFile)((0, import_node_path6.join)(dir, "last-error.txt"), r.apply(d.blocker.output));
1195
- const from = {
1196
- ...d.from,
1197
- message: d.from.message === null ? null : r.apply(d.from.message),
1198
- resetsAt: d.from.resetsAt ? d.from.resetsAt.toISOString() : null
1199
- };
1200
- const goal = d.goal === null ? null : r.apply(d.goal);
1201
- const title = r.apply(d.title);
1202
- const meta = {
1203
- harlay: 1,
1204
- id,
1205
- createdAt: d.createdAt.toISOString(),
1206
- title,
1207
- goal,
1208
- from,
1209
- workdir: d.workdir,
1210
- unknownRecords: d.unknownRecords,
1211
- git: d.git ? { branch: d.git.branch, head: d.git.head, files: d.git.files.length, diffSha: d.git.diffSha } : null,
1212
- redactions: { ...r.counts }
1213
- };
1214
- await (0, import_promises5.writeFile)((0, import_node_path6.join)(dir, "meta.json"), JSON.stringify(meta, null, 2) + "\n");
1215
- await (0, import_promises5.writeFile)(latestPath, brief);
1216
- const state2 = { harlay: 1, lastId: id, goal, title };
1217
- await (0, import_promises5.writeFile)((0, import_node_path6.join)(harlayDir, "state.json"), JSON.stringify(state2, null, 2) + "\n");
1218
- if (d.git) await ensureGitExclude(d.workdir);
1219
- return { id, dir, briefPath, latestPath, redactions: { ...r.counts } };
1220
- }
1221
-
1222
- // ../../packages/core/src/launch/launchers.ts
1223
- var import_node_child_process2 = require("node:child_process");
1224
- var import_promises6 = require("node:fs/promises");
1225
- var import_node_path7 = require("node:path");
1226
- var import_node_util2 = require("node:util");
1227
- var pexec2 = (0, import_node_util2.promisify)(import_node_child_process2.execFile);
1228
- var HANDOFF_PROMPT = "Read .harlay/HANDOFF.md and continue the task.";
1229
- var DEFAULT_MODES = { "claude-code": "app", codex: "app", cursor: "app" };
1230
- var defaultRunner = {
1231
- async open(args) {
1232
- await pexec2("/usr/bin/open", args);
1233
- },
1234
- async writeExecutable(path, content) {
1235
- await (0, import_promises6.mkdir)((0, import_node_path7.dirname)(path), { recursive: true });
1236
- await (0, import_promises6.writeFile)(path, content, { mode: 493 });
1237
- await (0, import_promises6.chmod)(path, 493);
1238
- },
1239
- sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms))
1240
- };
1241
- function shq(s) {
1242
- return `'${s.replace(/'/g, `'\\''`)}'`;
1243
- }
1244
- var enc = encodeURIComponent;
1245
- function planLaunch(o) {
1246
- if (o.mode === "app") {
1247
- switch (o.target) {
1248
- case "claude-code":
1249
- return { target: o.target, mode: o.mode, steps: [{ kind: "open", args: [`claude://code/new?folder=${enc(o.cwd)}&q=${enc(o.prompt)}`] }] };
1250
- case "codex":
1251
- return { target: o.target, mode: o.mode, steps: [{ kind: "open", args: [`codex://threads/new?path=${enc(o.cwd)}&prompt=${enc(o.prompt)}`] }] };
1252
- case "cursor":
1253
- return {
1254
- target: o.target,
1255
- mode: o.mode,
1256
- steps: [
1257
- { kind: "open", args: ["-a", "Cursor", o.cwd] },
1258
- { kind: "wait", ms: 1500 },
1259
- { kind: "open", args: [`cursor://anysphere.cursor-deeplink/prompt?text=${enc(o.prompt)}`] }
1260
- ]
1261
- };
1370
+ async function readPreviousHandoff(workdir) {
1371
+ const raw = await readIfExists((0, import_node_path7.join)(workdir, ".harlay", "handoff.json"));
1372
+ if (raw !== null) {
1373
+ let rec;
1374
+ try {
1375
+ rec = validateRecord(JSON.parse(raw));
1376
+ } catch {
1377
+ return null;
1262
1378
  }
1379
+ if (!rec) return null;
1380
+ const { task } = rec;
1381
+ return {
1382
+ id: rec.id,
1383
+ goal: task.originalGoal,
1384
+ title: task.title,
1385
+ recentUserMessages: task.recentUserMessages,
1386
+ pinnedRules: task.pinnedRules,
1387
+ moreRules: typeof task.moreRules === "number" ? task.moreRules : 0
1388
+ };
1263
1389
  }
1264
- const bin = o.binaries[o.target];
1265
- if (!bin) throw new HarlayError(`${TOOL_LABEL[o.target]} CLI not found. Install it or switch ${TOOL_LABEL[o.target]} to app mode.`);
1266
- const argv = o.target === "codex" ? [bin, "-C", o.cwd, o.prompt] : o.target === "cursor" ? [bin, "--workspace", o.cwd, o.prompt] : [bin, o.prompt];
1267
- const path = (0, import_node_path7.join)(o.scriptDir, `continue-${o.target}.command`);
1268
- const content = `#!/bin/zsh -l
1269
- cd -- ${shq(o.cwd)} || exit 1
1270
- exec ${argv.map(shq).join(" ")}
1271
- `;
1272
- return {
1273
- target: o.target,
1274
- mode: o.mode,
1275
- steps: [
1276
- { kind: "script", path, content },
1277
- { kind: "open", args: ["-a", o.terminalApp, path] }
1278
- ]
1279
- };
1390
+ const state2 = await readHandoffState(workdir);
1391
+ return state2 ? { id: state2.lastId, goal: state2.goal, title: state2.title, recentUserMessages: [], pinnedRules: [], moreRules: 0 } : null;
1280
1392
  }
1281
- async function executeLaunch(plan, runner = defaultRunner) {
1282
- for (const step of plan.steps) {
1283
- if (step.kind === "open") await runner.open(step.args);
1284
- else if (step.kind === "script") await runner.writeExecutable(step.path, step.content);
1285
- else await runner.sleep(step.ms);
1286
- }
1393
+ function addCounts(a, b) {
1394
+ const out = { ...a };
1395
+ for (const [k, n] of Object.entries(b)) out[k] = (out[k] ?? 0) + n;
1396
+ return out;
1397
+ }
1398
+ async function writeHandoff(d, o) {
1399
+ const harlayDir = (0, import_node_path7.join)(d.workdir, ".harlay");
1400
+ const latestPath = (0, import_node_path7.join)(harlayDir, "HANDOFF.md");
1401
+ const id = await allocateHandoffId((0, import_node_path7.join)(harlayDir, "handoffs"), d.baseId);
1402
+ const dir = (0, import_node_path7.join)(harlayDir, "handoffs", id);
1403
+ await (0, import_promises6.mkdir)(dir, { recursive: true });
1404
+ const r = new Redactor();
1405
+ const body = buildRecord(d, id, o.previousId, r);
1406
+ const chosen = choosePrompt(body, (p) => promptFits(o.target, o.mode, p));
1407
+ const record = { ...body, prompt: { target: o.target, tier: chosen.tier, chars: chosen.text.length } };
1408
+ const json = JSON.stringify(record, null, 2) + "\n";
1409
+ const recordPath = (0, import_node_path7.join)(dir, "handoff.json");
1410
+ await (0, import_promises6.writeFile)(recordPath, json);
1411
+ await (0, import_promises6.writeFile)((0, import_node_path7.join)(dir, "prompt.md"), chosen.text + "\n");
1412
+ await (0, import_promises6.writeFile)((0, import_node_path7.join)(dir, "transcript.md"), r.apply(stripControl(d.transcript)));
1413
+ if (d.git) await (0, import_promises6.writeFile)((0, import_node_path7.join)(dir, "diff.patch"), r.apply(d.git.diff));
1414
+ if (d.lastFailure) await (0, import_promises6.writeFile)((0, import_node_path7.join)(dir, "last-error.txt"), r.apply(stripControl(d.lastFailure.output)));
1415
+ await (0, import_promises6.writeFile)(latestPath, renderPrompt(body, "full") + "\n");
1416
+ await (0, import_promises6.writeFile)((0, import_node_path7.join)(harlayDir, "handoff.json"), json);
1417
+ if (d.git) await ensureGitExclude(d.workdir);
1418
+ const redactions = addCounts(d.redactions ?? {}, r.counts);
1419
+ return { id, dir, recordPath, latestPath, prompt: chosen.text, tier: chosen.tier, redactions };
1287
1420
  }
1288
1421
 
1289
1422
  // ../../packages/core/src/launch/resolve.ts
@@ -1385,10 +1518,9 @@ async function runHandoff(o) {
1385
1518
  }
1386
1519
  const session = await readerFor(o.readers, o.session.tool).readSession(o.session);
1387
1520
  const git2 = await takeGitSnapshot(cwd);
1388
- const state2 = await readHandoffState(git2?.root ?? cwd);
1389
- const previous = state2 ? { id: state2.lastId, goal: state2.goal, title: state2.title } : null;
1521
+ const previous = await readPreviousHandoff(git2?.root ?? cwd);
1390
1522
  const data = extractHandoff(session, git2, { now: o.now ?? /* @__PURE__ */ new Date(), reason: o.reason, previous });
1391
- const written = await writeHandoff(data);
1523
+ const written = await writeHandoff(data, { target: o.target, mode: o.mode, previousId: data.carriedFrom });
1392
1524
  if (o.launch === false) return { written, plan: null, data };
1393
1525
  let plan;
1394
1526
  try {
@@ -1397,7 +1529,7 @@ async function runHandoff(o) {
1397
1529
  target: o.target,
1398
1530
  mode: o.mode,
1399
1531
  cwd: data.workdir,
1400
- prompt: HANDOFF_PROMPT,
1532
+ prompt: written.prompt,
1401
1533
  scriptDir: written.dir,
1402
1534
  binaries,
1403
1535
  terminalApp: o.terminalApp ?? "Terminal"
@@ -2047,11 +2179,11 @@ function bridgeCommand(dataDir2) {
2047
2179
  const q = `'${scriptPath(dataDir2).replace(/'/g, `'\\''`)}'`;
2048
2180
  return `${q} ${BRIDGE_MARKER}`;
2049
2181
  }
2050
- function isObj2(x) {
2182
+ function isObj3(x) {
2051
2183
  return typeof x === "object" && x !== null && !Array.isArray(x);
2052
2184
  }
2053
2185
  function isOurs(statusLine) {
2054
- return isObj2(statusLine) && typeof statusLine.command === "string" && statusLine.command.includes(BRIDGE_MARKER);
2186
+ return isObj3(statusLine) && typeof statusLine.command === "string" && statusLine.command.includes(BRIDGE_MARKER);
2055
2187
  }
2056
2188
  async function readSettings(path) {
2057
2189
  let text;
@@ -2062,7 +2194,7 @@ async function readSettings(path) {
2062
2194
  throw e;
2063
2195
  }
2064
2196
  const parsed = JSON.parse(text);
2065
- if (!isObj2(parsed)) throw new Error(`${path} is not a JSON object`);
2197
+ if (!isObj3(parsed)) throw new Error(`${path} is not a JSON object`);
2066
2198
  return parsed;
2067
2199
  }
2068
2200
  async function writeAtomic(path, text) {
@@ -2087,13 +2219,13 @@ async function installBridge(home2, dataDir2) {
2087
2219
  const prev = s.statusLine;
2088
2220
  if (prev !== void 0) {
2089
2221
  await writeAtomic(previousJson(dataDir2), JSON.stringify(prev, null, 2) + "\n");
2090
- const cmd = isObj2(prev) && typeof prev.command === "string" ? prev.command : "";
2222
+ const cmd = isObj3(prev) && typeof prev.command === "string" ? prev.command : "";
2091
2223
  await writeAtomic(previousCommand(dataDir2), cmd);
2092
2224
  } else {
2093
2225
  await (0, import_promises12.rm)(previousJson(dataDir2), { force: true });
2094
2226
  await (0, import_promises12.rm)(previousCommand(dataDir2), { force: true });
2095
2227
  }
2096
- s.statusLine = { ...isObj2(prev) ? prev : {}, type: "command", command: bridgeCommand(dataDir2) };
2228
+ s.statusLine = { ...isObj3(prev) ? prev : {}, type: "command", command: bridgeCommand(dataDir2) };
2097
2229
  await (0, import_promises12.mkdir)((0, import_node_path12.join)(home2, ".claude"), { recursive: true });
2098
2230
  await writeAtomic(path, JSON.stringify(s, null, 2) + "\n");
2099
2231
  }