hypomnema 1.6.2 → 1.7.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 (62) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.ko.md +39 -14
  4. package/README.md +39 -14
  5. package/commands/capture.md +8 -6
  6. package/commands/crystallize.md +39 -20
  7. package/docs/ARCHITECTURE.md +49 -14
  8. package/docs/CONTRIBUTING.md +31 -29
  9. package/hooks/base-store.mjs +265 -0
  10. package/hooks/hooks.json +7 -1
  11. package/hooks/hypo-auto-minimal-crystallize.mjs +63 -20
  12. package/hooks/hypo-auto-stage.mjs +30 -0
  13. package/hooks/hypo-cwd-change.mjs +31 -2
  14. package/hooks/hypo-file-watch.mjs +21 -2
  15. package/hooks/hypo-first-prompt.mjs +19 -3
  16. package/hooks/hypo-lookup.mjs +86 -29
  17. package/hooks/hypo-personal-check.mjs +24 -3
  18. package/hooks/hypo-session-record.mjs +2 -3
  19. package/hooks/hypo-session-start.mjs +89 -7
  20. package/hooks/hypo-shared.mjs +904 -128
  21. package/hooks/proposal-store.mjs +513 -0
  22. package/package.json +40 -14
  23. package/scripts/capture.mjs +556 -37
  24. package/scripts/crystallize.mjs +639 -108
  25. package/scripts/doctor.mjs +304 -9
  26. package/scripts/feedback-sync.mjs +515 -44
  27. package/scripts/graph.mjs +9 -2
  28. package/scripts/init.mjs +230 -34
  29. package/scripts/lib/extensions.mjs +656 -1
  30. package/scripts/lib/hypo-ignore.mjs +54 -6
  31. package/scripts/lib/hypo-root.mjs +56 -6
  32. package/scripts/lib/page-usage.mjs +15 -2
  33. package/scripts/lib/pkg-json.mjs +40 -0
  34. package/scripts/lib/plugin-detect.mjs +96 -6
  35. package/scripts/lib/wd-match.mjs +23 -5
  36. package/scripts/lib/wikilink.mjs +32 -6
  37. package/scripts/lint.mjs +20 -4
  38. package/scripts/proposal.mjs +1032 -0
  39. package/scripts/query.mjs +25 -4
  40. package/scripts/resume.mjs +34 -12
  41. package/scripts/stats.mjs +28 -8
  42. package/scripts/uninstall.mjs +141 -6
  43. package/scripts/upgrade.mjs +197 -15
  44. package/skills/crystallize/SKILL.md +44 -7
  45. package/skills/debate/SKILL.md +88 -0
  46. package/skills/debate/references/orchestration-patterns.md +83 -0
  47. package/templates/.hyposcanignore +10 -0
  48. package/templates/SCHEMA.md +12 -0
  49. package/templates/gitignore +5 -0
  50. package/templates/hypo-config.md +1 -1
  51. package/templates/hypo-guide.md +6 -0
  52. package/scripts/.gitkeep +0 -0
  53. package/scripts/check-bilingual.mjs +0 -153
  54. package/scripts/check-readme-version.mjs +0 -126
  55. package/scripts/check-tracker-ids.mjs +0 -426
  56. package/scripts/check-versions.mjs +0 -171
  57. package/scripts/install-git-hooks.mjs +0 -293
  58. package/scripts/lib/changelog-classify.mjs +0 -216
  59. package/scripts/lib/check-bilingual.mjs +0 -244
  60. package/scripts/lib/check-tracker-ids.mjs +0 -217
  61. package/scripts/lib/pre-commit-format.mjs +0 -251
  62. package/scripts/pre-commit-format.mjs +0 -198
@@ -25,6 +25,7 @@ import {
25
25
  projectSuggestionsPath,
26
26
  collectProjectWorkingDirs,
27
27
  } from '../hooks/hypo-shared.mjs';
28
+ import { listProposals } from '../hooks/proposal-store.mjs';
28
29
  import {
29
30
  discoverExtensions,
30
31
  parseManifest,
@@ -35,16 +36,40 @@ import {
35
36
  resolveInstallFile,
36
37
  buildHookCommand,
37
38
  parseExtKey,
39
+ parseSkillKey,
40
+ parseSkillShaValue,
41
+ isFlatShaValue,
38
42
  EXT_TYPES,
39
43
  CODEX_TYPES,
40
44
  } from './lib/extensions.mjs';
41
45
  import { sha256, readFileIfRegular, readPkgJson } from './lib/pkg-json.mjs';
42
46
  import { resolveCliOnPath, classifyInstall } from '../hooks/version-check.mjs';
47
+ import { isHypomnemaPluginEnabled } from './lib/plugin-detect.mjs';
43
48
 
44
49
  const HOME = homedir();
45
50
  const SCRIPT_DIR = fileURLToPath(new URL('.', import.meta.url));
46
51
  const PKG_ROOT = join(SCRIPT_DIR, '..');
47
52
 
53
+ // ── install channel ───────────────────────────────────────────────────────────
54
+ //
55
+ // A plugin-channel install registers its 14 core hooks straight out of the
56
+ // package's own hooks/hooks.json (CLAUDE_PLUGIN_ROOT) — Claude Code auto-wires
57
+ // them, they are never copied into ~/.claude/hooks and never listed in
58
+ // ~/.claude/settings.json. checkHooks/checkSettingsJson used to treat that
59
+ // empty state as "missing" and prescribe `/hypo:init`, which then copied +
60
+ // registered the very same hooks a second time (every hook firing twice).
61
+ //
62
+ // `pluginMode` mirrors upgrade.mjs's own detector: this doctor.mjs is itself
63
+ // running from a `.claude/plugins/…` root. `hypomnemaPluginEnabled` catches the
64
+ // dual-install variant — a manual/npm doctor.mjs run while the plugin is ALSO
65
+ // enabled in settings.json (see lib/plugin-detect.mjs; fails open on any
66
+ // uncertainty). Either signal means Claude's core hook surface is
67
+ // plugin-managed, not missing.
68
+ const pluginMode = PKG_ROOT.replace(/\\/g, '/').includes('/.claude/plugins/');
69
+ const hypomnemaPluginEnabled =
70
+ !pluginMode && isHypomnemaPluginEnabled(join(HOME, '.claude', 'settings.json'));
71
+ const coreManagedByPlugin = pluginMode || hypomnemaPluginEnabled;
72
+
48
73
  // Shown after every fatal package-integrity error. These conditions mean the
49
74
  // shipped hooks/hooks.json is missing or malformed — never a user mistake —
50
75
  // so the only useful next step is a re-install of the package.
@@ -255,7 +280,17 @@ function checkFiles(hypoDir) {
255
280
  }
256
281
  }
257
282
 
258
- function checkHooks() {
283
+ // .hyposcanignore is optional (scan-only exclusions, not a privacy boundary),
284
+ // so its absence is info-level — pass either way, never warn/fail.
285
+ function checkScanIgnoreFile(hypoDir) {
286
+ const present = existsSync(join(hypoDir, '.hyposcanignore'));
287
+ pass(
288
+ 'File: .hyposcanignore',
289
+ present ? 'present' : 'optional — not present, no scan-only exclusions configured',
290
+ );
291
+ }
292
+
293
+ function checkHooks(coreManagedByPlugin) {
259
294
  const claudeHooks = join(HOME, '.claude', 'hooks');
260
295
  const allFiles = [...Object.values(HOOK_MAP).flat(), ...SHARED_FILES];
261
296
 
@@ -266,6 +301,13 @@ function checkHooks() {
266
301
 
267
302
  if (missing === 0) {
268
303
  pass('Hook files installed', claudeHooks);
304
+ } else if (coreManagedByPlugin && missing === allFiles.length) {
305
+ // Plugin channel: an empty ~/.claude/hooks is the expected, healthy state
306
+ // (the plugin loader provides the hooks), not a missing install.
307
+ pass(
308
+ 'Hook files installed',
309
+ `provided by the plugin loader (hooks/hooks.json) — none copied to ${claudeHooks} (expected)`,
310
+ );
269
311
  } else if (missing < allFiles.length) {
270
312
  warn('Hook files installed', `${missing}/${allFiles.length} missing in ${claudeHooks}`);
271
313
  } else {
@@ -273,10 +315,17 @@ function checkHooks() {
273
315
  }
274
316
  }
275
317
 
276
- function checkSettingsJson() {
318
+ function checkSettingsJson(coreManagedByPlugin) {
277
319
  const settingsPath = join(HOME, '.claude', 'settings.json');
278
320
  if (!existsSync(settingsPath)) {
279
- warn('settings.json hook registrations', 'settings.json not found');
321
+ if (coreManagedByPlugin) {
322
+ pass(
323
+ 'settings.json hook registrations',
324
+ 'provided by the plugin loader (hooks/hooks.json) — settings.json entries not required',
325
+ );
326
+ } else {
327
+ warn('settings.json hook registrations', 'settings.json not found');
328
+ }
280
329
  return;
281
330
  }
282
331
 
@@ -310,6 +359,13 @@ function checkSettingsJson() {
310
359
  'settings.json hook registrations',
311
360
  `${registered}/${total} registered — run /hypo:init to merge missing entries`,
312
361
  );
362
+ } else if (coreManagedByPlugin) {
363
+ // Same reasoning as checkHooks: the plugin loader never touches settings.json,
364
+ // so 0/total here is the expected plugin-channel state, not a missing install.
365
+ pass(
366
+ 'settings.json hook registrations',
367
+ `0/${total} registered in settings.json — provided by the plugin loader (hooks/hooks.json) instead (expected)`,
368
+ );
313
369
  } else {
314
370
  fail('settings.json hook registrations', `0/${total} registered — run /hypo:init`);
315
371
  }
@@ -669,6 +725,24 @@ function checkProjectSuggestions(hypoDir) {
669
725
  }
670
726
  }
671
727
 
728
+ function checkProposals(hypoDir) {
729
+ // Vault-wide count of parked write-proposals (T8). listProposals is the
730
+ // count source rather than a raw readdir: it already skips malformed and
731
+ // spoofed-id artifacts, so the number matches exactly what `hypomnema proposal
732
+ // list/apply/discard` can act on. Surface only: warn (never fail), pass at 0,
733
+ // because a pending proposal is a normal state awaiting review, not a broken
734
+ // install, and the check discovers without changing any state.
735
+ const count = listProposals(hypoDir).length;
736
+ if (count === 0) {
737
+ pass('Pending proposals', 'No parked write-proposals');
738
+ } else {
739
+ warn(
740
+ 'Pending proposals',
741
+ `${count} parked write-proposal(s) awaiting review; inspect with \`hypomnema proposal list\``,
742
+ );
743
+ }
744
+ }
745
+
672
746
  function checkCodexPaths() {
673
747
  const codexHooks = join(HOME, '.codex', 'hooks');
674
748
  const allFiles = [...Object.values(HOOK_MAP).flat(), ...SHARED_FILES];
@@ -805,6 +879,58 @@ function checkExtensions(hypoDir, claudeHome, target = 'claude') {
805
879
  // Skip keys outside this target's covered types (defensive: a Claude run records
806
880
  // skills/agents that a codex target never installs — don't false-flag them).
807
881
  if (!types.includes(key.split('/')[0])) continue;
882
+
883
+ // A directory skill records one key whose value is a per-file SHA map. Joining
884
+ // that key straight onto root would land on the skill DIRECTORY and report it
885
+ // as "not a regular file" on every run. Check the subtree file by file instead.
886
+ const skillKey = parseSkillKey(key);
887
+ if (skillKey) {
888
+ const nested = parseSkillShaValue(recSHA);
889
+ if (!nested) {
890
+ problems.push({
891
+ severity: 'warn',
892
+ msg: `${key} has a corrupt ownership record — run upgrade --apply`,
893
+ });
894
+ continue;
895
+ }
896
+ const skillRoot = join(root, 'skills', skillKey.installDir);
897
+ for (const [rel, sha] of Object.entries(nested)) {
898
+ const filePath = join(skillRoot, ...rel.split('/'));
899
+ const label = `${key}/${rel}`;
900
+ if (!existsSync(filePath)) {
901
+ problems.push({
902
+ severity: 'warn',
903
+ msg: `${label} recorded but not installed — run upgrade --apply`,
904
+ });
905
+ continue;
906
+ }
907
+ const buf = readFileIfRegular(filePath);
908
+ if (buf === null) {
909
+ problems.push({
910
+ severity: 'warn',
911
+ msg: `${label} is not a regular file — left untouched`,
912
+ });
913
+ continue;
914
+ }
915
+ if (sha256(buf) !== sha) {
916
+ problems.push({
917
+ severity: 'warn',
918
+ msg: `${label} modified since install (drift) — use --force-extensions`,
919
+ });
920
+ }
921
+ }
922
+ continue;
923
+ }
924
+
925
+ // A flat key must carry a plain hex SHA; a wrong-shaped value is a corrupt
926
+ // record, not a drifted file.
927
+ if (!isFlatShaValue(recSHA)) {
928
+ problems.push({
929
+ severity: 'warn',
930
+ msg: `${key} has a corrupt ownership record — run upgrade --apply`,
931
+ });
932
+ continue;
933
+ }
808
934
  const destPath = join(root, key);
809
935
  if (!existsSync(destPath)) {
810
936
  problems.push({
@@ -1129,9 +1255,44 @@ function checkFeedbackProjection(hypoDir, claudeHome, projectId) {
1129
1255
  `${name}: ${reason} — run \`hypomnema feedback-sync --import-target-change\` to reconcile`,
1130
1256
  );
1131
1257
  } else {
1132
- // 2) build error WARN
1133
- const buildErr = targets.find(([, t]) => t.buildError);
1134
- if (buildErr) {
1258
+ // 2) build error. Split by kind:
1259
+ //
1260
+ // 'build-failed' FAIL. The target file EXISTS but its
1261
+ // <learned_behaviors> container is gone, so the projection cannot be
1262
+ // built: NOT ONE L1 rule is loaded on that machine and every later sync
1263
+ // is a silent no-op. This was a warn, which made a structurally broken
1264
+ // projection near-invisible — nothing else reported it either (the
1265
+ // PreCompact gate classified it as "nothing to do" and failed open), and
1266
+ // it went unnoticed on a real machine. A projection that cannot be built
1267
+ // is a failure, not a note.
1268
+ //
1269
+ // 'target-missing' → WARN, as before. No ~/.claude/CLAUDE.md yet is the
1270
+ // ordinary first-run state, not a broken one; failing here would fail
1271
+ // every new user's doctor run.
1272
+ //
1273
+ // Select the build-failed target SPECIFICALLY rather than taking the first
1274
+ // buildError of any kind: with more than one container target, a
1275
+ // 'target-missing' one earlier in iteration order would otherwise mask a
1276
+ // structurally broken one behind a warn — the exact invisibility this check
1277
+ // exists to end. Only `claude` carries a container today, so that masking is
1278
+ // latent, not live; the precompact gate filters the same way.
1279
+ const failedErr = targets.find(([, t]) => t.buildErrorKind === 'build-failed');
1280
+ const buildErr = failedErr || targets.find(([, t]) => t.buildError);
1281
+ if (failedErr) {
1282
+ const [name, t] = failedErr;
1283
+ // Name the remedy for THIS cause, not a fixed one. `--ensure-container`
1284
+ // restores a MISSING container; it cannot chmod an unreadable file, cannot
1285
+ // repoint a dangling symlink, and refuses a corrupt container outright — so
1286
+ // printing it for those causes hands the reader a command that does nothing
1287
+ // and teaches them to ignore the next failure too. feedback-sync decides the
1288
+ // remedy where it detects the cause and ships it as `buildErrorRemedy`; this
1289
+ // just prints it (one judgment, several consumers).
1290
+ fail(
1291
+ 'Feedback projection',
1292
+ `${name}: ${t.buildError} — the projection cannot be built, so NO rules are loaded from it ` +
1293
+ `and every sync is a silent no-op. ${t.buildErrorRemedy || ''}`.trimEnd(),
1294
+ );
1295
+ } else if (buildErr) {
1135
1296
  warn('Feedback projection', buildErr[1].buildError);
1136
1297
  } else if (targets.find(([, t]) => t.overCap)) {
1137
1298
  warn('Feedback projection', 'projection over cap — demote/archive feedback pages');
@@ -1144,7 +1305,23 @@ function checkFeedbackProjection(hypoDir, claudeHome, projectId) {
1144
1305
  }
1145
1306
  }
1146
1307
 
1147
- // 3) unresolved project-id is a separate, non-fatal concern (MEMORY skipped)
1308
+ // 3) side-file I/O problems: a WARN, and additive rather than part of the
1309
+ // if/else chain above — an unreadable feedback_<slug>.md copy is orthogonal
1310
+ // to the primary target's health. It must not fail: the index line still
1311
+ // projects, every rule still loads, and the remedy is a permission bit on a
1312
+ // named path — nothing `--ensure-container` (the build-failed remedy) can
1313
+ // touch. feedback-sync already put the path in the message.
1314
+ for (const [name, t] of targets) {
1315
+ for (const w of t.sideWarnings || []) {
1316
+ warn(
1317
+ 'Feedback projection side file',
1318
+ `${name}: ${w} — fix the permissions on that path; the primary projection still loads ` +
1319
+ `(\`--ensure-container\` does not fix this)`,
1320
+ );
1321
+ }
1322
+ }
1323
+
1324
+ // 4) unresolved project-id is a separate, non-fatal concern (MEMORY skipped)
1148
1325
  if (report.projectIdResolved === false) {
1149
1326
  warn(
1150
1327
  'Feedback projection',
@@ -1187,6 +1364,121 @@ function checkStaleSibling() {
1187
1364
  }
1188
1365
  }
1189
1366
 
1367
+ // ── package integrity ─────────────────────────────────────────────────────────
1368
+ //
1369
+ // ~/.claude/hypo-pkg.json is a snapshot written once by init/upgrade and never
1370
+ // re-validated: nothing tracks whether pkgRoot still exists, whether its
1371
+ // package.json version still matches the recorded pkgVersion, or whether
1372
+ // pkgRoot points at a dev checkout instead of a distributed copy. Command and
1373
+ // skill script resolution was unified onto this one pointer, so a stale or
1374
+ // dev-pointing pkgRoot means the wiki silently runs uncommitted WIP.
1375
+ // All three checks are WARN, never FAIL — a dogfooding maintainer must be
1376
+ // able to ignore them on purpose.
1377
+ function checkPkgIntegrity(claudeHome) {
1378
+ const pkgPath = join(claudeHome, 'hypo-pkg.json');
1379
+ const meta = readPkgJson(pkgPath);
1380
+ // Distinguish "no metadata file at all" (a fresh install — non-actionable, stay
1381
+ // silent) from "a file is present but has no pkgRoot" (incomplete metadata that
1382
+ // breaks runtime package resolution — warn rather than silently reading as OK).
1383
+ // readPkgJson renames a corrupt file aside and returns {}, so after that the path
1384
+ // no longer exists and this correctly falls into the silent fresh-install branch.
1385
+ if (!existsSync(pkgPath)) return;
1386
+ if (!meta || !meta.pkgRoot) {
1387
+ warn(
1388
+ 'hypo-pkg.json pkgRoot',
1389
+ `hypo-pkg.json has no pkgRoot field — metadata is incomplete; re-run /hypo:init or /hypo:upgrade`,
1390
+ );
1391
+ return;
1392
+ }
1393
+
1394
+ const { pkgRoot, pkgVersion } = meta;
1395
+
1396
+ if (!existsSync(pkgRoot)) {
1397
+ warn(
1398
+ 'hypo-pkg.json pkgRoot exists',
1399
+ `${pkgRoot} does not exist — metadata is stale; re-run /hypo:init or /hypo:upgrade`,
1400
+ );
1401
+ return;
1402
+ }
1403
+ pass('hypo-pkg.json pkgRoot exists', pkgRoot);
1404
+
1405
+ let actualVersion = null;
1406
+ let pkgJsonReadable = false;
1407
+ try {
1408
+ actualVersion = JSON.parse(readFileSync(join(pkgRoot, 'package.json'), 'utf-8')).version;
1409
+ pkgJsonReadable = true;
1410
+ } catch {
1411
+ warn('hypo-pkg.json version match', `${pkgRoot}/package.json is unreadable — cannot verify`);
1412
+ }
1413
+ // Every reachable state emits exactly one line: a silent skip here would read as
1414
+ // "verified" on the health report when nothing was actually checked.
1415
+ if (actualVersion) {
1416
+ if (pkgVersion && actualVersion !== pkgVersion) {
1417
+ warn(
1418
+ 'hypo-pkg.json version match',
1419
+ `recorded pkgVersion is ${pkgVersion}, but pkgRoot's package.json is v${actualVersion} — ` +
1420
+ `stale metadata; re-run /hypo:init or /hypo:upgrade`,
1421
+ );
1422
+ } else if (pkgVersion) {
1423
+ pass('hypo-pkg.json version match', `v${actualVersion}`);
1424
+ } else {
1425
+ warn(
1426
+ 'hypo-pkg.json version match',
1427
+ `metadata records no pkgVersion — cannot verify against pkgRoot's v${actualVersion}; ` +
1428
+ `re-run /hypo:init or /hypo:upgrade`,
1429
+ );
1430
+ }
1431
+ } else if (pkgJsonReadable) {
1432
+ warn(
1433
+ 'hypo-pkg.json version match',
1434
+ `${pkgRoot}/package.json has no "version" field — cannot verify`,
1435
+ );
1436
+ }
1437
+
1438
+ // dev-repo check: pkgRoot is a git working tree that is dirty or sits on a
1439
+ // commit with no exact release tag. Normal users should point at a
1440
+ // distributed copy (plugin cache or npm install), not a source checkout.
1441
+ if (existsSync(join(pkgRoot, '.git'))) {
1442
+ const status = spawnSync('git', ['-C', pkgRoot, 'status', '--porcelain'], {
1443
+ encoding: 'utf-8',
1444
+ });
1445
+ const tag = spawnSync('git', ['-C', pkgRoot, 'describe', '--tags', '--exact-match'], {
1446
+ encoding: 'utf-8',
1447
+ });
1448
+ // A non-zero `git describe` exit is the real "HEAD is not an exact tag" signal,
1449
+ // but a spawn failure (git not installed, ENOENT) sets `.error` / a null status
1450
+ // too. Treat those as "cannot classify" instead of silently mislabeling the
1451
+ // pkgRoot as an untagged dev checkout. `git status --porcelain` must exit 0 on
1452
+ // any real repo (clean OR dirty), so a non-zero status there means the repo is
1453
+ // corrupt/inaccessible — also "cannot classify", not "untagged". `git describe`
1454
+ // legitimately exits non-zero when HEAD carries no exact tag, so only its spawn
1455
+ // (error/null status) disqualifies it, not a non-zero exit.
1456
+ const gitUsable =
1457
+ status.error == null && status.status === 0 && tag.error == null && tag.status != null;
1458
+ if (!gitUsable) {
1459
+ warn(
1460
+ 'hypo-pkg.json pkgRoot install kind',
1461
+ `pkgRoot has a .git directory but git could not be run — cannot classify the install kind`,
1462
+ );
1463
+ return;
1464
+ }
1465
+ const dirty = status.stdout.trim().length > 0; // gitUsable already asserts status 0
1466
+ const untagged = tag.status !== 0;
1467
+ if (dirty || untagged) {
1468
+ const reasons = [];
1469
+ if (dirty) reasons.push('uncommitted changes');
1470
+ if (untagged) reasons.push('HEAD is not an exact release tag');
1471
+ warn(
1472
+ 'hypo-pkg.json pkgRoot install kind',
1473
+ `pkgRoot looks like a development checkout (${reasons.join(', ')}) — normal users should ` +
1474
+ `point at a distributed copy (plugin cache or npm install), not a source repo`,
1475
+ );
1476
+ } else {
1477
+ pass('hypo-pkg.json pkgRoot install kind', 'pkgRoot is a clean, tagged checkout');
1478
+ }
1479
+ }
1480
+ }
1481
+
1190
1482
  // ── main ─────────────────────────────────────────────────────────────────────
1191
1483
 
1192
1484
  const args = parseArgs(process.argv);
@@ -1197,11 +1489,13 @@ const rootOk = checkHypoRoot(args.hypoDir);
1197
1489
  if (rootOk) {
1198
1490
  checkDirectories(args.hypoDir);
1199
1491
  checkFiles(args.hypoDir);
1492
+ checkScanIgnoreFile(args.hypoDir);
1200
1493
  checkBrokenLinks(args.hypoDir, ignorePatterns);
1201
1494
  checkVerifyBy(args.hypoDir, ignorePatterns);
1202
1495
  }
1203
- checkHooks();
1204
- checkSettingsJson();
1496
+ checkHooks(coreManagedByPlugin);
1497
+ checkSettingsJson(coreManagedByPlugin);
1498
+ checkPkgIntegrity(args.claudeHome);
1205
1499
  checkStaleSibling();
1206
1500
  if (args.codex) checkCodexPaths();
1207
1501
  if (rootOk) checkExtensions(args.hypoDir, args.claudeHome, 'claude');
@@ -1209,6 +1503,7 @@ if (rootOk && args.codex) checkExtensions(args.hypoDir, args.claudeHome, 'codex'
1209
1503
  if (rootOk) checkProjectIndexAnchors(args.hypoDir);
1210
1504
  if (rootOk) checkSyncState(args.hypoDir);
1211
1505
  if (rootOk) checkProjectSuggestions(args.hypoDir);
1506
+ if (rootOk) checkProposals(args.hypoDir);
1212
1507
  if (rootOk) checkFeedbackProjection(args.hypoDir, args.claudeHome, args.projectId);
1213
1508
  checkGit(args.hypoDir);
1214
1509