@kolisachint/hoocode-agent 0.5.26 → 0.5.27
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/CHANGELOG.md +49 -0
- package/dist/core/capabilities/lexical.d.ts +4 -0
- package/dist/core/capabilities/lexical.d.ts.map +1 -1
- package/dist/core/capabilities/lexical.js +104 -4
- package/dist/core/capabilities/lexical.js.map +1 -1
- package/dist/core/capabilities/registry.d.ts +3 -1
- package/dist/core/capabilities/registry.d.ts.map +1 -1
- package/dist/core/capabilities/registry.js.map +1 -1
- package/dist/core/self-docs.d.ts +103 -0
- package/dist/core/self-docs.d.ts.map +1 -0
- package/dist/core/self-docs.js +351 -0
- package/dist/core/self-docs.js.map +1 -0
- package/dist/core/system-prompt.d.ts +12 -0
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +11 -1
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/extensions/core/hoo-core.d.ts +1 -0
- package/dist/extensions/core/hoo-core.d.ts.map +1 -1
- package/dist/extensions/core/hoo-core.js +3 -0
- package/dist/extensions/core/hoo-core.js.map +1 -1
- package/dist/extensions/core/mcp-loader.d.ts.map +1 -1
- package/dist/extensions/core/mcp-loader.js +8 -2
- package/dist/extensions/core/mcp-loader.js.map +1 -1
- package/dist/extensions/core/self-knowledge.d.ts +28 -0
- package/dist/extensions/core/self-knowledge.d.ts.map +1 -0
- package/dist/extensions/core/self-knowledge.js +199 -0
- package/dist/extensions/core/self-knowledge.js.map +1 -0
- package/docs/canvas.md +117 -0
- package/docs/compaction.md +4 -4
- package/docs/custom-provider.md +1 -1
- package/docs/development.md +1 -1
- package/docs/docs.json +27 -2
- package/docs/extensions.md +12 -12
- package/docs/index.md +8 -0
- package/docs/keybindings.md +2 -2
- package/docs/mcp.md +97 -0
- package/docs/models.md +1 -1
- package/docs/modes.md +87 -0
- package/docs/packages.md +4 -4
- package/docs/plugins.md +124 -0
- package/docs/prompt-templates.md +1 -1
- package/docs/providers.md +2 -2
- package/docs/quickstart.md +2 -2
- package/docs/rpc.md +5 -5
- package/docs/sdk.md +5 -5
- package/docs/session-format.md +3 -3
- package/docs/sessions.md +1 -1
- package/docs/settings.md +3 -3
- package/docs/shell-aliases.md +1 -1
- package/docs/skills.md +2 -2
- package/docs/terminal-setup.md +1 -1
- package/docs/termux.md +2 -2
- package/docs/themes.md +3 -3
- package/docs/usage.md +93 -4
- package/docs/windows.md +1 -1
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package.json +1 -1
- package/package.json +4 -4
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The agent's index of hoocode's *own* documentation.
|
|
3
|
+
*
|
|
4
|
+
* The startup banner promises "hoocode can explain its own features and look up
|
|
5
|
+
* its docs", and the docs really do ship with the install (`package.json`
|
|
6
|
+
* `files` includes `docs`, and `copy-binary-assets` copies them into `dist/` for
|
|
7
|
+
* the pkg binaries). What was missing is the only part that makes the promise
|
|
8
|
+
* true: telling the model they exist. `getDocsPath()` had exactly one consumer —
|
|
9
|
+
* `auth-guidance.ts`, which prints paths to the *human* — so nothing ever put a
|
|
10
|
+
* docs path into model context.
|
|
11
|
+
*
|
|
12
|
+
* That gap is not one the model can close by itself. Its cwd is the user's
|
|
13
|
+
* project, so `grep`/`find` there discover the user's docs, never hoocode's,
|
|
14
|
+
* which live in an install directory whose path it cannot derive.
|
|
15
|
+
*
|
|
16
|
+
* Descriptions come from `docs/index.md` rather than being duplicated here.
|
|
17
|
+
* That file is a curated, human-maintained table of contents, and a second
|
|
18
|
+
* hand-written list is how an index goes stale the first week nobody updates
|
|
19
|
+
* it. The directory listing stays the source of truth for *what exists*, so a
|
|
20
|
+
* new doc still shows up (described from its own first paragraph) on the day it
|
|
21
|
+
* lands, with or without an index entry.
|
|
22
|
+
*/
|
|
23
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
24
|
+
import { basename, dirname, join } from "node:path";
|
|
25
|
+
import { getChangelogPath, getDocsPath, getReadmePath } from "../config.js";
|
|
26
|
+
/** How much of a doc to read when deriving a fallback description. */
|
|
27
|
+
const HEAD_BYTES = 2048;
|
|
28
|
+
/** Cap on a derived description, so one run-on opening line cannot bloat the prompt. */
|
|
29
|
+
const MAX_DESCRIPTION = 110;
|
|
30
|
+
function truncate(text, max = MAX_DESCRIPTION) {
|
|
31
|
+
const clean = text.replace(/\s+/g, " ").trim();
|
|
32
|
+
if (clean.length <= max)
|
|
33
|
+
return clean;
|
|
34
|
+
return `${clean.slice(0, max - 1).trimEnd()}…`;
|
|
35
|
+
}
|
|
36
|
+
/** Strip inline markdown that adds noise but no meaning in a prompt listing. */
|
|
37
|
+
function stripInlineMarkdown(text) {
|
|
38
|
+
return text
|
|
39
|
+
.replace(/\[([^\]]+)\]\([^)]*\)/g, "$1") // links → their text
|
|
40
|
+
.replace(/[`*_]/g, "")
|
|
41
|
+
.trim();
|
|
42
|
+
}
|
|
43
|
+
function readHead(path) {
|
|
44
|
+
try {
|
|
45
|
+
// Whole-file read: these are small, and slicing bytes off a UTF-8 file can
|
|
46
|
+
// split a multi-byte character. Truncate after decoding instead.
|
|
47
|
+
return readFileSync(path, "utf-8").slice(0, HEAD_BYTES);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return "";
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** First `# ` heading, or undefined. */
|
|
54
|
+
function firstHeading(markdown) {
|
|
55
|
+
for (const line of markdown.split(/\r?\n/)) {
|
|
56
|
+
const match = /^#\s+(.+)$/.exec(line.trim());
|
|
57
|
+
if (match?.[1])
|
|
58
|
+
return stripInlineMarkdown(match[1]);
|
|
59
|
+
}
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* First real prose line: not a heading, blockquote, list item, fence, or table
|
|
64
|
+
* row. Used only for docs the curated index does not describe.
|
|
65
|
+
*/
|
|
66
|
+
function firstParagraph(markdown) {
|
|
67
|
+
let inFence = false;
|
|
68
|
+
for (const raw of markdown.split(/\r?\n/)) {
|
|
69
|
+
const line = raw.trim();
|
|
70
|
+
if (line.startsWith("```")) {
|
|
71
|
+
inFence = !inFence;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (inFence || line === "")
|
|
75
|
+
continue;
|
|
76
|
+
if (/^[#>|-]/.test(line) || /^\d+\./.test(line))
|
|
77
|
+
continue;
|
|
78
|
+
return stripInlineMarkdown(line);
|
|
79
|
+
}
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Titles and descriptions the docs maintain about themselves, keyed by filename.
|
|
84
|
+
*
|
|
85
|
+
* Matches list entries of the form `- [Title](file.md) - description`, which is
|
|
86
|
+
* how every section of `index.md` is written. Anything that does not match is
|
|
87
|
+
* skipped rather than guessed at.
|
|
88
|
+
*/
|
|
89
|
+
function parseCuratedIndex(docsRoot) {
|
|
90
|
+
const curated = new Map();
|
|
91
|
+
const indexPath = join(docsRoot, "index.md");
|
|
92
|
+
if (!existsSync(indexPath))
|
|
93
|
+
return curated;
|
|
94
|
+
let content;
|
|
95
|
+
try {
|
|
96
|
+
content = readFileSync(indexPath, "utf-8");
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return curated;
|
|
100
|
+
}
|
|
101
|
+
// `[Title](file.md)` followed by a dash of any width and the description.
|
|
102
|
+
const entry = /^\s*[-*]\s*\[([^\]]+)\]\(([^)#]+\.md)\)\s*[-–—:]\s*(.+?)\s*$/;
|
|
103
|
+
for (const line of content.split(/\r?\n/)) {
|
|
104
|
+
const match = entry.exec(line);
|
|
105
|
+
if (!match)
|
|
106
|
+
continue;
|
|
107
|
+
const [, title, target, description] = match;
|
|
108
|
+
const file = basename(target);
|
|
109
|
+
if (curated.has(file))
|
|
110
|
+
continue; // first mention wins
|
|
111
|
+
curated.set(file, { title: stripInlineMarkdown(title), description: truncate(stripInlineMarkdown(description)) });
|
|
112
|
+
}
|
|
113
|
+
return curated;
|
|
114
|
+
}
|
|
115
|
+
function describe(path, file, curated) {
|
|
116
|
+
const fromIndex = curated.get(file);
|
|
117
|
+
if (fromIndex) {
|
|
118
|
+
return { id: file, path, title: fromIndex.title, description: fromIndex.description };
|
|
119
|
+
}
|
|
120
|
+
// Not in the curated index — derive from the doc itself so new files are
|
|
121
|
+
// still usable the day they land.
|
|
122
|
+
const head = readHead(path);
|
|
123
|
+
return {
|
|
124
|
+
id: file,
|
|
125
|
+
path,
|
|
126
|
+
title: firstHeading(head) ?? file.replace(/\.md$/, ""),
|
|
127
|
+
description: truncate(firstParagraph(head) ?? ""),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
let cached;
|
|
131
|
+
/** Drop the cached listing. Tests, and anything that relocates the package root. */
|
|
132
|
+
export function resetSelfDocs() {
|
|
133
|
+
cached = undefined;
|
|
134
|
+
cachedSections = undefined;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Every shipped doc, sorted with the overview first and the rest alphabetical.
|
|
138
|
+
*
|
|
139
|
+
* Returns `[]` when the docs directory is absent rather than throwing: a source
|
|
140
|
+
* checkout, an odd packaging, or a trimmed container should degrade to "no docs
|
|
141
|
+
* section in the prompt", never to a failed session start.
|
|
142
|
+
*/
|
|
143
|
+
export function listSelfDocs() {
|
|
144
|
+
if (cached)
|
|
145
|
+
return cached;
|
|
146
|
+
const docsRoot = getDocsPath();
|
|
147
|
+
const docs = [];
|
|
148
|
+
if (existsSync(docsRoot)) {
|
|
149
|
+
const curated = parseCuratedIndex(docsRoot);
|
|
150
|
+
let files;
|
|
151
|
+
try {
|
|
152
|
+
files = readdirSync(docsRoot).filter((f) => f.endsWith(".md"));
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
files = [];
|
|
156
|
+
}
|
|
157
|
+
// Overview first: it is the doc that explains the others.
|
|
158
|
+
files.sort((a, b) => (a === "index.md" ? -1 : b === "index.md" ? 1 : a.localeCompare(b)));
|
|
159
|
+
for (const file of files) {
|
|
160
|
+
const path = join(docsRoot, file);
|
|
161
|
+
try {
|
|
162
|
+
if (!statSync(path).isFile())
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
docs.push(describe(path, file, curated));
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
// README and CHANGELOG sit beside the docs directory, not inside it, but the
|
|
172
|
+
// model needs them for the two questions the docs do not answer: what
|
|
173
|
+
// hoocode is, and what changed in this version.
|
|
174
|
+
const extras = [
|
|
175
|
+
{ path: getReadmePath(), title: "README", description: "What hoocode is, install, and a feature overview." },
|
|
176
|
+
{
|
|
177
|
+
path: getChangelogPath(),
|
|
178
|
+
title: "Changelog",
|
|
179
|
+
description: "Released versions and what changed in each.",
|
|
180
|
+
},
|
|
181
|
+
];
|
|
182
|
+
for (const extra of extras) {
|
|
183
|
+
if (!existsSync(extra.path))
|
|
184
|
+
continue;
|
|
185
|
+
docs.push({ id: basename(extra.path), path: extra.path, title: extra.title, description: extra.description });
|
|
186
|
+
}
|
|
187
|
+
cached = docs;
|
|
188
|
+
return docs;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* The system-prompt section, or `""` when there is nothing to point at.
|
|
192
|
+
*
|
|
193
|
+
* Deliberately just filenames. An earlier version carried a one-line summary
|
|
194
|
+
* per doc and cost ~860 tokens on every single turn, which is a poor trade for
|
|
195
|
+
* something most turns never use — and it stopped being necessary once
|
|
196
|
+
* SearchHooCode could retrieve at the heading level. Filenames alone still let
|
|
197
|
+
* the model go straight to `themes.md` or `keybindings.md` for the obvious
|
|
198
|
+
* cases, and anything less obvious is one search away. That is ~180 tokens.
|
|
199
|
+
*
|
|
200
|
+
* Directories are printed once rather than repeated per entry, for the same
|
|
201
|
+
* reason: the path was the single largest term on every line.
|
|
202
|
+
*/
|
|
203
|
+
export function formatSelfDocsForPrompt(docs = listSelfDocs()) {
|
|
204
|
+
if (docs.length === 0)
|
|
205
|
+
return "";
|
|
206
|
+
// Insertion order is already meaningful (overview first, then alphabetical,
|
|
207
|
+
// then README/CHANGELOG), so group without re-sorting.
|
|
208
|
+
const groups = new Map();
|
|
209
|
+
for (const doc of docs) {
|
|
210
|
+
const root = dirname(doc.path);
|
|
211
|
+
const bucket = groups.get(root);
|
|
212
|
+
if (bucket)
|
|
213
|
+
bucket.push(doc.id);
|
|
214
|
+
else
|
|
215
|
+
groups.set(root, [doc.id]);
|
|
216
|
+
}
|
|
217
|
+
const sections = [...groups].map(([root, files]) => `${root}/: ${files.join(", ")}`);
|
|
218
|
+
return `
|
|
219
|
+
|
|
220
|
+
# About hoocode itself
|
|
221
|
+
|
|
222
|
+
You are running inside hoocode. Its own docs ship with the install, listed below; hoocode is actively developed, so answer questions about it from these files rather than from memory. They sit outside the working directory, so searching the project will not find them. Use SearchHooCode to locate a specific heading, or read a file directly.
|
|
223
|
+
|
|
224
|
+
${sections.join("\n")}`;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* How much section body to keep.
|
|
228
|
+
*
|
|
229
|
+
* Every character past this is invisible to retrieval, so the cap is a recall
|
|
230
|
+
* limit, not just a size one: at 240 a question about `/grill` missed the
|
|
231
|
+
* section that documents it, because the term sat in the fourth sentence. 400
|
|
232
|
+
* covers the opening of essentially every section here for about 95KB more
|
|
233
|
+
* index across the corpus, which buys back that class of miss.
|
|
234
|
+
*/
|
|
235
|
+
const MAX_EXCERPT = 400;
|
|
236
|
+
/** `Custom tools` → `custom-tools`, so ids stay stable and readable. */
|
|
237
|
+
function slugify(heading) {
|
|
238
|
+
return (heading
|
|
239
|
+
.toLowerCase()
|
|
240
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
241
|
+
.replace(/^-+|-+$/g, "") || "section");
|
|
242
|
+
}
|
|
243
|
+
/** `extensions.md § Extensions › Custom tools` — what a search result is labelled with. */
|
|
244
|
+
export function sectionLabel(section) {
|
|
245
|
+
return section.headings.length > 0 ? `${section.file} § ${section.headings.join(" › ")}` : section.file;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Split one markdown file into sections at its headings.
|
|
249
|
+
*
|
|
250
|
+
* Fenced code is tracked so a `#` comment inside a bash block cannot be
|
|
251
|
+
* mistaken for a heading — which would otherwise split docs at every shell
|
|
252
|
+
* comment. Code *content* still lands in the excerpt: the exact identifiers
|
|
253
|
+
* someone searches for (`pi.registerTool`) usually live in the examples, and
|
|
254
|
+
* dropping them would blind the lexical leg to the best terms in the file.
|
|
255
|
+
*/
|
|
256
|
+
export function splitIntoSections(markdown, file, path) {
|
|
257
|
+
const lines = markdown.split(/\r?\n/);
|
|
258
|
+
const sections = [];
|
|
259
|
+
const trail = [];
|
|
260
|
+
const usedIds = new Set();
|
|
261
|
+
let current;
|
|
262
|
+
let body = [];
|
|
263
|
+
let inFence = false;
|
|
264
|
+
const flush = () => {
|
|
265
|
+
if (!current)
|
|
266
|
+
return;
|
|
267
|
+
current.excerpt = truncate(stripInlineMarkdown(body.join(" ")), MAX_EXCERPT);
|
|
268
|
+
sections.push(current);
|
|
269
|
+
body = [];
|
|
270
|
+
};
|
|
271
|
+
for (let i = 0; i < lines.length; i++) {
|
|
272
|
+
const raw = lines[i] ?? "";
|
|
273
|
+
if (raw.trimStart().startsWith("```")) {
|
|
274
|
+
inFence = !inFence;
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
const heading = inFence ? null : /^(#{1,6})\s+(.+?)\s*$/.exec(raw);
|
|
278
|
+
if (!heading) {
|
|
279
|
+
if (raw.trim() !== "")
|
|
280
|
+
body.push(raw.trim());
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
flush();
|
|
284
|
+
const depth = heading[1]?.length ?? 1;
|
|
285
|
+
const text = stripInlineMarkdown(heading[2] ?? "");
|
|
286
|
+
while (trail.length > 0 && (trail[trail.length - 1]?.depth ?? 0) >= depth)
|
|
287
|
+
trail.pop();
|
|
288
|
+
trail.push({ depth, text });
|
|
289
|
+
// Disambiguate repeated headings ("Example" appears eleven times in
|
|
290
|
+
// extensions.md) so ids stay unique and the registry does not collapse them.
|
|
291
|
+
let id = `${file}#${slugify(trail.map((t) => t.text).join("-"))}`;
|
|
292
|
+
if (usedIds.has(id)) {
|
|
293
|
+
let n = 2;
|
|
294
|
+
while (usedIds.has(`${id}-${n}`))
|
|
295
|
+
n++;
|
|
296
|
+
id = `${id}-${n}`;
|
|
297
|
+
}
|
|
298
|
+
usedIds.add(id);
|
|
299
|
+
current = { id, file, path, headings: trail.map((t) => t.text), line: i + 1, excerpt: "" };
|
|
300
|
+
}
|
|
301
|
+
flush();
|
|
302
|
+
return sections;
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Files kept out of the section index.
|
|
306
|
+
*
|
|
307
|
+
* The changelog is 40% of the corpus by section count and none of it answers
|
|
308
|
+
* "how does X work": it is hundreds of near-identical `Added`/`Fixed`/`Changed`
|
|
309
|
+
* headings under version numbers, which crowd real documentation out of the
|
|
310
|
+
* ranking while matching almost any query about a feature by name.
|
|
311
|
+
*
|
|
312
|
+
* `index.md` is excluded for the mirror-image reason: it is a table of contents,
|
|
313
|
+
* so its "sections" are lists of links whose text is every other doc's title and
|
|
314
|
+
* summary. That makes it match any query those docs would match, while carrying
|
|
315
|
+
* none of the content — a guaranteed false attractor that displaces the page it
|
|
316
|
+
* is pointing at.
|
|
317
|
+
*
|
|
318
|
+
* Both stay in the prompt's filename listing, one read away.
|
|
319
|
+
*/
|
|
320
|
+
const SECTION_INDEX_EXCLUDED = new Set(["CHANGELOG.md", "index.md"]);
|
|
321
|
+
let cachedSections;
|
|
322
|
+
/** Drop the cached section index. Tests, and anything that relocates the package root. */
|
|
323
|
+
export function resetSelfDocSections() {
|
|
324
|
+
cachedSections = undefined;
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Every section of every shipped doc.
|
|
328
|
+
*
|
|
329
|
+
* Reads each file once per session and caches; the docs are read-only install
|
|
330
|
+
* content, so there is nothing to invalidate on.
|
|
331
|
+
*/
|
|
332
|
+
export function listSelfDocSections() {
|
|
333
|
+
if (cachedSections)
|
|
334
|
+
return cachedSections;
|
|
335
|
+
const sections = [];
|
|
336
|
+
for (const doc of listSelfDocs()) {
|
|
337
|
+
if (SECTION_INDEX_EXCLUDED.has(doc.id))
|
|
338
|
+
continue;
|
|
339
|
+
let content;
|
|
340
|
+
try {
|
|
341
|
+
content = readFileSync(doc.path, "utf-8");
|
|
342
|
+
}
|
|
343
|
+
catch {
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
sections.push(...splitIntoSections(content, doc.id, doc.path));
|
|
347
|
+
}
|
|
348
|
+
cachedSections = sections;
|
|
349
|
+
return sections;
|
|
350
|
+
}
|
|
351
|
+
//# sourceMappingURL=self-docs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"self-docs.js","sourceRoot":"","sources":["../../src/core/self-docs.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The agent's index of hoocode's *own* documentation.\n *\n * The startup banner promises \"hoocode can explain its own features and look up\n * its docs\", and the docs really do ship with the install (`package.json`\n * `files` includes `docs`, and `copy-binary-assets` copies them into `dist/` for\n * the pkg binaries). What was missing is the only part that makes the promise\n * true: telling the model they exist. `getDocsPath()` had exactly one consumer —\n * `auth-guidance.ts`, which prints paths to the *human* — so nothing ever put a\n * docs path into model context.\n *\n * That gap is not one the model can close by itself. Its cwd is the user's\n * project, so `grep`/`find` there discover the user's docs, never hoocode's,\n * which live in an install directory whose path it cannot derive.\n *\n * Descriptions come from `docs/index.md` rather than being duplicated here.\n * That file is a curated, human-maintained table of contents, and a second\n * hand-written list is how an index goes stale the first week nobody updates\n * it. The directory listing stays the source of truth for *what exists*, so a\n * new doc still shows up (described from its own first paragraph) on the day it\n * lands, with or without an index entry.\n */\n\nimport { existsSync, readdirSync, readFileSync, statSync } from \"node:fs\";\nimport { basename, dirname, join } from \"node:path\";\nimport { getChangelogPath, getDocsPath, getReadmePath } from \"../config.js\";\n\nexport interface SelfDoc {\n\t/** Stable id: the filename, e.g. `skills.md`. Also how the model refers to it. */\n\tid: string;\n\t/** Absolute path, ready to hand to the read tool verbatim. */\n\tpath: string;\n\t/** Human title, e.g. \"Skills\". */\n\ttitle: string;\n\t/** One line on what the doc covers. May be empty if nothing could be derived. */\n\tdescription: string;\n}\n\n/** How much of a doc to read when deriving a fallback description. */\nconst HEAD_BYTES = 2048;\n\n/** Cap on a derived description, so one run-on opening line cannot bloat the prompt. */\nconst MAX_DESCRIPTION = 110;\n\nfunction truncate(text: string, max = MAX_DESCRIPTION): string {\n\tconst clean = text.replace(/\\s+/g, \" \").trim();\n\tif (clean.length <= max) return clean;\n\treturn `${clean.slice(0, max - 1).trimEnd()}…`;\n}\n\n/** Strip inline markdown that adds noise but no meaning in a prompt listing. */\nfunction stripInlineMarkdown(text: string): string {\n\treturn text\n\t\t.replace(/\\[([^\\]]+)\\]\\([^)]*\\)/g, \"$1\") // links → their text\n\t\t.replace(/[`*_]/g, \"\")\n\t\t.trim();\n}\n\nfunction readHead(path: string): string {\n\ttry {\n\t\t// Whole-file read: these are small, and slicing bytes off a UTF-8 file can\n\t\t// split a multi-byte character. Truncate after decoding instead.\n\t\treturn readFileSync(path, \"utf-8\").slice(0, HEAD_BYTES);\n\t} catch {\n\t\treturn \"\";\n\t}\n}\n\n/** First `# ` heading, or undefined. */\nfunction firstHeading(markdown: string): string | undefined {\n\tfor (const line of markdown.split(/\\r?\\n/)) {\n\t\tconst match = /^#\\s+(.+)$/.exec(line.trim());\n\t\tif (match?.[1]) return stripInlineMarkdown(match[1]);\n\t}\n\treturn undefined;\n}\n\n/**\n * First real prose line: not a heading, blockquote, list item, fence, or table\n * row. Used only for docs the curated index does not describe.\n */\nfunction firstParagraph(markdown: string): string | undefined {\n\tlet inFence = false;\n\tfor (const raw of markdown.split(/\\r?\\n/)) {\n\t\tconst line = raw.trim();\n\t\tif (line.startsWith(\"```\")) {\n\t\t\tinFence = !inFence;\n\t\t\tcontinue;\n\t\t}\n\t\tif (inFence || line === \"\") continue;\n\t\tif (/^[#>|-]/.test(line) || /^\\d+\\./.test(line)) continue;\n\t\treturn stripInlineMarkdown(line);\n\t}\n\treturn undefined;\n}\n\n/**\n * Titles and descriptions the docs maintain about themselves, keyed by filename.\n *\n * Matches list entries of the form `- [Title](file.md) - description`, which is\n * how every section of `index.md` is written. Anything that does not match is\n * skipped rather than guessed at.\n */\nfunction parseCuratedIndex(docsRoot: string): Map<string, { title: string; description: string }> {\n\tconst curated = new Map<string, { title: string; description: string }>();\n\tconst indexPath = join(docsRoot, \"index.md\");\n\tif (!existsSync(indexPath)) return curated;\n\n\tlet content: string;\n\ttry {\n\t\tcontent = readFileSync(indexPath, \"utf-8\");\n\t} catch {\n\t\treturn curated;\n\t}\n\n\t// `[Title](file.md)` followed by a dash of any width and the description.\n\tconst entry = /^\\s*[-*]\\s*\\[([^\\]]+)\\]\\(([^)#]+\\.md)\\)\\s*[-–—:]\\s*(.+?)\\s*$/;\n\tfor (const line of content.split(/\\r?\\n/)) {\n\t\tconst match = entry.exec(line);\n\t\tif (!match) continue;\n\t\tconst [, title, target, description] = match;\n\t\tconst file = basename(target);\n\t\tif (curated.has(file)) continue; // first mention wins\n\t\tcurated.set(file, { title: stripInlineMarkdown(title), description: truncate(stripInlineMarkdown(description)) });\n\t}\n\treturn curated;\n}\n\nfunction describe(path: string, file: string, curated: Map<string, { title: string; description: string }>): SelfDoc {\n\tconst fromIndex = curated.get(file);\n\tif (fromIndex) {\n\t\treturn { id: file, path, title: fromIndex.title, description: fromIndex.description };\n\t}\n\t// Not in the curated index — derive from the doc itself so new files are\n\t// still usable the day they land.\n\tconst head = readHead(path);\n\treturn {\n\t\tid: file,\n\t\tpath,\n\t\ttitle: firstHeading(head) ?? file.replace(/\\.md$/, \"\"),\n\t\tdescription: truncate(firstParagraph(head) ?? \"\"),\n\t};\n}\n\nlet cached: SelfDoc[] | undefined;\n\n/** Drop the cached listing. Tests, and anything that relocates the package root. */\nexport function resetSelfDocs(): void {\n\tcached = undefined;\n\tcachedSections = undefined;\n}\n\n/**\n * Every shipped doc, sorted with the overview first and the rest alphabetical.\n *\n * Returns `[]` when the docs directory is absent rather than throwing: a source\n * checkout, an odd packaging, or a trimmed container should degrade to \"no docs\n * section in the prompt\", never to a failed session start.\n */\nexport function listSelfDocs(): SelfDoc[] {\n\tif (cached) return cached;\n\n\tconst docsRoot = getDocsPath();\n\tconst docs: SelfDoc[] = [];\n\n\tif (existsSync(docsRoot)) {\n\t\tconst curated = parseCuratedIndex(docsRoot);\n\t\tlet files: string[];\n\t\ttry {\n\t\t\tfiles = readdirSync(docsRoot).filter((f) => f.endsWith(\".md\"));\n\t\t} catch {\n\t\t\tfiles = [];\n\t\t}\n\t\t// Overview first: it is the doc that explains the others.\n\t\tfiles.sort((a, b) => (a === \"index.md\" ? -1 : b === \"index.md\" ? 1 : a.localeCompare(b)));\n\t\tfor (const file of files) {\n\t\t\tconst path = join(docsRoot, file);\n\t\t\ttry {\n\t\t\t\tif (!statSync(path).isFile()) continue;\n\t\t\t} catch {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tdocs.push(describe(path, file, curated));\n\t\t}\n\t}\n\n\t// README and CHANGELOG sit beside the docs directory, not inside it, but the\n\t// model needs them for the two questions the docs do not answer: what\n\t// hoocode is, and what changed in this version.\n\tconst extras: Array<{ path: string; title: string; description: string }> = [\n\t\t{ path: getReadmePath(), title: \"README\", description: \"What hoocode is, install, and a feature overview.\" },\n\t\t{\n\t\t\tpath: getChangelogPath(),\n\t\t\ttitle: \"Changelog\",\n\t\t\tdescription: \"Released versions and what changed in each.\",\n\t\t},\n\t];\n\tfor (const extra of extras) {\n\t\tif (!existsSync(extra.path)) continue;\n\t\tdocs.push({ id: basename(extra.path), path: extra.path, title: extra.title, description: extra.description });\n\t}\n\n\tcached = docs;\n\treturn docs;\n}\n\n/**\n * The system-prompt section, or `\"\"` when there is nothing to point at.\n *\n * Deliberately just filenames. An earlier version carried a one-line summary\n * per doc and cost ~860 tokens on every single turn, which is a poor trade for\n * something most turns never use — and it stopped being necessary once\n * SearchHooCode could retrieve at the heading level. Filenames alone still let\n * the model go straight to `themes.md` or `keybindings.md` for the obvious\n * cases, and anything less obvious is one search away. That is ~180 tokens.\n *\n * Directories are printed once rather than repeated per entry, for the same\n * reason: the path was the single largest term on every line.\n */\nexport function formatSelfDocsForPrompt(docs: readonly SelfDoc[] = listSelfDocs()): string {\n\tif (docs.length === 0) return \"\";\n\n\t// Insertion order is already meaningful (overview first, then alphabetical,\n\t// then README/CHANGELOG), so group without re-sorting.\n\tconst groups = new Map<string, string[]>();\n\tfor (const doc of docs) {\n\t\tconst root = dirname(doc.path);\n\t\tconst bucket = groups.get(root);\n\t\tif (bucket) bucket.push(doc.id);\n\t\telse groups.set(root, [doc.id]);\n\t}\n\n\tconst sections = [...groups].map(([root, files]) => `${root}/: ${files.join(\", \")}`);\n\n\treturn `\n\n# About hoocode itself\n\nYou are running inside hoocode. Its own docs ship with the install, listed below; hoocode is actively developed, so answer questions about it from these files rather than from memory. They sit outside the working directory, so searching the project will not find them. Use SearchHooCode to locate a specific heading, or read a file directly.\n\n${sections.join(\"\\n\")}`;\n}\n\n// ---------------------------------------------------------------------------\n// Section index\n// ---------------------------------------------------------------------------\n\n/**\n * A single heading's worth of a doc.\n *\n * Doc-level retrieval would add nothing the prompt listing above does not\n * already give: thirty files with a summary each are cheap enough to list in\n * full, so a search that answers \"read extensions.md\" is a round trip for\n * information the model already had. The questions that actually need\n * retrieval are the ones inside a 1,100-line file — \"how do I register a\n * tool?\" should land on `extensions.md § Custom tools` with a line number, not\n * on the file.\n */\nexport interface SelfDocSection {\n\t/** `<file>#<slug>`, unique across the corpus. */\n\tid: string;\n\t/** Filename, e.g. `extensions.md`. */\n\tfile: string;\n\t/** Absolute path to the file. */\n\tpath: string;\n\t/** Heading trail from the document title down, e.g. `[\"Extensions\", \"Custom tools\"]`. */\n\theadings: string[];\n\t/** 1-based line of the heading, so a reader can jump straight to it. */\n\tline: number;\n\t/** Start of the section body, for ranking and for showing why a hit matched. */\n\texcerpt: string;\n}\n\n/**\n * How much section body to keep.\n *\n * Every character past this is invisible to retrieval, so the cap is a recall\n * limit, not just a size one: at 240 a question about `/grill` missed the\n * section that documents it, because the term sat in the fourth sentence. 400\n * covers the opening of essentially every section here for about 95KB more\n * index across the corpus, which buys back that class of miss.\n */\nconst MAX_EXCERPT = 400;\n\n/** `Custom tools` → `custom-tools`, so ids stay stable and readable. */\nfunction slugify(heading: string): string {\n\treturn (\n\t\theading\n\t\t\t.toLowerCase()\n\t\t\t.replace(/[^a-z0-9]+/g, \"-\")\n\t\t\t.replace(/^-+|-+$/g, \"\") || \"section\"\n\t);\n}\n\n/** `extensions.md § Extensions › Custom tools` — what a search result is labelled with. */\nexport function sectionLabel(section: SelfDocSection): string {\n\treturn section.headings.length > 0 ? `${section.file} § ${section.headings.join(\" › \")}` : section.file;\n}\n\n/**\n * Split one markdown file into sections at its headings.\n *\n * Fenced code is tracked so a `#` comment inside a bash block cannot be\n * mistaken for a heading — which would otherwise split docs at every shell\n * comment. Code *content* still lands in the excerpt: the exact identifiers\n * someone searches for (`pi.registerTool`) usually live in the examples, and\n * dropping them would blind the lexical leg to the best terms in the file.\n */\nexport function splitIntoSections(markdown: string, file: string, path: string): SelfDocSection[] {\n\tconst lines = markdown.split(/\\r?\\n/);\n\tconst sections: SelfDocSection[] = [];\n\tconst trail: Array<{ depth: number; text: string }> = [];\n\tconst usedIds = new Set<string>();\n\n\tlet current: SelfDocSection | undefined;\n\tlet body: string[] = [];\n\tlet inFence = false;\n\n\tconst flush = (): void => {\n\t\tif (!current) return;\n\t\tcurrent.excerpt = truncate(stripInlineMarkdown(body.join(\" \")), MAX_EXCERPT);\n\t\tsections.push(current);\n\t\tbody = [];\n\t};\n\n\tfor (let i = 0; i < lines.length; i++) {\n\t\tconst raw = lines[i] ?? \"\";\n\t\tif (raw.trimStart().startsWith(\"```\")) {\n\t\t\tinFence = !inFence;\n\t\t\tcontinue;\n\t\t}\n\t\tconst heading = inFence ? null : /^(#{1,6})\\s+(.+?)\\s*$/.exec(raw);\n\t\tif (!heading) {\n\t\t\tif (raw.trim() !== \"\") body.push(raw.trim());\n\t\t\tcontinue;\n\t\t}\n\n\t\tflush();\n\n\t\tconst depth = heading[1]?.length ?? 1;\n\t\tconst text = stripInlineMarkdown(heading[2] ?? \"\");\n\t\twhile (trail.length > 0 && (trail[trail.length - 1]?.depth ?? 0) >= depth) trail.pop();\n\t\ttrail.push({ depth, text });\n\n\t\t// Disambiguate repeated headings (\"Example\" appears eleven times in\n\t\t// extensions.md) so ids stay unique and the registry does not collapse them.\n\t\tlet id = `${file}#${slugify(trail.map((t) => t.text).join(\"-\"))}`;\n\t\tif (usedIds.has(id)) {\n\t\t\tlet n = 2;\n\t\t\twhile (usedIds.has(`${id}-${n}`)) n++;\n\t\t\tid = `${id}-${n}`;\n\t\t}\n\t\tusedIds.add(id);\n\n\t\tcurrent = { id, file, path, headings: trail.map((t) => t.text), line: i + 1, excerpt: \"\" };\n\t}\n\tflush();\n\n\treturn sections;\n}\n\n/**\n * Files kept out of the section index.\n *\n * The changelog is 40% of the corpus by section count and none of it answers\n * \"how does X work\": it is hundreds of near-identical `Added`/`Fixed`/`Changed`\n * headings under version numbers, which crowd real documentation out of the\n * ranking while matching almost any query about a feature by name.\n *\n * `index.md` is excluded for the mirror-image reason: it is a table of contents,\n * so its \"sections\" are lists of links whose text is every other doc's title and\n * summary. That makes it match any query those docs would match, while carrying\n * none of the content — a guaranteed false attractor that displaces the page it\n * is pointing at.\n *\n * Both stay in the prompt's filename listing, one read away.\n */\nconst SECTION_INDEX_EXCLUDED = new Set([\"CHANGELOG.md\", \"index.md\"]);\n\nlet cachedSections: SelfDocSection[] | undefined;\n\n/** Drop the cached section index. Tests, and anything that relocates the package root. */\nexport function resetSelfDocSections(): void {\n\tcachedSections = undefined;\n}\n\n/**\n * Every section of every shipped doc.\n *\n * Reads each file once per session and caches; the docs are read-only install\n * content, so there is nothing to invalidate on.\n */\nexport function listSelfDocSections(): SelfDocSection[] {\n\tif (cachedSections) return cachedSections;\n\n\tconst sections: SelfDocSection[] = [];\n\tfor (const doc of listSelfDocs()) {\n\t\tif (SECTION_INDEX_EXCLUDED.has(doc.id)) continue;\n\t\tlet content: string;\n\t\ttry {\n\t\t\tcontent = readFileSync(doc.path, \"utf-8\");\n\t\t} catch {\n\t\t\tcontinue;\n\t\t}\n\t\tsections.push(...splitIntoSections(content, doc.id, doc.path));\n\t}\n\n\tcachedSections = sections;\n\treturn sections;\n}\n"]}
|
|
@@ -29,6 +29,18 @@ export interface BuildSystemPromptOptions {
|
|
|
29
29
|
* agents exist without re-reading the agent registry each turn.
|
|
30
30
|
*/
|
|
31
31
|
agents?: AgentDefinition[];
|
|
32
|
+
/**
|
|
33
|
+
* Point the model at hoocode's own shipped docs so it can answer questions
|
|
34
|
+
* about hoocode itself.
|
|
35
|
+
*
|
|
36
|
+
* Defaults to true for the built-in prompt and false when `customPrompt`
|
|
37
|
+
* replaces it. Every other appended section (context files, skills, agents)
|
|
38
|
+
* only appears because the caller passed the content in; this one
|
|
39
|
+
* materializes on its own, so a caller who has taken over the system prompt
|
|
40
|
+
* gets it only by asking. That also keeps it out of light mode, whose whole
|
|
41
|
+
* point is a minimal fixed per-turn surface. Needs the read tool either way.
|
|
42
|
+
*/
|
|
43
|
+
includeSelfDocs?: boolean;
|
|
32
44
|
}
|
|
33
45
|
/** Build the system prompt with tools, guidelines, and context */
|
|
34
46
|
export declare function buildSystemPrompt(options: BuildSystemPromptOptions): string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"system-prompt.d.ts","sourceRoot":"","sources":["../../src/core/system-prompt.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,KAAK,eAAe,EAAkB,MAAM,wBAAwB,CAAC;AAE9E,OAAO,EAAyB,KAAK,KAAK,EAAE,MAAM,aAAa,CAAC;AAEhE,MAAM,WAAW,wBAAwB;IACxC,+CAA+C;IAC/C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,6FAA6F;IAC7F,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,0DAA0D;IAC1D,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,qFAAqF;IACrF,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC5B,uCAAuC;IACvC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,yBAAyB;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,gCAAgC;IAChC,YAAY,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACxD,yBAAyB;IACzB,MAAM,CAAC,EAAE,KAAK,EAAE,CAAC;IACjB;;;;OAIG;IACH,MAAM,CAAC,EAAE,eAAe,EAAE,CAAC;CAC3B;AAED,kEAAkE;AAClE,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,wBAAwB,GAAG,MAAM,CA8K3E","sourcesContent":["/**\n * System prompt construction and project context loading\n */\n\nimport { type AgentDefinition, TASK_TOOL_NAME } from \"./agent-frontmatter.js\";\nimport { formatAgentsForPrompt } from \"./agent-registry.js\";\nimport { formatSkillsForPrompt, type Skill } from \"./skills.js\";\n\nexport interface BuildSystemPromptOptions {\n\t/** Custom system prompt (replaces default). */\n\tcustomPrompt?: string;\n\t/** Tools to include in prompt. Default: [read, bash, edit, write, search, grep, find, ls] */\n\tselectedTools?: string[];\n\t/** Optional one-line tool snippets keyed by tool name. */\n\ttoolSnippets?: Record<string, string>;\n\t/** Additional guideline bullets appended to the default system prompt guidelines. */\n\tpromptGuidelines?: string[];\n\t/** Text to append to system prompt. */\n\tappendSystemPrompt?: string;\n\t/** Working directory. */\n\tcwd: string;\n\t/** Pre-loaded context files. */\n\tcontextFiles?: Array<{ path: string; content: string }>;\n\t/** Pre-loaded skills. */\n\tskills?: Skill[];\n\t/**\n\t * Available agents for delegation, emitted as `<available_agents>` XML.\n\t * Only populated when the Task tool is active so the model knows which\n\t * agents exist without re-reading the agent registry each turn.\n\t */\n\tagents?: AgentDefinition[];\n}\n\n/** Build the system prompt with tools, guidelines, and context */\nexport function buildSystemPrompt(options: BuildSystemPromptOptions): string {\n\tconst {\n\t\tcustomPrompt,\n\t\tselectedTools,\n\t\ttoolSnippets,\n\t\tpromptGuidelines,\n\t\tappendSystemPrompt,\n\t\tcwd,\n\t\tcontextFiles: providedContextFiles,\n\t\tskills: providedSkills,\n\t\tagents: providedAgents,\n\t} = options;\n\tconst resolvedCwd = cwd;\n\tconst promptCwd = resolvedCwd.replace(/\\\\/g, \"/\");\n\n\tconst now = new Date();\n\tconst year = now.getFullYear();\n\tconst month = String(now.getMonth() + 1).padStart(2, \"0\");\n\tconst day = String(now.getDate()).padStart(2, \"0\");\n\tconst date = `${year}-${month}-${day}`;\n\n\tconst appendSection = appendSystemPrompt ? `\\n\\n${appendSystemPrompt}` : \"\";\n\n\tconst contextFiles = providedContextFiles ?? [];\n\tconst skills = providedSkills ?? [];\n\tconst agents = providedAgents ?? [];\n\n\tif (customPrompt) {\n\t\tlet prompt = customPrompt;\n\n\t\tif (appendSection) {\n\t\t\tprompt += appendSection;\n\t\t}\n\n\t\t// Append project context files\n\t\tif (contextFiles.length > 0) {\n\t\t\tprompt += \"\\n\\n# Project Context\\n\\n\";\n\t\t\tprompt += \"Project-specific instructions and guidelines:\\n\\n\";\n\t\t\tfor (const { path: filePath, content } of contextFiles) {\n\t\t\t\tprompt += `## ${filePath}\\n\\n${content}\\n\\n`;\n\t\t\t}\n\t\t}\n\n\t\t// Append skills section (only if read tool is available)\n\t\tconst hasRead = !selectedTools || selectedTools.includes(\"read\");\n\t\tif (hasRead && skills.length > 0) {\n\t\t\tprompt += formatSkillsForPrompt(skills);\n\t\t}\n\n\t\t// Append agents section (only when Task tool is active)\n\t\tconst hasTask = !selectedTools || selectedTools.includes(TASK_TOOL_NAME);\n\t\tif (hasTask && agents.length > 0) {\n\t\t\tprompt += formatAgentsForPrompt(agents);\n\t\t}\n\n\t\t// Add date and working directory last\n\t\tprompt += `\\nCurrent date: ${date}`;\n\t\tprompt += `\\nCurrent working directory: ${promptCwd}`;\n\n\t\treturn prompt;\n\t}\n\n\t// Build tools list based on selected tools.\n\t// A tool appears in Available tools only when the caller provides a one-line snippet.\n\tconst tools = selectedTools || [\"read\", \"bash\", \"edit\", \"write\", \"search\", \"grep\", \"find\", \"ls\"];\n\tconst visibleTools = tools.filter((name) => !!toolSnippets?.[name]);\n\tconst toolsList =\n\t\tvisibleTools.length > 0 ? visibleTools.map((name) => `- ${name}: ${toolSnippets![name]}`).join(\"\\n\") : \"(none)\";\n\n\t// Build guidelines based on which tools are actually available\n\tconst guidelinesList: string[] = [];\n\tconst guidelinesSet = new Set<string>();\n\tconst addGuideline = (guideline: string): void => {\n\t\tif (guidelinesSet.has(guideline)) {\n\t\t\treturn;\n\t\t}\n\t\tguidelinesSet.add(guideline);\n\t\tguidelinesList.push(guideline);\n\t};\n\n\tconst hasBash = tools.includes(\"bash\");\n\tconst hasSearch = tools.includes(\"search\");\n\tconst hasGrep = tools.includes(\"grep\");\n\tconst hasFind = tools.includes(\"find\");\n\tconst hasLs = tools.includes(\"ls\");\n\tconst hasRead = tools.includes(\"read\");\n\n\t// File exploration guidelines. Name only the tools that are actually\n\t// registered (the condition used to OR the three but hardcode all three\n\t// names, advertising tools that might not exist) and map each to its job so\n\t// the model picks the right one instead of defaulting to its bash habit.\n\tconst explore: string[] = [];\n\tif (hasSearch) explore.push(\"search (find where code lives by concept or identifier)\");\n\tif (hasGrep) explore.push(\"grep (exact line/regex search)\");\n\tif (hasFind) explore.push(\"find (locate files by name/glob)\");\n\tif (hasLs) explore.push(\"ls (list directory contents)\");\n\tif (explore.length > 0) {\n\t\taddGuideline(\n\t\t\t`For file exploration use the dedicated tools — ${explore.join(\", \")} — instead of bash; they are faster, and respect .gitignore where applicable`,\n\t\t);\n\t} else if (hasBash) {\n\t\taddGuideline(\"Use bash for file exploration (ls, rg/grep, find)\");\n\t}\n\n\t// Single source of truth for the search↔grep decision. Gated on both tools\n\t// being active so we never reference a tool that isn't registered — grep.ts\n\t// and search.ts intentionally no longer cross-reference each other, since a\n\t// tool factory can't know what else is in the bundle.\n\tif (hasSearch && hasGrep) {\n\t\taddGuideline(\n\t\t\t\"Between search and grep: search finds where code lives by concept, behavior, or half-known name (ranked results); grep enumerates exact matching lines, regexes, and counts (output proportional to matches)\",\n\t\t);\n\t}\n\n\tfor (const guideline of promptGuidelines ?? []) {\n\t\tconst normalized = guideline.trim();\n\t\tif (normalized.length > 0) {\n\t\t\taddGuideline(normalized);\n\t\t}\n\t}\n\n\t// Always include these\n\taddGuideline(\"Be concise in your responses\");\n\taddGuideline(\"No preamble or postamble; do not restate the task or summarize what you just did\");\n\taddGuideline('Do not add closers like \"Let me know\" or \"Hope this helps\"');\n\taddGuideline(\n\t\t\"Do not narrate routine tool calls or results — the permission gate already shows them; speak when you have the answer or need a decision\",\n\t);\n\taddGuideline(\n\t\t\"Match the surrounding code's conventions for comments, docstrings, and types — do not add or strip them by default\",\n\t);\n\taddGuideline(\"Show file paths clearly when working with files\");\n\n\tconst guidelines = guidelinesList.map((g) => `- ${g}`).join(\"\\n\");\n\n\tlet prompt = `You are an expert coding assistant operating inside hoocode, a coding agent harness. You help users by reading files, executing commands, editing code, and writing new files.\n\nAvailable tools:\n${toolsList}\n\nIn addition to the tools above, you may have access to other custom tools depending on the project.\n\nGuidelines:\n${guidelines}`;\n\n\tif (appendSection) {\n\t\tprompt += appendSection;\n\t}\n\n\t// Append project context files\n\tif (contextFiles.length > 0) {\n\t\tprompt += \"\\n\\n# Project Context\\n\\n\";\n\t\tprompt += \"Project-specific instructions and guidelines:\\n\\n\";\n\t\tfor (const { path: filePath, content } of contextFiles) {\n\t\t\tprompt += `## ${filePath}\\n\\n${content}\\n\\n`;\n\t\t}\n\t}\n\n\t// Append skills section (only if read tool is available)\n\tif (hasRead && skills.length > 0) {\n\t\tprompt += formatSkillsForPrompt(skills);\n\t}\n\n\t// Append agents section (only when Task tool is active)\n\tconst hasTask = tools.includes(TASK_TOOL_NAME);\n\tif (hasTask && agents.length > 0) {\n\t\tprompt += formatAgentsForPrompt(agents);\n\t}\n\n\t// Add date and working directory last\n\tprompt += `\\nCurrent date: ${date}`;\n\tprompt += `\\nCurrent working directory: ${promptCwd}`;\n\n\treturn prompt;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"system-prompt.d.ts","sourceRoot":"","sources":["../../src/core/system-prompt.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,KAAK,eAAe,EAAkB,MAAM,wBAAwB,CAAC;AAG9E,OAAO,EAAyB,KAAK,KAAK,EAAE,MAAM,aAAa,CAAC;AAEhE,MAAM,WAAW,wBAAwB;IACxC,+CAA+C;IAC/C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,6FAA6F;IAC7F,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,0DAA0D;IAC1D,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,qFAAqF;IACrF,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC5B,uCAAuC;IACvC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,yBAAyB;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,gCAAgC;IAChC,YAAY,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACxD,yBAAyB;IACzB,MAAM,CAAC,EAAE,KAAK,EAAE,CAAC;IACjB;;;;OAIG;IACH,MAAM,CAAC,EAAE,eAAe,EAAE,CAAC;IAC3B;;;;;;;;;;OAUG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,kEAAkE;AAClE,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,wBAAwB,GAAG,MAAM,CA0L3E","sourcesContent":["/**\n * System prompt construction and project context loading\n */\n\nimport { type AgentDefinition, TASK_TOOL_NAME } from \"./agent-frontmatter.js\";\nimport { formatAgentsForPrompt } from \"./agent-registry.js\";\nimport { formatSelfDocsForPrompt } from \"./self-docs.js\";\nimport { formatSkillsForPrompt, type Skill } from \"./skills.js\";\n\nexport interface BuildSystemPromptOptions {\n\t/** Custom system prompt (replaces default). */\n\tcustomPrompt?: string;\n\t/** Tools to include in prompt. Default: [read, bash, edit, write, search, grep, find, ls] */\n\tselectedTools?: string[];\n\t/** Optional one-line tool snippets keyed by tool name. */\n\ttoolSnippets?: Record<string, string>;\n\t/** Additional guideline bullets appended to the default system prompt guidelines. */\n\tpromptGuidelines?: string[];\n\t/** Text to append to system prompt. */\n\tappendSystemPrompt?: string;\n\t/** Working directory. */\n\tcwd: string;\n\t/** Pre-loaded context files. */\n\tcontextFiles?: Array<{ path: string; content: string }>;\n\t/** Pre-loaded skills. */\n\tskills?: Skill[];\n\t/**\n\t * Available agents for delegation, emitted as `<available_agents>` XML.\n\t * Only populated when the Task tool is active so the model knows which\n\t * agents exist without re-reading the agent registry each turn.\n\t */\n\tagents?: AgentDefinition[];\n\t/**\n\t * Point the model at hoocode's own shipped docs so it can answer questions\n\t * about hoocode itself.\n\t *\n\t * Defaults to true for the built-in prompt and false when `customPrompt`\n\t * replaces it. Every other appended section (context files, skills, agents)\n\t * only appears because the caller passed the content in; this one\n\t * materializes on its own, so a caller who has taken over the system prompt\n\t * gets it only by asking. That also keeps it out of light mode, whose whole\n\t * point is a minimal fixed per-turn surface. Needs the read tool either way.\n\t */\n\tincludeSelfDocs?: boolean;\n}\n\n/** Build the system prompt with tools, guidelines, and context */\nexport function buildSystemPrompt(options: BuildSystemPromptOptions): string {\n\tconst {\n\t\tcustomPrompt,\n\t\tselectedTools,\n\t\ttoolSnippets,\n\t\tpromptGuidelines,\n\t\tappendSystemPrompt,\n\t\tcwd,\n\t\tcontextFiles: providedContextFiles,\n\t\tskills: providedSkills,\n\t\tagents: providedAgents,\n\t\tincludeSelfDocs,\n\t} = options;\n\tconst resolvedCwd = cwd;\n\tconst promptCwd = resolvedCwd.replace(/\\\\/g, \"/\");\n\n\tconst now = new Date();\n\tconst year = now.getFullYear();\n\tconst month = String(now.getMonth() + 1).padStart(2, \"0\");\n\tconst day = String(now.getDate()).padStart(2, \"0\");\n\tconst date = `${year}-${month}-${day}`;\n\n\tconst appendSection = appendSystemPrompt ? `\\n\\n${appendSystemPrompt}` : \"\";\n\tconst wantSelfDocs = includeSelfDocs ?? !customPrompt;\n\n\tconst contextFiles = providedContextFiles ?? [];\n\tconst skills = providedSkills ?? [];\n\tconst agents = providedAgents ?? [];\n\n\tif (customPrompt) {\n\t\tlet prompt = customPrompt;\n\n\t\tif (appendSection) {\n\t\t\tprompt += appendSection;\n\t\t}\n\n\t\t// Append project context files\n\t\tif (contextFiles.length > 0) {\n\t\t\tprompt += \"\\n\\n# Project Context\\n\\n\";\n\t\t\tprompt += \"Project-specific instructions and guidelines:\\n\\n\";\n\t\t\tfor (const { path: filePath, content } of contextFiles) {\n\t\t\t\tprompt += `## ${filePath}\\n\\n${content}\\n\\n`;\n\t\t\t}\n\t\t}\n\n\t\t// Append skills section (only if read tool is available)\n\t\tconst hasRead = !selectedTools || selectedTools.includes(\"read\");\n\t\tif (hasRead && skills.length > 0) {\n\t\t\tprompt += formatSkillsForPrompt(skills);\n\t\t}\n\n\t\t// Append agents section (only when Task tool is active)\n\t\tconst hasTask = !selectedTools || selectedTools.includes(TASK_TOOL_NAME);\n\t\tif (hasTask && agents.length > 0) {\n\t\t\tprompt += formatAgentsForPrompt(agents);\n\t\t}\n\n\t\t// Append hoocode's own docs (only if read tool is available)\n\t\tif (wantSelfDocs && hasRead) {\n\t\t\tprompt += formatSelfDocsForPrompt();\n\t\t}\n\n\t\t// Add date and working directory last\n\t\tprompt += `\\nCurrent date: ${date}`;\n\t\tprompt += `\\nCurrent working directory: ${promptCwd}`;\n\n\t\treturn prompt;\n\t}\n\n\t// Build tools list based on selected tools.\n\t// A tool appears in Available tools only when the caller provides a one-line snippet.\n\tconst tools = selectedTools || [\"read\", \"bash\", \"edit\", \"write\", \"search\", \"grep\", \"find\", \"ls\"];\n\tconst visibleTools = tools.filter((name) => !!toolSnippets?.[name]);\n\tconst toolsList =\n\t\tvisibleTools.length > 0 ? visibleTools.map((name) => `- ${name}: ${toolSnippets![name]}`).join(\"\\n\") : \"(none)\";\n\n\t// Build guidelines based on which tools are actually available\n\tconst guidelinesList: string[] = [];\n\tconst guidelinesSet = new Set<string>();\n\tconst addGuideline = (guideline: string): void => {\n\t\tif (guidelinesSet.has(guideline)) {\n\t\t\treturn;\n\t\t}\n\t\tguidelinesSet.add(guideline);\n\t\tguidelinesList.push(guideline);\n\t};\n\n\tconst hasBash = tools.includes(\"bash\");\n\tconst hasSearch = tools.includes(\"search\");\n\tconst hasGrep = tools.includes(\"grep\");\n\tconst hasFind = tools.includes(\"find\");\n\tconst hasLs = tools.includes(\"ls\");\n\tconst hasRead = tools.includes(\"read\");\n\n\t// File exploration guidelines. Name only the tools that are actually\n\t// registered (the condition used to OR the three but hardcode all three\n\t// names, advertising tools that might not exist) and map each to its job so\n\t// the model picks the right one instead of defaulting to its bash habit.\n\tconst explore: string[] = [];\n\tif (hasSearch) explore.push(\"search (find where code lives by concept or identifier)\");\n\tif (hasGrep) explore.push(\"grep (exact line/regex search)\");\n\tif (hasFind) explore.push(\"find (locate files by name/glob)\");\n\tif (hasLs) explore.push(\"ls (list directory contents)\");\n\tif (explore.length > 0) {\n\t\taddGuideline(\n\t\t\t`For file exploration use the dedicated tools — ${explore.join(\", \")} — instead of bash; they are faster, and respect .gitignore where applicable`,\n\t\t);\n\t} else if (hasBash) {\n\t\taddGuideline(\"Use bash for file exploration (ls, rg/grep, find)\");\n\t}\n\n\t// Single source of truth for the search↔grep decision. Gated on both tools\n\t// being active so we never reference a tool that isn't registered — grep.ts\n\t// and search.ts intentionally no longer cross-reference each other, since a\n\t// tool factory can't know what else is in the bundle.\n\tif (hasSearch && hasGrep) {\n\t\taddGuideline(\n\t\t\t\"Between search and grep: search finds where code lives by concept, behavior, or half-known name (ranked results); grep enumerates exact matching lines, regexes, and counts (output proportional to matches)\",\n\t\t);\n\t}\n\n\tfor (const guideline of promptGuidelines ?? []) {\n\t\tconst normalized = guideline.trim();\n\t\tif (normalized.length > 0) {\n\t\t\taddGuideline(normalized);\n\t\t}\n\t}\n\n\t// Always include these\n\taddGuideline(\"Be concise in your responses\");\n\taddGuideline(\"No preamble or postamble; do not restate the task or summarize what you just did\");\n\taddGuideline('Do not add closers like \"Let me know\" or \"Hope this helps\"');\n\taddGuideline(\n\t\t\"Do not narrate routine tool calls or results — the permission gate already shows them; speak when you have the answer or need a decision\",\n\t);\n\taddGuideline(\n\t\t\"Match the surrounding code's conventions for comments, docstrings, and types — do not add or strip them by default\",\n\t);\n\taddGuideline(\"Show file paths clearly when working with files\");\n\n\tconst guidelines = guidelinesList.map((g) => `- ${g}`).join(\"\\n\");\n\n\tlet prompt = `You are an expert coding assistant operating inside hoocode, a coding agent harness. You help users by reading files, executing commands, editing code, and writing new files.\n\nAvailable tools:\n${toolsList}\n\nIn addition to the tools above, you may have access to other custom tools depending on the project.\n\nGuidelines:\n${guidelines}`;\n\n\tif (appendSection) {\n\t\tprompt += appendSection;\n\t}\n\n\t// Append project context files\n\tif (contextFiles.length > 0) {\n\t\tprompt += \"\\n\\n# Project Context\\n\\n\";\n\t\tprompt += \"Project-specific instructions and guidelines:\\n\\n\";\n\t\tfor (const { path: filePath, content } of contextFiles) {\n\t\t\tprompt += `## ${filePath}\\n\\n${content}\\n\\n`;\n\t\t}\n\t}\n\n\t// Append skills section (only if read tool is available)\n\tif (hasRead && skills.length > 0) {\n\t\tprompt += formatSkillsForPrompt(skills);\n\t}\n\n\t// Append agents section (only when Task tool is active)\n\tconst hasTask = tools.includes(TASK_TOOL_NAME);\n\tif (hasTask && agents.length > 0) {\n\t\tprompt += formatAgentsForPrompt(agents);\n\t}\n\n\t// Append hoocode's own docs (only if read tool is available)\n\tif (wantSelfDocs && hasRead) {\n\t\tprompt += formatSelfDocsForPrompt();\n\t}\n\n\t// Add date and working directory last\n\tprompt += `\\nCurrent date: ${date}`;\n\tprompt += `\\nCurrent working directory: ${promptCwd}`;\n\n\treturn prompt;\n}\n"]}
|
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { TASK_TOOL_NAME } from "./agent-frontmatter.js";
|
|
5
5
|
import { formatAgentsForPrompt } from "./agent-registry.js";
|
|
6
|
+
import { formatSelfDocsForPrompt } from "./self-docs.js";
|
|
6
7
|
import { formatSkillsForPrompt } from "./skills.js";
|
|
7
8
|
/** Build the system prompt with tools, guidelines, and context */
|
|
8
9
|
export function buildSystemPrompt(options) {
|
|
9
|
-
const { customPrompt, selectedTools, toolSnippets, promptGuidelines, appendSystemPrompt, cwd, contextFiles: providedContextFiles, skills: providedSkills, agents: providedAgents, } = options;
|
|
10
|
+
const { customPrompt, selectedTools, toolSnippets, promptGuidelines, appendSystemPrompt, cwd, contextFiles: providedContextFiles, skills: providedSkills, agents: providedAgents, includeSelfDocs, } = options;
|
|
10
11
|
const resolvedCwd = cwd;
|
|
11
12
|
const promptCwd = resolvedCwd.replace(/\\/g, "/");
|
|
12
13
|
const now = new Date();
|
|
@@ -15,6 +16,7 @@ export function buildSystemPrompt(options) {
|
|
|
15
16
|
const day = String(now.getDate()).padStart(2, "0");
|
|
16
17
|
const date = `${year}-${month}-${day}`;
|
|
17
18
|
const appendSection = appendSystemPrompt ? `\n\n${appendSystemPrompt}` : "";
|
|
19
|
+
const wantSelfDocs = includeSelfDocs ?? !customPrompt;
|
|
18
20
|
const contextFiles = providedContextFiles ?? [];
|
|
19
21
|
const skills = providedSkills ?? [];
|
|
20
22
|
const agents = providedAgents ?? [];
|
|
@@ -41,6 +43,10 @@ export function buildSystemPrompt(options) {
|
|
|
41
43
|
if (hasTask && agents.length > 0) {
|
|
42
44
|
prompt += formatAgentsForPrompt(agents);
|
|
43
45
|
}
|
|
46
|
+
// Append hoocode's own docs (only if read tool is available)
|
|
47
|
+
if (wantSelfDocs && hasRead) {
|
|
48
|
+
prompt += formatSelfDocsForPrompt();
|
|
49
|
+
}
|
|
44
50
|
// Add date and working directory last
|
|
45
51
|
prompt += `\nCurrent date: ${date}`;
|
|
46
52
|
prompt += `\nCurrent working directory: ${promptCwd}`;
|
|
@@ -136,6 +142,10 @@ ${guidelines}`;
|
|
|
136
142
|
if (hasTask && agents.length > 0) {
|
|
137
143
|
prompt += formatAgentsForPrompt(agents);
|
|
138
144
|
}
|
|
145
|
+
// Append hoocode's own docs (only if read tool is available)
|
|
146
|
+
if (wantSelfDocs && hasRead) {
|
|
147
|
+
prompt += formatSelfDocsForPrompt();
|
|
148
|
+
}
|
|
139
149
|
// Add date and working directory last
|
|
140
150
|
prompt += `\nCurrent date: ${date}`;
|
|
141
151
|
prompt += `\nCurrent working directory: ${promptCwd}`;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"system-prompt.js","sourceRoot":"","sources":["../../src/core/system-prompt.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * System prompt construction and project context loading\n */\n\nimport { type AgentDefinition, TASK_TOOL_NAME } from \"./agent-frontmatter.js\";\nimport { formatAgentsForPrompt } from \"./agent-registry.js\";\nimport { formatSkillsForPrompt, type Skill } from \"./skills.js\";\n\nexport interface BuildSystemPromptOptions {\n\t/** Custom system prompt (replaces default). */\n\tcustomPrompt?: string;\n\t/** Tools to include in prompt. Default: [read, bash, edit, write, search, grep, find, ls] */\n\tselectedTools?: string[];\n\t/** Optional one-line tool snippets keyed by tool name. */\n\ttoolSnippets?: Record<string, string>;\n\t/** Additional guideline bullets appended to the default system prompt guidelines. */\n\tpromptGuidelines?: string[];\n\t/** Text to append to system prompt. */\n\tappendSystemPrompt?: string;\n\t/** Working directory. */\n\tcwd: string;\n\t/** Pre-loaded context files. */\n\tcontextFiles?: Array<{ path: string; content: string }>;\n\t/** Pre-loaded skills. */\n\tskills?: Skill[];\n\t/**\n\t * Available agents for delegation, emitted as `<available_agents>` XML.\n\t * Only populated when the Task tool is active so the model knows which\n\t * agents exist without re-reading the agent registry each turn.\n\t */\n\tagents?: AgentDefinition[];\n}\n\n/** Build the system prompt with tools, guidelines, and context */\nexport function buildSystemPrompt(options: BuildSystemPromptOptions): string {\n\tconst {\n\t\tcustomPrompt,\n\t\tselectedTools,\n\t\ttoolSnippets,\n\t\tpromptGuidelines,\n\t\tappendSystemPrompt,\n\t\tcwd,\n\t\tcontextFiles: providedContextFiles,\n\t\tskills: providedSkills,\n\t\tagents: providedAgents,\n\t} = options;\n\tconst resolvedCwd = cwd;\n\tconst promptCwd = resolvedCwd.replace(/\\\\/g, \"/\");\n\n\tconst now = new Date();\n\tconst year = now.getFullYear();\n\tconst month = String(now.getMonth() + 1).padStart(2, \"0\");\n\tconst day = String(now.getDate()).padStart(2, \"0\");\n\tconst date = `${year}-${month}-${day}`;\n\n\tconst appendSection = appendSystemPrompt ? `\\n\\n${appendSystemPrompt}` : \"\";\n\n\tconst contextFiles = providedContextFiles ?? [];\n\tconst skills = providedSkills ?? [];\n\tconst agents = providedAgents ?? [];\n\n\tif (customPrompt) {\n\t\tlet prompt = customPrompt;\n\n\t\tif (appendSection) {\n\t\t\tprompt += appendSection;\n\t\t}\n\n\t\t// Append project context files\n\t\tif (contextFiles.length > 0) {\n\t\t\tprompt += \"\\n\\n# Project Context\\n\\n\";\n\t\t\tprompt += \"Project-specific instructions and guidelines:\\n\\n\";\n\t\t\tfor (const { path: filePath, content } of contextFiles) {\n\t\t\t\tprompt += `## ${filePath}\\n\\n${content}\\n\\n`;\n\t\t\t}\n\t\t}\n\n\t\t// Append skills section (only if read tool is available)\n\t\tconst hasRead = !selectedTools || selectedTools.includes(\"read\");\n\t\tif (hasRead && skills.length > 0) {\n\t\t\tprompt += formatSkillsForPrompt(skills);\n\t\t}\n\n\t\t// Append agents section (only when Task tool is active)\n\t\tconst hasTask = !selectedTools || selectedTools.includes(TASK_TOOL_NAME);\n\t\tif (hasTask && agents.length > 0) {\n\t\t\tprompt += formatAgentsForPrompt(agents);\n\t\t}\n\n\t\t// Add date and working directory last\n\t\tprompt += `\\nCurrent date: ${date}`;\n\t\tprompt += `\\nCurrent working directory: ${promptCwd}`;\n\n\t\treturn prompt;\n\t}\n\n\t// Build tools list based on selected tools.\n\t// A tool appears in Available tools only when the caller provides a one-line snippet.\n\tconst tools = selectedTools || [\"read\", \"bash\", \"edit\", \"write\", \"search\", \"grep\", \"find\", \"ls\"];\n\tconst visibleTools = tools.filter((name) => !!toolSnippets?.[name]);\n\tconst toolsList =\n\t\tvisibleTools.length > 0 ? visibleTools.map((name) => `- ${name}: ${toolSnippets![name]}`).join(\"\\n\") : \"(none)\";\n\n\t// Build guidelines based on which tools are actually available\n\tconst guidelinesList: string[] = [];\n\tconst guidelinesSet = new Set<string>();\n\tconst addGuideline = (guideline: string): void => {\n\t\tif (guidelinesSet.has(guideline)) {\n\t\t\treturn;\n\t\t}\n\t\tguidelinesSet.add(guideline);\n\t\tguidelinesList.push(guideline);\n\t};\n\n\tconst hasBash = tools.includes(\"bash\");\n\tconst hasSearch = tools.includes(\"search\");\n\tconst hasGrep = tools.includes(\"grep\");\n\tconst hasFind = tools.includes(\"find\");\n\tconst hasLs = tools.includes(\"ls\");\n\tconst hasRead = tools.includes(\"read\");\n\n\t// File exploration guidelines. Name only the tools that are actually\n\t// registered (the condition used to OR the three but hardcode all three\n\t// names, advertising tools that might not exist) and map each to its job so\n\t// the model picks the right one instead of defaulting to its bash habit.\n\tconst explore: string[] = [];\n\tif (hasSearch) explore.push(\"search (find where code lives by concept or identifier)\");\n\tif (hasGrep) explore.push(\"grep (exact line/regex search)\");\n\tif (hasFind) explore.push(\"find (locate files by name/glob)\");\n\tif (hasLs) explore.push(\"ls (list directory contents)\");\n\tif (explore.length > 0) {\n\t\taddGuideline(\n\t\t\t`For file exploration use the dedicated tools — ${explore.join(\", \")} — instead of bash; they are faster, and respect .gitignore where applicable`,\n\t\t);\n\t} else if (hasBash) {\n\t\taddGuideline(\"Use bash for file exploration (ls, rg/grep, find)\");\n\t}\n\n\t// Single source of truth for the search↔grep decision. Gated on both tools\n\t// being active so we never reference a tool that isn't registered — grep.ts\n\t// and search.ts intentionally no longer cross-reference each other, since a\n\t// tool factory can't know what else is in the bundle.\n\tif (hasSearch && hasGrep) {\n\t\taddGuideline(\n\t\t\t\"Between search and grep: search finds where code lives by concept, behavior, or half-known name (ranked results); grep enumerates exact matching lines, regexes, and counts (output proportional to matches)\",\n\t\t);\n\t}\n\n\tfor (const guideline of promptGuidelines ?? []) {\n\t\tconst normalized = guideline.trim();\n\t\tif (normalized.length > 0) {\n\t\t\taddGuideline(normalized);\n\t\t}\n\t}\n\n\t// Always include these\n\taddGuideline(\"Be concise in your responses\");\n\taddGuideline(\"No preamble or postamble; do not restate the task or summarize what you just did\");\n\taddGuideline('Do not add closers like \"Let me know\" or \"Hope this helps\"');\n\taddGuideline(\n\t\t\"Do not narrate routine tool calls or results — the permission gate already shows them; speak when you have the answer or need a decision\",\n\t);\n\taddGuideline(\n\t\t\"Match the surrounding code's conventions for comments, docstrings, and types — do not add or strip them by default\",\n\t);\n\taddGuideline(\"Show file paths clearly when working with files\");\n\n\tconst guidelines = guidelinesList.map((g) => `- ${g}`).join(\"\\n\");\n\n\tlet prompt = `You are an expert coding assistant operating inside hoocode, a coding agent harness. You help users by reading files, executing commands, editing code, and writing new files.\n\nAvailable tools:\n${toolsList}\n\nIn addition to the tools above, you may have access to other custom tools depending on the project.\n\nGuidelines:\n${guidelines}`;\n\n\tif (appendSection) {\n\t\tprompt += appendSection;\n\t}\n\n\t// Append project context files\n\tif (contextFiles.length > 0) {\n\t\tprompt += \"\\n\\n# Project Context\\n\\n\";\n\t\tprompt += \"Project-specific instructions and guidelines:\\n\\n\";\n\t\tfor (const { path: filePath, content } of contextFiles) {\n\t\t\tprompt += `## ${filePath}\\n\\n${content}\\n\\n`;\n\t\t}\n\t}\n\n\t// Append skills section (only if read tool is available)\n\tif (hasRead && skills.length > 0) {\n\t\tprompt += formatSkillsForPrompt(skills);\n\t}\n\n\t// Append agents section (only when Task tool is active)\n\tconst hasTask = tools.includes(TASK_TOOL_NAME);\n\tif (hasTask && agents.length > 0) {\n\t\tprompt += formatAgentsForPrompt(agents);\n\t}\n\n\t// Add date and working directory last\n\tprompt += `\\nCurrent date: ${date}`;\n\tprompt += `\\nCurrent working directory: ${promptCwd}`;\n\n\treturn prompt;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"system-prompt.js","sourceRoot":"","sources":["../../src/core/system-prompt.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * System prompt construction and project context loading\n */\n\nimport { type AgentDefinition, TASK_TOOL_NAME } from \"./agent-frontmatter.js\";\nimport { formatAgentsForPrompt } from \"./agent-registry.js\";\nimport { formatSelfDocsForPrompt } from \"./self-docs.js\";\nimport { formatSkillsForPrompt, type Skill } from \"./skills.js\";\n\nexport interface BuildSystemPromptOptions {\n\t/** Custom system prompt (replaces default). */\n\tcustomPrompt?: string;\n\t/** Tools to include in prompt. Default: [read, bash, edit, write, search, grep, find, ls] */\n\tselectedTools?: string[];\n\t/** Optional one-line tool snippets keyed by tool name. */\n\ttoolSnippets?: Record<string, string>;\n\t/** Additional guideline bullets appended to the default system prompt guidelines. */\n\tpromptGuidelines?: string[];\n\t/** Text to append to system prompt. */\n\tappendSystemPrompt?: string;\n\t/** Working directory. */\n\tcwd: string;\n\t/** Pre-loaded context files. */\n\tcontextFiles?: Array<{ path: string; content: string }>;\n\t/** Pre-loaded skills. */\n\tskills?: Skill[];\n\t/**\n\t * Available agents for delegation, emitted as `<available_agents>` XML.\n\t * Only populated when the Task tool is active so the model knows which\n\t * agents exist without re-reading the agent registry each turn.\n\t */\n\tagents?: AgentDefinition[];\n\t/**\n\t * Point the model at hoocode's own shipped docs so it can answer questions\n\t * about hoocode itself.\n\t *\n\t * Defaults to true for the built-in prompt and false when `customPrompt`\n\t * replaces it. Every other appended section (context files, skills, agents)\n\t * only appears because the caller passed the content in; this one\n\t * materializes on its own, so a caller who has taken over the system prompt\n\t * gets it only by asking. That also keeps it out of light mode, whose whole\n\t * point is a minimal fixed per-turn surface. Needs the read tool either way.\n\t */\n\tincludeSelfDocs?: boolean;\n}\n\n/** Build the system prompt with tools, guidelines, and context */\nexport function buildSystemPrompt(options: BuildSystemPromptOptions): string {\n\tconst {\n\t\tcustomPrompt,\n\t\tselectedTools,\n\t\ttoolSnippets,\n\t\tpromptGuidelines,\n\t\tappendSystemPrompt,\n\t\tcwd,\n\t\tcontextFiles: providedContextFiles,\n\t\tskills: providedSkills,\n\t\tagents: providedAgents,\n\t\tincludeSelfDocs,\n\t} = options;\n\tconst resolvedCwd = cwd;\n\tconst promptCwd = resolvedCwd.replace(/\\\\/g, \"/\");\n\n\tconst now = new Date();\n\tconst year = now.getFullYear();\n\tconst month = String(now.getMonth() + 1).padStart(2, \"0\");\n\tconst day = String(now.getDate()).padStart(2, \"0\");\n\tconst date = `${year}-${month}-${day}`;\n\n\tconst appendSection = appendSystemPrompt ? `\\n\\n${appendSystemPrompt}` : \"\";\n\tconst wantSelfDocs = includeSelfDocs ?? !customPrompt;\n\n\tconst contextFiles = providedContextFiles ?? [];\n\tconst skills = providedSkills ?? [];\n\tconst agents = providedAgents ?? [];\n\n\tif (customPrompt) {\n\t\tlet prompt = customPrompt;\n\n\t\tif (appendSection) {\n\t\t\tprompt += appendSection;\n\t\t}\n\n\t\t// Append project context files\n\t\tif (contextFiles.length > 0) {\n\t\t\tprompt += \"\\n\\n# Project Context\\n\\n\";\n\t\t\tprompt += \"Project-specific instructions and guidelines:\\n\\n\";\n\t\t\tfor (const { path: filePath, content } of contextFiles) {\n\t\t\t\tprompt += `## ${filePath}\\n\\n${content}\\n\\n`;\n\t\t\t}\n\t\t}\n\n\t\t// Append skills section (only if read tool is available)\n\t\tconst hasRead = !selectedTools || selectedTools.includes(\"read\");\n\t\tif (hasRead && skills.length > 0) {\n\t\t\tprompt += formatSkillsForPrompt(skills);\n\t\t}\n\n\t\t// Append agents section (only when Task tool is active)\n\t\tconst hasTask = !selectedTools || selectedTools.includes(TASK_TOOL_NAME);\n\t\tif (hasTask && agents.length > 0) {\n\t\t\tprompt += formatAgentsForPrompt(agents);\n\t\t}\n\n\t\t// Append hoocode's own docs (only if read tool is available)\n\t\tif (wantSelfDocs && hasRead) {\n\t\t\tprompt += formatSelfDocsForPrompt();\n\t\t}\n\n\t\t// Add date and working directory last\n\t\tprompt += `\\nCurrent date: ${date}`;\n\t\tprompt += `\\nCurrent working directory: ${promptCwd}`;\n\n\t\treturn prompt;\n\t}\n\n\t// Build tools list based on selected tools.\n\t// A tool appears in Available tools only when the caller provides a one-line snippet.\n\tconst tools = selectedTools || [\"read\", \"bash\", \"edit\", \"write\", \"search\", \"grep\", \"find\", \"ls\"];\n\tconst visibleTools = tools.filter((name) => !!toolSnippets?.[name]);\n\tconst toolsList =\n\t\tvisibleTools.length > 0 ? visibleTools.map((name) => `- ${name}: ${toolSnippets![name]}`).join(\"\\n\") : \"(none)\";\n\n\t// Build guidelines based on which tools are actually available\n\tconst guidelinesList: string[] = [];\n\tconst guidelinesSet = new Set<string>();\n\tconst addGuideline = (guideline: string): void => {\n\t\tif (guidelinesSet.has(guideline)) {\n\t\t\treturn;\n\t\t}\n\t\tguidelinesSet.add(guideline);\n\t\tguidelinesList.push(guideline);\n\t};\n\n\tconst hasBash = tools.includes(\"bash\");\n\tconst hasSearch = tools.includes(\"search\");\n\tconst hasGrep = tools.includes(\"grep\");\n\tconst hasFind = tools.includes(\"find\");\n\tconst hasLs = tools.includes(\"ls\");\n\tconst hasRead = tools.includes(\"read\");\n\n\t// File exploration guidelines. Name only the tools that are actually\n\t// registered (the condition used to OR the three but hardcode all three\n\t// names, advertising tools that might not exist) and map each to its job so\n\t// the model picks the right one instead of defaulting to its bash habit.\n\tconst explore: string[] = [];\n\tif (hasSearch) explore.push(\"search (find where code lives by concept or identifier)\");\n\tif (hasGrep) explore.push(\"grep (exact line/regex search)\");\n\tif (hasFind) explore.push(\"find (locate files by name/glob)\");\n\tif (hasLs) explore.push(\"ls (list directory contents)\");\n\tif (explore.length > 0) {\n\t\taddGuideline(\n\t\t\t`For file exploration use the dedicated tools — ${explore.join(\", \")} — instead of bash; they are faster, and respect .gitignore where applicable`,\n\t\t);\n\t} else if (hasBash) {\n\t\taddGuideline(\"Use bash for file exploration (ls, rg/grep, find)\");\n\t}\n\n\t// Single source of truth for the search↔grep decision. Gated on both tools\n\t// being active so we never reference a tool that isn't registered — grep.ts\n\t// and search.ts intentionally no longer cross-reference each other, since a\n\t// tool factory can't know what else is in the bundle.\n\tif (hasSearch && hasGrep) {\n\t\taddGuideline(\n\t\t\t\"Between search and grep: search finds where code lives by concept, behavior, or half-known name (ranked results); grep enumerates exact matching lines, regexes, and counts (output proportional to matches)\",\n\t\t);\n\t}\n\n\tfor (const guideline of promptGuidelines ?? []) {\n\t\tconst normalized = guideline.trim();\n\t\tif (normalized.length > 0) {\n\t\t\taddGuideline(normalized);\n\t\t}\n\t}\n\n\t// Always include these\n\taddGuideline(\"Be concise in your responses\");\n\taddGuideline(\"No preamble or postamble; do not restate the task or summarize what you just did\");\n\taddGuideline('Do not add closers like \"Let me know\" or \"Hope this helps\"');\n\taddGuideline(\n\t\t\"Do not narrate routine tool calls or results — the permission gate already shows them; speak when you have the answer or need a decision\",\n\t);\n\taddGuideline(\n\t\t\"Match the surrounding code's conventions for comments, docstrings, and types — do not add or strip them by default\",\n\t);\n\taddGuideline(\"Show file paths clearly when working with files\");\n\n\tconst guidelines = guidelinesList.map((g) => `- ${g}`).join(\"\\n\");\n\n\tlet prompt = `You are an expert coding assistant operating inside hoocode, a coding agent harness. You help users by reading files, executing commands, editing code, and writing new files.\n\nAvailable tools:\n${toolsList}\n\nIn addition to the tools above, you may have access to other custom tools depending on the project.\n\nGuidelines:\n${guidelines}`;\n\n\tif (appendSection) {\n\t\tprompt += appendSection;\n\t}\n\n\t// Append project context files\n\tif (contextFiles.length > 0) {\n\t\tprompt += \"\\n\\n# Project Context\\n\\n\";\n\t\tprompt += \"Project-specific instructions and guidelines:\\n\\n\";\n\t\tfor (const { path: filePath, content } of contextFiles) {\n\t\t\tprompt += `## ${filePath}\\n\\n${content}\\n\\n`;\n\t\t}\n\t}\n\n\t// Append skills section (only if read tool is available)\n\tif (hasRead && skills.length > 0) {\n\t\tprompt += formatSkillsForPrompt(skills);\n\t}\n\n\t// Append agents section (only when Task tool is active)\n\tconst hasTask = tools.includes(TASK_TOOL_NAME);\n\tif (hasTask && agents.length > 0) {\n\t\tprompt += formatAgentsForPrompt(agents);\n\t}\n\n\t// Append hoocode's own docs (only if read tool is available)\n\tif (wantSelfDocs && hasRead) {\n\t\tprompt += formatSelfDocsForPrompt();\n\t}\n\n\t// Add date and working directory last\n\tprompt += `\\nCurrent date: ${date}`;\n\tprompt += `\\nCurrent working directory: ${promptCwd}`;\n\n\treturn prompt;\n}\n"]}
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* - loop.ts — /loop cron scheduling + autonomous continuation
|
|
13
13
|
* - marketplace.ts — /plugin marketplace + install/remove
|
|
14
14
|
* - canvas.ts — /canvas list/open/close for canvas extensions
|
|
15
|
+
* - self-knowledge.ts — SearchHooCode over hoocode's own docs and capabilities
|
|
15
16
|
* - prompt-reactive/ — runtime plugin-reuse nudge (reactive to tool/turn cues)
|
|
16
17
|
* - config.ts — hoo-config.json types, I/O, and merge rules
|
|
17
18
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hoo-core.d.ts","sourceRoot":"","sources":["../../../src/extensions/core/hoo-core.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"hoo-core.d.ts","sourceRoot":"","sources":["../../../src/extensions/core/hoo-core.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AAenE,iBAAS,OAAO,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CAcvC;;;;;;;AAMD,eAAe,OAAO,CAAC","sourcesContent":["/**\n * hoo-core — HooCode built-in core extension (composition root).\n *\n * Each concern lives in its own module in this directory:\n * - permission-gate.ts — prompts before bash/write/edit; hard tool/command policy\n * - mcp-loader.ts — discovers MCP server configs, connects, registers tools\n * - modes.ts — active mode resolution, mode prompts, /mode /plan /approve\n * - cost.ts — /cost session token + cost totals\n * - scaffold.ts — /new-skill /new-agent /new-command\n * - ask-options.ts — the ask_options tool (inline decision pane)\n * - thinking-escalation.ts — raise thinking after tool errors, then restore\n * - loop.ts — /loop cron scheduling + autonomous continuation\n * - marketplace.ts — /plugin marketplace + install/remove\n * - canvas.ts — /canvas list/open/close for canvas extensions\n * - self-knowledge.ts — SearchHooCode over hoocode's own docs and capabilities\n * - prompt-reactive/ — runtime plugin-reuse nudge (reactive to tool/turn cues)\n * - config.ts — hoo-config.json types, I/O, and merge rules\n *\n * `bin/hoocode.js` loads this module's default export as the built-in\n * extension factory.\n */\n\nimport type { ExtensionAPI } from \"../../core/extensions/types.js\";\nimport { setupAskOptions } from \"./ask-options.js\";\nimport { setupCanvas } from \"./canvas.js\";\nimport { setupCost } from \"./cost.js\";\nimport { setupLearn } from \"./learn.js\";\nimport { setupLoop } from \"./loop.js\";\nimport { setupMarketplace } from \"./marketplace.js\";\nimport { setupMcpLoader } from \"./mcp-loader.js\";\nimport { setupMode } from \"./modes.js\";\nimport { setupPermissionGate } from \"./permission-gate.js\";\nimport { setupPromptReactiveNudges } from \"./prompt-reactive/nudges.js\";\nimport { setupScaffold } from \"./scaffold.js\";\nimport { setupSelfKnowledge } from \"./self-knowledge.js\";\nimport { setupThinkingEscalation } from \"./thinking-escalation.js\";\n\nfunction hooCore(pi: ExtensionAPI): void {\n\tsetupPermissionGate(pi);\n\tsetupMcpLoader(pi);\n\tsetupMode(pi);\n\tsetupCost(pi);\n\tsetupScaffold(pi);\n\tsetupAskOptions(pi);\n\tsetupThinkingEscalation(pi);\n\tsetupLoop(pi);\n\tsetupMarketplace(pi);\n\tsetupCanvas(pi);\n\tsetupPromptReactiveNudges(pi);\n\tsetupLearn(pi);\n\tsetupSelfKnowledge(pi);\n}\n\nhooCore.displayName = \"hoo-core\";\n// Built-in plumbing, not a user-installed extension: it is always present, so\n// listing it in the startup resource summary is noise, not information.\nhooCore.internal = true;\nexport default hooCore;\n"]}
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* - loop.ts — /loop cron scheduling + autonomous continuation
|
|
13
13
|
* - marketplace.ts — /plugin marketplace + install/remove
|
|
14
14
|
* - canvas.ts — /canvas list/open/close for canvas extensions
|
|
15
|
+
* - self-knowledge.ts — SearchHooCode over hoocode's own docs and capabilities
|
|
15
16
|
* - prompt-reactive/ — runtime plugin-reuse nudge (reactive to tool/turn cues)
|
|
16
17
|
* - config.ts — hoo-config.json types, I/O, and merge rules
|
|
17
18
|
*
|
|
@@ -29,6 +30,7 @@ import { setupMode } from "./modes.js";
|
|
|
29
30
|
import { setupPermissionGate } from "./permission-gate.js";
|
|
30
31
|
import { setupPromptReactiveNudges } from "./prompt-reactive/nudges.js";
|
|
31
32
|
import { setupScaffold } from "./scaffold.js";
|
|
33
|
+
import { setupSelfKnowledge } from "./self-knowledge.js";
|
|
32
34
|
import { setupThinkingEscalation } from "./thinking-escalation.js";
|
|
33
35
|
function hooCore(pi) {
|
|
34
36
|
setupPermissionGate(pi);
|
|
@@ -43,6 +45,7 @@ function hooCore(pi) {
|
|
|
43
45
|
setupCanvas(pi);
|
|
44
46
|
setupPromptReactiveNudges(pi);
|
|
45
47
|
setupLearn(pi);
|
|
48
|
+
setupSelfKnowledge(pi);
|
|
46
49
|
}
|
|
47
50
|
hooCore.displayName = "hoo-core";
|
|
48
51
|
// Built-in plumbing, not a user-installed extension: it is always present, so
|