@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,610 @@
1
+ import {
2
+ loadMigrated,
3
+ recordMigrated
4
+ } from "./chunk-2SVAHH5N.js";
5
+ import "./chunk-SSDLHWSF.js";
6
+ import "./chunk-B67BK5GQ.js";
7
+
8
+ // src/migrate/run.ts
9
+ import { cp, lstat, mkdir, readFile as readFile2, readlink, rm } from "fs/promises";
10
+ import { dirname, join as join2, resolve } from "path";
11
+
12
+ // src/migrate/discover.ts
13
+ import { readFile, readdir, stat } from "fs/promises";
14
+ import { join } from "path";
15
+ var RULE_FILES = [
16
+ { file: "CLAUDE.md", tool: "Claude Code" },
17
+ { file: "CLAUDE.local.md", tool: "Claude Code" },
18
+ { file: "AGENTS.md", tool: "Codex / OpenAI" },
19
+ { file: "GEMINI.md", tool: "Gemini CLI" },
20
+ { file: ".cursorrules", tool: "Cursor" },
21
+ { file: ".windsurfrules", tool: "Windsurf" },
22
+ { file: ".clinerules", tool: "Cline" },
23
+ { file: ".continuerules", tool: "Continue" },
24
+ { file: ".aider.conf.yml", tool: "Aider" },
25
+ { file: "CONVENTIONS.md", tool: "Aider" },
26
+ { file: ".rules", tool: "Zed" },
27
+ { file: ".github/copilot-instructions.md", tool: "GitHub Copilot" },
28
+ /**
29
+ * Not a tool's files — the project's own product and design context, which several skills read as their
30
+ * source of truth. They come as a PAIR: the skill that consumes them prints "the project's PRODUCT.md (and
31
+ * DESIGN.md when present)", so taking one without the other imports half a brief.
32
+ *
33
+ * They are here because they are instruction material by any test that matters: a real project's DESIGN.md
34
+ * held 17 KB of binding conventions (tokens, type scale, spacing, what never to do) and its PRODUCT.md the
35
+ * register those conventions serve. Leaving them behind meant every agent rebuilt its taste from scratch
36
+ * while the answer sat in the repo root.
37
+ */
38
+ { file: "PRODUCT.md", tool: "project convention", own: true },
39
+ { file: "DESIGN.md", tool: "project convention", own: true }
40
+ ];
41
+ var RULE_DIRS = [
42
+ { dir: ".cursor/rules", tool: "Cursor", ext: /\.mdc?$/ },
43
+ { dir: ".github/instructions", tool: "GitHub Copilot", ext: /\.md$/ }
44
+ ];
45
+ var MAX_FINDING_BYTES = 6e4;
46
+ function claudeProjectSlug(cwd) {
47
+ return cwd.replace(/\//g, "-");
48
+ }
49
+ async function read(path) {
50
+ try {
51
+ const s = await stat(path);
52
+ if (!s.isFile() || s.size === 0) return void 0;
53
+ if (s.size > MAX_FINDING_BYTES) return { bytes: s.size };
54
+ return { bytes: s.size, text: await readFile(path, "utf8") };
55
+ } catch {
56
+ return void 0;
57
+ }
58
+ }
59
+ async function listDir(dir) {
60
+ try {
61
+ return (await readdir(dir, { withFileTypes: true })).filter((e) => e.isFile()).map((e) => e.name);
62
+ } catch {
63
+ return [];
64
+ }
65
+ }
66
+ async function discover(opts) {
67
+ const { cwd, home } = opts;
68
+ const out = [];
69
+ const push = async (kind, tool, path, label, own = false) => {
70
+ const r = await read(path);
71
+ if (r) out.push({ kind, tool, path, label, bytes: r.bytes, ...own ? { own } : {}, ...r.text ? { text: r.text } : {} });
72
+ };
73
+ for (const { file, tool, own } of RULE_FILES) await push("rules", tool, join(cwd, file), file, own);
74
+ await push("rules", "Claude Code (user-level)", join(home, ".claude", "CLAUDE.md"), "~/.claude/CLAUDE.md");
75
+ await push("rules", "Codex (user-level)", join(home, ".codex", "AGENTS.md"), "~/.codex/AGENTS.md");
76
+ for (const { dir, tool, ext } of RULE_DIRS) {
77
+ for (const name of await listDir(join(cwd, dir))) {
78
+ if (ext.test(name)) await push("rules", tool, join(cwd, dir, name), `${dir}/${name}`);
79
+ }
80
+ }
81
+ const memDir = join(home, ".claude", "projects", claudeProjectSlug(cwd), "memory");
82
+ for (const name of await listDir(memDir)) {
83
+ if (!name.endsWith(".md") || name === "MEMORY.md") continue;
84
+ await push("memory", "Claude Code", join(memDir, name), `memory/${name}`);
85
+ }
86
+ const seenSkills = /* @__PURE__ */ new Set();
87
+ for (const [rel, tool] of [[".claude/skills", "Claude Code"], [".agents/skills", "agent skills"]]) {
88
+ const skillsDir = join(cwd, rel);
89
+ let entries;
90
+ try {
91
+ entries = await readdir(skillsDir, { withFileTypes: true });
92
+ } catch {
93
+ continue;
94
+ }
95
+ for (const e of entries) {
96
+ if (seenSkills.has(e.name)) continue;
97
+ if (!e.isDirectory()) {
98
+ if (!e.isSymbolicLink()) continue;
99
+ try {
100
+ if (!(await stat(join(skillsDir, e.name))).isDirectory()) continue;
101
+ } catch {
102
+ continue;
103
+ }
104
+ }
105
+ const before = out.length;
106
+ await push("skill", tool, join(skillsDir, e.name, "SKILL.md"), `${rel}/${e.name}`);
107
+ if (out.length > before) seenSkills.add(e.name);
108
+ }
109
+ }
110
+ for (const [file, tool] of [[".mcp.json", "Claude Code"], [".cursor/mcp.json", "Cursor"], [".vscode/mcp.json", "VS Code"]]) {
111
+ await push("mcp", tool, join(cwd, file), file);
112
+ }
113
+ for (const settings of [".claude/settings.json", ".claude/settings.local.json"]) {
114
+ await push("mcp", "Claude Code", join(cwd, settings), settings);
115
+ }
116
+ for (const [dir, kind] of [[".claude/agents", "agent"], [".claude/commands", "command"]]) {
117
+ for (const name of await listDir(join(cwd, dir))) {
118
+ if (name.endsWith(".md")) await push(kind, "Claude Code", join(cwd, dir, name), `${dir}/${name}`);
119
+ }
120
+ }
121
+ return out;
122
+ }
123
+ function summarize(findings) {
124
+ if (!findings.length) return "No configuration from another coding tool was found in this project.";
125
+ const byKind = /* @__PURE__ */ new Map();
126
+ for (const f of findings) {
127
+ const list = byKind.get(f.kind);
128
+ if (list) list.push(f);
129
+ else byKind.set(f.kind, [f]);
130
+ }
131
+ const LABEL = {
132
+ rules: "Instruction files",
133
+ memory: "Remembered facts",
134
+ skill: "Skills",
135
+ mcp: "MCP / settings",
136
+ agent: "Subagents",
137
+ command: "Slash commands"
138
+ };
139
+ const rows = [];
140
+ for (const kind of ["rules", "memory", "skill", "mcp", "agent", "command"]) {
141
+ const list = byKind.get(kind);
142
+ if (!list?.length) continue;
143
+ const tools = [...new Set(list.map((f) => f.tool))].join(", ");
144
+ const files = list.slice(0, 4).map((f) => `\`${f.label}\``).join(", ");
145
+ const more = list.length > 4 ? ` +${list.length - 4} more` : "";
146
+ rows.push(`- **${LABEL[kind]}** (${list.length}, from ${tools}): ${files}${more}`);
147
+ }
148
+ return rows.join("\n");
149
+ }
150
+ function hasAnything(findings) {
151
+ return findings.some((f) => f.kind === "rules" || f.kind === "memory" || f.kind === "skill" || f.kind === "mcp");
152
+ }
153
+
154
+ // src/migrate/extract.ts
155
+ var BATCH = 20;
156
+ var SYSTEM = "You migrate instructions from one coding assistant to another. You are precise about what does and does not transfer, and you never invent an instruction that was not there.";
157
+ var CLASSIFY = (body) => `Below is instruction material from a project that was developed with another coding assistant. It is being moved into a different assistant, which has its OWN tools, commands and agent model.
158
+
159
+ Classify every distinct instruction into exactly one of:
160
+
161
+ **rule** \u2014 a standing directive that must be obeyed on EVERY task, forever. Before choosing this, apply the test: would this still need saying on a task that has nothing to do with the section it came from? Language requirements, commit conventions, things never to do, security mandates pass that test. Process detail does NOT: "in phase 3 the implementer does X", "the plan document contains these sections", "run this before that" describe a procedure, and a procedure belongs in the knowledge that is recalled when relevant, not in the instructions of every agent on every task.
162
+ A rule is inlined into every prompt permanently, so a long list of them is itself a defect: expect only a handful from any document, and classify the rest as fact.
163
+ **fact** \u2014 durable knowledge about this project or user, INCLUDING its processes and conventions: stack, architecture decisions, workflow phases, service names, preferences. Recalled when relevant. This is the right home for most of what looks instructional.
164
+ **skip** \u2014 two kinds of thing.
165
+ (a) Anything that only means something in the original tool: named tools, slash commands, subagent types, file paths of that tool's own config, its plugin or hook system. Also anything stale, empty, or purely descriptive of the document itself.
166
+ (b) ORCHESTRATION \u2014 how work is sequenced and gated. The receiving assistant runs its own fixed pipeline (brainstorm \u2192 specification \u2192 clarification \u2192 plan \u2192 task breakdown \u2192 parallel implementation waves \u2192 multi-lens review \u2192 council \u2192 judge \u2192 acceptance gate), and it cannot be reconfigured by an imported instruction. So numbered phases ("Phase 0 \u2026 Phase 7"), what happens in which phase, phase entry/exit gates, when to branch or open a worktree, when review runs, who approves what, and the order of any of it are SUPERSEDED \u2014 not merely irrelevant. Importing them would leave agents following two different workflows at once. Skip them even when they are stated as mandatory, and even as a fact: a remembered description of a competing workflow is still a competing workflow.
167
+ What a phase PRODUCES can still be a rule if it stands alone: "every requirement must be testable" survives without the phase it was written in; "in Phase 1 the analyst writes the spec" does not.
168
+
169
+ Rules for your output:
170
+ - REWRITE each kept item so it stands alone, in English, without referring to the original tool. If it cannot survive that rewrite, it is a skip.
171
+ - Do NOT merge distinct instructions, and do NOT invent any that are not present.
172
+ - For every skip, say in one short phrase WHAT made it tool-specific \u2014 the user decides whether to disagree, and cannot do that without the reason.
173
+ - Prefer skip when unsure. A wrong rule is applied to every task forever; a missed one can be added later.
174
+
175
+ Answer with a fenced json block:
176
+ {"items":[{"text":"...","disposition":"rule|fact|skip","reason":"..."}]}
177
+
178
+ Material:
179
+
180
+ ${body}`;
181
+ function parseItems(text) {
182
+ const fence = /```(?:json)?\s*([\s\S]*?)```/.exec(text);
183
+ const raw = fence ? fence[1] : text.slice(text.indexOf("{"));
184
+ try {
185
+ const parsed = JSON.parse(raw);
186
+ if (!Array.isArray(parsed.items)) return void 0;
187
+ return parsed.items.filter((i) => typeof i === "object" && i !== null && typeof i.text === "string" && typeof i.disposition === "string");
188
+ } catch {
189
+ return void 0;
190
+ }
191
+ }
192
+ async function ask(provider, model, prompt, signal) {
193
+ const req = {
194
+ model,
195
+ messages: [{ role: "system", content: SYSTEM }, { role: "user", content: prompt }],
196
+ tools: []
197
+ };
198
+ let out = "";
199
+ for await (const ev of provider.chat(req, signal ?? new AbortController().signal)) {
200
+ if (ev.type === "text-delta") out += ev.text;
201
+ else if (ev.type === "error") throw new Error(ev.message);
202
+ }
203
+ return out;
204
+ }
205
+ async function askChain(provider, models, dead, prompt, signal) {
206
+ const usable = models.filter((m) => m && !dead.has(m));
207
+ const chain = usable.length ? usable : models.filter(Boolean);
208
+ if (!chain.length) throw new Error("no model is configured for the migration");
209
+ let last;
210
+ for (const m of chain) {
211
+ try {
212
+ return await ask(provider, m, prompt, signal);
213
+ } catch (e) {
214
+ last = e;
215
+ dead.add(m);
216
+ }
217
+ }
218
+ throw last instanceof Error ? last : new Error(String(last));
219
+ }
220
+ var DISPOSITIONS = /* @__PURE__ */ new Set(["rule", "fact", "skip"]);
221
+ async function classify(opts) {
222
+ const text = await askChain(opts.provider, opts.models, opts.dead ?? /* @__PURE__ */ new Set(), CLASSIFY(opts.body), opts.signal);
223
+ const items = parseItems(text);
224
+ if (!items) throw new Error("the classification could not be read");
225
+ return items.filter((i) => i.text.trim()).map((i) => ({
226
+ text: i.text.trim(),
227
+ disposition: DISPOSITIONS.has(i.disposition) ? i.disposition : "skip",
228
+ reason: (i.reason ?? "").trim() || "no reason given",
229
+ source: opts.source
230
+ }));
231
+ }
232
+ var MAX_CHUNK_CHARS = 8e3;
233
+ function chunkProse(text, max = MAX_CHUNK_CHARS) {
234
+ const parts = text.split(/\n(?=#{1,3} )/);
235
+ const out = [];
236
+ let buf = "";
237
+ for (const part of parts) {
238
+ if (part.length >= max) {
239
+ if (buf.trim()) {
240
+ out.push(buf);
241
+ buf = "";
242
+ }
243
+ out.push(part.slice(0, max * 2));
244
+ continue;
245
+ }
246
+ if (buf.length + part.length > max) {
247
+ out.push(buf);
248
+ buf = part;
249
+ } else buf += (buf ? "\n" : "") + part;
250
+ }
251
+ if (buf.trim()) out.push(buf);
252
+ return out.filter((c) => c.trim());
253
+ }
254
+ function batches(findings, size = BATCH) {
255
+ const out = [];
256
+ for (const f of findings.filter((x) => x.kind === "rules" && x.text)) {
257
+ const chunks = chunkProse(f.text);
258
+ chunks.forEach((body, i) => out.push({
259
+ body: `### ${f.label}${chunks.length > 1 ? ` (part ${i + 1}/${chunks.length})` : ""}
260
+ ${body}`,
261
+ source: f.label
262
+ }));
263
+ }
264
+ const mem = findings.filter((x) => x.kind === "memory" && x.text);
265
+ for (let i = 0; i < mem.length; i += size) {
266
+ const chunk = mem.slice(i, i + size);
267
+ out.push({
268
+ body: chunk.map((f) => `### ${f.label}
269
+ ${f.text}`).join("\n\n"),
270
+ source: `${chunk.length} remembered facts`
271
+ });
272
+ }
273
+ return out;
274
+ }
275
+ async function extractAll(opts) {
276
+ const jobs = batches(opts.findings);
277
+ const dead = /* @__PURE__ */ new Set();
278
+ const candidates = [];
279
+ const failed = [];
280
+ let done = 0;
281
+ const queue = [...jobs];
282
+ const worker = async () => {
283
+ for (; ; ) {
284
+ const job = queue.shift();
285
+ if (!job || opts.signal?.aborted) return;
286
+ try {
287
+ candidates.push(...await classify({ ...opts, dead, body: job.body, source: job.source }));
288
+ } catch (e) {
289
+ failed.push({ source: job.source, error: e instanceof Error ? e.message : String(e) });
290
+ }
291
+ opts.onProgress?.(++done, jobs.length, job.source);
292
+ }
293
+ };
294
+ await Promise.all(Array.from({ length: Math.min(opts.concurrency ?? 5, queue.length) }, worker));
295
+ return { candidates, failed };
296
+ }
297
+ function groupForReview(e) {
298
+ return {
299
+ rules: e.candidates.filter((c) => c.disposition === "rule"),
300
+ facts: e.candidates.filter((c) => c.disposition === "fact"),
301
+ skipped: e.candidates.filter((c) => c.disposition === "skip")
302
+ };
303
+ }
304
+ var MAX_RULES = 25;
305
+ var CONSOLIDATE = (items, max) => `Below are ${items.length} candidate standing rules extracted from one project's instruction documents. They overlap heavily and many are procedure rather than principle.
306
+
307
+ Produce AT MOST ${max} rules. Each one is inlined into every agent's instructions on every task, forever, so the list is a permanent cost and must earn its size.
308
+
309
+ How to decide:
310
+ - MERGE candidates that say the same thing differently into one rule that covers both. Losing a distinction that matters is worse than a slightly longer rule.
311
+ - KEEP what would still need saying on a task unrelated to where it came from: language requirements, commit and branch conventions, security mandates, review standards, things never to do.
312
+ - DEMOTE domain and project detail that is real knowledge but does not belong in every prompt \u2014 say which ones, and they are kept as facts rather than discarded.
313
+ - DROP anything describing HOW WORK IS SEQUENCED: numbered phases, phase gates, what runs before what, when review or branching happens. The receiving assistant has its own fixed pipeline and cannot adopt another one, so keeping such an item \u2014 even as a fact \u2014 leaves agents holding two workflows. List these under "dropped".
314
+ - Do NOT invent a rule that is not in the list, and do not weaken one into a vague slogan.
315
+
316
+ Answer with a fenced json block:
317
+ {"rules":["<the consolidated rule>", \u2026],"demoted":["<the exact original text>", \u2026],"dropped":["<the exact original text>", \u2026]}
318
+
319
+ Candidates:
320
+ ${items.map((t, i) => `${i + 1}. ${t}`).join("\n")}`;
321
+ async function consolidateRules(opts) {
322
+ const max = opts.max ?? MAX_RULES;
323
+ if (opts.candidates.length <= max) return { rules: opts.candidates, demoted: [], dropped: [] };
324
+ const texts = opts.candidates.map((c) => c.text);
325
+ try {
326
+ const out = await askChain(opts.provider, opts.models, /* @__PURE__ */ new Set(), CONSOLIDATE(texts, max), opts.signal);
327
+ const fence = /```(?:json)?\s*([\s\S]*?)```/.exec(out);
328
+ const parsed = JSON.parse(fence ? fence[1] : out.slice(out.indexOf("{")));
329
+ const rules = Array.isArray(parsed.rules) ? parsed.rules.filter((r) => typeof r === "string") : [];
330
+ if (!rules.length) throw new Error("no rules came back");
331
+ const named = (key) => new Set(
332
+ Array.isArray(parsed[key]) ? parsed[key].filter((d) => typeof d === "string") : []
333
+ );
334
+ const demotedText = named("demoted");
335
+ const droppedText = named("dropped");
336
+ return {
337
+ // The consolidated text is new, so the source is the whole set rather than any one file.
338
+ rules: rules.slice(0, max).map((text) => ({
339
+ text,
340
+ disposition: "rule",
341
+ reason: "consolidated",
342
+ source: "migration"
343
+ })),
344
+ // Anything the model named as demoted is kept as a fact; anything it simply did not mention was
345
+ // merged into a surviving rule and is already represented.
346
+ demoted: opts.candidates.filter((c) => demotedText.has(c.text) && !droppedText.has(c.text)).map((c) => ({ ...c, disposition: "fact", reason: "project detail, kept as knowledge" })),
347
+ dropped: opts.candidates.filter((c) => droppedText.has(c.text)).map((c) => ({ ...c, disposition: "skip", reason: "describes a workflow this pipeline replaces" }))
348
+ };
349
+ } catch {
350
+ return { rules: opts.candidates, demoted: [], dropped: [] };
351
+ }
352
+ }
353
+
354
+ // src/migrate/run.ts
355
+ var YES = /^(yes|import|copy|evet)/i;
356
+ function countKind(store, kind) {
357
+ return store.all().filter((m) => (m.kind ?? "fact") === kind).length;
358
+ }
359
+ var SAMPLE = 6;
360
+ function sample(items) {
361
+ return items.slice(0, SAMPLE).map((c) => `- ${c.text}`).join("\n") + (items.length > SAMPLE ? `
362
+ - _\u2026and ${items.length - SAMPLE} more_` : "");
363
+ }
364
+ async function runMigration(deps) {
365
+ const result = { rules: 0, facts: 0, skills: 0, skipped: 0, declined: [], failedBatches: 0, removed: 0 };
366
+ const findings = await discover({ cwd: deps.cwd, home: deps.home });
367
+ if (!hasAnything(findings)) {
368
+ deps.note("No configuration from another coding tool was found here \u2014 nothing to migrate.");
369
+ return result;
370
+ }
371
+ deps.note(`**Found in this project:**
372
+ ${summarize(findings)}`);
373
+ const prose = findings.filter((f) => (f.kind === "rules" || f.kind === "memory") && f.text);
374
+ if (prose.length) {
375
+ deps.note(`Reading ${prose.length} file(s) to work out which instructions still apply here. _This is the part that costs tokens; nothing is imported until you say so._`);
376
+ deps.busy?.("reading instructions", deps.models[0]);
377
+ const extraction = await extractAll({
378
+ provider: deps.provider,
379
+ models: deps.models,
380
+ findings: prose,
381
+ ...deps.signal ? { signal: deps.signal } : {},
382
+ onProgress: (done, total, source) => deps.progress?.(
383
+ "read",
384
+ `\u{1F4D6} Reading **${done}/${total}** \u2014 _${source}_`
385
+ )
386
+ });
387
+ deps.idle?.();
388
+ result.failedBatches = extraction.failed.length;
389
+ if (extraction.failed.length) {
390
+ const reasons = [...new Set(extraction.failed.map((f) => f.error))].slice(0, 3);
391
+ deps.note(`\u26A0\uFE0F **${extraction.failed.length} batch(es) could not be read** \u2014 ` + reasons.map((r) => `\`${r.slice(0, 160)}\``).join("; ") + `
392
+
393
+ _Nothing from those files was imported. Re-run \`/migrate\` once the cause above is cleared._`);
394
+ }
395
+ const raw = groupForReview(extraction);
396
+ result.skipped = raw.skipped.length;
397
+ let rules = raw.rules;
398
+ const facts = [...raw.facts];
399
+ if (rules.length > MAX_RULES) {
400
+ deps.note(`${rules.length} rule candidates \u2014 consolidating, since every rule is inlined into every prompt forever.`);
401
+ deps.busy?.("consolidating rules", deps.models[0]);
402
+ const c = await consolidateRules({
403
+ provider: deps.provider,
404
+ models: deps.models,
405
+ candidates: rules,
406
+ ...deps.signal ? { signal: deps.signal } : {}
407
+ });
408
+ deps.idle?.();
409
+ if (c.rules.length < rules.length) {
410
+ deps.note(`Consolidated **${rules.length} \u2192 ${c.rules.length}** rules${c.demoted.length ? `, kept ${c.demoted.length} as fact(s)` : ""}${c.dropped.length ? `, dropped ${c.dropped.length} describing a workflow horse-code replaces` : ""}.`);
411
+ rules = c.rules;
412
+ facts.push(...c.demoted);
413
+ result.skipped += c.dropped.length;
414
+ } else {
415
+ deps.note(`\u26A0\uFE0F Consolidation did not run \u2014 you are being asked about all ${rules.length} candidates.`);
416
+ }
417
+ }
418
+ if (rules.length) {
419
+ const answer = await deps.ask(
420
+ `**${rules.length} standing rule(s)**. A rule goes into EVERY agent's instructions, for every task, permanently.
421
+
422
+ ${sample(rules)}
423
+
424
+ Import them?`,
425
+ { options: [
426
+ { label: "Yes \u2014 import all", description: `${rules.length} rules become permanent memory` },
427
+ { label: "No", description: "Nothing is written; you can re-run /migrate later" }
428
+ ] }
429
+ );
430
+ if (YES.test(answer.trim())) {
431
+ const before = countKind(deps.memStore, "rule");
432
+ for (const c of rules) await deps.memStore.add(c.text, "rule");
433
+ result.rules = countKind(deps.memStore, "rule") - before;
434
+ } else result.declined.push("rules");
435
+ }
436
+ if (facts.length) {
437
+ const answer = await deps.ask(
438
+ `**${facts.length} project fact(s)** were found \u2014 recalled when relevant rather than always.
439
+
440
+ ${sample(facts)}
441
+
442
+ Import them?`,
443
+ { options: [
444
+ { label: "Yes \u2014 import all", description: `${facts.length} facts become memory` },
445
+ { label: "No", description: "Nothing is written" }
446
+ ] }
447
+ );
448
+ if (YES.test(answer.trim())) {
449
+ const before = countKind(deps.memStore, "fact");
450
+ for (const c of facts) await deps.memStore.add(c.text, "fact");
451
+ result.facts = countKind(deps.memStore, "fact") - before;
452
+ } else result.declined.push("facts");
453
+ }
454
+ if (raw.skipped.length) {
455
+ const skipped = raw.skipped;
456
+ const why = skipped.slice(0, SAMPLE).map((c) => `- ~~${c.text.slice(0, 90)}~~ \u2014 ${c.reason}`).join("\n");
457
+ deps.note(`**Left behind (${skipped.length})** \u2014 these only meant something in the original tool:
458
+ ${why}` + (skipped.length > SAMPLE ? `
459
+ - _\u2026and ${skipped.length - SAMPLE} more_` : "") + `
460
+
461
+ _If one of these should have transferred, add it with \`/memory add\`._`);
462
+ }
463
+ }
464
+ const existing = new Set(deps.existingSkills?.() ?? []);
465
+ const allSkills = findings.filter((f) => f.kind === "skill");
466
+ const skills = allSkills.filter((f) => !existing.has(f.label.split("/").pop()));
467
+ const alreadyHave = allSkills.length - skills.length;
468
+ if (alreadyHave) deps.note(`${alreadyHave} skill(s) are ones horse-code already ships \u2014 left alone.`);
469
+ if (skills.length) {
470
+ const answer = await deps.ask(
471
+ `**${skills.length} skill(s)** were found. The format is the same as ours, so they transfer as they are:
472
+ ${skills.slice(0, SAMPLE).map((s) => `- \`${s.label.split("/").pop()}\``).join("\n")}${skills.length > SAMPLE ? `
473
+ - _\u2026and ${skills.length - SAMPLE} more_` : ""}
474
+
475
+ Copy them into this project?`,
476
+ { options: [
477
+ { label: "Yes \u2014 copy all", description: "Copied to .horsecode/skills/, then routed per task" },
478
+ { label: "No", description: "They stay where they are" }
479
+ ] }
480
+ );
481
+ if (YES.test(answer.trim())) {
482
+ const copied = [];
483
+ for (const [i, s] of skills.entries()) {
484
+ const name = s.label.split("/").pop();
485
+ deps.progress?.("copy", `\u{1F4E6} Copying **${i + 1}/${skills.length}** \u2014 \`${name}\``);
486
+ const dest = join2(deps.cwd, ".horsecode", "skills", name);
487
+ try {
488
+ await mkdir(dirname(dest), { recursive: true });
489
+ await cp(dirname(s.path), dest, { recursive: true, dereference: true });
490
+ copied.push(name);
491
+ result.skills++;
492
+ } catch {
493
+ }
494
+ }
495
+ if (copied.length) {
496
+ const removable = [];
497
+ for (const s of skills) {
498
+ const name = s.label.split("/").pop();
499
+ if (!copied.includes(name)) continue;
500
+ const dir = dirname(s.path);
501
+ const paths = [dir];
502
+ try {
503
+ if ((await lstat(dir)).isSymbolicLink()) paths.push(resolve(dirname(dir), await readlink(dir)));
504
+ } catch {
505
+ }
506
+ removable.push({ name, paths });
507
+ }
508
+ const answer2 = await deps.ask(
509
+ `**${removable.length} skill(s)** are now in \`.horsecode/skills/\` AND still at their old path.
510
+
511
+ Remove the originals? horse-code reads its own directory, so the old copies are twins that will drift \u2014 but OTHER tools in this project read them too.
512
+
513
+ _Removed from: ${[...new Set(removable.flatMap((r) => r.paths.map((p) => dirname(p).replace(deps.cwd, "."))))].join(", ")}_`,
514
+ { options: [
515
+ { label: "No \u2014 keep them", description: "Nothing is deleted; the two copies can diverge" },
516
+ { label: "Yes \u2014 remove the originals", description: `${removable.length} directory(ies) deleted from their old location` }
517
+ ] }
518
+ );
519
+ if (YES.test(answer2.trim())) {
520
+ for (const [i, r] of removable.entries()) {
521
+ deps.progress?.("remove", `\u{1F9F9} Removing **${i + 1}/${removable.length}** \u2014 \`${r.name}\``);
522
+ for (const path of r.paths) {
523
+ try {
524
+ await rm(path, { recursive: true, force: true });
525
+ result.removed++;
526
+ } catch {
527
+ }
528
+ }
529
+ }
530
+ deps.note(`\u{1F9F9} Removed ${result.removed} original skill location(s). _Both skill trees are usually tracked by git \u2014 \`git checkout\` brings them back if this was wrong._`);
531
+ } else result.declined.push("removing the originals");
532
+ }
533
+ if (copied.length && deps.assignSkills) {
534
+ deps.note(`Working out which roles should carry ${copied.length} newly installed skill(s)\u2026`);
535
+ deps.busy?.("assigning skills", deps.models[0]);
536
+ try {
537
+ const assigned = await deps.assignSkills(copied);
538
+ deps.idle?.();
539
+ deps.note(assigned || "No role needed one of them permanently \u2014 every agent can still fetch them on demand.");
540
+ } catch (e) {
541
+ deps.idle?.();
542
+ deps.note(`Skill assignment did not run (${e instanceof Error ? e.message : String(e)}) \u2014 run \`/roles adjust\` to do it.`);
543
+ }
544
+ }
545
+ } else result.declined.push("skills");
546
+ }
547
+ const mcp = await mcpServers(findings);
548
+ if (mcp.length) {
549
+ deps.note(`**${mcp.length} MCP server(s)** are configured for the other tool: ${mcp.map((m) => `\`${m}\``).join(", ")}.
550
+
551
+ _These are not imported automatically \u2014 add the ones you want under \`mcp\` in your config, and mark a read-only server \`"readOnly": true\` so every agent can use it._`);
552
+ }
553
+ const migratedRules = findings.filter((f) => f.kind === "rules" && !f.own && !f.label.startsWith("~/")).map((f) => f.label);
554
+ if (migratedRules.length) {
555
+ const already = (await loadMigrated(deps.cwd))?.files ?? [];
556
+ const unrecorded = migratedRules.filter((f) => !already.includes(f));
557
+ const stop = result.rules > 0 ? unrecorded.length > 0 : unrecorded.length > 0 && countKind(deps.memStore, "rule") > 0 && /^y/i.test(await deps.ask(
558
+ `${unrecorded.map((f) => `\`${f}\``).join(", ")} ${unrecorded.length === 1 ? "was" : "were"} left in place, and this project already has rules in memory \u2014 so ${unrecorded.length === 1 ? "it was" : "they were"} most likely migrated already. Stop reading ${unrecorded.length === 1 ? "it" : "them"} as this project's rules?`,
559
+ { options: [
560
+ { label: "Yes \u2014 the rules are in memory", description: "An agent that opens the file is told where the rules went" },
561
+ { label: "No \u2014 keep reading them", description: "The file stays a source of rules alongside memory" }
562
+ ] }
563
+ ));
564
+ if (stop) {
565
+ await recordMigrated(deps.cwd, unrecorded).catch(() => {
566
+ });
567
+ deps.note(`\u{1F512} ${unrecorded.map((f) => `\`${f}\``).join(", ")} ${unrecorded.length === 1 ? "is" : "are"} no longer read as rules \u2014 their content is in this project's memory, and an agent that opens one is told where the rules went.`);
568
+ }
569
+ }
570
+ return result;
571
+ }
572
+ async function mcpServers(findings) {
573
+ const names = /* @__PURE__ */ new Set();
574
+ for (const f of findings.filter((x) => x.kind === "mcp")) {
575
+ try {
576
+ const raw = f.text ?? await readFile2(f.path, "utf8");
577
+ const parsed = JSON.parse(raw);
578
+ for (const key of Object.keys(parsed.mcpServers ?? parsed.servers ?? {})) names.add(key);
579
+ } catch {
580
+ }
581
+ }
582
+ return [...names];
583
+ }
584
+ function describeResult(r) {
585
+ const bits = [];
586
+ if (r.rules) bits.push(`**${r.rules} rule(s)** \u2014 now in every agent's instructions`);
587
+ if (r.facts) bits.push(`**${r.facts} fact(s)** \u2014 recalled when relevant`);
588
+ if (r.skills) {
589
+ bits.push(`**${r.skills} skill(s)** \u2014 copied to \`.horsecode/skills/\`` + (r.removed ? `, and ${r.removed} old location(s) removed` : ""));
590
+ }
591
+ const failed = r.failedBatches ? `
592
+
593
+ \u26A0\uFE0F **${r.failedBatches} batch(es) failed to read** \u2014 the instructions and remembered facts in them were NOT imported. Re-run \`/migrate\` once that is cleared.` : "";
594
+ if (!bits.length) {
595
+ return `Nothing was imported.${r.declined.length ? ` You declined: ${r.declined.join(", ")}.` : ""}${failed}`;
596
+ }
597
+ const declined = r.declined.length ? `
598
+
599
+ _Declined: ${r.declined.join(", ")} \u2014 re-run \`/migrate\` to revisit._` : "";
600
+ const left = r.skipped ? `
601
+
602
+ ${r.skipped} instruction(s) were left behind as tool-specific.` : "";
603
+ const head = r.failedBatches ? "**Migration finished with failures.**" : "**Migration complete.**";
604
+ return `${head}
605
+ ${bits.map((b) => `- ${b}`).join("\n")}${left}${declined}${failed}`;
606
+ }
607
+ export {
608
+ describeResult,
609
+ runMigration
610
+ };
@@ -0,0 +1,13 @@
1
+ import {
2
+ removeSkillSource,
3
+ saveMcpServer,
4
+ saveRoleSkills,
5
+ saveSkillSource
6
+ } from "./chunk-DKVIN43T.js";
7
+ import "./chunk-H2FDGPVW.js";
8
+ export {
9
+ removeSkillSource,
10
+ saveMcpServer,
11
+ saveRoleSkills,
12
+ saveSkillSource
13
+ };
@@ -0,0 +1,29 @@
1
+ // src/session/source-cache.ts
2
+ import { readFileSync, writeFileSync, mkdirSync } from "fs";
3
+ import { join, dirname } from "path";
4
+ import { createHash } from "crypto";
5
+ var cachePath = (home) => join(home, ".horsecode", "sources.json");
6
+ var keyFor = (baseUrl) => createHash("sha256").update(baseUrl).digest("hex").slice(0, 12);
7
+ function loadSourceCache(home, baseUrl) {
8
+ try {
9
+ const data = JSON.parse(readFileSync(cachePath(home), "utf8"));
10
+ return data[keyFor(baseUrl)]?.sources;
11
+ } catch {
12
+ return void 0;
13
+ }
14
+ }
15
+ function saveSourceCache(home, baseUrl, sources, now = Date.now()) {
16
+ const path = cachePath(home);
17
+ let data = {};
18
+ try {
19
+ data = JSON.parse(readFileSync(path, "utf8"));
20
+ } catch {
21
+ }
22
+ data[keyFor(baseUrl)] = { sources, at: now };
23
+ mkdirSync(dirname(path), { recursive: true });
24
+ writeFileSync(path, JSON.stringify(data), "utf8");
25
+ }
26
+ export {
27
+ loadSourceCache,
28
+ saveSourceCache
29
+ };