@hizliemre/horse-code 0.1.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.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +150 -0
  3. package/dist/app-SB2L34JW.js +6217 -0
  4. package/dist/chunk-2DGO2BUB.js +4490 -0
  5. package/dist/chunk-2SVAHH5N.js +60 -0
  6. package/dist/chunk-3XVZXTB6.js +4469 -0
  7. package/dist/chunk-5UWA2UBM.js +69 -0
  8. package/dist/chunk-7TBYMFMG.js +147 -0
  9. package/dist/chunk-B67BK5GQ.js +34 -0
  10. package/dist/chunk-BY4DP7IE.js +20 -0
  11. package/dist/chunk-DKVIN43T.js +54 -0
  12. package/dist/chunk-DTWKSZXY.js +162 -0
  13. package/dist/chunk-F2IALVBU.js +212 -0
  14. package/dist/chunk-FFYBY2NA.js +392 -0
  15. package/dist/chunk-FGVJFMK5.js +123 -0
  16. package/dist/chunk-H2FDGPVW.js +42 -0
  17. package/dist/chunk-HBSC2HT2.js +85 -0
  18. package/dist/chunk-IW2KBAVZ.js +21 -0
  19. package/dist/chunk-JWAEW7AJ.js +121 -0
  20. package/dist/chunk-NNTIACT4.js +163 -0
  21. package/dist/chunk-O74BDQKS.js +28 -0
  22. package/dist/chunk-PGOYDOI4.js +426 -0
  23. package/dist/chunk-QF4MP6BS.js +69 -0
  24. package/dist/chunk-SSDLHWSF.js +35 -0
  25. package/dist/chunk-TOPZL5SU.js +1052 -0
  26. package/dist/chunk-YBWTCXUS.js +153 -0
  27. package/dist/chunk-YILDXPSI.js +1363 -0
  28. package/dist/clean-YOQATBMZ.js +18 -0
  29. package/dist/cli.js +1495 -0
  30. package/dist/discover-5URG7C4J.js +52 -0
  31. package/dist/fix-HBBOTUWM.js +34 -0
  32. package/dist/frontmatter-UNIPNLLO.js +6 -0
  33. package/dist/git-VTSZALSR.js +6 -0
  34. package/dist/install-O34KMWJB.js +113 -0
  35. package/dist/main-branch-KGWUINYQ.js +19 -0
  36. package/dist/ongoing-OV5XROTU.js +70 -0
  37. package/dist/project-graph-IOPCSZUA.js +56 -0
  38. package/dist/run-LQOZ5I7Z.js +610 -0
  39. package/dist/save-skills-OHYGVTQ4.js +13 -0
  40. package/dist/source-cache-XEK5WN7I.js +29 -0
  41. package/dist/trace-ZMB7LT7W.js +66 -0
  42. package/dist/trace-adopt-C6TUWFJL.js +79 -0
  43. package/dist/trace-run-F23MFTY4.js +24 -0
  44. package/dist/triage-2J3T5PVQ.js +30 -0
  45. package/dist/verify-WQ3GHION.js +479 -0
  46. package/dist/worktree-F7TWLWLN.js +87 -0
  47. package/package.json +64 -0
@@ -0,0 +1,426 @@
1
+ import {
2
+ stateRoot,
3
+ writableStateRoot
4
+ } from "./chunk-SSDLHWSF.js";
5
+ import {
6
+ writeAtomic
7
+ } from "./chunk-B67BK5GQ.js";
8
+
9
+ // src/engine/project-graph.ts
10
+ import { readFile, stat } from "fs/promises";
11
+ import { existsSync, readFileSync, statSync } from "fs";
12
+ import { spawn } from "child_process";
13
+ import { join } from "path";
14
+ var GRAPH_DIR = "graphify-out";
15
+ var GRAPH_FILE = "graph.json";
16
+ var STAMP_FILE = ".graph-commit.json";
17
+ function stampPath(cwd) {
18
+ return join(cwd, GRAPH_DIR, STAMP_FILE);
19
+ }
20
+ async function readStamp(cwd) {
21
+ try {
22
+ const raw = JSON.parse(await readFile(stampPath(cwd), "utf8"));
23
+ return typeof raw.commit === "string" && raw.commit ? raw : void 0;
24
+ } catch {
25
+ return void 0;
26
+ }
27
+ }
28
+ function git(cwd, args) {
29
+ return new Promise((resolve) => {
30
+ const child = spawn("git", args, { cwd, stdio: ["ignore", "pipe", "ignore"] });
31
+ let out = "";
32
+ child.stdout.on("data", (d) => {
33
+ if (out.length < 4e6) out += d.toString();
34
+ });
35
+ child.on("error", () => resolve(""));
36
+ child.on("close", (c) => resolve(c === 0 ? out : ""));
37
+ });
38
+ }
39
+ async function changedSince(cwd, commit) {
40
+ const known = await git(cwd, ["cat-file", "-e", `${commit}^{commit}`]);
41
+ if (known === void 0) return void 0;
42
+ const reachable = await git(cwd, ["rev-parse", "--verify", "--quiet", `${commit}^{commit}`]);
43
+ if (!reachable.trim()) return void 0;
44
+ const committed = await git(cwd, ["diff", "--name-only", `${commit}`, "HEAD"]);
45
+ const working = await git(cwd, ["status", "--porcelain", "--untracked-files=all"]);
46
+ const dirty = working.split("\n").map((l) => l.slice(3).trim()).filter(Boolean);
47
+ return [.../* @__PURE__ */ new Set([...committed.split("\n").filter(Boolean), ...dirty])];
48
+ }
49
+ var LABELS_FILE = ".graphify_labels.json";
50
+ function graphPath(cwd) {
51
+ return join(cwd, GRAPH_DIR, GRAPH_FILE);
52
+ }
53
+ function labelsPath(cwd) {
54
+ return join(cwd, GRAPH_DIR, LABELS_FILE);
55
+ }
56
+ function areaOf(g, n) {
57
+ return n?.community === void 0 ? void 0 : g.areas.get(n.community);
58
+ }
59
+ var UNNAMED = /^community[\s_-]*\d+$/i;
60
+ function parseAreas(raw) {
61
+ const out = /* @__PURE__ */ new Map();
62
+ if (!raw) return out;
63
+ try {
64
+ const doc = JSON.parse(raw);
65
+ if (typeof doc !== "object" || doc === null || Array.isArray(doc)) return out;
66
+ for (const [k, v] of Object.entries(doc)) {
67
+ const id = Number(k);
68
+ if (!Number.isInteger(id) || typeof v !== "string") continue;
69
+ const name = v.trim();
70
+ if (name && !UNNAMED.test(name)) out.set(id, name);
71
+ }
72
+ } catch {
73
+ }
74
+ return out;
75
+ }
76
+ function parseGraph(raw, labelsRaw) {
77
+ let doc;
78
+ try {
79
+ doc = JSON.parse(raw);
80
+ } catch {
81
+ return void 0;
82
+ }
83
+ const rawNodes = Array.isArray(doc.nodes) ? doc.nodes : [];
84
+ const rawEdges = Array.isArray(doc.links) ? doc.links : Array.isArray(doc.edges) ? doc.edges : [];
85
+ const nodes = rawNodes.filter((n) => typeof n === "object" && n !== null && typeof n.id === "string");
86
+ const edges = rawEdges.filter((e) => typeof e === "object" && e !== null && typeof e.source === "string" && typeof e.target === "string");
87
+ const byId = new Map(nodes.map((n) => [n.id, n]));
88
+ const incident = /* @__PURE__ */ new Map();
89
+ for (const e of edges) {
90
+ for (const end of [e.source, e.target]) {
91
+ const list = incident.get(end);
92
+ if (list) list.push(e);
93
+ else incident.set(end, [e]);
94
+ }
95
+ }
96
+ return { nodes, edges, byId, incident, areas: parseAreas(labelsRaw) };
97
+ }
98
+ async function pruneAreaNames(cwd) {
99
+ const path = labelsPath(cwd);
100
+ let labels;
101
+ try {
102
+ const doc = JSON.parse(await readFile(path, "utf8"));
103
+ if (typeof doc !== "object" || doc === null || Array.isArray(doc)) return 0;
104
+ labels = doc;
105
+ } catch {
106
+ return 0;
107
+ }
108
+ const graph = await loadGraph(cwd);
109
+ if (!graph) return 0;
110
+ const live = /* @__PURE__ */ new Set();
111
+ for (const n of graph.nodes) if (n.community !== void 0) live.add(n.community);
112
+ if (!live.size) return 0;
113
+ const kept = Object.keys(labels).filter((k) => live.has(Number(k)));
114
+ if (kept.length === Object.keys(labels).length) return 0;
115
+ const ordered = {};
116
+ for (const k of kept.sort((a, b) => Number(a) - Number(b))) ordered[k] = labels[k];
117
+ await writeAtomic(path, `${JSON.stringify(ordered, null, 2)}
118
+ `);
119
+ return Object.keys(labels).length - kept.length;
120
+ }
121
+ async function loadGraph(cwd) {
122
+ try {
123
+ const labels = await readFile(labelsPath(cwd), "utf8").catch(() => void 0);
124
+ return parseGraph(await readFile(graphPath(cwd), "utf8"), labels);
125
+ } catch {
126
+ return void 0;
127
+ }
128
+ }
129
+ function graphRoot(cwd) {
130
+ const root = stateRoot(cwd);
131
+ return existsSync(join(root, GRAPH_DIR, GRAPH_FILE)) ? root : void 0;
132
+ }
133
+ var cache = /* @__PURE__ */ new Map();
134
+ function stampOf(path) {
135
+ try {
136
+ const st = statSync(path);
137
+ return `${st.mtimeMs}:${st.size}`;
138
+ } catch {
139
+ return "";
140
+ }
141
+ }
142
+ function loadGraphSync(cwd) {
143
+ const root = graphRoot(cwd);
144
+ if (root === void 0) return void 0;
145
+ const path = graphPath(root);
146
+ const labels = labelsPath(root);
147
+ try {
148
+ const stamp = `${stampOf(path)}|${stampOf(labels)}`;
149
+ const hit = cache.get(path);
150
+ if (hit && hit.stamp === stamp) return hit.graph;
151
+ let labelsRaw;
152
+ try {
153
+ labelsRaw = readFileSync(labels, "utf8");
154
+ } catch {
155
+ }
156
+ const graph = parseGraph(readFileSync(path, "utf8"), labelsRaw);
157
+ cache.set(path, { stamp, graph });
158
+ return graph;
159
+ } catch {
160
+ return void 0;
161
+ }
162
+ }
163
+ var CODE_EXT = /\.(ts|tsx|js|jsx|mjs|cjs|py|go|rs|java|rb|c|h|cc|cpp|hpp|cs|php|swift|kt|scala)$/;
164
+ var NOT_INDEXED = /(^|\/)(dist|build|out|node_modules|vendor|coverage|graphify-out|\.[^/]+)\//;
165
+ var MAX_STALE_CHECK = 5e3;
166
+ function gitFiles(cwd) {
167
+ return new Promise((resolve) => {
168
+ const child = spawn("git", ["ls-files", "--cached", "--others", "--exclude-standard"], { cwd, stdio: ["ignore", "pipe", "ignore"] });
169
+ let out = "";
170
+ child.stdout.on("data", (d) => {
171
+ if (out.length < 4e6) out += d.toString();
172
+ });
173
+ child.on("error", () => resolve([]));
174
+ child.on("close", (c) => resolve(c === 0 ? out.split("\n").filter(Boolean) : []));
175
+ });
176
+ }
177
+ async function graphStatus(cwd) {
178
+ const path = graphPath(cwd);
179
+ if (!existsSync(path)) return { built: false, nodes: 0, edges: 0, stale: false, staleBecause: [] };
180
+ const [g, st] = await Promise.all([loadGraph(cwd), stat(path).catch(() => void 0)]);
181
+ const builtAt = st?.mtimeMs;
182
+ const staleBecause = [];
183
+ const known = /* @__PURE__ */ new Set();
184
+ for (const n of g?.nodes ?? []) if (n.source_file) known.add(n.source_file);
185
+ const counts = (f) => !NOT_INDEXED.test(f) && (known.has(f) || CODE_EXT.test(f));
186
+ const stamp = await readStamp(cwd);
187
+ if (stamp) {
188
+ const changed = await changedSince(cwd, stamp.commit);
189
+ if (changed) {
190
+ for (const f of changed) {
191
+ if (staleBecause.length >= 3) break;
192
+ if (counts(f)) staleBecause.push(f);
193
+ }
194
+ return {
195
+ built: true,
196
+ nodes: g?.nodes.length ?? 0,
197
+ edges: g?.edges.length ?? 0,
198
+ ...builtAt !== void 0 && { builtAt },
199
+ stale: staleBecause.length > 0,
200
+ staleBecause
201
+ };
202
+ }
203
+ }
204
+ if (builtAt !== void 0) {
205
+ const listed = await gitFiles(cwd);
206
+ const candidates = listed.filter(counts).slice(0, MAX_STALE_CHECK);
207
+ for (const f of candidates) {
208
+ if (staleBecause.length >= 3) break;
209
+ try {
210
+ const s = await stat(join(cwd, f));
211
+ if (s.mtimeMs > builtAt) staleBecause.push(f);
212
+ } catch {
213
+ }
214
+ }
215
+ }
216
+ return {
217
+ built: true,
218
+ nodes: g?.nodes.length ?? 0,
219
+ edges: g?.edges.length ?? 0,
220
+ ...builtAt !== void 0 && { builtAt },
221
+ stale: staleBecause.length > 0,
222
+ staleBecause
223
+ };
224
+ }
225
+ async function graphifyPython() {
226
+ const shim = await which("graphify");
227
+ if (shim) {
228
+ try {
229
+ const first = (await readFile(shim, "utf8")).split("\n", 1)[0];
230
+ const m = /^#!\s*(\S+)/.exec(first);
231
+ if (m && existsSync(m[1])) return m[1];
232
+ } catch {
233
+ }
234
+ }
235
+ const py = await which("python3");
236
+ if (!py) return void 0;
237
+ return (await run(py, ["-c", "import graphify"])).code === 0 ? py : void 0;
238
+ }
239
+ function which(cmd) {
240
+ return new Promise((resolve) => {
241
+ const child = spawn("sh", ["-c", `command -v ${cmd}`], { stdio: ["ignore", "pipe", "ignore"] });
242
+ let out = "";
243
+ child.stdout.on("data", (d) => out += d.toString());
244
+ child.on("error", () => resolve(void 0));
245
+ child.on("close", (c) => resolve(c === 0 && out.trim() ? out.trim() : void 0));
246
+ });
247
+ }
248
+ var BUILD_IDLE_TIMEOUT_MS = 3e5;
249
+ function run(cmd, args, cwd, idleMs = BUILD_IDLE_TIMEOUT_MS) {
250
+ return new Promise((resolve) => {
251
+ const child = spawn(cmd, args, { ...cwd ? { cwd } : {}, stdio: ["ignore", "pipe", "pipe"] });
252
+ let out = "";
253
+ let timedOut = false;
254
+ let timer;
255
+ const arm = () => {
256
+ clearTimeout(timer);
257
+ timer = setTimeout(() => {
258
+ timedOut = true;
259
+ child.kill("SIGKILL");
260
+ }, idleMs);
261
+ };
262
+ const take = (d) => {
263
+ arm();
264
+ if (out.length < 2e5) out += d.toString();
265
+ };
266
+ arm();
267
+ child.stdout.on("data", take);
268
+ child.stderr.on("data", take);
269
+ child.on("error", (e) => {
270
+ clearTimeout(timer);
271
+ resolve({ code: 1, out: e.message });
272
+ });
273
+ child.on("close", (c) => {
274
+ clearTimeout(timer);
275
+ resolve({ code: c ?? 1, out, timedOut });
276
+ });
277
+ });
278
+ }
279
+ function failureReason(out) {
280
+ const lines = out.split("\n").map((l) => l.trim()).filter(Boolean);
281
+ const diagnostic = lines.filter((l) => /(Traceback|Error|error:|Exception|No such file|Permission denied|MemoryError|Killed)/.test(l) && !/AST extraction:/.test(l));
282
+ const pick = diagnostic.length ? diagnostic.slice(-3) : lines.slice(-3);
283
+ return pick.join(" ").slice(0, 300);
284
+ }
285
+ function pruneTooling(doc, alsoDrop = /* @__PURE__ */ new Set()) {
286
+ const nodes = Array.isArray(doc.nodes) ? doc.nodes : [];
287
+ const keep = nodes.filter((n) => {
288
+ const f = typeof n.source_file === "string" ? n.source_file : "";
289
+ return !f || !NOT_INDEXED.test(f) && !alsoDrop.has(f);
290
+ });
291
+ const removed = nodes.length - keep.length;
292
+ if (!removed) return { removed: 0, kept: nodes.length };
293
+ const ids = new Set(keep.map((n) => String(n.id)));
294
+ doc.nodes = keep;
295
+ for (const key of ["links", "edges"]) {
296
+ const list = doc[key];
297
+ if (!Array.isArray(list)) continue;
298
+ doc[key] = list.filter((e) => ids.has(String(e.source)) && ids.has(String(e.target)));
299
+ }
300
+ return { removed, kept: keep.length };
301
+ }
302
+ async function writtenTracePaths(cwd) {
303
+ const out = /* @__PURE__ */ new Set();
304
+ try {
305
+ const { loadTraceIndex, traceRootRel } = await import("./trace-ZMB7LT7W.js");
306
+ const index = await loadTraceIndex(cwd);
307
+ const root = traceRootRel().replace(/\\/g, "/");
308
+ for (const [file, rec] of Object.entries(index.traces)) {
309
+ if (!rec.doc) out.add(`${root}/${file}.md`);
310
+ }
311
+ } catch {
312
+ }
313
+ return out;
314
+ }
315
+ var LABEL_LOSS_LIMIT = 0.25;
316
+ function namedCount(doc) {
317
+ return Object.values(doc).filter((v) => !(typeof v === "string" && UNNAMED.test(v.trim()))).length;
318
+ }
319
+ async function restoreSharedLabels(cwd) {
320
+ try {
321
+ await git(cwd, ["checkout", "--", `${GRAPH_DIR}/${LABELS_FILE}`]);
322
+ } catch {
323
+ }
324
+ }
325
+ async function buildProjectGraph(cwd) {
326
+ const py = await graphifyPython();
327
+ if (!py) {
328
+ return {
329
+ ok: false,
330
+ message: "graphify is not installed. `uv tool install graphifyy` (or `pipx install graphifyy`) \u2014 it is MIT, pure AST parsing, and costs no tokens."
331
+ };
332
+ }
333
+ const shared = writableStateRoot(cwd) === void 0;
334
+ if (shared) await restoreSharedLabels(cwd);
335
+ try {
336
+ return await runGraphBuild(py, cwd, shared);
337
+ } finally {
338
+ if (shared) await restoreSharedLabels(cwd);
339
+ }
340
+ }
341
+ async function runGraphBuild(py, cwd, shared) {
342
+ const script = "from graphify.watch import _rebuild_code\nfrom pathlib import Path\nimport sys\nsys.exit(0 if _rebuild_code(Path('.')) else 1)\n";
343
+ const r = await run(py, ["-c", script], cwd);
344
+ if (r.timedOut) {
345
+ return {
346
+ ok: false,
347
+ message: `Graph build stopped: it produced no output for ${Math.round(BUILD_IDLE_TIMEOUT_MS / 6e4)} minutes and was killed. Last it said: ${failureReason(r.out)}`
348
+ };
349
+ }
350
+ if (r.code !== 0) return { ok: false, message: `Graph build failed: ${failureReason(r.out)}` };
351
+ let pruned = 0;
352
+ try {
353
+ const path = graphPath(cwd);
354
+ const doc = JSON.parse(await readFile(path, "utf8"));
355
+ const res = pruneTooling(doc, await writtenTracePaths(cwd));
356
+ if (res.removed) {
357
+ await writeAtomic(path, JSON.stringify(doc));
358
+ pruned = res.removed;
359
+ }
360
+ } catch {
361
+ }
362
+ try {
363
+ const head = (await git(cwd, ["rev-parse", "HEAD"])).trim();
364
+ if (head) await writeAtomic(stampPath(cwd), `${JSON.stringify({ commit: head })}
365
+ `);
366
+ } catch {
367
+ }
368
+ let lost = 0;
369
+ if (!shared) {
370
+ try {
371
+ const lp = labelsPath(cwd);
372
+ const doc = JSON.parse(await readFile(lp, "utf8"));
373
+ const head = await git(cwd, ["show", `HEAD:${GRAPH_DIR}/${LABELS_FILE}`]).catch(() => "");
374
+ const before = head ? namedCount(JSON.parse(head)) : 0;
375
+ const after = namedCount(doc);
376
+ if (before && after < before * (1 - LABEL_LOSS_LIMIT)) {
377
+ await restoreSharedLabels(cwd);
378
+ lost = before - after;
379
+ } else {
380
+ const named = Object.fromEntries(Object.entries(doc).filter(([, v]) => !(typeof v === "string" && UNNAMED.test(v.trim()))));
381
+ if (Object.keys(doc).length !== Object.keys(named).length) {
382
+ const ordered = {};
383
+ for (const k of Object.keys(named).sort((a, b) => Number(a) - Number(b))) ordered[k] = named[k];
384
+ await writeAtomic(lp, `${JSON.stringify(ordered, null, 2)}
385
+ `);
386
+ }
387
+ }
388
+ } catch {
389
+ }
390
+ }
391
+ const g = await loadGraph(cwd);
392
+ return {
393
+ ok: true,
394
+ message: `Graph built: ${g?.nodes.length ?? 0} nodes, ${g?.edges.length ?? 0} edges.` + (pruned ? ` (${pruned.toLocaleString("en-US")} tooling node(s) left out \u2014 skills and agent state are not the project.)` : "") + (lost ? ` (Community names kept as they were: this build named ${lost.toLocaleString("en-US")} fewer than the committed file, which is damage rather than progress. Re-run \`/graph build\` when the build can finish uninterrupted.)` : ""),
395
+ nodes: g?.nodes.length ?? 0,
396
+ edges: g?.edges.length ?? 0
397
+ };
398
+ }
399
+
400
+ export {
401
+ GRAPH_DIR,
402
+ GRAPH_FILE,
403
+ STAMP_FILE,
404
+ stampPath,
405
+ readStamp,
406
+ changedSince,
407
+ LABELS_FILE,
408
+ graphPath,
409
+ labelsPath,
410
+ areaOf,
411
+ parseAreas,
412
+ parseGraph,
413
+ pruneAreaNames,
414
+ loadGraph,
415
+ graphRoot,
416
+ loadGraphSync,
417
+ MAX_STALE_CHECK,
418
+ graphStatus,
419
+ graphifyPython,
420
+ BUILD_IDLE_TIMEOUT_MS,
421
+ failureReason,
422
+ pruneTooling,
423
+ LABEL_LOSS_LIMIT,
424
+ namedCount,
425
+ buildProjectGraph
426
+ };
@@ -0,0 +1,69 @@
1
+ import {
2
+ patchConfig
3
+ } from "./chunk-H2FDGPVW.js";
4
+
5
+ // src/engine/main-branch.ts
6
+ import { readFile } from "fs/promises";
7
+ import { join } from "path";
8
+ var COMMON_MAIN_BRANCHES = ["main", "master", "development", "develop", "trunk"];
9
+ async function recordedMainBranch(cwd) {
10
+ try {
11
+ const raw = await readFile(join(cwd, ".horsecode", "config.json"), "utf8");
12
+ const parsed = JSON.parse(raw);
13
+ if (!parsed || typeof parsed !== "object") return void 0;
14
+ const v = parsed.mainBranch;
15
+ return typeof v === "string" && v.trim() ? v.trim() : void 0;
16
+ } catch {
17
+ return void 0;
18
+ }
19
+ }
20
+ async function saveMainBranch(cwd, branch) {
21
+ return patchConfig(cwd, (current) => ({ ...current, mainBranch: branch }));
22
+ }
23
+ async function detectMainBranch(cwd, git) {
24
+ const r = await git(["symbolic-ref", "--short", "refs/remotes/origin/HEAD"], cwd);
25
+ if (r.code !== 0) return void 0;
26
+ const name = r.stdout.trim().replace(/^origin\//, "");
27
+ return name || void 0;
28
+ }
29
+ async function remoteBranches(cwd, git) {
30
+ const r = await git(["for-each-ref", "--format=%(refname:short)", "refs/remotes/origin"], cwd);
31
+ if (r.code !== 0) return [];
32
+ return r.stdout.split("\n").map((s) => s.trim().replace(/^origin\//, "")).filter((s) => s && s !== "HEAD");
33
+ }
34
+ function mainBranchChoices(remote, current) {
35
+ const have = remote.filter((b) => b !== current && !b.startsWith("hc/"));
36
+ const common = COMMON_MAIN_BRANCHES.filter((b) => have.includes(b));
37
+ const rest = have.filter((b) => !common.includes(b));
38
+ return [...common, ...rest].slice(0, 8);
39
+ }
40
+ var MAIN_BRANCH_QUESTION = "Which branch is this project's main one? I sync it into the session branch before continuing, so the work carries on against current code. I'll remember the answer and won't ask again.";
41
+ async function resolveMainBranch(deps) {
42
+ const recorded = await recordedMainBranch(deps.cwd);
43
+ if (recorded) return recorded;
44
+ const detected = await detectMainBranch(deps.cwd, deps.git);
45
+ if (detected) {
46
+ await saveMainBranch(deps.cwd, detected);
47
+ deps.note?.(`\u{1F33F} Main branch: \`${detected}\` (from \`origin/HEAD\`) \u2014 remembered for this project.`);
48
+ return detected;
49
+ }
50
+ const choices = mainBranchChoices(await remoteBranches(deps.cwd, deps.git));
51
+ const answer = (await deps.askUser(
52
+ await (deps.phrase ?? ((t) => Promise.resolve(t)))(MAIN_BRANCH_QUESTION),
53
+ choices.length ? { options: choices } : void 0
54
+ )).trim();
55
+ if (!answer) return void 0;
56
+ await saveMainBranch(deps.cwd, answer);
57
+ deps.note?.(`\u{1F33F} Main branch: \`${answer}\` \u2014 remembered for this project.`);
58
+ return answer;
59
+ }
60
+
61
+ export {
62
+ COMMON_MAIN_BRANCHES,
63
+ recordedMainBranch,
64
+ saveMainBranch,
65
+ detectMainBranch,
66
+ mainBranchChoices,
67
+ MAIN_BRANCH_QUESTION,
68
+ resolveMainBranch
69
+ };
@@ -0,0 +1,35 @@
1
+ // src/engine/session-scope.ts
2
+ import { dirname, join, resolve, sep } from "path";
3
+ var HC_DIR = ".horsecode";
4
+ var WORKTREES = join(HC_DIR, "worktrees");
5
+ var BASE = "base";
6
+ function sessionBase(cwd) {
7
+ const abs = resolve(cwd);
8
+ const marker = `${sep}${WORKTREES}${sep}`;
9
+ const at = abs.indexOf(marker);
10
+ if (at < 0) return void 0;
11
+ const after = abs.slice(at + marker.length);
12
+ const job = after.split(sep)[0];
13
+ if (!job) return void 0;
14
+ return join(abs.slice(0, at), WORKTREES, job, BASE);
15
+ }
16
+ function stateRoot(cwd) {
17
+ return sessionBase(cwd) ?? resolve(cwd);
18
+ }
19
+ function writableStateRoot(cwd) {
20
+ return sessionBase(cwd);
21
+ }
22
+ function inLinkedWorktree(cwd, run) {
23
+ if (sessionBase(cwd) !== void 0) return true;
24
+ const dir = run(["rev-parse", "--absolute-git-dir"])?.trim();
25
+ const common = run(["rev-parse", "--path-format=absolute", "--git-common-dir"])?.trim();
26
+ if (!dir || !common) return false;
27
+ return resolve(dir) !== resolve(common);
28
+ }
29
+
30
+ export {
31
+ sessionBase,
32
+ stateRoot,
33
+ writableStateRoot,
34
+ inLinkedWorktree
35
+ };