drafted 1.18.2 → 1.18.4
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/cli/drafted.mjs +1 -1
- package/mcp/server.mjs +72 -13
- package/mcp/test-search-terms.mjs +42 -0
- package/mcp/test-skill-paths.mjs +29 -0
- package/package.json +1 -1
- package/plugin/skills/drafted/SKILL.md +1 -1
package/cli/drafted.mjs
CHANGED
|
@@ -1679,7 +1679,7 @@ function emitSkillResult(format, obj) {
|
|
|
1679
1679
|
|
|
1680
1680
|
skillCmd
|
|
1681
1681
|
.command('add')
|
|
1682
|
-
.description('Create a skill in Drafted from stdin JSON {name,description,content,readme
|
|
1682
|
+
.description('Create a skill in Drafted from stdin JSON {name,description,content,readme?,tags?,triggerPatterns?} — readme is optional; omitting it returns the skill with a warning, not an error')
|
|
1683
1683
|
.option('--format <fmt>', 'output format: json or text', 'text')
|
|
1684
1684
|
.action(async (opts) => {
|
|
1685
1685
|
requireLogin();
|
package/mcp/server.mjs
CHANGED
|
@@ -273,6 +273,31 @@ export function stripUrlOrigin(p) {
|
|
|
273
273
|
try { return new URL(s).pathname; } catch { return s; }
|
|
274
274
|
}
|
|
275
275
|
|
|
276
|
+
// A skill is a DIRECTORY: /skills/<slug>/<file>. SKILL.md is the skill's content
|
|
277
|
+
// (the skills table row); every other path is a supporting file (README.md,
|
|
278
|
+
// references/, scripts/) living in skill_files. The handler used to keep only
|
|
279
|
+
// segment 0, so `write /skills/<slug>/README.md` silently rewrote SKILL.md — and
|
|
280
|
+
// the create-time README gate then looked unsatisfiable from this tool.
|
|
281
|
+
// Pure + exported (asserted in mcp/test-skill-paths.mjs).
|
|
282
|
+
// Search matches every TERM in the query, not the raw phrase. A whole-string
|
|
283
|
+
// substring test means a natural multi-word search ("tool surface parity MCP")
|
|
284
|
+
// never hits a project whose name/slug/description contains each of those words
|
|
285
|
+
// — while the single word "parity" does. That reads as "no such project" for a
|
|
286
|
+
// project that is right there. Pure + exported (asserted in mcp/test-search-terms.mjs).
|
|
287
|
+
export function matchesAllTerms(parts, query) {
|
|
288
|
+
const terms = String(query || '').toLowerCase().split(/\s+/).filter(Boolean);
|
|
289
|
+
if (!terms.length) return false;
|
|
290
|
+
const hay = parts.map((v) => String(v || '')).join(' ').toLowerCase();
|
|
291
|
+
return terms.every((t) => hay.includes(t));
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export function splitSkillPath(p) {
|
|
295
|
+
const rest = /^\/skills\/?$/.test(p || '') ? '' : String(p || '').replace(/^\/skills\/?/, '');
|
|
296
|
+
const segs = rest.split('/').filter(Boolean);
|
|
297
|
+
const filePath = segs.slice(1).join('/');
|
|
298
|
+
return { slug: segs[0] || '', filePath, isSkillMd: !filePath || filePath === 'SKILL.md' };
|
|
299
|
+
}
|
|
300
|
+
|
|
276
301
|
|
|
277
302
|
export function createMcpServer(transport) {
|
|
278
303
|
// Remote transports (hosted HTTP MCP for claude.ai / ChatGPT) run on the
|
|
@@ -1543,8 +1568,11 @@ async function connectAgentWs() {
|
|
|
1543
1568
|
// suggestedName (basename of the working directory) lets the server offer it in the
|
|
1544
1569
|
// name-before-work gate message and as the tab placeholder.
|
|
1545
1570
|
try { agentWs.send(JSON.stringify({ type: 'agent-hello', agentLabel: getAgentLabel(), suggestedName: getSuggestedSessionName() })); } catch {}
|
|
1571
|
+
// Same open-race guard as the hello above: the socket can leave 'open' before
|
|
1572
|
+
// this second send lands, and an unguarded throw here kills the stdio child
|
|
1573
|
+
// (taking the agent's whole MCP connection with it) for a presence message.
|
|
1546
1574
|
if (getState().projectId) {
|
|
1547
|
-
agentWs.send(JSON.stringify({ type: 'join', projectId: getState().projectId, agent: true, agentLabel: getAgentLabel() }));
|
|
1575
|
+
try { agentWs.send(JSON.stringify({ type: 'join', projectId: getState().projectId, agent: true, agentLabel: getAgentLabel() })); } catch {}
|
|
1548
1576
|
}
|
|
1549
1577
|
});
|
|
1550
1578
|
|
|
@@ -2759,7 +2787,7 @@ tool('fs', 'Navigate Drafted like a local filesystem. An org is the top folder:
|
|
|
2759
2787
|
ops: z.array(z.any()).optional().describe('[edit] hashline ops for text frames. Apply ONLY against a fresh fs(read) of the frame: each op = {type, lineHash, newContent} where type is one of replace | delete | insertAfter | insertBefore, and lineHash is the FULL anchor from the read output (line number + hash, e.g. the token left of the |, like 42srt) — NOT a bare hash. delete/replace consume the target line; at most one replace/delete per line per edit (combine into one replace). If an op targets a line that changed since read it is rejected — re-read and retry. Element ops for excalidraw ({id,x,y,...}), or structured ops for office'),
|
|
2760
2788
|
state: z.any().optional().describe('[write] app-frame state (JSON) to persist for a deployed windowType:"app" frame; the canvas hydrates the app from it on load. Max 64KB.'),
|
|
2761
2789
|
metadata: z.any().optional().describe('[write] JSON metadata to attach to the written frame (e.g. {"tour": {title, auto?, steps}} to define a guided tour on this frame). Max 64KB.'),
|
|
2762
|
-
readme: z.string().optional().describe('[write] README.md markdown
|
|
2790
|
+
readme: z.string().optional().describe('[write] README.md markdown for a skill at /o/<org>/skills/<slug> — optional on both create and update. Stored as a supporting skill file so the skill directory renders a landing page on GitHub when exported; a skill created without one is returned with a warning, never rejected. Equivalent to a later fs(write, path="/o/<org>/skills/<slug>/README.md").'),
|
|
2763
2791
|
recursive: z.boolean().optional().describe('[ls] recurse into subdirectories'),
|
|
2764
2792
|
lines: z.string().optional().describe('[read] line range (e.g. "1-50") — partial read; content is hashline-annotated so a later edit stays surgical'),
|
|
2765
2793
|
pattern: z.string().optional().describe('[ls] filter filenames (e.g. "*.html")'),
|
|
@@ -2933,16 +2961,21 @@ tool('fs', 'Navigate Drafted like a local filesystem. An org is the top folder:
|
|
|
2933
2961
|
|
|
2934
2962
|
// ── Root: /skills/... ──────────────────────────────────────────
|
|
2935
2963
|
if (p.startsWith('/skills')) {
|
|
2936
|
-
const slug
|
|
2964
|
+
const { slug, filePath, isSkillMd } = splitSkillPath(p);
|
|
2965
|
+
const fileUrl = (id) => `/api/skills/${id}/files/${filePath.split('/').map(encodeURIComponent).join('/')}`;
|
|
2937
2966
|
if (['write', 'mv', 'rm'].includes(action)) {
|
|
2938
2967
|
await requireBoundOrgForProjectlessMutation(org || orgFromPath);
|
|
2939
2968
|
}
|
|
2940
2969
|
switch (action) {
|
|
2941
2970
|
case 'ls': {
|
|
2942
|
-
// A specific slug is a targeted ls:
|
|
2971
|
+
// A specific slug is a targeted ls: the skill's directory — SKILL.md
|
|
2972
|
+
// plus its supporting files, so a README is discoverable.
|
|
2943
2973
|
if (slug) {
|
|
2944
2974
|
const s = await api('GET', `/api/skills/slug/${slug}`, undefined, orgHeader).catch(() => null);
|
|
2945
|
-
|
|
2975
|
+
if (!s) return ok([]);
|
|
2976
|
+
const listed = await api('GET', `/api/skills/${s.id}/files`, undefined, orgHeader).catch(() => null);
|
|
2977
|
+
const files = (listed?.files || []).map((f) => f.path || f).filter((f) => f !== 'SKILL.md');
|
|
2978
|
+
return ok({ slug: s.slug, name: s.name, description: s.description, files: ['SKILL.md', ...files] });
|
|
2946
2979
|
}
|
|
2947
2980
|
const list = await api('GET', '/api/skills', undefined, orgHeader);
|
|
2948
2981
|
const skills = Array.isArray(list) ? list : (list?.skills || []);
|
|
@@ -2950,6 +2983,13 @@ tool('fs', 'Navigate Drafted like a local filesystem. An org is the top folder:
|
|
|
2950
2983
|
}
|
|
2951
2984
|
case 'read': {
|
|
2952
2985
|
if (!slug) return err(new Error('read /skills/<slug>'));
|
|
2986
|
+
if (!isSkillMd) {
|
|
2987
|
+
const owner = await api('GET', `/api/skills/slug/${slug}`, undefined, orgHeader).catch(() => null);
|
|
2988
|
+
if (!owner?.id) return err(new Error(`no Drafted-authored skill "${slug}" — supporting files exist only for skills stored in Drafted; a repo-indexed skill lives in git (read its SKILL.md with fs(read, path="/skills/${slug}"))`));
|
|
2989
|
+
const f = await api('GET', fileUrl(owner.id), undefined, orgHeader).catch(() => null);
|
|
2990
|
+
if (f?.content == null) return err(new Error(`file not found in skill ${slug}: ${filePath} (fs(ls, path="/skills/${slug}") lists what is there)`));
|
|
2991
|
+
return ok(f.content);
|
|
2992
|
+
}
|
|
2953
2993
|
let s = null;
|
|
2954
2994
|
try { s = await api('GET', `/api/skills/slug/${slug}`, undefined, orgHeader); } catch { /* not in the skills table — fall through to the repo index */ }
|
|
2955
2995
|
if (s) return ok(s?.content || '');
|
|
@@ -2968,6 +3008,15 @@ tool('fs', 'Navigate Drafted like a local filesystem. An org is the top folder:
|
|
|
2968
3008
|
}
|
|
2969
3009
|
if (!text) return err(new Error('write /skills/<slug> requires content or file_path'));
|
|
2970
3010
|
const existing = await api('GET', `/api/skills/slug/${slug}`, undefined, orgHeader).catch(() => null);
|
|
3011
|
+
if (!isSkillMd) {
|
|
3012
|
+
// Supporting file inside an existing skill. It can't create the skill —
|
|
3013
|
+
// the skill row is what owns the file — so say what call does.
|
|
3014
|
+
if (!existing?.id) {
|
|
3015
|
+
return err(new Error(`skill ${slug} does not exist yet — create it first with fs(write, path="/skills/${slug}", content=<SKILL.md markdown>) (add readme=<README.md markdown> to ship a README in the same call), then write supporting files into it.`));
|
|
3016
|
+
}
|
|
3017
|
+
const written = await api('PUT', fileUrl(existing.id), { content: text }, orgHeader);
|
|
3018
|
+
return ok(written || { slug, path: filePath, written: true });
|
|
3019
|
+
}
|
|
2971
3020
|
if (!existing) {
|
|
2972
3021
|
const g2 = g2Block(gs);
|
|
2973
3022
|
if (g2) return err(new Error(g2));
|
|
@@ -2976,9 +3025,14 @@ tool('fs', 'Navigate Drafted like a local filesystem. An org is the top folder:
|
|
|
2976
3025
|
// Derive a description from the slug when the agent didn't pass one
|
|
2977
3026
|
// (the API requires a non-empty description on create).
|
|
2978
3027
|
const description = args.description || existing?.description || `Reusable procedure: ${name.toLowerCase()}`;
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
3028
|
+
let result;
|
|
3029
|
+
if (existing) {
|
|
3030
|
+
result = await api('PUT', `/api/skills/${existing.id}`, { content: text }, orgHeader);
|
|
3031
|
+
// A `readme` passed with an update is a README write, not a no-op.
|
|
3032
|
+
if (readme) await api('PUT', `/api/skills/${existing.id}/files/README.md`, { content: readme }, orgHeader);
|
|
3033
|
+
} else {
|
|
3034
|
+
result = await api('POST', '/api/skills', { slug, name, content: text, description, ...(readme ? { readme } : {}) }, orgHeader);
|
|
3035
|
+
}
|
|
2982
3036
|
// Writing an archived skill restores it (update flow un-archives).
|
|
2983
3037
|
if (result?.id && existing?.archived) {
|
|
2984
3038
|
try { await api('POST', `/api/skills/${result.id}/restore`, undefined, orgHeader); } catch { /* best-effort */ }
|
|
@@ -2987,6 +3041,7 @@ tool('fs', 'Navigate Drafted like a local filesystem. An org is the top folder:
|
|
|
2987
3041
|
}
|
|
2988
3042
|
case 'mv': {
|
|
2989
3043
|
if (!to || !to.startsWith('/skills')) return err(new Error('mv within /skills requires to=/skills/...'));
|
|
3044
|
+
if (!isSkillMd) return err(new Error('mv moves a whole skill (/skills/<slug>), not a file inside one — rewrite the file at its new path and rm the old one'));
|
|
2990
3045
|
const toSlug = to.replace(/^\/skills\/?/, '').split('/')[0];
|
|
2991
3046
|
const result = await api('POST', '/api/skills/fork', { from: slug, to: toSlug, ...(org ? { org } : {}) }, orgHeader);
|
|
2992
3047
|
return ok(result);
|
|
@@ -2994,6 +3049,7 @@ tool('fs', 'Navigate Drafted like a local filesystem. An org is the top folder:
|
|
|
2994
3049
|
case 'rm': {
|
|
2995
3050
|
const s = await api('GET', `/api/skills/slug/${slug}`, undefined, orgHeader);
|
|
2996
3051
|
if (!s?.id) return err(new Error(`skill not found: ${slug}`));
|
|
3052
|
+
if (!isSkillMd) return ok(await api('DELETE', fileUrl(s.id), undefined, orgHeader));
|
|
2997
3053
|
return ok(await api('DELETE', `/api/skills/${s.id}`, undefined, orgHeader));
|
|
2998
3054
|
}
|
|
2999
3055
|
case 'search': {
|
|
@@ -3050,10 +3106,7 @@ tool('fs', 'Navigate Drafted like a local filesystem. An org is the top folder:
|
|
|
3050
3106
|
// addressable paths instead of the whole inventory.
|
|
3051
3107
|
const q = String(query || '').trim();
|
|
3052
3108
|
if (!q) return err(new Error('search requires a query — e.g. fs(search, path="/projects", query="<terms>")'));
|
|
3053
|
-
const
|
|
3054
|
-
const hitProjects = rows.filter(x =>
|
|
3055
|
-
[x.name, x.slug, x.description].some(v => String(v || '').toLowerCase().includes(needle))
|
|
3056
|
-
);
|
|
3109
|
+
const hitProjects = rows.filter(x => matchesAllTerms([x.name, x.slug, x.description], q));
|
|
3057
3110
|
let frames = [];
|
|
3058
3111
|
let frameError = null;
|
|
3059
3112
|
try {
|
|
@@ -3080,6 +3133,12 @@ tool('fs', 'Navigate Drafted like a local filesystem. An org is the top folder:
|
|
|
3080
3133
|
out.push(`\nFrames matching "${q}" (${frames.length}${frames.length > 25 ? ', first 25' : ''}):\n${lines.join('\n')}`);
|
|
3081
3134
|
} else if (frameError) {
|
|
3082
3135
|
out.push(`\n(frame search unavailable: ${frameError} — project matches above are complete, frame matches were not checked)`);
|
|
3136
|
+
} else {
|
|
3137
|
+
// Say the frame leg ran and found nothing. Printing only the project leg
|
|
3138
|
+
// leaves "frames were never searched" and "no frame matched" looking the
|
|
3139
|
+
// same, and frame search matches LABELS only — so a term that lives in
|
|
3140
|
+
// frame content is a miss, not an absence.
|
|
3141
|
+
out.push(`\nNo frame label matches "${q}" (frame search matches labels, not frame content).`);
|
|
3083
3142
|
}
|
|
3084
3143
|
return ok(out.join('\n'));
|
|
3085
3144
|
}
|
|
@@ -3287,7 +3346,7 @@ tool('fs', 'Navigate Drafted like a local filesystem. An org is the top folder:
|
|
|
3287
3346
|
// Express parse projectId as an ARRAY and the scope match nothing.
|
|
3288
3347
|
const res = await api('GET', `/api/search?q=${encodeURIComponent(q)}`);
|
|
3289
3348
|
const frames = Array.isArray(res) ? res : (res?.results || []);
|
|
3290
|
-
if (!frames.length) return ok(`No frames matching "${q}".`);
|
|
3349
|
+
if (!frames.length) return ok(`No frames matching "${q}" (frame search matches labels, not frame content).`);
|
|
3291
3350
|
const lines = frames.slice(0, 50).map(f =>
|
|
3292
3351
|
` /o/${f.orgSlug || f.orgId}/projects/${f.projectSlug || f.projectId}/${f.layer}/${f.lane || ''}/${f.label}`.replace(/\/\//g, '/')
|
|
3293
3352
|
);
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Regression for fs(search) term matching on /projects.
|
|
2
|
+
// Run: `node mcp/test-search-terms.mjs`. No framework — asserts the pure core.
|
|
3
|
+
//
|
|
4
|
+
// Why this exists: search used to substring-test the WHOLE query against each
|
|
5
|
+
// field, so `query="tool surface parity MCP Flow"` missed a project whose
|
|
6
|
+
// description says "tool surface" and "parity" — while `query="parity"` hit it.
|
|
7
|
+
// An agent reads that miss as "the project does not exist" and stops looking.
|
|
8
|
+
import assert from 'node:assert/strict';
|
|
9
|
+
import { matchesAllTerms } from './server.mjs';
|
|
10
|
+
|
|
11
|
+
const proj = [
|
|
12
|
+
'Flow AI MCP Server Feature Card',
|
|
13
|
+
'flow-ai-mcp-server-feature-card',
|
|
14
|
+
'Covers the BEO-2527 parity guard for the agent tool surface.',
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
// The reported failure: a natural multi-word query spanning name AND description.
|
|
18
|
+
assert.equal(matchesAllTerms(proj, 'tool surface parity MCP Flow'), true);
|
|
19
|
+
// The single word that did work must keep working.
|
|
20
|
+
assert.equal(matchesAllTerms(proj, 'parity'), true);
|
|
21
|
+
// Terms match across fields and across a hyphenated slug.
|
|
22
|
+
assert.equal(matchesAllTerms(proj, 'flow feature card'), true);
|
|
23
|
+
assert.equal(matchesAllTerms(proj, 'BEO-2527'), true);
|
|
24
|
+
// Case and surrounding whitespace are irrelevant.
|
|
25
|
+
assert.equal(matchesAllTerms(proj, ' PARITY Guard '), true);
|
|
26
|
+
|
|
27
|
+
// AND, not OR: one unmatched term is a miss. An OR would return most of the org.
|
|
28
|
+
assert.equal(matchesAllTerms(proj, 'parity kubernetes'), false);
|
|
29
|
+
assert.equal(matchesAllTerms(proj, 'kubernetes'), false);
|
|
30
|
+
|
|
31
|
+
// An empty query never matches — the caller rejects it before this, and
|
|
32
|
+
// "every term" over zero terms would otherwise be vacuously true for every row.
|
|
33
|
+
assert.equal(matchesAllTerms(proj, ''), false);
|
|
34
|
+
assert.equal(matchesAllTerms(proj, ' '), false);
|
|
35
|
+
assert.equal(matchesAllTerms(proj, undefined), false);
|
|
36
|
+
|
|
37
|
+
// Null/undefined fields don't throw or match a stray "null"/"undefined" term.
|
|
38
|
+
assert.equal(matchesAllTerms([null, undefined, 'alpha'], 'alpha'), true);
|
|
39
|
+
assert.equal(matchesAllTerms([null, undefined, 'alpha'], 'null'), false);
|
|
40
|
+
|
|
41
|
+
console.log('search term matching ok — every term must hit, across fields, never vacuously');
|
|
42
|
+
process.exit(0);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Regression for the /skills path split used by fs(ls|read|write|rm).
|
|
2
|
+
// Run: `node mcp/test-skill-paths.mjs`. No framework — asserts the pure core.
|
|
3
|
+
//
|
|
4
|
+
// Why this exists: a skill is a directory. Before this split the handler kept
|
|
5
|
+
// only the first segment, so `fs(write, path="/skills/<slug>/README.md")` wrote
|
|
6
|
+
// SKILL.md instead — the agent's README landed on top of the skill body and the
|
|
7
|
+
// create-time README gate stayed unsatisfiable. isSkillMd is the load-bearing
|
|
8
|
+
// bit: true routes to the skills row, false routes to skill_files.
|
|
9
|
+
import assert from 'node:assert/strict';
|
|
10
|
+
import { splitSkillPath } from './server.mjs';
|
|
11
|
+
|
|
12
|
+
// Root and bare slug — no file, so the skill row itself.
|
|
13
|
+
assert.deepEqual(splitSkillPath('/skills'), { slug: '', filePath: '', isSkillMd: true });
|
|
14
|
+
assert.deepEqual(splitSkillPath('/skills/'), { slug: '', filePath: '', isSkillMd: true });
|
|
15
|
+
assert.deepEqual(splitSkillPath('/skills/my-skill'), { slug: 'my-skill', filePath: '', isSkillMd: true });
|
|
16
|
+
assert.deepEqual(splitSkillPath('/skills/my-skill/'), { slug: 'my-skill', filePath: '', isSkillMd: true });
|
|
17
|
+
|
|
18
|
+
// SKILL.md is the skill body, addressed explicitly — same destination as the bare slug.
|
|
19
|
+
assert.deepEqual(splitSkillPath('/skills/my-skill/SKILL.md'), { slug: 'my-skill', filePath: 'SKILL.md', isSkillMd: true });
|
|
20
|
+
|
|
21
|
+
// Everything else is a supporting file, and the subpath survives.
|
|
22
|
+
assert.deepEqual(splitSkillPath('/skills/my-skill/README.md'), { slug: 'my-skill', filePath: 'README.md', isSkillMd: false });
|
|
23
|
+
assert.deepEqual(splitSkillPath('/skills/my-skill/references/api.md'), { slug: 'my-skill', filePath: 'references/api.md', isSkillMd: false });
|
|
24
|
+
|
|
25
|
+
// Case matters: readme.md is a different file from README.md, and neither is SKILL.md.
|
|
26
|
+
assert.equal(splitSkillPath('/skills/s/skill.md').isSkillMd, false);
|
|
27
|
+
|
|
28
|
+
console.log('skill path split ok — SKILL.md routes to the skill row, everything else to skill files');
|
|
29
|
+
process.exit(0);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "drafted",
|
|
3
|
-
"version": "1.18.
|
|
3
|
+
"version": "1.18.4",
|
|
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": [
|
|
@@ -80,7 +80,7 @@ These bookend the loop. Prime/feed at the start, deposit at the end.
|
|
|
80
80
|
## Skill authoring
|
|
81
81
|
|
|
82
82
|
- **Git is the source of truth for skills; Drafted renders, indexes, and searches.** When a repo is connected to a folder, the skills under `.agents/skills/` in that repo ARE the skills for that folder — authoring happens by committing to git, not by writing into Drafted. Drafted write paths for a repo-owned skill return `409 repo_owned` naming the repo, branch, and path to commit to. Use `repo(action="rescan")` after a push so the index reflects the change.
|
|
83
|
-
- **For a folder with NO connected repo, author skills in Drafted** with `fs(write, path="/o/<org>/skills/<slug>", content=..., readme=...)` (G2 gate first). A `README.md`
|
|
83
|
+
- **For a folder with NO connected repo, author skills in Drafted** with `fs(write, path="/o/<org>/skills/<slug>", content=..., readme=...)` (G2 gate first). A `README.md` (the `readme` param) is optional but recommended — it is what renders for the skill directory on GitHub if the skill is ever exported; omitting it creates the skill with a warning. A skill is a directory: `fs(write, path="/o/<org>/skills/<slug>/README.md", content=...)` and any other supporting file work once the skill exists, and `fs(ls, path="/o/<org>/skills/<slug>")` lists them. `fs(mv, path="/o/<org>/skills/<old>", to="/o/<org>/skills/<new>")` renames.
|
|
84
84
|
- **Knowledge goes in the wiki, procedure in skills.** Drafted owns org knowledge (wiki pages via `fs` under `/o/<org>/wiki/`); git owns reusable procedure (skills under `.agents/skills/`). Don't put a skill body in a wiki page or a knowledge doc in a SKILL.md.
|
|
85
85
|
- **Machine-specific build output is never portable.** Build `node_modules`, downloaded browsers, compiled binaries into a `.skillinstall/` directory inside the skill — Drafted always strips it on push and skill push auto-gitignores it, so the rebuildable bundle stays local while the method and recipe live in git/Drafted.
|
|
86
86
|
- **Improve skills when you find a better way.** Fix or distill a skill that underperformed rather than leaving it stale.
|