umtri-mcp 1.3.3 → 1.4.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/README.md CHANGED
@@ -56,26 +56,34 @@ appear and then error, re-check the token and its scope — `read` tokens cannot
56
56
  **Read** — `get_graph`, `get_bug`, `get_impact`, `list_projects`, `list_bugs`, `list_seasons`,
57
57
  `list_events`
58
58
 
59
- **Write** — `create_project`, `create_node`, `update_node`, `delete_node`, `create_edge`,
60
- `delete_edge`, `create_api`, `update_api`, `delete_api`, `create_bug`, `update_bug`, `delete_bug`,
61
- `create_season` (acts only after the user confirms)
59
+ **Write** — `create_project`, `create_node`, `update_node`, `delete_node` (a subtree only with
60
+ `cascade: true`), `create_edge`, `delete_edge`, `create_api`, `update_api`, `delete_api`,
61
+ `create_bug`, `update_bug`, `delete_bug`, `create_season` (acts only after the user confirms)
62
62
 
63
63
  **Plan loop** — `commit_plan`, `record_commit`
64
64
 
65
+ **Wiki** — `list_wiki`, `get_wiki`, `list_wiki_revisions`, `write_wiki`, `delete_wiki`. The ground's
66
+ encyclopedia: pages form a table of contents (`parent`, `position`) read from an overview down,
67
+ each has a kind, and every write comes back with warnings when a page strays from the skeleton.
68
+
69
+ **Feedback** — `send_feedback` files a suggestion or defect about Umtri itself.
70
+
65
71
  `get_graph` returns a slice, not a dump: scope by subtree (`rootId`), by layer (`maxType`), by role,
66
72
  or by season, and control description weight separately. A large tree stays cheap to read.
67
73
 
68
74
  ## Resources
69
75
 
70
- Seven read-only documents the agent can pull for domain rules — the plant vocabulary
76
+ Ten read-only documents the agent can pull for domain rules — the plant vocabulary
71
77
  (trunk/limb/twig/leaf/vein), what counts as a node and what does not, how seasons work, how plan
72
- nodes are meant to be realized, and how a ground behaves while transplanting.
78
+ nodes are meant to be realized, how a ground behaves while transplanting, keeping the tree in step
79
+ with commits, and how to write and organize the wiki.
73
80
 
74
81
  ```
75
- umtri://rules/vocabulary umtri://rules/transplant
76
- umtri://rules/vocabulary-detailed umtri://rules/plan
77
- umtri://rules/seasons umtri://about/vision
78
- umtri://rules/system-structure
82
+ umtri://rules/vocabulary umtri://rules/plan
83
+ umtri://rules/vocabulary-detailed umtri://rules/commit-sync
84
+ umtri://rules/system-structure umtri://rules/wiki
85
+ umtri://rules/seasons umtri://templates/wiki
86
+ umtri://rules/transplant umtri://about/vision
79
87
  ```
80
88
 
81
89
  ## Links
package/core/policy.js CHANGED
@@ -232,52 +232,9 @@ export function reparentMetadataHint(patch) {
232
232
  }
233
233
 
234
234
  // ── wiki 본문 ────────────────────────────────────────────────────────
235
- // 위키를 로그로 쓰는 경향이 실제로 관찰됐다 — 작업을 끝낸 에이전트가 페이지를 열어
236
- // "이번에 뭘 했는지"를 덧붙이고, 몇 번 반복되면 페이지가 세션 요약 더미가 된다.
237
- // 규칙 문서(umtri://rules/wiki)에 적어뒀지만 리소스는 pull-only라 안 읽으면 그만이라,
238
- // 쓰는 순간에 신호를 준다.
239
- //
240
- // **거부하지 않고 경고만 한다.** 판정이 휴리스틱이고, 날짜가 정당하게 필요한 문서도
241
- // 있기 때문이다(마이그레이션 위험 고지 등). 오탐이 잦으면 경고 자체가 무시되므로
242
- // 정밀도가 높은 신호만 고른다 — 셋 다 산문형 레퍼런스에는 거의 나오지 않는 형태다.
243
- // 날짜가 제목 **앞머리**에 올 때만 잡는다. "## 2026-08-27 — 인증 분리"는 일지 항목이지만
244
- // "## 확정된 결정 (2026-07-28)"은 결정에 날짜를 단 것이라 정당하다. 괄호 안 날짜까지
245
- // 잡으면 오탐이 늘고, 오탐이 늘면 경고를 아무도 안 본다.
246
- const WIKI_DATE_HEADING_RE = /^#{1,6}\s*[([]?\s*20\d{2}\s*[-./년]\s*\d{1,2}\s*[-./월]\s*\d{1,2}/m;
247
- const WIKI_CHANGELOG_HEADING_RE = /^#{1,6}\s*(?:.*(?:change\s*(?:log|history)|revision history|release notes|변경\s*(?:이력|내역)|작업\s*이력|업데이트\s*내역|진행\s*(?:표시|상황)).*)$/im;
248
- const WIKI_CHECKBOX_RE = /^\s*[-*+]\s*\[[ xX]\]/gm;
249
-
250
- export function wikiLogShapeCheck(body) {
251
- if (!body || typeof body !== 'string') return [];
252
- const out = [];
253
-
254
- if (WIKI_DATE_HEADING_RE.test(body)) {
255
- out.push({
256
- rule: 'wiki-dated-entry',
257
- severity: 'warn',
258
- message: 'This body has a date heading. A wiki page is a description of how something works now, not a dated log — the page\'s own past is kept in its revisions (list_wiki_revisions) and who-changed-what in list_events. Fold the fact into the prose and drop the date, unless the date is itself a live hazard a reader must know.',
259
- });
260
- }
261
-
262
- if (WIKI_CHANGELOG_HEADING_RE.test(body)) {
263
- out.push({
264
- rule: 'wiki-changelog-section',
265
- severity: 'warn',
266
- message: 'This body has a change-history / progress section. Revisions already record what the page said before, so that section duplicates them and goes stale. Delete it and let the prose describe the current state.',
267
- });
268
- }
269
-
270
- const boxes = body.match(WIKI_CHECKBOX_RE);
271
- if (boxes && boxes.length >= 3) {
272
- out.push({
273
- rule: 'wiki-progress-checklist',
274
- severity: 'info',
275
- message: `This body has ${boxes.length} checklist items. Outstanding work belongs in bugs (create_bug) or plan nodes, where it can be tracked and closed — a checklist in a wiki page is read by nobody and rots. Keep the page to what is true now.`,
276
- });
277
- }
278
-
279
- return out;
280
- }
235
+ // 위키 판정(로그 형태 + 백과사전 뼈대)은 서버 server/data/wikiLint.js에 있고 쓰기 응답의
236
+ // warnings[]로 온다. 여기 두면 REST로 쓴 문서는 판정을 안 받고, stdio 사용자는 패키지를
237
+ // 새로 받을 때까지 옛 판정에 묶인다.
281
238
 
282
239
  // ── 도구 호출 마크업 유출 ────────────────────────────────────────────
283
240
  // 인자가 필드로 쪼개지지 못하고 첫 필드에 통째로 들어오는 사고가 실제로 있었다
@@ -55,8 +55,9 @@ remove it.
55
55
 
56
56
  All normal guards return: past seasons reject new/moved nodes, grown nodes lock
57
57
  their shape and timeline, `delete_node` is soft-only and refuses nodes with
58
- active children. Treat the tree as settled — small, observable changes only
59
- (see umtri://about/vision).
58
+ active children unless you pass `cascade: true` (only when the human asked to
59
+ remove the whole branch — soft-deleted nodes cannot be restored). Treat the
60
+ tree as settled — small, observable changes only (see umtri://about/vision).
60
61
 
61
62
  ### What stays editable on a grown node
62
63
 
@@ -0,0 +1,80 @@
1
+ # Wiki Templates
2
+
3
+ A ground's wiki is read front to back: an overview at the top, chapters under
4
+ it, entries under those. These are recommended tables of contents by project
5
+ type. Pick the one closest to the project, adapt it, and grow into it.
6
+
7
+ **Use a template as a map, not a stub generator.** Create a page when you have
8
+ something true to write in it — an empty page reads the same as a page nobody
9
+ has written yet, and a table of contents full of them hides the ones that
10
+ matter. The overview and chapter pages come first; entries arrive as knowledge
11
+ does.
12
+
13
+ How to read the tables:
14
+
15
+ - Indentation is the tree (`parent`), order is `position`.
16
+ - Titles are plain nouns in the reader's language. Each entry shows an English
17
+ and a Korean title; use the one that matches the people reading the wiki.
18
+ - `kind` decides the skeleton (see `umtri://rules/wiki`): `overview` needs only
19
+ a lead, `concept` / `component` need `## Spec`, `rule` needs `## Rules`,
20
+ `glossary` is a term table.
21
+ - Skip what the project does not have. Add what it has that is not listed.
22
+
23
+ ## Web service
24
+
25
+ A server-backed product used in the browser: frontend, API server, database,
26
+ deployed somewhere.
27
+
28
+ | Page | 제목 | kind | What it holds |
29
+ | --- | --- | --- | --- |
30
+ | Overview | 개요 | overview | What the product is, who uses it, the stack in one table, how to read this wiki |
31
+ |   Concepts | 개념 | overview | The domain model a newcomer must learn before the code makes sense |
32
+ |     *one page per core concept* | | concept | The entities and rules of the domain (e.g. order, account, workspace) |
33
+ |   Architecture | 구성 | overview | How the system is put together; one sentence per part below |
34
+ |     Server | 서버 | component | Request pipeline, routing, error shape, where business logic lives |
35
+ |       Authentication | 인증 | component | Sessions/tokens, who can call what |
36
+ |       *one page per server domain* | | component | e.g. billing, notifications, search |
37
+ |     Database | 데이터베이스 | component | Tables, ownership, migrations, invariants the schema enforces |
38
+ |     Frontend | 프론트엔드 | component | Routing, state, design system, i18n |
39
+ |       *one page per major screen or flow* | | component | |
40
+ |     Integrations | 외부 연동 | component | Payment, mail, third-party APIs — contracts and failure modes |
41
+ |   Operations | 운영 | overview | How the product is shipped and kept running |
42
+ |     Deployment | 배포 | rule | Environments, order of steps, what must never be skipped |
43
+ |     *conventions* | | rule | e.g. commit sync, code review, copy and i18n, secrets |
44
+ |   Glossary | 용어 | glossary | Product vocabulary and names that mean something specific here |
45
+
46
+ ## App service
47
+
48
+ A mobile or desktop app, usually with a backend it talks to.
49
+
50
+ | Page | 제목 | kind | What it holds |
51
+ | --- | --- | --- | --- |
52
+ | Overview | 개요 | overview | What the app does, platforms, the stack, how to read this wiki |
53
+ |   Concepts | 개념 | overview | The domain model |
54
+ |     *one page per core concept* | | concept | |
55
+ |   Architecture | 구성 | overview | App, backend and the boundary between them |
56
+ |     App | 앱 | component | Navigation, state, offline behaviour, platform differences |
57
+ |       *one page per major screen or flow* | | component | |
58
+ |     Backend | 백엔드 | component | API the app calls, auth, sync |
59
+ |     Device features | 기기 기능 | component | Push, permissions, camera, storage — what the OS allows and when |
60
+ |   Operations | 운영 | overview | |
61
+ |     Release | 릴리스 | rule | Build, signing, store review, versioning, staged rollout |
62
+ |     *conventions* | | rule | |
63
+ |   Glossary | 용어 | glossary | |
64
+
65
+ ## Library
66
+
67
+ A package other code depends on: SDK, framework, CLI, shared module.
68
+
69
+ | Page | 제목 | kind | What it holds |
70
+ | --- | --- | --- | --- |
71
+ | Overview | 개요 | overview | What it solves, who depends on it, supported runtimes |
72
+ |   Concepts | 개념 | overview | The mental model users must hold |
73
+ |     *one page per core concept* | | concept | |
74
+ |   Public API | 공개 API | overview | The surface other code relies on |
75
+ |     *one page per module or entry point* | | component | Behaviour, guarantees, errors |
76
+ |   Internals | 내부 구조 | component | How it works inside; what may change without notice |
77
+ |   Operations | 운영 | overview | |
78
+ |     Releases | 릴리스 | rule | Versioning, compatibility promises, deprecation, publishing |
79
+ |     *conventions* | | rule | |
80
+ |   Glossary | 용어 | glossary | |
@@ -1,22 +1,127 @@
1
1
  # Writing a Ground's Wiki
2
2
 
3
- The tree holds structure. The wiki holds what the tree cannot: the conventions a
4
- project settled on, the decisions it made and why, the vocabulary it uses, what
5
- an investigation concluded.
3
+ The tree holds structure. The wiki is **the project's encyclopedia**: it holds
4
+ what the tree cannot — how each part works, the rules the project keeps, the
5
+ concepts it is built on, the words it uses.
6
6
 
7
- Two things to remember. The first is the shape of a page:
7
+ Four things to remember.
8
8
 
9
- > **A page is named after a thing, and its revisions are that thing's life.**
10
- > Revision 1 is the plan. Later revisions are what it became. The newest
11
- > revision is what is true now.
9
+ > **The wiki is read front to back.** It is a tree — an overview at the top,
10
+ > chapters under it, entries under those. Someone who reads it in order should
11
+ > come out understanding the project.
12
12
 
13
- The second is what a page *is*:
13
+ > **One page is one entry.** It is named after a thing, and its body is that
14
+ > thing's current spec and rules.
14
15
 
15
- > **A page is a description, not a log.**
16
- > It tells a reader how something works right now. It does not narrate what
17
- > happened to it.
16
+ > **Background goes in footnotes.** How it came to be, what was tried before,
17
+ > side notes — `[^name]`, not body text.
18
+
19
+ > **Revisions are the page's life.** Revision 1 is the plan, later revisions
20
+ > are what it became, the newest is what is true now. The body never narrates
21
+ > that history.
22
+
23
+ ## The table of contents
24
+
25
+ Every page has a place in the tree: a `parent` page and a `position` among its
26
+ siblings. The app draws the table of contents from it, numbers the entries, and
27
+ links each page to the previous and next one in reading order.
28
+ `list_wiki` with `order="tree"` returns the same order.
18
29
 
19
- Everything below follows from those two.
30
+ - **One overview at the top.** It says what the project is, who it is for, the
31
+ stack at a glance, and how the wiki is laid out. Only `overview` pages stand
32
+ at the top level; any other page without a parent is flagged.
33
+ - **Chapters under it** — usually Concepts, Architecture, Operations, Glossary.
34
+ A chapter page is an `overview` too: a lead and one sentence per page under it.
35
+ - **Entries under the chapters**, nested where one thing is part of another
36
+ (Authentication under Server, Write policy under MCP).
37
+ - **Order teaches.** Put what a newcomer needs first before what depends on it:
38
+ the core concept before its details, the server before its domains.
39
+
40
+ Starting a wiki, or reorganizing one with no clear shape: read
41
+ `umtri://templates/wiki` and pick the table of contents for the project type.
42
+ Grow into it — create a page when there is something true to put in it, not as
43
+ an empty placeholder.
44
+
45
+ ## Titles
46
+
47
+ A title is **a plain noun for the thing, in the reader's language**: `조직`,
48
+ `배포`, `영향 분석` for a Korean-speaking team; `Organizations`, `Deployment`,
49
+ `Impact analysis` for an English one. Product vocabulary and proper names stay
50
+ as they are (`MCP`, `Season`).
51
+
52
+ The table of contents is made of titles; it only reads as a structure when each
53
+ line is a name. So no description after a dash or colon, no parenthetical, no
54
+ sentence. "Organizations — tenancy, teams and who pays" belongs in the lead,
55
+ not the title. The write warns about titles that look like that.
56
+
57
+ ## Kinds
58
+
59
+ Every page has a `kind`. It decides which sections the page needs.
60
+
61
+ | kind | For | Needs |
62
+ | --- | --- | --- |
63
+ | `overview` | The top page and chapter pages — what is under them and why in that order | a lead only |
64
+ | `concept` | A product or domain concept, a model (node roles, the plan loop) | `## Spec` |
65
+ | `component` | How one part of the system works (MCP, organizations, billing) | `## Spec` |
66
+ | `rule` | A convention that must be kept (commit sync, deploy order) | `## Rules` |
67
+ | `glossary` | The project's vocabulary | nothing — a table or list |
68
+
69
+ Pick by what a reader arrives asking. "What is X?" is a concept. "How does X
70
+ work / how do I change it?" is a component. "What must I always / never do?" is
71
+ a rule. If a page answers two of these, it is probably two pages — or one
72
+ component page with a strong Rules section.
73
+
74
+ ## The skeleton
75
+
76
+ ```
77
+ Lead paragraph — what this is, in 1–3 sentences.
78
+
79
+ ## 스펙 (Spec)
80
+ How it works now. Break it down with ### headings.
81
+
82
+ ## 규칙 (Rules)
83
+ What you get wrong if you don't know it: invariants, prohibitions, required order.
84
+
85
+ [^name]: Background, history, rejected alternatives, side notes.
86
+ ```
87
+
88
+ - **The lead comes first**, as a paragraph. Not a heading, not a `> Status:`
89
+ banner, not a list. A reader who stops after it should know what the thing is.
90
+ - **Only two top-level sections**: `## 스펙` / `## Spec` and `## 규칙` /
91
+ `## Rules`. Everything else is a `###` under one of them. (`overview` and
92
+ `glossary` pages are free-form after the lead.)
93
+ - **Spec** is descriptive: how it works. **Rules** is normative: what must hold.
94
+ "Tokens are hashed with SHA-256" is spec. "Never log a raw token" is a rule.
95
+ - Write the body in the language the project uses; the section names are
96
+ recognised in Korean and English.
97
+
98
+ ## Footnotes: where background lives
99
+
100
+ An encyclopedia entry is not stripped of reasons — but it is careful about
101
+ *which* reasons sit in the text.
102
+
103
+ **The test: would someone about to change this code get it wrong without
104
+ knowing this?**
105
+
106
+ - **Yes** → it is part of the spec or a rule. Keep it in the body. "The webhook
107
+ handler retries three times, because Toss redelivers on any non-2xx" — the
108
+ "because" stops someone from removing the retry.
109
+ - **No** → it is background. Put it in a footnote. How the decision was
110
+ reached, the approach that came before, who asked for it, a caveat worth
111
+ knowing but not acting on.
112
+
113
+ ```
114
+ Team orgs keep `plan='free'`.[^plan]
115
+
116
+ [^plan]: `plan` describes the org's own subscription. An earlier draft billed
117
+ teams through the owner's personal plan; that was dropped when billing moved to
118
+ the team itself.
119
+ ```
120
+
121
+ Footnote syntax is `[^name]` in the text and `[^name]: …` on its own line,
122
+ usually at the end. The app renders references as superscripts and collects the
123
+ definitions into a notes list at the bottom. Every reference needs a
124
+ definition, and every definition needs a reference.
20
125
 
21
126
  ## Write reference, not a diary
22
127
 
@@ -25,26 +130,19 @@ the page, and appends a paragraph about what it just did. Do that a few times
25
130
  and the page is a pile of session summaries that nobody can read as an
26
131
  explanation of anything.
27
132
 
28
- **The page should read like an encyclopedia entry for the thing, in the present
29
- tense.** Someone who arrives knowing nothing should finish it understanding how
30
- the thing works. They should not have to reconstruct the current state by
31
- replaying a timeline.
32
-
33
- | Log — do not write this | Description — write this |
133
+ | Log — do not write this | Entry — write this |
34
134
  | --- | --- |
35
- | "Added retry logic to the webhook handler today." | "The webhook handler retries three times with backoff. Toss redelivers on any non-2xx, so a slow database would otherwise double-charge." |
36
- | "## 2026-08-27 — split the auth middleware" | "Auth runs in two stages: session or bearer resolution, then scope check. They are separate because MCP loopback needs the second without the first." |
37
- | "Phase 1 done, Phase 2 in progress" | (belongs in bugs or plan nodes, not here) |
135
+ | "Added retry logic to the webhook handler today." | "The webhook handler retries three times with backoff." (+ the reason, in the body or a footnote) |
136
+ | "## 2026-08-27 — split the auth middleware" | "Auth runs in two stages: session or bearer resolution, then scope check." |
137
+ | "> Status: Phase 1–3 implemented" | (nothing — progress lives in bugs and plan nodes) |
138
+ | "Decision ⑦: tokens capped at 3" | A rule: "A user holds at most 3 active write tokens." |
38
139
  | "## Change history" / "## 변경 이력" | (delete it — that is what revisions are for) |
39
140
 
40
- Concretely, a page should not contain:
41
-
42
- - **Date headings** or dated entries of any kind
43
- - **Progress checklists** — "Phase 2 완료", "- [x] W3", "TODO: …"
44
- - **Sentences about the act of working** — "we added", "이번에 …했다", "next I will"
45
- - **A changelog section**
46
-
47
- ### Why this is not a loss
141
+ A page does not contain dated headings, status or source banners, progress
142
+ checklists, sentences about the act of working ("we added", "이번에 …했다"),
143
+ or a changelog section. A numbered decision log is the same mistake in a
144
+ different shape: turn each live decision into a spec sentence or a rule, and
145
+ move how it was decided into a footnote.
48
146
 
49
147
  The temporal record already exists, in better places:
50
148
 
@@ -53,158 +151,103 @@ The temporal record already exists, in better places:
53
151
  | What this page said before | revisions (`list_wiki_revisions`) |
54
152
  | Who changed what, when | the ground's history (`list_events`) |
55
153
  | Which commit touched which code | `record_commit` → the node's `metadata.commits` |
56
- | What still needs doing | bugs (`create_bug`) or plan nodes |
154
+ | What still needs doing, what is undecided | bugs (`create_bug`) or plan nodes |
57
155
  | What you plan to do | revision 1 of the page, written *before* you build |
58
156
 
59
- **Because the history lives in the revisions, the body is free to be purely
60
- present-tense.** That is the whole point of versioning the page: you never have
61
- to keep an old paragraph around just to preserve the record of it. Rewrite the
62
- sentence to say what is true now, and the previous wording stays readable in the
63
- revision underneath.
157
+ Past events belong in the body only when the event is *itself* a live
158
+ constraint: "the `apex` type was renamed to `leaf`; old exports may still carry
159
+ it." That is not a diary entry, it is a hazard.
64
160
 
65
- A page that logs is doing, badly, a job four other things already do well — and
66
- failing at the one job only it can do.
161
+ ## The write tells you what is off
67
162
 
68
- ### The exception: what "why" survives
69
-
70
- Present tense does not mean stripping the reasoning. "The webhook handler
71
- retries because Toss redelivers on any non-2xx" is present tense and is exactly
72
- the sentence worth keeping. The test is not *is this about the past* but **would
73
- someone changing this code get it wrong without knowing it** — see "What stays
74
- on the page" below.
75
-
76
- Past events belong in a page only when the event is *itself* a live constraint:
77
- "the `apex` type was renamed to `leaf` in 2026-05; old exports may still carry
78
- it." That is not a diary entry, it is a hazard a reader needs.
163
+ `write_wiki` answers with `warnings[]` when the saved page strays from the
164
+ skeleton: no kind, no parent (outside the table of contents), a title that is
165
+ not a plain noun, no lead, a status banner, a dated heading, a checklist, a
166
+ change-history section, a top-level section other than Spec/Rules, a missing
167
+ required section, or a footnote without its pair. The write is saved either
168
+ way. Read the warnings, fix the page, write again.
79
169
 
80
170
  ## Name the thing, not the document
81
171
 
82
- A page is called `mcp`, `billing`, `impact-analysis` — the subject it is about.
172
+ The page slug (its URL name) is called `mcp`, `billing`, `impact-analysis` —
173
+ the subject it is about, in lowercase English even when the title is Korean.
83
174
 
84
175
  Never name a page after what it currently *is*: `prd-mcp`, `mcp-spec`,
85
- `mcp-notes-v2`, `mcp-final`. Those names are accurate for a week. The page that
86
- begins life as a plan for MCP and ends as the description of the MCP you built
87
- is the same page the whole time, and `prd-mcp` becomes a lie the moment the
88
- first line of it ships.
176
+ `mcp-notes-v2`, `mcp-final`. The page that begins as a plan for MCP and ends as
177
+ the description of the MCP you built is the same page the whole time.
89
178
 
90
179
  Use lowercase letters, digits and hyphens. Say what a maintainer would say out
91
180
  loud: `deploy`, not `deployment-process-documentation`.
92
181
 
93
182
  If a name turns out wrong, use `write_wiki`'s **`rename`**. Do not create a new
94
- page and delete the old one — that throws away every revision, which is the part
95
- of the page that was worth keeping. A rename on its own adds no revision; a
96
- revision records content, and the name is not content.
183
+ page and delete the old one — that throws away every revision.
97
184
 
98
185
  ## Revision 1 is the plan
99
186
 
100
- Before building something, write the page. What you are about to do, why this
101
- way, what you decided against. That is revision 1, and it is a real deliverable:
102
- another agent picking up the work reads it instead of guessing.
187
+ Before building something, write the page: what you are about to do, why this
188
+ way, what you decided against. That is revision 1, and another agent picking up
189
+ the work reads it instead of guessing. A plan is the one revision allowed to be
190
+ in the future tense.
103
191
 
104
192
  This replaces keeping design docs in the repo. A plan in a repo file goes stale
105
- silently — nothing makes anyone revisit it, and six months later no one can tell
106
- whether it describes the code or someone's old intention. A plan that is
107
- revision 1 of a living page cannot rot the same way, because the page keeps
108
- being updated and the plan stays readable underneath as history.
193
+ silently; a plan that is revision 1 of a living page stays readable underneath
194
+ as history.
109
195
 
110
- Plan nodes and this page do different jobs and work together: the plan nodes say
111
- *where* in the structure something goes, the page says *what and why*. See
196
+ Plan nodes and the page work together: the plan nodes say *where* in the
197
+ structure something goes, the page says *what and why*. See
112
198
  `umtri://rules/plan`.
113
199
 
114
- ## Write back what you actually built — with `newRevision: true`
115
-
116
- When the thing exists, come back and make the page describe it. Not "the plan
117
- plus a note that it's done" — the page should now read as a description of what
118
- is there.
119
-
120
- **Pass `newRevision: true` on that write.** This matters more than it looks.
121
- Writes fold together when they come from the same author within about half an
122
- hour, so that eight passes of polishing leave one revision instead of eight.
123
- That folding cannot tell "still drafting the plan" from "the plan has become the
124
- thing" — clock and author look identical in both cases. Without the flag, an
125
- agent that plans and then builds in one sitting overwrites its own plan, and
126
- revision 1 is gone.
200
+ ## Write back what you built — with `newRevision: true`
127
201
 
128
- Use it whenever the page crosses a boundary: plan → built, v1 → v2, one approach
129
- abandoned for another. Skip it while you are still working on the same stage.
202
+ When the thing exists, rewrite the page so it describes it. Not "the plan plus
203
+ a note that it's done" — an entry for what is there.
130
204
 
131
- ## What stays on the page, what becomes history
205
+ **Pass `newRevision: true` on that write.** Writes by the same author within
206
+ about half an hour fold into one revision. That folding cannot tell "still
207
+ drafting the plan" from "the plan has become the thing"; without the flag, an
208
+ agent that plans and builds in one sitting overwrites its own plan.
132
209
 
133
- The page is the current state. But "current state" is not just the mechanics —
134
- **a live reason is part of the current spec.**
135
-
136
- | Stays on the page | Becomes history |
137
- | --- | --- |
138
- | How it works now | How it used to work |
139
- | The constraint that makes it work this way | A constraint that no longer applies |
140
- | A decision you would repeat, and why | The full argument that led to a reversed decision |
141
- | The conclusion of a reversal: "we tried A, moved to B, because —" | The prose written while A was still true |
142
-
143
- The test: **would someone about to change this code get it wrong without
144
- knowing this?** If yes, it belongs on the page, however old it is.
145
-
146
- A rule like "team org keeps `plan='free'` because plan describes that org's own
147
- subscription and a team org has none" is not history. It is a live trap, and a
148
- page that files it away as an old decision will get rewritten wrong.
149
-
150
- A page that strips out every reason becomes a manual, and a manual is exactly
151
- the thing the tree could have held. Do not optimize the wiki into uselessness.
210
+ Use it whenever the page crosses a boundary: plan → built, v1 → v2, one
211
+ approach abandoned for another.
152
212
 
153
213
  ## One page per thing
154
214
 
155
215
  Split when a page starts answering two questions a reader would arrive with
156
- separately. Merge — or never split — when the answer to one requires the other.
157
-
158
- Do not split by document type. `mcp` and `mcp-architecture` are not two pages;
159
- they are one page at two points in its life. Do not create a page per file, per
160
- sprint, or per meeting. A ground with forty one-paragraph pages is harder to
161
- read than the code.
216
+ separately. Do not split by document type — `mcp` and `mcp-architecture` are
217
+ one page at two points in its life. Do not create a page per file, per sprint,
218
+ or per meeting.
162
219
 
163
220
  ## Choosing how to write
164
221
 
165
222
  | | |
166
223
  | --- | --- |
167
224
  | The thing changed | `mode="replace"` — **read the page first** (`get_wiki`) |
168
- | The page is missing a whole topic you can add as its own section | `mode="append"` |
225
+ | The page lacks a whole topic you can add as its own `###` | `mode="replace"` with it folded in; `append` only if it truly stands alone at the end |
169
226
  | Crossing into a new stage | add `newRevision: true` |
170
227
  | A write went wrong | `list_wiki_revisions`, then `mode="restore"` with the rev |
171
- | You read, thought, then wrote, and someone else may have written meanwhile | add `baseRev` — the write is refused instead of clobbering |
172
-
173
- **`append` is the narrow case, not the default.** It is for adding a section
174
- that was genuinely absent — a new subsystem the page never covered. It is not
175
- for tacking on what you just did. If your addition would only make sense to
176
- someone who knows what happened this week, you want `replace`: fold the new fact
177
- into the sentence that was already there.
228
+ | Someone else may have written meanwhile | add `baseRev` — the write is refused instead of clobbering |
178
229
 
179
- After any write, look at what came back. `previousBodyChars` is how long the
180
- body was before you wrote. If it is much larger than what you just wrote, you
181
- replaced someone's work — check `list_wiki_revisions` before moving on. Nothing
182
- is lost, but nobody will tell you later.
230
+ After any write, check `previousBodyChars`. If it is much larger than what you
231
+ just wrote, you replaced someone's work — look at `list_wiki_revisions`.
183
232
 
184
233
  ## Attaching to nodes
185
234
 
186
- A page can hang off a node or float free. Floating is normal and expected early:
187
- knowledge arrives before structure, and a page that has to wait for a node is a
188
- page that never gets written.
189
-
190
- Once the tree has grown, list the unattached pages (`list_wiki` with
191
- `node="none"`) and attach the ones that now have a home. Deleting a node
192
- detaches its pages rather than deleting them.
235
+ A page can hang off a node or float free. Floating is normal early: knowledge
236
+ arrives before structure. Once the tree has grown, list the unattached pages
237
+ (`list_wiki` with `node="none"`) and attach the ones that now have a home.
238
+ `list_wiki` with `kind="none"` lists the pages not yet classified.
193
239
 
194
240
  ## Deleting
195
241
 
196
- Almost never. Deleting removes the page **and its entire revision history** —
197
- unlike an overwrite, it cannot be undone.
198
-
199
- A stale page is not a reason to delete. Rewrite it, and let the old state live
200
- in its revisions. Delete only a page that should never have existed: a
201
- duplicate, a test, something filed under the wrong ground.
242
+ Almost never. Deleting removes the page **and its entire revision history**.
243
+ A stale page is not a reason to delete — rewrite it. Delete only a page that
244
+ should never have existed: a duplicate, a test, something filed under the wrong
245
+ ground.
202
246
 
203
247
  ## What does not belong here
204
248
 
205
- - **Structure** — that is what nodes are for. Do not maintain a prose copy of
206
- the tree; it will drift within a week.
207
- - **Defects** — use bugs. A wiki page is not a to-do list.
208
- - **Requirements dumped in a ground's description** — that field is a one-line
209
- summary, not a spec. Move it here.
210
- - **Secrets** — tokens, keys, passwords. The wiki is prose, not a vault.
249
+ - **Structure** — that is what nodes are for. Do not keep a prose copy of the tree.
250
+ - **Defects and open questions** — use bugs.
251
+ - **Reports and archives** — an investigation's conclusion becomes spec or rules
252
+ on the page it is about; the report itself is not an entry.
253
+ - **Secrets** — tokens, keys, passwords.
package/core/resources.js CHANGED
@@ -55,7 +55,14 @@ const RESOURCES = [
55
55
  uri: 'umtri://rules/wiki',
56
56
  file: 'wiki.md',
57
57
  title: 'How to write a ground\'s wiki',
58
- description: 'A wiki page is named after a thing, and its revisions are that thing\'s life: rev 1 is the plan, later revisions are what it became, the newest is what is true now. Covers naming (the subject, never "prd-x"), writing the plan first, marking the plan→built boundary with newRevision, what stays on the page versus what becomes history (a live reason is current spec, not history), when to split pages, replace/append/restore/baseRev, attaching to nodes, and why deleting is almost never right. Read before writing or reorganizing wiki pages.',
58
+ description: 'The wiki is the project\'s encyclopedia, read front to back as a tree: overview → chapters → entries. Covers the tree (parent/position), noun titles, kinds and their skeleton (lead → Spec → Rules → footnotes), background in footnotes, why a page is a description and not a log, naming, revisions (plan first, newRevision at the plan→built boundary), and why deleting is almost never right. Read before writing or reorganizing wiki pages.',
59
+ },
60
+ {
61
+ name: 'wiki-templates',
62
+ uri: 'umtri://templates/wiki',
63
+ file: 'wiki-templates.md',
64
+ title: 'Wiki tables of contents by project type',
65
+ description: 'Recommended wiki trees for a web service, an app service and a library — page, Korean title, kind, and what each holds. Read when starting a ground\'s wiki or when its table of contents has no clear shape.',
59
66
  },
60
67
  {
61
68
  name: 'system-structure',
package/core/tools.js CHANGED
@@ -5,7 +5,7 @@
5
5
  // - api: createApiClient(...) 결과 — get/post/put/patch/delete
6
6
 
7
7
  import { z } from 'zod';
8
- import { validateNode, validateEdge, validateUpdate, validateBugUpdate, wikiLogShapeCheck, toolMarkupCheck } from './policy.js';
8
+ import { validateNode, validateEdge, validateUpdate, validateBugUpdate, toolMarkupCheck } from './policy.js';
9
9
 
10
10
  // LLM이 소비하는 결과 — 들여쓰기는 순수 토큰 낭비라 minify. (큰 그래프에서 ~19% 절감)
11
11
  function jsonResult(data) {
@@ -177,6 +177,12 @@ function compressProjects(projects, { descMode = 'excerpt' } = {}) {
177
177
  openBugs: p.openBugs,
178
178
  seasonCount: p.seasonCount,
179
179
  };
180
+ // 팀 ground면 소속과 내 역할을 싣는다 — viewer면 쓰기 도구가 403으로 막힌다는 걸 미리 알 수 있게.
181
+ // personal이면 생략(대부분의 ground가 personal이라 매번 실으면 잡음이다).
182
+ if (p.org && p.org.kind === 'team') {
183
+ out.org = p.org.slug;
184
+ out.role = p.role;
185
+ }
180
186
  // 문서가 있다는 사실만 흘린다. 0이면 생략 — 대부분의 ground가 0이라 매번 실으면 잡음이다.
181
187
  if (p.wikiCount) out.wikiCount = p.wikiCount;
182
188
  if (p.nowSeasonLabel) out.nowSeasonLabel = p.nowSeasonLabel;
@@ -329,7 +335,7 @@ export function registerTools(server, { api }) {
329
335
  'list_projects',
330
336
  {
331
337
  title: 'List grounds (projects)',
332
- description: 'Returns all grounds the authenticated user can access, with latest activity timestamp. Use this first to discover slugs for other tools. By default returns a summary view — each ground carries slug, name, isActive, transplanting, nodeCount, openBugs, seasonCount, nowSeasonLabel, latestActivityAt, plus a 200-char description excerpt. A ground with wiki pages also carries wikiCount — prose the tree cannot hold (conventions, decisions, glossary); read it with list_wiki. Pass view="full" for all fields (seedMeta — the ground\'s human-written intent: goal, endDate, budget, audience — plus typeCounts, raw metadata, timestamps); the icon base64 in metadata is always stripped (UI-only).',
338
+ description: 'Returns all grounds the authenticated user can access, with latest activity timestamp. Use this first to discover slugs for other tools. By default returns a summary view — each ground carries slug, name, isActive, transplanting, nodeCount, openBugs, seasonCount, nowSeasonLabel, latestActivityAt, plus a 200-char description excerpt. A ground that belongs to a team also carries org (the team slug) and role (owner/editor/viewer) — a viewer can read but every write tool returns 403 there. A ground with wiki pages also carries wikiCount — prose the tree cannot hold (conventions, decisions, glossary); read it with list_wiki. Pass view="full" for all fields (seedMeta — the ground\'s human-written intent: goal, endDate, budget, audience — plus typeCounts, raw metadata, timestamps); the icon base64 in metadata is always stripped (UI-only).',
333
339
  inputSchema: z.object({
334
340
  view: z.enum(['summary', 'full']).optional().describe('summary (default) for a compact list; full for every field.'),
335
341
  }),
@@ -590,18 +596,20 @@ export function registerTools(server, { api }) {
590
596
  'create_project',
591
597
  {
592
598
  title: 'Create a new ground (project)',
593
- description: 'Creates a new ground. Slug must match /^[a-z0-9][a-z0-9-]{0,49}$/ and be unique. The authenticated user owns it. Requires a write-scope token.',
599
+ description: 'Creates a new ground. Slug must match /^[a-z0-9][a-z0-9-]{0,49}$/ and be unique. By default it goes into the authenticated user\'s personal space; pass org (a team slug, as list_projects shows it) to create it in a team — only the team\'s owner can. Requires a write-scope token.',
594
600
  inputSchema: z.object({
595
601
  slug: z.string().regex(/^[a-z0-9][a-z0-9-]{0,49}$/).describe('URL slug. Lowercase letters, digits, hyphens; 1–50 chars.'),
596
602
  name: z.string().min(1).max(200).describe('Display name.'),
597
603
  description: z.string().optional(),
598
604
  visibility: z.enum(['private', 'unlisted', 'public']).optional().describe('Defaults to private.'),
605
+ org: z.string().min(1).optional().describe('Team slug to create the ground in. Omit for your personal space. You must own the team.'),
599
606
  }),
600
607
  },
601
- async ({ slug, name, description, visibility }) => {
608
+ async ({ slug, name, description, visibility, org }) => {
602
609
  try {
603
610
  const markupWarnings = toolMarkupCheck({ description });
604
611
  const body = { slug, name };
612
+ if (org !== undefined) body.org = org;
605
613
  if (description !== undefined) body.description = description;
606
614
  if (visibility !== undefined) body.visibility = visibility;
607
615
  const created = await api.post('/api/projects', body);
@@ -777,14 +785,15 @@ export function registerTools(server, { api }) {
777
785
  'delete_node',
778
786
  {
779
787
  title: 'Soft-delete a node',
780
- description: 'Sets removed_at on the node. By policy this tool rejects deletion if the node has any active descendant — delete children explicitly first to avoid accidental cascades. (The underlying REST API would cascade; the MCP layer guards against silent loss.) EXCEPTION — while the ground is transplanting (project.transplanting=true), the active-descendant guard is lifted (subtree cascade allowed) and you may pass hard=true to permanently remove import mistakes, including grown (past-season) nodes. Once the human roots the ground, normal guards return.',
788
+ description: 'Sets removed_at on the node. By default this tool rejects deletion if the node has any active descendant, so a subtree is never lost silently. To soft-delete the node together with all its active descendants, pass cascade=true — only when the human asked to remove the whole branch; soft-deleted nodes cannot be restored. The response lists the cascaded ids. EXCEPTION — while the ground is transplanting (project.transplanting=true), the active-descendant guard is lifted (subtree cascade allowed) and you may pass hard=true to permanently remove import mistakes, including grown (past-season) nodes. Once the human roots the ground, normal guards return.',
781
789
  inputSchema: z.object({
782
790
  slug: z.string().min(1).describe('Ground slug.'),
783
791
  id: z.string().min(1).describe('Node id.'),
792
+ cascade: z.boolean().optional().describe('Also soft-delete every active descendant. Without it, a node with active children is rejected.'),
784
793
  hard: z.boolean().optional().describe('Permanently delete (incl. descendants via FK cascade) instead of soft-delete. Only honored while the ground is transplanting; irreversible — use for import cleanup.'),
785
794
  }),
786
795
  },
787
- async ({ slug, id, hard }) => {
796
+ async ({ slug, id, cascade, hard }) => {
788
797
  try {
789
798
  const graph = await api.get(`/api/projects/${encodeURIComponent(slug)}/graph`);
790
799
  const target = (graph.nodes || []).find(n => n.id === id);
@@ -792,13 +801,15 @@ export function registerTools(server, { api }) {
792
801
 
793
802
  const transplanting = graph.project?.transplanting === true;
794
803
 
795
- // 옮겨심는 중이면 cascade 허용(가드 해제). 평상시엔 활성 자손 있으면 거부.
796
- if (!transplanting) {
804
+ // 평상시엔 활성 자손이 있으면 거부한다 — 삭제된 노드는 되살릴 경로가 없어서, 가지 하나를 지우려다
805
+ // 서브트리가 통째로 사라지는 걸 막는다. cascade=true로 의도를 밝히면 서버의 cascade를 그대로 탄다.
806
+ // 옮겨심는 중이면 가드 해제.
807
+ if (!transplanting && cascade !== true) {
797
808
  const children = (graph.nodes || []).filter(n => n.parent === id && !n.removedAt);
798
809
  if (children.length > 0) {
799
810
  const list = children.slice(0, 5).map(c => `${c.id}("${c.label}")`).join(', ');
800
811
  const more = children.length > 5 ? ` (+${children.length - 5} more)` : '';
801
- throw new Error(`Node has ${children.length} active child node(s). Delete them first. Children: ${list}${more}`);
812
+ throw new Error(`Node has ${children.length} active child node(s). Delete them first, or pass cascade=true to delete the whole subtree. Children: ${list}${more}`);
802
813
  }
803
814
  }
804
815
 
@@ -1156,19 +1167,21 @@ export function registerTools(server, { api }) {
1156
1167
  'list_wiki',
1157
1168
  {
1158
1169
  title: 'List wiki pages of a ground',
1159
- description: 'Lists the ground\'s wiki — the prose the tree cannot hold: conventions, decisions and why they were made, glossary, onboarding. Reference entries describing how things work, not a log of what happened: the change record is list_events, the page\'s own past is in its revisions. Not the structure itself (get_graph) and not defects (list_bugs). Bodies come back as 200-char excerpts by default so a scan stays cheap; pass body="full" only when you mean to read them, or body="none" for a pure index. A page may hang off a node (nodeId) or float free — pass node=<id> for one node\'s pages, or node="none" for the pages not yet attached to anything, which is the list worth reviewing when the tree has grown.',
1170
+ description: 'Lists the ground\'s wiki — the prose the tree cannot hold: conventions, decisions and why they were made, glossary, onboarding. Reference entries describing how things work, not a log of what happened: the change record is list_events, the page\'s own past is in its revisions. Not the structure itself (get_graph) and not defects (list_bugs). Bodies come back as 200-char excerpts by default so a scan stays cheap; pass body="full" only when you mean to read them, or body="none" for a pure index. A page may hang off a node (nodeId) or float free — pass node=<id> for one node\'s pages, or node="none" for the pages not yet attached to anything, which is the list worth reviewing when the tree has grown. The wiki is a tree: each page has a parent page and a position among its siblings, starting from an overview page. order="tree" returns the whole table of contents in reading order (depth-first, with depth) — use it to read the wiki front to back or to see where a new page belongs. Every page also has a kind (overview / concept / component / rule / glossary); kind="none" lists the pages not yet classified.',
1160
1171
  inputSchema: z.object({
1161
1172
  slug: z.string().min(1).describe('Ground slug.'),
1162
1173
  node: z.string().optional().describe('Node id to filter by, or "none" for pages attached to nothing yet. Omit for all.'),
1174
+ kind: z.enum(['overview', 'concept', 'component', 'rule', 'glossary', 'none']).optional().describe('Only pages of this kind, or "none" for unclassified pages. Omit for all.'),
1163
1175
  body: z.enum(['none', 'excerpt', 'full']).optional().describe('How much body text per page. Default "excerpt" (200 chars).'),
1164
1176
  limit: z.coerce.number().int().positive().optional(),
1165
- order: z.enum(['asc', 'desc']).optional().describe('By last update. Default "desc" (most recently touched first).'),
1177
+ order: z.enum(['asc', 'desc', 'tree']).optional().describe('"desc" (default) / "asc" by last update, or "tree" for the table of contents in reading order.'),
1166
1178
  }),
1167
1179
  },
1168
- async ({ slug, node, body, limit, order }) => {
1180
+ async ({ slug, node, kind, body, limit, order }) => {
1169
1181
  try {
1170
1182
  const qs = [];
1171
1183
  if (node) qs.push(`node=${encodeURIComponent(node)}`);
1184
+ if (kind) qs.push(`kind=${kind}`);
1172
1185
  if (body) qs.push(`body=${body}`);
1173
1186
  if (limit) qs.push(`limit=${limit}`);
1174
1187
  if (order) qs.push(`order=${order}`);
@@ -1230,6 +1243,7 @@ export function registerTools(server, { api }) {
1230
1243
  title: 'Create or update a wiki page',
1231
1244
  description: [
1232
1245
  'Writes one wiki page. Creates it if the page slug is new, updates it otherwise — you do not need to check first.',
1246
+ '**The wiki is the project\'s encyclopedia: one page is one entry, and its body is the current spec and rules of that thing.** It is a tree read front to back: an overview page at the top, chapter pages under it, entries under those — place every page with parent (a page slug or id) and position (its order among siblings), and check list_wiki order="tree" to see where it belongs. For a new wiki, follow the table of contents in umtri://templates/wiki. The title is a plain noun for the thing in the reader\'s language (조직, 배포, Billing) — no dash, colon, parenthetical or sentence; the description goes in the lead. Give every page a kind — overview (the top page and chapter pages that introduce what is under them), concept (a product/domain concept or model), component (how one part of the system works), rule (a convention that must be kept), glossary (the project\'s vocabulary). Write it to this skeleton: a lead paragraph first (1–3 sentences saying what the thing is — not a heading, quote or list), then "## 스펙" / "## Spec" (how it works now; break it down with ###), then "## 규칙" / "## Rules" (invariants, prohibitions, required order). concept and component need Spec; rule needs Rules; overview and glossary are free-form after the lead. No other top-level (##) sections. Background, history, rejected alternatives and side notes go into footnotes — [^name] in the text, [^name]: … at the end — not into the body. A reason stays in the body only if someone changing the code would get it wrong without it.',
1233
1247
  'Write what the tree cannot hold and what a maintainer would otherwise have to reconstruct: why a decision went the way it did, the vocabulary this project uses, what an investigation concluded. Do not restate the structure — that is what nodes are for.',
1234
1248
  '**A page is a description, not a log.** Write it in the present tense, as a reference entry for the thing: how it works now, and the reasons that still constrain it. Do not append what you just did, do not add dated entries or a change-history section, do not keep progress checklists here — those go to bugs, plan nodes, list_events, and record_commit, and the page\'s own past is already kept in its revisions. When something changes, rewrite the sentence that is now wrong rather than adding a sentence saying it changed.',
1235
1249
  'mode="replace" (default) overwrites the whole body — read the page first, or you will drop what was there; this is the normal way to update a page. mode="append" is the narrow case: adding a section the page genuinely lacked, never tacking on this session\'s summary. mode="restore" puts an earlier revision back: pass rev=<n> and nothing else, and the page returns to what it said then.',
@@ -1239,6 +1253,7 @@ export function registerTools(server, { api }) {
1239
1253
  'rename moves the page to a new slug, keeping its id and its whole revision history — do not create a new page and delete the old one, that throws the history away.',
1240
1254
  'The response tells you what the write did: rev is the revision it landed in, coalesced says whether it folded into your previous one, and previousBodyChars is how long the body was before you wrote. If previousBodyChars is much larger than what you just wrote, you replaced someone\'s work — check list_wiki_revisions before moving on.',
1241
1255
  'baseRev is optional and is how you avoid clobbering a concurrent edit: pass the rev you read, and the write is refused if the page moved on since. Use it when you read, think, then write.',
1256
+ 'The response carries warnings[] when the page strays from the skeleton or reads like a log (status banners, dated headings, checklists, change-history sections, unmatched footnotes). The write is saved either way — fix what they point at and write again.',
1242
1257
  'The change also lands in the ground\'s history (list_events, entityType="wiki"), where the body is recorded as a length delta rather than a copy of the prose — the prose itself lives in the revisions.',
1243
1258
  'Requires a write-scope token.',
1244
1259
  ].join(' '),
@@ -1248,6 +1263,9 @@ export function registerTools(server, { api }) {
1248
1263
  title: z.string().min(1).max(200).optional().describe('Display title. Required when creating a page.'),
1249
1264
  body: z.string().optional().describe('Markdown body. With mode="append" this is the text to add. Ignored by mode="restore".'),
1250
1265
  node: z.string().nullable().optional().describe('Node id to attach to. null detaches. Omit to leave the current attachment untouched.'),
1266
+ kind: z.enum(['overview', 'concept', 'component', 'rule', 'glossary']).nullable().optional().describe('Page kind — decides the required sections. Give it when creating. Omit to keep the current kind.'),
1267
+ parent: z.string().nullable().optional().describe('Parent page (slug or id) — where this page sits in the table of contents. null moves it to the top level (only overview pages belong there). Omit to keep it.'),
1268
+ position: z.coerce.number().int().nullable().optional().describe('Order among siblings under the same parent (ascending; pages without one come after, by title). Omit to keep it.'),
1251
1269
  mode: z.enum(['replace', 'append', 'restore']).optional().describe('"replace" (default) overwrites the body — the normal way to update a page; "append" adds a missing section to the end (not a session summary); "restore" brings back the revision named by rev.'),
1252
1270
  rev: z.coerce.number().int().positive().optional().describe('With mode="restore": the revision number to bring back (from list_wiki_revisions).'),
1253
1271
  baseRev: z.coerce.number().int().positive().optional().describe('The revision you based this write on. If the page has moved past it, the write is refused instead of overwriting.'),
@@ -1260,22 +1278,24 @@ export function registerTools(server, { api }) {
1260
1278
  rename: z.string().regex(/^[a-z0-9][a-z0-9-]{0,79}$/).optional().describe('New page slug. Keeps the id and the revision history; refused if that name is taken.'),
1261
1279
  }),
1262
1280
  },
1263
- async ({ slug, page, title, body, node, mode, rev, baseRev, newRevision, rename }) => {
1281
+ async ({ slug, page, title, body, node, kind, parent, position, mode, rev, baseRev, newRevision, rename }) => {
1264
1282
  try {
1265
1283
  const payload = { slug: page };
1266
1284
  if (title !== undefined) payload.title = title;
1267
1285
  if (body !== undefined) payload.body = body;
1268
1286
  if (node !== undefined) payload.node = node;
1287
+ if (kind !== undefined) payload.kind = kind;
1288
+ if (parent !== undefined) payload.parent = parent;
1289
+ if (position !== undefined) payload.position = position;
1269
1290
  if (mode !== undefined) payload.mode = mode;
1270
1291
  if (rev !== undefined) payload.rev = rev;
1271
1292
  if (baseRev !== undefined) payload.baseRev = baseRev;
1272
1293
  if (newRevision !== undefined) payload.newRevision = newRevision;
1273
1294
  if (rename !== undefined) payload.rename = rename;
1274
1295
  const written = await api.put(`/api/projects/${encodeURIComponent(slug)}/wiki`, payload);
1275
- // 로그 형태 경고는 쓰기를 막지 않는다 — 이미 저장된 뒤에 붙인다. 판정이 휴리스틱이라
1276
- // 거부하면 정당한 문서까지 막히고, 어차피 리비전이 있어 되돌릴 수 있다.
1277
- // 검사 대상은 저장된 본문이다: append면 이어붙인 결과 전체를 봐야 한다.
1278
- const warnings = wikiLogShapeCheck(written?.body)
1296
+ // 본문 판정(뼈대·로그 형태)은 서버가 저장된 본문에 걸어 warnings로 돌려준다.
1297
+ // 여기선 도구 호출 마크업 유출만 더한다 — 그건 MCP 인자 직렬화 사고라 이 층의 일이다.
1298
+ const warnings = (written?.warnings ?? [])
1279
1299
  .concat(toolMarkupCheck({ title, body: written?.body }));
1280
1300
  return jsonResult(warnings.length ? { ...written, warnings } : written);
1281
1301
  } catch (e) { return errorResult(e); }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "umtri-mcp",
3
- "version": "1.3.3",
3
+ "version": "1.4.0",
4
4
  "description": "MCP server for Umtri — give your AI coding agent a persistent map of your project's structure, dependencies, and bugs.",
5
5
  "mcpName": "io.umtri/umtri",
6
6
  "keywords": [