@nanhara/hara 0.102.0 → 0.104.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +34 -0
- package/dist/agent/compact.js +22 -0
- package/dist/agent/loop.js +16 -1
- package/dist/agent/touched.js +18 -0
- package/dist/gateway/feishu.js +5 -1
- package/dist/index.js +19 -2
- package/dist/providers/anthropic.js +5 -1
- package/dist/tools/agent.js +16 -3
- package/dist/tui/InputBox.js +9 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,40 @@ All notable changes to `@nanhara/hara`.
|
|
|
5
5
|
> Versioning (pre-1.0, SemVer-style): the **minor** (middle) number bumps for a **new feature**; the
|
|
6
6
|
> **patch** (last) number bumps for **optimizations/fixes of existing features**.
|
|
7
7
|
|
|
8
|
+
## 0.104.0 — compaction keeps your working files + honest context accounting
|
|
9
|
+
|
|
10
|
+
Closes the last two adopted items from the Claude Code internals study, and un-breaks the release
|
|
11
|
+
pipeline.
|
|
12
|
+
|
|
13
|
+
- **Post-compaction file restore** (CC's TW5): compaction now re-attaches the CURRENT on-disk content
|
|
14
|
+
of the files the conversation was most recently working with (top 5, byte-capped) — the summary is
|
|
15
|
+
no longer the model's only anchor, so it doesn't re-read its own working set or act on a stale
|
|
16
|
+
memory of an edited file.
|
|
17
|
+
- **Context threshold ladder + cache-aware accounting**: the footer's `ctx N%` turns yellow at 60%
|
|
18
|
+
and red at 80% (auto-compact fires at 85), so compaction never surprises you. And on Anthropic
|
|
19
|
+
endpoints, input accounting now includes cache reads/writes — cached sessions used to under-report
|
|
20
|
+
context fullness so badly that auto-compact could never fire before overflow.
|
|
21
|
+
- **Release pipeline fixed** (every `v*` tag's release workflow had been failing silently):
|
|
22
|
+
the Docker image's `npm ci` ran the `prepare → tsc` hook before src was copied; and the standalone
|
|
23
|
+
binaries died on the Feishu SDK's default import under bun's ESM resolution. Both corrected —
|
|
24
|
+
binaries + ghcr image ship again from this tag.
|
|
25
|
+
|
|
26
|
+
## 0.103.0 — the project-analysis SOP (why hara felt slow on "analyze this repo")
|
|
27
|
+
|
|
28
|
+
The execution layer could always parallelize reads and fan out read-only sub-agents — but nothing
|
|
29
|
+
TAUGHT the model, so it explored one call per turn. Distilled from codex's prompt discipline and
|
|
30
|
+
Claude Code's Explore-agent pattern:
|
|
31
|
+
|
|
32
|
+
- **System prompt playbook**: batch independent tool calls in one response (reads execute in
|
|
33
|
+
parallel); analyzing a project starts with a ONE-batch wide sweep (manifest + README + build/CI
|
|
34
|
+
config) then narrow grep/glob; more than ~3 searches → fan out `agent` sub-agents, several in one
|
|
35
|
+
response.
|
|
36
|
+
- **`agent` tool grows WHEN-TO-USE / WHEN-NOT-TO-USE guidance** (CC's heuristic): narrow lookups go
|
|
37
|
+
to direct tools; open-ended "how does X work across the codebase" goes to sub-agents.
|
|
38
|
+
- **Built-in `explore` persona** — `agent(role:"explore")` works with zero setup: read-only, parallel
|
|
39
|
+
searches, excerpts not whole files, returns conclusions with path:line refs, never dumps. A
|
|
40
|
+
user-defined explore role still wins.
|
|
41
|
+
|
|
8
42
|
## 0.102.0 — a slow network never feels dead
|
|
9
43
|
|
|
10
44
|
Jeff + a designer colleague both hit the same thing: press Enter on a slow connection and hara
|
package/dist/agent/compact.js
CHANGED
|
@@ -20,6 +20,28 @@ export const COMPACT_SYSTEM = "Summarize the conversation so far into a structur
|
|
|
20
20
|
"7. All user messages — EVERY user message so far (excluding tool results), verbatim and in order; abbreviate only huge pasted blobs with […]. These are the ground truth of intent.\n" +
|
|
21
21
|
"8. Next step — the immediate next action, INCLUDING a direct verbatim quote of the user's most recent request so there is no drift.\n" +
|
|
22
22
|
"Be specific and concrete. Drop the <analysis>; output only the headed summary.";
|
|
23
|
+
/** Post-compaction file restore (Claude Code's TW5): re-attach the CURRENT content of the files the
|
|
24
|
+
* conversation was just working with, so the summary isn't the model's only anchor — it doesn't have
|
|
25
|
+
* to re-read its own working set next turn (and can't act on a stale memory of an edited file).
|
|
26
|
+
* `readFn` is injected (returns null for unreadable/gone files); byte caps bound the token cost. */
|
|
27
|
+
export function buildFileRestore(paths, readFn, opts) {
|
|
28
|
+
const per = opts?.perFileBytes ?? 8_192;
|
|
29
|
+
let budget = opts?.totalBytes ?? 24_576;
|
|
30
|
+
const parts = [];
|
|
31
|
+
for (const p of paths) {
|
|
32
|
+
if (budget <= 0)
|
|
33
|
+
break;
|
|
34
|
+
const raw = readFn(p);
|
|
35
|
+
if (raw == null)
|
|
36
|
+
continue;
|
|
37
|
+
const clipped = raw.slice(0, Math.min(per, budget));
|
|
38
|
+
budget -= clipped.length;
|
|
39
|
+
parts.push(`--- ${p}${clipped.length < raw.length ? " (truncated)" : ""} ---\n${clipped}`);
|
|
40
|
+
}
|
|
41
|
+
if (!parts.length)
|
|
42
|
+
return null;
|
|
43
|
+
return `Files you were recently working with (CURRENT on-disk content, restored after compaction):\n\n${parts.join("\n\n")}`;
|
|
44
|
+
}
|
|
23
45
|
/** Whether to auto-compact now: enabled, the history is substantial enough to be worth summarizing, and the
|
|
24
46
|
* last turn filled the context past the threshold (so the NEXT turn would risk overflow). */
|
|
25
47
|
export function shouldAutoCompact(ctxPct, historyLen, autoCompact, threshold = AUTO_COMPACT_PCT) {
|
package/dist/agent/loop.js
CHANGED
|
@@ -13,6 +13,10 @@ import { classifyError, failoverAction, errorHint } from "./failover.js";
|
|
|
13
13
|
import { currentTodos, renderTodos } from "../tools/todo.js";
|
|
14
14
|
import { drainReminders, wrapReminders, pushReminder, todoStaleReminder, TODO_STALE_ROUNDS } from "./reminders.js";
|
|
15
15
|
import { setTurnPhase } from "./phase.js";
|
|
16
|
+
import { recordTouch } from "./touched.js";
|
|
17
|
+
import { resolve as resolvePath } from "node:path";
|
|
18
|
+
/** File tools whose `path` input marks the file as "recently worked with" (post-compaction restore). */
|
|
19
|
+
const FILE_TOUCH_TOOLS = new Set(["read_file", "edit_file", "write_file"]);
|
|
16
20
|
/** Stall watchdog ceiling: a model attempt that streams NOTHING for this long is treated as a dead /
|
|
17
21
|
* stalled connection and aborted into the normal error→failover path — instead of hanging on
|
|
18
22
|
* "working Ns" forever (the "pressed Enter, thought it failed" report). Generous default because
|
|
@@ -49,7 +53,13 @@ const HARA_SYSTEM = (cwd) => `You are hara, a coding agent running in the user's
|
|
|
49
53
|
Working directory: ${cwd}
|
|
50
54
|
Be concise and direct. Use the provided tools to read files, edit/write files, and run shell
|
|
51
55
|
commands. Prefer small, verifiable steps; edit existing files with edit_file rather than rewriting
|
|
52
|
-
them whole.
|
|
56
|
+
them whole. Batch INDEPENDENT tool calls in a single response — especially reads (read_file / grep /
|
|
57
|
+
glob / ls run in PARALLEL when requested together); one-call-per-turn exploration is the slowest thing
|
|
58
|
+
you can do. When analyzing a project, start wide in ONE batch — manifest (package.json / Cargo.toml /
|
|
59
|
+
pyproject.toml / go.mod), README, build/CI config — then chase only what the task needs with narrow
|
|
60
|
+
grep/glob; don't read whole large files when a targeted search answers the question. For broad,
|
|
61
|
+
open-ended exploration (more than ~3 searches), spawn \`agent\` sub-agents — several in one response for
|
|
62
|
+
independent questions (role "explore") — each returns conclusions, not dumps. For a multi-step task, call \`todo_write\` to plan a short checklist and keep it updated as
|
|
53
63
|
you go (one item in_progress at a time) — skip it for trivial one-step tasks. You have a persistent
|
|
54
64
|
memory: use memory_search before answering about prior decisions,
|
|
55
65
|
conventions, or the user's preferences, and memory_write to proactively save durable facts you learn.
|
|
@@ -375,6 +385,11 @@ export async function runAgent(history, opts) {
|
|
|
375
385
|
results[idx] = { id: p.tu.id, name: p.tu.name, content: pre.message, isError: true };
|
|
376
386
|
return;
|
|
377
387
|
}
|
|
388
|
+
// Track the MAIN conversation's working files for post-compaction restore (quiet fan-out
|
|
389
|
+
// sub-agents read broadly — their files aren't "what the user was working on").
|
|
390
|
+
if (!opts.quiet && FILE_TOUCH_TOOLS.has(p.tu.name) && typeof p.tu.input?.path === "string") {
|
|
391
|
+
recordTouch(resolvePath(ctx.cwd, String(p.tu.input.path)));
|
|
392
|
+
}
|
|
378
393
|
const res = await p.tool.run(p.tu.input, ctx);
|
|
379
394
|
// append any not-yet-seen subdirectory AGENTS.md/CLAUDE.md this call touched (monorepo-local conventions)
|
|
380
395
|
results[idx] = { id: p.tu.id, name: p.tu.name, content: res + subdirHint(p.tu.input, ctx.cwd) };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Recently-touched file tracker — feeds post-compaction FILE RESTORE (Claude Code's TW5 pattern).
|
|
2
|
+
// The loop records every file the MAIN conversation reads/edits; when the history is compacted to a
|
|
3
|
+
// summary, the top-N most-recent files get their CURRENT on-disk content re-attached — so the model
|
|
4
|
+
// doesn't lose the very files it was working on and re-read them all next turn.
|
|
5
|
+
const touched = new Map(); // absolute path → last-touch timestamp
|
|
6
|
+
export function recordTouch(path) {
|
|
7
|
+
touched.set(path, Date.now());
|
|
8
|
+
}
|
|
9
|
+
/** Most-recently-touched first. */
|
|
10
|
+
export function recentTouched(n = 5) {
|
|
11
|
+
return [...touched.entries()]
|
|
12
|
+
.sort((a, b) => b[1] - a[1])
|
|
13
|
+
.slice(0, n)
|
|
14
|
+
.map(([p]) => p);
|
|
15
|
+
}
|
|
16
|
+
export function clearTouched() {
|
|
17
|
+
touched.clear();
|
|
18
|
+
}
|
package/dist/gateway/feishu.js
CHANGED
|
@@ -3,7 +3,11 @@
|
|
|
3
3
|
// outbound. Creds from HARA_FEISHU_APP_ID / HARA_FEISHU_APP_SECRET (+ HARA_FEISHU_DOMAIN=lark for larksuite.com).
|
|
4
4
|
// Same ChatAdapter shape as the others, so all cross-platform gateway logic (send_file, system context,
|
|
5
5
|
// stuck-guard, image attach/describe) works unchanged. v1 = p2p (DM) only; group support is a fast-follow.
|
|
6
|
-
import
|
|
6
|
+
// Namespace import + default fallback: node resolves this SDK as CJS (default = module object), but
|
|
7
|
+
// bun's bundler resolves its ESM build (named exports only, NO default) — `import lark from` made
|
|
8
|
+
// every binaries release fail. This form works under both resolutions.
|
|
9
|
+
import * as larkNs from "@larksuiteoapi/node-sdk";
|
|
10
|
+
const lark = (larkNs.default ?? larkNs);
|
|
7
11
|
import { createReadStream, createWriteStream, mkdirSync } from "node:fs";
|
|
8
12
|
import { pipeline } from "node:stream/promises";
|
|
9
13
|
import { join, basename } from "node:path";
|
package/dist/index.js
CHANGED
|
@@ -24,7 +24,8 @@ import { clearEnrollment, enrollDevice, heartbeat, syncOrgRoles } from "./org-fl
|
|
|
24
24
|
import { loadActiveProfile, listProfiles, useProfile, addProfile, upsertProfile, removeProfile, setModel as setProfileModel, resetModel as resetProfileModel, getProfile, effectiveModel, routingLabel, routeHost, activeId, resolveActive, setFlagOverride, writePin, removePin, pinFilePath, DEFAULT_ORG_ID, PERSONAL_ID, } from "./profile/profile.js";
|
|
25
25
|
import { loadPermissionRules, scaffoldPermissions, globalPermissionsPath, projectPermissionsPath } from "./security/permissions.js";
|
|
26
26
|
import { routingProvider } from "./agent/route.js";
|
|
27
|
-
import { shouldAutoCompact, COMPACT_SYSTEM } from "./agent/compact.js";
|
|
27
|
+
import { shouldAutoCompact, COMPACT_SYSTEM, buildFileRestore } from "./agent/compact.js";
|
|
28
|
+
import { recentTouched } from "./agent/touched.js";
|
|
28
29
|
import { checkForUpdate } from "./update-check.js";
|
|
29
30
|
import { formatContextReport } from "./agent/context-report.js";
|
|
30
31
|
import { userTurnPreviews, rewindTo } from "./agent/rewind.js";
|
|
@@ -36,6 +37,7 @@ import { addJob, removeJob, setEnabled, resolveJob, loadJobs, recordRun, logPath
|
|
|
36
37
|
import { runTick, runJobOnce, selfArgv } from "./cron/runner.js";
|
|
37
38
|
import { installScheduler, uninstallScheduler, isInstalled } from "./cron/install.js";
|
|
38
39
|
import { getTools } from "./tools/registry.js";
|
|
40
|
+
import { EXPLORE_SYSTEM } from "./tools/agent.js";
|
|
39
41
|
import { createAnthropicProvider } from "./providers/anthropic.js";
|
|
40
42
|
import { createOpenAIProvider } from "./providers/openai.js";
|
|
41
43
|
import { qwenDeviceLogin, getValidQwenAuth } from "./providers/qwen-oauth.js";
|
|
@@ -743,6 +745,18 @@ async function compactConversation(provider, history, meta, stats) {
|
|
|
743
745
|
meta.workingSet = workingSetFromSummary(summary); // survives the history wipe + injects into the next turns
|
|
744
746
|
history.length = 0;
|
|
745
747
|
history.push({ role: "user", content: `Summary of our conversation so far (continue from here):\n\n${summary}` });
|
|
748
|
+
// TW5-style file restore: the summary alone loses the working set's ACTUAL content — re-attach the
|
|
749
|
+
// most recently touched files (current on-disk state, byte-capped) so work continues without re-reads.
|
|
750
|
+
const restore = buildFileRestore(recentTouched(5), (p) => {
|
|
751
|
+
try {
|
|
752
|
+
return readFileSync(p, "utf8");
|
|
753
|
+
}
|
|
754
|
+
catch {
|
|
755
|
+
return null;
|
|
756
|
+
}
|
|
757
|
+
});
|
|
758
|
+
if (restore)
|
|
759
|
+
history.push({ role: "user", content: restore });
|
|
746
760
|
stats.input += r.usage?.input ?? 0;
|
|
747
761
|
stats.output += r.usage?.output ?? 0;
|
|
748
762
|
stats.lastInput = r.usage?.input ?? 0; // ctx% now reflects the (small) summary, not the old full turn
|
|
@@ -765,6 +779,9 @@ async function maybeAutoCompact(provider, history, meta, stats, cfg, notify) {
|
|
|
765
779
|
async function runSubagent(cfg, baseProvider, cwd, sandbox, projectContext, stats, task, roleId) {
|
|
766
780
|
const roles = loadRoles(cwd);
|
|
767
781
|
const role = roleId ? roles.find((r) => r.id === roleId) : undefined;
|
|
782
|
+
// Built-in explore persona: `agent(role:"explore")` works with ZERO user setup (a user-defined
|
|
783
|
+
// explore role still wins — it was found above and carries its own system).
|
|
784
|
+
const builtinSystem = !role && roleId === "explore" ? EXPLORE_SYSTEM : undefined;
|
|
768
785
|
const __subModel = effectiveRoleModel(role?.model, cfg.model);
|
|
769
786
|
const provider = __subModel ? ((await buildProvider({ ...cfg, model: __subModel })) ?? baseProvider) : baseProvider;
|
|
770
787
|
// A sub-agent runs full-auto + UNCONFIRMED + parallel, so it is ALWAYS read-only — a role may narrow
|
|
@@ -780,7 +797,7 @@ async function runSubagent(cfg, baseProvider, cwd, sandbox, projectContext, stat
|
|
|
780
797
|
projectContext,
|
|
781
798
|
memory: memoryDigest(cwd),
|
|
782
799
|
stats,
|
|
783
|
-
systemOverride: role?.system,
|
|
800
|
+
systemOverride: role?.system ?? builtinSystem,
|
|
784
801
|
toolFilter,
|
|
785
802
|
quiet: true,
|
|
786
803
|
});
|
|
@@ -113,7 +113,11 @@ export function createAnthropicProvider(opts) {
|
|
|
113
113
|
.filter((b) => b.type === "tool_use")
|
|
114
114
|
.map((b) => ({ id: b.id, name: b.name, input: b.input }));
|
|
115
115
|
const stop = msg.stop_reason === "tool_use" ? "tool_use" : "end";
|
|
116
|
-
|
|
116
|
+
// Cache-aware input accounting: Anthropic's input_tokens EXCLUDES cache reads/writes, so a
|
|
117
|
+
// cached session under-reported context fullness badly (ctx% stayed tiny → auto-compact never
|
|
118
|
+
// fired → overflow). Total context = fresh + cache_creation + cache_read (CC's zY5 equivalent).
|
|
119
|
+
const u = msg.usage;
|
|
120
|
+
const usage = { input: (u?.input_tokens ?? 0) + (u?.cache_creation_input_tokens ?? 0) + (u?.cache_read_input_tokens ?? 0), output: u?.output_tokens ?? 0 };
|
|
117
121
|
return { text, toolUses, stop, usage };
|
|
118
122
|
},
|
|
119
123
|
};
|
package/dist/tools/agent.js
CHANGED
|
@@ -2,11 +2,24 @@
|
|
|
2
2
|
// turn run in PARALLEL (kind "read" → concurrent), making the footer's ⛁ count real. Sub-agents are
|
|
3
3
|
// read-only by default (safe to parallelize); the actual spawn is provided via ctx.spawn.
|
|
4
4
|
import { registerTool } from "./registry.js";
|
|
5
|
+
/** Built-in persona for `role: "explore"` (no setup needed — index.ts falls back to this when the
|
|
6
|
+
* user hasn't defined an explore role). Claude-Code's Explore-agent playbook: read-only, parallel,
|
|
7
|
+
* excerpts-not-files, conclusions-not-dumps. */
|
|
8
|
+
export const EXPLORE_SYSTEM = "You are a fast, READ-ONLY codebase explorer. Navigate with grep/glob/ls/read_file and be quick: " +
|
|
9
|
+
"issue your searches and file reads as MULTIPLE PARALLEL tool calls in one round whenever they are " +
|
|
10
|
+
"independent — never one-per-turn. Read targeted excerpts, not whole files. You cannot modify anything. " +
|
|
11
|
+
"Answer with CONCLUSIONS: the finding, the relevant paths with line references, and what they mean for " +
|
|
12
|
+
"the question — never dump raw file contents. Match your depth to the task: a quick lookup stays quick; " +
|
|
13
|
+
"an architecture question deserves a thorough sweep across naming conventions and directories.";
|
|
5
14
|
registerTool({
|
|
6
15
|
name: "agent",
|
|
7
|
-
description: "Delegate an independent sub-task to a fresh sub-agent and get its
|
|
8
|
-
"
|
|
9
|
-
"
|
|
16
|
+
description: "Delegate an independent sub-task to a fresh READ-ONLY sub-agent and get its conclusions. " +
|
|
17
|
+
"WHEN TO USE: open-ended exploration ('how does X work across the codebase', 'find everything that touches Y') " +
|
|
18
|
+
"that would take more than ~3 searches — pass role \"explore\" for a fast search specialist; and spawning " +
|
|
19
|
+
"SEVERAL agents in ONE response for independent questions (they run in parallel). " +
|
|
20
|
+
"WHEN NOT TO USE: reading a specific known file (read_file), finding one symbol (grep), or searching " +
|
|
21
|
+
"within 2-3 known files — direct tools are faster. Never for edits. " +
|
|
22
|
+
"Pass a `role` id to use that role's persona + tools.",
|
|
10
23
|
input_schema: {
|
|
11
24
|
type: "object",
|
|
12
25
|
properties: {
|
package/dist/tui/InputBox.js
CHANGED
|
@@ -54,21 +54,25 @@ export function footerParts(model, s, cwdShort, route) {
|
|
|
54
54
|
return {
|
|
55
55
|
prefix: ` ${model} · `,
|
|
56
56
|
mode: s.approval,
|
|
57
|
-
suffix: `${routeSeg} · ${cwdShort} · ↑${tok(s.input)} ↓${tok(s.output)} ·
|
|
57
|
+
suffix: `${routeSeg} · ${cwdShort} · ↑${tok(s.input)} ↓${tok(s.output)} · `,
|
|
58
|
+
ctx: `ctx ${s.ctxPct}%`,
|
|
59
|
+
// Claude Code's threshold ladder (60 warn / 80 error / 92 compact): hara auto-compacts at 85, so
|
|
60
|
+
// the footer escalates BEFORE that — yellow at 60, red at 80 — and the user sees compaction coming.
|
|
61
|
+
ctxLevel: s.ctxPct >= 80 ? "high" : s.ctxPct >= 60 ? "warn" : "ok",
|
|
58
62
|
};
|
|
59
63
|
}
|
|
60
|
-
/** Back-compat: the full footer as one string
|
|
64
|
+
/** Back-compat: the full footer as one string. Kept for any pure consumer/test. */
|
|
61
65
|
export function footerLine(model, s, cwdShort, route) {
|
|
62
66
|
const p = footerParts(model, s, cwdShort, route);
|
|
63
|
-
return p.prefix + p.mode + p.suffix;
|
|
67
|
+
return p.prefix + p.mode + p.suffix + p.ctx;
|
|
64
68
|
}
|
|
65
69
|
// The merged status footer: model · approval · route · cwd · usage · ctx. The active approval mode is
|
|
66
70
|
// colored inline (the always-on ModeBar is gone — shift+tab now pops a transient selector instead), so
|
|
67
71
|
// the outer <Text> is NOT dim: only the prefix/suffix are dimmed and the mode token stays bright.
|
|
68
72
|
// Memoized so a prompt keystroke doesn't reconcile it.
|
|
69
73
|
const Footer = memo(function Footer({ model, s, cwdShort, route }) {
|
|
70
|
-
const { prefix, mode, suffix } = footerParts(model, s, cwdShort, route);
|
|
71
|
-
return (_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: prefix }), _jsx(Text, { color: approvalColor(s.approval), bold: true, children: mode }), _jsx(Text, { dimColor: true, children: suffix })] }));
|
|
74
|
+
const { prefix, mode, suffix, ctx, ctxLevel } = footerParts(model, s, cwdShort, route);
|
|
75
|
+
return (_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: prefix }), _jsx(Text, { color: approvalColor(s.approval), bold: true, children: mode }), _jsx(Text, { dimColor: true, children: suffix }), ctxLevel === "ok" ? _jsx(Text, { dimColor: true, children: ctx }) : _jsx(Text, { color: ctxLevel === "high" ? "red" : "yellow", bold: true, children: ctx })] }));
|
|
72
76
|
});
|
|
73
77
|
// The rounded TOP edge of the input box, carrying the session name in the right corner (it "rides"
|
|
74
78
|
// the border — codex-style titled panel, and where hara has always shown it). Drawn by hand because
|