@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.
- package/LICENSE +21 -0
- package/README.md +150 -0
- package/dist/app-SB2L34JW.js +6217 -0
- package/dist/chunk-2DGO2BUB.js +4490 -0
- package/dist/chunk-2SVAHH5N.js +60 -0
- package/dist/chunk-3XVZXTB6.js +4469 -0
- package/dist/chunk-5UWA2UBM.js +69 -0
- package/dist/chunk-7TBYMFMG.js +147 -0
- package/dist/chunk-B67BK5GQ.js +34 -0
- package/dist/chunk-BY4DP7IE.js +20 -0
- package/dist/chunk-DKVIN43T.js +54 -0
- package/dist/chunk-DTWKSZXY.js +162 -0
- package/dist/chunk-F2IALVBU.js +212 -0
- package/dist/chunk-FFYBY2NA.js +392 -0
- package/dist/chunk-FGVJFMK5.js +123 -0
- package/dist/chunk-H2FDGPVW.js +42 -0
- package/dist/chunk-HBSC2HT2.js +85 -0
- package/dist/chunk-IW2KBAVZ.js +21 -0
- package/dist/chunk-JWAEW7AJ.js +121 -0
- package/dist/chunk-NNTIACT4.js +163 -0
- package/dist/chunk-O74BDQKS.js +28 -0
- package/dist/chunk-PGOYDOI4.js +426 -0
- package/dist/chunk-QF4MP6BS.js +69 -0
- package/dist/chunk-SSDLHWSF.js +35 -0
- package/dist/chunk-TOPZL5SU.js +1052 -0
- package/dist/chunk-YBWTCXUS.js +153 -0
- package/dist/chunk-YILDXPSI.js +1363 -0
- package/dist/clean-YOQATBMZ.js +18 -0
- package/dist/cli.js +1495 -0
- package/dist/discover-5URG7C4J.js +52 -0
- package/dist/fix-HBBOTUWM.js +34 -0
- package/dist/frontmatter-UNIPNLLO.js +6 -0
- package/dist/git-VTSZALSR.js +6 -0
- package/dist/install-O34KMWJB.js +113 -0
- package/dist/main-branch-KGWUINYQ.js +19 -0
- package/dist/ongoing-OV5XROTU.js +70 -0
- package/dist/project-graph-IOPCSZUA.js +56 -0
- package/dist/run-LQOZ5I7Z.js +610 -0
- package/dist/save-skills-OHYGVTQ4.js +13 -0
- package/dist/source-cache-XEK5WN7I.js +29 -0
- package/dist/trace-ZMB7LT7W.js +66 -0
- package/dist/trace-adopt-C6TUWFJL.js +79 -0
- package/dist/trace-run-F23MFTY4.js +24 -0
- package/dist/triage-2J3T5PVQ.js +30 -0
- package/dist/verify-WQ3GHION.js +479 -0
- package/dist/worktree-F7TWLWLN.js +87 -0
- package/package.json +64 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import {
|
|
2
|
+
writeAtomicSync
|
|
3
|
+
} from "./chunk-B67BK5GQ.js";
|
|
4
|
+
|
|
5
|
+
// src/engine/checkpoint.ts
|
|
6
|
+
import { existsSync, readFileSync, rmSync, statSync } from "fs";
|
|
7
|
+
import { join } from "path";
|
|
8
|
+
function checkpointPath(worktreeRoot) {
|
|
9
|
+
return join(worktreeRoot, "checkpoint.json");
|
|
10
|
+
}
|
|
11
|
+
function checkpointKey(prompt) {
|
|
12
|
+
return prompt.trim().replace(/\s+/g, " ").toLowerCase();
|
|
13
|
+
}
|
|
14
|
+
var CONTINUE_FILLER = /* @__PURE__ */ new Set([
|
|
15
|
+
// Turkish: devam + its inflections, "where we left off", and the usual padding.
|
|
16
|
+
"devam",
|
|
17
|
+
"et",
|
|
18
|
+
"et.",
|
|
19
|
+
"edelim",
|
|
20
|
+
"edin",
|
|
21
|
+
"ediyoruz",
|
|
22
|
+
"edece\u011Fiz",
|
|
23
|
+
"edecegiz",
|
|
24
|
+
"edeceksin",
|
|
25
|
+
"ettik",
|
|
26
|
+
"edeyim",
|
|
27
|
+
"kald\u0131\u011F\u0131m\u0131z",
|
|
28
|
+
"kaldigimiz",
|
|
29
|
+
"kald\u0131\u011F\u0131n",
|
|
30
|
+
"kaldigin",
|
|
31
|
+
"kald\u0131\u011F\u0131",
|
|
32
|
+
"kaldigi",
|
|
33
|
+
"kald\u0131k",
|
|
34
|
+
"kaldik",
|
|
35
|
+
"yerden",
|
|
36
|
+
"hadi",
|
|
37
|
+
"l\xFCtfen",
|
|
38
|
+
"lutfen",
|
|
39
|
+
"tamam",
|
|
40
|
+
"bakal\u0131m",
|
|
41
|
+
"bakalim",
|
|
42
|
+
"art\u0131k",
|
|
43
|
+
"artik",
|
|
44
|
+
"mi",
|
|
45
|
+
"m\u0131",
|
|
46
|
+
"mu",
|
|
47
|
+
"m\xFC",
|
|
48
|
+
// English.
|
|
49
|
+
"continue",
|
|
50
|
+
"resume",
|
|
51
|
+
"carry",
|
|
52
|
+
"on",
|
|
53
|
+
"keep",
|
|
54
|
+
"going",
|
|
55
|
+
"pick",
|
|
56
|
+
"up",
|
|
57
|
+
"where",
|
|
58
|
+
"from",
|
|
59
|
+
"we",
|
|
60
|
+
"left",
|
|
61
|
+
"off",
|
|
62
|
+
"let's",
|
|
63
|
+
"lets",
|
|
64
|
+
"us",
|
|
65
|
+
"please",
|
|
66
|
+
"ok",
|
|
67
|
+
"okay",
|
|
68
|
+
"just",
|
|
69
|
+
"the",
|
|
70
|
+
"it",
|
|
71
|
+
"you",
|
|
72
|
+
"i",
|
|
73
|
+
"and",
|
|
74
|
+
"then",
|
|
75
|
+
"now"
|
|
76
|
+
]);
|
|
77
|
+
var CONTINUE_VERB = /(^|\s)(devam|kald[ıi]\w*|continue|resume|carry on|keep going|pick up (where|from))(\s|$|[.,!?])/i;
|
|
78
|
+
function isContinuePrompt(text) {
|
|
79
|
+
const t = text.trim();
|
|
80
|
+
if (!t || t.length > 200) return false;
|
|
81
|
+
if (!CONTINUE_VERB.test(t)) return false;
|
|
82
|
+
const rest = t.toLowerCase().split(/[\s,.!?;:]+/).filter(Boolean).filter((w) => !CONTINUE_FILLER.has(w));
|
|
83
|
+
return rest.length <= 1;
|
|
84
|
+
}
|
|
85
|
+
function checkpointMtime(worktreeRoot) {
|
|
86
|
+
try {
|
|
87
|
+
return statSync(checkpointPath(worktreeRoot)).mtimeMs;
|
|
88
|
+
} catch {
|
|
89
|
+
return 0;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function readCheckpoint(worktreeRoot) {
|
|
93
|
+
const p = checkpointPath(worktreeRoot);
|
|
94
|
+
if (!existsSync(p)) return null;
|
|
95
|
+
try {
|
|
96
|
+
const cp = JSON.parse(readFileSync(p, "utf8"));
|
|
97
|
+
if (!Array.isArray(cp.done)) return null;
|
|
98
|
+
return cp;
|
|
99
|
+
} catch {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function writeCheckpoint(worktreeRoot, cp) {
|
|
104
|
+
try {
|
|
105
|
+
writeAtomicSync(checkpointPath(worktreeRoot), JSON.stringify(cp, null, 2));
|
|
106
|
+
} catch {
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function clearCheckpoint(worktreeRoot) {
|
|
110
|
+
try {
|
|
111
|
+
rmSync(checkpointPath(worktreeRoot), { force: true });
|
|
112
|
+
} catch {
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export {
|
|
117
|
+
checkpointKey,
|
|
118
|
+
isContinuePrompt,
|
|
119
|
+
checkpointMtime,
|
|
120
|
+
readCheckpoint,
|
|
121
|
+
writeCheckpoint,
|
|
122
|
+
clearCheckpoint
|
|
123
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// src/config/patch.ts
|
|
2
|
+
import { mkdir, readFile, writeFile, rename } from "fs/promises";
|
|
3
|
+
import { dirname, join } from "path";
|
|
4
|
+
async function patchConfig(home, mutate) {
|
|
5
|
+
const path = join(home, ".horsecode", "config.json");
|
|
6
|
+
let current = {};
|
|
7
|
+
try {
|
|
8
|
+
const raw = await readFile(path, "utf8");
|
|
9
|
+
if (raw.trim()) {
|
|
10
|
+
const parsed = JSON.parse(raw);
|
|
11
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return false;
|
|
12
|
+
current = parsed;
|
|
13
|
+
}
|
|
14
|
+
} catch (e) {
|
|
15
|
+
if (e.code !== "ENOENT") return false;
|
|
16
|
+
}
|
|
17
|
+
const next = mutate(current);
|
|
18
|
+
if (!next) return false;
|
|
19
|
+
try {
|
|
20
|
+
await mkdir(dirname(path), { recursive: true });
|
|
21
|
+
const tmp = `${path}.tmp`;
|
|
22
|
+
await writeFile(tmp, `${JSON.stringify(next, null, 2)}
|
|
23
|
+
`, "utf8");
|
|
24
|
+
await rename(tmp, path);
|
|
25
|
+
return true;
|
|
26
|
+
} catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function objectField(current, key) {
|
|
31
|
+
const v = current[key];
|
|
32
|
+
return typeof v === "object" && v !== null && !Array.isArray(v) ? { ...v } : {};
|
|
33
|
+
}
|
|
34
|
+
function arrayField(current, key) {
|
|
35
|
+
return Array.isArray(current[key]) ? [...current[key]] : [];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export {
|
|
39
|
+
patchConfig,
|
|
40
|
+
objectField,
|
|
41
|
+
arrayField
|
|
42
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ToolRegistry,
|
|
3
|
+
runStructuredRole
|
|
4
|
+
} from "./chunk-YILDXPSI.js";
|
|
5
|
+
|
|
6
|
+
// src/engine/user-language.ts
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
var Translated = z.object({
|
|
9
|
+
text: z.string().describe("The same text, in the requested language. No commentary, no quotes.")
|
|
10
|
+
});
|
|
11
|
+
var PROMPT = "You translate one short piece of interface text for a terminal tool. Return the SAME text in the language you are given: same meaning, same tone, same length, nothing added and nothing explained. Leave branch names, file paths, commands and other identifiers exactly as they are \u2014 they are not words, they are names. Return the result via submit.";
|
|
12
|
+
function isEnglish(language) {
|
|
13
|
+
return !language?.trim() || /^english$/i.test(language.trim());
|
|
14
|
+
}
|
|
15
|
+
async function inUserLanguage(deps, text, language) {
|
|
16
|
+
if (isEnglish(language)) return text;
|
|
17
|
+
try {
|
|
18
|
+
const { role: agentRole, model, fallbacks, onExhausted, onFallback } = deps.roleRegistry.fallbackOpts("refiner");
|
|
19
|
+
const said = await runStructuredRole({
|
|
20
|
+
provider: deps.provider,
|
|
21
|
+
role: agentRole,
|
|
22
|
+
model,
|
|
23
|
+
fallbacks,
|
|
24
|
+
onExhausted,
|
|
25
|
+
onFallback,
|
|
26
|
+
// The project's own rules shape what the user reads — the same reason normalizeQuestion carries them.
|
|
27
|
+
systemPrompt: PROMPT + deps.roleRegistry.ruleSuffix(),
|
|
28
|
+
tools: new ToolRegistry(),
|
|
29
|
+
messages: [{ role: "user", content: `Language: ${language}
|
|
30
|
+
|
|
31
|
+
${text}` }],
|
|
32
|
+
permission: deps.permission,
|
|
33
|
+
approve: deps.approve,
|
|
34
|
+
cwd: ".",
|
|
35
|
+
signal: deps.signal
|
|
36
|
+
}, Translated);
|
|
37
|
+
return said.text.trim() || text;
|
|
38
|
+
} catch {
|
|
39
|
+
return text;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
var Asked = z.object({
|
|
43
|
+
question: z.string().describe("The question, in the requested language."),
|
|
44
|
+
options: z.array(z.object({
|
|
45
|
+
label: z.string().describe("The choice's label, in the requested language. Keep it short \u2014 it is a name."),
|
|
46
|
+
description: z.string().optional().describe("Its one-line explanation, in the requested language.")
|
|
47
|
+
})).describe("Same choices, same order. Never add, drop or reorder them.")
|
|
48
|
+
});
|
|
49
|
+
async function askInUserLanguage(deps, askUser, language, question, options) {
|
|
50
|
+
if (isEnglish(language) || !options?.length) {
|
|
51
|
+
const q = isEnglish(language) ? question : await inUserLanguage(deps, question, language);
|
|
52
|
+
return askUser(q, options ? { options } : void 0);
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
const { role: agentRole, model, fallbacks, onExhausted, onFallback } = deps.roleRegistry.fallbackOpts("refiner");
|
|
56
|
+
const said = await runStructuredRole({
|
|
57
|
+
provider: deps.provider,
|
|
58
|
+
role: agentRole,
|
|
59
|
+
model,
|
|
60
|
+
fallbacks,
|
|
61
|
+
onExhausted,
|
|
62
|
+
onFallback,
|
|
63
|
+
systemPrompt: PROMPT + deps.roleRegistry.ruleSuffix(),
|
|
64
|
+
tools: new ToolRegistry(),
|
|
65
|
+
messages: [{ role: "user", content: `Language: ${language}
|
|
66
|
+
|
|
67
|
+
${JSON.stringify({ question, options })}` }],
|
|
68
|
+
permission: deps.permission,
|
|
69
|
+
approve: deps.approve,
|
|
70
|
+
cwd: ".",
|
|
71
|
+
signal: deps.signal
|
|
72
|
+
}, Asked);
|
|
73
|
+
if (said.options.length !== options.length) return askUser(question, { options });
|
|
74
|
+
const answer = (await askUser(said.question, { options: said.options })).trim();
|
|
75
|
+
const at = said.options.findIndex((o) => o.label.trim() === answer);
|
|
76
|
+
return at >= 0 ? options[at].label : answer;
|
|
77
|
+
} catch {
|
|
78
|
+
return askUser(question, { options });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export {
|
|
83
|
+
inUserLanguage,
|
|
84
|
+
askInUserLanguage
|
|
85
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// src/worktree/git.ts
|
|
2
|
+
import { spawn } from "child_process";
|
|
3
|
+
var defaultGitRunner = (args, cwd) => new Promise((resolve) => {
|
|
4
|
+
let stdout = "";
|
|
5
|
+
let stderr = "";
|
|
6
|
+
let child;
|
|
7
|
+
try {
|
|
8
|
+
child = spawn("git", args, { cwd });
|
|
9
|
+
} catch (e) {
|
|
10
|
+
resolve({ stdout, stderr: e instanceof Error ? e.message : String(e), code: -1 });
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
child.stdout?.on("data", (d) => stdout += d.toString());
|
|
14
|
+
child.stderr?.on("data", (d) => stderr += d.toString());
|
|
15
|
+
child.on("error", (e) => resolve({ stdout, stderr: stderr + e.message, code: -1 }));
|
|
16
|
+
child.on("close", (code) => resolve({ stdout, stderr, code: code ?? -1 }));
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
export {
|
|
20
|
+
defaultGitRunner
|
|
21
|
+
};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Board,
|
|
3
|
+
refreshAfterChange,
|
|
4
|
+
runTaskCycle
|
|
5
|
+
} from "./chunk-3XVZXTB6.js";
|
|
6
|
+
import {
|
|
7
|
+
defaultGitRunner
|
|
8
|
+
} from "./chunk-IW2KBAVZ.js";
|
|
9
|
+
|
|
10
|
+
// src/engine/fix.ts
|
|
11
|
+
function cardFromFinding(f, id) {
|
|
12
|
+
return {
|
|
13
|
+
id,
|
|
14
|
+
title: f.title,
|
|
15
|
+
/**
|
|
16
|
+
* Its own acceptance criteria, with a floor.
|
|
17
|
+
*
|
|
18
|
+
* The gate can only check what it was given, so a finding reported without criteria would pass by having
|
|
19
|
+
* been attempted. The detail is what the person actually saw, which is the one statement that is always
|
|
20
|
+
* true of a real fix: that is no longer what happens.
|
|
21
|
+
*/
|
|
22
|
+
acceptance: f.acceptance.length ? f.acceptance : [`No longer happens: ${f.detail}`],
|
|
23
|
+
files: f.files
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
var FIX_ATTEMPTS = 2;
|
|
27
|
+
function whyItDidNotTake(notes) {
|
|
28
|
+
return `An earlier attempt at this in the same session did not settle it. The acceptance check said: ${notes.join("; ") || "it was still not true afterwards"}. That is a description of what is still wrong \u2014 find why the change did not take rather than making the same one again.`;
|
|
29
|
+
}
|
|
30
|
+
async function runFix(deps, workdir, f, id = "fix-1") {
|
|
31
|
+
let last = { title: f.title, fixed: false, notes: [] };
|
|
32
|
+
for (let attempt = 1; attempt <= FIX_ATTEMPTS; attempt++) {
|
|
33
|
+
const cardId = attempt === 1 ? id : `${id}-retry${attempt - 1}`;
|
|
34
|
+
try {
|
|
35
|
+
const board = new Board();
|
|
36
|
+
board.addCard(cardFromFinding(f, cardId));
|
|
37
|
+
if (attempt > 1) board.addReviewNote(cardId, whyItDidNotTake(last.notes));
|
|
38
|
+
const verdict = await runTaskCycle(deps, board, cardId, workdir);
|
|
39
|
+
last = {
|
|
40
|
+
title: f.title,
|
|
41
|
+
fixed: verdict.verdict === "pass",
|
|
42
|
+
notes: verdict.verdict === "pass" ? [] : verdict.noProgress ? ["nothing was written"] : verdict.notes
|
|
43
|
+
};
|
|
44
|
+
} catch (e) {
|
|
45
|
+
last = { title: f.title, fixed: false, notes: [e instanceof Error ? e.message : String(e)] };
|
|
46
|
+
}
|
|
47
|
+
if (last.fixed) return last;
|
|
48
|
+
}
|
|
49
|
+
return last;
|
|
50
|
+
}
|
|
51
|
+
async function dirtyPaths(git, cwd) {
|
|
52
|
+
const r = await git(["status", "--porcelain"], cwd);
|
|
53
|
+
if (r.code !== 0) return /* @__PURE__ */ new Set();
|
|
54
|
+
return new Set(r.stdout.split("\n").map((l) => l.slice(3).trim()).filter(Boolean));
|
|
55
|
+
}
|
|
56
|
+
async function commitOnly(git, cwd, before, message) {
|
|
57
|
+
const after = await dirtyPaths(git, cwd);
|
|
58
|
+
const mine = [...after].filter((p) => !before.has(p));
|
|
59
|
+
if (!mine.length) return [];
|
|
60
|
+
const add = await git(["add", "--", ...mine], cwd);
|
|
61
|
+
if (add.code !== 0) return [];
|
|
62
|
+
const staged = await git(["diff", "--cached", "--quiet", "--", ...mine], cwd);
|
|
63
|
+
if (staged.code === 0) return [];
|
|
64
|
+
const r = await git(["commit", "-m", message, "--", ...mine], cwd);
|
|
65
|
+
return r.code === 0 ? mine : [];
|
|
66
|
+
}
|
|
67
|
+
async function commitFix(workdir, f, before) {
|
|
68
|
+
return commitOnly(defaultGitRunner, workdir, before, `fix: ${f.title}`);
|
|
69
|
+
}
|
|
70
|
+
function describeFix(r) {
|
|
71
|
+
return r.fixed ? `\u{1F527} Fixed: **${r.title}** \u2014 implemented, reviewed, and checked against what the finding asked for.` : `\u26A0\uFE0F Not fixed: **${r.title}**${r.notes.length ? ` \u2014 ${r.notes.slice(0, 3).join("; ")}` : ""}. It stays in the report as an open finding.`;
|
|
72
|
+
}
|
|
73
|
+
async function runSmallChange(deps, workdir, title, prompt, spec) {
|
|
74
|
+
const before = await dirtyPaths(defaultGitRunner, workdir);
|
|
75
|
+
let res;
|
|
76
|
+
try {
|
|
77
|
+
const board = new Board();
|
|
78
|
+
const acceptance = spec.acceptance.length ? spec.acceptance : [`The request is satisfied: ${prompt}`];
|
|
79
|
+
board.addCard({ id: "small-1", title: prompt, acceptance, files: spec.files });
|
|
80
|
+
const verdict = await runTaskCycle(deps, board, "small-1", workdir);
|
|
81
|
+
res = {
|
|
82
|
+
title,
|
|
83
|
+
fixed: verdict.verdict === "pass",
|
|
84
|
+
notes: verdict.verdict === "pass" ? [] : verdict.noProgress ? ["nothing was written"] : verdict.notes
|
|
85
|
+
};
|
|
86
|
+
} catch (e) {
|
|
87
|
+
res = { title, fixed: false, notes: [e instanceof Error ? e.message : String(e)] };
|
|
88
|
+
}
|
|
89
|
+
const files = res.fixed ? await commitOnly(defaultGitRunner, workdir, before, prompt) : [];
|
|
90
|
+
await refreshAfterChange({
|
|
91
|
+
cwd: workdir,
|
|
92
|
+
files,
|
|
93
|
+
provider: deps.provider,
|
|
94
|
+
models: deps.roleRegistry.chainFor("tracer", 0),
|
|
95
|
+
signal: deps.signal,
|
|
96
|
+
...deps.note ? { note: deps.note } : {}
|
|
97
|
+
});
|
|
98
|
+
return { ...res, committed: files.length > 0 };
|
|
99
|
+
}
|
|
100
|
+
function describeSmallChange(r, reason, branch) {
|
|
101
|
+
if (!r.fixed) {
|
|
102
|
+
return `\u26A0\uFE0F Not done: ${r.notes.join("; ") || "the change did not pass review"}.
|
|
103
|
+
|
|
104
|
+
Nothing was committed. Ask again with more detail, or say so and I will treat it as a full piece of work.`;
|
|
105
|
+
}
|
|
106
|
+
return `\u2705 Done \u2014 implemented, reviewed, and checked against what was asked.
|
|
107
|
+
|
|
108
|
+
_Handled as a small change (${reason}): no branch, no spec, no plan._ ` + (r.committed ? `Committed on \`${branch}\`.` : `Nothing was left to commit \u2014 the working tree already had it.`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export {
|
|
112
|
+
cardFromFinding,
|
|
113
|
+
FIX_ATTEMPTS,
|
|
114
|
+
runFix,
|
|
115
|
+
dirtyPaths,
|
|
116
|
+
commitOnly,
|
|
117
|
+
commitFix,
|
|
118
|
+
describeFix,
|
|
119
|
+
runSmallChange,
|
|
120
|
+
describeSmallChange
|
|
121
|
+
};
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import {
|
|
2
|
+
defaultGitRunner
|
|
3
|
+
} from "./chunk-IW2KBAVZ.js";
|
|
4
|
+
import {
|
|
5
|
+
briefForPrompt,
|
|
6
|
+
briefPrompt,
|
|
7
|
+
briefStatus,
|
|
8
|
+
gatherBriefInput,
|
|
9
|
+
saveBrief
|
|
10
|
+
} from "./chunk-DTWKSZXY.js";
|
|
11
|
+
import {
|
|
12
|
+
ensureGitignore,
|
|
13
|
+
loadTraceIndex,
|
|
14
|
+
planTraces,
|
|
15
|
+
pruneTraces,
|
|
16
|
+
saveTrace,
|
|
17
|
+
saveTraceIndex,
|
|
18
|
+
traceCoverage,
|
|
19
|
+
tracePrompt,
|
|
20
|
+
traceRootRel,
|
|
21
|
+
traceable
|
|
22
|
+
} from "./chunk-FFYBY2NA.js";
|
|
23
|
+
import {
|
|
24
|
+
loadGraph
|
|
25
|
+
} from "./chunk-PGOYDOI4.js";
|
|
26
|
+
|
|
27
|
+
// src/engine/trace-run.ts
|
|
28
|
+
async function traceableFiles(cwd, opts) {
|
|
29
|
+
const r = await defaultGitRunner(["ls-files", "--cached", "--others", "--exclude-standard"], cwd);
|
|
30
|
+
return traceable(r.stdout.split("\n").filter(Boolean), opts);
|
|
31
|
+
}
|
|
32
|
+
async function coverageFor(cwd) {
|
|
33
|
+
return traceCoverage(cwd, await traceableFiles(cwd), await loadTraceIndex(cwd));
|
|
34
|
+
}
|
|
35
|
+
var TRACE_CONCURRENCY = 6;
|
|
36
|
+
var INDEX_CHECKPOINT = 25;
|
|
37
|
+
function describePlan(plan, model) {
|
|
38
|
+
if (!plan.jobs.length) {
|
|
39
|
+
return plan.upToDate ? `All ${plan.upToDate} traces are current \u2014 nothing to write, nothing to spend.` : "No files to trace.";
|
|
40
|
+
}
|
|
41
|
+
const kIn = Math.round(plan.estimatedInputTokens / 1e3);
|
|
42
|
+
const kOut = Math.round(plan.estimatedOutputTokens / 1e3);
|
|
43
|
+
const skipped = plan.skipped.length ? `
|
|
44
|
+
|
|
45
|
+
${plan.skipped.length} file(s) skipped as too large to trace economically (e.g. ${plan.skipped.slice(0, 3).map((s) => `\`${s.file}\``).join(", ")}${plan.skipped.length > 3 ? ", \u2026" : ""}).` : "";
|
|
46
|
+
const cached = plan.upToDate ? `
|
|
47
|
+
${plan.upToDate} file(s) already have a current trace and will be left alone.` : "";
|
|
48
|
+
return `**Tracing ${plan.jobs.length} file(s)** with \`${model}\`.
|
|
49
|
+
|
|
50
|
+
This is the part of understanding your project that costs tokens \u2014 the graph was free, this is not. Each file is read once and described in ~150 words.
|
|
51
|
+
|
|
52
|
+
Rough cost: **~${kIn}k input + ~${kOut}k output tokens**.${cached}${skipped}`;
|
|
53
|
+
}
|
|
54
|
+
async function traceOne(provider, model, job, signal, brief) {
|
|
55
|
+
const req = {
|
|
56
|
+
model,
|
|
57
|
+
messages: [
|
|
58
|
+
{ role: "system", content: "You write terse, factual reference notes about source files. You never speculate." },
|
|
59
|
+
{ role: "user", content: tracePrompt(job, brief) }
|
|
60
|
+
],
|
|
61
|
+
tools: []
|
|
62
|
+
};
|
|
63
|
+
let out = "";
|
|
64
|
+
for await (const ev of provider.chat(req, signal)) {
|
|
65
|
+
if (ev.type === "text-delta") out += ev.text;
|
|
66
|
+
else if (ev.type === "error") throw new Error(ev.message);
|
|
67
|
+
}
|
|
68
|
+
const body = out.replace(/<\/?think>/gi, "").trim();
|
|
69
|
+
if (!body) throw new Error("empty response");
|
|
70
|
+
return body;
|
|
71
|
+
}
|
|
72
|
+
async function runTraces(opts) {
|
|
73
|
+
const { cwd, plan } = opts;
|
|
74
|
+
const signal = opts.signal ?? new AbortController().signal;
|
|
75
|
+
const index = await loadTraceIndex(cwd);
|
|
76
|
+
const brief = briefForPrompt(cwd);
|
|
77
|
+
const failed = [];
|
|
78
|
+
let written = 0;
|
|
79
|
+
let done = 0;
|
|
80
|
+
const queue = [...plan.jobs];
|
|
81
|
+
const worker = async () => {
|
|
82
|
+
for (; ; ) {
|
|
83
|
+
const job = queue.shift();
|
|
84
|
+
if (!job || signal.aborted) return;
|
|
85
|
+
let wroteTo;
|
|
86
|
+
let words;
|
|
87
|
+
let error;
|
|
88
|
+
try {
|
|
89
|
+
const body = await traceOne(opts.provider, opts.model, job, signal, brief);
|
|
90
|
+
const rec = await saveTrace(cwd, job, body, opts.model);
|
|
91
|
+
index.traces[job.file] = rec;
|
|
92
|
+
wroteTo = `${traceRootRel()}/${job.file}.md`;
|
|
93
|
+
words = body.split(/\s+/).filter(Boolean).length;
|
|
94
|
+
written++;
|
|
95
|
+
if (written % INDEX_CHECKPOINT === 0) await saveTraceIndex(cwd, index);
|
|
96
|
+
} catch (e) {
|
|
97
|
+
if (signal.aborted) return;
|
|
98
|
+
error = e instanceof Error ? e.message : String(e);
|
|
99
|
+
failed.push({ file: job.file, error });
|
|
100
|
+
}
|
|
101
|
+
opts.onProgress?.({
|
|
102
|
+
done: ++done,
|
|
103
|
+
total: plan.jobs.length,
|
|
104
|
+
file: job.file,
|
|
105
|
+
...wroteTo !== void 0 && { wroteTo },
|
|
106
|
+
...words !== void 0 && { words },
|
|
107
|
+
...error !== void 0 && { error }
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
await Promise.all(Array.from({ length: Math.min(TRACE_CONCURRENCY, queue.length) }, worker));
|
|
112
|
+
const pruned = opts.liveFiles ? await pruneTraces(cwd, opts.liveFiles, index) : [];
|
|
113
|
+
await saveTraceIndex(cwd, index);
|
|
114
|
+
const wroteGitignore = written > 0 && await ensureGitignore(cwd);
|
|
115
|
+
return { written, failed, pruned, upToDate: plan.upToDate, cancelled: signal.aborted, wroteGitignore };
|
|
116
|
+
}
|
|
117
|
+
async function planFor(cwd, files) {
|
|
118
|
+
return planTraces(cwd, files, await loadGraph(cwd), await loadTraceIndex(cwd));
|
|
119
|
+
}
|
|
120
|
+
async function buildBrief(opts) {
|
|
121
|
+
if (!opts.force) {
|
|
122
|
+
const st = await briefStatus(opts.cwd, opts.files);
|
|
123
|
+
if (st.built && !st.stale) {
|
|
124
|
+
return { ok: true, skipped: true, message: `Project brief is current (${st.sources.length} document(s)) \u2014 not rewritten.` };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const input = await gatherBriefInput(opts.cwd, opts.files);
|
|
128
|
+
if (!input) {
|
|
129
|
+
return { ok: false, message: "No documentation found (README, docs/, specs/) \u2014 traces will describe the code without product context." };
|
|
130
|
+
}
|
|
131
|
+
const signal = opts.signal ?? new AbortController().signal;
|
|
132
|
+
const req = {
|
|
133
|
+
model: opts.model,
|
|
134
|
+
messages: [
|
|
135
|
+
{ role: "system", content: "You write factual project briefings from documentation. You never invent facts the documents do not state." },
|
|
136
|
+
{ role: "user", content: briefPrompt(input) }
|
|
137
|
+
],
|
|
138
|
+
tools: []
|
|
139
|
+
};
|
|
140
|
+
let out = "";
|
|
141
|
+
try {
|
|
142
|
+
for await (const ev of opts.provider.chat(req, signal)) {
|
|
143
|
+
if (ev.type === "text-delta") out += ev.text;
|
|
144
|
+
else if (ev.type === "error") throw new Error(ev.message);
|
|
145
|
+
}
|
|
146
|
+
} catch (e) {
|
|
147
|
+
return { ok: false, message: `Project brief failed (${e instanceof Error ? e.message : String(e)}) \u2014 tracing can still run without it.` };
|
|
148
|
+
}
|
|
149
|
+
const body = out.replace(/<\/?think>/gi, "").trim();
|
|
150
|
+
if (!body) return { ok: false, message: "The brief came back empty \u2014 tracing can still run without it." };
|
|
151
|
+
await saveBrief(opts.cwd, body, { hash: input.hash, sources: input.sources.map((s) => s.file), writtenAt: Date.now(), model: opts.model });
|
|
152
|
+
return { ok: true, message: `**Project brief** written from ${input.sources.length} document(s): ${input.sources.slice(0, 6).map((s) => `\`${s.file}\``).join(", ")}` };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export {
|
|
156
|
+
traceableFiles,
|
|
157
|
+
coverageFor,
|
|
158
|
+
TRACE_CONCURRENCY,
|
|
159
|
+
describePlan,
|
|
160
|
+
runTraces,
|
|
161
|
+
planFor,
|
|
162
|
+
buildBrief
|
|
163
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// src/providers/models.ts
|
|
2
|
+
var KNOWN_FREE_PROVIDER = /^(ddgw|duckduckgo|veoaifree)/i;
|
|
3
|
+
function isFreeModel(id, name) {
|
|
4
|
+
const n = name ?? "";
|
|
5
|
+
if (/🆓/.test(n) || /\bfree\b/i.test(n)) return true;
|
|
6
|
+
if (/-free\b/i.test(id)) return true;
|
|
7
|
+
const provider = id.split("/")[0];
|
|
8
|
+
return /free/i.test(provider) || KNOWN_FREE_PROVIDER.test(provider);
|
|
9
|
+
}
|
|
10
|
+
async function listOmniRouteModels(opts) {
|
|
11
|
+
const fetchFn = opts.fetch ?? globalThis.fetch;
|
|
12
|
+
const headers = {};
|
|
13
|
+
if (opts.apiKey) headers.Authorization = `Bearer ${opts.apiKey}`;
|
|
14
|
+
const base = opts.baseUrl.replace(/\/$/, "");
|
|
15
|
+
const res = await fetchFn(`${base}/api/v1/models`, { headers });
|
|
16
|
+
if (!res.ok) throw new Error(`omniroute models ${res.status}`);
|
|
17
|
+
const body = await res.json();
|
|
18
|
+
const allow = opts.sources && opts.sources.length ? new Set(opts.sources) : void 0;
|
|
19
|
+
const kept = (body.data ?? []).filter((m) => typeof m.id === "string").filter((m) => !isFreeModel(m.id, typeof m.name === "string" ? m.name : void 0)).filter((m) => !allow || typeof m.owned_by === "string" && allow.has(m.owned_by));
|
|
20
|
+
const keptIds = new Set(kept.map((m) => m.id));
|
|
21
|
+
const ids = kept.filter((m) => !(typeof m.parent === "string" && keptIds.has(m.parent))).map((m) => m.id);
|
|
22
|
+
return [...new Set(ids)].sort();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export {
|
|
26
|
+
isFreeModel,
|
|
27
|
+
listOmniRouteModels
|
|
28
|
+
};
|