meetsoma 0.1.6 → 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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,32 @@ All notable changes to Soma (`meetsoma` on npm).
4
4
 
5
5
  Format follows [Keep a Changelog](https://keepachangelog.com/). Versioning follows [Semantic Versioning](https://semver.org/).
6
6
 
7
+ ## [0.2.0] — 2026-04-02
8
+
9
+ Corresponds to Soma agent v0.8.0.
10
+
11
+ ### New
12
+
13
+ - **`soma doctor`** — check project health, auto-fix stale settings/body/protocols, run migrations.
14
+ - **`soma status` / `soma health`** — quick project health check (renamed from old `soma doctor`).
15
+ - **`soma --version`** — shows both agent and CLI versions.
16
+ - **`soma --help`** — delegates to core agent for full help output.
17
+ - **`--scan` / `--all` flags** — doctor multi-project support (delegates to core).
18
+
19
+ ### Changed
20
+
21
+ - **CLI routing overhaul** — doctor/status/health/update/version all route through thin-cli properly.
22
+ - **Stale protocol detection** — doctor identifies outdated bundled protocols and offers to update them.
23
+ - **Version-aware pending file** — `_doctor-pending.md` injected on boot when migration is available.
24
+
25
+ ### Fixed
26
+
27
+ - **Help header** — shows "CLI v0.2.0" instead of bare version number.
28
+ - **Orphaned `showStatus()`** — removed dead code from thin-cli.
29
+ - **Placeholder text** — replaced thin-cli stubs with real output.
30
+
31
+ ---
32
+
7
33
  ## [0.1.4] — 2026-03-28
8
34
 
9
35
  Corresponds to Soma agent v0.6.5.
package/README.md CHANGED
@@ -25,36 +25,83 @@ An AI coding agent that grows with you. Identity, protocols, and muscle memory
25
25
 
26
26
  Most agents forget everything between sessions. Soma doesn't.
27
27
 
28
- ## What Happens
29
-
30
- On first run, you'll meet the personality engine — no AI, just sentence templates. It introduces Soma and lets you ask questions before committing. Press Enter to install.
28
+ ## Install
31
29
 
30
+ ```bash
31
+ npm install -g meetsoma
32
32
  ```
33
- σ
34
33
 
35
- Soma — the AI agent that remembers
34
+ This installs the Soma CLI a thin bootstrap layer. On first run, it downloads the full agent runtime (~50MB) from GitHub.
35
+
36
+ ### What Happens
37
+
38
+ ```
39
+ npm install -g meetsoma ← installs CLI only (~50KB)
40
+ soma ← first run: downloads agent runtime
41
+ soma ← every run after: starts a session
42
+ ```
36
43
 
37
- No compaction. Ever.
44
+ **Requirements:** Node.js ≥ 20.6, git, an API key (Anthropic, Google, or OpenAI).
38
45
 
39
- ──────────────────────────────────────────────────────────
46
+ ## First Run
40
47
 
41
- → Enter Install Soma
42
- ? Ask me something first
48
+ ```bash
49
+ cd your-project
50
+ soma
43
51
  ```
44
52
 
45
- After install, run `soma` in any project. It creates `.soma/`, reads your codebase, and writes its own identity. By session two, it remembers.
53
+ Soma detects your project (language, framework, package manager, monorepo signals), creates `.soma/`, and writes its own identity. By session two, it remembers.
46
54
 
47
55
  ## Commands
48
56
 
57
+ ### CLI (from your shell)
58
+
59
+ ```bash
60
+ soma # fresh session
61
+ soma inhale # resume from last session's preload
62
+ soma -c # continue last session (full history)
63
+ soma -r # pick a session to resume
64
+ soma focus <keyword> # prime for a topic
65
+ soma map <name> # boot with a workflow MAP
66
+
67
+ soma doctor # project health + migration check
68
+ soma status # infrastructure health check
69
+ soma --version # show agent + CLI versions
70
+ soma --help # full command reference
71
+ soma --help scripts # list installed scripts
72
+ soma --help commands # all CLI + TUI commands
73
+ ```
74
+
75
+ ### Session (inside the TUI)
76
+
77
+ ```
78
+ /exhale save state + write preload for next session
79
+ /breathe save + rotate into fresh session
80
+ /inhale check preload status
81
+ /rest disable keepalive + exhale
82
+ /pin <name> keep a muscle/protocol hot
83
+ /kill <name> drop to cold
84
+ /hub install install from the community hub
85
+ /soma doctor run migration analysis
86
+ ```
87
+
88
+ ## Two Versions
89
+
90
+ Soma has two independently-versioned layers:
91
+
92
+ ```
93
+ σ Soma v0.7.1 ← agent version (the runtime — features, protocols, templates)
94
+ CLI v0.2.0 ← CLI version (this npm package — install flow, delegation)
95
+ ```
96
+
97
+ The **agent version** is what matters for features. The **CLI version** is the bootstrap.
98
+
99
+ ### Updating
100
+
49
101
  ```bash
50
- soma # start a session
51
- soma focus <keyword> # load relevant muscles + MAPs
52
- soma inhale # resume from last session
53
- soma --map <name> # load a workflow
54
-
55
- soma init # install the runtime
56
- soma doctor # check health
57
- soma update # check for updates
102
+ soma init # update the agent runtime
103
+ npm install -g meetsoma # update the CLI (rare)
104
+ soma doctor # check if project .soma/ needs migration
58
105
  ```
59
106
 
60
107
  ## The Five Ideas
@@ -69,11 +116,16 @@ soma update # check for updates
69
116
 
70
117
  **Heat** — attention management. Used things stay. Unused things cool.
71
118
 
72
- ## Requirements
119
+ ## How It Works
120
+
121
+ ```
122
+ ~/.soma/agent/ ← global runtime (git clone, updated via soma init)
123
+ your-project/.soma/ ← per-project memory (identity, protocols, sessions)
124
+ ```
125
+
126
+ The CLI delegates to the runtime. The runtime loads your project's `.soma/`, resolves the identity chain (project → parent → global), and boots the agent with your accumulated context.
73
127
 
74
- - Node.js 20.6
75
- - git
76
- - API key (Anthropic, Google, or OpenAI)
128
+ On boot, Soma silently adds any missing settings or protocols from newer versions (Tier 1 auto-fix). Bigger migrations are handled by `/soma doctor` inside the TUI.
77
129
 
78
130
  ---
79
131
 
package/dist/thin-cli.js CHANGED
@@ -8,7 +8,7 @@
8
8
  * For returning users: detects installed runtime → delegates to it.
9
9
  */
10
10
 
11
- import { existsSync, readFileSync, writeFileSync, appendFileSync, mkdirSync, readdirSync, statSync } from "fs";
11
+ import { existsSync, readFileSync, writeFileSync, appendFileSync, mkdirSync, readdirSync, statSync, readlinkSync } from "fs";
12
12
  import { join, dirname } from "path";
13
13
  import { homedir, platform } from "os";
14
14
  import { fileURLToPath } from "url";
@@ -55,6 +55,29 @@ function isInstalled() {
55
55
  return (hasDist || hasDev) && hasDeps;
56
56
  }
57
57
 
58
+ function getAgentVersion() {
59
+ try {
60
+ // In dev mode, core/ is symlinked — resolve to find the real repo's package.json
61
+ let pkgPath = join(CORE_DIR, "package.json");
62
+ try {
63
+ const realCore = readlinkSync(join(CORE_DIR, "core"));
64
+ if (realCore) {
65
+ // core/ symlink → repos/agent/core → go up one level for package.json
66
+ const devPkg = join(dirname(realCore), "package.json");
67
+ if (existsSync(devPkg)) pkgPath = devPkg;
68
+ }
69
+ } catch { /* not a symlink, use default */ }
70
+ return JSON.parse(readFileSync(pkgPath, "utf-8")).version;
71
+ } catch { return null; }
72
+ }
73
+
74
+ function getProjectVersion() {
75
+ try {
76
+ const settingsPath = join(process.cwd(), ".soma", "settings.json");
77
+ return JSON.parse(readFileSync(settingsPath, "utf-8")).version || null;
78
+ } catch { return null; }
79
+ }
80
+
58
81
  // ── Browser ──────────────────────────────────────────────────────────
59
82
 
60
83
  function openBrowser(url) {
@@ -695,7 +718,13 @@ function showHelp() {
695
718
  }
696
719
 
697
720
  function showVersion() {
698
- console.log(`soma v${VERSION}`);
721
+ const agentV = getAgentVersion();
722
+ if (agentV) {
723
+ console.log(`σ Soma v${agentV}`);
724
+ console.log(` CLI v${VERSION}`);
725
+ } else {
726
+ console.log(`soma v${VERSION}`);
727
+ }
699
728
  }
700
729
 
701
730
  async function showAbout() {
@@ -889,7 +918,7 @@ async function initSoma() {
889
918
  // Save config
890
919
  const config = readConfig();
891
920
  config.installedAt = config.installedAt || new Date().toISOString();
892
- config.coreVersion = VERSION;
921
+ config.coreVersion = getAgentVersion() || VERSION;
893
922
  config.installPath = installDir;
894
923
  writeConfig(config);
895
924
 
@@ -938,6 +967,15 @@ async function checkAndUpdate() {
938
967
 
939
968
  if (behind === 0) {
940
969
  console.log(` ${green("✓")} Already up to date.`);
970
+
971
+ // Check project version staleness even when runtime is current
972
+ const agentV = getAgentVersion();
973
+ const projectV = getProjectVersion();
974
+ if (agentV && projectV && projectV < agentV) {
975
+ console.log(` ${yellow("⚠")} Project .soma/ is at ${cyan(`v${projectV}`)}, agent is at ${cyan(`v${agentV}`)}.`);
976
+ console.log(` Run ${green("soma doctor")} to check for updates.`);
977
+ }
978
+
941
979
  console.log("");
942
980
  console.log(` ${dim("Soma is set up and ready.")} Run ${green("soma")} ${dim("in a project to start a session.")}`);
943
981
  console.log("");
@@ -1015,12 +1053,13 @@ function checkForUpdates() {
1015
1053
  printSigma();
1016
1054
  console.log(` ${bold("Soma")} — Update Check`);
1017
1055
  console.log("");
1018
- console.log(` CLI version: ${cyan(`v${VERSION}`)}`);
1056
+ const agentV = getAgentVersion();
1057
+ if (agentV) {
1058
+ console.log(` Soma: ${cyan(`v${agentV}`)}`);
1059
+ }
1060
+ console.log(` CLI: ${cyan(`v${VERSION}`)}`);
1019
1061
 
1020
1062
  const config = readConfig();
1021
- if (config.coreVersion) {
1022
- console.log(` Core version: ${cyan(`v${config.coreVersion}`)}`);
1023
- }
1024
1063
 
1025
1064
  // Check npm for CLI updates
1026
1065
  try {
@@ -1061,7 +1100,7 @@ function checkForUpdates() {
1061
1100
  console.log("");
1062
1101
  }
1063
1102
 
1064
- async function doctor() {
1103
+ async function healthCheck() {
1065
1104
  printSigma();
1066
1105
  console.log(` ${bold("Soma")} — Health Check`);
1067
1106
  console.log("");
@@ -1107,12 +1146,26 @@ async function doctor() {
1107
1146
  : join(CORE_DIR, "core");
1108
1147
  check(existsSync(coreDir), "Core modules present", "Core modules missing");
1109
1148
 
1110
- // Git repo health
1111
- try {
1112
- execSync("git status --porcelain", { cwd: CORE_DIR, stdio: "ignore" });
1113
- check(true, "Git repo healthy", "");
1114
- } catch {
1115
- warn(false, "", "Core git repo has issues");
1149
+ // Git repo health (skip in dev mode — no .git when using symlinks)
1150
+ if (existsSync(join(CORE_DIR, ".git"))) {
1151
+ try {
1152
+ execSync("git status --porcelain", { cwd: CORE_DIR, stdio: "ignore" });
1153
+ check(true, "Git repo healthy", "");
1154
+ } catch {
1155
+ warn(false, "", "Core git repo has issues");
1156
+ }
1157
+ } else {
1158
+ // Dev mode — core/ is symlinked, no .git expected
1159
+ try {
1160
+ const realCore = readlinkSync(join(CORE_DIR, "core"));
1161
+ if (realCore) {
1162
+ check(true, "Dev mode (symlinked)", "");
1163
+ } else {
1164
+ warn(false, "", "Core git repo missing — run soma init");
1165
+ }
1166
+ } catch {
1167
+ warn(false, "", "Core git repo missing — run soma init");
1168
+ }
1116
1169
  }
1117
1170
  }
1118
1171
 
@@ -1148,36 +1201,309 @@ async function doctor() {
1148
1201
  console.log("");
1149
1202
  }
1150
1203
 
1151
- function showStatus() {
1204
+ async function projectDoctor() {
1205
+ const doctorArgs = args.slice(1);
1206
+ const wantsScan = doctorArgs.includes("--scan");
1207
+ const wantsAll = doctorArgs.includes("--all");
1208
+
1209
+ // --scan and --all need the runtime — delegate to core
1210
+ if ((wantsScan || wantsAll) && isInstalled()) {
1211
+ await delegateToCore();
1212
+ return;
1213
+ }
1214
+
1152
1215
  printSigma();
1153
- console.log(` ${bold("Soma")} Status`);
1216
+ const agentV = getAgentVersion();
1217
+ const projectV = getProjectVersion();
1218
+ const installed = isInstalled();
1219
+
1220
+ console.log(` ${bold("Soma")} — Doctor`);
1154
1221
  console.log("");
1155
1222
 
1156
- const config = readConfig();
1157
- const installed = isInstalled();
1223
+ if (!installed) {
1224
+ console.log(` ${red("✗")} Soma not installed. Run ${green("soma init")} first.`);
1225
+ console.log("");
1226
+ return;
1227
+ }
1158
1228
 
1159
- console.log(` Version: ${cyan(`v${VERSION}`)}`);
1160
- console.log(` Home: ${dim(SOMA_HOME)}`);
1161
- console.log(` Installed: ${installed ? green("yes") : red("no")}`);
1162
- if (config.installedAt) {
1163
- console.log(` Since: ${dim(config.installedAt.split("T")[0])}`);
1229
+ // Version overview
1230
+ console.log(` Agent: ${cyan(`v${agentV || "unknown"}`)}`);
1231
+ if (projectV) {
1232
+ console.log(` Project: ${cyan(`v${projectV}`)}`);
1164
1233
  }
1234
+ console.log(` CLI: ${dim(`v${VERSION}`)}`);
1235
+ console.log("");
1165
1236
 
1166
- if (installed && config.installPath) {
1167
- try {
1168
- const hash = execSync("git rev-parse --short HEAD", {
1169
- cwd: config.installPath, encoding: "utf-8"
1170
- }).trim();
1171
- const branch = execSync("git rev-parse --abbrev-ref HEAD", {
1172
- cwd: config.installPath, encoding: "utf-8"
1173
- }).trim();
1174
- console.log(` Core: ${dim(`${branch}@${hash}`)}`);
1175
- } catch {}
1237
+ const hasSomaDir = existsSync(join(process.cwd(), ".soma"));
1238
+
1239
+ if (!hasSomaDir) {
1240
+ console.log(` ${yellow("⚠")} No .soma/ in current directory.`);
1241
+ console.log(` Run ${green("soma init")} to set up this project, or ${green("soma doctor --scan")} to find projects.`);
1242
+ console.log("");
1243
+ return;
1244
+ }
1245
+
1246
+ if (!projectV) {
1247
+ console.log(` ${yellow("⚠")} Project .soma/ has no version (pre-versioning).`);
1248
+ console.log(` This project was likely created before v0.6.3.`);
1249
+ console.log(` Run ${green("soma init")} to bring it up to date.`);
1250
+ console.log("");
1251
+ return;
1176
1252
  }
1177
1253
 
1254
+ if (agentV && projectV === agentV) {
1255
+ console.log(` ${green("\u2713")} Project is up to date.`);
1256
+ console.log("");
1257
+ await healthCheck();
1258
+ return;
1259
+ }
1260
+
1261
+ if (agentV && projectV < agentV) {
1262
+ console.log(` ${yellow("\u26a0")} Project .soma/ is at ${cyan(`v${projectV}`)} , agent is at ${cyan(`v${agentV}`)} .`);
1263
+ console.log("");
1264
+
1265
+ // Tier 1: Auto-fix safe things right now
1266
+ let fixes = 0;
1267
+ const somaDir = join(process.cwd(), ".soma");
1268
+
1269
+ // Add missing settings keys
1270
+ const settingsPath = join(somaDir, "settings.json");
1271
+ if (existsSync(settingsPath)) {
1272
+ try {
1273
+ const current = JSON.parse(readFileSync(settingsPath, "utf-8"));
1274
+ let changed = false;
1275
+ const add = (k, v) => { if (!(k in current)) { current[k] = v; changed = true; fixes++; } };
1276
+ add("doctor", { autoUpdate: true, declinedVersion: null });
1277
+ add("breathe", { auto: false, triggerAt: 50, rotateAt: 70, graceSeconds: 30 });
1278
+ add("context", { notifyAt: 50, warnAt: 70, urgentAt: 80, autoExhaleAt: 85 });
1279
+ add("preload", { staleAfterHours: 48, lastSessionLogs: 0 });
1280
+ add("scratch", { autoInject: false });
1281
+ add("guard", { coreFiles: "warn", bashCommands: "warn", gitIdentity: null });
1282
+ add("checkpoints", { enabled: true, intervalMinutes: 5, squashOnPush: true });
1283
+ add("persona", { name: null, emoji: "\u03c3" });
1284
+ add("inherit", { identity: true, protocols: true, muscles: true, tools: true });
1285
+ if (changed) writeFileSync(settingsPath, JSON.stringify(current, null, "\t") + "\n");
1286
+ } catch {}
1287
+ }
1288
+
1289
+ // Add missing body/ + templates
1290
+ const bodyDir = join(somaDir, "body");
1291
+ // Resolve agent root — follow dev symlinks if needed
1292
+ let agentRoot = CORE_DIR;
1293
+ try {
1294
+ const realCore = readlinkSync(join(CORE_DIR, "core"));
1295
+ if (realCore) {
1296
+ const devRoot = dirname(realCore);
1297
+ if (existsSync(join(devRoot, "body", "_public"))) agentRoot = devRoot;
1298
+ }
1299
+ } catch {}
1300
+ // Also check dist/ parent (prod layout)
1301
+ if (!existsSync(join(agentRoot, "body", "_public"))) {
1302
+ const parent = dirname(agentRoot);
1303
+ if (existsSync(join(parent, "body", "_public"))) agentRoot = parent;
1304
+ }
1305
+ const bundledBody = join(agentRoot, "body", "_public");
1306
+ if (existsSync(bundledBody)) {
1307
+ try {
1308
+ if (!existsSync(bodyDir)) mkdirSync(bodyDir, { recursive: true });
1309
+ for (const f of readdirSync(bundledBody).filter(f => f.endsWith(".md"))) {
1310
+ const dest = join(bodyDir, f);
1311
+ if (!existsSync(dest)) { writeFileSync(dest, readFileSync(join(bundledBody, f), "utf-8")); fixes++; }
1312
+ }
1313
+ } catch {}
1314
+ }
1315
+
1316
+ // Add missing protocols
1317
+ const protoDir = join(somaDir, "amps", "protocols");
1318
+ const bundledProtos = existsSync(join(CORE_DIR, "dist", "content", "protocols"))
1319
+ ? join(CORE_DIR, "dist", "content", "protocols")
1320
+ : existsSync(join(CORE_DIR, "content", "protocols"))
1321
+ ? join(CORE_DIR, "content", "protocols") : null;
1322
+ if (bundledProtos) {
1323
+ if (!existsSync(protoDir)) mkdirSync(protoDir, { recursive: true });
1324
+ for (const f of readdirSync(bundledProtos).filter(f => f.endsWith(".md") && f !== "_template.md" && f !== "README.md")) {
1325
+ const dest = join(protoDir, f);
1326
+ if (!existsSync(dest)) { writeFileSync(dest, readFileSync(join(bundledProtos, f), "utf-8")); fixes++; }
1327
+ }
1328
+ }
1329
+
1330
+ // Add missing bundled scripts
1331
+ const scriptsDir = join(somaDir, "amps", "scripts");
1332
+ const bundledScripts = existsSync(join(agentRoot, "dist", "content", "scripts"))
1333
+ ? join(agentRoot, "dist", "content", "scripts")
1334
+ : existsSync(join(agentRoot, "content", "scripts"))
1335
+ ? join(agentRoot, "content", "scripts")
1336
+ : existsSync(join(agentRoot, "scripts"))
1337
+ ? join(agentRoot, "scripts") : null;
1338
+ if (bundledScripts) {
1339
+ if (!existsSync(scriptsDir)) mkdirSync(scriptsDir, { recursive: true });
1340
+ for (const f of readdirSync(bundledScripts).filter(f => f.endsWith(".sh"))) {
1341
+ const dest = join(scriptsDir, f);
1342
+ if (!existsSync(dest)) {
1343
+ writeFileSync(dest, readFileSync(join(bundledScripts, f), "utf-8"), { mode: 0o755 });
1344
+ fixes++;
1345
+ }
1346
+ }
1347
+ }
1348
+
1349
+ // Bump version after fixes
1350
+ if (fixes > 0) {
1351
+ try {
1352
+ const s = JSON.parse(readFileSync(settingsPath, "utf-8"));
1353
+ s.version = agentV;
1354
+ writeFileSync(settingsPath, JSON.stringify(s, null, "\t") + "\n");
1355
+ } catch {}
1356
+ console.log(` ${green("\u2713")} Applied ${fixes} automatic fixes`);
1357
+ const bc = existsSync(join(somaDir, "body")) ? readdirSync(join(somaDir, "body")).filter(f => f.endsWith(".md")).length : 0;
1358
+ const pc = existsSync(protoDir) ? readdirSync(protoDir).filter(f => f.endsWith(".md")).length : 0;
1359
+ console.log(` ${bc} body files, ${pc} protocols, settings updated`);
1360
+ console.log(` Version bumped to ${cyan(`v${agentV}`)}`);
1361
+
1362
+ // Scan + fix stale protocols (exist but differ from bundled)
1363
+ let staleUpdated = 0;
1364
+ let staleSkipped = [];
1365
+ if (bundledProtos && existsSync(protoDir)) {
1366
+ for (const f of readdirSync(protoDir).filter(f => f.endsWith(".md") && f !== "_template.md" && f !== "README.md")) {
1367
+ const bundledFile = join(bundledProtos, f);
1368
+ if (!existsSync(bundledFile)) continue;
1369
+ const projRaw = readFileSync(join(protoDir, f), "utf-8");
1370
+ const bundledRaw = readFileSync(bundledFile, "utf-8");
1371
+ const strip = s => s.replace(/^(heat|loads|runs|last-run|heat-default):.*\n?/gm, "").trim();
1372
+ if (strip(projRaw) === strip(bundledRaw)) continue;
1373
+ // Preserve user's runtime fields (heat, loads) when updating content
1374
+ const heatLine = projRaw.match(/^heat:.*$/m);
1375
+ const loadsLine = projRaw.match(/^loads:.*$/m);
1376
+ let updated = bundledRaw;
1377
+ if (heatLine) updated = updated.replace(/^heat:.*$/m, heatLine[0]);
1378
+ if (loadsLine) updated = updated.replace(/^loads:.*$/m, loadsLine[0]);
1379
+ writeFileSync(join(protoDir, f), updated);
1380
+ staleUpdated++;
1381
+ }
1382
+ }
1383
+
1384
+ console.log("");
1385
+ if (staleUpdated > 0) {
1386
+ console.log(` ${green("\u2713")} ${staleUpdated} protocols updated to latest version`);
1387
+ console.log(` ${dim("Heat and load counts preserved. Content updated (coaching-voice rewrites, new TL;DR).")}`);
1388
+ }
1389
+ if (staleSkipped.length > 0) {
1390
+ console.log(` ${yellow("\u26a0")} ${staleSkipped.length} protocols skipped (may be customized)`);
1391
+ }
1392
+ console.log("");
1393
+ const totalRemaining = staleSkipped.length;
1394
+ if (totalRemaining > 0) {
1395
+ console.log(` ${dim("CLI handled: files, scripts, settings, protocols, version bump.")}`);
1396
+ console.log(` ${dim("Remaining: " + totalRemaining + " items need review.")}`);
1397
+ console.log(` ${dim("For full migration:")} ${green("soma")} ${dim("then")} ${green("/soma doctor")}`);
1398
+ } else {
1399
+ console.log(` ${green("\u2713")} Full migration complete from CLI.`);
1400
+ console.log(` ${dim("No TUI session needed — all updates applied.")}`);
1401
+ }
1402
+
1403
+ // Write _doctor-pending.md — only if there are remaining items for the agent
1404
+ // If CLI handled everything, write a brief "complete" note instead
1405
+ try {
1406
+ const pendingPath = join(somaDir, "body", "_doctor-pending.md");
1407
+ if (staleSkipped.length === 0) {
1408
+ // Full migration done — write brief completion note
1409
+ const done = [
1410
+ "---",
1411
+ "type: template",
1412
+ "name: doctor-pending",
1413
+ "status: complete",
1414
+ `created: ${new Date().toISOString().split("T")[0]}`,
1415
+ "description: CLI doctor completed full migration — verify with /soma doctor then delete this file",
1416
+ "---",
1417
+ "",
1418
+ "# Doctor Update — Complete",
1419
+ "",
1420
+ `The CLI doctor migrated this project from v${projectV} to v${agentV} on ${new Date().toISOString().split("T")[0]}.`,
1421
+ "",
1422
+ `Applied: ${fixes} file fixes + ${staleUpdated} protocol updates. All settings, body, scripts, protocols current.`,
1423
+ "",
1424
+ "Run \`/soma doctor\` to verify, then delete this file.",
1425
+ ];
1426
+ writeFileSync(pendingPath, done.join("\n"));
1427
+ } else {
1428
+ const pending = [
1429
+ "---",
1430
+ "type: template",
1431
+ "name: doctor-pending",
1432
+ "status: active",
1433
+ `created: ${new Date().toISOString().split("T")[0]}`,
1434
+ "description: Written by soma doctor CLI — tells the agent what was fixed and what needs attention",
1435
+ "---",
1436
+ "",
1437
+ "# Doctor Update — Pending",
1438
+ "",
1439
+ `The CLI doctor ran on ${new Date().toISOString().split("T")[0]} and updated this project from v${projectV} to v${agentV}.`,
1440
+ "",
1441
+ "## What the CLI fixed (Tier 1 — automatic, safe)",
1442
+ "",
1443
+ `- ${bc} body template files (added missing ones, preserved your customized files)`,
1444
+ `- ${pc} protocols present (added missing bundled protocols)`,
1445
+ `- Settings: added missing configuration sections with safe defaults`,
1446
+ `- Version bumped to v${agentV}`,
1447
+ "",
1448
+ `## What the CLI also fixed`,
1449
+ "",
1450
+ `- ${staleUpdated} protocols updated to latest version (heat/loads preserved)`,
1451
+ "",
1452
+ staleSkipped.length > 0 ? `## What still needs attention (${staleSkipped.length} items)` : "## Status: Complete",
1453
+ "",
1454
+ staleSkipped.length > 0
1455
+ ? `**${staleSkipped.length} protocols** may be customized and were not auto-updated. The agent needs to:\n- Diff each against bundled version\n- Decide whether to update or preserve`
1456
+ : "All mechanical updates applied by CLI. The agent can verify with /soma doctor.",
1457
+ "",
1458
+ "**To complete the migration:**",
1459
+ "",
1460
+ "Read the migration phases that apply to your version jump:",
1461
+ "```",
1462
+ ];
1463
+ // Find which phases apply (only from projectV onward)
1464
+ let agentRoot2 = CORE_DIR;
1465
+ try {
1466
+ const rc = readlinkSync(join(CORE_DIR, "core"));
1467
+ if (rc) { const dr = dirname(rc); if (existsSync(join(dr, "migrations", "phases"))) agentRoot2 = dr; }
1468
+ } catch {}
1469
+ if (!existsSync(join(agentRoot2, "migrations", "phases"))) {
1470
+ const pr = dirname(agentRoot2);
1471
+ if (existsSync(join(pr, "migrations", "phases"))) agentRoot2 = pr;
1472
+ }
1473
+ const phasesDir2 = join(agentRoot2, "migrations", "phases");
1474
+ if (existsSync(phasesDir2)) {
1475
+ const allPhases = readdirSync(phasesDir2).filter(f => f.endsWith(".md")).sort();
1476
+ const relevant = allPhases.filter(f => {
1477
+ const m = f.match(/^v([\d.]+)-to-v/);
1478
+ return m && m[1] >= projectV;
1479
+ });
1480
+ pending.push(`${relevant.length} phases apply (v${projectV} \u2192 v${agentV}):`);
1481
+ pending.push("\`\`\`");
1482
+ for (const f of relevant) {
1483
+ pending.push(" " + join(phasesDir2, f));
1484
+ }
1485
+ }
1486
+ pending.push("```");
1487
+ pending.push("");
1488
+ pending.push("Execute each phase's **Actions** section in order.");
1489
+ pending.push("After completing all phases, delete this file — it's no longer needed.");
1490
+ pending.push("");
1491
+ writeFileSync(pendingPath, pending.join("\n"));
1492
+ }
1493
+ } catch { /* non-fatal */ }
1494
+ } else {
1495
+ console.log(` ${green("\u2713")} Version bumped to ${cyan(`v${agentV}`)}`);
1496
+ console.log(` ${dim("No file changes needed — project structure is current.")}`);
1497
+ }
1498
+ } else {
1499
+ console.log(` ${green("\u2713")} Project version: ${cyan(`v${projectV}`)}`);
1500
+ }
1501
+
1178
1502
  console.log("");
1503
+ await healthCheck();
1179
1504
  }
1180
1505
 
1506
+
1181
1507
  // ── Delegation ───────────────────────────────────────────────────────
1182
1508
 
1183
1509
  async function delegateToCore() {
@@ -1276,7 +1602,12 @@ const cmd = args[0];
1276
1602
  if (cmd === "--version" || cmd === "-v" || cmd === "-V") {
1277
1603
  showVersion();
1278
1604
  } else if (cmd === "--help" || cmd === "-h") {
1279
- showHelp();
1605
+ if (isInstalled()) {
1606
+ // Delegate to core — richer help with scripts, commands, hub
1607
+ await delegateToCore();
1608
+ } else {
1609
+ showHelp();
1610
+ }
1280
1611
  } else if (cmd === "about") {
1281
1612
  await showAbout();
1282
1613
  } else if (cmd === "init") {
@@ -1291,15 +1622,15 @@ if (cmd === "--version" || cmd === "-v" || cmd === "-V") {
1291
1622
  // Installed, project init (new project or --template/--orphan)
1292
1623
  await delegateToCore();
1293
1624
  } else {
1294
- // Installed + .soma/ exists — check for updates, don't re-run setup
1625
+ // Installed + .soma/ exists — check for updates + project staleness
1295
1626
  await checkAndUpdate();
1296
1627
  }
1297
1628
  } else if (cmd === "update") {
1298
1629
  checkForUpdates();
1299
1630
  } else if (cmd === "doctor") {
1300
- await doctor();
1301
- } else if (cmd === "status") {
1302
- showStatus();
1631
+ await projectDoctor();
1632
+ } else if (cmd === "status" || cmd === "health") {
1633
+ await healthCheck();
1303
1634
  } else if (isInstalled()) {
1304
1635
  // Core installed — delegate to runtime
1305
1636
  await delegateToCore();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "meetsoma",
3
- "version": "0.1.6",
3
+ "version": "0.2.0",
4
4
  "description": "Soma \u2014 the AI coding agent with self-growing memory",
5
5
  "type": "module",
6
6
  "bin": {