@markstanden/opencode-docs-plugin 0.5.0 → 0.6.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/CONVENTION.md +25 -9
- package/README.md +7 -2
- package/package.json +1 -1
- package/src/SKILL.md +48 -91
- package/src/index.ts +150 -70
package/CONVENTION.md
CHANGED
|
@@ -53,11 +53,11 @@ are skipped by name.
|
|
|
53
53
|
Every knowledge doc opens with a `---` front-matter block. The fields, in
|
|
54
54
|
canonical order:
|
|
55
55
|
|
|
56
|
-
| Field | Required | Meaning
|
|
57
|
-
| ---------- | -------- |
|
|
58
|
-
| `Area` | yes | One top-level path segment; the doc's group.
|
|
59
|
-
| `Keywords` | yes | Comma-separated search terms; the second half of the doc's identity.
|
|
60
|
-
| `Summary` | yes | One self-contained sentence
|
|
56
|
+
| Field | Required | Meaning |
|
|
57
|
+
| ---------- | -------- | ----------------------------------------------------------------------------------------------------------- |
|
|
58
|
+
| `Area` | yes | One top-level path segment; the doc's group. |
|
|
59
|
+
| `Keywords` | yes | Comma-separated search terms; the second half of the doc's identity. |
|
|
60
|
+
| `Summary` | yes | One self-contained sentence that lets a reader decide whether to open the file — not a second introduction. |
|
|
61
61
|
|
|
62
62
|
`Date` is **derived, never written**: the file's last git commit date, falling
|
|
63
63
|
back to mtime. A hand-maintained `Date` is a finding, not a field.
|
|
@@ -79,6 +79,12 @@ git. Optional invisible metadata may be added later; it is not required.
|
|
|
79
79
|
- At least one claim section as an `H2`/`H3` heading — `Facts`, `Inferred`,
|
|
80
80
|
`Tested`, `Failed approaches`, `Open questions` — so a reader knows how much
|
|
81
81
|
to rely on each claim. Group claims by section rather than mixing them.
|
|
82
|
+
- Include only the sections the doc actually needs. An empty heading is noise;
|
|
83
|
+
leave it out.
|
|
84
|
+
- Write so the next reader decides fast: lead with the actionable fact, include
|
|
85
|
+
its reason, limits and evidence where they affect its use, and omit discovery
|
|
86
|
+
chronology, repeated conclusions and empty sections. Under `Tested`, record
|
|
87
|
+
the check, its result and the conditions that mattered.
|
|
82
88
|
- A failed attempt is a valid current doc or section: state what was tried, why
|
|
83
89
|
it failed, what is true now, and what not to retry. Do not keep a doc whose
|
|
84
90
|
claims are no longer true.
|
|
@@ -92,6 +98,11 @@ git. Optional invisible metadata may be added later; it is not required.
|
|
|
92
98
|
`add` refuses a near-twin write and returns the candidates to reconcile
|
|
93
99
|
against — edit before add, merge near-twins, split a bloated doc into
|
|
94
100
|
per-topic files.
|
|
101
|
+
- The reconcile applies to a **create**: with `path` omitted, the target is
|
|
102
|
+
derived and the near-twin guard runs. An explicit `path` is the update
|
|
103
|
+
contract and skips it, which is how you replace a doc in place. Passing an
|
|
104
|
+
explicit _new_ path is therefore a deliberate override, not a protected
|
|
105
|
+
write.
|
|
95
106
|
- Near-twins are per lake. The same topic at repo and global scope is
|
|
96
107
|
legitimate; the narrower doc states its delta and links up.
|
|
97
108
|
|
|
@@ -129,7 +140,12 @@ knowledge when it records the outcome and the current truth.
|
|
|
129
140
|
|
|
130
141
|
## Adopting it
|
|
131
142
|
|
|
132
|
-
Run `check` to audit an existing tree, and the
|
|
133
|
-
proposing a commit.
|
|
134
|
-
|
|
135
|
-
|
|
143
|
+
Run `check` to audit an existing tree, and the project's own tests before
|
|
144
|
+
proposing a commit. `add` enforces the per-doc structural rules at the write
|
|
145
|
+
boundary — front-matter, `H1`, claim section, identity term floor, and the
|
|
146
|
+
near-twin guard on a create — while the cross-document audits (near-twin pairs
|
|
147
|
+
and relative links) stay with `check`. A missing lake root is a note, not an
|
|
148
|
+
error: the plugin scans what is there, search and help flag the absent path, and
|
|
149
|
+
`add` refuses rather than creating a tree at a mistyped root. Create the
|
|
150
|
+
directory deliberately. Wiki lakes are read-only and are reported as not
|
|
151
|
+
validated, never silently skipped.
|
package/README.md
CHANGED
|
@@ -80,7 +80,7 @@ The simplest setup uses `root` — one repo lake:
|
|
|
80
80
|
{
|
|
81
81
|
"package": "@markstanden/opencode-docs-plugin",
|
|
82
82
|
"options": {
|
|
83
|
-
"root": "
|
|
83
|
+
"root": "./docs",
|
|
84
84
|
},
|
|
85
85
|
},
|
|
86
86
|
],
|
|
@@ -159,6 +159,12 @@ Lake paths never follow symlinks: a symlinked doc is skipped by search and
|
|
|
159
159
|
`check`, refused by `read`, and refused by `add`. The configured `root` itself
|
|
160
160
|
may be a symlink; the content beneath it must be real.
|
|
161
161
|
|
|
162
|
+
A missing lake root is flagged, never created. The lake stays dormant until the
|
|
163
|
+
directory exists: `search` appends a note naming the lake and path, `help` marks
|
|
164
|
+
it `— missing`, and `add` refuses with the path rather than bootstrapping a
|
|
165
|
+
tree. Create the directory deliberately. Area folders beneath an existing root
|
|
166
|
+
are still created on write.
|
|
167
|
+
|
|
162
168
|
`maxDocLines` (default 200, `0` disables) is a **soft** note on a long doc, not a
|
|
163
169
|
refusal. `perLakeQuota` (default 3, `0` disables) is the per-source result
|
|
164
170
|
reservation.
|
|
@@ -195,7 +201,6 @@ Standalone extract of the docs tooling, under active development.
|
|
|
195
201
|
npm install
|
|
196
202
|
npm test # node --test
|
|
197
203
|
npm run typecheck
|
|
198
|
-
defined comply # the gate: bootstrap, repair and verify
|
|
199
204
|
```
|
|
200
205
|
|
|
201
206
|
Tests run the plugin source directly under Node's type-stripping (`noEmit`,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@markstanden/opencode-docs-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "OpenCode V2 plugin: a `docs` tool and `/docs` command over configurable knowledge lakes (repo, machine, global and read-only wiki), nested README.md search, and the writing convention they share",
|
|
6
6
|
"repository": {
|
package/src/SKILL.md
CHANGED
|
@@ -1,13 +1,8 @@
|
|
|
1
1
|
# Docs
|
|
2
2
|
|
|
3
|
-
Durable knowledge lives in **lakes
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
doc at the narrowest true scope).
|
|
7
|
-
|
|
8
|
-
Call the `docs` tool at top level. Inside `execute` (Code Mode) the same tool
|
|
9
|
-
is reachable as the twin `tools.docs.docs_search(…)` — same verbs, same lakes —
|
|
10
|
-
so batch it with the rest of the catalog there.
|
|
3
|
+
Durable knowledge lives in **lakes**: **find** it (search, then read), or
|
|
4
|
+
**record** one focused doc at the narrowest true scope. Call the `docs` tool;
|
|
5
|
+
inside `execute` it is `tools.docs.docs_search(…)`, same verbs.
|
|
11
6
|
|
|
12
7
|
## Classes
|
|
13
8
|
|
|
@@ -20,98 +15,60 @@ so batch it with the rest of the catalog there.
|
|
|
20
15
|
| **Wiki** | human-readable wiki | narrative for humans | read-only |
|
|
21
16
|
|
|
22
17
|
Record at the **narrowest scope where the whole claim is true**. Two scopes
|
|
23
|
-
means two
|
|
18
|
+
means two docs; one omnibus doc is out of place everywhere.
|
|
24
19
|
|
|
25
20
|
## Find it
|
|
26
21
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
- **a `search`** — docs ranked over title, path, keywords, body headings and
|
|
32
|
-
code spans, area, summary and wiki prose. When no doc contains every term it
|
|
33
|
-
returns the closest matches under a `Closest matches` header rather than a
|
|
34
|
-
dead end. A term must appear as a **substring** to match — a typo returns
|
|
35
|
-
nothing, by design. Terms shorter than `MIN_TERM_LENGTH` (3) are ignored
|
|
36
|
-
unless they are the whole query.
|
|
37
|
-
- **every call** reserves up to `perLakeQuota` results per source (default 3),
|
|
38
|
-
caps at `MAX_RESULTS` (10), and ends with `… and N more not shown.` when
|
|
39
|
-
anything was cut. The reservation keeps a broad wiki from burying a focused
|
|
40
|
-
repo doc.
|
|
41
|
-
- **exactly `help`** — the configured lakes, their classes and writability,
|
|
42
|
-
then the ranking rules.
|
|
43
|
-
|
|
44
|
-
Then open the match with the tool's `read` verb — pass the root-relative path,
|
|
45
|
-
or `lake:path` when several lakes are configured. A project README result is
|
|
46
|
-
prefixed `instructions:`; read it with the built-in `read` tool instead.
|
|
47
|
-
`/docs [search]` runs the same search into the session (`/docs search <text>` is
|
|
48
|
-
the explicit form).
|
|
49
|
-
|
|
50
|
-
Every `*.md` under a knowledge lake is covered except the excluded areas —
|
|
51
|
-
scratch (`plans/`) and vendored mirrors (`requirements/`, `wiki/`). `Area` comes
|
|
52
|
-
from the path, `date` from git. Wiki lakes are read-only; their pages are
|
|
53
|
-
indexed as prose and tables, and their summaries are derived from the first
|
|
54
|
-
paragraph. A README is search-only, rendered as `instructions:path`, and is
|
|
55
|
-
never validated.
|
|
56
|
-
|
|
57
|
-
## Read it
|
|
58
|
-
|
|
59
|
-
- Consult matching docs before a broad grep: they are the curated knowledge.
|
|
60
|
-
- Class precedence on a tie is repo, instructions, global, machine, wiki. A
|
|
61
|
-
non-primary lake is prefixed (`global:platform/regions.md`,
|
|
62
|
-
`wiki:Integrations/Billing.md`).
|
|
63
|
-
- A `Summary` is a self-contained sentence — enough to decide whether to open
|
|
64
|
-
the file.
|
|
22
|
+
A **blank `search`** lists recent docs, one line each; a **`search`** ranks
|
|
23
|
+
them, falling back to `Closest matches` and matching terms as **substrings** —
|
|
24
|
+
so a typo finds nothing and terms under `MIN_TERM_LENGTH` (3) are ignored.
|
|
25
|
+
`help` lists the lakes and ranking.
|
|
65
26
|
|
|
66
|
-
|
|
27
|
+
Open the match with `read`: the root-relative path, or `lake:path` when several
|
|
28
|
+
lakes exist. A README result is `instructions:path` and opens with the built-in
|
|
29
|
+
`read`. Consult matching docs before a broad grep; a tie ranks repo,
|
|
30
|
+
instructions, global, machine, wiki. Excluded areas: `plans/`, `requirements/`
|
|
31
|
+
and `wiki/`.
|
|
67
32
|
|
|
68
|
-
|
|
69
|
-
right lake so the next human or agent skips the rediscovery. The convention is
|
|
70
|
-
this plugin's `CONVENTION.md`, restated here.
|
|
33
|
+
## Record it
|
|
71
34
|
|
|
72
|
-
|
|
35
|
+
Write to `<area>/<title>.md` under the right lake; `CONVENTION.md` is
|
|
36
|
+
authoritative.
|
|
73
37
|
|
|
74
|
-
1. **Read first, never duplicate.**
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
from touched docs.
|
|
78
|
-
2. **Choose one class.** Repo-specific goes to RepoContext. Personal setup goes
|
|
79
|
-
to MachineContext. Cross-repo org knowledge goes to GlobalContext. How-to
|
|
80
|
-
commands stay in the README. Human narrative goes to the wiki, read-only.
|
|
38
|
+
1. **Read first, never duplicate.** Read the docs relevant to the task: the same
|
|
39
|
+
area, keyword hits on its entities, or links from touched docs.
|
|
40
|
+
2. **Choose one class** from the table — the narrowest that holds the claim.
|
|
81
41
|
3. **Reconcile, then add.** Edit before add; merge near-twins; split a bloated
|
|
82
|
-
doc
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
One artifact per topic
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
42
|
+
doc per topic; after a split, merge or move, repoint inbound references and
|
|
43
|
+
delete the emptied doc — no superseded stubs.
|
|
44
|
+
|
|
45
|
+
One artifact per topic and class, never one dump; the compaction nudge shapes
|
|
46
|
+
`Record with docs add:` blocks for this.
|
|
47
|
+
|
|
48
|
+
Record through the typed `add`: metadata (`Area`, `Keywords`, `Summary`) and a
|
|
49
|
+
body with an `H1` and a claim section — Facts, Inferred, Tested, Failed approaches, Open questions. Omit `path` to create `<Area>/<slug>.md`; pass `path` to replace **wholesale**, so omitted body content is lost and the near-twin reconcile is skipped. Refused: blank metadata, traversal, a non-`.md` or symlinked target, no claim section, and — only when `path` is omitted — a same-area near-twin.
|
|
50
|
+
|
|
51
|
+
A doc is current or deleted; no `Status` or `Supersedes`. A failed attempt is
|
|
52
|
+
valid knowledge when it records what was tried, why it failed, what is true now,
|
|
53
|
+
and what not to retry.
|
|
54
|
+
|
|
55
|
+
Lead with the actionable fact, keeping the reason, limits and evidence that
|
|
56
|
+
affect using it; drop chronology, repeated conclusions and empty sections. Keep
|
|
57
|
+
the service names, errors, paths and commands someone will search for. A
|
|
58
|
+
`Summary` decides whether to open the file.
|
|
59
|
+
|
|
60
|
+
Never record secrets, credentials, tokens or customer data — name the vault or
|
|
61
|
+
variable, never the value — and no personal data in GlobalContext or Wiki.
|
|
102
62
|
|
|
103
63
|
## Check it
|
|
104
64
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
Finally, run the repo's gate (`defined comply` where `.defined.json` exists,
|
|
115
|
-
else what its README documents) before proposing a commit. Fix failures; never
|
|
116
|
-
paper over a red gate with a docs commit. Prepare the commit and **wait for
|
|
65
|
+
`check` audits the lakes: front-matter, an `H1`, a claim section, no
|
|
66
|
+
hand-maintained `Date`, a distinctive identity, near-twins and broken relative
|
|
67
|
+
links. Obsolete fields, long docs and missing roots are **notes**, not findings;
|
|
68
|
+
wiki lakes are not validated. `add` enforces the structural rules at the write
|
|
69
|
+
boundary; cross-document audits stay with `check`. `maxDocLines` (default 200,
|
|
70
|
+
`0` disables) is a soft size signal, not a refusal.
|
|
71
|
+
|
|
72
|
+
Run the project's tests and typecheck before proposing a commit, fixing
|
|
73
|
+
failures rather than papering over them. Prepare the commit and **wait for
|
|
117
74
|
explicit approval** — recording findings is not commit approval. Never push.
|
package/src/index.ts
CHANGED
|
@@ -89,13 +89,13 @@ export const DOCS_INPUT = Schema.Struct({
|
|
|
89
89
|
search: Schema.optional(
|
|
90
90
|
Schema.String.annotate({
|
|
91
91
|
description:
|
|
92
|
-
"Case-insensitive search over title, path, keywords,
|
|
92
|
+
"Case-insensitive search over title, path, keywords, headings, code spans, area, summary and wiki prose. Blank lists the most recently changed docs; a partial match returns the closest.",
|
|
93
93
|
}),
|
|
94
94
|
),
|
|
95
95
|
lake: Schema.optional(
|
|
96
96
|
Schema.String.annotate({
|
|
97
97
|
description:
|
|
98
|
-
"Optional lake name
|
|
98
|
+
"Optional lake name: search and check are limited to it, read and add target it. Omit to search every lake, read the unique match, or add to the first writable repo lake.",
|
|
99
99
|
}),
|
|
100
100
|
),
|
|
101
101
|
add: Schema.optional(
|
|
@@ -103,7 +103,7 @@ export const DOCS_INPUT = Schema.Struct({
|
|
|
103
103
|
path: Schema.optional(
|
|
104
104
|
Schema.String.annotate({
|
|
105
105
|
description:
|
|
106
|
-
"Update target, e.g. `guides/getting-started.md
|
|
106
|
+
"Update target, e.g. `guides/getting-started.md`, relative to the lake root: replaces that doc wholesale, so omitted body content is lost, and skips the near-twin reconcile. Omit to create `<Area>/<slug>.md` from the H1.",
|
|
107
107
|
}),
|
|
108
108
|
),
|
|
109
109
|
metadata: Schema.Struct({
|
|
@@ -112,27 +112,27 @@ export const DOCS_INPUT = Schema.Struct({
|
|
|
112
112
|
Summary: Schema.String,
|
|
113
113
|
}).annotate({
|
|
114
114
|
description:
|
|
115
|
-
"Front-matter metadata; serialised, never copied. All three
|
|
115
|
+
"Front-matter metadata; serialised, never copied. All three are required and must not be blank.",
|
|
116
116
|
}),
|
|
117
117
|
body: Schema.String.annotate({
|
|
118
118
|
description:
|
|
119
|
-
"Markdown body: an H1,
|
|
119
|
+
"Markdown body: an H1 title, plus at least one claim section as an H2/H3 — Facts, Inferred, Tested, Failed approaches, Open questions.",
|
|
120
120
|
}),
|
|
121
121
|
}).annotate({
|
|
122
122
|
description:
|
|
123
|
-
"The only mutating mode: writes a new knowledge doc, or wholesale-replaces the doc at `path`.
|
|
123
|
+
"The only mutating mode: writes a new knowledge doc, or wholesale-replaces the doc at `path`. Refused: blank metadata, path traversal, a non-.md or symlinked target, no claim section, and a same-area near-twin when `path` is omitted. Wiki lakes are read-only.",
|
|
124
124
|
}),
|
|
125
125
|
),
|
|
126
126
|
check: Schema.optional(
|
|
127
127
|
Schema.Boolean.annotate({
|
|
128
128
|
description:
|
|
129
|
-
"Validate every knowledge doc
|
|
129
|
+
"Validate every knowledge doc: front-matter, an H1, a claim section, no hand-maintained Date, a distinctive H1+Keywords identity, near-twins, broken relative links, a soft length note. Wiki lakes and READMEs are not validated.",
|
|
130
130
|
}),
|
|
131
131
|
),
|
|
132
132
|
read: Schema.optional(
|
|
133
133
|
Schema.String.annotate({
|
|
134
134
|
description:
|
|
135
|
-
"A root-relative doc path, or `lake:path
|
|
135
|
+
"A root-relative doc path, or `lake:path`: returns the doc verbatim, front-matter and body, so find-then-read is one call. Lake docs and wiki pages only; project READMEs use the built-in `read` tool.",
|
|
136
136
|
}),
|
|
137
137
|
),
|
|
138
138
|
});
|
|
@@ -844,6 +844,8 @@ export interface LakeInfo {
|
|
|
844
844
|
writable: boolean;
|
|
845
845
|
/** Whether `check` validates it. */
|
|
846
846
|
validated: boolean;
|
|
847
|
+
/** Whether the root is absent, so the lake contributes nothing yet. */
|
|
848
|
+
missing?: boolean;
|
|
847
849
|
}
|
|
848
850
|
|
|
849
851
|
/**
|
|
@@ -857,8 +859,9 @@ export function helpText(lakes: readonly LakeInfo[]): string {
|
|
|
857
859
|
for (const lake of lakes) {
|
|
858
860
|
const access = lake.writable ? "writable" : "read-only";
|
|
859
861
|
const checked = lake.validated ? "checked" : "not checked";
|
|
862
|
+
const missing = lake.missing === true ? " — missing" : "";
|
|
860
863
|
lines.push(
|
|
861
|
-
`- ${lake.name} (${lake.class}, ${access}, ${checked}): ${lake.root}`,
|
|
864
|
+
`- ${lake.name} (${lake.class}, ${access}, ${checked}): ${lake.root}${missing}`,
|
|
862
865
|
);
|
|
863
866
|
}
|
|
864
867
|
lines.push(
|
|
@@ -1319,7 +1322,11 @@ export type AddOutcome =
|
|
|
1319
1322
|
path: string;
|
|
1320
1323
|
/** The serialised file contents. */
|
|
1321
1324
|
contents: string;
|
|
1322
|
-
/**
|
|
1325
|
+
/**
|
|
1326
|
+
* Same-area docs already on disk. Not rendered in the tool result —
|
|
1327
|
+
* the success path names the path and outcome only — but retained for
|
|
1328
|
+
* programmatic callers that want the surrounding context.
|
|
1329
|
+
*/
|
|
1323
1330
|
neighbours: readonly DocEntry[];
|
|
1324
1331
|
/** Soft advisory lines (e.g. a long doc worth splitting). */
|
|
1325
1332
|
notes: readonly string[];
|
|
@@ -1526,6 +1533,14 @@ export function prepareAdd(
|
|
|
1526
1533
|
if (h1Of(add.body) === undefined) {
|
|
1527
1534
|
return refuse("The body must carry an H1 (`# Title`).");
|
|
1528
1535
|
}
|
|
1536
|
+
// The same rule `check` applies, enforced before anything touches disk: a
|
|
1537
|
+
// doc that `check` would flag on the next run should never be written.
|
|
1538
|
+
if (!hasClaimSection(add.body)) {
|
|
1539
|
+
return refuse(
|
|
1540
|
+
"the body carries no claim section; add an H2/H3 headed Facts, " +
|
|
1541
|
+
"Inferred, Tested, Failed approaches or Open questions.",
|
|
1542
|
+
);
|
|
1543
|
+
}
|
|
1529
1544
|
if (!hasDistinctiveIdentity(reconcileQuery(add))) {
|
|
1530
1545
|
return refuse(
|
|
1531
1546
|
`the H1 and Keywords together carry no term of MIN_TERM_LENGTH ` +
|
|
@@ -1589,6 +1604,24 @@ export const CLAIM_SECTIONS = [
|
|
|
1589
1604
|
"Open questions",
|
|
1590
1605
|
] as const;
|
|
1591
1606
|
|
|
1607
|
+
/**
|
|
1608
|
+
* Whether a body carries at least one recognisable claim section as an H2/H3
|
|
1609
|
+
* heading. This is the one rule {@link prepareAdd} and {@link validateDoc}
|
|
1610
|
+
* share, so `add` cannot accept a doc that `check` would immediately flag.
|
|
1611
|
+
*
|
|
1612
|
+
* It reads through {@link scanBody}, so a fenced code block or an inline code
|
|
1613
|
+
* span cannot masquerade as a section, and a `# H1` or `#### H4` does not
|
|
1614
|
+
* satisfy it.
|
|
1615
|
+
* @param body Markdown body, front-matter already stripped.
|
|
1616
|
+
* @returns True when a claim-section heading is present.
|
|
1617
|
+
*/
|
|
1618
|
+
export function hasClaimSection(body: string): boolean {
|
|
1619
|
+
const claims = CLAIM_SECTIONS.map((section) => section.toLowerCase());
|
|
1620
|
+
return scanBody(body).headings.some((heading) =>
|
|
1621
|
+
claims.includes(heading.toLowerCase()),
|
|
1622
|
+
);
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1592
1625
|
/**
|
|
1593
1626
|
* The required front-matter fields that are blank in a raw doc: `Keywords` is
|
|
1594
1627
|
* blank when it yields no terms (matching `add`'s typed rule in
|
|
@@ -1633,10 +1666,7 @@ export function validateDoc(path: string, raw: string): string[] {
|
|
|
1633
1666
|
"hand-maintained `Date` — the date is derived by git, never written.",
|
|
1634
1667
|
);
|
|
1635
1668
|
}
|
|
1636
|
-
|
|
1637
|
-
if (
|
|
1638
|
-
!scanBody(body).headings.some((h) => claims.includes(h.toLowerCase()))
|
|
1639
|
-
) {
|
|
1669
|
+
if (!hasClaimSection(body)) {
|
|
1640
1670
|
findings.push(
|
|
1641
1671
|
"no claim section (Facts / Inferred / Tested / Failed approaches / Open questions).",
|
|
1642
1672
|
);
|
|
@@ -2048,13 +2078,17 @@ export function missNudgeFor(
|
|
|
2048
2078
|
* never a session dump (the near-twin guard catches duplicates, not dumps).
|
|
2049
2079
|
*/
|
|
2050
2080
|
export const RECORD_NUDGE =
|
|
2051
|
-
"Before summarising
|
|
2052
|
-
"(decisions, traps, runbooks, verified
|
|
2053
|
-
"
|
|
2081
|
+
"Before summarising, the active task comes first. If the session " +
|
|
2082
|
+
"established durable knowledge (decisions, traps, runbooks, verified " +
|
|
2083
|
+
"behaviour), add one `Record with docs add:` block per durable topic and " +
|
|
2084
|
+
"scope — coalesce related findings rather than one block per observation, " +
|
|
2054
2085
|
"never one session dump, and never one mixed-scope doc. Each block names " +
|
|
2055
|
-
"its
|
|
2056
|
-
"
|
|
2057
|
-
"
|
|
2086
|
+
"its class (repo, machine or global), `Area`, `H1` title and `Keywords`, and " +
|
|
2087
|
+
"keeps what was learned, why it matters, the evidence or status, any " +
|
|
2088
|
+
"unresolved uncertainty and the intended scope, so the next agent can " +
|
|
2089
|
+
"reconcile before writing. Reference a finding already recorded by path " +
|
|
2090
|
+
"rather than restating it, and assume persistence only where the session " +
|
|
2091
|
+
"confirms it.";
|
|
2058
2092
|
|
|
2059
2093
|
/**
|
|
2060
2094
|
* Resolve the plugin's `nudge` option: unset means on; a boolean passes
|
|
@@ -2097,7 +2131,10 @@ export interface Lake extends LakeSpec {
|
|
|
2097
2131
|
read: (path: string) => string;
|
|
2098
2132
|
/** Whether a root-relative path exists. */
|
|
2099
2133
|
exists: (path: string) => boolean;
|
|
2100
|
-
/**
|
|
2134
|
+
/**
|
|
2135
|
+
* Writes a root-relative path, creating parent directories beneath an
|
|
2136
|
+
* existing root; throws when the root is absent rather than creating it.
|
|
2137
|
+
*/
|
|
2101
2138
|
write: (path: string, contents: string) => void;
|
|
2102
2139
|
/** Environment notes for `check` (missing root, unversioned root). */
|
|
2103
2140
|
notes: readonly string[];
|
|
@@ -2186,7 +2223,7 @@ export function createLake(
|
|
|
2186
2223
|
};
|
|
2187
2224
|
return {
|
|
2188
2225
|
...spec,
|
|
2189
|
-
notes: unversionedNote(spec.root),
|
|
2226
|
+
notes: unversionedNote(spec.root, spec.name),
|
|
2190
2227
|
linkCheck: true,
|
|
2191
2228
|
collect: () =>
|
|
2192
2229
|
collectDocs(spec.root, (paths) => fallbackDates(spec.root, paths), {
|
|
@@ -2202,6 +2239,14 @@ export function createLake(
|
|
|
2202
2239
|
},
|
|
2203
2240
|
write: (path, contents) => {
|
|
2204
2241
|
const target = guarded(path);
|
|
2242
|
+
// The root is configured, never invented: a mistyped path must not
|
|
2243
|
+
// bootstrap a lake. `add` pre-checks for a friendlier refusal.
|
|
2244
|
+
if (!existsSync(spec.root)) {
|
|
2245
|
+
throw new Error(
|
|
2246
|
+
`lake '${spec.name}' root ${spec.root} does not exist; ` +
|
|
2247
|
+
"create it first.",
|
|
2248
|
+
);
|
|
2249
|
+
}
|
|
2205
2250
|
mkdirSync(dirname(target), { recursive: true });
|
|
2206
2251
|
writeFileSync(target, contents);
|
|
2207
2252
|
},
|
|
@@ -2725,12 +2770,26 @@ function searchDocs(
|
|
|
2725
2770
|
closest,
|
|
2726
2771
|
omitted,
|
|
2727
2772
|
} = rankDocs(entries, query, { perLakeQuota: runtime.perLakeQuota });
|
|
2773
|
+
const content = render(ranked, {
|
|
2774
|
+
closest,
|
|
2775
|
+
omitted,
|
|
2776
|
+
primaryLake: primaryLakeName(runtime.lakes) ?? "",
|
|
2777
|
+
});
|
|
2778
|
+
// A dormant lake contributes nothing, so without the note a typo'd root
|
|
2779
|
+
// looks like an empty lake rather than a broken path.
|
|
2780
|
+
const missing = runtime.lakes
|
|
2781
|
+
.filter(
|
|
2782
|
+
(lake) =>
|
|
2783
|
+
lake.root !== "" &&
|
|
2784
|
+
(lakeName === undefined || lakeName === lake.name) &&
|
|
2785
|
+
!existsSync(lake.root),
|
|
2786
|
+
)
|
|
2787
|
+
.map((lake) => missingRootNote(lake.root, lake.name));
|
|
2728
2788
|
return {
|
|
2729
|
-
content:
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
}),
|
|
2789
|
+
content:
|
|
2790
|
+
missing.length > 0
|
|
2791
|
+
? `${content}\n\n${missing.join("\n")}`
|
|
2792
|
+
: content,
|
|
2734
2793
|
count: ranked.length,
|
|
2735
2794
|
omitted,
|
|
2736
2795
|
};
|
|
@@ -2806,6 +2865,16 @@ function addDoc(
|
|
|
2806
2865
|
if ("reason" in target) {
|
|
2807
2866
|
return { content: target.reason, written: false };
|
|
2808
2867
|
}
|
|
2868
|
+
// A typo'd root must not bootstrap a new tree; the area directories below
|
|
2869
|
+
// an existing root still create on write.
|
|
2870
|
+
if (target.root !== "" && !existsSync(target.root)) {
|
|
2871
|
+
return {
|
|
2872
|
+
content:
|
|
2873
|
+
`${REFUSE} lake '${target.name}' root ${target.root} does ` +
|
|
2874
|
+
"not exist — create it first.",
|
|
2875
|
+
written: false,
|
|
2876
|
+
};
|
|
2877
|
+
}
|
|
2809
2878
|
const entries = target.collect();
|
|
2810
2879
|
const outcome = prepareAdd(add, entries, {
|
|
2811
2880
|
maxDocLines: runtime.maxDocLines,
|
|
@@ -2829,16 +2898,12 @@ function addDoc(
|
|
|
2829
2898
|
const head = `${address(target, outcome.path, runtime.lakes)} — ${
|
|
2830
2899
|
updated ? "updated" : "created"
|
|
2831
2900
|
}.`;
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
outcome.neighbours,
|
|
2836
|
-
{ primaryLake: target.name },
|
|
2837
|
-
)}`
|
|
2838
|
-
: "";
|
|
2901
|
+
// The path and outcome are the answer; a list of same-area neighbours was
|
|
2902
|
+
// unsolicited context that grew with the area and was picked by collection
|
|
2903
|
+
// order, not relevance. Refusals still carry their candidates.
|
|
2839
2904
|
const notes =
|
|
2840
2905
|
outcome.notes.length > 0 ? `\n\n${outcome.notes.join("\n")}` : "";
|
|
2841
|
-
return { content: `${head}${
|
|
2906
|
+
return { content: `${head}${notes}`, written: true };
|
|
2842
2907
|
}
|
|
2843
2908
|
|
|
2844
2909
|
/** A parsed read request: the lake (when known) and the path. */
|
|
@@ -2950,6 +3015,7 @@ function lakeInfo(lakes: readonly Lake[]): LakeInfo[] {
|
|
|
2950
3015
|
root: lake.root,
|
|
2951
3016
|
writable: lake.writable,
|
|
2952
3017
|
validated: lake.profile === "knowledge",
|
|
3018
|
+
missing: lake.root !== "" && !existsSync(lake.root),
|
|
2953
3019
|
}));
|
|
2954
3020
|
}
|
|
2955
3021
|
|
|
@@ -2974,11 +3040,11 @@ export interface DocsCommandDeps extends RuntimeInput {
|
|
|
2974
3040
|
* the full doctrine the native `docs` description carries.
|
|
2975
3041
|
*/
|
|
2976
3042
|
const TWIN_DESCRIPTION =
|
|
2977
|
-
"Search, read, add and check the configured knowledge lakes
|
|
2978
|
-
"project `README.md` files
|
|
2979
|
-
"
|
|
2980
|
-
"
|
|
2981
|
-
"
|
|
3043
|
+
"Search, read, add and check the configured knowledge lakes and nested " +
|
|
3044
|
+
"project `README.md` files (search-only) from inside `execute` — the `docs` " +
|
|
3045
|
+
"tool's Code Mode twin, same verbs and lakes. A blank search lists recent " +
|
|
3046
|
+
"docs; `read` returns a matched doc verbatim; `add` writes through a typed " +
|
|
3047
|
+
"DTO; `check` validates the lakes.";
|
|
2982
3048
|
|
|
2983
3049
|
/**
|
|
2984
3050
|
* Build the `docs` tool from its dependencies.
|
|
@@ -3010,13 +3076,13 @@ export function createDocsTool(
|
|
|
3010
3076
|
name,
|
|
3011
3077
|
description: codemode
|
|
3012
3078
|
? TWIN_DESCRIPTION
|
|
3013
|
-
: "
|
|
3014
|
-
"
|
|
3015
|
-
"
|
|
3016
|
-
|
|
3017
|
-
"
|
|
3018
|
-
|
|
3019
|
-
"
|
|
3079
|
+
: "Search the configured knowledge lakes and nested project " +
|
|
3080
|
+
"`README.md` files before a broad grep for how-to knowledge, " +
|
|
3081
|
+
"decisions or known problems. A blank search lists recent docs; " +
|
|
3082
|
+
"`help` shows the lakes and ranking. `read` returns a lake doc " +
|
|
3083
|
+
"verbatim; project `instructions:` results open with the built-in " +
|
|
3084
|
+
"read tool. `add` writes or replaces a knowledge doc; `check` " +
|
|
3085
|
+
"audits convention compliance.",
|
|
3020
3086
|
input: DOCS_INPUT,
|
|
3021
3087
|
options: codemode
|
|
3022
3088
|
? { namespace: "docs", codemode: true }
|
|
@@ -3099,9 +3165,7 @@ export function createDocsCommand(deps: DocsCommandDeps): CommandDefinition {
|
|
|
3099
3165
|
return {
|
|
3100
3166
|
name: "docs",
|
|
3101
3167
|
description:
|
|
3102
|
-
"Search the configured knowledge lakes, ranked, plus nested project `README.md` files (search only); `add <text>` records new knowledge through the tool's typed `add`; `check` validates the
|
|
3103
|
-
MAX_RESULTS +
|
|
3104
|
-
" most recently changed.",
|
|
3168
|
+
"Search the configured knowledge lakes, ranked, plus nested project `README.md` files (search only); `add <text>` records new knowledge through the tool's typed `add`; `check` validates the lakes; a blank search lists recent docs.",
|
|
3105
3169
|
async execute({ sessionID, prompt }) {
|
|
3106
3170
|
const text = prompt.text.trim();
|
|
3107
3171
|
if (isHelp(text)) {
|
|
@@ -3166,18 +3230,39 @@ export function resolveRoot(
|
|
|
3166
3230
|
}
|
|
3167
3231
|
|
|
3168
3232
|
/**
|
|
3169
|
-
*
|
|
3170
|
-
*
|
|
3171
|
-
*
|
|
3233
|
+
* The advisory note for a lake root that is absent. A missing root is a
|
|
3234
|
+
* configuration smell, not a failure: the lake stays dormant until the
|
|
3235
|
+
* directory exists, and `add` refuses rather than creating it.
|
|
3236
|
+
* @param root The resolved lake root.
|
|
3237
|
+
* @param lake The lake name, when known.
|
|
3238
|
+
* @returns The note line.
|
|
3239
|
+
*/
|
|
3240
|
+
export function missingRootNote(root: string, lake?: string): string {
|
|
3241
|
+
const subject =
|
|
3242
|
+
lake === undefined
|
|
3243
|
+
? `lake root ${root}`
|
|
3244
|
+
: `lake '${lake}' root ${root}`;
|
|
3245
|
+
return (
|
|
3246
|
+
`note: ${subject} does not exist — it is ignored until it does. ` +
|
|
3247
|
+
"A missing root is not an error."
|
|
3248
|
+
);
|
|
3249
|
+
}
|
|
3250
|
+
|
|
3251
|
+
/**
|
|
3252
|
+
* The environment notes for a lake root: missing, or unversioned. An absent
|
|
3253
|
+
* root is flagged but tolerated; an unversioned root still works — dates fall
|
|
3254
|
+
* back to file mtimes and the knowledge has no history — so it is surfaced,
|
|
3255
|
+
* not refused.
|
|
3172
3256
|
* @param root The lake root to probe.
|
|
3173
|
-
* @
|
|
3257
|
+
* @param lake The lake name, when known.
|
|
3258
|
+
* @returns The note lines; empty when the root is present and version-controlled.
|
|
3174
3259
|
*/
|
|
3175
|
-
export function unversionedNote(
|
|
3260
|
+
export function unversionedNote(
|
|
3261
|
+
root: string,
|
|
3262
|
+
lake?: string,
|
|
3263
|
+
): readonly string[] {
|
|
3176
3264
|
if (!existsSync(root)) {
|
|
3177
|
-
return [
|
|
3178
|
-
"note: this lake root does not exist yet — it is ignored until " +
|
|
3179
|
-
"it does. A missing root is not an error.",
|
|
3180
|
-
];
|
|
3265
|
+
return [missingRootNote(root, lake)];
|
|
3181
3266
|
}
|
|
3182
3267
|
const probe = spawnSync("git", ["rev-parse", "--show-toplevel"], {
|
|
3183
3268
|
cwd: root,
|
|
@@ -3323,17 +3408,12 @@ export default Plugin.define({
|
|
|
3323
3408
|
id: Skill.ID.make("docs"),
|
|
3324
3409
|
name: Skill.Name.make("Docs"),
|
|
3325
3410
|
description:
|
|
3326
|
-
"
|
|
3327
|
-
"(repo, machine, global
|
|
3328
|
-
"
|
|
3329
|
-
"
|
|
3330
|
-
"
|
|
3331
|
-
"
|
|
3332
|
-
"for how-to knowledge, when a task names a service, flag " +
|
|
3333
|
-
"or error string the lake may already explain, at the " +
|
|
3334
|
-
"start of unfamiliar work, when hunting for prior " +
|
|
3335
|
-
"decisions or traps, when a session learns something the " +
|
|
3336
|
-
"next one will need, and when closing out a session.",
|
|
3411
|
+
"Entry point to the configured knowledge lakes " +
|
|
3412
|
+
"(repo, machine, global, read-only wiki) and nested project " +
|
|
3413
|
+
"README.md files: find and read the right doc, then record " +
|
|
3414
|
+
"focused knowledge at the narrowest true scope. Use before " +
|
|
3415
|
+
"searching the codebase for how-to knowledge, or when a " +
|
|
3416
|
+
"session learns something durable.",
|
|
3337
3417
|
path: AbsolutePath.make(fileURLToPath(skillURL)),
|
|
3338
3418
|
content: skill,
|
|
3339
3419
|
});
|