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/upgrade.mjs
CHANGED
|
@@ -49,9 +49,11 @@ import {
|
|
|
49
49
|
sha256 as sha256Buf,
|
|
50
50
|
isRegularFile,
|
|
51
51
|
readFileIfRegular,
|
|
52
|
+
readVersionAtRoot,
|
|
53
|
+
writeDualSkipProvenance,
|
|
52
54
|
} from './lib/pkg-json.mjs';
|
|
53
55
|
import { syncExtensions } from './lib/extensions.mjs';
|
|
54
|
-
import { isHypomnemaPluginEnabled } from './lib/plugin-detect.mjs';
|
|
56
|
+
import { isHypomnemaPluginEnabled, resolveEnabledPluginRoot } from './lib/plugin-detect.mjs';
|
|
55
57
|
import { classifyInstall, downgradeGuardMessage } from '../hooks/version-check.mjs';
|
|
56
58
|
|
|
57
59
|
const HOME = homedir();
|
|
@@ -213,26 +215,58 @@ const SHARED_FILES = _hookConfig.shared ?? [];
|
|
|
213
215
|
|
|
214
216
|
// ── checks ───────────────────────────────────────────────────────────────────
|
|
215
217
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
218
|
+
// Shared by SCHEMA.md and hypo-guide.md: both templates carry a top-level
|
|
219
|
+
// `version:` frontmatter stamp, and both are compared the same way — installed
|
|
220
|
+
// copy vs. the package's own template — without ever writing to hypoPath. This
|
|
221
|
+
// function is read-only; nothing downstream may use its return value to
|
|
222
|
+
// overwrite the installed file (hand-authored wiki files are never
|
|
223
|
+
// silently clobbered).
|
|
224
|
+
function checkTemplateVersion(hypoDir, filename) {
|
|
225
|
+
const pkgPath = join(TEMPLATES, filename);
|
|
226
|
+
const hypoPath = join(hypoDir, filename);
|
|
219
227
|
|
|
220
228
|
const pkgVersion = existsSync(pkgPath)
|
|
221
229
|
? parseVersion((parseFrontmatter(readFileSync(pkgPath, 'utf-8')) ?? {}).version)
|
|
222
230
|
: null;
|
|
223
|
-
|
|
231
|
+
|
|
232
|
+
const hypoExists = existsSync(hypoPath);
|
|
233
|
+
const hypoVersion = hypoExists
|
|
224
234
|
? parseVersion((parseFrontmatter(readFileSync(hypoPath, 'utf-8')) ?? {}).version)
|
|
225
235
|
: null;
|
|
226
236
|
|
|
237
|
+
// File present but carries no `version:` frontmatter field at all — a
|
|
238
|
+
// pre-versioning install (the file predates this stamp existing). This is
|
|
239
|
+
// deliberately distinguished from "file missing entirely" (still routed to
|
|
240
|
+
// bumpType's 'unknown' below): a bare `bumpType(null, pkgVersion)` cannot
|
|
241
|
+
// tell the two apart, and folding this case into plain 'unknown' is exactly
|
|
242
|
+
// what let an already-installed, unstamped hypo-guide.md report "up to
|
|
243
|
+
// date" — the file this check exists to catch. The caller decides whether
|
|
244
|
+
// 'unstamped' counts as drift; this helper only classifies.
|
|
245
|
+
const unstamped = hypoExists && !hypoVersion && !!pkgVersion;
|
|
246
|
+
|
|
227
247
|
return {
|
|
228
248
|
installed: hypoVersion?.raw ?? null,
|
|
229
249
|
current: pkgVersion?.raw ?? null,
|
|
230
|
-
bump: bumpType(hypoVersion, pkgVersion),
|
|
250
|
+
bump: unstamped ? 'unstamped' : bumpType(hypoVersion, pkgVersion),
|
|
231
251
|
hypoPath,
|
|
232
252
|
pkgPath,
|
|
233
253
|
};
|
|
234
254
|
}
|
|
235
255
|
|
|
256
|
+
function checkSchemaVersion(hypoDir) {
|
|
257
|
+
return checkTemplateVersion(hypoDir, 'SCHEMA.md');
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// hypo-guide.md had no update channel at all — upgrade.mjs never
|
|
261
|
+
// looked at it, and init.mjs only ever writes it once (skip if present), so an
|
|
262
|
+
// installed vault's copy went permanently stale the moment the shipped template
|
|
263
|
+
// changed, with no signal to the user. This check restores visibility (a warning
|
|
264
|
+
// on drift) without adding a write path: --apply still never touches an
|
|
265
|
+
// installed hypo-guide.md, exactly like SCHEMA.md's existing Option C contract.
|
|
266
|
+
function checkGuideVersion(hypoDir) {
|
|
267
|
+
return checkTemplateVersion(hypoDir, 'hypo-guide.md');
|
|
268
|
+
}
|
|
269
|
+
|
|
236
270
|
// Target-aware: the same core-hook integrity check runs against ~/.claude/hooks/
|
|
237
271
|
// for the default claude target, and against ~/.codex/hooks/ when --codex is set
|
|
238
272
|
// The function reads only (apply happens in applyHookFiles).
|
|
@@ -479,6 +513,11 @@ const GITIGNORE_REQUIRED_ENTRIES = [
|
|
|
479
513
|
pattern: '.cache/',
|
|
480
514
|
comment: '# Hypomnema runtime cache (page-usage log, session growth metrics)',
|
|
481
515
|
},
|
|
516
|
+
{
|
|
517
|
+
pattern: '*.lock',
|
|
518
|
+
comment:
|
|
519
|
+
'# Append-lock files (transient <target>.lock; a crashed holder can leave a stale one)',
|
|
520
|
+
},
|
|
482
521
|
];
|
|
483
522
|
|
|
484
523
|
function checkGitignore(hypoDir) {
|
|
@@ -836,6 +875,15 @@ function writePluginModeMetadata() {
|
|
|
836
875
|
return true;
|
|
837
876
|
}
|
|
838
877
|
|
|
878
|
+
// readVersionAtRoot and writeDualSkipProvenance now live in ./lib/pkg-json.mjs
|
|
879
|
+
// (imported above) — writeDualSkipProvenance is deliberately not
|
|
880
|
+
// writePluginModeMetadata(): that function hardcodes THIS npm run's
|
|
881
|
+
// PKG_ROOT/PKG_VERSION and always drops `commands` — both correct for true
|
|
882
|
+
// plugin mode, both wrong for a dualSkip correction, where the identity being
|
|
883
|
+
// written is the OTHER install's. Moving both into lib/pkg-json.mjs (alongside
|
|
884
|
+
// the other pkgJsonPath-taking helpers already there) also makes the writer's
|
|
885
|
+
// TOCTOU refusal directly unit-testable without spawning the whole script.
|
|
886
|
+
|
|
839
887
|
// ── main ─────────────────────────────────────────────────────────────────────
|
|
840
888
|
|
|
841
889
|
const args = parseArgs(process.argv);
|
|
@@ -886,9 +934,41 @@ const managesClaudeCore = !pluginMode && (!dualInstallCoreConflict || args.allow
|
|
|
886
934
|
const dualSkip = dualInstallCoreConflict && !args.allowDualInstall;
|
|
887
935
|
|
|
888
936
|
const schema = checkSchemaVersion(args.hypoDir);
|
|
937
|
+
const guide = checkGuideVersion(args.hypoDir);
|
|
889
938
|
const hooks = checkHookFiles(claudeHooksDir);
|
|
890
939
|
const settings = checkSettingsJson(claudeSettingsPath, claudeHooksDir);
|
|
891
940
|
const pkgJson = checkPkgJson();
|
|
941
|
+
// dualSkip provenance self-heal: the registry root the enabled plugin key
|
|
942
|
+
// POSITIVELY resolves to (same lookup init.mjs's resolveDurableRoot uses), or
|
|
943
|
+
// null when the registry is absent/unreadable/corrupt or names no usable entry
|
|
944
|
+
// for the enabled key. Computed unconditionally (fail-open on any uncertainty —
|
|
945
|
+
// see lib/plugin-detect.mjs) so both the --apply write below and the read-only
|
|
946
|
+
// report can agree on the same positively-resolved identity without a second
|
|
947
|
+
// registry read racing the first.
|
|
948
|
+
const dualSkipRegistryRoot = dualSkip
|
|
949
|
+
? resolveEnabledPluginRoot(
|
|
950
|
+
claudeSettingsPath,
|
|
951
|
+
join(HOME, '.claude', 'plugins', 'installed_plugins.json'),
|
|
952
|
+
)
|
|
953
|
+
: null;
|
|
954
|
+
// The recorded pkgRoot currently on disk, read non-mutatingly (readPkgJsonSafe
|
|
955
|
+
// would rename aside a corrupt file — a write — before this script has decided
|
|
956
|
+
// whether to touch anything).
|
|
957
|
+
const dualSkipRecordedRoot = (() => {
|
|
958
|
+
try {
|
|
959
|
+
const v = JSON.parse(readFileSync(pkgJsonPath(), 'utf-8')).pkgRoot;
|
|
960
|
+
return typeof v === 'string' ? v : null;
|
|
961
|
+
} catch {
|
|
962
|
+
return null;
|
|
963
|
+
}
|
|
964
|
+
})();
|
|
965
|
+
// A CONDITIONAL self-heal, never an unconditional rewrite: correct only when the
|
|
966
|
+
// registry positively resolves a usable root that DIFFERS from what's already
|
|
967
|
+
// recorded. An unresolvable registry (null) must PRESERVE the existing pointer,
|
|
968
|
+
// not blank it or fall back to this npm PKG_ROOT — a null here is "cannot prove
|
|
969
|
+
// a correction is needed", not "there is nothing to preserve".
|
|
970
|
+
const dualSkipWouldCorrect =
|
|
971
|
+
dualSkip && dualSkipRegistryRoot !== null && dualSkipRegistryRoot !== dualSkipRecordedRoot;
|
|
892
972
|
const commands = checkCommands();
|
|
893
973
|
const oldHookRefs = checkOldHookNames(claudeSettingsPath);
|
|
894
974
|
const hypoignore = checkHypoignore(args.hypoDir);
|
|
@@ -948,15 +1028,41 @@ const missingSettingsCodex = settingsCodex
|
|
|
948
1028
|
const invalidSettingsCodex = settingsCodex
|
|
949
1029
|
? settingsCodex.some((s) => s.status === 'invalid-json')
|
|
950
1030
|
: false;
|
|
951
|
-
|
|
952
|
-
//
|
|
953
|
-
//
|
|
954
|
-
//
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
1031
|
+
// SCHEMA.md keeps its pre-existing behavior: 'unstamped' is deliberately NOT
|
|
1032
|
+
// counted as drift here (SCHEMA.md is user-owned vocabulary, Option C — an
|
|
1033
|
+
// unstamped SCHEMA.md was already the "cannot compare" case before this
|
|
1034
|
+
// classification existed, and no SCHEMA test exercises it as actionable).
|
|
1035
|
+
const schemaDrift =
|
|
1036
|
+
schema.bump !== 'none' &&
|
|
1037
|
+
schema.bump !== 'unknown' &&
|
|
1038
|
+
schema.bump !== 'ahead' &&
|
|
1039
|
+
schema.bump !== 'unstamped';
|
|
1040
|
+
// hypo-guide.md: 'unstamped' DOES count as drift — this is the exact shape of
|
|
1041
|
+
// an already-installed pre-versioning vault, the population this change targets.
|
|
1042
|
+
// Excluding it (as the old unknown-only check did) is the bug this fixes.
|
|
1043
|
+
const guideDrift = guide.bump !== 'none' && guide.bump !== 'unknown' && guide.bump !== 'ahead';
|
|
1044
|
+
// Dual-install: when core is skipped, hypo-pkg.json is normally left pointing
|
|
1045
|
+
// at the PLUGIN's package root (preserved identity), so checkPkgJson() reports
|
|
1046
|
+
// it 'stale' relative to this npm/manual PKG_ROOT. That mismatch is USUALLY
|
|
1047
|
+
// intentional — `--apply` will not rewrite an already-correct plugin pointer
|
|
1048
|
+
// — so it must not count as actionable drift by default, or the user would be
|
|
1049
|
+
// nagged to run --apply forever. The one exception is dualSkipWouldCorrect: an
|
|
1050
|
+
// npm-first divergence where the recorded pointer differs from what the plugin
|
|
1051
|
+
// registry POSITIVELY resolves (e.g. `npm init`, enable the plugin, `npm init`
|
|
1052
|
+
// again — the npm root stays recorded forever otherwise). THAT case IS
|
|
1053
|
+
// actionable — `--apply` corrects it via writeDualSkipProvenance — so it must
|
|
1054
|
+
// count as drift, or the user has no signal to ever run --apply. dualSkipWouldCorrect
|
|
1055
|
+
// is checked FIRST and unconditionally (not gated behind `pkgJson.status !==
|
|
1056
|
+
// 'up-to-date'`): checkPkgJson() compares the recorded pointer only against
|
|
1057
|
+
// THIS run's own PKG_ROOT, so an npm-first divergence where the recorded
|
|
1058
|
+
// pointer happens to equal PKG_ROOT (but the registry resolves a DIFFERENT
|
|
1059
|
+
// plugin root) would read 'up-to-date' and slip past that status check
|
|
1060
|
+
// entirely — leaving a correctable divergence permanently unreported. A
|
|
1061
|
+
// genuinely missing/corrupt file (status 'missing') is still surfaced (warning
|
|
958
1062
|
// below), because the runtime then cannot resolve its package root at all.
|
|
959
|
-
const pkgJsonDrift =
|
|
1063
|
+
const pkgJsonDrift =
|
|
1064
|
+
dualSkipWouldCorrect ||
|
|
1065
|
+
(pkgJson.status !== 'up-to-date' && !(dualSkip && pkgJson.status === 'stale'));
|
|
960
1066
|
const staleCommands = commands.filter((c) => c.status === 'stale' || c.status === 'missing');
|
|
961
1067
|
const userModifiedCommands = commands.filter((c) => c.status === 'user-modified');
|
|
962
1068
|
const orphanedCommands = commands.filter((c) => c.status === 'orphaned');
|
|
@@ -968,6 +1074,11 @@ let migrationPath = null;
|
|
|
968
1074
|
let appliedHooks = [];
|
|
969
1075
|
let appliedSettings = [];
|
|
970
1076
|
let appliedPkgJson = false;
|
|
1077
|
+
// True only once the dualSkip provenance self-heal actually WROTE a correction
|
|
1078
|
+
// this run (a positively-resolved registry root that differed from the
|
|
1079
|
+
// recorded one). Distinct from dualSkipWouldCorrect (a pre-apply preview
|
|
1080
|
+
// computed above), which stays true even in a dry run.
|
|
1081
|
+
let dualSkipCorrected = false;
|
|
971
1082
|
let appliedHookNameRenames = [];
|
|
972
1083
|
let appliedHooksCodex = [];
|
|
973
1084
|
let appliedSettingsCodex = [];
|
|
@@ -1054,6 +1165,20 @@ if (args.apply) {
|
|
|
1054
1165
|
// user to resolve the dual install.
|
|
1055
1166
|
if (pkgJson.status === 'missing') {
|
|
1056
1167
|
appliedPkgJson = writePluginModeMetadata();
|
|
1168
|
+
} else if (dualSkipWouldCorrect) {
|
|
1169
|
+
// npm-first correction: a usable pkgRoot IS already recorded (status
|
|
1170
|
+
// 'stale' or 'up-to-date'), but the registry positively resolves a
|
|
1171
|
+
// DIFFERENT usable root — e.g. `npm init`, then enable the plugin, then
|
|
1172
|
+
// `npm init` again, leaving the npm root recorded forever. Correct the
|
|
1173
|
+
// provenance to the registry's own root/version. When the registry
|
|
1174
|
+
// cannot positively resolve one (dualSkipWouldCorrect is false — null
|
|
1175
|
+
// registry lookup, or it already matches what's recorded), fall through
|
|
1176
|
+
// to preserving the existing pointer untouched; never blank it.
|
|
1177
|
+
// writeDualSkipProvenance can still refuse here (TOCTOU: the registry
|
|
1178
|
+
// root's own package.json became unreadable between resolution and this
|
|
1179
|
+
// write) — dualSkipCorrected only follows an ACTUAL write, never assume it.
|
|
1180
|
+
appliedPkgJson = writeDualSkipProvenance(pkgJsonPath(), dualSkipRegistryRoot);
|
|
1181
|
+
dualSkipCorrected = appliedPkgJson;
|
|
1057
1182
|
} else {
|
|
1058
1183
|
appliedPkgJson = false;
|
|
1059
1184
|
}
|
|
@@ -1134,6 +1259,7 @@ const hasDrift =
|
|
|
1134
1259
|
(managesClaudeCore && claudeCoreDrift) ||
|
|
1135
1260
|
pkgJsonDrift ||
|
|
1136
1261
|
schemaDrift ||
|
|
1262
|
+
guideDrift ||
|
|
1137
1263
|
hypoignore.status === 'needs-migration' ||
|
|
1138
1264
|
gitignore.status === 'needs-migration' ||
|
|
1139
1265
|
extDrift ||
|
|
@@ -1150,6 +1276,7 @@ if (args.json) {
|
|
|
1150
1276
|
coreManagedBy: managesClaudeCore ? 'self' : pluginMode ? 'plugin' : 'plugin-enabled',
|
|
1151
1277
|
dualInstallOverride: args.allowDualInstall,
|
|
1152
1278
|
schema,
|
|
1279
|
+
guide,
|
|
1153
1280
|
hooks,
|
|
1154
1281
|
settings,
|
|
1155
1282
|
pkgJson,
|
|
@@ -1249,6 +1376,12 @@ if (schema.bump === 'none') {
|
|
|
1249
1376
|
lines.push(
|
|
1250
1377
|
`⚠ SCHEMA version ${schema.installed} (installed is ahead of package ${schema.current})`,
|
|
1251
1378
|
);
|
|
1379
|
+
} else if (schema.bump === 'unstamped') {
|
|
1380
|
+
// Not counted as drift for SCHEMA.md (Option C, unchanged) — still surfaced
|
|
1381
|
+
// as advisory-only visibility, same spirit as the pre-existing 'unknown' text.
|
|
1382
|
+
lines.push(
|
|
1383
|
+
`⚠ SCHEMA version installed has no version stamp (pre-versioning copy), package=${schema.current} (cannot compare automatically)`,
|
|
1384
|
+
);
|
|
1252
1385
|
} else if (schema.bump === 'major') {
|
|
1253
1386
|
lines.push(
|
|
1254
1387
|
`✗ SCHEMA version ${schema.installed} → ${schema.current} [MAJOR — --apply writes MIGRATION report; SCHEMA must be merged manually]`,
|
|
@@ -1259,6 +1392,33 @@ if (schema.bump === 'none') {
|
|
|
1259
1392
|
);
|
|
1260
1393
|
}
|
|
1261
1394
|
|
|
1395
|
+
// hypo-guide.md version stamp. Visibility only — a drift warning,
|
|
1396
|
+
// never a write: --apply must not overwrite an installed hypo-guide.md, since a
|
|
1397
|
+
// user may have customized it (same Option C contract as SCHEMA.md above).
|
|
1398
|
+
if (guide.bump === 'none') {
|
|
1399
|
+
lines.push(`✓ hypo-guide.md v${guide.installed} (up to date)`);
|
|
1400
|
+
} else if (guide.bump === 'unknown') {
|
|
1401
|
+
lines.push(
|
|
1402
|
+
`⚠ hypo-guide.md installed=${guide.installed ?? 'not found (no version stamp)'}, package=${guide.current ?? 'not found'} (cannot compare)`,
|
|
1403
|
+
);
|
|
1404
|
+
} else if (guide.bump === 'ahead') {
|
|
1405
|
+
lines.push(
|
|
1406
|
+
`⚠ hypo-guide.md v${guide.installed} (installed is ahead of package v${guide.current})`,
|
|
1407
|
+
);
|
|
1408
|
+
} else if (guide.bump === 'unstamped') {
|
|
1409
|
+
// The target case: the file exists (this is not a fresh/missing
|
|
1410
|
+
// install) but predates the version stamp entirely. Counted as drift above
|
|
1411
|
+
// (guideDrift) — the message must be actionable, not "cannot compare",
|
|
1412
|
+
// since a pre-versioning copy silently reporting "up to date" was the bug.
|
|
1413
|
+
lines.push(
|
|
1414
|
+
`⚠ hypo-guide.md installed copy has no version stamp (pre-versioning stale copy; package=v${guide.current}) — review templates/hypo-guide.md and update your copy manually; --apply does not overwrite it`,
|
|
1415
|
+
);
|
|
1416
|
+
} else {
|
|
1417
|
+
lines.push(
|
|
1418
|
+
`⚠ hypo-guide.md v${guide.installed} → v${guide.current} [package template changed — review templates/hypo-guide.md and update your copy manually; --apply does not overwrite it]`,
|
|
1419
|
+
);
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1262
1422
|
// Hook files (target-aware so --codex can mirror the same block).
|
|
1263
1423
|
function pushHookSummary(hookList, label, targetPath) {
|
|
1264
1424
|
const colHook = `Hook files${label}`.padEnd(20);
|
|
@@ -1318,7 +1478,28 @@ if (managesClaudeCore) {
|
|
|
1318
1478
|
if (settingsCodex) pushSettingsSummary(settingsCodex, ' (codex)', invalidSettingsCodex);
|
|
1319
1479
|
|
|
1320
1480
|
// Package metadata
|
|
1321
|
-
if (dualSkip &&
|
|
1481
|
+
if (dualSkip && dualSkipCorrected) {
|
|
1482
|
+
// npm-first correction actually WROTE this run: the pre-apply pkgJson
|
|
1483
|
+
// classification above was computed against the running npm PKG_ROOT, so
|
|
1484
|
+
// without this branch a corrected pointer would still be reported as
|
|
1485
|
+
// "preserved — not rewritten" (or "up to date"/"stale") below, contradicting
|
|
1486
|
+
// what --apply just did. Report the explicit outcome instead, with the
|
|
1487
|
+
// registry package's own version — the identity now on disk.
|
|
1488
|
+
const correctedVersion = readVersionAtRoot(dualSkipRegistryRoot);
|
|
1489
|
+
lines.push(
|
|
1490
|
+
`✓ Package metadata hypo-pkg.json corrected to enabled plugin registry root` +
|
|
1491
|
+
(correctedVersion ? ` (v${correctedVersion})` : ''),
|
|
1492
|
+
);
|
|
1493
|
+
} else if (dualSkip && dualSkipWouldCorrect) {
|
|
1494
|
+
// Same divergence, but this was a dry run (no --apply): preview what
|
|
1495
|
+
// --apply would do rather than claiming the identity is already preserved.
|
|
1496
|
+
const previewVersion = readVersionAtRoot(dualSkipRegistryRoot);
|
|
1497
|
+
lines.push(
|
|
1498
|
+
`⚠ Package metadata hypo-pkg.json points at a stale npm/manual root — \`--apply\` will` +
|
|
1499
|
+
` correct it to the enabled plugin registry root` +
|
|
1500
|
+
(previewVersion ? ` (v${previewVersion})` : ''),
|
|
1501
|
+
);
|
|
1502
|
+
} else if (dualSkip && pkgJson.status === 'stale') {
|
|
1322
1503
|
// Dual-install: the 'stale' here is the preserved plugin identity (pkgRoot points at
|
|
1323
1504
|
// the plugin, not this npm/manual copy). That is intentional — not actionable.
|
|
1324
1505
|
lines.push(
|
|
@@ -1572,6 +1753,7 @@ const totalDrift =
|
|
|
1572
1753
|
claudeCoreCount +
|
|
1573
1754
|
(pkgJsonDrift ? 1 : 0) +
|
|
1574
1755
|
(schemaDrift ? 1 : 0) +
|
|
1756
|
+
(guideDrift ? 1 : 0) +
|
|
1575
1757
|
(hypoignore.status === 'needs-migration' ? hypoignore.missing.length : 0) +
|
|
1576
1758
|
(gitignore.status === 'needs-migration' ? gitignore.missing.length : 0) +
|
|
1577
1759
|
extCheck.actions.filter(
|
|
@@ -11,7 +11,7 @@ When invoked to close a session — via an explicit close signal ("세션 종료
|
|
|
11
11
|
|
|
12
12
|
## What this does
|
|
13
13
|
|
|
14
|
-
- **Close mode**: walks the checklist (session-state, project hot.md, root hot.md, session-log, open-questions(변경 시), log.md) plus a lint step, then writes via `/hypo:crystallize` in `--apply-session-close --payload=<path>` mode
|
|
14
|
+
- **Close mode**: walks the checklist (session-state, project hot.md, root hot.md, session-log, open-questions(변경 시), log.md) plus a lint step, then writes via `/hypo:crystallize` in `--apply-session-close --payload=<path> --session-id=<id>` mode, which runs the lint gate automatically, **scoped to the files it writes** (debt elsewhere is a non-blocking notice). **`--session-id` is required.** Before writing a byte, the apply resolves that session's transcript and refuses the whole close, exit 1 and nothing on disk, unless the **user** asked for it (`session-id-required` / `transcript-unresolved` / `no-user-close-signal`). A refusal is the answer, not an obstacle: ask the user, and re-run only if they say yes. `--check-session-close` is a read-only dry-run of the **full** PreCompact gate: close files + scoped lint + design-history + feedback projection, sharing one function (`precompactGateStatus`) with the gate. A green check means no gate blocker needs a human fix, so it is the signal to declare the session closed (pass `--transcript-path` to widen the lint scope to this session's edited files exactly as the interactive hook does). It is not a hard guarantee: the live `/compact` can still differ on a context-≥70% prompt, `HYPO_SKIP_GATE`, or a transcript-scoped lint error the check did not see.
|
|
15
15
|
- **Synthesis mode**: finds tag clusters (≥ N pages), orphan pages (no outbound `[[wikilinks]]`), and draft / stub pages, then guides consolidation into `pages/syntheses/<topic>.md` with back-links and `index.md` updates.
|
|
16
16
|
|
|
17
17
|
---
|
|
@@ -102,15 +102,52 @@ Once it passes, report each item with ✓:
|
|
|
102
102
|
- ✓ session-log entry
|
|
103
103
|
- ✓ open-questions (or skipped if unchanged)
|
|
104
104
|
- ✓ log.md entry
|
|
105
|
-
- **marker written?** (required
|
|
105
|
+
- **marker written?** (required): check `markerWritten` in the JSON output. If `true`, report "session-close marker written." If `false`, do NOT declare the session closed or complete; recover per the branches below.
|
|
106
106
|
|
|
107
|
-
If `markerWritten: true
|
|
107
|
+
If `markerWritten: true`, ask: "Session closed. Would you like to also run knowledge synthesis now, or stop here?"
|
|
108
108
|
|
|
109
|
-
If `
|
|
109
|
+
**If `ok: false` with an authority `reason`, the close did not happen at all.** No file was written, nothing was committed. Do not report a partial close, and do not write the files by hand to get around it.
|
|
110
110
|
|
|
111
|
-
- `
|
|
112
|
-
- `transcript-unresolved
|
|
113
|
-
-
|
|
111
|
+
- `session-id-required`: `--session-id` was omitted. Pass the main conversation's id and re-run.
|
|
112
|
+
- `transcript-unresolved`: the id resolved no transcript, so it is most likely a background-task or Agent-thread uuid rather than the main conversation's. Get the right one and re-run.
|
|
113
|
+
- `no-user-close-signal`: the transcript is this session's, and the user never asked to close in wording the gate recognizes (e.g. "세션 마무리까지 진행해줘" falls outside the close-signal set). Re-running the same id changes nothing. Confirm intent once with `AskUserQuestion` (header "세션", one option **세션 마무리**); if the user picks it, that answer becomes a recognized close signal, so the same command now applies everything: writes, commit, and marker. If the user declines, the session stays open and nothing is written. Do NOT change the close-signal matcher.
|
|
114
|
+
|
|
115
|
+
**If `ok: false` with `stage: "session-id-mismatch"`,** the payload carries a `sessionId` that is not the `--session-id` you passed, so this file was authored for a different session (a reused or overwritten temp path). Nothing was written. Do not force it: rebuild the payload for THIS session (write it to a session-scoped path, e.g. `/tmp/hypo-session-close-<session-id>.json`) and re-run.
|
|
116
|
+
|
|
117
|
+
If the apply succeeded but `markerWritten: false`, branch on `markerSkipReason` (`compact-gate-not-ok`, `commit-failed: …`, `marker-did-not-land`): surface it verbatim and resolve the underlying blocker before re-running.
|
|
118
|
+
|
|
119
|
+
### If the close came back `ok: false, stage: "proposal-pending"`
|
|
120
|
+
|
|
121
|
+
A concurrent session wrote one of the pages this close wants to replace, so the close
|
|
122
|
+
withheld the bytes rather than clobber that session's work, and parked them. Nothing is
|
|
123
|
+
lost and nothing is broken. The close is waiting on a decision only the user makes:
|
|
124
|
+
whether to replace the other session's bytes.
|
|
125
|
+
|
|
126
|
+
Do NOT write the pages with the Write tool and mark the session closed. That path skips
|
|
127
|
+
the store entirely and silently destroys the other session's edits. Drive it out properly:
|
|
128
|
+
|
|
129
|
+
1. **Look at `proposals`.** If the array is EMPTY, no page was parked. The only conflicts
|
|
130
|
+
were append-lock timeouts, which the next close re-reads and re-appends by itself. Just
|
|
131
|
+
re-run the same close command.
|
|
132
|
+
2. **`hypomnema proposal challenge --session-id <id> --ids <id,...>`** with the ids from
|
|
133
|
+
`proposals`. It re-reads each page from disk and prints the diff between what is there
|
|
134
|
+
now and what this close wants to write.
|
|
135
|
+
3. **Show the user those diffs** and tell them plainly what would be overwritten. Then give
|
|
136
|
+
them the approval line the command printed, and ask them to type it:
|
|
137
|
+
|
|
138
|
+
apply-proposals <nonce>
|
|
139
|
+
|
|
140
|
+
They have to TYPE it. A click on `AskUserQuestion` does not approve an overwrite (you
|
|
141
|
+
author the option labels, so a click cannot prove they approved this specific write),
|
|
142
|
+
and the command refuses it. If they would rather keep the other session's page, run
|
|
143
|
+
`hypomnema proposal discard <id>` instead and re-run the close.
|
|
144
|
+
4. **`hypomnema proposal resolve --session-id <id>`** once they have typed it. It verifies
|
|
145
|
+
the approval in the transcript, checks the pages have not moved since they saw the diff,
|
|
146
|
+
and writes them.
|
|
147
|
+
5. **Re-run the close.** `resolve` writes the pages; it does NOT close the session. The
|
|
148
|
+
re-run skips what already landed and finishes the close.
|
|
149
|
+
6. **Check `markerWritten` again** before you say the session is closed. It is never
|
|
150
|
+
automatic.
|
|
114
151
|
|
|
115
152
|
---
|
|
116
153
|
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Structured debate to re-verify a wiki knowledge claim before you trust it, or to harden a hard-to-reverse decision into a durable wiki record. Runs a fixed order (interrogate, then verify against code and docs, then synthesize). Use when hypo:verify flags a page as overdue and you need to actually re-check its claim, when a decision is hard to reverse (architecture, refactor, external API integration) and should leave an ADR, or when the user asks to "debate", "stress-test", or "verify this claim". Skip it for reversible or already-obvious calls.
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
You are running `/hypo:debate`. It formalizes an informal practice (a two-worker adversarial
|
|
6
|
+
review) into a reproducible fixed order, so the same rigor applies every time.
|
|
7
|
+
|
|
8
|
+
The premise: separate the mind that **generates and interrogates** from the mind that
|
|
9
|
+
**verifies**. Interrogation without verification lets a plausible wrong answer survive;
|
|
10
|
+
verification without interrogation confirms a wrong premise with precision. The order is
|
|
11
|
+
fixed for that reason.
|
|
12
|
+
|
|
13
|
+
The debate always ends in a durable wiki artifact: a refreshed page, a correction, or an
|
|
14
|
+
ADR. A debate that leaves nothing on disk was just a conversation. This is the producer
|
|
15
|
+
reviewer pattern applied to a single claim; for the orchestration vocabulary it belongs to,
|
|
16
|
+
see [references/orchestration-patterns.md](references/orchestration-patterns.md).
|
|
17
|
+
|
|
18
|
+
## Two uses
|
|
19
|
+
|
|
20
|
+
1. **Re-verify a wiki claim (primary).** `/hypo:verify` surfaces pages whose `verify_by_date`
|
|
21
|
+
is overdue (or that carry no `verify_by` question at all). This skill is the deep re-check
|
|
22
|
+
behind that prompt: does the page's `verify_by` question still hold against the current
|
|
23
|
+
code and docs? The outcome updates the page exactly as `/hypo:verify` Step 4 does (see
|
|
24
|
+
below), with the debate supplying the evidence.
|
|
25
|
+
2. **Harden a hard-to-reverse decision.** Architecture choices, refactor direction, external
|
|
26
|
+
API or dependency integration, where the first-pass answer is often wrong (concurrency,
|
|
27
|
+
migrations, security boundaries, distributed state). The decision must land as an ADR or a
|
|
28
|
+
wiki page; if it would not, it is not big enough for a debate.
|
|
29
|
+
|
|
30
|
+
## When not to use
|
|
31
|
+
|
|
32
|
+
- A change whose diff you can state in one sentence. Just do it.
|
|
33
|
+
- A reversible call, an already-obvious choice, or a one-off lookup. The debate's fixed cost
|
|
34
|
+
exceeds its value.
|
|
35
|
+
|
|
36
|
+
## Fixed three phases
|
|
37
|
+
|
|
38
|
+
### Phase 1. Interrogate (do not answer yet)
|
|
39
|
+
|
|
40
|
+
Attack the claim or decision. Build a list of questions, not conclusions.
|
|
41
|
+
|
|
42
|
+
- Surface hidden assumptions: "this rests on X being true. What if X is false?"
|
|
43
|
+
- Stand up at least two alternatives (three including the incumbent). Best and worst case of
|
|
44
|
+
each.
|
|
45
|
+
- List failure modes: how does this decision bite six months from now?
|
|
46
|
+
- Output: a list of **falsifiable claims**. Each must be resolvable to true / false by code,
|
|
47
|
+
docs, or a command ("p95 under 50ms", not "it will be fast"). For a wiki claim, restate the
|
|
48
|
+
page's `verify_by` question in a form you can actually check against the current code or
|
|
49
|
+
docs.
|
|
50
|
+
|
|
51
|
+
### Phase 2. Verify (against code, docs, and the wiki)
|
|
52
|
+
|
|
53
|
+
Check each Phase 1 claim, one at a time. Do not invent new claims here.
|
|
54
|
+
|
|
55
|
+
- Read the code, find the docs, run the command. Judge each claim true / false / unknown.
|
|
56
|
+
- Unknown is unknown. A zero-result grep is not proof of absence.
|
|
57
|
+
- For a wiki claim, check it against the page's own cited sources and the code or docs it
|
|
58
|
+
describes, not against memory.
|
|
59
|
+
- For genuine independence, hand this phase to a reviewer who did not run the interrogation
|
|
60
|
+
(a fresh-context subagent, or a second person). Self-review of your own interrogation
|
|
61
|
+
breeds confirmation bias.
|
|
62
|
+
- Output: a **per-claim verdict table** (claim, verdict, evidence line or command).
|
|
63
|
+
|
|
64
|
+
### Phase 3. Synthesize
|
|
65
|
+
|
|
66
|
+
Reconcile Phases 1 and 2 into one outcome, and write it down.
|
|
67
|
+
|
|
68
|
+
- Build the conclusion only from claims that survived verification. For each rejected
|
|
69
|
+
alternative, leave one line on why it fell.
|
|
70
|
+
- Name the residual risk (remaining unknowns, fail-open points) alongside the outcome. Do not
|
|
71
|
+
hide it.
|
|
72
|
+
- Land the durable artifact:
|
|
73
|
+
- Wiki claim: answer the page's `verify_by` question **yes / no / partially**. On yes,
|
|
74
|
+
refresh `last_reviewed` to today and push `verify_by_date` forward. On no or partially,
|
|
75
|
+
edit the page to correct it, then refresh both fields. This is exactly `/hypo:verify`
|
|
76
|
+
Step 4; the debate is what earns the answer.
|
|
77
|
+
- Decision: record it as an ADR (or a wiki page) holding the **decision, rationale, and
|
|
78
|
+
residual risk**. That record, not the discussion, is the deliverable.
|
|
79
|
+
|
|
80
|
+
## Execution notes
|
|
81
|
+
|
|
82
|
+
- Run the three phases inline in order, or split only Phase 2 out to an independent reviewer.
|
|
83
|
+
That independence contributes the most to decision quality (the verifier's fresh context).
|
|
84
|
+
- Keep the boundary between interrogation (1) and verification (2). If a new alternative
|
|
85
|
+
occurs during verification, note it for Phase 1 and finish verifying the current claims
|
|
86
|
+
before opening a new round. Mixing the two blurs both.
|
|
87
|
+
- Do not accept a reviewer's verdict uncritically. The final call at synthesis stays with the
|
|
88
|
+
person running the debate.
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Multi-agent orchestration patterns
|
|
2
|
+
|
|
3
|
+
A shared vocabulary for designing multi-agent work. `/hypo:debate` is one instance of it
|
|
4
|
+
(the producer-reviewer pattern). When you orchestrate several agents around your wiki work,
|
|
5
|
+
name the pattern first, then build.
|
|
6
|
+
|
|
7
|
+
The one selection rule: **does the next stage need a barrier?** If not, pipeline. If it must
|
|
8
|
+
see everything at once, fan-out / fan-in. If several angles look at the same target, expert
|
|
9
|
+
pool. If one agent must grade another's output, producer-reviewer. If one place must hold the
|
|
10
|
+
decision, supervisor. If delegation spawns more delegation, hierarchical.
|
|
11
|
+
|
|
12
|
+
## 1. Pipeline
|
|
13
|
+
|
|
14
|
+
Each item flows through the stages independently. No barrier between stages, so item A can be
|
|
15
|
+
in stage 3 while item B is still in stage 1. With enough parallel capacity, wall-clock trends
|
|
16
|
+
toward the slowest single-item chain rather than the sum of per-stage worsts (setting aside
|
|
17
|
+
pipeline fill and drain). With limited capacity, items queue and it degrades toward the
|
|
18
|
+
barrier case.
|
|
19
|
+
|
|
20
|
+
- Use when: every item runs the same chain and items do not wait on each other (per-file
|
|
21
|
+
transforms, a find then fix then verify done per item).
|
|
22
|
+
- Mistake: adding a barrier because you think a stage needs all prior results. That is
|
|
23
|
+
fan-out / fan-in, not a pipeline. Do any flatten / map / filter inside a stage.
|
|
24
|
+
|
|
25
|
+
## 2. Fan-out / fan-in
|
|
26
|
+
|
|
27
|
+
Spread work in parallel (fan-out), then gather all of it at a barrier (fan-in) to merge,
|
|
28
|
+
dedup, or aggregate. Justified only when stage N needs every result of stage N-1 together.
|
|
29
|
+
|
|
30
|
+
- Use when: dedup across the full set before an expensive downstream step, early-exit when
|
|
31
|
+
the total is zero, or a result that must be compared against all the others.
|
|
32
|
+
- Mistake: forgetting the barrier's wait cost. Fast workers idle waiting on slow ones. Use it
|
|
33
|
+
only when you genuinely need all results.
|
|
34
|
+
|
|
35
|
+
## 3. Expert pool
|
|
36
|
+
|
|
37
|
+
Several agents look at the same target through **different lenses** at once. Each holds a
|
|
38
|
+
different expertise (correctness, security, performance, reproducibility) or a different
|
|
39
|
+
viewpoint, blind to what the others see.
|
|
40
|
+
|
|
41
|
+
- Use when: one search angle cannot find everything, or a defect can break in more than one
|
|
42
|
+
way. The goal is **perspective diversity**, not redundant voting.
|
|
43
|
+
- Mistake: giving N agents the same prompt and calling it an expert pool. That is just
|
|
44
|
+
redundancy. The lenses must actually differ.
|
|
45
|
+
|
|
46
|
+
## 4. Producer-reviewer
|
|
47
|
+
|
|
48
|
+
A producer makes something and an **independent** reviewer verifies it adversarially. Use it
|
|
49
|
+
when the maker should not grade their own work. Prompt the reviewer to find why it is wrong
|
|
50
|
+
first. `/hypo:debate` is this pattern applied to one claim.
|
|
51
|
+
|
|
52
|
+
- Use when: catching defects in a change, a design, or an artifact from a fresh context.
|
|
53
|
+
- Mistake: handing the reviewer the producer's rationale. The reviewer should see the target,
|
|
54
|
+
not the conclusion, to stay independent. Do not accept the review uncritically; the final
|
|
55
|
+
call stays with the orchestrator.
|
|
56
|
+
|
|
57
|
+
## 5. Supervisor
|
|
58
|
+
|
|
59
|
+
One orchestrator delegates to workers and integrates the results. The **decision and the
|
|
60
|
+
human gate stay with the orchestrator**; only the heavy execution goes down.
|
|
61
|
+
|
|
62
|
+
- Use when: keeping the main context for decisions while isolating large raw material or
|
|
63
|
+
diffs in subagents. When you delegate, hand over everything the main context knows: the
|
|
64
|
+
goal, the read / write scope, the constraints already confirmed, the verify command, the
|
|
65
|
+
return schema.
|
|
66
|
+
- Mistake: treating a worker's output as evidence. It is a candidate. Trust it blindly and
|
|
67
|
+
you get a phantom backlog.
|
|
68
|
+
|
|
69
|
+
## 6. Hierarchical delegation
|
|
70
|
+
|
|
71
|
+
A tree of delegation: a higher worker re-delegates to lower ones. Splits a large problem into
|
|
72
|
+
sub-problems handled by depth.
|
|
73
|
+
|
|
74
|
+
- Use when: a breadth or depth one orchestrator cannot hold (a large migration, a broad
|
|
75
|
+
audit).
|
|
76
|
+
- Mistake: allowing unbounded nesting depth. Keep the hierarchy to a controlled one or two
|
|
77
|
+
levels, and do not let a subagent open its own sub-delegation without a reason.
|
|
78
|
+
|
|
79
|
+
## When not to reach for multi-agent at all
|
|
80
|
+
|
|
81
|
+
If the main context already holds the material and the task finishes faster than the delegation
|
|
82
|
+
overhead, do it inline. Before picking a pattern, ask whether the work is worth splitting. Most
|
|
83
|
+
single lookups are not.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# .hyposcanignore — patterns excluded from scan (lint/graph/stats/query/verify/doctor) only
|
|
2
|
+
# One glob per line. Lines starting with # are comments.
|
|
3
|
+
# Matched files are skipped by catalog scans but are still committed and still
|
|
4
|
+
# readable by hooks. For secrets and anything that must never be committed or
|
|
5
|
+
# surfaced by a hook, use .hypoignore instead — this file does NOT block commits.
|
|
6
|
+
# Syntax follows .gitignore glob rules.
|
|
7
|
+
|
|
8
|
+
# Draft / scratch (uncomment to hide from scans without blocking their commit)
|
|
9
|
+
# drafts/
|
|
10
|
+
# scratch/
|
package/templates/SCHEMA.md
CHANGED
|
@@ -91,11 +91,23 @@ Optional fields (add as needed):
|
|
|
91
91
|
confidence: high | medium | low | speculative
|
|
92
92
|
evidence_strength: direct | inferred | hearsay
|
|
93
93
|
scope: always | project | session
|
|
94
|
+
visibility_scope: shared | machine:<device>
|
|
94
95
|
source: <slug or URL>
|
|
95
96
|
verify_by: <question to re-check at next review>
|
|
96
97
|
verify_by_date: YYYY-MM-DD
|
|
97
98
|
```
|
|
98
99
|
|
|
100
|
+
`visibility_scope` is a different axis from `scope`: `scope` is memory lifetime,
|
|
101
|
+
`visibility_scope` is where a page may surface. Omitting it means `shared` (the
|
|
102
|
+
implicit default of every pre-existing page), so lookup/query/injection stay
|
|
103
|
+
unchanged. `machine:<device>` hides the page from every machine except the one
|
|
104
|
+
whose `currentDevice()` output equals `<device>`. Copy that exact string from the
|
|
105
|
+
newest `device` value in `.cache/sessions/index.jsonl`, or from a recent
|
|
106
|
+
session-log shard's `device:` frontmatter (both are `currentDevice()` outputs); a
|
|
107
|
+
hand-typed value that does not match will hide the page on its own machine too.
|
|
108
|
+
An empty owner (`machine:`) hides the page everywhere. `agent:<id>` is a reserved
|
|
109
|
+
forward-compat value that is not filtered yet.
|
|
110
|
+
|
|
99
111
|
### 3.1. `feedback` type — projection fields
|
|
100
112
|
|
|
101
113
|
Feedback pages are the single source of truth for behavior corrections;
|
package/templates/gitignore
CHANGED
|
@@ -3,3 +3,8 @@
|
|
|
3
3
|
|
|
4
4
|
# Runtime cache: sync-state.json contains hostname() and timestamps
|
|
5
5
|
.cache/
|
|
6
|
+
|
|
7
|
+
# Append-lock files: transient <target>.lock guards around session-log / log.md
|
|
8
|
+
# writes (crystallize apply, deriveRootLogEntries). Normally deleted immediately;
|
|
9
|
+
# a crashed holder can leave a stale one, which must never be committed.
|
|
10
|
+
*.lock
|