@balacode/mental 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/.claude-plugin/marketplace.json +17 -0
  2. package/.claude-plugin/plugin.json +22 -0
  3. package/.cursor-plugin/plugin.json +21 -0
  4. package/.mcp.json +8 -0
  5. package/CHANGELOG.md +42 -0
  6. package/LICENSE +21 -0
  7. package/README.md +277 -0
  8. package/assets/logo.svg +19 -0
  9. package/bin/cli.mjs +135 -0
  10. package/bin/commands/attention.mjs +139 -0
  11. package/bin/commands/decide.mjs +104 -0
  12. package/bin/commands/doctor.mjs +150 -0
  13. package/bin/commands/heartbeat.mjs +21 -0
  14. package/bin/commands/hooks.mjs +41 -0
  15. package/bin/commands/install.mjs +86 -0
  16. package/bin/commands/journal.mjs +54 -0
  17. package/bin/commands/link.mjs +18 -0
  18. package/bin/commands/list.mjs +51 -0
  19. package/bin/commands/local.mjs +118 -0
  20. package/bin/commands/note.mjs +61 -0
  21. package/bin/commands/reindex.mjs +48 -0
  22. package/bin/commands/remap.mjs +76 -0
  23. package/bin/commands/search.mjs +55 -0
  24. package/bin/commands/serve.mjs +16 -0
  25. package/bin/commands/show.mjs +61 -0
  26. package/bin/commands/split.mjs +56 -0
  27. package/bin/commands/status.mjs +136 -0
  28. package/bin/commands/uninstall.mjs +58 -0
  29. package/bin/commands/where.mjs +29 -0
  30. package/bin/lib/args.mjs +117 -0
  31. package/bin/lib/bindings.mjs +404 -0
  32. package/bin/lib/entry.mjs +35 -0
  33. package/bin/lib/git.mjs +149 -0
  34. package/bin/lib/heartbeat.mjs +118 -0
  35. package/bin/lib/hooks.mjs +144 -0
  36. package/bin/lib/ignore.mjs +122 -0
  37. package/bin/lib/import-legacy.mjs +183 -0
  38. package/bin/lib/index.mjs +574 -0
  39. package/bin/lib/install-cli.mjs +100 -0
  40. package/bin/lib/install-skills.mjs +120 -0
  41. package/bin/lib/mcp.mjs +389 -0
  42. package/bin/lib/okf.mjs +746 -0
  43. package/bin/lib/output.mjs +112 -0
  44. package/bin/lib/pkg.mjs +22 -0
  45. package/bin/lib/resolve.mjs +302 -0
  46. package/bin/lib/uninstall.mjs +56 -0
  47. package/hooks/session-start.sh +4 -0
  48. package/mcp.json +11 -0
  49. package/package.json +43 -0
  50. package/plugin.json +21 -0
  51. package/rules/mental.mdc +18 -0
  52. package/skills/mental/SKILL.md +277 -0
  53. package/skills/mental/references/templates.md +186 -0
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Stable agent JSON envelope + human printers.
3
+ * Emoji is TTY-only. `--json` never includes a brand mark.
4
+ */
5
+
6
+ /**
7
+ * `MENTAL_ASCII=1` for consoles that cannot render emoji (legacy cmd.exe).
8
+ * @param {NodeJS.ProcessEnv} [env]
9
+ */
10
+ export function useAsciiBrand(env = process.env) {
11
+ const v = env.MENTAL_ASCII;
12
+ return v === "1" || v === "true" || v === "yes";
13
+ }
14
+
15
+ /**
16
+ * @param {NodeJS.ProcessEnv} [env]
17
+ */
18
+ export function brandMark(env = process.env) {
19
+ return useAsciiBrand(env) ? "[mental]" : "🧠";
20
+ }
21
+
22
+ const KIND_EMOJI = {
23
+ journal: "📓",
24
+ attention: "🚦",
25
+ decision: "🎯",
26
+ note: "📝",
27
+ read: "🔍",
28
+ };
29
+
30
+ const KIND_ASCII = {
31
+ journal: "[journal]",
32
+ attention: "[attention]",
33
+ decision: "[decision]",
34
+ note: "[note]",
35
+ read: "[read]",
36
+ };
37
+
38
+ /**
39
+ * Type mark for TTY writes/reads. `--json` must not call this.
40
+ * @param {"journal" | "attention" | "decision" | "note" | "read"} kind
41
+ * @param {NodeJS.ProcessEnv} [env]
42
+ */
43
+ export function kindMark(kind, env = process.env) {
44
+ if (useAsciiBrand(env)) return KIND_ASCII[kind] || "[mental]";
45
+ return KIND_EMOJI[kind] || brandMark(env);
46
+ }
47
+
48
+ /**
49
+ * @param {"journal" | "attention" | "decision" | "note" | "read"} kind
50
+ * @param {string} text
51
+ * @param {NodeJS.ProcessEnv} [env]
52
+ */
53
+ export function kindLine(kind, text, env = process.env) {
54
+ return `${kindMark(kind, env)} ${text}`;
55
+ }
56
+
57
+ /**
58
+ * Prefix a TTY success line with the Mental mark.
59
+ * @param {string} text
60
+ * @param {NodeJS.ProcessEnv} [env]
61
+ */
62
+ export function brandLine(text, env = process.env) {
63
+ return `${brandMark(env)} ${text}`;
64
+ }
65
+
66
+ /**
67
+ * @param {boolean} ok
68
+ * @param {object} [data]
69
+ * @param {{ code: string, message: string }} [error]
70
+ */
71
+ export function envelope(ok, data, error) {
72
+ return ok ? { ok: true, data } : { ok: false, error };
73
+ }
74
+
75
+ /**
76
+ * @param {NodeJS.WritableStream} out
77
+ * @param {boolean} json
78
+ * @param {boolean} ok
79
+ * @param {object} [data]
80
+ * @param {{ code: string, message: string }} [error]
81
+ * @param {(data: object) => string} [format]
82
+ */
83
+ export function printResult(out, json, ok, data, error, format) {
84
+ if (json) {
85
+ out.write(`${JSON.stringify(envelope(ok, data, error))}\n`);
86
+ return;
87
+ }
88
+ if (!ok) {
89
+ out.write(`${error?.message || "error"}\n`);
90
+ return;
91
+ }
92
+ out.write(`${format ? format(data) : JSON.stringify(data, null, 2)}\n`);
93
+ }
94
+
95
+ /** @param {import('./resolve.mjs').WhereData} data */
96
+ export function formatWhere(data) {
97
+ const id = data.id ?? "—";
98
+ const lines = [
99
+ `root: ${data.root}`,
100
+ `id: ${id}`,
101
+ `mode: ${data.mode}`,
102
+ `reason: ${data.reason}`,
103
+ `gitRoot: ${data.gitRoot ?? "—"}`,
104
+ ];
105
+ if (data.imported?.copied?.length) {
106
+ lines.push(`imported: ${data.imported.copied.length} file(s) from ${data.imported.from}`);
107
+ }
108
+ if (data.indexed?.ok) {
109
+ lines.push(`index: ${data.indexed.concepts} concept(s) → ${data.indexed.path}`);
110
+ }
111
+ return lines.join("\n");
112
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Package identity shared across the CLI.
3
+ */
4
+ import { readFileSync } from "node:fs";
5
+ import { join } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+
8
+ /** Absolute path to the package root (parent of `bin/`). */
9
+ export const PKG_ROOT = fileURLToPath(new URL("../..", import.meta.url));
10
+
11
+ const pkg = JSON.parse(readFileSync(join(PKG_ROOT, "package.json"), "utf8"));
12
+
13
+ export const NAME = pkg.name;
14
+ export const CMD = Object.keys(pkg.bin ?? {})[0] ?? "mental";
15
+ export const VERSION = pkg.version;
16
+
17
+ export const SKILLS_DIR = join(PKG_ROOT, "skills");
18
+ export const RULES_DIR = join(PKG_ROOT, "rules");
19
+
20
+ /** Managed-block markers for user AGENTS.md / CLAUDE.md. */
21
+ export const BEGIN = `<!-- BEGIN ${CMD} (managed — edits inside are overwritten on reinstall) -->`;
22
+ export const END = `<!-- END ${CMD} -->`;
@@ -0,0 +1,302 @@
1
+ /**
2
+ * Deterministic active-bundle resolution. Exclusive nearest-wins — no overlay.
3
+ *
4
+ * Order: --dir / MENTAL_DIR → opted-in ./.mental/ (`mental local`) → git binding
5
+ * → personal ~/.mental.
6
+ *
7
+ * Leftover Balakit `./.mental` (no `.mental-local` marker) is imported into
8
+ * `~/.mental/projects/<uuid>/` on write resolve. Source is never deleted.
9
+ */
10
+ import { statSync } from "node:fs";
11
+ import { dirname, join, parse, resolve } from "node:path";
12
+ import { findGitRoot, getRemoteUrl } from "./git.mjs";
13
+ import {
14
+ projectSliceDir,
15
+ recordLegacyImport,
16
+ resolveOrCreateBinding,
17
+ userMentalDir,
18
+ } from "./bindings.mjs";
19
+ import { importLegacyBundle, isOptedInLocal } from "./import-legacy.mjs";
20
+ import { reindexBundle } from "./index.mjs";
21
+
22
+ /**
23
+ * @typedef {'env' | 'local' | 'home' | 'personal'} ResolveMode
24
+ *
25
+ * @typedef {{
26
+ * from: string,
27
+ * to: string,
28
+ * copied: string[],
29
+ * skipped: number,
30
+ * error?: string,
31
+ * }} ImportResult
32
+ *
33
+ * @typedef {{
34
+ * root: string,
35
+ * id: string | null,
36
+ * mode: ResolveMode,
37
+ * reason: string,
38
+ * gitRoot: string | null,
39
+ * imported?: ImportResult | null,
40
+ * indexed?: { ok: boolean, path: string | null, concepts: number, backend: string, error?: string },
41
+ * }} WhereData
42
+ */
43
+
44
+ function isDir(p) {
45
+ try {
46
+ return statSync(p).isDirectory();
47
+ } catch {
48
+ return false;
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Walk cwd → git root (or filesystem root, stop at $HOME) for a project
54
+ * `.mental/` directory. Never treat the user store `~/.mental` as a local
55
+ * project bundle (that would silently overlay every non-git cwd under home).
56
+ *
57
+ * @param {string} cwd
58
+ * @param {{ home: string, gitRoot: string | null }} opts
59
+ * @returns {string | null} absolute path to the `.mental` directory
60
+ */
61
+ export function findLocalMental(cwd, { home, gitRoot }) {
62
+ const start = resolve(cwd);
63
+ const homeAbs = resolve(home);
64
+ const userStore = resolve(userMentalDir(home));
65
+ const gitAbs = gitRoot ? resolve(gitRoot) : null;
66
+ const { root: fsRoot } = parse(start);
67
+
68
+ let dir = start;
69
+ while (true) {
70
+ const candidate = join(dir, ".mental");
71
+ if (isDir(candidate)) {
72
+ const abs = resolve(candidate);
73
+ if (abs !== userStore) return abs;
74
+ }
75
+
76
+ if (gitAbs && dir === gitAbs) break;
77
+ if (dir === homeAbs) break;
78
+ if (dir === fsRoot) break;
79
+
80
+ const parent = dirname(dir);
81
+ if (parent === dir) break;
82
+ dir = parent;
83
+ }
84
+ return null;
85
+ }
86
+
87
+ /**
88
+ * @param {{
89
+ * gitRoot: string,
90
+ * home: string,
91
+ * env: NodeJS.ProcessEnv,
92
+ * write: boolean,
93
+ * }} opts
94
+ */
95
+ function bindGit({ gitRoot, home, env, write }) {
96
+ const origin = getRemoteUrl(gitRoot, "origin", { env });
97
+ const upstream = getRemoteUrl(gitRoot, "upstream", { env });
98
+ return resolveOrCreateBinding({
99
+ gitRoot,
100
+ origin,
101
+ upstream,
102
+ home,
103
+ write,
104
+ });
105
+ }
106
+
107
+ /**
108
+ * Copy leftover `./.mental` into the UUID slice. Fail open: import errors
109
+ * do not block resolve.
110
+ *
111
+ * @param {{ leftover: string, dest: string, home: string, id: string }} opts
112
+ * @returns {ImportResult}
113
+ */
114
+ function importLeftover({ leftover, dest, home, id }) {
115
+ try {
116
+ const result = importLegacyBundle(leftover, dest);
117
+ recordLegacyImport(home, id, leftover, { copied: result.copied });
118
+ return result;
119
+ } catch (err) {
120
+ return {
121
+ from: leftover,
122
+ to: dest,
123
+ copied: [],
124
+ skipped: 0,
125
+ error: err instanceof Error ? err.message : String(err),
126
+ };
127
+ }
128
+ }
129
+
130
+ /**
131
+ * @param {WhereData} data
132
+ * @param {{ write: boolean, home: string, env: NodeJS.ProcessEnv }} ctx
133
+ */
134
+ function withIndex(data, { write, home, env }) {
135
+ if (write && data.id && home) {
136
+ data.indexed = reindexBundle({ root: data.root, id: data.id, home, env });
137
+ }
138
+ return { ok: true, data };
139
+ }
140
+
141
+ /**
142
+ * @param {{
143
+ * cwd?: string,
144
+ * home?: string | null,
145
+ * env?: NodeJS.ProcessEnv,
146
+ * dir?: string | null,
147
+ * write?: boolean,
148
+ * }} [opts]
149
+ * @returns {{ ok: true, data: WhereData } | { ok: false, error: { code: string, message: string } }}
150
+ */
151
+ export function resolveBundle({
152
+ cwd = process.cwd(),
153
+ home = process.env.HOME ?? process.env.USERPROFILE ?? null,
154
+ env = process.env,
155
+ dir = null,
156
+ write = true,
157
+ } = {}) {
158
+ const gitRoot = findGitRoot(cwd, { env });
159
+
160
+ const dirOverride = dir || env.MENTAL_DIR || null;
161
+ if (dirOverride) {
162
+ const abs = resolve(cwd, dirOverride);
163
+ if (!isDir(abs)) {
164
+ return {
165
+ ok: false,
166
+ error: {
167
+ code: "env-dir-missing",
168
+ message: `MENTAL_DIR / --dir is not a directory: ${abs}`,
169
+ },
170
+ };
171
+ }
172
+ return {
173
+ ok: true,
174
+ data: {
175
+ root: abs,
176
+ id: null,
177
+ mode: "env",
178
+ reason: dir ? `--dir ${abs}` : `MENTAL_DIR=${abs}`,
179
+ gitRoot,
180
+ },
181
+ };
182
+ }
183
+
184
+ if (!home) {
185
+ return {
186
+ ok: false,
187
+ error: {
188
+ code: "no-home",
189
+ message: "HOME is unset; Mental will not write. Set HOME or MENTAL_DIR.",
190
+ },
191
+ };
192
+ }
193
+
194
+ const leftover = findLocalMental(cwd, { home, gitRoot });
195
+ const optedIn = Boolean(leftover && isOptedInLocal(leftover));
196
+
197
+ if (optedIn && leftover) {
198
+ let id = null;
199
+ let reason = `walk-up found opted-in ${leftover}`;
200
+ if (gitRoot) {
201
+ try {
202
+ const bound = bindGit({ gitRoot, home, env, write });
203
+ if (!bound.ok) {
204
+ return { ok: false, error: { code: bound.code, message: bound.message } };
205
+ }
206
+ if (bound.id) {
207
+ id = bound.id;
208
+ reason = `${reason}; ${bound.reason}`;
209
+ }
210
+ } catch (err) {
211
+ return {
212
+ ok: false,
213
+ error: {
214
+ code: "bindings",
215
+ message: err instanceof Error ? err.message : String(err),
216
+ },
217
+ };
218
+ }
219
+ }
220
+ return withIndex(
221
+ {
222
+ root: leftover,
223
+ id,
224
+ mode: "local",
225
+ reason,
226
+ gitRoot,
227
+ },
228
+ { write, home, env },
229
+ );
230
+ }
231
+
232
+ if (gitRoot) {
233
+ try {
234
+ const bound = bindGit({ gitRoot, home, env, write });
235
+ if (!bound.ok) {
236
+ return { ok: false, error: { code: bound.code, message: bound.message } };
237
+ }
238
+ if (!bound.id) {
239
+ return {
240
+ ok: true,
241
+ data: {
242
+ root: join(userMentalDir(home), "projects"),
243
+ id: null,
244
+ mode: "home",
245
+ reason: leftover
246
+ ? `${bound.reason}; leftover ${leftover} imports on next write`
247
+ : bound.reason,
248
+ gitRoot,
249
+ },
250
+ };
251
+ }
252
+ const dest = projectSliceDir(home, bound.id);
253
+ /** @type {ImportResult | null} */
254
+ let imported = null;
255
+ let reason = bound.reason;
256
+ if (write && leftover) {
257
+ imported = importLeftover({ leftover, dest, home, id: bound.id });
258
+ const n = imported.copied.length;
259
+ reason =
260
+ n > 0
261
+ ? `${bound.reason}; imported ${n} leftover file(s) from ${leftover}`
262
+ : imported.error
263
+ ? `${bound.reason}; leftover import failed: ${imported.error}`
264
+ : `${bound.reason}; leftover ${leftover} already imported`;
265
+ } else if (leftover) {
266
+ reason = `${bound.reason}; leftover ${leftover} imports on next write`;
267
+ }
268
+ return withIndex(
269
+ {
270
+ root: dest,
271
+ id: bound.id,
272
+ mode: "home",
273
+ reason,
274
+ gitRoot,
275
+ imported,
276
+ },
277
+ { write, home, env },
278
+ );
279
+ } catch (err) {
280
+ return {
281
+ ok: false,
282
+ error: {
283
+ code: "bindings",
284
+ message: err instanceof Error ? err.message : String(err),
285
+ },
286
+ };
287
+ }
288
+ }
289
+
290
+ return {
291
+ ok: true,
292
+ data: {
293
+ root: userMentalDir(home),
294
+ id: null,
295
+ mode: "personal",
296
+ reason: leftover
297
+ ? `not a git repo; leftover ${leftover} not imported (need a git root)`
298
+ : "not a git repo; personal ~/.mental",
299
+ gitRoot: null,
300
+ },
301
+ };
302
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Remove Mental skill+rule copies from user agent dirs. Never deletes OKF
3
+ * unless the uninstall command also gets `--delete-data DELETE`.
4
+ */
5
+ import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
6
+ import { join } from "node:path";
7
+ import { BEGIN, END } from "./pkg.mjs";
8
+ import { userInstallTargets } from "./install-skills.mjs";
9
+
10
+ function esc(s) {
11
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
12
+ }
13
+
14
+ /**
15
+ * Strip the managed BEGIN/END block from AGENTS.md / CLAUDE.md.
16
+ * @param {string} file
17
+ */
18
+ export function removeManaged(file) {
19
+ if (!existsSync(file)) return false;
20
+ const cur = readFileSync(file, "utf8");
21
+ if (!cur.includes(BEGIN) || !cur.includes(END)) return false;
22
+ const re = new RegExp(`\\n?${esc(BEGIN)}[\\s\\S]*?${esc(END)}\\n?`);
23
+ const next = cur.replace(re, "\n").replace(/^\n+/, "").replace(/\n{3,}/g, "\n\n");
24
+ writeFileSync(file, next);
25
+ return true;
26
+ }
27
+
28
+ /**
29
+ * @param {{ home: string, projectDir?: string | null }} opts
30
+ */
31
+ export function uninstallSkills({ home, projectDir = null }) {
32
+ const targets = userInstallTargets(home);
33
+ /** @type {string[]} */
34
+ const removed = [];
35
+ for (const dest of targets.skills) {
36
+ if (existsSync(dest)) {
37
+ rmSync(dest, { recursive: true, force: true });
38
+ removed.push(dest);
39
+ }
40
+ }
41
+ if (existsSync(targets.cursorRule)) {
42
+ rmSync(targets.cursorRule, { force: true });
43
+ removed.push(targets.cursorRule);
44
+ }
45
+ for (const doc of targets.managedDocs) {
46
+ if (removeManaged(doc)) removed.push(doc);
47
+ }
48
+ if (projectDir) {
49
+ const vendored = join(projectDir, ".github", "skills", "mental");
50
+ if (existsSync(vendored)) {
51
+ rmSync(vendored, { recursive: true, force: true });
52
+ removed.push(vendored);
53
+ }
54
+ }
55
+ return { ok: true, removed };
56
+ }
@@ -0,0 +1,4 @@
1
+ #!/bin/sh
2
+ # Optional session-start hook. Default off — enable later with `mental hooks on`.
3
+ # Caps output so agent context stays small.
4
+ mental status --json 2>/dev/null | head -c 4096
package/mcp.json ADDED
@@ -0,0 +1,11 @@
1
+ {
2
+ "$schema": "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json",
3
+ "mcpServers": {
4
+ "mental": {
5
+ "type": "stdio",
6
+ "command": "./bin/cli.mjs",
7
+ "args": ["serve"],
8
+ "cwd": "${PLUGIN_ROOT}"
9
+ }
10
+ }
11
+ }
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@balacode/mental",
3
+ "version": "0.2.0",
4
+ "description": "Never reconstruct where you left off. Mental keeps the resume, the decisions, and what's still in the air.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "mental": "bin/cli.mjs"
9
+ },
10
+ "engines": {
11
+ "node": ">=18"
12
+ },
13
+ "scripts": {
14
+ "test": "node --test test/*.test.mjs",
15
+ "mental": "node bin/cli.mjs"
16
+ },
17
+ "files": [
18
+ "bin",
19
+ "skills",
20
+ "rules",
21
+ "hooks",
22
+ "plugin.json",
23
+ "mcp.json",
24
+ "assets",
25
+ ".cursor-plugin",
26
+ ".claude-plugin",
27
+ ".mcp.json",
28
+ "README.md",
29
+ "CHANGELOG.md",
30
+ "LICENSE"
31
+ ],
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://github.com/afaraha8403/mental.git"
35
+ },
36
+ "homepage": "https://github.com/afaraha8403/mental#readme",
37
+ "bugs": {
38
+ "url": "https://github.com/afaraha8403/mental/issues"
39
+ },
40
+ "publishConfig": {
41
+ "access": "public"
42
+ }
43
+ }
package/plugin.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
+ "name": "mental",
4
+ "version": "0.2.0",
5
+ "description": "Never reconstruct where you left off. Mental keeps the resume, the decisions, and what's still in the air — so you and your agents continue as if the last session never ended.",
6
+ "author": {
7
+ "name": "Ali Farahat",
8
+ "url": "https://github.com/afaraha8403"
9
+ },
10
+ "homepage": "https://github.com/afaraha8403/mental#readme",
11
+ "repository": "https://github.com/afaraha8403/mental",
12
+ "license": "MIT",
13
+ "keywords": [
14
+ "continuity",
15
+ "journal",
16
+ "decisions",
17
+ "okf",
18
+ "skills",
19
+ "mcp"
20
+ ]
21
+ }
@@ -0,0 +1,18 @@
1
+ ---
2
+ description: Project continuity is Mental. Orient and hand off via the CLI; never grep OKF.
3
+ alwaysApply: true
4
+ ---
5
+
6
+ Continuity is Mental. On start/finish of real work, or orientation questions, use the Mental skill.
7
+
8
+ Run `mental where` then `mental heartbeat --json` (or `mental status --json` when notes are needed). Do not grep `.mental` or `~/.mental`. Do not parse YAML frontmatter yourself.
9
+
10
+ Mid-chat, not just start/finish: before changing an approach, `mental search "…" --json` for prior decisions. When residue surfaces ("X said…", a concern, "park this"), record `mental attention … --json` immediately — do not wait for the handoff. If other agents may have written since you oriented, re-pulse `mental heartbeat --json`; it is cheap and derives git live.
11
+
12
+ If `mental` is not on PATH, try `npx @balacode/mental …`. If that fails, continue the user's coding task and mention install. Missing Mental must not block work.
13
+
14
+ Never commit Mental data. Never write secrets. Never edit gitignore; tell the user to run `mental doctor`.
15
+
16
+ If you invoked `mental` this turn, end the user-visible reply with `<br>`, then the title `🧠 Mental`, then type lines under it (example in the Mental skill). Not a code fence.
17
+
18
+ On a pasted transcript or a plan-progress question ("where in the plan / what's left?"), use the Mental skill. Cheap reload is `mental heartbeat --json`, not a notes dump.