@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.
- package/LICENSE +55 -0
- package/README.md +238 -2
- package/dist/cli.d.ts +11 -0
- package/dist/cli.js +458 -0
- package/dist/core/acl.d.ts +37 -0
- package/dist/core/acl.js +136 -0
- package/dist/core/adopt.d.ts +68 -0
- package/dist/core/adopt.js +665 -0
- package/dist/core/audit-log.d.ts +133 -0
- package/dist/core/audit-log.js +290 -0
- package/dist/core/blocks.d.ts +35 -0
- package/dist/core/blocks.js +417 -0
- package/dist/core/codeowners.d.ts +62 -0
- package/dist/core/codeowners.js +133 -0
- package/dist/core/config.d.ts +101 -0
- package/dist/core/config.js +175 -0
- package/dist/core/coverage.d.ts +25 -0
- package/dist/core/coverage.js +41 -0
- package/dist/core/duration.d.ts +20 -0
- package/dist/core/duration.js +60 -0
- package/dist/core/edits.d.ts +13 -0
- package/dist/core/edits.js +20 -0
- package/dist/core/errors.d.ts +11 -0
- package/dist/core/errors.js +13 -0
- package/dist/core/frontmatter.d.ts +29 -0
- package/dist/core/frontmatter.js +49 -0
- package/dist/core/gate.d.ts +17 -0
- package/dist/core/gate.js +25 -0
- package/dist/core/gfm.d.ts +13 -0
- package/dist/core/gfm.js +244 -0
- package/dist/core/github.d.ts +152 -0
- package/dist/core/github.js +234 -0
- package/dist/core/host.d.ts +125 -0
- package/dist/core/host.js +22 -0
- package/dist/core/identity.d.ts +39 -0
- package/dist/core/identity.js +127 -0
- package/dist/core/init.d.ts +25 -0
- package/dist/core/init.js +86 -0
- package/dist/core/links.d.ts +82 -0
- package/dist/core/links.js +345 -0
- package/dist/core/lint.d.ts +16 -0
- package/dist/core/lint.js +149 -0
- package/dist/core/mcp/audit-transport.d.ts +16 -0
- package/dist/core/mcp/audit-transport.js +238 -0
- package/dist/core/mcp/server.d.ts +19 -0
- package/dist/core/mcp/server.js +216 -0
- package/dist/core/mcp/text.d.ts +14 -0
- package/dist/core/mcp/text.js +43 -0
- package/dist/core/mirror-view.d.ts +68 -0
- package/dist/core/mirror-view.js +367 -0
- package/dist/core/new-page.d.ts +52 -0
- package/dist/core/new-page.js +49 -0
- package/dist/core/policy.d.ts +10 -0
- package/dist/core/policy.js +30 -0
- package/dist/core/polish.d.ts +42 -0
- package/dist/core/polish.js +1000 -0
- package/dist/core/printable.d.ts +4 -0
- package/dist/core/printable.js +16 -0
- package/dist/core/references.d.ts +87 -0
- package/dist/core/references.js +1744 -0
- package/dist/core/repo.d.ts +46 -0
- package/dist/core/repo.js +118 -0
- package/dist/core/schema.d.ts +50 -0
- package/dist/core/schema.js +51 -0
- package/dist/core/search.d.ts +32 -0
- package/dist/core/search.js +132 -0
- package/dist/core/serve/auth.d.ts +23 -0
- package/dist/core/serve/auth.js +38 -0
- package/dist/core/serve/server.d.ts +21 -0
- package/dist/core/serve/server.js +138 -0
- package/dist/core/serve/sessions.d.ts +40 -0
- package/dist/core/serve/sessions.js +203 -0
- package/dist/core/serve/token.d.ts +10 -0
- package/dist/core/serve/token.js +96 -0
- package/dist/core/service.d.ts +473 -0
- package/dist/core/service.js +973 -0
- package/dist/core/sla.d.ts +16 -0
- package/dist/core/sla.js +20 -0
- package/dist/core/stale.d.ts +16 -0
- package/dist/core/stale.js +24 -0
- package/dist/core/tool-registry.d.ts +31 -0
- package/dist/core/tool-registry.js +80 -0
- package/dist/core/url.d.ts +4 -0
- package/dist/core/url.js +31 -0
- package/dist/core/verify-pages.d.ts +19 -0
- package/dist/core/verify-pages.js +110 -0
- package/dist/core/version.d.ts +22 -0
- package/dist/core/version.js +32 -0
- package/dist/core/writes.d.ts +58 -0
- package/dist/core/writes.js +113 -0
- package/dist/web/assets/index-8J-FUbb0.js +29 -0
- package/dist/web/assets/index-BGpAZzeM.css +1 -0
- package/dist/web/index.html +14 -0
- package/docs/format-spec.md +349 -0
- package/package.json +75 -4
- package/plugin/.claude-plugin/plugin.json +8 -0
- package/plugin/.mcp.json +8 -0
- package/plugin/bin/docsworth-mcp.mjs +46 -0
- package/plugin/skills/draft-runbook/SKILL.md +75 -0
- package/plugin/skills/polish-page/SKILL.md +64 -0
- package/plugin/skills/update-after-incident/SKILL.md +59 -0
- package/plugin/skills/verify-page/SKILL.md +46 -0
- 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 `& < > " ' − +` 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.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
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
|
+
}
|
package/plugin/.mcp.json
ADDED
|
@@ -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.
|