hypomnema 1.7.3 → 1.7.4
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 +3 -3
- package/README.md +3 -3
- package/commands/capture.md +1 -1
- package/commands/crystallize.md +2 -2
- package/commands/uninstall.md +16 -4
- package/docs/ARCHITECTURE.md +1 -1
- package/docs/CONTRIBUTING.md +13 -4
- package/hooks/close-gate-store.mjs +435 -0
- package/hooks/hooks.json +2 -1
- package/hooks/hypo-close-guard.mjs +24 -4
- package/hooks/hypo-personal-check.mjs +1 -1
- package/hooks/hypo-session-end.mjs +21 -2
- package/hooks/hypo-shared.mjs +257 -161
- package/package.json +2 -1
- package/scripts/capture.mjs +15 -4
- package/scripts/crystallize.mjs +56 -8
- package/scripts/init.mjs +26 -11
- package/scripts/lib/design-history-stale.mjs +26 -7
- package/scripts/lib/extensions.mjs +60 -16
- package/scripts/lib/git-hooks-dir.mjs +129 -2
- package/scripts/lib/slug-resolver.mjs +181 -0
- package/scripts/lint.mjs +27 -23
- package/scripts/rename.mjs +38 -141
- package/scripts/uninstall.mjs +155 -58
- package/skills/crystallize/SKILL.md +2 -2
- package/templates/hypo-config.md +1 -1
package/scripts/uninstall.mjs
CHANGED
|
@@ -18,12 +18,21 @@
|
|
|
18
18
|
* same rules as init/lint/query)
|
|
19
19
|
* --shell-config=<path> Shell rc file to strip the shell block from (default: checks both
|
|
20
20
|
* ~/.zshrc and ~/.bashrc, since init may have run under either shell)
|
|
21
|
+
* --keep-shell Skip the shell rc `claude()` block removal entirely
|
|
22
|
+
* --keep-wiki-hook Skip the wiki pre-commit hook removal entirely
|
|
21
23
|
*
|
|
22
24
|
* The wiki's git pre-commit hook and the shell rc's `claude()` wrapper function are removed
|
|
23
25
|
* only when they still carry the marker init.mjs wrote (WIKI_PRE_COMMIT_MARKER_START /
|
|
24
26
|
* SHELL_MARKER_START, both from ./lib/git-hooks-dir.mjs). A user's own pre-commit hook, a
|
|
25
27
|
* symlinked hook target, and any rc content outside the marker block are never touched.
|
|
26
28
|
*
|
|
29
|
+
* --hooks-dir only redirects where the ~/.claude/hooks/*.mjs removal looks; it does NOT bound
|
|
30
|
+
* the rc-block and wiki-hook removals above, since those live outside ~/.claude entirely and a
|
|
31
|
+
* hypo-dir override already exists for the latter. A caller that wants an uninstall run scoped
|
|
32
|
+
* to a throwaway hooks dir (a sandboxed CI check, for instance) and NOT touching the real
|
|
33
|
+
* machine's shell rc files or scanning for a real vault must say so explicitly with
|
|
34
|
+
* --keep-shell --keep-wiki-hook.
|
|
35
|
+
*
|
|
27
36
|
* Extensions: hypo-ext-* hard-copies under
|
|
28
37
|
* ~/.claude/{hooks,commands,skills,agents}/ and ~/.codex/{hooks,commands}/ (with
|
|
29
38
|
* --codex) are removed when their on-disk SHA matches the recorded one in
|
|
@@ -69,6 +78,11 @@ import { removeProvenanceSidecar } from './lib/pkg-provenance.mjs';
|
|
|
69
78
|
import {
|
|
70
79
|
hooksDirForInstall,
|
|
71
80
|
unsafeHookTargetReason,
|
|
81
|
+
findMarkerSpan,
|
|
82
|
+
isOwnedWikiPreCommitBody,
|
|
83
|
+
isOwnedShellFunctionBody,
|
|
84
|
+
canonicalize,
|
|
85
|
+
isInside,
|
|
72
86
|
WIKI_PRE_COMMIT_MARKER_START,
|
|
73
87
|
WIKI_PRE_COMMIT_MARKER_END,
|
|
74
88
|
SHELL_MARKER_START,
|
|
@@ -455,54 +469,19 @@ function stripExtensionSettings(settingsPath, hooksDir, apply, ownedCommands = n
|
|
|
455
469
|
return { stripped };
|
|
456
470
|
}
|
|
457
471
|
|
|
458
|
-
// ── marker-span validation (shared by both removal paths below) ────────────
|
|
459
|
-
|
|
460
|
-
// Two independent indexOf() calls cannot tell "well-formed" apart from
|
|
461
|
-
// "duplicated" or "swapped": if a file happens to hold two full copies of the
|
|
462
|
-
// block, indexOf finds only the first END, so slicing [firstStart, firstEnd]
|
|
463
|
-
// leaves the second copy's install behind with no report of it. If END
|
|
464
|
-
// precedes START (a hand-edited or corrupted file), slicing [start, end) with
|
|
465
|
-
// start > end does not error, it silently duplicates whatever sits between
|
|
466
|
-
// them into the "removed" span. Neither this script nor the file it is
|
|
467
|
-
// touching has a way back from either outcome, so a span is only trusted when
|
|
468
|
-
// both markers appear EXACTLY once and START comes before END.
|
|
469
|
-
function countOccurrences(content, needle) {
|
|
470
|
-
let count = 0;
|
|
471
|
-
let idx = 0;
|
|
472
|
-
while ((idx = content.indexOf(needle, idx)) !== -1) {
|
|
473
|
-
count++;
|
|
474
|
-
idx += needle.length;
|
|
475
|
-
}
|
|
476
|
-
return count;
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
function findMarkerSpan(content, startMarker, endMarker) {
|
|
480
|
-
const startCount = countOccurrences(content, startMarker);
|
|
481
|
-
const endCount = countOccurrences(content, endMarker);
|
|
482
|
-
if (startCount !== 1 || endCount !== 1) {
|
|
483
|
-
return {
|
|
484
|
-
ok: false,
|
|
485
|
-
reason: `expected exactly one start and one end marker, found ${startCount} start / ${endCount} end`,
|
|
486
|
-
};
|
|
487
|
-
}
|
|
488
|
-
const startIdx = content.indexOf(startMarker);
|
|
489
|
-
const endIdx = content.indexOf(endMarker);
|
|
490
|
-
if (!(startIdx < endIdx)) {
|
|
491
|
-
return { ok: false, reason: 'the end marker appears before the start marker' };
|
|
492
|
-
}
|
|
493
|
-
return { ok: true, startIdx, endIdx };
|
|
494
|
-
}
|
|
495
|
-
|
|
496
472
|
// ── wiki pre-commit hook removal ────────────────────────────────────────────
|
|
497
473
|
|
|
498
474
|
// Mirrors init.mjs's own resolution (hooksDirForInstall) as the PRIMARY
|
|
499
475
|
// candidate, so this finds the hook wherever init would put it today,
|
|
500
476
|
// including under a core.hooksPath override. A second, best-effort candidate
|
|
501
|
-
// (the vault's plain .git/hooks/pre-commit) is checked too
|
|
502
|
-
//
|
|
503
|
-
//
|
|
504
|
-
//
|
|
505
|
-
//
|
|
477
|
+
// (the vault's plain .git/hooks/pre-commit) is checked too, but it only
|
|
478
|
+
// recovers ONE specific drift: an install that went to the default .git/hooks
|
|
479
|
+
// and had core.hooksPath pointed elsewhere afterward. It does not recover the
|
|
480
|
+
// general case (an install that went to a non-default core.hooksPath which was
|
|
481
|
+
// then repointed somewhere else again), since the only fallback candidate is
|
|
482
|
+
// the plain .git/hooks path, never the original custom one. Both candidates go
|
|
483
|
+
// through the same marker/ownership gate below, so widening the search costs
|
|
484
|
+
// nothing in safety, only in how many places we bother to look.
|
|
506
485
|
// A hook that still carries our marker is removed; a user's own pre-commit (no
|
|
507
486
|
// marker), a symlinked/non-regular target, and a hook whose marker is
|
|
508
487
|
// duplicated, swapped, or missing its shebang are all left standing.
|
|
@@ -512,7 +491,9 @@ function removeWikiPreCommitHook(hypoDir, apply) {
|
|
|
512
491
|
const result = { removed: [], skipped: [], bakPresent: [] };
|
|
513
492
|
|
|
514
493
|
if (!hypoDir || !existsSync(join(hypoDir, 'hypo-config.md'))) {
|
|
515
|
-
result.skipped.push(
|
|
494
|
+
result.skipped.push(
|
|
495
|
+
`no Hypomnema vault found${hypoDir ? ` at ${hypoDir}` : ''} — nothing to remove`,
|
|
496
|
+
);
|
|
516
497
|
return result;
|
|
517
498
|
}
|
|
518
499
|
|
|
@@ -527,7 +508,23 @@ function removeWikiPreCommitHook(hypoDir, apply) {
|
|
|
527
508
|
// correctly via the primary candidate above).
|
|
528
509
|
const legacyGitDir = join(hypoDir, '.git');
|
|
529
510
|
if (existsSync(legacyGitDir) && statSync(legacyGitDir).isDirectory()) {
|
|
530
|
-
|
|
511
|
+
const legacyHooksDir = join(legacyGitDir, 'hooks');
|
|
512
|
+
// Canonicalize before trusting this candidate. The primary path above
|
|
513
|
+
// already refuses to write through a `.git/hooks` that resolves outside
|
|
514
|
+
// the repo (resolveGitHooksDir's `owned` check), but that refusal does
|
|
515
|
+
// nothing for THIS fallback, which built its candidate by string join,
|
|
516
|
+
// not by resolving anything. If `.git/hooks` (or any ancestor of it) is
|
|
517
|
+
// itself a symlink to an external directory, the join above still points
|
|
518
|
+
// there, and the leaf-only unsafeHookTargetReason() below cannot see it:
|
|
519
|
+
// lstat on the FINAL path component says nothing about a symlink the OS
|
|
520
|
+
// already followed to reach that component. Codex reproduced exactly
|
|
521
|
+
// this (2026-08-27): a symlinked `.git/hooks` pointing at an external
|
|
522
|
+
// directory let this fallback delete a file the primary path had
|
|
523
|
+
// already, correctly, refused to touch.
|
|
524
|
+
const resolvedCandidate = canonicalize(legacyHooksDir);
|
|
525
|
+
if (isInside(resolvedCandidate, canonicalize(legacyGitDir))) {
|
|
526
|
+
candidateDirs.push(legacyHooksDir);
|
|
527
|
+
}
|
|
531
528
|
}
|
|
532
529
|
|
|
533
530
|
const seen = new Set();
|
|
@@ -547,9 +544,6 @@ function removeWikiPreCommitHook(hypoDir, apply) {
|
|
|
547
544
|
if (seen.has(key)) continue;
|
|
548
545
|
seen.add(key);
|
|
549
546
|
|
|
550
|
-
const bakPath = `${hookPath}.bak`;
|
|
551
|
-
if (existsSync(bakPath)) result.bakPresent.push(bakPath);
|
|
552
|
-
|
|
553
547
|
const unsafe = unsafeHookTargetReason(hookPath);
|
|
554
548
|
if (unsafe) {
|
|
555
549
|
result.skipped.push(`${hookPath} (${unsafe})`);
|
|
@@ -564,11 +558,22 @@ function removeWikiPreCommitHook(hypoDir, apply) {
|
|
|
564
558
|
result.skipped.push(`${hookPath} (cannot read: ${e.code || e.message})`);
|
|
565
559
|
continue;
|
|
566
560
|
}
|
|
567
|
-
if (
|
|
561
|
+
if (
|
|
562
|
+
!content.includes(WIKI_PRE_COMMIT_MARKER_START) ||
|
|
563
|
+
!content.includes(WIKI_PRE_COMMIT_MARKER_END)
|
|
564
|
+
) {
|
|
568
565
|
result.skipped.push(`${hookPath} (not managed by Hypomnema — preserving)`);
|
|
569
566
|
continue;
|
|
570
567
|
}
|
|
571
568
|
|
|
569
|
+
// Report the backup only past the ownership gate above: a vault with no
|
|
570
|
+
// Hypomnema hook at all (or a symlinked/unreadable one) can still happen
|
|
571
|
+
// to have a stray pre-commit.bak lying around, and attributing that to
|
|
572
|
+
// "from --force-commands" before confirming this IS a Hypomnema-managed
|
|
573
|
+
// hook would misdescribe someone else's file.
|
|
574
|
+
const bakPath = `${hookPath}.bak`;
|
|
575
|
+
if (existsSync(bakPath)) result.bakPresent.push(bakPath);
|
|
576
|
+
|
|
572
577
|
const span = findMarkerSpan(content, WIKI_PRE_COMMIT_MARKER_START, WIKI_PRE_COMMIT_MARKER_END);
|
|
573
578
|
if (!span.ok) {
|
|
574
579
|
result.skipped.push(`${hookPath} (${span.reason} — preserving)`);
|
|
@@ -589,11 +594,28 @@ function removeWikiPreCommitHook(hypoDir, apply) {
|
|
|
589
594
|
const before = content.slice(0, span.startIdx);
|
|
590
595
|
const after = content.slice(span.endIdx + WIKI_PRE_COMMIT_MARKER_END.length);
|
|
591
596
|
if (!/^#![^\n]*\n$/.test(before)) {
|
|
592
|
-
result.skipped.push(
|
|
597
|
+
result.skipped.push(
|
|
598
|
+
`${hookPath} (hook carries content before the Hypomnema block — preserving)`,
|
|
599
|
+
);
|
|
593
600
|
continue;
|
|
594
601
|
}
|
|
595
602
|
if (after.trim() !== '') {
|
|
596
|
-
result.skipped.push(
|
|
603
|
+
result.skipped.push(
|
|
604
|
+
`${hookPath} (hook carries content after the Hypomnema block — preserving)`,
|
|
605
|
+
);
|
|
606
|
+
continue;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// A well-formed span (one start, one end, in order) with a bare shebang
|
|
610
|
+
// before it and nothing after proves only the SHAPE around the block is
|
|
611
|
+
// ours. It says nothing about what is INSIDE the block — a marker pair
|
|
612
|
+
// can be hand-copied around arbitrary content, including a user's own
|
|
613
|
+
// check (codex BLOCKER, 2026-08-27). Refuse unless the body itself is
|
|
614
|
+
// recognizable as what wikiPreCommitContent() writes.
|
|
615
|
+
if (!isOwnedWikiPreCommitBody(content, span)) {
|
|
616
|
+
result.skipped.push(
|
|
617
|
+
`${hookPath} (marker span present but its body does not match the hook Hypomnema writes — preserving)`,
|
|
618
|
+
);
|
|
597
619
|
continue;
|
|
598
620
|
}
|
|
599
621
|
|
|
@@ -601,6 +623,26 @@ function removeWikiPreCommitHook(hypoDir, apply) {
|
|
|
601
623
|
result.removed.push(hookPath);
|
|
602
624
|
}
|
|
603
625
|
|
|
626
|
+
// Every candidate came back plain-absent (existsSync(hookPath) was false for
|
|
627
|
+
// all of them): nothing was removed, and nothing was skipped-with-a-reason
|
|
628
|
+
// either, so silence here would read as "there was nothing to say" when it
|
|
629
|
+
// actually means "the fallback above did not find a marked hook anywhere it
|
|
630
|
+
// looked". Report that explicitly instead of just going quiet — and name the
|
|
631
|
+
// one drift this cannot recover from: if core.hooksPath pointed somewhere
|
|
632
|
+
// else at install time and has since been repointed AGAIN (custom to
|
|
633
|
+
// custom, not the default-to-custom case the fallback above does cover),
|
|
634
|
+
// the hook Hypomnema wrote is still sitting at that first custom path,
|
|
635
|
+
// still executable, and can fail a future commit there with no cleanup
|
|
636
|
+
// path from this run.
|
|
637
|
+
if (candidateDirs.length > 0 && result.removed.length === 0 && result.skipped.length === 0) {
|
|
638
|
+
result.skipped.push(
|
|
639
|
+
`no Hypomnema-marked pre-commit hook found in ${candidateDirs.join(' or ')} — if core.hooksPath ` +
|
|
640
|
+
`pointed somewhere else at install time and has since changed again, the hook Hypomnema wrote ` +
|
|
641
|
+
`may still be sitting at that earlier path; it will keep running on every commit there and can ` +
|
|
642
|
+
`fail commits until it is removed by hand`,
|
|
643
|
+
);
|
|
644
|
+
}
|
|
645
|
+
|
|
604
646
|
return result;
|
|
605
647
|
}
|
|
606
648
|
|
|
@@ -626,7 +668,23 @@ function removeShellFunctionBlock(shellConfigPath, apply) {
|
|
|
626
668
|
return { path: shellConfigPath, removed: false, skipped: span.reason };
|
|
627
669
|
}
|
|
628
670
|
|
|
629
|
-
|
|
671
|
+
// A well-formed span proves only that a start and an end marker exist in
|
|
672
|
+
// order — nothing about what sits between them. A marker pair copy-pasted
|
|
673
|
+
// around a user's own function (or appended to, inside the same span) would
|
|
674
|
+
// pass every check above and get removed along with that user's code
|
|
675
|
+
// (codex BLOCKER, 2026-08-27). Refuse unless the body between the markers
|
|
676
|
+
// is byte-identical to what init.mjs installs.
|
|
677
|
+
if (!isOwnedShellFunctionBody(content, span)) {
|
|
678
|
+
return {
|
|
679
|
+
path: shellConfigPath,
|
|
680
|
+
removed: false,
|
|
681
|
+
skipped:
|
|
682
|
+
'marker span present but its body does not match the shell function Hypomnema installs',
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
const updated =
|
|
687
|
+
content.slice(0, span.startIdx) + content.slice(span.endIdx + SHELL_MARKER_END.length);
|
|
630
688
|
if (apply) writeFileSync(shellConfigPath, updated);
|
|
631
689
|
return { path: shellConfigPath, removed: true, skipped: null };
|
|
632
690
|
}
|
|
@@ -642,12 +700,16 @@ function parseArgs(argv) {
|
|
|
642
700
|
forceExtensions: false,
|
|
643
701
|
hypoDir: null,
|
|
644
702
|
shellConfig: null,
|
|
703
|
+
keepShell: false,
|
|
704
|
+
keepWikiHook: false,
|
|
645
705
|
};
|
|
646
706
|
for (const arg of argv.slice(2)) {
|
|
647
707
|
if (arg === '--apply') args.apply = true;
|
|
648
708
|
else if (arg === '--codex') args.codex = true;
|
|
649
709
|
else if (arg === '--force-commands') args.forceCommands = true;
|
|
650
710
|
else if (arg === '--force-extensions') args.forceExtensions = true;
|
|
711
|
+
else if (arg === '--keep-shell') args.keepShell = true;
|
|
712
|
+
else if (arg === '--keep-wiki-hook') args.keepWikiHook = true;
|
|
651
713
|
else if (arg.startsWith('--hooks-dir=')) args.hooksDir = arg.slice(12);
|
|
652
714
|
// expandHome mirrors init.mjs's own --hypo-dir/--shell-config parsing
|
|
653
715
|
// (init.mjs's parseArgs) exactly, reusing the same function from
|
|
@@ -795,6 +857,22 @@ function stripSettingsJson(settingsPath, hooksDir, hookMap, apply) {
|
|
|
795
857
|
const args = parseArgs(process.argv);
|
|
796
858
|
const dryRun = !args.apply;
|
|
797
859
|
|
|
860
|
+
// --hooks-dir only redirects the ~/.claude/hooks/*.mjs cleanup below (see the
|
|
861
|
+
// module doc comment). A caller who passes it alone, expecting the run to
|
|
862
|
+
// stay confined to that throwaway directory, is surprised when the real
|
|
863
|
+
// machine's shell rc files and auto-resolved wiki vault get touched too
|
|
864
|
+
// (codex CONCERN, 2026-08-27). Warn before any of the removal functions run,
|
|
865
|
+
// not just in the final report, since by the time that report prints under
|
|
866
|
+
// --apply the files are already gone.
|
|
867
|
+
if (args.hooksDir && !args.keepShell && !args.keepWikiHook) {
|
|
868
|
+
console.error(
|
|
869
|
+
`⚠ --hooks-dir only redirects the ~/.claude/hooks/*.mjs cleanup. This run will still ` +
|
|
870
|
+
`${dryRun ? 'inspect' : 'modify'} the real shell rc files (~/.zshrc, ~/.bashrc, or --shell-config) ` +
|
|
871
|
+
`and the auto-resolved wiki vault's pre-commit hook. Pass --keep-shell and/or --keep-wiki-hook to ` +
|
|
872
|
+
`scope this run to --hooks-dir only.`,
|
|
873
|
+
);
|
|
874
|
+
}
|
|
875
|
+
|
|
798
876
|
const { hookMap, hookFiles } = loadHookFiles();
|
|
799
877
|
|
|
800
878
|
const claudeHooksDir = args.hooksDir ?? join(HOME, '.claude', 'hooks');
|
|
@@ -805,16 +883,35 @@ const settingsResult = stripSettingsJson(claudeSettings, claudeHooksDir, hookMap
|
|
|
805
883
|
const commandResult = removeCommands(args.apply, args.forceCommands);
|
|
806
884
|
|
|
807
885
|
// Wiki-side cleanup: the git pre-commit hook and the shell rc block init.mjs
|
|
808
|
-
// installs outside ~/.claude entirely. Both are independent of --codex/--hooks-dir
|
|
809
|
-
|
|
810
|
-
|
|
886
|
+
// installs outside ~/.claude entirely. Both are independent of --codex/--hooks-dir:
|
|
887
|
+
// --hooks-dir only redirects the ~/.claude/hooks/*.mjs removal above, it does not
|
|
888
|
+
// bound these two, since they live outside ~/.claude and already have their own
|
|
889
|
+
// scoping flags (--hypo-dir, --shell-config). A caller that wants an uninstall run
|
|
890
|
+
// confined to a throwaway --hooks-dir and NOT touching the real machine's shell rc
|
|
891
|
+
// files or scanning for a real vault (a sandboxed CI check, for instance) says so
|
|
892
|
+
// explicitly with --keep-shell / --keep-wiki-hook rather than relying on
|
|
893
|
+
// --hooks-dir to imply it.
|
|
894
|
+
// resolveHypoRoot() scans a fixed list of candidate directories under HOME
|
|
895
|
+
// (see ./lib/hypo-root.mjs). --keep-wiki-hook says the caller does not want
|
|
896
|
+
// this run touching a vault at all, so the scan itself is skipped rather than
|
|
897
|
+
// run and then discarded — matching the module doc comment above ("--keep-
|
|
898
|
+
// wiki-hook" is described as skipping the cleanup outright, not as skipping
|
|
899
|
+
// only the removal after still resolving a root).
|
|
900
|
+
const hypoDir = args.keepWikiHook ? null : (args.hypoDir ?? resolveHypoRoot());
|
|
901
|
+
const preCommitResult = args.keepWikiHook
|
|
902
|
+
? {
|
|
903
|
+
removed: [],
|
|
904
|
+
skipped: ['--keep-wiki-hook passed: wiki pre-commit hook cleanup skipped'],
|
|
905
|
+
bakPresent: [],
|
|
906
|
+
}
|
|
907
|
+
: removeWikiPreCommitHook(hypoDir, args.apply);
|
|
811
908
|
|
|
812
909
|
const shellConfigCandidates = args.shellConfig
|
|
813
910
|
? [args.shellConfig]
|
|
814
911
|
: [join(HOME, '.zshrc'), join(HOME, '.bashrc')];
|
|
815
|
-
const shellBlockOutcomes =
|
|
816
|
-
|
|
817
|
-
.filter(Boolean);
|
|
912
|
+
const shellBlockOutcomes = args.keepShell
|
|
913
|
+
? []
|
|
914
|
+
: shellConfigCandidates.map((p) => removeShellFunctionBlock(p, args.apply)).filter(Boolean);
|
|
818
915
|
const shellBlockResults = shellBlockOutcomes.filter((r) => r.removed).map((r) => r.path);
|
|
819
916
|
const shellBlockSkipped = shellBlockOutcomes.filter((r) => !r.removed);
|
|
820
917
|
|
|
@@ -50,8 +50,8 @@ If `/hypo:crystallize` was invoked as a session-close action, run through this c
|
|
|
50
50
|
Surface each of these four to the user first. Every one is **advisory** (identity guard): the user confirms or declines, and none performs an automatic action, writes a file on its own, or bypasses the mandatory gate.
|
|
51
51
|
|
|
52
52
|
- **Trivial-session check (#44)** — Was this session trivial (a single bug fix, a single-file edit, or Q&A with no durable artifact)? If so, recommend skipping session-close: *"이 세션은 trivial해 보입니다 — session-close를 건너뛸까요?"* A trivial skip is a recommendation, **not** a bypass: it must not mark the session closed, must not run `--mark-session-closed`, and must not claim `/compact` can pass. Any real close still requires all 5 mandatory files.
|
|
53
|
-
- **ADR-candidate check (#41)
|
|
54
|
-
- **design-history staleness check (#42)
|
|
53
|
+
- **ADR-candidate check (#41).** Did this session make an architectural or design decision (a new pattern, a tradeoff, a convention)? If yes, ask whether it warrants an ADR and capture that intent in the session-log entry. If nothing rose to ADR level, you may record the literal marker `ADR 없음: <one-line reason>` in that same session-log entry, but gate it on #42's bar, not this one: the marker is machine-read and W8 treats a session-log entry carrying `ADR 없음` (and no ADR reference) as a *no-design* session, excluding it from the design-history staleness check. So write `ADR 없음` only when the session had no design change at all. If it had a sub-ADR design shift (background, tradeoff, or differentiation), append to design-history (#42a) instead: writing the marker there would suppress the W8 nudge that shift needs. **Never auto-write an ADR file.** The session-log note is the only action here. This check carries no `decisions/` directory precondition: run it whether or not that directory exists.
|
|
54
|
+
- **design-history staleness check (#42).** Two branches, so a stale W8 never blocks a clean close: (a) if this session changed design decisions that `projects/<name>/design-history.md` does not yet reflect (including background, tradeoff, or differentiation shifts that are below ADR level but still belong in the ledger), recommend appending to it now: W8 flags this mechanically, and an active-project W8 hard-blocks at PreCompact, so append before you commit, not after the gate fires. **If the file does not exist yet and this session had a design change, recommend creating it now** with that change as the first entry; lint separately flags a missing-but-needed file as W14, a warning that never blocks. (b) only if this session made **no** design change at all does the `ADR 없음` marker from #41 exempt the entry from W8; do **not** touch design-history, and do not create the file just to satisfy this branch's check. Caution: `ADR 없음` means "no design change," which is a stricter bar than "no ADR-level decision." A session with a sub-ADR design shift should take branch (a) and append; writing `ADR 없음` there would suppress the W8 nudge it actually needs. Never auto-write the file yourself in either branch: recommend it, and let the user decide.
|
|
55
55
|
- **Ingest check (#43)** — Did this session consume trustworthy external knowledge (a fetched URL, official docs, or code you verified directly)? If so, recommend running `/hypo:ingest` to capture it under `sources/`. Proceed only on the user's confirmation.
|
|
56
56
|
|
|
57
57
|
When uncertain, surface the question rather than skip it. None of the four blocks the close or writes on its own.
|