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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.ko.md +39 -14
- package/README.md +39 -14
- package/commands/capture.md +8 -6
- package/commands/crystallize.md +39 -20
- package/docs/ARCHITECTURE.md +49 -14
- package/docs/CONTRIBUTING.md +31 -29
- package/hooks/base-store.mjs +265 -0
- package/hooks/hooks.json +7 -1
- package/hooks/hypo-auto-minimal-crystallize.mjs +63 -20
- package/hooks/hypo-auto-stage.mjs +30 -0
- package/hooks/hypo-cwd-change.mjs +31 -2
- package/hooks/hypo-file-watch.mjs +21 -2
- package/hooks/hypo-first-prompt.mjs +19 -3
- package/hooks/hypo-lookup.mjs +86 -29
- package/hooks/hypo-personal-check.mjs +24 -3
- package/hooks/hypo-session-record.mjs +2 -3
- package/hooks/hypo-session-start.mjs +89 -7
- package/hooks/hypo-shared.mjs +904 -128
- package/hooks/proposal-store.mjs +513 -0
- package/package.json +40 -14
- package/scripts/capture.mjs +556 -37
- package/scripts/crystallize.mjs +639 -108
- package/scripts/doctor.mjs +304 -9
- package/scripts/feedback-sync.mjs +515 -44
- package/scripts/graph.mjs +9 -2
- package/scripts/init.mjs +230 -34
- package/scripts/lib/extensions.mjs +656 -1
- package/scripts/lib/hypo-ignore.mjs +54 -6
- package/scripts/lib/hypo-root.mjs +56 -6
- package/scripts/lib/page-usage.mjs +15 -2
- package/scripts/lib/pkg-json.mjs +40 -0
- package/scripts/lib/plugin-detect.mjs +96 -6
- package/scripts/lib/wd-match.mjs +23 -5
- package/scripts/lib/wikilink.mjs +32 -6
- package/scripts/lint.mjs +20 -4
- package/scripts/proposal.mjs +1032 -0
- package/scripts/query.mjs +25 -4
- package/scripts/resume.mjs +34 -12
- package/scripts/stats.mjs +28 -8
- package/scripts/uninstall.mjs +141 -6
- package/scripts/upgrade.mjs +197 -15
- package/skills/crystallize/SKILL.md +44 -7
- package/skills/debate/SKILL.md +88 -0
- package/skills/debate/references/orchestration-patterns.md +83 -0
- package/templates/.hyposcanignore +10 -0
- package/templates/SCHEMA.md +12 -0
- package/templates/gitignore +5 -0
- package/templates/hypo-config.md +1 -1
- package/templates/hypo-guide.md +6 -0
- package/scripts/.gitkeep +0 -0
- package/scripts/check-bilingual.mjs +0 -153
- package/scripts/check-readme-version.mjs +0 -126
- package/scripts/check-tracker-ids.mjs +0 -426
- package/scripts/check-versions.mjs +0 -171
- package/scripts/install-git-hooks.mjs +0 -293
- package/scripts/lib/changelog-classify.mjs +0 -216
- package/scripts/lib/check-bilingual.mjs +0 -244
- package/scripts/lib/check-tracker-ids.mjs +0 -217
- package/scripts/lib/pre-commit-format.mjs +0 -251
- package/scripts/pre-commit-format.mjs +0 -198
package/scripts/doctor.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
1133
|
-
|
|
1134
|
-
|
|
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)
|
|
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
|
|