drafted 1.17.16 → 1.17.18
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/mcp/server.mjs +47 -7
- package/mcp/test-file-path-text.mjs +58 -0
- package/package.json +2 -2
- package/plugin/commands/create-project.md +5 -5
- package/plugin/commands/create-skill.md +3 -3
- package/plugin/commands/extract.md +1 -1
- package/plugin/commands/improve-project-harness.md +2 -2
- package/plugin/commands/improve-skill.md +2 -2
- package/plugin/commands/improve-wiki.md +3 -3
- package/plugin/commands/ingest.md +2 -2
package/mcp/server.mjs
CHANGED
|
@@ -129,6 +129,37 @@ export function runWithRequestState(initial, fn) {
|
|
|
129
129
|
// Stripped from the advertised schema on remote transports.
|
|
130
130
|
const LOCAL_ONLY_PARAMS = ['file_path'];
|
|
131
131
|
|
|
132
|
+
/**
|
|
133
|
+
* Read a local file as UTF-8 text for a `file_path` write to /wiki or /skills.
|
|
134
|
+
*
|
|
135
|
+
* Those roots are markdown — they take `content`, not bytes — so this is a
|
|
136
|
+
* different seam from the /projects upload path, which base64s the file and
|
|
137
|
+
* sends a contentType. Without this, writing a page or a SKILL.md that already
|
|
138
|
+
* exists on disk means pasting the whole file through the caller's context,
|
|
139
|
+
* which for anything sizeable is both slow and lossy.
|
|
140
|
+
*
|
|
141
|
+
* stdio only: `file_path` is stripped from the advertised schema on remote
|
|
142
|
+
* transports (LOCAL_ONLY_PARAMS), because the server has no access to the
|
|
143
|
+
* caller's filesystem.
|
|
144
|
+
*
|
|
145
|
+
* Exported for `mcp/test-file-path-text.mjs`.
|
|
146
|
+
*/
|
|
147
|
+
export function textFromLocalFile(filePath) {
|
|
148
|
+
let resolved = filePath;
|
|
149
|
+
try { resolved = resolve(filePath); } catch { /* keep as-is */ }
|
|
150
|
+
if (!existsSync(resolved)) throw new Error(`file not found: ${filePath}`);
|
|
151
|
+
if (statSync(resolved).isDirectory()) {
|
|
152
|
+
throw new Error(`${filePath} is a directory — pass a file`);
|
|
153
|
+
}
|
|
154
|
+
const buf = readFileSync(resolved);
|
|
155
|
+
// A NUL byte means this is not text. Writing it would store mojibake that
|
|
156
|
+
// only shows up much later, when someone reads the page back.
|
|
157
|
+
if (buf.includes(0)) {
|
|
158
|
+
throw new Error(`${filePath} looks binary — /wiki and /skills take text (markdown)`);
|
|
159
|
+
}
|
|
160
|
+
return buf.toString('utf8');
|
|
161
|
+
}
|
|
162
|
+
|
|
132
163
|
// Per-tool params whose value is arbitrary / deeply-nested JSON. Zod renders
|
|
133
164
|
// these (z.any(), z.array(z.any()), z.object({}).passthrough()) as an UNTYPED
|
|
134
165
|
// schema — an empty `{}` or an array whose `items` has no `type`. ChatGPT's
|
|
@@ -2697,7 +2728,7 @@ tool('fs', 'Navigate Drafted like a local filesystem. An org is the top folder:
|
|
|
2697
2728
|
to: z.string().optional().describe('[mv] destination path'),
|
|
2698
2729
|
query: z.string().optional().describe('[search] term to match against names/content'),
|
|
2699
2730
|
content: z.string().optional().describe('[write] inline HTML/markdown/text'),
|
|
2700
|
-
file_path: z.string().optional().describe('[write] absolute path to a local file to upload (stdio only)'),
|
|
2731
|
+
file_path: z.string().optional().describe('[write] absolute path to a local file to upload (stdio only). Under /projects it uploads bytes (images, PDFs, office files); under /wiki and /skills it reads the file as UTF-8 text, so a markdown file on disk can be written straight to a page or a SKILL.md without pasting it.'),
|
|
2701
2732
|
base64: z.string().optional().describe('[write] base64-encoded binary content'),
|
|
2702
2733
|
googleType: z.enum(['google-doc', 'google-sheet', 'google-slide']).optional().describe('[write] explicit Google Workspace type (also derived from .google-* filename extension)'),
|
|
2703
2734
|
title: z.string().optional().describe('[write + googleType] title for a new native file'),
|
|
@@ -2808,7 +2839,11 @@ tool('fs', 'Navigate Drafted like a local filesystem. An org is the top folder:
|
|
|
2808
2839
|
return ok(page.content);
|
|
2809
2840
|
}
|
|
2810
2841
|
case 'write': {
|
|
2811
|
-
|
|
2842
|
+
let text = content;
|
|
2843
|
+
if (text === undefined && file_path) {
|
|
2844
|
+
try { text = textFromLocalFile(file_path); } catch (e) { return err(e); }
|
|
2845
|
+
}
|
|
2846
|
+
if (!text) return err(new Error('write to /wiki/... requires content or file_path'));
|
|
2812
2847
|
// OKF boundary rule: /a/b.md ≡ a/b — strip a trailing .md so both spellings work.
|
|
2813
2848
|
const canonPath = wikiPath.replace(/\.md$/, '');
|
|
2814
2849
|
const existing = await api('GET', `/api/wiki/page?path=${encodeURIComponent(canonPath)}`, undefined, orgHeader).catch(() => null);
|
|
@@ -2817,9 +2852,9 @@ tool('fs', 'Navigate Drafted like a local filesystem. An org is the top folder:
|
|
|
2817
2852
|
// here silently forced every MCP-created page to type "Page" and a
|
|
2818
2853
|
// path-segment title, discarding the author's frontmatter. The server
|
|
2819
2854
|
// lifts both from the preamble and falls back to the path segment.
|
|
2820
|
-
const body = { path: canonPath, content };
|
|
2855
|
+
const body = { path: canonPath, content: text };
|
|
2821
2856
|
const result = existing
|
|
2822
|
-
? await api('PUT', `/api/wiki/page?path=${encodeURIComponent(canonPath)}`, { content }, orgHeader)
|
|
2857
|
+
? await api('PUT', `/api/wiki/page?path=${encodeURIComponent(canonPath)}`, { content: text }, orgHeader)
|
|
2823
2858
|
: await api('POST', '/api/wiki/pages', body, orgHeader);
|
|
2824
2859
|
return ok(result || { path: canonPath, written: true });
|
|
2825
2860
|
}
|
|
@@ -2894,7 +2929,12 @@ tool('fs', 'Navigate Drafted like a local filesystem. An org is the top folder:
|
|
|
2894
2929
|
return ok(s?.content || '');
|
|
2895
2930
|
}
|
|
2896
2931
|
case 'write': {
|
|
2897
|
-
if (!slug
|
|
2932
|
+
if (!slug) return err(new Error('write /skills/<slug> requires a slug'));
|
|
2933
|
+
let text = content;
|
|
2934
|
+
if (text === undefined && file_path) {
|
|
2935
|
+
try { text = textFromLocalFile(file_path); } catch (e) { return err(e); }
|
|
2936
|
+
}
|
|
2937
|
+
if (!text) return err(new Error('write /skills/<slug> requires content or file_path'));
|
|
2898
2938
|
const existing = await api('GET', `/api/skills/slug/${slug}`, undefined, orgHeader).catch(() => null);
|
|
2899
2939
|
if (!existing) {
|
|
2900
2940
|
const g2 = g2Block(gs);
|
|
@@ -2905,8 +2945,8 @@ tool('fs', 'Navigate Drafted like a local filesystem. An org is the top folder:
|
|
|
2905
2945
|
// (the API requires a non-empty description on create).
|
|
2906
2946
|
const description = args.description || existing?.description || `Reusable procedure: ${name.toLowerCase()}`;
|
|
2907
2947
|
const result = existing
|
|
2908
|
-
? await api('PUT', `/api/skills/${existing.id}`, { content }, orgHeader)
|
|
2909
|
-
: await api('POST', '/api/skills', { slug, name, content, description }, orgHeader);
|
|
2948
|
+
? await api('PUT', `/api/skills/${existing.id}`, { content: text }, orgHeader)
|
|
2949
|
+
: await api('POST', '/api/skills', { slug, name, content: text, description }, orgHeader);
|
|
2910
2950
|
// Writing an archived skill restores it (update flow un-archives).
|
|
2911
2951
|
if (result?.id && existing?.archived) {
|
|
2912
2952
|
try { await api('POST', `/api/skills/${result.id}/restore`, undefined, orgHeader); } catch { /* best-effort */ }
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression for textFromLocalFile() — the `file_path` text seam used by
|
|
3
|
+
* fs(write) on /wiki and /skills.
|
|
4
|
+
*
|
|
5
|
+
* node mcp/test-file-path-text.mjs
|
|
6
|
+
*
|
|
7
|
+
* Why this exists: /projects uploads bytes (base64 + contentType), /wiki and
|
|
8
|
+
* /skills take markdown text. Routing a file down the wrong seam stores
|
|
9
|
+
* mojibake that only surfaces when someone reads the page back, so the binary
|
|
10
|
+
* guard below is the load-bearing assertion, not a nicety.
|
|
11
|
+
*/
|
|
12
|
+
import assert from 'node:assert/strict';
|
|
13
|
+
import { mkdtempSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
14
|
+
import { tmpdir } from 'node:os';
|
|
15
|
+
import { join } from 'node:path';
|
|
16
|
+
import { textFromLocalFile } from './server.mjs';
|
|
17
|
+
|
|
18
|
+
const dir = mkdtempSync(join(tmpdir(), 'drafted-filepath-'));
|
|
19
|
+
|
|
20
|
+
// Plain markdown round-trips byte-for-byte.
|
|
21
|
+
const md = join(dir, 'page.md');
|
|
22
|
+
const body = '# Title\n\nBody with a UTF-8 em dash — and an accent é.\n';
|
|
23
|
+
writeFileSync(md, body, 'utf8');
|
|
24
|
+
assert.equal(textFromLocalFile(md), body);
|
|
25
|
+
|
|
26
|
+
// Multi-byte characters survive — a latin1 read would mangle these.
|
|
27
|
+
assert.ok(textFromLocalFile(md).includes('—'));
|
|
28
|
+
assert.ok(textFromLocalFile(md).includes('é'));
|
|
29
|
+
|
|
30
|
+
// Empty file returns '' rather than throwing. The caller decides: both write
|
|
31
|
+
// sites treat falsy text as "no content supplied" and error with guidance.
|
|
32
|
+
const empty = join(dir, 'empty.md');
|
|
33
|
+
writeFileSync(empty, '');
|
|
34
|
+
assert.equal(textFromLocalFile(empty), '');
|
|
35
|
+
|
|
36
|
+
// Binary is refused. A PNG header contains NUL bytes.
|
|
37
|
+
const png = join(dir, 'shot.png');
|
|
38
|
+
writeFileSync(png, Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00]));
|
|
39
|
+
assert.throws(() => textFromLocalFile(png), /looks binary/);
|
|
40
|
+
|
|
41
|
+
// Directories are refused with a clear message, not an EISDIR stack trace.
|
|
42
|
+
const sub = join(dir, 'nested');
|
|
43
|
+
mkdirSync(sub);
|
|
44
|
+
assert.throws(() => textFromLocalFile(sub), /is a directory/);
|
|
45
|
+
|
|
46
|
+
// Missing files name the path the caller passed, not the resolved one.
|
|
47
|
+
assert.throws(() => textFromLocalFile(join(dir, 'nope.md')), /file not found/);
|
|
48
|
+
|
|
49
|
+
// Relative paths resolve against cwd rather than being read blindly.
|
|
50
|
+
assert.throws(() => textFromLocalFile('definitely-not-here-xyz.md'), /file not found/);
|
|
51
|
+
|
|
52
|
+
console.log('file_path text seam ok — utf8 preserved, binary and dirs refused');
|
|
53
|
+
|
|
54
|
+
// Importing server.mjs opens the MCP WebSocket at module scope, which pins the
|
|
55
|
+
// event loop open forever. mcp/test-org-guards.mjs only exits because it
|
|
56
|
+
// finishes in ~176ms — before the socket connects — so it passes on a race
|
|
57
|
+
// rather than by design. Exit explicitly instead of inheriting that luck.
|
|
58
|
+
process.exit(0);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "drafted",
|
|
3
|
-
"version": "1.17.
|
|
3
|
+
"version": "1.17.18",
|
|
4
4
|
"description": "Drafted — visual thinking surface for humans and AI agents. Renders HTML, markdown, images, and code as frames on a zoomable canvas, with MCP tools for AI agents and real-time sync for humans.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
"react": "^18.3.1",
|
|
73
73
|
"react-dom": "^18.3.1",
|
|
74
74
|
"sharp": "^0.34.5",
|
|
75
|
-
"ws": "^8.
|
|
75
|
+
"ws": "^8.21.3",
|
|
76
76
|
"yaml": "^2.8.3",
|
|
77
77
|
"zod": "^4.3.6"
|
|
78
78
|
},
|
|
@@ -8,13 +8,13 @@ Spin up a project on the surface, or generalize one into a reusable template. Go
|
|
|
8
8
|
First ask: **a new project, or a reusable template?** (This command does both.)
|
|
9
9
|
|
|
10
10
|
Search before creating (also satisfies the gates):
|
|
11
|
-
1. `fs(search, path/o/<org>/wiki", query="<terms>")` for relevant knowledge. [G1]
|
|
12
|
-
2. `fs(search, path/o/<org>/skills", query="<topic>")` for procedures that should be attached. [G2/G3]
|
|
11
|
+
1. `fs(search, path="/o/<org>/wiki", query="<terms>")` for relevant knowledge. [G1]
|
|
12
|
+
2. `fs(search, path="/o/<org>/skills", query="<topic>")` for procedures that should be attached. [G2/G3]
|
|
13
13
|
|
|
14
14
|
Then:
|
|
15
|
-
- Create the project — a project is a folder: `fs(mkdir, path/o/<org>/projects/<name>")`. It starts with **no layers**; you define them by writing into them.
|
|
16
|
-
- `fs(write, path/o/<org>/projects/<name>/<layer>/<lane>/brief.md", content=...)` a real brief at the earliest layer (goal, audience, constraints — 6-12 lines, not a placeholder). The layer + lane auto-create.
|
|
15
|
+
- Create the project — a project is a folder: `fs(mkdir, path="/o/<org>/projects/<name>")`. It starts with **no layers**; you define them by writing into them.
|
|
16
|
+
- `fs(write, path="/o/<org>/projects/<name>/<layer>/<lane>/brief.md", content=...)` a real brief at the earliest layer (goal, audience, constraints — 6-12 lines, not a placeholder). The layer + lane auto-create.
|
|
17
17
|
- Attach the relevant skills you found via the skill gates.
|
|
18
|
-
- `fs(ls, path/o/<org>/projects/<name>")` to confirm the structure, and hand the user the `projectUrl` from the response.
|
|
18
|
+
- `fs(ls, path="/o/<org>/projects/<name>")` to confirm the structure, and hand the user the `projectUrl` from the response.
|
|
19
19
|
|
|
20
20
|
For a **template**: build the layer structure + anchored guidance, then note how to fork it next time. Don't speculatively fill downstream frames — wait for direction.
|
|
@@ -6,8 +6,8 @@ argument-hint: <what the procedure is for>
|
|
|
6
6
|
Turn a repeatable way of working into a Drafted skill so every future agent in the org follows it. Procedure: $ARGUMENTS
|
|
7
7
|
|
|
8
8
|
Do the searches first (they also satisfy the gates):
|
|
9
|
-
1. `fs(search, path/o/<org>/wiki", query="<terms>")` for relevant org knowledge the procedure should reference. [G1]
|
|
10
|
-
2. `fs(search, path/o/<org>/skills", query="<topic>")` for prior art — an existing skill to improve instead of duplicating. [G2 — also enforced on writing a new skill.] If a close match exists, prefer `/drafted:improve-skill`.
|
|
9
|
+
1. `fs(search, path="/o/<org>/wiki", query="<terms>")` for relevant org knowledge the procedure should reference. [G1]
|
|
10
|
+
2. `fs(search, path="/o/<org>/skills", query="<topic>")` for prior art — an existing skill to improve instead of duplicating. [G2 — also enforced on writing a new skill.] If a close match exists, prefer `/drafted:improve-skill`.
|
|
11
11
|
|
|
12
12
|
Then define a PROPER procedure, not a vague note:
|
|
13
13
|
- a clear trigger ("when to use this"),
|
|
@@ -15,4 +15,4 @@ Then define a PROPER procedure, not a vague note:
|
|
|
15
15
|
- success criteria / what "done right" looks like,
|
|
16
16
|
- written in the second person so any agent can follow it directly. Keep it sharp (~40 lines).
|
|
17
17
|
|
|
18
|
-
Show the draft to the user with a proposed `name` (Title Case), one-line `description`, `tags` (3-5), and `triggerPatterns`. After approval, write it with `fs(write, path/o/<org>/skills/<slug>", content=...)` — the slug is derived from the name. Confirm with the slug and note it will auto-surface on matching tasks.
|
|
18
|
+
Show the draft to the user with a proposed `name` (Title Case), one-line `description`, `tags` (3-5), and `triggerPatterns`. After approval, write it with `fs(write, path="/o/<org>/skills/<slug>", content=...)` — the slug is derived from the name. Confirm with the slug and note it will auto-surface on matching tasks.
|
|
@@ -7,7 +7,7 @@ Session-end deposit. Harvest what's durable from this conversation back into the
|
|
|
7
7
|
|
|
8
8
|
Review the session, then **present the user options for which store(s) to deposit into** — don't auto-decide. Offer any that apply:
|
|
9
9
|
|
|
10
|
-
- **Knowledge → wiki** — durable facts, decisions, or findings worth keeping. Search first (`fs(search, path/o/<org>/wiki", ...)`) to avoid fragmenting, then `fs(write, path/o/<org>/wiki/<path>", ...)`.
|
|
10
|
+
- **Knowledge → wiki** — durable facts, decisions, or findings worth keeping. Search first (`fs(search, path="/o/<org>/wiki", ...)`) to avoid fragmenting, then `fs(write, path="/o/<org>/wiki/<path>", ...)`.
|
|
11
11
|
- **Procedure → skill** — a repeatable way of working that emerged. Follow `/drafted:create-skill`.
|
|
12
12
|
- **Template → surface** — a reusable project structure that emerged. Build it as a project via `fs(mkdir, ...)` + frames.
|
|
13
13
|
|
|
@@ -7,8 +7,8 @@ When the user had to correct the work in this project, codify the correction so
|
|
|
7
7
|
|
|
8
8
|
1. Identify the recurring correction(s) or standing rule from this session.
|
|
9
9
|
2. Choose the right Drafted-side gate per correction (each counts toward the project's context budget):
|
|
10
|
-
- **Project anchor** — a brief, constraint, or style guide that must be in context project-wide: `fs(write, path/o/<org>/projects/<project>/<layer>/<lane>/<file>", content=...)` it, then anchor it so it surfaces on open. [G5]
|
|
11
|
-
- **Attached skill** — a procedure that must be loaded before work: `/drafted:create-skill` (or `fs(search, path/o/<org>/skills", ...)` for an existing one), then attach it to the project. [G4]
|
|
10
|
+
- **Project anchor** — a brief, constraint, or style guide that must be in context project-wide: `fs(write, path="/o/<org>/projects/<project>/<layer>/<lane>/<file>", content=...)` it, then anchor it so it surfaces on open. [G5]
|
|
11
|
+
- **Attached skill** — a procedure that must be loaded before work: `/drafted:create-skill` (or `fs(search, path="/o/<org>/skills", ...)` for an existing one), then attach it to the project. [G4]
|
|
12
12
|
- **Layer rule** — a standing instruction for one stage: set that layer's rules so work in it is gated. [G6]
|
|
13
13
|
3. Propose which mechanism for each correction, confirm with the user, then apply.
|
|
14
14
|
4. Confirm what is now enforced — the next session in this project will be gated on it automatically.
|
|
@@ -5,10 +5,10 @@ argument-hint: <which skill, and what's off>
|
|
|
5
5
|
|
|
6
6
|
Improve an existing org skill when it underperformed in practice. Skill / issue: $ARGUMENTS
|
|
7
7
|
|
|
8
|
-
1. `fs(search, path/o/<org>/skills", query="<slug>")` then `fs(read, path/o/<org>/skills/<slug>")` the skill in question — read it fully.
|
|
8
|
+
1. `fs(search, path="/o/<org>/skills", query="<slug>")` then `fs(read, path="/o/<org>/skills/<slug>")` the skill in question — read it fully.
|
|
9
9
|
2. Pinpoint the inefficiency: a missing step, a wrong instruction, an ambiguous trigger, or a step that wastes effort.
|
|
10
10
|
3. Propose the specific edit to the user — show before/after of the changed steps.
|
|
11
|
-
4. After approval, `fs(write, path/o/<org>/skills/<slug>", content=<updated>)` — writing an existing skill updates it (the version bumps automatically). If it was archived, writing restores it.
|
|
11
|
+
4. After approval, `fs(write, path="/o/<org>/skills/<slug>", content=<updated>)` — writing an existing skill updates it (the version bumps automatically). If it was archived, writing restores it.
|
|
12
12
|
5. Confirm what changed so the next agent benefits.
|
|
13
13
|
|
|
14
14
|
Skills are the org's stable processes — every fix compounds across everyone who uses them.
|
|
@@ -5,10 +5,10 @@ argument-hint: <what's wrong, or the topic to clean up>
|
|
|
5
5
|
|
|
6
6
|
Improve the org wiki when knowledge has drifted. Issue/topic: $ARGUMENTS
|
|
7
7
|
|
|
8
|
-
1. `fs(search, path/o/<org>/wiki", query="<terms>")` (3-5 paraphrased queries) and `fs(read, path/o/<org>/wiki/<path>")` the affected pages — don't trust titles, open them.
|
|
8
|
+
1. `fs(search, path="/o/<org>/wiki", query="<terms>")` (3-5 paraphrased queries) and `fs(read, path="/o/<org>/wiki/<path>")` the affected pages — don't trust titles, open them.
|
|
9
9
|
2. Diagnose: duplicate pages, a stale fact, a contradiction, or knowledge fragmented across pages.
|
|
10
10
|
3. Propose the fix to the user: consolidate duplicates, correct the fact, reconcile the contradiction, or re-link fragments.
|
|
11
|
-
4. Apply with `fs(write, path/o/<org>/wiki/<path>", content=<updated>)` (hashline `edit` for surgical changes) or `fs(mv, path/o/<org>/wiki/<from>", to/o/<org>/wiki/<to>")` (which rewrites inbound links). Check what links to a page before moving or archiving it.
|
|
12
|
-
5. Never hard-delete — `fs(rm, path/o/<org>/wiki/<path>")` moves a page to the archive folder.
|
|
11
|
+
4. Apply with `fs(write, path="/o/<org>/wiki/<path>", content=<updated>)` (hashline `edit` for surgical changes) or `fs(mv, path="/o/<org>/wiki/<from>", to="/o/<org>/wiki/<to>")` (which rewrites inbound links). Check what links to a page before moving or archiving it.
|
|
12
|
+
5. Never hard-delete — `fs(rm, path="/o/<org>/wiki/<path>")` moves a page to the archive folder.
|
|
13
13
|
|
|
14
14
|
Leave the wiki more coherent than you found it — fewer, sharper, better-linked pages.
|
|
@@ -12,9 +12,9 @@ First decide WHAT to ingest. If it's obvious from the conversation (a research r
|
|
|
12
12
|
3. **Interrogate the user** (grill-me) — when the knowledge is in their head. Interview relentlessly, walking ONE branch of the decision tree at a time and proposing your recommended answer at each step, until you reach shared understanding. Then structure what you captured.
|
|
13
13
|
|
|
14
14
|
Then deposit:
|
|
15
|
-
- `fs(search, path/o/<org>/wiki", query="<terms>")` first (3-5 paraphrased queries) so you don't fragment existing pages.
|
|
15
|
+
- `fs(search, path="/o/<org>/wiki", query="<terms>")` first (3-5 paraphrased queries) so you don't fragment existing pages.
|
|
16
16
|
- Propose the page set to the user before writing.
|
|
17
|
-
- Write with `fs(write, path/o/<org>/wiki/<path>", content=...)` per page. Cross-link related pages.
|
|
17
|
+
- Write with `fs(write, path="/o/<org>/wiki/<path>", content=...)` per page. Cross-link related pages.
|
|
18
18
|
- Confirm what landed where, with the page links.
|
|
19
19
|
|
|
20
20
|
The wiki is the org's durable knowledge — write for the next agent and teammate, not just this session.
|