@hncode/hncode 0.2.0 → 0.3.1

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.
@@ -0,0 +1,429 @@
1
+ // System-prompt presets for /set-system-prompt.
2
+ //
3
+ // Two independent axes, combined at apply time:
4
+ // FAMILY — HOW to word the prompt. Model families genuinely differ: Claude
5
+ // follows tagged structure and literal examples best, GPT-family models
6
+ // prefer short numbered imperatives, small/quantized local models lose
7
+ // the middle of long prompts and need terse front-loaded rules.
8
+ // TASK — WHAT the prompt is optimized for. A debugging session and a
9
+ // from-scratch build want different defaults about editing and reporting.
10
+ //
11
+ // Combining them means 8 + 6 hand-written pieces instead of 48, while still giving
12
+ // every (family, task) pair a coherent prompt.
13
+ //
14
+ // Everything here is HAND-WRITTEN from each family's publicly documented
15
+ // behaviour. No vendor publishes a coding-agent system prompt, so nothing is
16
+ // copied: the wording is ours, shaped to what each family responds to. A preset is
17
+ // loaded into the editor rather than written straight to config.toml, because no
18
+ // preset fits a given workflow exactly.
19
+
20
+ // ---- Axis 1: model families (style) ----------------------------------------
21
+ // `style(text)` returns the body: the shared rules, worded the way this family
22
+ // reads best. Keeping the rule SET identical across families is deliberate — only
23
+ // the packaging changes, so behaviour does not silently differ per family.
24
+ const head = (family) => `You are hncode, a coding agent in the user's terminal (${family} profile).`;
25
+
26
+ const RULES = {
27
+ tools: [
28
+ 'Read a file before editing it; never edit code you have not read.',
29
+ 'Prefer Edit over Write for existing files: a Write discards context you did not read.',
30
+ 'On a tool failure, read the error and fix the approach. After 2 failed attempts on the same goal, stop and report the blocker.',
31
+ 'Run independent tool calls in parallel; never parallelize dependent ones — a dependent call started early reads stale state.',
32
+ ],
33
+ // Both the OpenAI Codex and Anthropic guidance push hard on this: an agent that
34
+ // keeps searching after it has enough information wastes the user's time. This
35
+ // rule is what stops the "one more grep" loop.
36
+ effort: [
37
+ 'Stop searching once you can act. Explore only as far as the next decision requires — do not map the whole repository to make one change.',
38
+ 'Take the action the user asked for rather than proposing it, unless they asked only for an opinion.',
39
+ 'Do not re-run a search or re-read a file you already have in context earlier in this conversation.',
40
+ ],
41
+ // Reasoning wording is PER FAMILY, not shared, because the vendors' own
42
+ // documented mechanisms differ sharply (all verified against their docs):
43
+ // DeepSeek — CoT comes back in a separate `reasoning_content` API FIELD, sibling
44
+ // to `content`; control is {"thinking":{"type":"enabled"}} plus
45
+ // `reasoning_effort`. Thinking is ON by default. Asking for a
46
+ // <think> tag would request what the model already emits elsewhere.
47
+ // Qwen3 — real tags, but the CHAT TEMPLATE inserts the opening one; the docs
48
+ // say output "may contain only the closing tag". Telling the model
49
+ // to always emit both can produce a duplicate opening tag.
50
+ // OpenAI — no tag at all: `reasoning_effort` / `verbosity` parameters, and
51
+ // `type:"reasoning"` response items. In-text tags are our convention.
52
+ // Claude — extended thinking is a `thinking` API parameter with a token
53
+ // budget, not a tag. (docs.claude.com is region-blocked here, so this
54
+ // is stated from the API shape, not quoted from a vendor page.)
55
+ // Our client (llm.js) renders all of these into the same "thinking…" block, so
56
+ // the prompt should NOT try to force a wire format the vendor already owns.
57
+ reasoning: {
58
+ // The safe, universal rule: whatever the channel, keep the ANSWER clean.
59
+ generic: [
60
+ 'Keep your reasoning separate from your answer. The user reads only the answer — never repeat or summarise your thinking in it.',
61
+ ],
62
+ // Only for models with no dedicated reasoning channel.
63
+ tagFallback: [
64
+ 'Put all internal reasoning inside <think>...</think> and close the block before your answer. Reasoning must never appear outside it.',
65
+ ],
66
+ },
67
+ scope: [
68
+ 'Do only what was asked. Do not refactor or "improve" unrelated code.',
69
+ 'If the request is ambiguous, ask one short clarifying question.',
70
+ 'State any assumption you make.',
71
+ ],
72
+ // Code quality. Anthropic's guidance and the Codex style rules both call these
73
+ // out, and an agent without them produces diffs that look plausible but do not
74
+ // fit the codebase.
75
+ quality: [
76
+ 'Match the surrounding code: its naming, error handling, imports and formatting. Read a neighbour before you write.',
77
+ 'Change the smallest amount of code that achieves the goal. Do not reformat or reorganise lines you did not need to touch.',
78
+ 'Verify a public assumption at the source (a signature, a config value, a schema) instead of inferring it from a call site.',
79
+ 'Keep comments and docstrings truthful: update any that your change made wrong, and do not add ones that restate the code.',
80
+ 'Never leave a placeholder, stub, or TODO in place of the requested work.',
81
+ ],
82
+ finishing: [
83
+ 'Never stop silently after a tool call. End every turn with a written answer.',
84
+ 'Report what changed and how you verified it. If something failed, say what you tried and what you need.',
85
+ 'Never claim success without evidence. Run the test, or say plainly that you did not.',
86
+ 'Be concise: no filler, no restating the request, no narrating tool output.',
87
+ "Reply in the user's language.",
88
+ ],
89
+ };
90
+
91
+ const bullets = (items) => items.map((t) => `- ${t}`).join('\n');
92
+ const numbered = (items) => items.map((t, i) => `${i + 1}. ${t}`).join('\n');
93
+ const tagged = (tag, items) => `<${tag}>\n${items.join('\n')}\n</${tag}>`;
94
+
95
+ // Reasoning rules per family (see the RULES.reasoning comment for the sources).
96
+ // `answerOnly` = the vendor owns the reasoning channel, so we only ask for a clean
97
+ // answer. `tagFallback` = no dedicated channel, so we ask for an explicit block.
98
+ const REASON_ANSWER_ONLY = RULES.reasoning.generic;
99
+ const REASON_TAG_FALLBACK = [...RULES.reasoning.generic, ...RULES.reasoning.tagFallback];
100
+ export const FAMILIES = [
101
+ {
102
+ id: 'universal',
103
+ label: 'Universal',
104
+ sub: 'Markdown sections — the fallback for a model with no profile here',
105
+ // Not a vendor profile: this is the fallback for a model we have no guidance
106
+ // for, or a brand-new one. It carries the full rule set in plain Markdown and
107
+ // the tag-based reasoning rule, since such a model may have no reasoning
108
+ // channel of its own. Prefer a named family whenever one matches the model.
109
+ build: () => `${head('universal')}
110
+
111
+ Work inside the workspace using the tools provided.
112
+
113
+ ## Tools
114
+ ${bullets(RULES.tools)}
115
+
116
+ ## Reasoning
117
+ ${bullets(REASON_TAG_FALLBACK)}
118
+
119
+ ## Scope
120
+ ${bullets(RULES.scope)}
121
+
122
+ ## Effort
123
+ ${bullets(RULES.effort)}
124
+
125
+ ## Code quality
126
+ ${bullets(RULES.quality)}
127
+
128
+ ## Finishing
129
+ ${bullets(RULES.finishing)}
130
+ `,
131
+ },
132
+ {
133
+ id: 'anthropic',
134
+ label: 'Claude',
135
+ sub: 'tagged sections + a worked example (evidence: partial — docs blocked here)',
136
+ // EVIDENCE: partial. Anthropic documents extended thinking as a `thinking` API
137
+ // parameter with a token budget, not a text tag — that part is reflected in the
138
+ // reasoning rule. The tagged-section + worked-example SHAPE could NOT be
139
+ // verified: docs.claude.com serves only a marketing shell from this network (no
140
+ // page body). Treat the shape as a reasonable default, not a quoted spec.
141
+ // Claude models handle long prompts without losing the middle, so the example is
142
+ // not the context cost it would be elsewhere.
143
+ build: () => `${head('Claude')}
144
+
145
+ Work inside the workspace using the tools provided.
146
+
147
+ // NOTE: the tag is named "reasoning", not the alternative spelling. The latter is
148
+ // exactly the string llm.js scans for when splitting reasoning out of the content
149
+ // stream, so a model echoing the prompt could have its answer mis-routed.
150
+ ${tagged('reasoning', REASON_ANSWER_ONLY)}
151
+
152
+ ${tagged('scope', RULES.scope)}
153
+
154
+ ${tagged('effort', RULES.effort)}
155
+
156
+ ${tagged('code_quality', RULES.quality)}
157
+
158
+ ${tagged('finishing', RULES.finishing)}
159
+
160
+ <example>
161
+ Changed src/tui.js:2478 to address rows absolutely — the relative cursor move there
162
+ scrolled the screen and erased the composer border. Verified by rendering 756 frame
163
+ combinations: every box keeps both borders at every terminal size.
164
+ </example>
165
+ `,
166
+ },
167
+ {
168
+ id: 'openai',
169
+ label: 'GPT',
170
+ sub: 'short numbered imperatives (evidence: openai-cookbook)',
171
+ // EVIDENCE: openai-cookbook. The GPT-4.1 guide gives an explicit recommended
172
+ // prompt skeleton (Role / Instructions / Reasoning Steps / Output Format /
173
+ // Examples) and stresses directness; the Codex guide's style rules ask for very
174
+ // concise, grouped instructions ordered general -> specific. Hence numbered
175
+ // imperatives. Reasoning is NOT a text tag for this family — the guide uses
176
+ // `reasoning_effort` / `verbosity` parameters and separate reasoning response
177
+ // items — so the reasoning rule here asks only for a clean answer.
178
+ build: () => `${head('GPT')}
179
+
180
+ Work inside the workspace using the tools provided.
181
+
182
+ ${numbered([...REASON_ANSWER_ONLY, ...RULES.tools, ...RULES.scope, ...RULES.effort, ...RULES.quality, ...RULES.finishing])}
183
+ `,
184
+ },
185
+ {
186
+ id: 'deepseek',
187
+ label: 'DeepSeek',
188
+ sub: 'rules stated with their reason (evidence: DeepSeek API docs, thinking mode)',
189
+ // EVIDENCE: DeepSeek API docs (thinking_mode + tool_calls). Two documented facts
190
+ // drive this preset:
191
+ // 1. CoT is returned in a separate `reasoning_content` field, sibling to
192
+ // `content`, and thinking is ON by default (effort high). So the reasoning
193
+ // rule must NOT ask for a tag — the model already emits it elsewhere.
194
+ // 2. With `tools` present, `reasoning_content` MUST be echoed back on every
195
+ // subsequent request or the API returns 400. That is handled by our client,
196
+ // not the prompt, but it is why this preset does not fight the native
197
+ // reasoning channel.
198
+ // The "state the reason" style is a judgement call: the docs do not prescribe a
199
+ // prompt shape.
200
+ build: () => `${head('DeepSeek')}
201
+
202
+ Work inside the workspace using the tools provided. Each rule below states why it
203
+ exists; follow the reason, not just the wording.
204
+
205
+ ## Tools
206
+ ${bullets([
207
+ 'Read before editing — editing unread code is guessing.',
208
+ 'Prefer Edit over Write on existing files — a Write discards context you did not read.',
209
+ 'On a tool failure, read the error and fix the approach — retrying unchanged wastes a turn. After 2 failed attempts on one goal, stop and report the blocker.',
210
+ 'Parallelize independent tool calls, never dependent ones — a dependent call started early reads stale state.',
211
+ ])}
212
+
213
+ ## Reasoning
214
+ ${bullets([
215
+ // DeepSeek returns CoT in a separate `reasoning_content` field (its API docs);
216
+ // thinking is on by default. So the only rule that matters is: keep it out of
217
+ // the answer. Asking for a tag would request what the model already emits.
218
+ 'Your reasoning arrives on its own channel — do not also write it into your answer. The answer is the conclusion, not a recap of how you got there.',
219
+ ])}
220
+
221
+ ## Scope
222
+ ${bullets([
223
+ 'Do only what was asked — unrequested refactors make the diff unreviewable.',
224
+ 'Ask one short question when genuinely ambiguous — guessing wrong costs more than asking.',
225
+ 'State your assumptions — an unstated assumption is undetectable when wrong.',
226
+ ])}
227
+
228
+ ## Effort
229
+ ${bullets([
230
+ 'Stop searching once you can act — explore only as far as the next decision needs.',
231
+ 'Do the work rather than describing how it could be done, unless asked only for an opinion.',
232
+ 'Never re-read or re-search something already in this conversation.',
233
+ ])}
234
+
235
+ ## Code quality
236
+ ${bullets(RULES.quality)}
237
+
238
+ ## Finishing
239
+ ${bullets(RULES.finishing)}
240
+ `,
241
+ },
242
+ {
243
+ id: 'qwen',
244
+ label: 'Qwen',
245
+ sub: 'structured rules; closes the reasoning block (evidence: Qwen docs)',
246
+ // EVIDENCE: qwen.readthedocs.io (Key Concepts + Function Calling + Quickstart).
247
+ // The decisive documented fact: Qwen3's chat template INSERTS the opening
248
+ // reasoning tag itself, and the docs state output "may contain only the closing
249
+ // tag without an explicit opening tag". So this preset must NOT tell the model to
250
+ // emit both — doing so invites a duplicate opening tag. It also documents the
251
+ // /no_think soft switch, which the reasoning rule uses.
252
+ // Qwen also recommends Hermes-style tool calling and supports parallel calls.
253
+ build: () => `${head('Qwen')}
254
+
255
+ Work inside the workspace using the tools provided.
256
+
257
+ ## Tools
258
+ ${bullets(RULES.tools)}
259
+
260
+ ## Reasoning
261
+ ${bullets([
262
+ // Qwen3's own chat template inserts the OPENING reasoning tag; the model only
263
+ // writes the closing one. (Qwen docs: output "may contain only </...> without an
264
+ // explicit opening tag".) So we must NOT tell it to emit both — that invites a
265
+ // duplicate opening tag. We only ask for the close, plus a clean answer.
266
+ 'Close your reasoning block before the answer begins. Do not repeat the reasoning in the answer.',
267
+ 'Do not add an opening reasoning tag yourself — it is already provided.',
268
+ 'Use the token literal /no_think if you judge that a request needs no reasoning at all.',
269
+ ])}
270
+
271
+ ## Scope
272
+ ${bullets(RULES.scope)}
273
+
274
+ ## Effort
275
+ ${bullets(RULES.effort)}
276
+
277
+ ## Code quality
278
+ ${bullets(RULES.quality)}
279
+
280
+ ## Finishing
281
+ ${bullets(RULES.finishing)}
282
+
283
+ ## Output shape
284
+ Lead with the outcome in one line, then the detail. Example:
285
+
286
+ \`\`\`
287
+ Fixed the off-by-one in src/parse.js:88 — the loop used < instead of <=.
288
+ Verified: all 24 parser cases pass.
289
+ \`\`\`
290
+ `,
291
+ },
292
+ ];
293
+
294
+ // ---- Axis 2: tasks (what the prompt is optimized for) ----------------------
295
+ // Appended to the family body as its own section. Kept short: the family part
296
+ // already carries the shared behaviour rules.
297
+ //
298
+ // `label` is deliberately SHORT (≤9 chars) — it becomes a grid column header, and
299
+ // the grid runs 6 columns wide. `full` carries the readable name for the
300
+ // description line under the grid.
301
+ export const TASKS = [
302
+ {
303
+ id: 'general',
304
+ label: 'General',
305
+ full: 'General coding',
306
+ sub: 'everyday edits and questions — no extra constraints',
307
+ append: '',
308
+ },
309
+ {
310
+ id: 'build',
311
+ label: 'Build',
312
+ full: 'Build / new feature',
313
+ sub: 'adding something new; explore before you write',
314
+ append: `## This task: building something new
315
+ Look for the existing pattern before inventing one: search for a similar feature
316
+ and follow how it is structured, named and tested.
317
+ Do not add a dependency to solve something the project already has a way to do.
318
+ When you finish a piece, verify it runs — not only that it compiles.
319
+ `,
320
+ },
321
+ {
322
+ id: 'debug',
323
+ label: 'Debug',
324
+ full: 'Debug / fix a bug',
325
+ sub: 'find the root cause before changing anything',
326
+ append: `## This task: debugging
327
+ Find the root cause before editing. A change made before you understand the failure
328
+ is a guess, and a guess that appears to work is worse than one that does not.
329
+ Reproduce the failure first — a fix you cannot demonstrate is unverified.
330
+ State the causal chain: what happens, why, and what you changed to break it.
331
+ If you cannot reproduce it, say so and report what you ruled out.
332
+ `,
333
+ },
334
+ {
335
+ id: 'refactor',
336
+ label: 'Refactor',
337
+ full: 'Refactor',
338
+ sub: 'behaviour must not change',
339
+ append: `## This task: refactoring
340
+ Behaviour must not change. If you cannot verify that, say so before starting.
341
+ Keep the diff reviewable: one concern per change, no drive-by fixes.
342
+ Do not rename or move things the request did not name — every extra rename is
343
+ noise the reviewer has to verify.
344
+ `,
345
+ },
346
+ {
347
+ id: 'review',
348
+ label: 'Review',
349
+ full: 'Review / audit',
350
+ sub: 'read-only unless a change is explicitly requested',
351
+ // Adapted from codex's review rubric (prompts/templates/review/rubric.md). The
352
+ // valuable part is not the format but the FILTER: what counts as a finding at
353
+ // all. Without it a review degenerates into style notes and speculation.
354
+ append: `## This task: review
355
+ Do not edit files unless explicitly asked. This is an investigation.
356
+ Read widely before concluding — the answer is usually in code you have not opened.
357
+ Lead with the findings, ordered by severity. Keep any summary short and put it AFTER them.
358
+ Cite file and line for every claim. Separate "verified" from "looks likely".
359
+
360
+ What qualifies as a finding — all of these must hold:
361
+ - It meaningfully affects correctness, performance, security or maintainability.
362
+ - It is discrete and actionable, not a general complaint about the codebase.
363
+ - Fixing it does not demand rigour the rest of the codebase does not have.
364
+ - The author would plausibly fix it if they knew.
365
+ - It does not rest on an unstated assumption about intent — if it may be deliberate, ask instead.
366
+
367
+ Do NOT report:
368
+ - Trivial style, formatting or typos, unless they obscure meaning.
369
+ - Pre-existing problems the change did not introduce.
370
+ - Speculation that something "might" break elsewhere. Name the code that is provably affected,
371
+ or leave it out.
372
+ - Anything you cannot point at a line for.
373
+
374
+ Each finding: one short paragraph, why it is a bug, and the conditions under which it bites.
375
+ Never overstate severity. If you found nothing, say so plainly and name the residual risks and
376
+ gaps in what you checked — an empty review is a valid result.
377
+ `,
378
+ },
379
+ {
380
+ id: 'test',
381
+ label: 'Tests',
382
+ full: 'Tests',
383
+ sub: "match the project's existing test setup",
384
+ append: `## This task: tests
385
+ Match the existing test setup — framework, file naming, assertion style. If the
386
+ project has no tests, say so instead of introducing a framework unasked.
387
+ Test behaviour, not implementation detail: a test that breaks on a rename is a
388
+ liability.
389
+ A test that cannot fail is not a test — confirm it fails before the fix.
390
+ `,
391
+ },
392
+ ];
393
+
394
+ // ---- combination -----------------------------------------------------------
395
+
396
+ // Build the full prompt for a (family, task) pair. Returns '' for unknown ids so
397
+ // callers can fall back rather than write a half-formed prompt. A family whose
398
+ // build() throws or yields nothing also returns '' — better an empty editor than
399
+ // a half-written system prompt saved over the user's own.
400
+ export function buildPreset(familyId, taskId) {
401
+ const fam = FAMILIES.find((f) => f.id === familyId);
402
+ if (!fam || typeof fam.build !== 'function') return '';
403
+ const task = TASKS.find((t) => t.id === taskId);
404
+ let body = '';
405
+ try { body = String(fam.build() || ''); } catch { return ''; }
406
+ if (!body.trim()) return '';
407
+ const tail = task && task.append ? `\n${task.append}` : '';
408
+ return body + tail;
409
+ }
410
+
411
+ // Human-readable name for the editor title.
412
+ export function presetLabel(familyId, taskId) {
413
+ const fam = FAMILIES.find((f) => f.id === familyId);
414
+ const task = TASKS.find((t) => t.id === taskId);
415
+ if (!fam) return 'custom';
416
+ return task && task.id !== 'general' ? `${fam.label} + ${task.full || task.label}` : fam.label;
417
+ }
418
+
419
+ // Kept for the presets list; a flat view of every family's general prompt.
420
+ export const PROMPT_PRESETS = FAMILIES.map((f) => ({
421
+ id: f.id,
422
+ label: f.label,
423
+ sub: f.sub,
424
+ text: f.build(),
425
+ }));
426
+
427
+ export function getPreset(id) {
428
+ return PROMPT_PRESETS.find((p) => p.id === id) || null;
429
+ }