hypomnema 1.8.0 → 1.8.1
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/CHANGELOG.md +16 -0
- package/README.ko.md +3 -1
- package/README.md +3 -1
- package/commands/resume.md +1 -0
- package/docs/ARCHITECTURE.md +19 -5
- package/docs/CONTRIBUTING.md +13 -6
- package/hooks/hypo-session-start.mjs +175 -0
- package/package.json +1 -1
- package/scripts/init.mjs +5 -1
- package/scripts/lib/crystallize-close-apply.mjs +679 -445
- package/scripts/lint.mjs +76 -28
- package/scripts/resume.mjs +177 -2
- package/templates/hypo-config.md +1 -1
- package/skills/crystallize/SKILL.md +0 -189
- package/skills/graph/SKILL.md +0 -58
- package/skills/ingest/SKILL.md +0 -107
- package/skills/lint/SKILL.md +0 -59
- package/skills/query/SKILL.md +0 -62
- package/skills/verify/SKILL.md +0 -96
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,22 @@ All notable changes to Hypomnema are documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.8.1] - 2026-09-07
|
|
9
|
+
|
|
10
|
+
### Bug Fixes
|
|
11
|
+
|
|
12
|
+
#### English
|
|
13
|
+
|
|
14
|
+
- Six slash names (`crystallize`, `graph`, `ingest`, `lint`, `query`, `verify`) shipped as both a `commands/*.md` file and a `skills/*/SKILL.md` directory, so each registered twice as a component and the command palette repeated them. Only the `commands/` half ever loaded, so the duplicates are removed with no behavior change, and `smoke:plugin` now fails on a name present in both directories (decisions/0100). ([#282](https://github.com/sk-lim19f/Hypomnema/pull/282))
|
|
15
|
+
- A session close now checks that the root files it writes still carry a usable frontmatter block, and refuses a close that would leave invalid YAML in one of them. ([#281](https://github.com/sk-lim19f/Hypomnema/pull/281))
|
|
16
|
+
- `/hypo:resume` and SessionStart now surface uncommitted vault work that belongs to another project, so a session does not silently adopt it. ([#280](https://github.com/sk-lim19f/Hypomnema/pull/280))
|
|
17
|
+
|
|
18
|
+
#### 한국어
|
|
19
|
+
|
|
20
|
+
- 여섯 슬래시 이름(`crystallize`, `graph`, `ingest`, `lint`, `query`, `verify`)이 `commands/*.md` 파일과 `skills/*/SKILL.md` 디렉터리로 둘 다 출하되어 component 가 두 번씩 등록되고 명령 팔레트가 그만큼 반복됐습니다. 실제로 로드된 것은 `commands/` 쪽뿐이라 중복을 동작 변화 없이 지웠고, 이제 한 이름이 두 디렉터리에 다 있으면 `smoke:plugin` 이 실패합니다 (decisions/0100). ([#282](https://github.com/sk-lim19f/Hypomnema/pull/282))
|
|
21
|
+
- 세션 마무리가 자기가 쓰는 루트 파일에 쓸 만한 프론트매터 블록이 남는지 검사하고, 그중 하나에 깨진 YAML 을 남길 마무리를 거절합니다. ([#281](https://github.com/sk-lim19f/Hypomnema/pull/281))
|
|
22
|
+
- `/hypo:resume` 과 SessionStart 가 다른 프로젝트 소관의 미커밋 변경을 한 줄로 알려, 세션이 그것을 조용히 자기 일로 삼지 않게 합니다. ([#280](https://github.com/sk-lim19f/Hypomnema/pull/280))
|
|
23
|
+
|
|
8
24
|
## [1.8.0] - 2026-09-03
|
|
9
25
|
|
|
10
26
|
### New Features
|
package/README.ko.md
CHANGED
|
@@ -289,7 +289,9 @@ PostToolUse 훅 둘은 matcher 없이 등록되고 각자 tool_name으로 거릅
|
|
|
289
289
|
|
|
290
290
|
### Claude Agent Skills
|
|
291
291
|
|
|
292
|
-
합성이 핵심인
|
|
292
|
+
`commands/` 아래의 모든 파일이 그 자체로 Claude Agent Skill입니다. 대화 내용이 그 `description`과 맞으면 슬래시 명령 없이도 자동으로 호출됩니다. (플러그인 설치에서 실측한 것입니다. npm 경로가 설치하는 `~/.claude/commands/hypo/` 사본도 같은 방식으로 자동 호출되는지는 재 보지 않았습니다.) 합성이 핵심인 여섯(`ingest`, `query`, `crystallize`, `lint`, `verify`, `graph`)에서 이 동작이 가장 잘 드러납니다. `debate`만 반대쪽에 있습니다. `commands/` 아래에 파일이 없고 `skills/debate/SKILL.md`에만 있는데, 플러그인이 `skills/`도 함께 싣기 때문에 `/hypo:debate`는 나머지와 똑같이 동작합니다. 세 단계(심문, 검증, 종합)로 구조화된 검토를 실행해 위키 주장을 재검증하거나 되돌리기 어려운 결정을 ADR로 굳힙니다. 위 명령어 표에 없는 것은 그 표가 `commands/` 아래 파일을 세기 때문이지 칠 수 있는 것을 세기 때문이 아닙니다. 명령어를 정확히 몰라도 하려는 일을 말로 적으면 됩니다.
|
|
293
|
+
|
|
294
|
+
이름 하나는 두 디렉터리 중 한 곳에서만 출하됩니다. flat `commands/<name>.md`와 디렉터리 `skills/<name>/SKILL.md`는 둘 다 `/hypo:<name>`을 차지하고, 둘을 함께 실어도 사람용 표면과 모델용 표면으로 갈리지 않습니다. 한쪽만 이기고 나머지는 죽은 무게가 됩니다. 충돌이 있으면 `npm run smoke:plugin`이 실패합니다. 이 검사는 이 패키지의 `commands/`와 `skills/`만 봅니다. `~/hypomnema/extensions/`에 둔 직접 만든 확장은 검사하지 않으므로, 스스로 캡처한 `commands/mine.md`와 `skills/mine/`은 여전히 함께 동기화되어 같은 방식으로 충돌할 수 있습니다.
|
|
293
295
|
|
|
294
296
|
| 이렇게 말하면 | 트리거되는 스킬 |
|
|
295
297
|
|---|---|
|
package/README.md
CHANGED
|
@@ -287,7 +287,9 @@ Every maintenance command also runs from a plain shell, which is what you need w
|
|
|
287
287
|
|
|
288
288
|
### Claude Agent Skills
|
|
289
289
|
|
|
290
|
-
|
|
290
|
+
Every file under `commands/` is itself a Claude Agent Skill, so any of them auto-triggers when the conversation matches its description, with no slash command required. (Measured on a plugin install; whether the npm path's `~/.claude/commands/hypo/` copies auto-trigger the same way has not been measured.) You will notice this most on the synthesis-heavy ones (`ingest`, `query`, `crystallize`, `lint`, `verify`, `graph`). `debate` is the one that lives the other way around, in `skills/debate/SKILL.md` with no file under `commands/`; the plugin loads `skills/` too, so `/hypo:debate` works like the rest. It runs a structured three-phase review (interrogate, verify, synthesize) to re-verify a wiki claim or harden a hard-to-reverse decision into an ADR. It is absent from the command tables above because those count the files under `commands/`, not what you can type.
|
|
291
|
+
|
|
292
|
+
Each name ships from exactly one of the two directories. A flat `commands/<name>.md` and a directory `skills/<name>/SKILL.md` both claim `/hypo:<name>`, and shipping the pair does not give you one surface for people and another for the model: only one wins and the other is dead weight. `npm run smoke:plugin` fails on a collision. That gate covers this package's own `commands/` and `skills/`; your own extensions under `~/hypomnema/extensions/` are not checked, so a `commands/mine.md` and a `skills/mine/` you captured yourself can still both sync and collide the same way.
|
|
291
293
|
|
|
292
294
|
| Say this | Skill it triggers |
|
|
293
295
|
|---|---|
|
package/commands/resume.md
CHANGED
|
@@ -29,6 +29,7 @@ When `--project` is omitted, the script prefers the project whose `working_dir`
|
|
|
29
29
|
## Step 2 — Present session state
|
|
30
30
|
|
|
31
31
|
Show the output from the script:
|
|
32
|
+
- **`[WIKI: ...]` notice**, if present, first and verbatim. It names uncommitted changes elsewhere in the vault (another project, or unattributable to any project) that are not this session's scope. Do not fold this line into the project summary or fetch/edit the paths it names without the user's explicit instruction.
|
|
32
33
|
- **Project** name
|
|
33
34
|
- **Next tasks** from `session-state.md`
|
|
34
35
|
- **Background** from `hot.md` (what was done last session, condensed)
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -47,7 +47,7 @@ hypomnema/
|
|
|
47
47
|
│ ├── frontmatter.mjs
|
|
48
48
|
│ ├── hypo-root.mjs
|
|
49
49
|
│ └── hypo-ignore.mjs
|
|
50
|
-
├── skills/ ← Agent Skills
|
|
50
|
+
├── skills/ ← directory-form Agent Skills (skills/<name>/SKILL.md)
|
|
51
51
|
├── templates/ ← baseline files copied on init
|
|
52
52
|
│ ├── hypo-config.md, index.md, hot.md, log.md, SCHEMA.md, hypo-guide.md
|
|
53
53
|
│ ├── Home.md, Overview.md, hypo-automation.md, hypo-help.md
|
|
@@ -79,15 +79,29 @@ Each user-facing operation is a pair: an LLM-facing prompt + a Node.js script.
|
|
|
79
79
|
|
|
80
80
|
**Commands shipped in v1.1:** `init`, `doctor`, `upgrade`, `uninstall`, `ingest`, `query`, `crystallize`, `resume`, `feedback`, `verify`, `lint`, `stats`, `graph`.
|
|
81
81
|
|
|
82
|
-
|
|
82
|
+
Every one of those files is itself an Agent Skill; see below.
|
|
83
83
|
|
|
84
84
|
---
|
|
85
85
|
|
|
86
86
|
## Agent Skills
|
|
87
87
|
|
|
88
|
-
`
|
|
88
|
+
A flat `commands/<name>.md` and a directory `skills/<name>/SKILL.md` are the same kind of
|
|
89
|
+
component to the plugin loader: both are Agent Skills, both claim `/hypo:<name>`, and both
|
|
90
|
+
auto-load when a conversation matches their `description`. (That is measured on a plugin
|
|
91
|
+
install. `init`/`upgrade` do not deploy the plugin's own `skills/` at all, so on the npm path
|
|
92
|
+
only `commands/` is installed — into `~/.claude/commands/hypo/` — and whether those copies
|
|
93
|
+
auto-trigger the same way has not been measured.) The directory form is what a skill needs when
|
|
94
|
+
it carries more than one file (`skills/debate/references/`); the flat form is everything else.
|
|
89
95
|
|
|
90
|
-
|
|
96
|
+
**One name ships from one directory, never both** (in this package; the extensions channel
|
|
97
|
+
under `~/hypomnema/extensions/` is not covered by the gate and can still sync a co-named pair)**.** Shipping a pair does not give you a
|
|
98
|
+
human-facing surface and a model-facing surface: nothing in the frontmatter separates them, only
|
|
99
|
+
one wins, and the loser is dead weight that still costs always-on tokens and still appears in
|
|
100
|
+
`claude plugin details`. Six pairs shipped that way from v1.0.0 until 2026-09-04, and `commands/`
|
|
101
|
+
won every one of them. `scripts/smoke-plugin.mjs` now fails the build on a collision.
|
|
102
|
+
|
|
103
|
+
> v1.0 originally planned flat `skills/*.md` files and shipped `<name>/SKILL.md` instead; both
|
|
104
|
+
> forms have loaded since v1.0.0. The 2026-09-04 correction is that they are not two surfaces.
|
|
91
105
|
|
|
92
106
|
---
|
|
93
107
|
|
|
@@ -385,7 +399,7 @@ If `git status` shows no `.md` changes, the diff step is skipped — Stop hook f
|
|
|
385
399
|
|
|
386
400
|
### Citation convention
|
|
387
401
|
|
|
388
|
-
The
|
|
402
|
+
**The convention is current.** It is stated in `commands/query.md`, in `commands/crystallize.md`, and in `templates/hypo-guide.md` §8, which `init` writes into a vault that does not already have it (an existing copy is left alone). What went away on 2026-09-04 is one particular copy of it: an identical footer written into six `skills/*/SKILL.md` files, which were the losing half of the name collision above and never reached anyone. Whether to repeat that footer in the remaining `commands/*.md` is open. The observability audit never depended on it either way: it scores sessions on tool and command usage recorded by `hypo-session-record` (search / ingest / feedback counts), not on inline citations.
|
|
389
403
|
|
|
390
404
|
---
|
|
391
405
|
|
package/docs/CONTRIBUTING.md
CHANGED
|
@@ -45,12 +45,12 @@ See [ARCHITECTURE.md](ARCHITECTURE.md) for the full breakdown. Short version:
|
|
|
45
45
|
|
|
46
46
|
| Path | What lives there |
|
|
47
47
|
|---|---|
|
|
48
|
-
| `commands/` | LLM-facing prompts for `/hypo:*` slash commands |
|
|
48
|
+
| `commands/` | LLM-facing prompts for `/hypo:*` slash commands — each is also an Agent Skill (auto-triggers on its `description`) |
|
|
49
49
|
| `scripts/` | Node.js implementations called by the commands |
|
|
50
50
|
| `scripts/lib/` | Shared helpers for scripts (`frontmatter`, `hypo-root`, `hypo-ignore`) |
|
|
51
51
|
| `hooks/` | Lifecycle hooks + `hooks.json` registry |
|
|
52
52
|
| `hooks/hypo-shared.mjs` | Shared hook utilities — read the deployment constraint below |
|
|
53
|
-
| `skills/<name>/SKILL.md` | Agent Skills
|
|
53
|
+
| `skills/<name>/SKILL.md` | Directory-form Agent Skills — only for a skill needing more than one file (`skills/debate/references/`). The name must not collide with `commands/` |
|
|
54
54
|
| `templates/` | Files copied into new wiki vaults on init |
|
|
55
55
|
| `tests/` | Test suite (no external deps): `harness.mjs`, `helpers.mjs`, `runner.mjs` (entry), and one `<area>.test.mjs` per production area |
|
|
56
56
|
| `docs/` | ARCHITECTURE, CONTRIBUTING |
|
|
@@ -77,7 +77,7 @@ Scripts under `scripts/` are not deployed and may freely import from `scripts/li
|
|
|
77
77
|
|
|
78
78
|
1. Edit `commands/<name>.md` — the LLM-facing prompt.
|
|
79
79
|
2. Edit `scripts/<name>.mjs` — the Node.js logic.
|
|
80
|
-
3.
|
|
80
|
+
3. Do **not** add `skills/<name>/SKILL.md` for it. A `commands/*.md` file is already an Agent Skill and auto-triggers on its `description`; a same-named directory skill claims the same `/hypo:<name>`, and `npm run smoke:plugin` fails on the collision. To change when it auto-triggers, edit that `description`.
|
|
81
81
|
4. Update the command table in `README.md` and `README.ko.md`.
|
|
82
82
|
5. Add coverage to the matching `tests/<area>.test.mjs` (a new command usually means a new area file).
|
|
83
83
|
|
|
@@ -109,10 +109,17 @@ If you need to share new logic, prefer extending an existing helper over adding
|
|
|
109
109
|
|
|
110
110
|
### Adding an Agent Skill
|
|
111
111
|
|
|
112
|
-
|
|
113
|
-
|
|
112
|
+
Use the directory form only when the skill needs more than one file (`skills/debate/references/`).
|
|
113
|
+
Everything else belongs in `commands/`, which loads the same way.
|
|
114
|
+
|
|
115
|
+
1. Create `skills/<name>/SKILL.md`. **The name must not collide with any `commands/<name>.md`** —
|
|
116
|
+
both claim `/hypo:<name>`, only one wins, and `npm run smoke:plugin` fails on the pair.
|
|
117
|
+
2. The frontmatter must include a `description` carrying the trigger criteria. `name` is
|
|
118
|
+
optional — omit it and the directory name is the invocation name (`skills/debate/SKILL.md`
|
|
119
|
+
does this). If you do set it, it must equal the directory name; `smoke:plugin` fails otherwise.
|
|
114
120
|
3. The skill body is the LLM prompt — keep it focused on the synthesis task.
|
|
115
|
-
4.
|
|
121
|
+
4. Describe it in the **Claude Agent Skills** section of `README.md` and `README.ko.md`, the way
|
|
122
|
+
`debate` is described there. (There is no separate skills inventory table.)
|
|
116
123
|
|
|
117
124
|
---
|
|
118
125
|
|
|
@@ -484,6 +484,170 @@ function pendingProposalNotice() {
|
|
|
484
484
|
return '';
|
|
485
485
|
}
|
|
486
486
|
}
|
|
487
|
+
// ── foreign-project uncommitted notice ──────────────────────────────────────
|
|
488
|
+
// Same signal precompactGateStatus already computes for its own gate
|
|
489
|
+
// (closeAccountableScope / sessionTouchTrusted, hypo-shared.mjs), surfaced
|
|
490
|
+
// here instead for the AGENT reading additionalContext: a project this
|
|
491
|
+
// session isn't scoped to may still have uncommitted changes sitting in the
|
|
492
|
+
// shared vault, and without this line the agent has no way to tell those
|
|
493
|
+
// apart from its own unfinished work. Cannot import hypo-shared.mjs's
|
|
494
|
+
// `projectOfPath` / `gitDirtyFiles` here, not a technical constraint (both
|
|
495
|
+
// are simply not exported), but an ownership decision: hypo-shared.mjs
|
|
496
|
+
// belongs to a different lane, so this hook keeps a local, self-contained
|
|
497
|
+
// duplicate rather than adding an export for it. The cost is real: a future
|
|
498
|
+
// fix to gitDirtyFiles (e.g. its rename re-attribution) does not propagate
|
|
499
|
+
// here automatically.
|
|
500
|
+
|
|
501
|
+
const FOREIGN_GIT_TIMEOUT_MS = 3000;
|
|
502
|
+
// Cap on how many foreign project names the notice spells out. Past this the
|
|
503
|
+
// rest collapse into a count, so one session cannot grow the prompt by however
|
|
504
|
+
// many projects are dirty.
|
|
505
|
+
const FOREIGN_NAME_CAP = 5;
|
|
506
|
+
|
|
507
|
+
/** `projects/<slug>/...` → `<slug>`; everything else → null. `null` here does
|
|
508
|
+
* not mean "not one project's work": the caller below folds every non-null
|
|
509
|
+
* hit into a per-name foreign count and every null hit into a nameless
|
|
510
|
+
* "unattributed" count, and both feed the same notice. Not the same
|
|
511
|
+
* classification hypo-auto-commit.mjs's commit message uses for its own
|
|
512
|
+
* "(N paths across M projects)" count (hypo-shared.mjs's private
|
|
513
|
+
* `projectOfPath`): that one folds a non-`projects/` path to its first path
|
|
514
|
+
* segment (`extensions`, `hot.md`, ...) for a tally; this one folds it to
|
|
515
|
+
* `null` because attribution, not tallying, is the job here. A top-level
|
|
516
|
+
* segment is not a project name, and this notice must not present it as
|
|
517
|
+
* one.
|
|
518
|
+
*/
|
|
519
|
+
function projectOfPath(relPath) {
|
|
520
|
+
const parts = relPath.split('/');
|
|
521
|
+
return parts[0] === 'projects' && parts.length > 1 && parts[1] ? parts[1] : null;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/** Vault-relative dirty paths (tracked + untracked), normalized to be
|
|
525
|
+
* relative to `hypoDir` itself via `git rev-parse --show-prefix` (empty when
|
|
526
|
+
* `hypoDir` IS the repo top level), the same normalization
|
|
527
|
+
* hypo-shared.mjs's `gitDirtyFiles` applies for staging correctness: without
|
|
528
|
+
* it, a vault nested under a larger host repo reports paths relative to that
|
|
529
|
+
* repo's top level, and every one of them would fail to classify as this
|
|
530
|
+
* vault's own. NUL-separated porcelain so Korean project/page names survive
|
|
531
|
+
* intact. This also re-attributes a rename/copy's `from` path (codex
|
|
532
|
+
* 3rd-round review follow-up), the same as gitDirtyFiles does for
|
|
533
|
+
* staging correctness, so a rename OUT of a foreign project is not silently
|
|
534
|
+
* lost just because its destination happens to land under `ownProject`.
|
|
535
|
+
*
|
|
536
|
+
* Returns `null`, not `[]`, on any git failure (repo missing, `rev-parse` or
|
|
537
|
+
* `status` non-zero, or a timeout): folding "cannot enumerate" into the same
|
|
538
|
+
* empty array a truly clean repo returns would render the two identically,
|
|
539
|
+
* which is the silent failure this notice exists to catch (mirrors
|
|
540
|
+
* `gitDirtyFiles`'s own contract: "an empty return here just means 'cannot
|
|
541
|
+
* attribute', not 'clean'"). A clean repo returns `[]`.
|
|
542
|
+
*/
|
|
543
|
+
function listDirtyPaths(hypoDir) {
|
|
544
|
+
const prefixRes = spawnSync('git', ['-C', hypoDir, 'rev-parse', '--show-prefix'], {
|
|
545
|
+
encoding: 'utf-8',
|
|
546
|
+
timeout: FOREIGN_GIT_TIMEOUT_MS,
|
|
547
|
+
});
|
|
548
|
+
if (prefixRes.status !== 0) return null;
|
|
549
|
+
// trimEnd(), not trim(): the prefix is a real path segment, and a leading
|
|
550
|
+
// space or control char in a directory name is valid there. trim() would
|
|
551
|
+
// strip it off the front, so the stripped prefix no longer matches the
|
|
552
|
+
// (untouched) start of every path `git status` reports, and every path
|
|
553
|
+
// under that directory would wrongly read as "outside the vault" (the
|
|
554
|
+
// notice going silent for exactly the same reason a missing rename `from`
|
|
555
|
+
// does below). Only the trailing `\n` `--show-prefix` always appends needs
|
|
556
|
+
// stripping.
|
|
557
|
+
const prefix = (prefixRes.stdout || '').trimEnd();
|
|
558
|
+
|
|
559
|
+
const r = spawnSync('git', ['-C', hypoDir, 'status', '--porcelain', '-uall', '-z'], {
|
|
560
|
+
encoding: 'utf-8',
|
|
561
|
+
timeout: FOREIGN_GIT_TIMEOUT_MS,
|
|
562
|
+
});
|
|
563
|
+
if (r.status !== 0) return null;
|
|
564
|
+
const out = [];
|
|
565
|
+
const records = (r.stdout || '').split('\0');
|
|
566
|
+
const toVaultRelative = (f) => {
|
|
567
|
+
if (!f) return null;
|
|
568
|
+
if (!prefix) return f; // hypoDir IS the repo top level, nothing to strip
|
|
569
|
+
return f.startsWith(prefix) ? f.slice(prefix.length) : null; // outside the vault
|
|
570
|
+
};
|
|
571
|
+
for (let i = 0; i < records.length; i++) {
|
|
572
|
+
const rec = records[i];
|
|
573
|
+
if (!rec) continue;
|
|
574
|
+
const xy = rec.slice(0, 2);
|
|
575
|
+
const file = rec.slice(3); // destination path for a rename/copy
|
|
576
|
+
const isRenameOrCopy = xy[0] === 'R' || xy[1] === 'R' || xy[0] === 'C' || xy[1] === 'C';
|
|
577
|
+
// A rename/copy emits a paired `to\0from` record. Attribute BOTH: the
|
|
578
|
+
// origin project lost a file just as surely as the destination gained
|
|
579
|
+
// one, and dropping `from` (as this used to) silently loses that origin
|
|
580
|
+
// whenever it differs from the destination's project (a rename INTO
|
|
581
|
+
// ownProject from a foreign one would otherwise vanish entirely). One
|
|
582
|
+
// rename/copy is therefore counted as up to 2 dirty paths, inflating
|
|
583
|
+
// foreignCount/unattributedCount by one per cross-project rename; the
|
|
584
|
+
// name Set below still de-dupes, so the project NAME list does not grow.
|
|
585
|
+
let fromFile = null;
|
|
586
|
+
if (isRenameOrCopy) {
|
|
587
|
+
i++;
|
|
588
|
+
fromFile = records[i] || null;
|
|
589
|
+
}
|
|
590
|
+
const rel = toVaultRelative(file);
|
|
591
|
+
if (rel) out.push(rel);
|
|
592
|
+
const relFrom = toVaultRelative(fromFile);
|
|
593
|
+
if (relFrom) out.push(relFrom);
|
|
594
|
+
}
|
|
595
|
+
return out;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/** One-line notice covering two counts: uncommitted paths under a named
|
|
599
|
+
* project other than `ownProject` (`projects/<slug>/...`, or, when
|
|
600
|
+
* `ownProject` is null, no cwd-matched project this session, so ANY named
|
|
601
|
+
* project counts), and uncommitted paths this classifier cannot attribute to
|
|
602
|
+
* any project at all (everything else, root vault infra, `extensions/`,
|
|
603
|
+
* `_specs/`, ...). This is attribution, not narrowing, so the
|
|
604
|
+
* unattributed bucket is surfaced with a count rather than silently dropped
|
|
605
|
+
* just because it has no project name to show. '' only when enumeration
|
|
606
|
+
* succeeded and both counts are zero, so the quiet path stays quiet exactly
|
|
607
|
+
* there. A `null` from `listDirtyPaths` (enumeration failed) gets its own
|
|
608
|
+
* distinct line instead: the caller must not read "could not tell" as
|
|
609
|
+
* "nothing foreign".
|
|
610
|
+
*/
|
|
611
|
+
function foreignUncommittedNotice(hypoDir, ownProject) {
|
|
612
|
+
const dirty = listDirtyPaths(hypoDir);
|
|
613
|
+
if (dirty === null) {
|
|
614
|
+
return '[WIKI: 미커밋 변경의 귀속을 확인하지 못했습니다. git 상태를 근거로 작업 범위를 정하지 마십시오.]';
|
|
615
|
+
}
|
|
616
|
+
const foreignProjects = new Set();
|
|
617
|
+
let foreignCount = 0;
|
|
618
|
+
let unattributedCount = 0;
|
|
619
|
+
for (const f of dirty) {
|
|
620
|
+
const slug = projectOfPath(f);
|
|
621
|
+
if (slug === ownProject) continue;
|
|
622
|
+
if (slug) {
|
|
623
|
+
foreignProjects.add(slug);
|
|
624
|
+
foreignCount++;
|
|
625
|
+
} else {
|
|
626
|
+
unattributedCount++;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
if (foreignCount === 0 && unattributedCount === 0) return '';
|
|
630
|
+
const clauses = [];
|
|
631
|
+
if (foreignCount > 0) {
|
|
632
|
+
// The slug comes from a directory name in `git status` output, so it is
|
|
633
|
+
// untrusted text on its way into a prompt. sanitizeProjForPrompt is the same
|
|
634
|
+
// guard the hot-cache notices in this file already use; skipping it here would
|
|
635
|
+
// let a newline or a control char in a project directory name break the
|
|
636
|
+
// one-line notice apart and inject into the surrounding context. The name list
|
|
637
|
+
// is also capped, because an unbounded one grows the context by however many
|
|
638
|
+
// projects happen to be dirty.
|
|
639
|
+
const all = [...foreignProjects].sort();
|
|
640
|
+
const shown = all.slice(0, FOREIGN_NAME_CAP).map((s) => `projects/${sanitizeProjForPrompt(s)}`);
|
|
641
|
+
const names =
|
|
642
|
+
all.length > FOREIGN_NAME_CAP
|
|
643
|
+
? `${shown.join(', ')} 외 ${all.length - FOREIGN_NAME_CAP}개`
|
|
644
|
+
: shown.join(', ');
|
|
645
|
+
clauses.push(`현재 프로젝트 외 ${names} 변경 ${foreignCount}건`);
|
|
646
|
+
}
|
|
647
|
+
if (unattributedCount > 0) clauses.push(`귀속 불명 변경 ${unattributedCount}건`);
|
|
648
|
+
return `[WIKI: ${clauses.join(', ')}이 있습니다. 사용자 명시 지시 없이는 이 세션 작업으로 편입하지 마십시오.]`;
|
|
649
|
+
}
|
|
650
|
+
|
|
487
651
|
const GLOBAL_HOT = join(HYPO_DIR, 'hot.md');
|
|
488
652
|
const HOT_CHARS = 2000;
|
|
489
653
|
const STATE_CHARS = 2000;
|
|
@@ -613,6 +777,17 @@ process.stdin.on('end', () => {
|
|
|
613
777
|
const MARKER_FILE = sessionMarkerPath(sessionId);
|
|
614
778
|
const hit = findProjectFiles(cwd);
|
|
615
779
|
|
|
780
|
+
// ownProject = the cwd-matched project (or null on a MISS, per
|
|
781
|
+
// foreignUncommittedNotice's docstring). Late-appended into `notices` /
|
|
782
|
+
// `noticePrefix`, same pattern the MISS branch's suggestLine uses below —
|
|
783
|
+
// both need `hit` first, which isn't resolved until this line.
|
|
784
|
+
const foreignNotice = foreignUncommittedNotice(HYPO_DIR, hit ? hit.proj : null);
|
|
785
|
+
if (foreignNotice) {
|
|
786
|
+
notices.push(foreignNotice);
|
|
787
|
+
noticePrefix = notices.length ? `${notices.join('\n\n')}\n\n` : '';
|
|
788
|
+
process.stderr.write(`\n\x1b[33m${foreignNotice}\x1b[0m\n`);
|
|
789
|
+
}
|
|
790
|
+
|
|
616
791
|
// Observed-base snapshot for the write=proposal gate. Deliberately AFTER gitPull: the base must
|
|
617
792
|
// describe the tree this session actually starts from, remote merges
|
|
618
793
|
// included, or the first close would raise a proposal against content the
|
package/package.json
CHANGED
package/scripts/init.mjs
CHANGED
|
@@ -173,7 +173,11 @@ Init options:
|
|
|
173
173
|
--lint-strict Opt-in: also gate the wiki pre-commit hook on
|
|
174
174
|
\`lint --strict\` (promotes STRICT_PROMOTE_IDS warnings
|
|
175
175
|
to a blocking error), sequenced after the .hypoignore
|
|
176
|
-
guard.
|
|
176
|
+
guard. Exception: root hot.md/log.md's own
|
|
177
|
+
no-frontmatter warning stays a warn even under this
|
|
178
|
+
flag, so a legacy vault predating the frontmatter
|
|
179
|
+
convention still commits. Off by default; re-run init
|
|
180
|
+
to add or drop it.
|
|
177
181
|
--dry-run Show what would be done without making changes
|
|
178
182
|
--version Print the installed package version and exit
|
|
179
183
|
--help, -h Show this help message
|