drafted 1.7.26 → 1.8.1
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/install-mcp.sh +46 -16
- package/mcp/gates.mjs +124 -0
- package/mcp/server.mjs +165 -164
- package/package.json +5 -3
- package/plugin/commands/create-project.md +23 -0
- package/plugin/commands/create-skill.md +18 -0
- package/plugin/commands/extract.md +16 -0
- package/plugin/commands/improve-project-harness.md +16 -0
- package/plugin/commands/improve-skill.md +14 -0
- package/plugin/commands/improve-wiki.md +14 -0
- package/plugin/commands/ingest.md +20 -0
- package/plugin/commands/onboard-drafted.md +17 -0
- package/plugin/skills/drafted/SKILL.md +84 -0
- package/src/shared/gate-budget.mjs +30 -0
package/install-mcp.sh
CHANGED
|
@@ -507,34 +507,64 @@ NODE
|
|
|
507
507
|
install_agent_instructions "$HOME/.claude/CLAUDE.md" "Claude global instructions"
|
|
508
508
|
install_agent_instructions "$HOME/.codex/CODEX.md" "Codex global instructions"
|
|
509
509
|
|
|
510
|
-
# ── Skills
|
|
510
|
+
# ── Skills & commands ─────────────────────────────────────────────
|
|
511
511
|
|
|
512
|
-
step "Installing skills"
|
|
512
|
+
step "Installing skills and commands"
|
|
513
513
|
|
|
514
|
-
#
|
|
514
|
+
# Resolve the plugin source: a local checkout when present (e.g. --local from this
|
|
515
|
+
# repo), otherwise the installed npm package's bundled plugin/ directory.
|
|
515
516
|
DRAFTED_PKG_DIR="$(node -e "try { console.log(require.resolve('drafted/package.json').replace('/package.json','')) } catch { process.exit(1) }" 2>/dev/null)" || true
|
|
517
|
+
if [ -d "$SCRIPT_DIR/plugin/skills" ] || [ -d "$SCRIPT_DIR/plugin/commands" ]; then
|
|
518
|
+
PLUGIN_SRC="$SCRIPT_DIR/plugin"
|
|
519
|
+
elif [ -n "$DRAFTED_PKG_DIR" ] && [ -d "$DRAFTED_PKG_DIR/plugin" ]; then
|
|
520
|
+
PLUGIN_SRC="$DRAFTED_PKG_DIR/plugin"
|
|
521
|
+
else
|
|
522
|
+
PLUGIN_SRC=""
|
|
523
|
+
fi
|
|
516
524
|
|
|
517
|
-
|
|
525
|
+
# Skills — Claude Code auto-loads these from ~/.claude/skills/<name>/SKILL.md
|
|
526
|
+
if [ -n "$PLUGIN_SRC" ] && [ -d "$PLUGIN_SRC/skills" ]; then
|
|
518
527
|
SKILLS_DIR="$HOME/.claude/skills"
|
|
519
528
|
mkdir -p "$SKILLS_DIR"
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
for f in "$DRAFTED_PKG_DIR/skills/"*.md; do
|
|
523
|
-
[ -f "$f" ] || continue
|
|
524
|
-
cp "$f" "$SKILLS_DIR/"
|
|
525
|
-
INSTALLED=$((INSTALLED + 1))
|
|
526
|
-
done
|
|
527
|
-
# Copy skill directories
|
|
528
|
-
for d in "$DRAFTED_PKG_DIR/skills/"*/; do
|
|
529
|
+
SKILL_COUNT=0
|
|
530
|
+
for d in "$PLUGIN_SRC/skills/"*/; do
|
|
529
531
|
[ -d "$d" ] || continue
|
|
530
532
|
DIRNAME="$(basename "$d")"
|
|
531
533
|
rm -rf "$SKILLS_DIR/$DIRNAME"
|
|
532
534
|
cp -r "$d" "$SKILLS_DIR/$DIRNAME"
|
|
533
|
-
|
|
535
|
+
SKILL_COUNT=$((SKILL_COUNT + 1))
|
|
534
536
|
done
|
|
535
|
-
ok "Installed $
|
|
537
|
+
ok "Installed $SKILL_COUNT skill(s) to $SKILLS_DIR"
|
|
538
|
+
else
|
|
539
|
+
echo -e " ${YELLOW}Skill source not found in package — skipping skills.${RESET}"
|
|
540
|
+
fi
|
|
541
|
+
|
|
542
|
+
# Commands — namespaced under drafted/ so they register as /drafted:<name>,
|
|
543
|
+
# matching the marketplace install and the commands' own cross-references.
|
|
544
|
+
if [ -n "$PLUGIN_SRC" ] && [ -d "$PLUGIN_SRC/commands" ]; then
|
|
545
|
+
CMD_COUNT=0
|
|
546
|
+
for f in "$PLUGIN_SRC/commands/"*.md; do
|
|
547
|
+
[ -f "$f" ] || continue
|
|
548
|
+
CMD_COUNT=$((CMD_COUNT + 1))
|
|
549
|
+
done
|
|
550
|
+
|
|
551
|
+
if [ "$CLIENT_CLAUDE_CODE" = true ]; then
|
|
552
|
+
CLAUDE_CMD_DIR="$HOME/.claude/commands/drafted"
|
|
553
|
+
rm -rf "$CLAUDE_CMD_DIR"
|
|
554
|
+
mkdir -p "$CLAUDE_CMD_DIR"
|
|
555
|
+
cp "$PLUGIN_SRC/commands/"*.md "$CLAUDE_CMD_DIR/" 2>/dev/null || true
|
|
556
|
+
ok "Installed $CMD_COUNT command(s) to $CLAUDE_CMD_DIR"
|
|
557
|
+
fi
|
|
558
|
+
|
|
559
|
+
if [ "$CLIENT_CODEX" = true ]; then
|
|
560
|
+
CODEX_CMD_DIR="$HOME/.codex/prompts/drafted"
|
|
561
|
+
rm -rf "$CODEX_CMD_DIR"
|
|
562
|
+
mkdir -p "$CODEX_CMD_DIR"
|
|
563
|
+
cp "$PLUGIN_SRC/commands/"*.md "$CODEX_CMD_DIR/" 2>/dev/null || true
|
|
564
|
+
ok "Installed $CMD_COUNT command(s) to $CODEX_CMD_DIR"
|
|
565
|
+
fi
|
|
536
566
|
else
|
|
537
|
-
echo -e " ${YELLOW}
|
|
567
|
+
echo -e " ${YELLOW}Command source not found in package — skipping commands.${RESET}"
|
|
538
568
|
fi
|
|
539
569
|
|
|
540
570
|
|
package/mcp/gates.mjs
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// Pure gate logic for the Drafted compounding harness.
|
|
2
|
+
//
|
|
3
|
+
// These helpers are intentionally side-effect free (no `api()`, no network, no
|
|
4
|
+
// module state) so they can be unit tested in isolation. mcp/server.mjs imports
|
|
5
|
+
// them and wires the decisions into the tool handlers + per-session getState().
|
|
6
|
+
//
|
|
7
|
+
// See docs/plans/compounding-harness.md and the Drafted "gates-checklist" frame.
|
|
8
|
+
|
|
9
|
+
// One combined per-project budget for the auto-injected priming set:
|
|
10
|
+
// attached-skill bodies + project anchor bodies + the active layer's rules.
|
|
11
|
+
// Single source of truth lives in src/shared so the server-side deposit caps
|
|
12
|
+
// enforce the identical limit. Re-exported here for the MCP gate helpers.
|
|
13
|
+
import { PROJECT_CONTEXT_BUDGET_CHARS, selectWithinBudget } from '../src/shared/gate-budget.mjs';
|
|
14
|
+
export { PROJECT_CONTEXT_BUDGET_CHARS, selectWithinBudget };
|
|
15
|
+
|
|
16
|
+
// ── Per-session gate flags (reset every MCP session) ──────────────────────────
|
|
17
|
+
|
|
18
|
+
export function createGateState() {
|
|
19
|
+
return { wikiSearched: false, skillSearched: false, templateSearched: false };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// kind: 'wiki' | 'skill' | 'template'
|
|
23
|
+
export function markSearched(gateState, kind) {
|
|
24
|
+
if (kind === 'wiki') gateState.wikiSearched = true;
|
|
25
|
+
else if (kind === 'skill') gateState.skillSearched = true;
|
|
26
|
+
else if (kind === 'template') gateState.templateSearched = true;
|
|
27
|
+
return gateState;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ── Enforce gates (create chain). Return an error string if blocked, else null ─
|
|
31
|
+
|
|
32
|
+
export function g1Block(gateState, wikiIndex) {
|
|
33
|
+
if (gateState.wikiSearched) return null;
|
|
34
|
+
let msg =
|
|
35
|
+
'G1: search the org wiki before reading or editing anything. ' +
|
|
36
|
+
'Call wiki(action="search", query="<relevant terms>") first, then retry. ' +
|
|
37
|
+
'More knowledge = less searching — start by drawing on what the org already knows.';
|
|
38
|
+
if (wikiIndex) msg += `\n\nWiki index (what exists to search):\n${wikiIndex}`;
|
|
39
|
+
return msg;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function g2Block(gateState) {
|
|
43
|
+
if (gateState.skillSearched) return null;
|
|
44
|
+
return (
|
|
45
|
+
'G2: search for prior-art skills before creating one. ' +
|
|
46
|
+
'Call skill(action="search", query="<topic>") first — if a close match exists, improve it ' +
|
|
47
|
+
'(/drafted:improve-skill) instead of duplicating — then retry skill(action="add").'
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function g3Block(gateState) {
|
|
52
|
+
const missing = [];
|
|
53
|
+
if (!gateState.wikiSearched) missing.push('wiki(action="search")');
|
|
54
|
+
if (!gateState.skillSearched) missing.push('skill(action="search")');
|
|
55
|
+
if (!gateState.templateSearched) missing.push('template(action="list")');
|
|
56
|
+
if (missing.length === 0) return null;
|
|
57
|
+
return (
|
|
58
|
+
`G3: search before creating a project. Run ${missing.join(', ')} first ` +
|
|
59
|
+
'(reuse existing knowledge, skills, and templates), then retry project(action="create").'
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ── Per-project context budget (auto-inject set) ──────────────────────────────
|
|
64
|
+
|
|
65
|
+
function itemChars(it) {
|
|
66
|
+
if (typeof it === 'number') return it;
|
|
67
|
+
if (typeof it === 'string') return it.length;
|
|
68
|
+
if (it && typeof it === 'object') {
|
|
69
|
+
if (typeof it.chars === 'number') return it.chars;
|
|
70
|
+
if (it.content != null) return String(it.content).length;
|
|
71
|
+
}
|
|
72
|
+
return 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function sumChars(items) {
|
|
76
|
+
if (!Array.isArray(items)) return 0;
|
|
77
|
+
return items.reduce((total, it) => total + itemChars(it), 0);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function budgetRemaining(currentTotal, budget = PROJECT_CONTEXT_BUDGET_CHARS) {
|
|
81
|
+
return Math.max(0, budget - currentTotal);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function wouldExceedBudget(currentTotal, addChars, budget = PROJECT_CONTEXT_BUDGET_CHARS) {
|
|
85
|
+
return currentTotal + addChars > budget;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function budgetError(currentTotal, addChars, label, budget = PROJECT_CONTEXT_BUDGET_CHARS) {
|
|
89
|
+
const remaining = budgetRemaining(currentTotal, budget);
|
|
90
|
+
return (
|
|
91
|
+
`Per-project context budget exceeded: ${label} needs ${addChars} chars but only ${remaining} ` +
|
|
92
|
+
`of ${budget} remain. Tighten existing attached skills / anchors / layer rules first ` +
|
|
93
|
+
`(see /drafted:improve-project-harness).`
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ── G6 layer-rule default detection ───────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
// A layer prompt is "default" (skip G6 inject) when it is empty or byte-identical
|
|
100
|
+
// to the template's default prompt for that layer key.
|
|
101
|
+
export function isLayerPromptDefault(layerPrompt, templateDefaultPrompt) {
|
|
102
|
+
const p = (layerPrompt ?? '').trim();
|
|
103
|
+
if (p === '') return true;
|
|
104
|
+
const d = (templateDefaultPrompt ?? '').trim();
|
|
105
|
+
return d !== '' && p === d;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function shouldInjectLayerPrompt(layerPrompt, templateDefaultPrompt) {
|
|
109
|
+
return !isLayerPromptDefault(layerPrompt, templateDefaultPrompt);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ── Wiki index formatting (bounded map injected with the G1 block) ────────────
|
|
113
|
+
|
|
114
|
+
export function formatWikiIndex(pages, max = 50) {
|
|
115
|
+
if (!Array.isArray(pages) || pages.length === 0) return '(wiki is empty — no pages yet)';
|
|
116
|
+
const lines = pages.slice(0, max).map((p) => {
|
|
117
|
+
const path = typeof p === 'string' ? p : p.path ?? p.slug ?? '';
|
|
118
|
+
const title = typeof p === 'object' && p && p.title ? ` — ${p.title}` : '';
|
|
119
|
+
return ` ${path}${title}`;
|
|
120
|
+
});
|
|
121
|
+
const more = pages.length > max ? `\n …and ${pages.length - max} more` : '';
|
|
122
|
+
return lines.join('\n') + more;
|
|
123
|
+
}
|
|
124
|
+
|
package/mcp/server.mjs
CHANGED
|
@@ -20,6 +20,16 @@ import { registerAppResource, RESOURCE_MIME_TYPE } from '@modelcontextprotocol/e
|
|
|
20
20
|
import WebSocket from 'ws';
|
|
21
21
|
import { LAYERS } from '../src/shared/constants.mjs';
|
|
22
22
|
import { emptyExcalidrawScene, excalidrawSceneFromMermaid, stringifyExcalidrawScene } from '../src/shared/excalidraw.mjs';
|
|
23
|
+
import { createGateState, markSearched, g1Block, g2Block, g3Block, selectWithinBudget, wouldExceedBudget, budgetError, formatWikiIndex, PROJECT_CONTEXT_BUDGET_CHARS } from './gates.mjs';
|
|
24
|
+
|
|
25
|
+
// Frame actions that mutate content — gated by G1 (wiki search before editing).
|
|
26
|
+
// Read-style actions (read, search, versions, get_*/read_*) are exempt.
|
|
27
|
+
const G1_MUTATING_FRAME_ACTIONS = new Set([
|
|
28
|
+
'write', 'write_excalidraw', 'edit', 'mv', 'anchor', 'restore_version',
|
|
29
|
+
'write_sheet_values', 'append_sheet_rows', 'clear_sheet_range', 'update_sheet',
|
|
30
|
+
'write_doc_content', 'append_doc_content', 'clear_doc_content', 'update_doc',
|
|
31
|
+
'write_slide_content', 'append_slides', 'clear_slides', 'update_slide',
|
|
32
|
+
]);
|
|
23
33
|
const { UMAMI_EVENTS, trackUmamiEvent } = await (async () => {
|
|
24
34
|
try {
|
|
25
35
|
return await import('../server/lib/umami.mjs');
|
|
@@ -76,6 +86,7 @@ function getOrCreateSessionState(sid) {
|
|
|
76
86
|
activeProjectId: null,
|
|
77
87
|
activeProjectMeta: null,
|
|
78
88
|
loadedSkillIds: new Set(),
|
|
89
|
+
gates: createGateState(),
|
|
79
90
|
cachedOrgId: null,
|
|
80
91
|
cachedOrgIdTime: 0,
|
|
81
92
|
wsSessionId: null,
|
|
@@ -112,6 +123,23 @@ export function runWithRequestState(initial, fn) {
|
|
|
112
123
|
// Stripped from the advertised schema on remote transports.
|
|
113
124
|
const LOCAL_ONLY_PARAMS = ['file_path'];
|
|
114
125
|
|
|
126
|
+
// Per-tool params whose value is arbitrary / deeply-nested JSON. Zod renders
|
|
127
|
+
// these (z.any(), z.array(z.any()), z.object({}).passthrough()) as an UNTYPED
|
|
128
|
+
// schema — an empty `{}` or an array whose `items` has no `type`. ChatGPT's
|
|
129
|
+
// connector import runs OpenAI's strict Structured-Outputs JSON-Schema
|
|
130
|
+
// validator over the whole `tools/list` and rejects the ENTIRE connector if any
|
|
131
|
+
// tool contains an untyped subschema (Claude tolerates it; ChatGPT does not).
|
|
132
|
+
// On remote transports we advertise each of these as a single JSON-encoded
|
|
133
|
+
// string (always a valid typed schema) and parse it back to an object in the
|
|
134
|
+
// tool wrapper before the handler runs — so the stdio contract, the Zod runtime
|
|
135
|
+
// validation, and every handler stay byte-for-byte unchanged.
|
|
136
|
+
const REMOTE_JSON_STRING_PARAMS = {
|
|
137
|
+
frame: ['excalidraw_data', 'requests'],
|
|
138
|
+
wiki: ['frontmatter', 'pages'],
|
|
139
|
+
project: ['layers'],
|
|
140
|
+
template: ['layers'],
|
|
141
|
+
};
|
|
142
|
+
|
|
115
143
|
// Remove sentences that reference local-file params from a tool description,
|
|
116
144
|
// so remote-transport descriptions don't mention options that were stripped.
|
|
117
145
|
function scrubLocalPathMentions(description) {
|
|
@@ -290,6 +318,15 @@ function tool(name, descOrSchema, schemaOrHandler, handler) {
|
|
|
290
318
|
inputSchema[k] = field.describe(scrubLocalPathMentions(field.description));
|
|
291
319
|
}
|
|
292
320
|
}
|
|
321
|
+
// Present arbitrary-JSON params as a JSON string so the advertised schema is
|
|
322
|
+
// ChatGPT-valid (untyped `{}` subschemas fail OpenAI's strict validator and
|
|
323
|
+
// fail the whole connector). The wrapper below parses them back.
|
|
324
|
+
for (const k of (REMOTE_JSON_STRING_PARAMS[name] || [])) {
|
|
325
|
+
const field = inputSchema[k];
|
|
326
|
+
if (!field) continue;
|
|
327
|
+
const orig = field.description ? ` ${field.description}` : '';
|
|
328
|
+
inputSchema[k] = z.string().optional().describe(`JSON-encoded value — pass a JSON string.${orig}`);
|
|
329
|
+
}
|
|
293
330
|
description = scrubLocalPathMentions(description);
|
|
294
331
|
}
|
|
295
332
|
const config = {
|
|
@@ -316,6 +353,17 @@ function tool(name, descOrSchema, schemaOrHandler, handler) {
|
|
|
316
353
|
try {
|
|
317
354
|
const requiredUpdateError = await getRequiredMcpUpdateError(name, args?.[0] || {});
|
|
318
355
|
if (requiredUpdateError) return err(new Error(requiredUpdateError));
|
|
356
|
+
// Reverse the remote JSON-string encoding (see REMOTE_JSON_STRING_PARAMS)
|
|
357
|
+
// so handlers receive the same object/array shapes they get on stdio.
|
|
358
|
+
if (isRemote && args?.[0] && typeof args[0] === 'object') {
|
|
359
|
+
for (const k of (REMOTE_JSON_STRING_PARAMS[name] || [])) {
|
|
360
|
+
const v = args[0][k];
|
|
361
|
+
if (typeof v === 'string' && v.trim()) {
|
|
362
|
+
try { args[0][k] = JSON.parse(v); }
|
|
363
|
+
catch { return err(new Error(`Param "${k}" must be a JSON-encoded ${k === 'excalidraw_data' || k === 'frontmatter' ? 'object' : 'array'} string; JSON.parse failed.`)); }
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
319
367
|
return await cb(...args);
|
|
320
368
|
} finally {
|
|
321
369
|
state.currentTool = previousTool;
|
|
@@ -921,94 +969,10 @@ function err(error) {
|
|
|
921
969
|
return { content: [{ type: 'text', text: error.message || String(error) }], isError: true };
|
|
922
970
|
}
|
|
923
971
|
|
|
924
|
-
// ── Anchor enforcement ────────────────────────────────────────────
|
|
925
|
-
// Track which frames this session has fully read (no line range = full read)
|
|
926
|
-
const readFrameIds = new Set();
|
|
927
|
-
|
|
928
|
-
// Cache anchored frames (refreshed on each check)
|
|
929
|
-
let anchoredCache = null;
|
|
930
|
-
let anchoredCacheTime = 0;
|
|
931
|
-
|
|
932
|
-
async function getAnchoredFrames() {
|
|
933
|
-
// Cache for 10 seconds to avoid hammering the API
|
|
934
|
-
if (anchoredCache && Date.now() - anchoredCacheTime < 10000) return anchoredCache;
|
|
935
|
-
try {
|
|
936
|
-
anchoredCache = await api('GET', '/api/designs/anchored');
|
|
937
|
-
anchoredCacheTime = Date.now();
|
|
938
|
-
return anchoredCache;
|
|
939
|
-
} catch {
|
|
940
|
-
return [];
|
|
941
|
-
}
|
|
942
|
-
}
|
|
943
|
-
|
|
944
972
|
function parseLayer(path) {
|
|
945
973
|
return path.replace(/^\/+/, '').split('/')[0];
|
|
946
974
|
}
|
|
947
975
|
|
|
948
|
-
async function checkAnchors(layer) {
|
|
949
|
-
const anchored = await getAnchoredFrames();
|
|
950
|
-
if (!Array.isArray(anchored)) return null;
|
|
951
|
-
|
|
952
|
-
const layerAnchors = anchored.filter(f => f.layer === layer);
|
|
953
|
-
if (layerAnchors.length === 0) return null;
|
|
954
|
-
|
|
955
|
-
const unread = layerAnchors.filter(f => !readFrameIds.has(f.id));
|
|
956
|
-
if (unread.length === 0) return null;
|
|
957
|
-
|
|
958
|
-
const paths = unread.map(f => `/${f.layer}/${f.lane}/${f.label}`);
|
|
959
|
-
return `This layer has ${layerAnchors.length} anchored frame(s) that must be read before making changes. ` +
|
|
960
|
-
`Unread anchors:\n${paths.map(p => ' read path="' + p + '"').join('\n')}\n\n` +
|
|
961
|
-
`Read all anchored frames first, then retry your operation.`;
|
|
962
|
-
}
|
|
963
|
-
|
|
964
|
-
// ── Project skill enforcement ─────────────────────────────────────
|
|
965
|
-
// Skills attached to a project must be loaded by the agent before any
|
|
966
|
-
// mutating operation in that project. Same shape as anchor enforcement.
|
|
967
|
-
//
|
|
968
|
-
// Loaded-skill state is per-MCP-session (see getSessionState) so that
|
|
969
|
-
// (a) skill.load + wiki.write across separate HTTP requests stay coherent
|
|
970
|
-
// and (b) one user's loads never satisfy another user's gate.
|
|
971
|
-
const projectSkillsCache = new Map(); // projectId -> { data, time }
|
|
972
|
-
|
|
973
|
-
async function getProjectSkills(projectId) {
|
|
974
|
-
if (!projectId) return [];
|
|
975
|
-
const cached = projectSkillsCache.get(projectId);
|
|
976
|
-
if (cached && Date.now() - cached.time < 10000) return cached.data;
|
|
977
|
-
try {
|
|
978
|
-
const result = await api('GET', `/api/projects/${projectId}/skills`);
|
|
979
|
-
const skills = Array.isArray(result?.skills) ? result.skills : (Array.isArray(result) ? result : []);
|
|
980
|
-
projectSkillsCache.set(projectId, { data: skills, time: Date.now() });
|
|
981
|
-
return skills;
|
|
982
|
-
} catch {
|
|
983
|
-
return [];
|
|
984
|
-
}
|
|
985
|
-
}
|
|
986
|
-
|
|
987
|
-
function invalidateProjectSkillsCache(projectId) {
|
|
988
|
-
if (projectId) projectSkillsCache.delete(projectId);
|
|
989
|
-
}
|
|
990
|
-
|
|
991
|
-
async function checkProjectSkills(projectId, operation) {
|
|
992
|
-
if (!projectId) return null;
|
|
993
|
-
const skills = await getProjectSkills(projectId);
|
|
994
|
-
if (skills.length === 0) return null;
|
|
995
|
-
const loaded = getSessionState().loadedSkillIds;
|
|
996
|
-
const unloaded = skills.filter(s => !loaded.has(s.id));
|
|
997
|
-
if (unloaded.length === 0) return null;
|
|
998
|
-
|
|
999
|
-
// Fire-and-forget: tell the server so any open browser shows a toast.
|
|
1000
|
-
api('POST', `/api/projects/${projectId}/skill-gate-events`, {
|
|
1001
|
-
unloadedSkills: unloaded.map(s => ({ id: s.id, name: s.name, slug: s.slug })),
|
|
1002
|
-
operation: operation || 'mutation',
|
|
1003
|
-
agent: process.env.DRAFTED_AGENT_NAME || null,
|
|
1004
|
-
}).catch(() => { /* non-fatal */ });
|
|
1005
|
-
|
|
1006
|
-
const lines = unloaded.map(s => ` skill(action="load", skill="${s.slug || s.id}") -- ${s.name}`);
|
|
1007
|
-
return `This project has ${skills.length} attached skill(s) that must be loaded before making changes. ` +
|
|
1008
|
-
`Unloaded skills:\n${lines.join('\n')}\n\n` +
|
|
1009
|
-
`Load all attached skills first, then retry your operation. Skills tell you HOW to do the work — they're not optional.`;
|
|
1010
|
-
}
|
|
1011
|
-
|
|
1012
976
|
// ── Org skill enforcement ────────────────────────────────────────
|
|
1013
977
|
// Same shape as project skill enforcement but for org-attached skills.
|
|
1014
978
|
// Wiki mutations require org skills to be loaded first.
|
|
@@ -1043,6 +1007,42 @@ async function getOrgSkills(orgId) {
|
|
|
1043
1007
|
} catch { return []; }
|
|
1044
1008
|
}
|
|
1045
1009
|
|
|
1010
|
+
// Sum the per-project priming content (attached skill bodies + project anchor
|
|
1011
|
+
// bodies) so deposits can be capped against the per-project context budget (G4/G5).
|
|
1012
|
+
async function getProjectPrimingSize(projectId) {
|
|
1013
|
+
if (!projectId) return 0;
|
|
1014
|
+
let total = 0;
|
|
1015
|
+
try {
|
|
1016
|
+
const { skills = [] } = (await api('GET', `/api/projects/${projectId}/skills`)) || {};
|
|
1017
|
+
for (const s of skills) {
|
|
1018
|
+
try { const full = await api('GET', `/api/skills/${s.id}`); total += (full?.content || '').length; } catch { /* skip */ }
|
|
1019
|
+
}
|
|
1020
|
+
} catch { /* skills unavailable */ }
|
|
1021
|
+
try {
|
|
1022
|
+
const anchored = await api('GET', `/api/designs/anchored?projectId=${projectId}`);
|
|
1023
|
+
for (const a of (Array.isArray(anchored) ? anchored : [])) total += (a?.content || '').length;
|
|
1024
|
+
} catch { /* anchors unavailable */ }
|
|
1025
|
+
return total;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
// Best-effort size (chars) of a frame's content by path / URL / id — for the
|
|
1029
|
+
// anchor deposit cap. Returns 0 if it can't be read (cap then allows).
|
|
1030
|
+
async function getFrameContentSize(path) {
|
|
1031
|
+
try {
|
|
1032
|
+
const s = String(path);
|
|
1033
|
+
const frameId = s.match(/\/f\/([a-f0-9-]{36})/)?.[1] || (s.match(/^[a-f0-9-]{36}$/) ? s : null);
|
|
1034
|
+
let result;
|
|
1035
|
+
if (frameId) {
|
|
1036
|
+
result = await api('GET', `/api/fs/by-id/${frameId}`);
|
|
1037
|
+
} else {
|
|
1038
|
+
const parts = s.replace(/^\/+/, '').split('/');
|
|
1039
|
+
if (parts.length !== 3) return 0;
|
|
1040
|
+
result = await api('GET', `/api/fs/${parts[0]}/${parts[1]}/${parts[2]}`);
|
|
1041
|
+
}
|
|
1042
|
+
return (result?.content || '').length;
|
|
1043
|
+
} catch { return 0; }
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
1046
|
async function checkOrgSkills(orgId, operation) {
|
|
1047
1047
|
if (!orgId) return null;
|
|
1048
1048
|
const skills = await getOrgSkills(orgId);
|
|
@@ -1323,42 +1323,62 @@ tool('project', 'START HERE for project management. Dispatch by `action`: list (
|
|
|
1323
1323
|
exec(`${cmd} ${JSON.stringify(url)}`);
|
|
1324
1324
|
}
|
|
1325
1325
|
}
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1326
|
+
// G4/G5 auto-inject (locked design): the project's attached skills + anchors
|
|
1327
|
+
// are pushed into the open response within the per-project context budget,
|
|
1328
|
+
// replacing the reject-style gate. Prefer the server-computed `priming`
|
|
1329
|
+
// (authoritative + fresh on deploy); fall back to MCP-side assembly for
|
|
1330
|
+
// older servers that don't return it.
|
|
1331
|
+
let responseExtras;
|
|
1332
|
+
const priming = result && result.priming ? result.priming : null;
|
|
1333
|
+
if (priming) {
|
|
1334
|
+
const primedSkills = priming.skills || [];
|
|
1335
|
+
const primedAnchors = priming.anchors || [];
|
|
1336
|
+
for (const s of primedSkills) getSessionState().loadedSkillIds.add(s.id);
|
|
1337
|
+
responseExtras = { url, opened: true, navigated, skills: primedSkills, anchors: primedAnchors };
|
|
1338
|
+
if (priming.budgetNotice) responseExtras.budgetNotice = priming.budgetNotice;
|
|
1339
|
+
} else {
|
|
1340
|
+
let projectSkillsList = [];
|
|
1341
|
+
try {
|
|
1342
|
+
const skillData = await api('GET', `/api/projects/${projectId}/skills`);
|
|
1343
|
+
projectSkillsList = skillData.skills || [];
|
|
1344
|
+
} catch { /* skills not available yet */ }
|
|
1333
1345
|
for (const s of projectSkillsList) {
|
|
1334
1346
|
try {
|
|
1335
1347
|
const full = await api('GET', `/api/skills/${s.id}`);
|
|
1336
1348
|
s.content = full.content;
|
|
1337
1349
|
s.files = full.files || [];
|
|
1338
|
-
|
|
1339
|
-
// so the project-skill gate doesn't immediately demand a re-load.
|
|
1340
|
-
getSessionState().loadedSkillIds.add(s.id);
|
|
1341
|
-
} catch { /* skip */ }
|
|
1350
|
+
} catch { /* leaves this skill without inlined content */ }
|
|
1342
1351
|
}
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1352
|
+
let anchors = [];
|
|
1353
|
+
try {
|
|
1354
|
+
const anchored = await api('GET', `/api/designs/anchored?projectId=${projectId}`);
|
|
1355
|
+
anchors = (Array.isArray(anchored) ? anchored : []).map(a => ({
|
|
1356
|
+
id: a.id,
|
|
1357
|
+
path: a.path || `/${a.layer || ''}/${a.lane || ''}/${a.label || ''}`,
|
|
1358
|
+
layer: a.layer,
|
|
1359
|
+
content: a.content || '',
|
|
1360
|
+
}));
|
|
1361
|
+
} catch { /* anchors unavailable */ }
|
|
1362
|
+
const sel = selectWithinBudget([...projectSkillsList, ...anchors], PROJECT_CONTEXT_BUDGET_CHARS);
|
|
1363
|
+
for (const it of sel.included) {
|
|
1364
|
+
if (projectSkillsList.includes(it)) getSessionState().loadedSkillIds.add(it.id);
|
|
1365
|
+
}
|
|
1366
|
+
for (const it of sel.deferred) { it.content = undefined; if ('files' in it) it.files = undefined; }
|
|
1367
|
+
responseExtras = { url, opened: true, navigated, skills: projectSkillsList, anchors };
|
|
1368
|
+
if (sel.deferred.length > 0) {
|
|
1369
|
+
responseExtras.budgetNotice =
|
|
1370
|
+
`${sel.deferred.length} attached skill(s)/anchor(s) exceeded the per-project context budget ` +
|
|
1371
|
+
`(${PROJECT_CONTEXT_BUDGET_CHARS} chars) and were not inlined — load explicitly if needed: ` +
|
|
1372
|
+
sel.deferred.map(it => it.slug || it.path || it.id).join(', ');
|
|
1357
1373
|
}
|
|
1358
1374
|
}
|
|
1375
|
+
// Don't echo the raw priming blob (surfaced via skills/anchors/budgetNotice).
|
|
1376
|
+
if (result && typeof result === 'object') delete result.priming;
|
|
1359
1377
|
return ok({ ...result, ...responseExtras });
|
|
1360
1378
|
}
|
|
1361
1379
|
case 'create': {
|
|
1380
|
+
const g3 = g3Block(getSessionState().gates);
|
|
1381
|
+
if (g3) return err(new Error(g3));
|
|
1362
1382
|
const { name, description, templateSlug } = args;
|
|
1363
1383
|
if (!name) throw new Error('name required for action=create');
|
|
1364
1384
|
const body = { name };
|
|
@@ -1400,8 +1420,11 @@ tool('template', 'Manage project templates in the org. Dispatch by `action`: lis
|
|
|
1400
1420
|
try {
|
|
1401
1421
|
const { action } = args;
|
|
1402
1422
|
switch (action) {
|
|
1403
|
-
case 'list':
|
|
1404
|
-
|
|
1423
|
+
case 'list': {
|
|
1424
|
+
const tpls = await api('GET', '/api/templates');
|
|
1425
|
+
markSearched(getSessionState().gates, 'template');
|
|
1426
|
+
return ok(tpls);
|
|
1427
|
+
}
|
|
1405
1428
|
case 'create': {
|
|
1406
1429
|
const { name, description, layers, skillSlugs, visibility } = args;
|
|
1407
1430
|
if (!name || !description || !layers) throw new Error('name, description, layers required for action=create');
|
|
@@ -1536,8 +1559,6 @@ tool('layer', 'Manage layers in a project. Dispatch by `action`: add/update/remo
|
|
|
1536
1559
|
try {
|
|
1537
1560
|
const { action, projectId } = args;
|
|
1538
1561
|
if (!projectId) throw new Error('projectId is required');
|
|
1539
|
-
const skillErr = await checkProjectSkills(projectId);
|
|
1540
|
-
if (skillErr) return err(new Error(skillErr));
|
|
1541
1562
|
switch (action) {
|
|
1542
1563
|
case 'add': {
|
|
1543
1564
|
const { key, label, type, width, height, description, prompt } = args;
|
|
@@ -1849,11 +1870,11 @@ tool('frame', 'Frame CRUD in the ACTIVE PROJECT. Dispatch by `action`: read (by
|
|
|
1849
1870
|
bullets: z.array(z.string()).optional(),
|
|
1850
1871
|
speakerNotes: z.string().optional(),
|
|
1851
1872
|
layout: z.string().optional(),
|
|
1852
|
-
})
|
|
1873
|
+
})).optional().describe('[write_slide_content|append_slides] Structured slide spec: [{ title, bullets, speakerNotes?, layout? }].'),
|
|
1853
1874
|
requests: z.array(z.any()).optional().describe('[update_doc|update_slide] Raw Google Docs/Slides batchUpdate requests for advanced updates only; common Doc/Slide population should use write_doc_content/append_doc_content/write_slide_content/append_slides.'),
|
|
1854
1875
|
slideObjectIds: z.array(z.string()).optional().describe('[clear_slides] Optional slide object IDs to delete. Omit to clear all slides.'),
|
|
1855
1876
|
range: z.string().optional().describe('[Sheet value actions] A1 range, e.g. Sheet1!A1 or Data!A:Z.'),
|
|
1856
|
-
values: z.array(z.array(z.
|
|
1877
|
+
values: z.array(z.array(z.union([z.string(), z.number(), z.boolean(), z.null()]))).optional().describe('[write_sheet_values|append_sheet_rows] 2D array of row values (each cell: string, number, boolean, or null).'),
|
|
1857
1878
|
valueInputOption: z.enum(['RAW', 'USER_ENTERED']).optional().describe('[write_sheet_values|append_sheet_rows] Google Sheets value input option. Defaults to USER_ENTERED.'),
|
|
1858
1879
|
majorDimension: z.enum(['ROWS', 'COLUMNS']).optional().describe('[Sheet value actions] Major dimension for values. Defaults to ROWS when writing/appending.'),
|
|
1859
1880
|
valueRenderOption: z.enum(['FORMATTED_VALUE', 'UNFORMATTED_VALUE', 'FORMULA']).optional().describe('[read_sheet_values] How values should be rendered. Defaults to Google Sheets API default.'),
|
|
@@ -1884,6 +1905,16 @@ tool('frame', 'Frame CRUD in the ACTIVE PROJECT. Dispatch by `action`: read (by
|
|
|
1884
1905
|
}, async (args) => {
|
|
1885
1906
|
try {
|
|
1886
1907
|
const { action } = args;
|
|
1908
|
+
// G1: don't edit frame content before the session has searched the wiki.
|
|
1909
|
+
// When blocking, auto-inject the (bounded) wiki index so the search lands.
|
|
1910
|
+
if (G1_MUTATING_FRAME_ACTIONS.has(action)) {
|
|
1911
|
+
const gs = getSessionState().gates;
|
|
1912
|
+
if (!gs.wikiSearched) {
|
|
1913
|
+
let wikiIndex = '';
|
|
1914
|
+
try { const tree = await api('GET', '/api/wiki/tree'); wikiIndex = formatWikiIndex(tree?.pages || []); } catch { /* index best-effort */ }
|
|
1915
|
+
return err(new Error(g1Block(gs, wikiIndex)));
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1887
1918
|
// Echo project context on every mutation so the agent sees where the
|
|
1888
1919
|
// write landed before it can develop a wrong assumption.
|
|
1889
1920
|
const projectCtx = getCurrentProjectContext();
|
|
@@ -1924,9 +1955,6 @@ tool('frame', 'Frame CRUD in the ACTIVE PROJECT. Dispatch by `action`: read (by
|
|
|
1924
1955
|
if (parts.length !== 3) throw new Error('Path must be /{layer}/{lane}/{filename}, a frame URL, or a frame ID');
|
|
1925
1956
|
result = await api('GET', `/api/fs/${parts[0]}/${parts[1]}/${parts[2]}${query}`);
|
|
1926
1957
|
}
|
|
1927
|
-
if (!lines && result.ok && result.id) {
|
|
1928
|
-
readFrameIds.add(result.id);
|
|
1929
|
-
}
|
|
1930
1958
|
// Surface content as the visible text. Some Claude clients prefer
|
|
1931
1959
|
// structuredContent over text when both are present and structured looks
|
|
1932
1960
|
// "complete" — so include content in BOTH places to ensure the agent
|
|
@@ -2058,10 +2086,6 @@ tool('frame', 'Frame CRUD in the ACTIVE PROJECT. Dispatch by `action`: read (by
|
|
|
2058
2086
|
const writeSources = [content != null, !!file_path, base64 != null, !!googleType].filter(Boolean).length;
|
|
2059
2087
|
if (writeSources > 1) throw new Error('Provide only one of content, file_path, base64, or googleType');
|
|
2060
2088
|
if (writeSources === 0) throw new Error('Provide content, file_path, base64, or googleType');
|
|
2061
|
-
const skillErr = await checkProjectSkills(getState().projectId);
|
|
2062
|
-
if (skillErr) return err(new Error(skillErr));
|
|
2063
|
-
const anchorErr = await checkAnchors(parseLayer(path));
|
|
2064
|
-
if (anchorErr) return err(new Error(anchorErr));
|
|
2065
2089
|
const parts = path.replace(/^\/+/, '').split('/');
|
|
2066
2090
|
if (parts.length !== 3) throw new Error('Path must be /{layer}/{lane}/{filename}');
|
|
2067
2091
|
if (googleType) {
|
|
@@ -2123,10 +2147,6 @@ tool('frame', 'Frame CRUD in the ACTIVE PROJECT. Dispatch by `action`: read (by
|
|
|
2123
2147
|
const { path, excalidraw_data, mermaid, width, height, color } = args;
|
|
2124
2148
|
if (!path) throw new Error('path required for action=write_excalidraw');
|
|
2125
2149
|
if (excalidraw_data != null && mermaid) throw new Error('Provide only one of excalidraw_data or mermaid');
|
|
2126
|
-
const skillErr = await checkProjectSkills(getState().projectId);
|
|
2127
|
-
if (skillErr) return err(new Error(skillErr));
|
|
2128
|
-
const anchorErr = await checkAnchors(parseLayer(path));
|
|
2129
|
-
if (anchorErr) return err(new Error(anchorErr));
|
|
2130
2150
|
const parts = path.replace(/^\/+/, '').split('/');
|
|
2131
2151
|
if (parts.length !== 3) throw new Error('Path must be /{layer}/{lane}/{filename}');
|
|
2132
2152
|
const filename = parts[2].toLowerCase().endsWith('.excalidraw') ? parts[2] : parts[2] + '.excalidraw';
|
|
@@ -2145,10 +2165,6 @@ tool('frame', 'Frame CRUD in the ACTIVE PROJECT. Dispatch by `action`: read (by
|
|
|
2145
2165
|
const { path, operations } = args;
|
|
2146
2166
|
if (!path) throw new Error('path required for action=edit');
|
|
2147
2167
|
if (!Array.isArray(operations) || operations.length === 0) throw new Error('operations (array) required for action=edit');
|
|
2148
|
-
const skillErr = await checkProjectSkills(getState().projectId);
|
|
2149
|
-
if (skillErr) return err(new Error(skillErr));
|
|
2150
|
-
const anchorErr = await checkAnchors(parseLayer(path));
|
|
2151
|
-
if (anchorErr) return err(new Error(anchorErr));
|
|
2152
2168
|
const result = await api('POST', '/api/fs/edit', { path, operations });
|
|
2153
2169
|
return ok(withProject(result), {
|
|
2154
2170
|
structuredContent: frameStructuredContent(result, projectCtx),
|
|
@@ -2170,20 +2186,19 @@ tool('frame', 'Frame CRUD in the ACTIVE PROJECT. Dispatch by `action`: read (by
|
|
|
2170
2186
|
note: 'No changes applied. Re-call with dryRun=false (or omit) to perform the rename.',
|
|
2171
2187
|
}));
|
|
2172
2188
|
}
|
|
2173
|
-
const skillErr = await checkProjectSkills(getState().projectId);
|
|
2174
|
-
if (skillErr) return err(new Error(skillErr));
|
|
2175
|
-
const fromErr = await checkAnchors(parseLayer(from));
|
|
2176
|
-
if (fromErr) return err(new Error(fromErr));
|
|
2177
|
-
const toErr = await checkAnchors(parseLayer(to));
|
|
2178
|
-
if (toErr) return err(new Error(toErr));
|
|
2179
2189
|
return ok(withProject(await api('POST', '/api/fs/mv', { from, to })));
|
|
2180
2190
|
}
|
|
2181
2191
|
case 'anchor': {
|
|
2182
2192
|
const { path, anchored } = args;
|
|
2183
2193
|
if (!path) throw new Error('path required for action=anchor');
|
|
2184
2194
|
if (typeof anchored !== 'boolean') throw new Error('anchored (boolean) required for action=anchor');
|
|
2185
|
-
|
|
2186
|
-
if
|
|
2195
|
+
// Cap (G5): anchoring adds the frame's body to the project's required-reading
|
|
2196
|
+
// set — reject if it would push the project past the per-project budget.
|
|
2197
|
+
if (anchored === true) {
|
|
2198
|
+
const used = await getProjectPrimingSize(getState().projectId);
|
|
2199
|
+
const addSize = await getFrameContentSize(path);
|
|
2200
|
+
if (wouldExceedBudget(used, addSize)) return err(new Error(budgetError(used, addSize, 'this anchored frame')));
|
|
2201
|
+
}
|
|
2187
2202
|
return ok(withProject(await api('POST', '/api/fs/anchor', { path, anchored })));
|
|
2188
2203
|
}
|
|
2189
2204
|
|
|
@@ -2202,8 +2217,6 @@ tool('frame', 'Frame CRUD in the ACTIVE PROJECT. Dispatch by `action`: read (by
|
|
|
2202
2217
|
case 'restore_version': {
|
|
2203
2218
|
const { versionId, reason } = args;
|
|
2204
2219
|
if (!versionId) throw new Error('versionId required for action=restore_version');
|
|
2205
|
-
const skillErr = await checkProjectSkills(getState().projectId);
|
|
2206
|
-
if (skillErr) return err(new Error(skillErr));
|
|
2207
2220
|
const result = await api('POST', '/api/fs/restore-version', { versionId, reason });
|
|
2208
2221
|
return ok(withProject(result));
|
|
2209
2222
|
}
|
|
@@ -2297,10 +2310,6 @@ tool('rm', 'Delete a frame or lane from the ACTIVE PROJECT. Response includes "p
|
|
|
2297
2310
|
path: z.string().describe('Path to delete: /{layer}/{lane}/{filename} or /{layer}/{lane} (deletes entire lane).'),
|
|
2298
2311
|
}, async ({ path }) => {
|
|
2299
2312
|
try {
|
|
2300
|
-
const skillErr = await checkProjectSkills(getState().projectId);
|
|
2301
|
-
if (skillErr) return err(new Error(skillErr));
|
|
2302
|
-
const anchorErr = await checkAnchors(parseLayer(path));
|
|
2303
|
-
if (anchorErr) return err(new Error(anchorErr));
|
|
2304
2313
|
const clean = path.replace(/^\/+|\/+$/g, '');
|
|
2305
2314
|
const result = await api('DELETE', `/api/fs/${clean}`);
|
|
2306
2315
|
return ok({ ...result, project: getCurrentProjectContext() });
|
|
@@ -2338,8 +2347,6 @@ tool('batch', 'Batch operations on the ACTIVE PROJECT. Response includes "projec
|
|
|
2338
2347
|
if (op.to) layers.add(parseLayer(op.to));
|
|
2339
2348
|
}
|
|
2340
2349
|
for (const layer of layers) {
|
|
2341
|
-
const anchorErr = await checkAnchors(layer);
|
|
2342
|
-
if (anchorErr) return err(new Error(anchorErr));
|
|
2343
2350
|
}
|
|
2344
2351
|
|
|
2345
2352
|
// Resolve file_path → base64 for write operations before sending to server
|
|
@@ -2429,8 +2436,6 @@ tool('asset', 'Manage supporting files (CSS, JS, images, fonts) in the ACTIVE PR
|
|
|
2429
2436
|
const projectId = getState().projectId;
|
|
2430
2437
|
if (!projectId) throw new Error('No active project. Call project(action="open") first.');
|
|
2431
2438
|
if (action === 'upload') {
|
|
2432
|
-
const skillErr = await checkProjectSkills(projectId);
|
|
2433
|
-
if (skillErr) return err(new Error(skillErr));
|
|
2434
2439
|
const { asset_path, file_path, content, base64: rawBase64, content_type, frame_id } = args;
|
|
2435
2440
|
if (!asset_path) throw new Error('asset_path is required for action=upload');
|
|
2436
2441
|
if (asset_path.includes('..')) throw new Error('asset_path must not contain ".."');
|
|
@@ -2460,8 +2465,6 @@ tool('asset', 'Manage supporting files (CSS, JS, images, fonts) in the ACTIVE PR
|
|
|
2460
2465
|
return ok(await api('GET', `/api/projects/${projectId}/assets${query}`));
|
|
2461
2466
|
}
|
|
2462
2467
|
if (action === 'rm') {
|
|
2463
|
-
const skillErr = await checkProjectSkills(projectId);
|
|
2464
|
-
if (skillErr) return err(new Error(skillErr));
|
|
2465
2468
|
const { asset_path } = args;
|
|
2466
2469
|
if (!asset_path) throw new Error('asset_path is required for action=rm');
|
|
2467
2470
|
if (asset_path.includes('..')) throw new Error('asset_path must not contain ".."');
|
|
@@ -2487,8 +2490,6 @@ tool('shape', 'Create a flowchart shape on the surface. Shapes are lightweight t
|
|
|
2487
2490
|
layoutIgnore: z.boolean().optional().describe('Exclude this shape from auto-layout. Use for annotations, labels, or manually positioned elements.'),
|
|
2488
2491
|
}, async ({ text, shape, layer, lane, color, fill, textColor, group, width, height, layoutIgnore }) => {
|
|
2489
2492
|
try {
|
|
2490
|
-
const skillErr = await checkProjectSkills(getState().projectId);
|
|
2491
|
-
if (skillErr) return err(new Error(skillErr));
|
|
2492
2493
|
const body = { text, shape };
|
|
2493
2494
|
if (layer) body.layer = layer;
|
|
2494
2495
|
if (lane) body.lane = lane;
|
|
@@ -2513,8 +2514,6 @@ tool('group', 'Create a group (swim lane / region) on the surface. Groups are la
|
|
|
2513
2514
|
order: z.number().optional().describe('Sort order for group positioning (lower = first). Default: 0'),
|
|
2514
2515
|
}, async ({ label, color, fill, layer, lane, order }) => {
|
|
2515
2516
|
try {
|
|
2516
|
-
const skillErr = await checkProjectSkills(getState().projectId);
|
|
2517
|
-
if (skillErr) return err(new Error(skillErr));
|
|
2518
2517
|
const body = { label };
|
|
2519
2518
|
if (color) body.color = color;
|
|
2520
2519
|
if (fill) body.fill = fill;
|
|
@@ -2537,8 +2536,6 @@ tool('connector', 'Create or remove connectors (arrows) between frames on the su
|
|
|
2537
2536
|
connectorId: z.string().optional().describe('[disconnect] connector ID to delete directly'),
|
|
2538
2537
|
}, async (args) => {
|
|
2539
2538
|
try {
|
|
2540
|
-
const skillErr = await checkProjectSkills(getState().projectId);
|
|
2541
|
-
if (skillErr) return err(new Error(skillErr));
|
|
2542
2539
|
const { action } = args;
|
|
2543
2540
|
if (action === 'connect') {
|
|
2544
2541
|
const { source, target, label, type = 'arrow-forward', color } = args;
|
|
@@ -2580,8 +2577,6 @@ tool('layout', 'Auto-arrange frames using graph layout algorithm. Positions conn
|
|
|
2580
2577
|
groups: z.boolean().optional().default(false).describe('Enable group clustering. When true, shapes assigned to a group (via the group param) are clustered together, and group frames are resized to enclose their members.'),
|
|
2581
2578
|
}, async ({ direction, groups }) => {
|
|
2582
2579
|
try {
|
|
2583
|
-
const skillErr = await checkProjectSkills(getState().projectId);
|
|
2584
|
-
if (skillErr) return err(new Error(skillErr));
|
|
2585
2580
|
const body = { direction };
|
|
2586
2581
|
if (groups) body.groups = true;
|
|
2587
2582
|
const result = await api('POST', '/api/layout', body);
|
|
@@ -2624,6 +2619,7 @@ tool('skill', 'Manage the Drafted skill library. Skills are reusable prompts/gui
|
|
|
2624
2619
|
const qs = params.toString();
|
|
2625
2620
|
const endpoint = query ? '/api/skills/search' : '/api/skills';
|
|
2626
2621
|
const result = await api('GET', `${endpoint}${qs ? '?' + qs : ''}`);
|
|
2622
|
+
markSearched(getSessionState().gates, 'skill');
|
|
2627
2623
|
const cap = Math.min(Math.max(1, limit || 25), 100);
|
|
2628
2624
|
if (Array.isArray(result?.skills)) {
|
|
2629
2625
|
if (result.skills.length > cap) {
|
|
@@ -2679,6 +2675,8 @@ tool('skill', 'Manage the Drafted skill library. Skills are reusable prompts/gui
|
|
|
2679
2675
|
return ok(await api('GET', `/api/orgs/${orgId}/skills`));
|
|
2680
2676
|
}
|
|
2681
2677
|
case 'add': {
|
|
2678
|
+
const g2 = g2Block(getSessionState().gates);
|
|
2679
|
+
if (g2) return err(new Error(g2));
|
|
2682
2680
|
const { name, description, content, tags, triggerPatterns } = args;
|
|
2683
2681
|
if (!name || !description || !content) throw new Error('name, description, content required for action=add');
|
|
2684
2682
|
const body = { name, description, content };
|
|
@@ -2707,8 +2705,15 @@ tool('skill', 'Manage the Drafted skill library. Skills are reusable prompts/gui
|
|
|
2707
2705
|
const { skillId } = args;
|
|
2708
2706
|
if (!skillId) throw new Error('skillId required for action=attach');
|
|
2709
2707
|
if (!getState().projectId) throw new Error('No active project. Call project(action="open") first.');
|
|
2708
|
+
// Cap (G4): keep attached skills + anchors within the per-project budget so
|
|
2709
|
+
// the auto-inject set always fits — reject the attach if it would overflow.
|
|
2710
|
+
try {
|
|
2711
|
+
const used = await getProjectPrimingSize(getState().projectId);
|
|
2712
|
+
let addSize = 0;
|
|
2713
|
+
try { const full = await api('GET', `/api/skills/${skillId}`); addSize = (full?.content || '').length; } catch { /* unknown size */ }
|
|
2714
|
+
if (wouldExceedBudget(used, addSize)) return err(new Error(budgetError(used, addSize, 'this skill')));
|
|
2715
|
+
} catch { /* size check is best-effort */ }
|
|
2710
2716
|
const result = await api('POST', `/api/projects/${getState().projectId}/skills`, { skillId });
|
|
2711
|
-
invalidateProjectSkillsCache(getState().projectId);
|
|
2712
2717
|
return ok(result);
|
|
2713
2718
|
}
|
|
2714
2719
|
case 'detach': {
|
|
@@ -2716,7 +2721,6 @@ tool('skill', 'Manage the Drafted skill library. Skills are reusable prompts/gui
|
|
|
2716
2721
|
if (!skillId) throw new Error('skillId required for action=detach');
|
|
2717
2722
|
if (!getState().projectId) throw new Error('No active project. Call project(action="open") first.');
|
|
2718
2723
|
const result = await api('DELETE', `/api/projects/${getState().projectId}/skills/${skillId}`);
|
|
2719
|
-
invalidateProjectSkillsCache(getState().projectId);
|
|
2720
2724
|
return ok(result);
|
|
2721
2725
|
}
|
|
2722
2726
|
case 'favorite': {
|
|
@@ -2911,6 +2915,7 @@ tool('wiki', 'Per-org wiki. Markdown pages with paths as hierarchy. You and othe
|
|
|
2911
2915
|
if (!searchQuery) throw new Error('query required for action=search');
|
|
2912
2916
|
const result = await api('GET', `/api/wiki/search?q=${encodeURIComponent(searchQuery)}&limit=${searchLimit}`);
|
|
2913
2917
|
const hits = (result.hits || []).map(h => ({ path: h.path, title: h.title, url: wikiBrowserUrl(h.path) }));
|
|
2918
|
+
markSearched(getSessionState().gates, 'wiki');
|
|
2914
2919
|
return ok({ hits });
|
|
2915
2920
|
}
|
|
2916
2921
|
|
|
@@ -2989,10 +2994,6 @@ tool('wiki', 'Per-org wiki. Markdown pages with paths as hierarchy. You and othe
|
|
|
2989
2994
|
const { path, excalidraw_data, mermaid, width, height, color } = args;
|
|
2990
2995
|
if (!path) throw new Error('path required for action=write_excalidraw');
|
|
2991
2996
|
if (excalidraw_data != null && mermaid) throw new Error('Provide only one of excalidraw_data or mermaid');
|
|
2992
|
-
const skillErr = await checkProjectSkills(getState().projectId);
|
|
2993
|
-
if (skillErr) return err(new Error(skillErr));
|
|
2994
|
-
const anchorErr = await checkAnchors(parseLayer(path));
|
|
2995
|
-
if (anchorErr) return err(new Error(anchorErr));
|
|
2996
2997
|
const parts = path.replace(/^\/+/, '').split('/');
|
|
2997
2998
|
if (parts.length !== 3) throw new Error('Path must be /{layer}/{lane}/{filename}');
|
|
2998
2999
|
const filename = parts[2].toLowerCase().endsWith('.excalidraw') ? parts[2] : parts[2] + '.excalidraw';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "drafted",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.1",
|
|
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": [
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
"server/lib/umami.mjs",
|
|
10
10
|
"src/shared/",
|
|
11
11
|
"agent-instructions/",
|
|
12
|
+
"plugin/skills/",
|
|
13
|
+
"plugin/commands/",
|
|
12
14
|
"install-mcp.sh"
|
|
13
15
|
],
|
|
14
16
|
"bin": {
|
|
@@ -28,9 +30,9 @@
|
|
|
28
30
|
"import:legacy": "tsx scripts/import-legacy.mts",
|
|
29
31
|
"test": "vitest run",
|
|
30
32
|
"test:watch": "vitest",
|
|
31
|
-
"version": "node scripts/sync-versions.mjs && git add plugin/.claude-plugin/plugin.json .claude-plugin/marketplace.json",
|
|
33
|
+
"version": "node scripts/sync-versions.mjs && git add plugin/.claude-plugin/plugin.json .claude-plugin/marketplace.json web-plugin/.claude-plugin/plugin.json web-plugin/.claude-plugin/marketplace.json",
|
|
32
34
|
"version:check": "node scripts/sync-versions.mjs",
|
|
33
|
-
"postpublish": "bash scripts/sync-plugin.sh \"chore: sync plugin to v$npm_package_version\"",
|
|
35
|
+
"postpublish": "bash scripts/sync-plugin.sh \"chore: sync plugin to v$npm_package_version\" && bash scripts/sync-web-plugin.sh \"chore: sync web-plugin to v$npm_package_version\"",
|
|
34
36
|
"deploy:check:google": "node scripts/check-google-drive-deploy.mjs",
|
|
35
37
|
"build:excalidraw": "node scripts/build-excalidraw-editor.mjs",
|
|
36
38
|
"prepublishOnly": "node scripts/check-npm-package.mjs"
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Start a new project — or a reusable template — with proper search of knowledge, skills, and templates first
|
|
3
|
+
argument-hint: <project name and what you want to do>
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Spin up a project on the surface, or generalize one into a reusable template. Goal: $ARGUMENTS
|
|
7
|
+
|
|
8
|
+
First ask: **a new project, or a reusable template?** (This command does both.)
|
|
9
|
+
|
|
10
|
+
Search before creating (also satisfies the gates):
|
|
11
|
+
1. `wiki(action="search")` for relevant knowledge. [G1]
|
|
12
|
+
2. `skill(action="search")` for procedures that should be attached.
|
|
13
|
+
3. `template(action="list")` for an existing template to fork. [G3 — also enforced on `project(action="create")`.]
|
|
14
|
+
|
|
15
|
+
Then:
|
|
16
|
+
- If a fitting template exists → `project(action="create", templateSlug=...)` to fork it; otherwise create fresh and define the layers.
|
|
17
|
+
- `project(action="open")` the new project.
|
|
18
|
+
- `frame(action="write")` a real brief at the earliest layer (goal, audience, constraints — 6-12 lines, not a placeholder).
|
|
19
|
+
- `frame(action="anchor")` the brief so downstream work surfaces it.
|
|
20
|
+
- Attach the relevant skills you found with `skill(action="attach")`.
|
|
21
|
+
- `focus` the brief so the user watches it land.
|
|
22
|
+
|
|
23
|
+
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.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Capture a repeatable procedure as an org skill — with proper knowledge and prior-art search
|
|
3
|
+
argument-hint: <what the procedure is for>
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Turn a repeatable way of working into a Drafted skill so every future agent in the org follows it. Procedure: $ARGUMENTS
|
|
7
|
+
|
|
8
|
+
Do the searches first (they also satisfy the gates):
|
|
9
|
+
1. `wiki(action="search")` for relevant org knowledge the procedure should reference. [G1]
|
|
10
|
+
2. `skill(action="search")` for prior art — an existing skill to improve instead of duplicating. [G2 — also enforced on `skill(action="add")`.] If a close match exists, prefer `/drafted:improve-skill`.
|
|
11
|
+
|
|
12
|
+
Then define a PROPER procedure, not a vague note:
|
|
13
|
+
- a clear trigger ("when to use this"),
|
|
14
|
+
- ordered, concrete steps,
|
|
15
|
+
- success criteria / what "done right" looks like,
|
|
16
|
+
- written in the second person so any agent can follow it directly. Keep it sharp (~40 lines).
|
|
17
|
+
|
|
18
|
+
Show the draft to the user with a proposed `name` (Title Case), one-line `description`, `tags` (3-5), and `triggerPatterns`. After approval, `skill(action="add")`. Confirm with the slug and note it will auto-surface on matching tasks.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Deposit what this session produced into the harness — knowledge, a skill, and/or a template
|
|
3
|
+
argument-hint: <optional: what to capture>
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Session-end deposit. Harvest what's durable from this conversation back into the harness so the next session starts smarter. Focus: $ARGUMENTS
|
|
7
|
+
|
|
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
|
+
|
|
10
|
+
- **Knowledge → wiki** — durable facts, decisions, or findings worth keeping. Search first (`wiki(action="search")`) to avoid fragmenting, then `wiki(action="write")`.
|
|
11
|
+
- **Procedure → skill** — a repeatable way of working that emerged. Follow `/drafted:create-skill`.
|
|
12
|
+
- **Template → surface** — a reusable project structure that emerged. `template(action="create")` from the current project.
|
|
13
|
+
|
|
14
|
+
Show the user the candidate deposits per store, let them pick which to keep, then write the chosen ones with their approval. Confirm what landed where.
|
|
15
|
+
|
|
16
|
+
One command, all three stores — because at session end the human shouldn't have to remember which store is which.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Turn corrections you made in a project into enforced gates (anchors, attached skills, layer rules)
|
|
3
|
+
argument-hint: <optional: the correction to enforce>
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
When the user had to correct the work in this project, codify the correction so future work is compelled to comply. This is the nurture → enforce bridge. Correction: $ARGUMENTS
|
|
7
|
+
|
|
8
|
+
1. Identify the recurring correction(s) or standing rule from this session.
|
|
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: `frame(action="write")` it, then `frame(action="anchor")`. [G5]
|
|
11
|
+
- **Attached skill** — a procedure that must be loaded before work: `/drafted:create-skill` (or `skill(action="search")` for an existing one), then `skill(action="attach")`. [G4]
|
|
12
|
+
- **Layer rule** — a standing instruction for one stage: set that layer's rules via `project(action="update", layers=...)`. [G6]
|
|
13
|
+
3. Propose which mechanism for each correction, confirm with the user, then apply.
|
|
14
|
+
4. Confirm what is now enforced — the next session in this project will be gated on it automatically.
|
|
15
|
+
|
|
16
|
+
Keep within the project's context budget — if a deposit would exceed it, tighten existing gates instead of piling on.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Fix a skill you noticed is inefficient, wrong, or missing a step
|
|
3
|
+
argument-hint: <which skill, and what's off>
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Improve an existing org skill when it underperformed in practice. Skill / issue: $ARGUMENTS
|
|
7
|
+
|
|
8
|
+
1. `skill(action="search")` then `skill(action="load")` the skill in question — read it fully.
|
|
9
|
+
2. Pinpoint the inefficiency: a missing step, a wrong instruction, an ambiguous trigger, or a step that wastes effort.
|
|
10
|
+
3. Propose the specific edit to the user — show before/after of the changed steps.
|
|
11
|
+
4. After approval, `skill(action="update")` (the version bumps automatically).
|
|
12
|
+
5. Confirm what changed so the next agent benefits.
|
|
13
|
+
|
|
14
|
+
Skills are the org's stable processes — every fix compounds across everyone who uses them.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Fix the wiki — reconcile stale, wrong, fragmented, or contradictory knowledge
|
|
3
|
+
argument-hint: <what's wrong, or the topic to clean up>
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Improve the org wiki when knowledge has drifted. Issue/topic: $ARGUMENTS
|
|
7
|
+
|
|
8
|
+
1. `wiki(action="search")` (3-5 paraphrased queries) and `wiki(action="read")` the affected pages — don't trust titles, open them.
|
|
9
|
+
2. Diagnose: duplicate pages, a stale fact, a contradiction, or knowledge fragmented across pages.
|
|
10
|
+
3. Propose the fix to the user: consolidate duplicates, correct the fact, reconcile the contradiction, or re-link fragments.
|
|
11
|
+
4. Apply with `wiki(action="edit")` (hashline) or `wiki(action="mv")` (which rewrites inbound links). Check `wiki(action="links")` before moving or deleting.
|
|
12
|
+
5. `wiki(action="log")` what changed.
|
|
13
|
+
|
|
14
|
+
Leave the wiki more coherent than you found it — fewer, sharper, better-linked pages.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Bring knowledge into the wiki — from a research output, existing documents, or by interrogating you
|
|
3
|
+
argument-hint: <optional: what to ingest, or a source>
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Feed durable knowledge into the org wiki. Source/intent: $ARGUMENTS
|
|
7
|
+
|
|
8
|
+
First decide WHAT to ingest. If it's obvious from the conversation (a research run just finished, or the user named a source), use that. Otherwise ask which of these three modes fits:
|
|
9
|
+
|
|
10
|
+
1. **A research producible** — the output of a research run (a provider-native `/deep-research` or similar). Structure it into wiki pages.
|
|
11
|
+
2. **Existing documents** — the user points at local / remote / connected folders. Scour them (spawn sub-agents if there are many) for relevant material, shortlist, confirm, then ingest the right ones. Folder scour needs file access (desktop / Cowork); on web, use connected sources.
|
|
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
|
+
|
|
14
|
+
Then deposit:
|
|
15
|
+
- `wiki(action="search")` first (3-5 paraphrased queries) so you don't fragment existing pages.
|
|
16
|
+
- Propose the page set to the user before writing.
|
|
17
|
+
- Write with `wiki(action="write")`, or `wiki(action="source-register")` + `wiki(action="bulk-write")` for a batch. Cross-link related pages.
|
|
18
|
+
- `wiki(action="log")` a one-line entry for the ingest.
|
|
19
|
+
|
|
20
|
+
The wiki is the org's durable knowledge — write for the next agent and teammate, not just this session.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: First-run setup — orient to Drafted and bootstrap the harness (wiki, skills, first project)
|
|
3
|
+
argument-hint: <optional: what you want to start working on>
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Onboard the user to Drafted — a producibles harness that compounds across three primitives: knowledge (the wiki), procedures (skills), and the project surface. Goal/context: $ARGUMENTS
|
|
7
|
+
|
|
8
|
+
This is the over-arching prime command: orient, then seed all three stores so every later session starts smart.
|
|
9
|
+
|
|
10
|
+
1. **Orient** — in 3-4 lines explain the loop: you *prime* from the harness (the system makes you search the wiki, and auto-loads the project's attached skills, anchors, and layer rules before you work), you *build*, then you *compound* (deposit what you learned). The more it's used, the less searching and the more stable the work.
|
|
11
|
+
2. **Confirm the org** — call `get_org`. Confirm the active organization is the one to build the harness in. If unsure, ask.
|
|
12
|
+
3. **Seed knowledge** — run the `/drafted:ingest` flow: help the user point at existing business materials (folders, docs, past research) or interrogate them for tacit knowledge, and land durable pages in the wiki.
|
|
13
|
+
4. **Seed procedures** — from those materials and the conversation, surface 1-3 candidate SOPs. For the most valuable, run the `/drafted:create-skill` flow.
|
|
14
|
+
5. **Seed the surface** — run the `/drafted:create-project` flow for the user's immediate piece of work (or a reusable template).
|
|
15
|
+
6. Close by showing what now exists (wiki pages, skills, project) and restate the one-line loop: prime → build → compound.
|
|
16
|
+
|
|
17
|
+
Keep it guided and conversational — one step at a time, confirming before each deposit. Don't dump everything at once.
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: drafted
|
|
3
|
+
description: Use the Drafted producibles harness — a compounding workspace that uplifts any AI across three primitives: knowledge (the org wiki), procedures (skills), and the project surface (frames on a shared real-time canvas). Prime from the harness before working, build durable artifacts as frames instead of burying output in chat, and deposit what you learned back so the next session starts smarter. When Google Drive is connected, prefer Google Workspace frames for docs, sheets, and slides.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Drafted — a producibles harness that compounds
|
|
7
|
+
|
|
8
|
+
Drafted makes any AI more effective by giving it a memory and a workspace that get better with use. It has three primitives that layer:
|
|
9
|
+
|
|
10
|
+
- **Knowledge — the org wiki.** Durable facts, decisions, conventions. *More knowledge = less searching.*
|
|
11
|
+
- **Procedures — skills.** Reusable SOPs the org has encoded. *Better skills = more stable process.*
|
|
12
|
+
- **Surface — projects.** Reviewable work as frames on a shared zoomable canvas the user watches live at `https://drafted.live`. *Reusable projects + anchors = velocity with accuracy.*
|
|
13
|
+
|
|
14
|
+
The point is the **compounding loop**: you don't produce in a vacuum, you draw on what the org already knows and leave it richer each pass.
|
|
15
|
+
|
|
16
|
+
## The loop — prime → build → compound
|
|
17
|
+
|
|
18
|
+
- **Prime (session start).** Pull accumulated value in. The system *enforces* this: you must search the wiki before working, and a project's attached skills, anchored frames, and layer rules are auto-loaded when you open it. Don't fight the gates — they make you start smart.
|
|
19
|
+
- **Build (the work).** Produce artifacts as frames on the surface.
|
|
20
|
+
- **Compound (session end / when you notice something).** Deposit learning back: capture knowledge, distill or fix a skill, harden the project. This is *your* responsibility — the system can't force it, so do it.
|
|
21
|
+
|
|
22
|
+
## Mental model
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
Organization
|
|
26
|
+
└── Project (a bounded piece of work, e.g. "Q4 strategy memo")
|
|
27
|
+
└── Layer (a stage of thinking, e.g. research → drafts → final)
|
|
28
|
+
└── Lane (a group of related frames, e.g. one competitor per lane)
|
|
29
|
+
└── Frame (an HTML / markdown / image file)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
**Layers are stages** — they depend on the project's template. `project(action="create")` returns the active layers; always check rather than assume. **Frames have addresses** like `/layer/lane/filename`; the canvas auto-arranges them by layer (vertical) and lane (horizontal).
|
|
33
|
+
|
|
34
|
+
## The gates you'll encounter (and how to satisfy them)
|
|
35
|
+
|
|
36
|
+
These reset every session. A gate that blocks you tells you exactly what to call next — do it, don't work around it.
|
|
37
|
+
|
|
38
|
+
- **G1 — wiki search before work.** Before you read or edit anything, `wiki(action="search")` for relevant org knowledge.
|
|
39
|
+
- **G2 — prior-art before a new skill.** `skill(action="add")` requires a `skill(action="search")` first.
|
|
40
|
+
- **G3 — prior-art before a new project.** `project(action="create")` requires wiki + skill + `template(action="list")` searches first.
|
|
41
|
+
- **G4 — attached skills** are auto-injected when you open a project. Follow them — they're how the org does this work.
|
|
42
|
+
- **G5 — the project's anchored frames** are auto-injected on open. They are required reading (briefs, constraints, style guides).
|
|
43
|
+
- **G6 — a layer's rules** are surfaced when you work in that layer. Honor them.
|
|
44
|
+
|
|
45
|
+
## The commands (when to reach for each)
|
|
46
|
+
|
|
47
|
+
These bookend the loop. Prime/feed at the start, deposit at the end.
|
|
48
|
+
|
|
49
|
+
- `/drafted:onboard-drafted` — first run: orient + bootstrap the harness (seed wiki, starter skills, first project).
|
|
50
|
+
- `/drafted:ingest` — bring knowledge into the wiki (a research output, existing documents, or by interrogating the user).
|
|
51
|
+
- `/drafted:create-skill` — capture a repeatable procedure (with knowledge + prior-art search).
|
|
52
|
+
- `/drafted:create-project` — start a project or a reusable template (with knowledge/skill/template search).
|
|
53
|
+
- `/drafted:improve-wiki` — fix stale / wrong / fragmented knowledge.
|
|
54
|
+
- `/drafted:improve-skill` — fix a skill that underperformed.
|
|
55
|
+
- `/drafted:improve-project-harness` — turn corrections into enforced gates (anchors / attached skills / layer rules).
|
|
56
|
+
- `/drafted:extract` — session-end: deposit knowledge, a skill, and/or a template (the user picks which).
|
|
57
|
+
|
|
58
|
+
## Tool surface (action-based)
|
|
59
|
+
|
|
60
|
+
`project(list|open|create|update|move)` · `frame(read|write|edit|anchor|mv|search|…)` · `ls` · `skill(search|load|list|add|update|attach|…)` · `wiki(search|read|write|edit|mv|links|log|…)` · `template(list|create|fork|…)` · `focus` · `get_org` · `asset` · `layer`.
|
|
61
|
+
|
|
62
|
+
## Sign in
|
|
63
|
+
|
|
64
|
+
If a tool returns an auth error: on a desktop/CLI agent run the `auth(action="login")` tool (it prints a one-time URL; the user opens it). On the web/Cowork connector, authorization happens via the connector's OAuth — ask the user to re-authorize the Drafted connector in their settings, then retry.
|
|
65
|
+
|
|
66
|
+
## Working on the surface
|
|
67
|
+
|
|
68
|
+
- **Always `project(action="open")` first.** Every read/write operates on the active project. Use `project(action="list")` to find one. Every response includes a `project` field — verify it matches your intent before writing.
|
|
69
|
+
- **Default to the surface for substantive artifacts.** When asked to draft, write, plan, analyze, compare, design, document, summarize, report, spec, model, or make a deck/table, create or update frames instead of leaving the durable result only in chat. One visible frame per artifact or section.
|
|
70
|
+
- **Prefer Google Workspace when Drive is connected.** Call `get_org`; if it reports `googleDrive.connected: true`, use `frame(action="write", googleType="google-doc"|"google-sheet"|"google-slide", …)` for docs, sheets, and decks; populate immediately with the native write actions.
|
|
71
|
+
- **Read before editing.** `frame(action="edit")` uses hashline addressing — every line in a `frame(action="read")` response gets a 4-char hash; pass it to edit for surgical changes.
|
|
72
|
+
- **`focus` after writing** so the user watches your work land on their surface.
|
|
73
|
+
|
|
74
|
+
## Quality conventions
|
|
75
|
+
|
|
76
|
+
- **Match format to layer intent.** Research/strategy/copy are usually markdown; visual work (wireframes, designs, dashboards) is HTML.
|
|
77
|
+
- **Respect the template's conventions.** Read a `design-system` layer before `/designs/`; read an `audience` layer before `/copy/`.
|
|
78
|
+
- **Wireframes are low-fidelity** (grayscale, placeholders); reserve color and real content for the designs/final layer.
|
|
79
|
+
- **Choose dimensions to fit content** — `autoSize: true` for HTML, or explicit `width`/`height`.
|
|
80
|
+
- **Don't re-read unchanged frames** you already have this conversation.
|
|
81
|
+
|
|
82
|
+
## Surface URL recognition
|
|
83
|
+
|
|
84
|
+
Any URL containing `/f/{uuid}` is a Drafted frame link. Use `frame(action="read", path=URL)` to get its content and `focus(target=URL)` to pan the canvas to it. Never `WebFetch` Drafted URLs — the MCP tools authenticate properly.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Single source of truth for the per-project auto-inject context budget.
|
|
2
|
+
// Shared by the MCP gate logic (mcp/gates.mjs) and the server-side deposit caps
|
|
3
|
+
// (server/lib/project-skill-routes.mjs) so web-remote, local-stdio, and raw-API
|
|
4
|
+
// callers all enforce the same limit. ~24k chars ≈ 6k tokens.
|
|
5
|
+
export const PROJECT_CONTEXT_BUDGET_CHARS = 24000;
|
|
6
|
+
|
|
7
|
+
function gateItemChars(it) {
|
|
8
|
+
if (it && typeof it === 'object') {
|
|
9
|
+
if (it.content != null) return String(it.content).length;
|
|
10
|
+
if (typeof it.chars === 'number') return it.chars;
|
|
11
|
+
}
|
|
12
|
+
if (typeof it === 'string') return it.length;
|
|
13
|
+
if (typeof it === 'number') return it;
|
|
14
|
+
return 0;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Greedily include items whose sizes fit the budget (in priority order); defer the
|
|
18
|
+
// rest. Shared by the server-side project-open priming assembly and the MCP gate
|
|
19
|
+
// fallback so both produce identical auto-inject sets.
|
|
20
|
+
export function selectWithinBudget(items, budget = PROJECT_CONTEXT_BUDGET_CHARS, sizeOf = gateItemChars) {
|
|
21
|
+
const included = [];
|
|
22
|
+
const deferred = [];
|
|
23
|
+
let used = 0;
|
|
24
|
+
for (const it of items || []) {
|
|
25
|
+
const size = sizeOf(it);
|
|
26
|
+
if (used + size <= budget) { included.push(it); used += size; }
|
|
27
|
+
else { deferred.push(it); }
|
|
28
|
+
}
|
|
29
|
+
return { included, deferred, used, budget };
|
|
30
|
+
}
|