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 +17 -9
- package/core/policy.js +3 -46
- package/core/resources/transplant.md +3 -2
- package/core/resources/wiki-templates.md +80 -0
- package/core/resources/wiki.md +180 -137
- package/core/resources.js +8 -1
- package/core/tools.js +37 -17
- package/package.json +1 -1
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
|
|
60
|
-
`
|
|
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
|
-
|
|
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,
|
|
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/
|
|
76
|
-
umtri://rules/vocabulary-detailed umtri://rules/
|
|
77
|
-
umtri://rules/
|
|
78
|
-
umtri://rules/
|
|
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
|
-
//
|
|
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
|
|
59
|
-
|
|
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 | |
|
package/core/resources/wiki.md
CHANGED
|
@@ -1,22 +1,127 @@
|
|
|
1
1
|
# Writing a Ground's Wiki
|
|
2
2
|
|
|
3
|
-
The tree holds structure. The wiki
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
7
|
+
Four things to remember.
|
|
8
8
|
|
|
9
|
-
> **
|
|
10
|
-
>
|
|
11
|
-
>
|
|
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
|
-
|
|
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
|
-
> **
|
|
16
|
-
>
|
|
17
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
36
|
-
| "## 2026-08-27 — split the auth middleware" | "Auth runs in two stages: session or bearer resolution, then scope check.
|
|
37
|
-
| "Phase 1
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
66
|
-
failing at the one job only it can do.
|
|
161
|
+
## The write tells you what is off
|
|
67
162
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
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`.
|
|
86
|
-
|
|
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
|
|
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
|
|
101
|
-
way, what you decided against. That is revision 1, and
|
|
102
|
-
|
|
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
|
|
106
|
-
|
|
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
|
|
111
|
-
|
|
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
|
|
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
|
-
|
|
129
|
-
|
|
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
|
-
|
|
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
|
-
|
|
134
|
-
|
|
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.
|
|
157
|
-
|
|
158
|
-
|
|
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
|
|
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
|
-
|
|
|
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,
|
|
180
|
-
|
|
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
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
|
206
|
-
|
|
207
|
-
- **
|
|
208
|
-
|
|
209
|
-
|
|
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: '
|
|
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,
|
|
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.
|
|
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
|
|
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
|
-
//
|
|
796
|
-
|
|
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('
|
|
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
|
-
|
|
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