@docsworth/cli 0.0.0-stage → 0.3.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 (103) hide show
  1. package/LICENSE +55 -0
  2. package/README.md +238 -2
  3. package/dist/cli.d.ts +11 -0
  4. package/dist/cli.js +458 -0
  5. package/dist/core/acl.d.ts +37 -0
  6. package/dist/core/acl.js +136 -0
  7. package/dist/core/adopt.d.ts +68 -0
  8. package/dist/core/adopt.js +665 -0
  9. package/dist/core/audit-log.d.ts +133 -0
  10. package/dist/core/audit-log.js +290 -0
  11. package/dist/core/blocks.d.ts +35 -0
  12. package/dist/core/blocks.js +417 -0
  13. package/dist/core/codeowners.d.ts +62 -0
  14. package/dist/core/codeowners.js +133 -0
  15. package/dist/core/config.d.ts +101 -0
  16. package/dist/core/config.js +175 -0
  17. package/dist/core/coverage.d.ts +25 -0
  18. package/dist/core/coverage.js +41 -0
  19. package/dist/core/duration.d.ts +20 -0
  20. package/dist/core/duration.js +60 -0
  21. package/dist/core/edits.d.ts +13 -0
  22. package/dist/core/edits.js +20 -0
  23. package/dist/core/errors.d.ts +11 -0
  24. package/dist/core/errors.js +13 -0
  25. package/dist/core/frontmatter.d.ts +29 -0
  26. package/dist/core/frontmatter.js +49 -0
  27. package/dist/core/gate.d.ts +17 -0
  28. package/dist/core/gate.js +25 -0
  29. package/dist/core/gfm.d.ts +13 -0
  30. package/dist/core/gfm.js +244 -0
  31. package/dist/core/github.d.ts +152 -0
  32. package/dist/core/github.js +234 -0
  33. package/dist/core/host.d.ts +125 -0
  34. package/dist/core/host.js +22 -0
  35. package/dist/core/identity.d.ts +39 -0
  36. package/dist/core/identity.js +127 -0
  37. package/dist/core/init.d.ts +25 -0
  38. package/dist/core/init.js +86 -0
  39. package/dist/core/links.d.ts +82 -0
  40. package/dist/core/links.js +345 -0
  41. package/dist/core/lint.d.ts +16 -0
  42. package/dist/core/lint.js +149 -0
  43. package/dist/core/mcp/audit-transport.d.ts +16 -0
  44. package/dist/core/mcp/audit-transport.js +238 -0
  45. package/dist/core/mcp/server.d.ts +19 -0
  46. package/dist/core/mcp/server.js +216 -0
  47. package/dist/core/mcp/text.d.ts +14 -0
  48. package/dist/core/mcp/text.js +43 -0
  49. package/dist/core/mirror-view.d.ts +68 -0
  50. package/dist/core/mirror-view.js +367 -0
  51. package/dist/core/new-page.d.ts +52 -0
  52. package/dist/core/new-page.js +49 -0
  53. package/dist/core/policy.d.ts +10 -0
  54. package/dist/core/policy.js +30 -0
  55. package/dist/core/polish.d.ts +42 -0
  56. package/dist/core/polish.js +1000 -0
  57. package/dist/core/printable.d.ts +4 -0
  58. package/dist/core/printable.js +16 -0
  59. package/dist/core/references.d.ts +87 -0
  60. package/dist/core/references.js +1744 -0
  61. package/dist/core/repo.d.ts +46 -0
  62. package/dist/core/repo.js +118 -0
  63. package/dist/core/schema.d.ts +50 -0
  64. package/dist/core/schema.js +51 -0
  65. package/dist/core/search.d.ts +32 -0
  66. package/dist/core/search.js +132 -0
  67. package/dist/core/serve/auth.d.ts +23 -0
  68. package/dist/core/serve/auth.js +38 -0
  69. package/dist/core/serve/server.d.ts +21 -0
  70. package/dist/core/serve/server.js +138 -0
  71. package/dist/core/serve/sessions.d.ts +40 -0
  72. package/dist/core/serve/sessions.js +203 -0
  73. package/dist/core/serve/token.d.ts +10 -0
  74. package/dist/core/serve/token.js +96 -0
  75. package/dist/core/service.d.ts +473 -0
  76. package/dist/core/service.js +973 -0
  77. package/dist/core/sla.d.ts +16 -0
  78. package/dist/core/sla.js +20 -0
  79. package/dist/core/stale.d.ts +16 -0
  80. package/dist/core/stale.js +24 -0
  81. package/dist/core/tool-registry.d.ts +31 -0
  82. package/dist/core/tool-registry.js +80 -0
  83. package/dist/core/url.d.ts +4 -0
  84. package/dist/core/url.js +31 -0
  85. package/dist/core/verify-pages.d.ts +19 -0
  86. package/dist/core/verify-pages.js +110 -0
  87. package/dist/core/version.d.ts +22 -0
  88. package/dist/core/version.js +32 -0
  89. package/dist/core/writes.d.ts +58 -0
  90. package/dist/core/writes.js +113 -0
  91. package/dist/web/assets/index-8J-FUbb0.js +29 -0
  92. package/dist/web/assets/index-BGpAZzeM.css +1 -0
  93. package/dist/web/index.html +14 -0
  94. package/docs/format-spec.md +349 -0
  95. package/package.json +75 -4
  96. package/plugin/.claude-plugin/plugin.json +8 -0
  97. package/plugin/.mcp.json +8 -0
  98. package/plugin/bin/docsworth-mcp.mjs +46 -0
  99. package/plugin/skills/draft-runbook/SKILL.md +75 -0
  100. package/plugin/skills/polish-page/SKILL.md +64 -0
  101. package/plugin/skills/update-after-incident/SKILL.md +59 -0
  102. package/plugin/skills/verify-page/SKILL.md +46 -0
  103. package/templates/github/knowledge.yml +51 -0
@@ -0,0 +1 @@
1
+ :root,:host{--dw-bg:#fbfbfa;--dw-surface:#fff;--dw-fg:#1d1d1b;--dw-muted:#6b6b66;--dw-line:#e4e3de;--dw-track:#ecebe6;--dw-accent:#2b5fa8;--dw-good:#2f7d4f;--dw-warn:#b7791f;--dw-bad:#b4442c;--dw-code:#f2f1ed;--dw-radius:8px;--dw-font:15px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif;--dw-mono:13px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace;--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light}@media (prefers-color-scheme:dark){:root,:host{--dw-bg:#161615;--dw-surface:#1d1d1c;--dw-fg:#ecebe6;--dw-muted:#9a9993;--dw-line:#2c2c2a;--dw-track:#2a2a28;--dw-accent:#7aa7e8;--dw-good:#58b07c;--dw-warn:#d9a441;--dw-bad:#e0735a;--dw-code:#242422;--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark}}*{box-sizing:border-box}.dw{background:var(--dw-bg);color:var(--dw-fg);font:var(--dw-font)}.dw a{color:var(--dw-accent);text-decoration:none}.dw a:hover{text-decoration:underline}.dw code{font:var(--dw-mono);background:var(--dw-code);border-radius:4px;padding:0 4px}.dw h1{margin:0 0 4px;font-size:24px;line-height:1.25}.dw h2{margin:28px 0 10px;font-size:17px}.dw-muted{color:var(--dw-muted)}.dw-num{font-variant-numeric:tabular-nums}.dw-header{border-bottom:1px solid var(--dw-line);background:var(--dw-surface);flex-wrap:wrap;align-items:center;gap:20px;padding:12px 20px;display:flex}.dw-brand{letter-spacing:-.01em;font-weight:650;color:var(--dw-fg)!important}.dw-nav{flex-wrap:wrap;gap:4px;display:flex}.dw-nav a{color:var(--dw-muted);border-radius:6px;padding:6px 10px}.dw-nav a[aria-current=page]{color:var(--dw-fg);background:var(--dw-track)}.dw-who{color:var(--dw-muted);align-items:center;gap:8px;margin-left:auto;font-size:13px;display:flex}.dw-main{max-width:1040px;margin:0 auto;padding:28px 20px 64px}.dw-sub{color:var(--dw-muted);margin:0 0 20px}.dw-save{flex-wrap:wrap;gap:8px;margin:0 0 20px;display:flex}.dw-save .dw-note{flex-basis:100%;margin:4px 0 0}.dw-tiles{grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:12px;display:grid}.dw-tile{border:1px solid var(--dw-line);border-radius:var(--dw-radius);background:var(--dw-surface);padding:14px 16px}.dw-tile b{font-variant-numeric:tabular-nums;font-size:28px;display:block}.dw-tile span{color:var(--dw-muted);font-size:13px}.dw-table{border-collapse:collapse;width:100%}.dw-table th,.dw-table td{text-align:left;border-bottom:1px solid var(--dw-line);vertical-align:middle;padding:8px 6px}.dw-table thead th{text-transform:uppercase;letter-spacing:.04em;color:var(--dw-muted);font-size:12px;font-weight:600}.dw-scroll{overflow-x:auto}.dw-bar{background:var(--dw-track);border-radius:4px;max-width:240px;height:8px;margin-bottom:4px;overflow:hidden}.dw-bar>span{height:100%;display:block}.dw-good{background:var(--dw-good)}.dw-warn{background:var(--dw-warn)}.dw-bad{background:var(--dw-bad)}.dw-badge{border:1px solid var(--dw-line);color:var(--dw-muted);white-space:nowrap;border-radius:999px;padding:0 7px;font-size:12px;line-height:18px;display:inline-block}.dw-badge.ok{color:var(--dw-good);border-color:currentColor}.dw-badge.warn{color:var(--dw-warn);border-color:currentColor}.dw-badge.bad{color:var(--dw-bad);border-color:currentColor}.dw-meta{color:var(--dw-muted);flex-wrap:wrap;align-items:center;gap:8px;font-size:13px;display:flex}.dw-search{gap:8px;margin-bottom:20px;display:flex}.dw-search input{flex:1}.dw-results{gap:10px;margin:0;padding:0;list-style:none;display:grid}.dw-result{border:1px solid var(--dw-line);border-radius:var(--dw-radius);background:var(--dw-surface);padding:12px 14px}.dw-result h3{margin:0 0 4px;font-size:16px}.dw-result p{color:var(--dw-muted);margin:6px 0 0;font-size:14px}.dw-excluded{border-left:3px solid var(--dw-warn);background:var(--dw-surface);margin:6px 0;padding:8px 12px}.dw-page{max-width:760px}.dw-prose{line-height:1.65}.dw-prose pre{font:var(--dw-mono);background:var(--dw-code);border-radius:var(--dw-radius);padding:12px 14px;overflow-x:auto}.dw-prose pre code{background:0 0;padding:0}.dw-prose .dw-table-scroll{margin:12px 0;overflow-x:auto}.dw-prose table{border-collapse:collapse;font-size:14px;line-height:1.45}.dw-prose th,.dw-prose td{border:1px solid var(--dw-line);text-align:left;vertical-align:top;padding:6px 10px}.dw-prose th{background:var(--dw-surface);font-weight:600}.dw-prose .dw-align-center{text-align:center}.dw-prose .dw-align-right{text-align:right}.dw-prose h1{margin-top:24px;font-size:22px}.dw-prose h2{margin-top:24px}.dw-note{border:1px solid var(--dw-line);border-left:3px solid var(--dw-warn);background:var(--dw-surface);border-radius:6px;margin:12px 0;padding:10px 12px;font-size:14px}.dw-error{border-left-color:var(--dw-bad)}.dw input,.dw select,.dw textarea,.dw button{font:inherit;color:inherit}.dw input,.dw select,.dw textarea{background:var(--dw-surface);border:1px solid var(--dw-line);border-radius:6px;width:100%;padding:8px 10px}.dw textarea{font:var(--dw-mono);resize:vertical;min-height:220px}.dw button{background:var(--dw-fg);color:var(--dw-bg);cursor:pointer;border:0;border-radius:6px;padding:8px 14px;font-weight:550}.dw button.secondary{color:var(--dw-fg);border:1px solid var(--dw-line);background:0 0}.dw button:disabled{opacity:.45;cursor:not-allowed}.dw-form{gap:14px;max-width:760px;display:grid}.dw-form label{gap:6px;font-size:14px;font-weight:550;display:grid}.dw-form .dw-row{grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:12px;display:grid}.dw-preview{font:var(--dw-mono);white-space:pre-wrap;background:var(--dw-code);border-radius:var(--dw-radius);max-height:260px;padding:12px 14px;overflow:auto}.dw-graph{border:1px solid var(--dw-line);border-radius:var(--dw-radius);background:var(--dw-surface);width:100%;height:auto;display:block}.dw-graph line{stroke:var(--dw-line);stroke-width:1.2px}.dw-graph circle{stroke:var(--dw-surface);stroke-width:1.5px;cursor:pointer}.dw-graph circle.dim{opacity:.35}.dw-graph text{fill:var(--dw-muted);pointer-events:none;font-size:11px}.dw-legend{color:var(--dw-muted);flex-wrap:wrap;gap:14px;margin:10px 0;font-size:13px;display:flex}.dw-teams{flex-wrap:wrap;gap:12px;margin:-8px 0 16px;font-size:14px;display:flex}.dw-swatch{vertical-align:-1px;border-radius:50%;width:10px;height:10px;margin-right:6px;display:inline-block}
@@ -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-8J-FUbb0.js"></script>
9
+ <link rel="stylesheet" crossorigin href="/assets/index-BGpAZzeM.css">
10
+ </head>
11
+ <body class="dw">
12
+ <div id="app"></div>
13
+ </body>
14
+ </html>
@@ -0,0 +1,349 @@
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
+ Inside the knowledge root, a directory whose name starts with a dot (an editor's
26
+ `.obsidian/`, say) is skipped, with everything in it: its files are neither pages nor lint
27
+ errors. The knowledge root itself is read whatever its name, and a file whose name starts
28
+ with a dot is a page like any other.
29
+
30
+ ## Frontmatter
31
+
32
+ At most 16 KB of YAML between the `---` lines; a page over that is reported by lint and
33
+ not served (frontmatter runs to a few hundred bytes, and YAML parsing slows past linear
34
+ on hostile input).
35
+
36
+ ```yaml
37
+ ---
38
+ title: Deploying to production
39
+ id: rb-deploy-prod # required, stable, unique; lowercase letters, digits, dashes
40
+ type: runbook # required: runbook | adr | service | guide | policy
41
+ status: canonical # required: canonical | draft | deprecated
42
+ owner_team: platform # required: team slug; CODEOWNERS must assign @<org>/platform
43
+ last_verified: 2026-10-15 # required: YYYY-MM-DD, a real calendar date
44
+ review_every: 90d # required: <n>d | <n>w | <n>m | <n>y
45
+ supersedes: [rb-deploy-legacy] # optional: ids this page replaces
46
+ applies_to: [service:payments] # optional: free-form tags, matched exactly by search filters
47
+ acl: internal # optional, default internal; or a team slug
48
+ description: One sentence. # optional; used in search and compiled skills
49
+ ---
50
+ ```
51
+
52
+ Unknown fields produce a lint warning, never an error, unless the config lists them in
53
+ `frontmatter.extra_fields`: keys a site generator keeps there, such as Docusaurus's
54
+ `sidebar_position`.
55
+
56
+ ## Links
57
+
58
+ Lint reads a page's links as CommonMark reads them. A `[x](y)` in code (a fenced or
59
+ indented block, or a code span) or in HTML is no link. A reference link counts through its
60
+ definition, and a definition no link uses is invisible.
61
+
62
+ A relative link must resolve inside the repo. Under the knowledge root it must name a page
63
+ (`.md`) or an attachment (a file with an extension, such as `diagram.png`). A link to a
64
+ directory, or to a file with no extension, is an error: it names no page, and a route like
65
+ Docusaurus's `/guides/setup` reads as one. Link the page itself (`guides/setup.md`).
66
+
67
+ A root-relative link to an attachment (`/img/x.png`) that names no file of the repo's
68
+ resolves in the directories `static_dirs` lists, as a docs site serves them (Docusaurus's
69
+ `static/`). A page never does: pages are under the knowledge root.
70
+
71
+ A root-relative link (`/docs`) is an error too when it names a directory or a file with no
72
+ extension outside the knowledge root, and, read from the knowledge root as a docs site
73
+ serves it (with or without a leading `docs/`, a number prefix, or a page's own `slug` or `id`),
74
+ also names a page. The repo's file and the page both answer to it. Link the one you mean,
75
+ with its extension.
76
+
77
+ ## Ownership
78
+
79
+ - CODEOWNERS uses GitHub's semantics: the last matching rule wins. It supports anchored,
80
+ `*`, `**` and trailing-slash patterns, and `docs/*` matches direct children only.
81
+ - The winning rule for a page's path must name **exactly one owner**, and that owner
82
+ must be a **team of the configured org** (`@<org>/<team>`). Individual owners (`@alice`,
83
+ emails), several owners, foreign-org teams and owner-less rules are all lint errors.
84
+ Individual ownership is banned deliberately: people change teams, and teams don't.
85
+ - `owner_team` must equal that team's slug.
86
+
87
+ ## Review SLA
88
+
89
+ Dates are date-only and compared as UTC calendar dates. The due date is
90
+ `last_verified + review_every`:
91
+
92
+ - `d` and `w` add fixed days.
93
+ - `m` and `y` add calendar months or years, clamped to the end of the month
94
+ (2026-01-31 + 1m = 2026-02-28).
95
+
96
+ States are evaluated in this order:
97
+
98
+ | State | Condition |
99
+ | --- | --- |
100
+ | `unverified` | `last_verified` or `review_every` missing or invalid (this is also a lint error) |
101
+ | `overdue` | today is after the due date (a lint warning on canonical pages) |
102
+ | `due_soon` | at most `sla.due_soon_days` (default 14) days until due |
103
+ | `fresh` | otherwise |
104
+
105
+ The due date itself is still within SLA.
106
+
107
+ ## Citation gate
108
+
109
+ A page is **citable** when all three hold:
110
+
111
+ 1. `status: canonical`.
112
+ 2. Its SLA state is `fresh` or `due_soon`.
113
+ 3. Its ownership resolves as above.
114
+
115
+ The gate is fixed by this spec; configuration cannot widen it.
116
+
117
+ - **`search`:** returns only citable pages. Matching pages that aren't citable are
118
+ listed separately as `excluded`, each with a reason such as "past review date (due
119
+ 2026-10-30), owned by platform". Agents can then say what was excluded instead of
120
+ silently missing it.
121
+ - **`read`:** returns any visible page, with `citable`, `flags` and `gate_reason`.
122
+
123
+ ## ACL
124
+
125
+ `acl` narrows which callers the **MCP server serves** a page to:
126
+
127
+ - **`internal`:** anyone the server serves at all.
128
+ - **A team slug:** only members of `@<org>/<slug>`.
129
+
130
+ In a page the caller can see, a reference to a page they can't see (a link in any form,
131
+ a URL, or its path in text) reads `[restricted page]`. A page that doesn't load counts as
132
+ one they can't see, and so does a link to a path under the knowledge root that is no page
133
+ at all: relative, or, with `github.repo` set, a GitHub URL into the repo on any branch.
134
+
135
+ Links are also read as a docs site serves them. A link to a hidden page's route reads
136
+ `[restricted page]` too: by its `slug`, by an `id` other than its file name, by its path
137
+ without number prefixes, or by its file's stem. So does a link without an extension that
138
+ names no page, file or directory in the repo (`/guides/gone`), for every reader. With
139
+ `site_url` set, a URL on the docs site counts as the route it names. A route written in
140
+ plain text, not as a link, is served as written.
141
+ See [security-model.md](security-model.md#the-one-read-path).
142
+
143
+ **`acl` isn't confidentiality.** GitHub has no per-file read permissions, so anyone with
144
+ read access to the knowledge repo can read every file in it. Keep confidential content
145
+ in a separate repository whose GitHub permissions match its audience. See
146
+ [security-model.md](security-model.md).
147
+
148
+ ## Runbook sections
149
+
150
+ For the skills compiler, a runbook has these `##` sections. The heading match is
151
+ case-insensitive:
152
+
153
+ | Section | Holds |
154
+ | --- | --- |
155
+ | `## When to use` | Triggers: alerts, symptoms, requests. This becomes the skill's description. |
156
+ | `## Steps` | Numbered actions with exact commands. |
157
+ | `## Checks` | How to tell it worked. |
158
+ | `## Rollback` | How to undo it, or why it can't be undone. |
159
+
160
+ A canonical runbook missing any of these still lints. It's reported and skipped by
161
+ `docsworth compile`.
162
+
163
+ ## Compiled skills
164
+
165
+ `docsworth compile` writes `skills/<id>/SKILL.md` in the Agent Skills format: `name` and
166
+ `description` frontmatter, plus string-valued `metadata`:
167
+
168
+ ```yaml
169
+ metadata:
170
+ source_id: rb-deploy-prod
171
+ source_path: knowledge/runbooks/deploy-production.md
172
+ source_sha256: <sha256 of the source file>
173
+ owner_team: platform
174
+ last_verified: 2026-10-15
175
+ review_every: 90d
176
+ review_due: 2027-01-13
177
+ compiled_at: 2026-11-02
178
+ stale: "false"
179
+ ```
180
+
181
+ A skill is **stale** when its source page changed (`source_sha256`), was re-verified
182
+ (`last_verified`), stopped being canonical, or was deleted. `docsworth compile --check` exits 1
183
+ on stale skills. `--mark-stale` sets `stale: "true"` and prefixes the description with
184
+ `[STALE: …]`. Runbooks with a non-`internal` ACL are compiled only with
185
+ `--include-restricted`.
186
+
187
+ Compiling removes only skills whose `generator` is `docsworth`. A skill whose generator
188
+ is `agent-kb`, the pre-release name, is rewritten if its page still compiles. If it
189
+ remains, `--check` reports it, but it's never removed or marked, because that name now
190
+ belongs to another publisher: delete it by hand.
191
+
192
+ ## Polish
193
+
194
+ `propose_edit` with `polish: true` proposes a restructure or rewording that must not
195
+ change what the page says. The agent sends the new `body` (what `read` returns as
196
+ `content`, rewritten), and the server keeps the frontmatter. Before any branch is
197
+ created, the server compares the current page with the proposed one and refuses
198
+ (`invalid`, naming the changes it saw, up to eight) unless all of these are unchanged.
199
+ Pages over 64 KB, before or after, aren't polished.
200
+
201
+ | Compared | How |
202
+ | --- | --- |
203
+ | Frontmatter | byte for byte |
204
+ | 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 |
205
+ | 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 |
206
+ | 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 |
207
+ | 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 |
208
+ | 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 |
209
+ | 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 |
210
+ | 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 |
211
+ | Struck-through text (`~~…~~`, `<del>`, `<s>`) | as a multiset, so striking an instruction out is a change |
212
+ | 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 |
213
+ | Words with digits (`TLSv1.2`, `IPv4`, `RHEL8`, `p99`) | as a multiset |
214
+ | 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 |
215
+ | 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`) |
216
+
217
+ Because the frontmatter and every link target are unchanged, a polished page stays as
218
+ valid as it was.
219
+
220
+ On a page that links to pages the caller can't see (shown as `[restricted page]` in
221
+ `read`), `propose_edit` takes only `edits`, and no polish. A whole `body` or `content`
222
+ written from the redacted view would overwrite or drop those links, and a polish
223
+ refusal would name them. An edit that writes `[restricted page]` is refused too. An
224
+ `append` is taken there: it adds to the end of the page as it is on the default branch,
225
+ so nothing written from the redacted view goes back into it.
226
+
227
+ ## New pages (`draft_page`)
228
+
229
+ `draft_page` opens a draft PR that adds one page, on a `docsworth/draft/<id>-…` branch.
230
+
231
+ - **The server names it.** Its id is `<prefix>-<slug>-<12 hex>` and its file
232
+ `<slug>-<12 hex>.md`, where the slug comes from the title and the hex is random. A name
233
+ the caller chose could be aimed at a page they can't see, and "already exists" would
234
+ then say it is there. Owners can rename it in the PR before merging.
235
+ - **Where it lands.** The type's directory (`runbooks/`, `services/<name>/index.md`,
236
+ `guides/`, `policies/`) or a `dir` under the knowledge root. CODEOWNERS must assign
237
+ `owner_team` there, so the page lints. Where the plain name is another team's, the
238
+ server tries `<owner_team>-<slug>-<12 hex>` (id `<prefix>-<owner_team>-<slug>-<12 hex>`)
239
+ in the same directory, so rules on file names (`runbooks/payments-*`) route the page to
240
+ its team; if that isn't the team's either, `draft_page` refuses and names the directory.
241
+ ADRs are written by hand.
242
+ - **What it holds.** `status: draft` (never citable until its owners make it canonical),
243
+ `last_verified` today, `review_every: 90d`, the caller's one-line title and optional
244
+ description, and `acl` `internal` or a team the caller is on. A `source_url` becomes a
245
+ first line, `> Captured from <url> on <date>.`
246
+
247
+ **How it's checked.** Besides its own tests, the guard is fuzzed against CommonMark's
248
+ reference parser (commonmark.js): pages are mutated at random, and no pair may change a
249
+ code block, inline code, a link (its text, target or title, and GitHub's bare-URL
250
+ autolinks), a digit or a negation word as commonmark.js renders it while the guard
251
+ reports nothing (`test/polish-fuzz.test.ts`, and `scripts/fuzz-polish.ts` for long
252
+ runs). Where CommonMark implementations disagree (a tab inside a link's or a
253
+ definition's parentheses), the page is refused rather than read one way.
254
+
255
+ **Not detected.** This is a structural check, not proof that the meaning is unchanged.
256
+ It doesn't see a renamed service or team in prose, a command written as plain prose,
257
+ a changed word that isn't a number's unit, a
258
+ number spelled out (`five`), two numbers with the same unit trading places (`wait 5
259
+ minutes`, `retry after 10 minutes`), steps without code put in another order, code
260
+ moved across headings that were reworded in the same polish, a negation moved to
261
+ 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
262
+ `polish(<id>): <title>`, with a body that lists what the guard compared and asks for
263
+ the diff to be read.
264
+
265
+ ## Mirror namespace
266
+
267
+ Mirrored pages live under `mirror.out_dir` (default `.knowledge/mirror/`), never under
268
+ the knowledge root, and are never citable. Each file carries a `mirror:` frontmatter block
269
+ (source, native id, url, `last_edited`, versions, `owner_team` from a `team-<slug>` or
270
+ `owner-<slug>` label, labels, acl). Each source and space directory also holds
271
+ `_scores.json` and `_report.json`. Pages with read restrictions in the source, and their
272
+ descendants, are never written, and the report lists them by native id only.
273
+
274
+ **Serving the mirror** (`mirror.serve: true`, off by default, and not yet recommended:
275
+ on the benchmark it was neutral on answer questions but just missed its bar on gate
276
+ questions). The MCP server then lists
277
+ matching mirrored pages in search under `mirrored`, at most 3, apart from `results`, each
278
+ with `citable: false`, its source URL and last edit. `read` takes a `mirror:` id and
279
+ returns the page with `citable: false` and `flags: ["mirrored"]`. Filters on page type
280
+ or `applies_to` leave mirrored pages out; `owner_team` applies to them.
281
+
282
+ - **Audience:** each space's `acl`, by the same rule as pages. A hidden and an unknown
283
+ mirror id answer alike, and a team space's page taints the session like a team page.
284
+ - **Links:** in a mirrored page someone can see, links to a repo page they can't see
285
+ read `[restricted page]` as they do in repo pages (by URL, say). In a page someone can
286
+ see, mirrored or from the repo, links to a mirrored
287
+ page they can't see read `[restricted page]` (and don't count toward ranking): markdown links (any title form, nested or escaped brackets),
288
+ bare URLs, and Confluence's linked-title form. A link is matched by the page it
289
+ names, so a relative path, a URL without its title slug, `?pageId=`, a Confluence
290
+ tiny link (`/wiki/x/…`), or a Notion id in any form is caught too, also encoded,
291
+ broken across lines or with markup inside, as for repo pages.
292
+ - **Ranking:** each audience gets its own index, so ranking, snippets and counts never
293
+ draw on a page outside that audience.
294
+ - **What is served:** only files one of this user's mirror runs wrote and that are
295
+ unchanged since, read through the records in the state directory. A new run is picked
296
+ up without a restart. An `out_dir` that resolves outside the repo serves nothing, with
297
+ a warning.
298
+
299
+ ## Config (`.knowledge/config.yaml`)
300
+
301
+ ```yaml
302
+ format: 1
303
+ knowledge_root: knowledge # a directory in the repo, named relative to it: not . or .., not absolute, not in .git or .knowledge; a trailing slash is ignored
304
+ static_dirs: [website/static] # optional: directories a docs site serves at its root; lint reads /img/x.png there too
305
+ site_url: https://docs.example.com/docs/ # optional: where a docs site serves the pages; redaction reads a URL under it as a route
306
+ github:
307
+ org: acme # required: team owners resolve to @acme/<team>
308
+ repo: acme/knowledge # optional: makes GitHub the ACL authority; enables PR tools
309
+ default_branch: main # optional: otherwise read from GitHub
310
+ api_url: https://ghe.example.com/api/v3 # optional: GitHub Enterprise Server
311
+ sla:
312
+ due_soon_days: 14
313
+ usage_log: true # page-id-only read log for the owner digest
314
+ frontmatter:
315
+ extra_fields: [sidebar_label, sidebar_position] # keys a site generator keeps in frontmatter; lint doesn't warn on them
316
+ audit_log: # attributed audit log; off unless enabled
317
+ enabled: true
318
+ dir: /var/lib/docsworth/audit # optional; default $XDG_STATE_HOME/docsworth/<repo>-<hash>/audit
319
+ include_queries: false # search text is not recorded unless true
320
+ required: false # true: nothing is answered or changed unless its event is written first (see security-model.md)
321
+ retention_days: 365
322
+ policy: # org policy; every default keeps v0.1 behaviour
323
+ admins: kb-admins # lint: CODEOWNERS must make this team own the config and CODEOWNERS
324
+ tools: # per-tool allowlists (search, read, list, stale, propose_edit, verify, draft_page); an empty list disables a tool for everyone
325
+ propose_edit: { teams: [platform, payments] }
326
+ writes: enabled # disabled: propose_edit, verify and draft_page are not offered
327
+ require_human_approval: false # true: Claude Code asks on every write call
328
+ include_stale: allowed # denied: search never returns gated pages inline
329
+ identity_cache_seconds: 300 # team-membership cache for reads (30-3600)
330
+ audit_http: # pro: ship each event to a collector (SIEM) as it happens
331
+ url_env: DOCSWORTH_AUDIT_URL # env var with the https:// URL (http only on loopback)
332
+ token_env: DOCSWORTH_AUDIT_TOKEN
333
+ required: false
334
+ skills:
335
+ out_dir: skills
336
+ mirror:
337
+ out_dir: .knowledge/mirror
338
+ serve: false # true: search lists matching mirrored pages under `mirrored` (never citable)
339
+ sources:
340
+ - kind: confluence
341
+ base_url: https://acme.atlassian.net
342
+ spaces: [{ key: ENG, acl: internal }]
343
+ - kind: notion
344
+ root_page_ids: [] # empty: everything shared with the integration
345
+ acl: internal
346
+ digest:
347
+ slack_webhook_env: SLACK_WEBHOOK_URL
348
+ team_webhooks: { security: SLACK_WEBHOOK_SECURITY }
349
+ ```
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.3.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
+ "commonmark": "^0.31.2",
65
+ "yaml": "^2.9.1",
66
+ "zod": "^4.6.7"
67
+ },
68
+ "devDependencies": {
69
+ "@mixmark-io/domino": "2.2.0",
70
+ "@types/commonmark": "0.27.10",
71
+ "@types/node": "^22.20.5",
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.3.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.3";
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.