@docsworth/cli 0.0.0-stage → 0.2.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.
Files changed (91) hide show
  1. package/LICENSE +55 -0
  2. package/README.md +186 -2
  3. package/dist/cli.d.ts +11 -0
  4. package/dist/cli.js +409 -0
  5. package/dist/core/acl.d.ts +28 -0
  6. package/dist/core/acl.js +102 -0
  7. package/dist/core/audit-log.d.ts +133 -0
  8. package/dist/core/audit-log.js +290 -0
  9. package/dist/core/blocks.d.ts +35 -0
  10. package/dist/core/blocks.js +417 -0
  11. package/dist/core/codeowners.d.ts +62 -0
  12. package/dist/core/codeowners.js +133 -0
  13. package/dist/core/config.d.ts +96 -0
  14. package/dist/core/config.js +131 -0
  15. package/dist/core/coverage.d.ts +25 -0
  16. package/dist/core/coverage.js +41 -0
  17. package/dist/core/duration.d.ts +20 -0
  18. package/dist/core/duration.js +60 -0
  19. package/dist/core/errors.d.ts +11 -0
  20. package/dist/core/errors.js +13 -0
  21. package/dist/core/frontmatter.d.ts +17 -0
  22. package/dist/core/frontmatter.js +40 -0
  23. package/dist/core/gate.d.ts +17 -0
  24. package/dist/core/gate.js +25 -0
  25. package/dist/core/github.d.ts +152 -0
  26. package/dist/core/github.js +234 -0
  27. package/dist/core/host.d.ts +125 -0
  28. package/dist/core/host.js +22 -0
  29. package/dist/core/identity.d.ts +39 -0
  30. package/dist/core/identity.js +127 -0
  31. package/dist/core/init.d.ts +25 -0
  32. package/dist/core/init.js +86 -0
  33. package/dist/core/lint.d.ts +17 -0
  34. package/dist/core/lint.js +115 -0
  35. package/dist/core/mcp/audit-transport.d.ts +16 -0
  36. package/dist/core/mcp/audit-transport.js +238 -0
  37. package/dist/core/mcp/server.d.ts +19 -0
  38. package/dist/core/mcp/server.js +207 -0
  39. package/dist/core/mcp/text.d.ts +14 -0
  40. package/dist/core/mcp/text.js +43 -0
  41. package/dist/core/mirror-view.d.ts +68 -0
  42. package/dist/core/mirror-view.js +367 -0
  43. package/dist/core/new-page.d.ts +52 -0
  44. package/dist/core/new-page.js +49 -0
  45. package/dist/core/policy.d.ts +10 -0
  46. package/dist/core/policy.js +30 -0
  47. package/dist/core/polish.d.ts +42 -0
  48. package/dist/core/polish.js +1000 -0
  49. package/dist/core/references.d.ts +52 -0
  50. package/dist/core/references.js +1242 -0
  51. package/dist/core/repo.d.ts +42 -0
  52. package/dist/core/repo.js +116 -0
  53. package/dist/core/schema.d.ts +46 -0
  54. package/dist/core/schema.js +49 -0
  55. package/dist/core/search.d.ts +24 -0
  56. package/dist/core/search.js +98 -0
  57. package/dist/core/serve/auth.d.ts +23 -0
  58. package/dist/core/serve/auth.js +38 -0
  59. package/dist/core/serve/server.d.ts +22 -0
  60. package/dist/core/serve/server.js +138 -0
  61. package/dist/core/serve/sessions.d.ts +39 -0
  62. package/dist/core/serve/sessions.js +130 -0
  63. package/dist/core/serve/token.d.ts +10 -0
  64. package/dist/core/serve/token.js +96 -0
  65. package/dist/core/service.d.ts +375 -0
  66. package/dist/core/service.js +815 -0
  67. package/dist/core/sla.d.ts +16 -0
  68. package/dist/core/sla.js +20 -0
  69. package/dist/core/stale.d.ts +16 -0
  70. package/dist/core/stale.js +23 -0
  71. package/dist/core/tool-registry.d.ts +31 -0
  72. package/dist/core/tool-registry.js +77 -0
  73. package/dist/core/url.d.ts +4 -0
  74. package/dist/core/url.js +31 -0
  75. package/dist/core/version.d.ts +22 -0
  76. package/dist/core/version.js +32 -0
  77. package/dist/core/writes.d.ts +58 -0
  78. package/dist/core/writes.js +113 -0
  79. package/dist/web/assets/index-BglLMIyN.css +1 -0
  80. package/dist/web/assets/index-DugFPoRu.js +25 -0
  81. package/dist/web/index.html +14 -0
  82. package/docs/format-spec.md +311 -0
  83. package/package.json +75 -4
  84. package/plugin/.claude-plugin/plugin.json +8 -0
  85. package/plugin/.mcp.json +8 -0
  86. package/plugin/bin/docsworth-mcp.mjs +46 -0
  87. package/plugin/skills/draft-runbook/SKILL.md +75 -0
  88. package/plugin/skills/polish-page/SKILL.md +64 -0
  89. package/plugin/skills/update-after-incident/SKILL.md +59 -0
  90. package/plugin/skills/verify-page/SKILL.md +46 -0
  91. package/templates/github/knowledge.yml +51 -0
@@ -0,0 +1,14 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>Docsworth</title>
7
+ <link rel="icon" href="data:," />
8
+ <script type="module" crossorigin src="/assets/index-DugFPoRu.js"></script>
9
+ <link rel="stylesheet" crossorigin href="/assets/index-BglLMIyN.css">
10
+ </head>
11
+ <body class="dw">
12
+ <div id="app"></div>
13
+ </body>
14
+ </html>
@@ -0,0 +1,311 @@
1
+ # Docsworth format spec
2
+
3
+ **Format version:** 1 · **License:** Apache-2.0
4
+
5
+ This is the public contract between a knowledge repo and the tools that read it:
6
+ `docsworth lint`, the MCP server, the skills compiler and the mirror. A repo that follows it
7
+ can be linted, served to agents and compiled into skills by any implementation.
8
+
9
+ ## Repo layout
10
+
11
+ ```
12
+ knowledge/
13
+ index.md # root map; agents read this first (required)
14
+ glossary.md # acronyms and codenames (recommended)
15
+ services/<name>/index.md # type: service
16
+ runbooks/<slug>.md # type: runbook
17
+ decisions/ADR-0001-<slug>.md # type: adr
18
+ onboarding/<role>.md # type: guide
19
+ <anything>/… # any type; only the three directories above constrain it
20
+ .knowledge/config.yaml # org, repo, SLA window, mirror sources
21
+ CODEOWNERS # or .github/CODEOWNERS or docs/CODEOWNERS
22
+ ```
23
+
24
+ Every `.md` file under the knowledge root is a page, and every page has frontmatter.
25
+
26
+ ## Frontmatter
27
+
28
+ At most 16 KB of YAML between the `---` lines; a page over that is reported by lint and
29
+ not served (frontmatter runs to a few hundred bytes, and YAML parsing slows past linear
30
+ on hostile input).
31
+
32
+ ```yaml
33
+ ---
34
+ title: Deploying to production
35
+ id: rb-deploy-prod # required, stable, unique; lowercase letters, digits, dashes
36
+ type: runbook # required: runbook | adr | service | guide | policy
37
+ status: canonical # required: canonical | draft | deprecated
38
+ owner_team: platform # required: team slug; CODEOWNERS must assign @<org>/platform
39
+ last_verified: 2026-10-15 # required: YYYY-MM-DD, a real calendar date
40
+ review_every: 90d # required: <n>d | <n>w | <n>m | <n>y
41
+ supersedes: [rb-deploy-legacy] # optional: ids this page replaces
42
+ applies_to: [service:payments] # optional: free-form tags, matched exactly by search filters
43
+ acl: internal # optional, default internal; or a team slug
44
+ description: One sentence. # optional; used in search and compiled skills
45
+ ---
46
+ ```
47
+
48
+ Unknown fields produce a lint warning, never an error.
49
+
50
+ ## Ownership
51
+
52
+ - CODEOWNERS uses GitHub's semantics: the last matching rule wins. It supports anchored,
53
+ `*`, `**` and trailing-slash patterns, and `docs/*` matches direct children only.
54
+ - The winning rule for a page's path must name **exactly one owner**, and that owner
55
+ must be a **team of the configured org** (`@<org>/<team>`). Individual owners (`@alice`,
56
+ emails), several owners, foreign-org teams and owner-less rules are all lint errors.
57
+ Individual ownership is banned deliberately: people change teams, and teams don't.
58
+ - `owner_team` must equal that team's slug.
59
+
60
+ ## Review SLA
61
+
62
+ Dates are date-only and compared as UTC calendar dates. The due date is
63
+ `last_verified + review_every`:
64
+
65
+ - `d` and `w` add fixed days.
66
+ - `m` and `y` add calendar months or years, clamped to the end of the month
67
+ (2026-01-31 + 1m = 2026-02-28).
68
+
69
+ States are evaluated in this order:
70
+
71
+ | State | Condition |
72
+ | --- | --- |
73
+ | `unverified` | `last_verified` or `review_every` missing or invalid (this is also a lint error) |
74
+ | `overdue` | today is after the due date (a lint warning on canonical pages) |
75
+ | `due_soon` | at most `sla.due_soon_days` (default 14) days until due |
76
+ | `fresh` | otherwise |
77
+
78
+ The due date itself is still within SLA.
79
+
80
+ ## Citation gate
81
+
82
+ A page is **citable** when all three hold:
83
+
84
+ 1. `status: canonical`.
85
+ 2. Its SLA state is `fresh` or `due_soon`.
86
+ 3. Its ownership resolves as above.
87
+
88
+ The gate is fixed by this spec; configuration cannot widen it.
89
+
90
+ - **`search`:** returns only citable pages. Matching pages that aren't citable are
91
+ listed separately as `excluded`, each with a reason such as "past review date (due
92
+ 2026-10-30), owned by platform". Agents can then say what was excluded instead of
93
+ silently missing it.
94
+ - **`read`:** returns any visible page, with `citable`, `flags` and `gate_reason`.
95
+
96
+ ## ACL
97
+
98
+ `acl` narrows which callers the **MCP server serves** a page to:
99
+
100
+ - **`internal`:** anyone the server serves at all.
101
+ - **A team slug:** only members of `@<org>/<slug>`.
102
+
103
+ In a page the caller can see, a reference to a page they can't see (a link in any form,
104
+ a URL, or its path in text) reads `[restricted page]`. A page that doesn't load counts as
105
+ one they can't see, and so does a link to a path under the knowledge root that is no page
106
+ at all: relative, or, with `github.repo` set, a GitHub URL into the repo on any branch.
107
+ See [security-model.md](security-model.md#the-one-read-path).
108
+
109
+ **`acl` isn't confidentiality.** GitHub has no per-file read permissions, so anyone with
110
+ read access to the knowledge repo can read every file in it. Keep confidential content
111
+ in a separate repository whose GitHub permissions match its audience. See
112
+ [security-model.md](security-model.md).
113
+
114
+ ## Runbook sections
115
+
116
+ For the skills compiler, a runbook has these `##` sections. The heading match is
117
+ case-insensitive:
118
+
119
+ | Section | Holds |
120
+ | --- | --- |
121
+ | `## When to use` | Triggers: alerts, symptoms, requests. This becomes the skill's description. |
122
+ | `## Steps` | Numbered actions with exact commands. |
123
+ | `## Checks` | How to tell it worked. |
124
+ | `## Rollback` | How to undo it, or why it can't be undone. |
125
+
126
+ A canonical runbook missing any of these still lints. It's reported and skipped by
127
+ `docsworth compile`.
128
+
129
+ ## Compiled skills
130
+
131
+ `docsworth compile` writes `skills/<id>/SKILL.md` in the Agent Skills format: `name` and
132
+ `description` frontmatter, plus string-valued `metadata`:
133
+
134
+ ```yaml
135
+ metadata:
136
+ source_id: rb-deploy-prod
137
+ source_path: knowledge/runbooks/deploy-production.md
138
+ source_sha256: <sha256 of the source file>
139
+ owner_team: platform
140
+ last_verified: 2026-10-15
141
+ review_every: 90d
142
+ review_due: 2027-01-13
143
+ compiled_at: 2026-11-02
144
+ stale: "false"
145
+ ```
146
+
147
+ A skill is **stale** when its source page changed (`source_sha256`), was re-verified
148
+ (`last_verified`), stopped being canonical, or was deleted. `docsworth compile --check` exits 1
149
+ on stale skills. `--mark-stale` sets `stale: "true"` and prefixes the description with
150
+ `[STALE: …]`. Runbooks with a non-`internal` ACL are compiled only with
151
+ `--include-restricted`.
152
+
153
+ Compiling removes only skills whose `generator` is `docsworth`. A skill whose generator
154
+ is `agent-kb`, the pre-release name, is rewritten if its page still compiles. If it
155
+ remains, `--check` reports it, but it's never removed or marked, because that name now
156
+ belongs to another publisher: delete it by hand.
157
+
158
+ ## Polish
159
+
160
+ `propose_edit` with `polish: true` proposes a restructure or rewording that must not
161
+ change what the page says. The agent sends the new `body` (what `read` returns as
162
+ `content`, rewritten), and the server keeps the frontmatter. Before any branch is
163
+ created, the server compares the current page with the proposed one and refuses
164
+ (`invalid`, naming the changes it saw, up to eight) unless all of these are unchanged.
165
+ Pages over 64 KB, before or after, aren't polished.
166
+
167
+ | Compared | How |
168
+ | --- | --- |
169
+ | Frontmatter | byte for byte |
170
+ | Code order | all code, fenced and inline, in reading order; and the code under each heading both versions have (ATX or setext, in a quote or a list item or neither, subsections included) stays under it. Headings match as written, apart from spacing and closing `#`s, and a repeated heading matches by its occurrence while both versions repeat it as often. Sections without code may move, and headings may be added or reworded |
171
+ | Code blocks, fenced (backticks or tildes) and indented (four spaces or a tab), in quotes and list items too | content, as CommonMark reads the page's block structure: a line is code only where CommonMark makes it code, and leaves code where CommonMark ends the block (a quote or list item that ends, an outdented line). Each line loses its containers' markers and up to the fence's own indentation, so moving a block into a list item or a quote is a polish but shifting its code isn't. Trailing spaces count (`\ ` ends a shell line differently from `\`), and so does a fence's info string (```` ```text ```` stays inert where ```` ```mermaid ```` draws a diagram), added, dropped or changed. The fence's characters and line endings are ignored, and a fenced block without an info string equals an indented one with the same lines |
172
+ | Reference definitions | read for the links that use them, and otherwise treated as invisible, as they render: nothing inside one counts as page text. A line is a definition only if CommonMark takes it as one: its destination in `<…>`, or bare with no space or control character and its parentheses balanced. Anything else renders, and is compared, as text. Code spans in a label, destination or title count as written, and labels match as CommonMark matches them (`[STRASSE]` uses `[straße]: …`). A page isn't polished at all if a definition-like line is indented by a tab or four spaces, has a label or title spanning lines, or follows an indented or `===` line: whether it renders depends on block structure this check doesn't model. Use `edits` there |
173
+ | HTML comments (outside code) | as a multiset, and kept out of every other row: a fact moved into a comment has left the page a reader sees. A comment inside a code block is code. A comment block (a line starting `<!--`, to its `-->`) holds text, as CommonMark reads it: code or a definition inside it isn't one. A comment opened mid-paragraph that wraps a code block makes the page unpolishable |
174
+ | Raw HTML (tags, processing instructions, declarations, CDATA, and any opener such as `<!--`, `<?` or `<div`, closed or not) | none may be added: an unclosed opener hides everything after it. A page that already has raw HTML (outside code), or a control character U+0000 to U+0004 (the check's own marks), isn't polished at all: where a tag sits decides what it hides (`<span hidden>…</span>`), which only a renderer could compare. Use `edits` there |
175
+ | Unusual characters | a polish may not add an invisible character (zero-width, soft hyphen, an ASCII control), even one the page already has or a letter from a script the page doesn't use (a Cyrillic `о` in an English word). Numeric entities and `&amp; &lt; &gt; &quot; &apos; &nbsp; &minus; &plus;` are decoded first, for this and for numbers and negation words. Other named entities are compared as written, each with what follows it: a polish adds, removes or moves none |
176
+ | Inline code, link targets | as multisets: order may change, nothing added or dropped. Each link is also compared with the text that shows it, as written apart from spacing, and its title, so links may move but not trade destinations or change what they say on hover. Link targets are inline and reference links (resolved through their definitions), autolinks (`<https://…>`, `<ops@example.com>`), bare URLs, emails and `www.` links, and HTML `href`/`src`, quoted or not. Punctuation after a bare URL isn't part of it, as GFM links it (`?!.,:*_~;` and an unbalanced `)`). A `](` that doesn't parse as a link is compared as written, to the end of its line |
177
+ | Struck-through text (`~~…~~`, `<del>`, `<s>`) | as a multiset, so striking an instruction out is a change |
178
+ | Task-list items (`- [ ]`, `1. [x]`) | each box with its task's text, as a multiset: tasks may move with their boxes, but ticking one, or moving a tick to another task, says a different step is done. A task's text stays as written |
179
+ | Words with digits (`TLSv1.2`, `IPv4`, `RHEL8`, `p99`) | as a multiset |
180
+ | Numbers (digits of any script) | each with its sign and the word after it (`-5 minutes`, `.5 seconds`, `3 retries`, `5-minute`) as a multiset, so swapping two numbers with different units is caught. Every run of digits in the visible text is compared too, whatever it's attached to (`on-c3`). A plural is the same unit. Case counts (`5 MB` isn't `5 Mb`, `500MB` isn't `500Mb`), except that a unit word of three letters or more may be capitalised (`5 Minutes`). Ordered-list markers aren't numbers where they surely start a list item (a `1`, after a blank line or a heading, or after list items at the same indentation), so renumbering a list is a polish; `retry\n3. times` is paragraph text in CommonMark, and its `3` is compared. The word may sit on the next line of the same paragraph; bold and italics are ignored |
181
+ | Negation and modal words | the count of each, in any case: not, never, no, must, should, cannot, can't, don't, doesn't, won't, shouldn't, mustn't, isn't, aren't, only, without, unless, except, nor, none, neither, always, and every other "n't" (didn't, wasn't, …) counted together, counted twice: with emphasis markers taken out and with them read as breaks, since an unclosed `**` is text in CommonMark (`no**t` isn't `not`) |
182
+
183
+ Because the frontmatter and every link target are unchanged, a polished page stays as
184
+ valid as it was.
185
+
186
+ On a page that links to pages the caller can't see (shown as `[restricted page]` in
187
+ `read`), `propose_edit` takes only `edits`, and no polish. A whole `body` or `content`
188
+ written from the redacted view would overwrite or drop those links, and a polish
189
+ refusal would name them. An edit that writes `[restricted page]` is refused too. An
190
+ `append` is taken there: it adds to the end of the page as it is on the default branch,
191
+ so nothing written from the redacted view goes back into it.
192
+
193
+ ## New pages (`draft_page`)
194
+
195
+ `draft_page` opens a draft PR that adds one page, on a `docsworth/draft/<id>-…` branch.
196
+
197
+ - **The server names it.** Its id is `<prefix>-<slug>-<12 hex>` and its file
198
+ `<slug>-<12 hex>.md`, where the slug comes from the title and the hex is random. A name
199
+ the caller chose could be aimed at a page they can't see, and "already exists" would
200
+ then say it is there. Owners can rename it in the PR before merging.
201
+ - **Where it lands.** The type's directory (`runbooks/`, `services/<name>/index.md`,
202
+ `guides/`, `policies/`) or a `dir` under the knowledge root. CODEOWNERS must assign
203
+ `owner_team` there, so the page lints. Where the plain name is another team's, the
204
+ server tries `<owner_team>-<slug>-<12 hex>` (id `<prefix>-<owner_team>-<slug>-<12 hex>`)
205
+ in the same directory, so rules on file names (`runbooks/payments-*`) route the page to
206
+ its team; if that isn't the team's either, `draft_page` refuses and names the directory.
207
+ ADRs are written by hand.
208
+ - **What it holds.** `status: draft` (never citable until its owners make it canonical),
209
+ `last_verified` today, `review_every: 90d`, the caller's one-line title and optional
210
+ description, and `acl` `internal` or a team the caller is on. A `source_url` becomes a
211
+ first line, `> Captured from <url> on <date>.`
212
+
213
+ **How it's checked.** Besides its own tests, the guard is fuzzed against CommonMark's
214
+ reference parser (commonmark.js): pages are mutated at random, and no pair may change a
215
+ code block, inline code, a link (its text, target or title, and GitHub's bare-URL
216
+ autolinks), a digit or a negation word as commonmark.js renders it while the guard
217
+ reports nothing (`test/polish-fuzz.test.ts`, and `scripts/fuzz-polish.ts` for long
218
+ runs). Where CommonMark implementations disagree (a tab inside a link's or a
219
+ definition's parentheses), the page is refused rather than read one way.
220
+
221
+ **Not detected.** This is a structural check, not proof that the meaning is unchanged.
222
+ It doesn't see a renamed service or team in prose, a command written as plain prose,
223
+ a changed word that isn't a number's unit, a
224
+ number spelled out (`five`), two numbers with the same unit trading places (`wait 5
225
+ minutes`, `retry after 10 minutes`), steps without code put in another order, code
226
+ moved across headings that were reworded in the same polish, a negation moved to
227
+ another clause, or a reworded sentence that now means something else. That's why the PR goes to the owner team like any other, titled
228
+ `polish(<id>): <title>`, with a body that lists what the guard compared and asks for
229
+ the diff to be read.
230
+
231
+ ## Mirror namespace
232
+
233
+ Mirrored pages live under `mirror.out_dir` (default `.knowledge/mirror/`), never under
234
+ the knowledge root, and are never citable. Each file carries a `mirror:` frontmatter block
235
+ (source, native id, url, `last_edited`, versions, `owner_team` from a `team-<slug>` or
236
+ `owner-<slug>` label, labels, acl). Each source and space directory also holds
237
+ `_scores.json` and `_report.json`. Pages with read restrictions in the source, and their
238
+ descendants, are never written, and the report lists them by native id only.
239
+
240
+ **Serving the mirror** (`mirror.serve: true`, off by default, and not yet recommended:
241
+ on the benchmark it was neutral on answer questions but just missed its bar on gate
242
+ questions). The MCP server then lists
243
+ matching mirrored pages in search under `mirrored`, at most 3, apart from `results`, each
244
+ with `citable: false`, its source URL and last edit. `read` takes a `mirror:` id and
245
+ returns the page with `citable: false` and `flags: ["mirrored"]`. Filters on page type
246
+ or `applies_to` leave mirrored pages out; `owner_team` applies to them.
247
+
248
+ - **Audience:** each space's `acl`, by the same rule as pages. A hidden and an unknown
249
+ mirror id answer alike, and a team space's page taints the session like a team page.
250
+ - **Links:** in a mirrored page someone can see, links to a repo page they can't see
251
+ read `[restricted page]` as they do in repo pages (by URL, say). In a page someone can
252
+ see, mirrored or from the repo, links to a mirrored
253
+ page they can't see read `[restricted page]` (and don't count toward ranking): markdown links (any title form, nested or escaped brackets),
254
+ bare URLs, and Confluence's linked-title form. A link is matched by the page it
255
+ names, so a relative path, a URL without its title slug, `?pageId=`, a Confluence
256
+ tiny link (`/wiki/x/…`), or a Notion id in any form is caught too, also encoded,
257
+ broken across lines or with markup inside, as for repo pages.
258
+ - **Ranking:** each audience gets its own index, so ranking, snippets and counts never
259
+ draw on a page outside that audience.
260
+ - **What is served:** only files one of this user's mirror runs wrote and that are
261
+ unchanged since, read through the records in the state directory. A new run is picked
262
+ up without a restart. An `out_dir` that resolves outside the repo serves nothing, with
263
+ a warning.
264
+
265
+ ## Config (`.knowledge/config.yaml`)
266
+
267
+ ```yaml
268
+ format: 1
269
+ knowledge_root: knowledge
270
+ github:
271
+ org: acme # required: team owners resolve to @acme/<team>
272
+ repo: acme/knowledge # optional: makes GitHub the ACL authority; enables PR tools
273
+ default_branch: main # optional: otherwise read from GitHub
274
+ api_url: https://ghe.example.com/api/v3 # optional: GitHub Enterprise Server
275
+ sla:
276
+ due_soon_days: 14
277
+ usage_log: true # page-id-only read log for the owner digest
278
+ audit_log: # attributed audit log; off unless enabled
279
+ enabled: true
280
+ dir: /var/lib/docsworth/audit # optional; default $XDG_STATE_HOME/docsworth/<repo>-<hash>/audit
281
+ include_queries: false # search text is not recorded unless true
282
+ required: false # true: nothing is answered or changed unless its event is written first (see security-model.md)
283
+ retention_days: 365
284
+ policy: # org policy; every default keeps v0.1 behaviour
285
+ admins: kb-admins # lint: CODEOWNERS must make this team own the config and CODEOWNERS
286
+ tools: # per-tool allowlists (search, read, list, stale, propose_edit, verify, draft_page); an empty list disables a tool for everyone
287
+ propose_edit: { teams: [platform, payments] }
288
+ writes: enabled # disabled: propose_edit, verify and draft_page are not offered
289
+ require_human_approval: false # true: Claude Code asks on every write call
290
+ include_stale: allowed # denied: search never returns gated pages inline
291
+ identity_cache_seconds: 300 # team-membership cache for reads (30-3600)
292
+ audit_http: # pro: ship each event to a collector (SIEM) as it happens
293
+ url_env: DOCSWORTH_AUDIT_URL # env var with the https:// URL (http only on loopback)
294
+ token_env: DOCSWORTH_AUDIT_TOKEN
295
+ required: false
296
+ skills:
297
+ out_dir: skills
298
+ mirror:
299
+ out_dir: .knowledge/mirror
300
+ serve: false # true: search lists matching mirrored pages under `mirrored` (never citable)
301
+ sources:
302
+ - kind: confluence
303
+ base_url: https://acme.atlassian.net
304
+ spaces: [{ key: ENG, acl: internal }]
305
+ - kind: notion
306
+ root_page_ids: [] # empty: everything shared with the integration
307
+ acl: internal
308
+ digest:
309
+ slack_webhook_env: SLACK_WEBHOOK_URL
310
+ team_webhooks: { security: SLACK_WEBHOOK_SECURITY }
311
+ ```
package/package.json CHANGED
@@ -1,6 +1,77 @@
1
1
  {
2
2
  "name": "@docsworth/cli",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.0",
4
+ "description": "Git-native knowledge layer for coding agents: team-owned pages, review SLAs, a citation gate, and an MCP server agents read for free.",
5
+ "type": "module",
6
+ "license": "SEE LICENSE IN LICENSE",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/docsworth/docsworth.git"
10
+ },
11
+ "homepage": "https://github.com/docsworth/docsworth#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/docsworth/docsworth/issues"
14
+ },
15
+ "engines": {
16
+ "node": ">=22.12"
17
+ },
18
+ "bin": {
19
+ "docsworth": "dist/cli.js"
20
+ },
21
+ "exports": {
22
+ "./host": {
23
+ "types": "./dist/core/host.d.ts",
24
+ "default": "./dist/core/host.js"
25
+ },
26
+ "./package.json": "./package.json"
27
+ },
28
+ "files": [
29
+ "dist",
30
+ "!dist/pro",
31
+ "plugin",
32
+ "templates",
33
+ "docs/format-spec.md"
34
+ ],
35
+ "workspaces": [
36
+ "packages/ui",
37
+ "packages/web",
38
+ "packages/extension"
39
+ ],
40
+ "publishConfig": {
41
+ "access": "public"
42
+ },
43
+ "scripts": {
44
+ "prebuild": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
45
+ "build": "tsc -p tsconfig.build.json && npm run build -w @docsworth/web && node scripts/copy-web.mjs",
46
+ "postbuild": "node -e \"require('node:fs').chmodSync('dist/cli.js', 0o755)\"",
47
+ "docsworth": "tsx --conditions=docsworth-source src/cli.ts",
48
+ "typecheck": "tsc --noEmit",
49
+ "test": "npm run test:core && npm run test:web",
50
+ "test:core": "npm run typecheck && vitest run",
51
+ "test:web": "npm test -w @docsworth/ui && npm test -w @docsworth/web && npm test -w @docsworth/extension",
52
+ "test:watch": "vitest",
53
+ "bench:search": "tsx scripts/bench-search.ts",
54
+ "live:github": "tsx scripts/live/github.ts",
55
+ "live:quickstart": "tsx scripts/live/quickstart.ts",
56
+ "live:skills": "tsx scripts/live/skills.ts",
57
+ "live:governance": "tsx scripts/live/governance.ts",
58
+ "live:polish": "tsx scripts/live/polish.ts",
59
+ "live:serve": "tsx scripts/live/serve.ts"
60
+ },
61
+ "dependencies": {
62
+ "@modelcontextprotocol/sdk": "^1.32.2",
63
+ "commander": "^15.0.0",
64
+ "yaml": "^2.9.1",
65
+ "zod": "^4.6.7"
66
+ },
67
+ "devDependencies": {
68
+ "@mixmark-io/domino": "2.2.0",
69
+ "@types/commonmark": "0.27.10",
70
+ "@types/node": "^22.20.5",
71
+ "commonmark": "0.31.2",
72
+ "entities": "3.0.1",
73
+ "tsx": "^4.23.15",
74
+ "typescript": "^7.0.2",
75
+ "vitest": "^5.1.0"
76
+ }
77
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "docsworth",
3
+ "version": "0.2.0",
4
+ "description": "Team-owned, verified engineering knowledge for Claude Code: an MCP server that cites only canonical pages inside their review SLA, plus skills to draft, update and verify runbooks through PRs.",
5
+ "author": { "name": "docsworth" },
6
+ "license": "Apache-2.0",
7
+ "keywords": ["knowledge", "runbooks", "docs-as-code", "mcp", "codeowners"]
8
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "mcpServers": {
3
+ "docsworth": {
4
+ "command": "node",
5
+ "args": ["${CLAUDE_PLUGIN_ROOT}/bin/docsworth-mcp.mjs"]
6
+ }
7
+ }
8
+ }
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env node
2
+ // Launches the Docsworth MCP server for Claude Code.
3
+ // DOCSWORTH_KNOWLEDGE_DIR local clone of the knowledge repo (default: the current project)
4
+ // DOCSWORTH_BIN a local `docsworth` executable to use instead of the published
5
+ // package: an absolute path, needed to use Docsworth pro, which an
6
+ // npx-run CLI can't see
7
+ import { spawn } from "node:child_process";
8
+ import { homedir } from "node:os";
9
+ import { resolve } from "node:path";
10
+
11
+ // The npm package to run. `agent-kb` on npm belongs to another publisher and nobody owns the
12
+ // bare `docsworth`: never fall back to either. Kept equal to PUBLISHED_PACKAGE in
13
+ // src/core/version.ts by test/package-name.test.ts.
14
+ const PUBLISHED_PACKAGE = "@docsworth/cli@^0.2";
15
+
16
+ // The repo reaches the server in the environment, never on the command line, which Windows
17
+ // runs through cmd.exe: a path would split at a space there, and what follows an `&` would
18
+ // run. A leading `~` is the home directory, as a shell would read it.
19
+ const dir = resolve((process.env.DOCSWORTH_KNOWLEDGE_DIR || process.cwd()).replace(/^~(?=$|[\\/])/, () => homedir()));
20
+ const env = { ...process.env, DOCSWORTH_KNOWLEDGE_DIR: dir };
21
+
22
+ // npx runs in the home directory, so a .npmrc in the project can't send `@docsworth` to
23
+ // another registry. A local CLI runs in the repo, which one too old to read the variable
24
+ // serves by default.
25
+ const [cmd, args, cwd] = process.env.DOCSWORTH_BIN
26
+ ? [process.env.DOCSWORTH_BIN, ["mcp"], dir]
27
+ : ["npx", ["-y", PUBLISHED_PACKAGE, "mcp"], homedir()];
28
+
29
+ // Windows starts npx, and npm's .cmd shims, only through cmd.exe: each word is quoted, so the
30
+ // `^` of the range and a space in DOCSWORTH_BIN arrive as they are. Quotes don't stop cmd.exe
31
+ // expanding %VAR% (or !VAR!), and a quote or a line break would end the word, so a
32
+ // DOCSWORTH_BIN with any of them is refused, not misread. No Windows path has them but %, !.
33
+ const windows = process.platform === "win32";
34
+ if (windows && /[%!"\x00-\x1f]/.test(cmd)) {
35
+ process.stderr.write(`docsworth: DOCSWORTH_BIN (${JSON.stringify(cmd)}) has a %, !, " or control character, which cmd.exe would read as part of a command; use a path without them\n`);
36
+ process.exit(1);
37
+ }
38
+ const child = windows
39
+ ? spawn([cmd, ...args].map((w) => `"${w}"`).join(" "), { stdio: "inherit", shell: true, cwd, env })
40
+ : spawn(cmd, args, { stdio: "inherit", cwd, env });
41
+ child.on("error", (e) => {
42
+ process.stderr.write(`docsworth: couldn't start ${cmd}: ${e.message}\n`);
43
+ process.exit(1);
44
+ });
45
+ child.on("exit", (code, signal) => process.exit(code ?? (signal ? 1 : 0)));
46
+ for (const s of ["SIGINT", "SIGTERM"]) process.on(s, () => child.kill(s));
@@ -0,0 +1,75 @@
1
+ ---
2
+ name: draft-runbook
3
+ description: Turn an incident write-up, postmortem, Slack thread or pasted notes into a draft runbook in the team's knowledge repo, opened as a draft pull request for the owning team. Use when someone says "write a runbook for this", "turn this incident into a runbook", or pastes an incident timeline and asks for a procedure.
4
+ ---
5
+
6
+ # Draft a runbook
7
+
8
+ The goal is a draft PR that adds one runbook in the Docsworth format. It's owned by one
9
+ team and has the four sections the skills compiler needs. Nothing merges without that
10
+ team's review.
11
+
12
+ ## 1. Gather the source
13
+
14
+ Work only from material the user gave you: pasted text, a file path, or an incident
15
+ document they point to. If a command or step isn't in the source, don't invent it. Leave
16
+ a `TODO(owner):` line where the source is silent.
17
+
18
+ ## 2. Check for an existing page first
19
+
20
+ Use the Docsworth `search` tool twice: once with the incident's symptoms, once with the
21
+ service name. Pass `include_stale: true` both times, so you also see excluded pages.
22
+
23
+ - If a runbook already covers this procedure, stop. Use the `update-after-incident`
24
+ skill instead and propose edits to that page. A second runbook for the same thing is
25
+ how wikis rot.
26
+ - If a gated or overdue page covers it, say so and name the owning team.
27
+
28
+ ## 3. Work out the owner
29
+
30
+ The owner is a team, never a person. Read the service page with the `read` tool (for
31
+ example `svc-<service>`) and use its `owner_team`. If several teams are plausible, ask
32
+ the user. You don't pick the file name: `draft_page` names the page and places it where
33
+ CODEOWNERS gives it to that team, as `<team>-<slug>` where CODEOWNERS routes runbooks by
34
+ prefix.
35
+
36
+ ## 4. Write the body
37
+
38
+ `draft_page` writes the frontmatter itself (`status: draft`, the owner, today's
39
+ `last_verified`, `review_every: 90d`), so write only the markdown after it, in this
40
+ shape. The skills compiler needs all four sections.
41
+
42
+ ```markdown
43
+ # <Imperative title, e.g. Drain the webhook backlog>
44
+
45
+ ## When to use
46
+ - <the alert, symptom or request that triggers this>
47
+
48
+ ## Steps
49
+ 1. <one action per step, with the exact command in backticks>
50
+
51
+ ## Checks
52
+ - <how to tell it worked>
53
+
54
+ ## Rollback
55
+ - <how to undo it, or why it cannot be undone>
56
+ ```
57
+
58
+ ## 5. Open the draft PR with `draft_page`
59
+
60
+ Call the Docsworth `draft_page` tool. Don't use git or `gh` for this: the tool opens the
61
+ PR from its own `docsworth/draft/` branch and never writes to the default branch.
62
+
63
+ - `type`: `runbook`
64
+ - `title`: the imperative title
65
+ - `owner_team`: the team from step 3
66
+ - `description`: one sentence, what this procedure does
67
+ - `body`: the markdown from step 4
68
+ - `source_url`: the incident document's link, if the user gave one
69
+ - `reason`: the incident it comes from, every `TODO(owner):` you left, and the service
70
+ it applies to (`applies_to: [service:<name>]`), which the owners add in review
71
+
72
+ If it refuses because CODEOWNERS gives the directory to another team, tell the user what
73
+ it said and ask which directory the team owns; pass that as `dir` and call it again.
74
+
75
+ Report the PR URL and the page's path, and list every `TODO(owner):` you left.
@@ -0,0 +1,64 @@
1
+ ---
2
+ name: polish-page
3
+ description: Tidy a Docsworth page without changing what it says, by restructuring and rewording it and opening a polish PR for the owning team. Use when someone says "clean up this runbook", "this page is hard to follow", "polish the docs for X", or asks to reformat a page to the runbook sections.
4
+ ---
5
+
6
+ # Polish a page
7
+
8
+ A polish makes a page easier to read and act on. It never changes a fact. The server
9
+ backs that up with a structural guard: `propose_edit` with `polish: true` refuses a
10
+ change to the page's code blocks, inline code, links, numbers or negation words. The
11
+ guard can't see meaning, or a command written as plain prose, so the owners still
12
+ review the diff.
13
+
14
+ ## 1. Read the page
15
+
16
+ Call `read` with the page id. Work from `content` exactly as returned: it's the page
17
+ body, without the frontmatter, which a polish never touches. If the user named a topic
18
+ rather than a page, `search` first and confirm which page they mean.
19
+
20
+ ## 2. Rewrite the body
21
+
22
+ You may:
23
+
24
+ - move sections that hold no code
25
+ - split long paragraphs
26
+ - turn a run of steps into a numbered list
27
+ - fix or add headings. A runbook reads best with `## When to use`, `## Steps`,
28
+ `## Checks` and `## Rollback`.
29
+ - tighten wording
30
+
31
+ Keep these exactly as they are:
32
+
33
+ - every command and code block (fenced or inline), in the same order and under the same
34
+ headings, and each fence's language tag (the word after ```): don't add or drop one
35
+ - every inline code span, link target and URL, and each link's text with its target
36
+ - any struck-through text, and nothing newly struck through
37
+ - each task-list item's box and text, as written
38
+ - every number with the word after it (`5 minutes`, `3 retries`)
39
+ - how often each negation or modal word appears: not, never, no, must, should,
40
+ cannot, can't, don't, doesn't, won't, shouldn't, mustn't, isn't, aren't, only,
41
+ without, unless, except, nor, none, neither, always
42
+
43
+ Don't add or drop facts, and keep steps in their order. If something looks wrong or
44
+ out of date, leave it and say so in the reason. Fixing it is an ordinary edit, which the owners review as one.
45
+
46
+ ## 3. Propose it
47
+
48
+ Call `propose_edit` with `id`, `polish: true`, the rewritten text as `body`, and a
49
+ `reason` describing what you restructured. The server keeps the frontmatter as it is.
50
+
51
+ If it refuses, the message names each change the guard saw. Put those back exactly
52
+ and try again. Don't switch to an ordinary edit to get past the guard.
53
+
54
+ If the page shows `[restricted page]`, it links to pages you can't see, and the server
55
+ won't take a polish of it from this session (or any whole-body rewrite). Tell the user
56
+ rather than working around it.
57
+
58
+ If it refuses because this session read pages with a narrower audience, don't work
59
+ around it. Tell the user which page to polish in a fresh session.
60
+
61
+ ## 4. Report
62
+
63
+ Give the PR URL and the owning team. Mention anything you noticed that looked wrong,
64
+ since a polish deliberately left it alone.