Add refs product KB and dual-plane local evolution (0.1.37).

Site writes device profiles/inventory via kbWriteRef; persona and kb-context require read-then-write refs plus memories for reusable experience.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-09-16 06:42:22 +08:00
parent 62d3f565ce
commit 0f31a6648a
16 changed files with 756 additions and 132 deletions

View file

@ -814,7 +814,16 @@ import { join as join3, relative, resolve, sep } from "node:path";
var KB_LOCAL_WRITABLE_ROOTS = Object.freeze([
"memories",
"drafts",
"suggestions"
"suggestions",
"refs"
]);
var REF_AREAS = Object.freeze([
"inventory",
"devices",
"topology",
"business",
"commands",
"handbooks"
]);
var MEMORY_BUCKET_DIR = Object.freeze({
note: "日常笔记",
@ -874,6 +883,10 @@ function suggestionFileName(opts) {
const date = normalizeIsoDate(opts.date, opts.now);
return `${date}-${sanitizeSlug(opts.slug)}.md`;
}
function refFileName(slug) {
const base = sanitizeSlug(slug, "note");
return base.toLowerCase().endsWith(".md") ? base : `${base}.md`;
}
function resolveWritableLocalPath(localRoot, ...segments) {
if (segments.some((s) => s.includes("\x00"))) {
throw new Error("path segment contains NUL");
@ -947,6 +960,19 @@ function draftDir(domain) {
return "drafts";
return join3("drafts", sanitizeSlug(d, "misc"));
}
function refDir(area, device) {
if (!REF_AREAS.includes(area)) {
throw new Error(`invalid refs area: ${area}`);
}
if (area === "devices") {
const host = sanitizeSlug(device?.trim() || "", "");
if (!host) {
throw new Error("refs/devices requires device (host_name)");
}
return join3("refs", "devices", host);
}
return join3("refs", area);
}
// src/netx/kb-context-skill.ts
var SKILL_NAME = "kb-context";
@ -957,18 +983,29 @@ function skillBody(snapshot) {
const localRow = localRoot ? `| kbLocal | \`${localRoot}\` |` : "| kbLocal | (MANIFEST paths.local missing) |";
const localGuide = localRoot ? [
"",
"**Site-writable (`kbLocal`)**: use host tools `netx__kbWriteMemory`, `netx__kbWriteDraft`,",
"`netx__kbWriteSuggestion`, `netx__kbUpdateLocal`, `netx__kbDeleteLocal`, `netx__kbListLocal`.",
"They jail writes to `memories/` / `drafts/` / `suggestions/` only.",
"Do **not** use workspace Write/bash for KB paths (sandbox).",
"`identity/` is persona/policy for the host — **read-only for the agent**; never rewrite it;",
"file boundary issues as `suggestions/` instead."
"### Dual plane (mandatory)",
"",
"- **HQ pack = read-only** (formal RCA, theory, packet, `_common`, `_skills`, shared cmdLib).",
`- **Writable evolution core**: \`${localRoot}\` → \`refs/\` | \`memories/\` | \`drafts/\` | \`suggestions/\`.`,
"- **All KB writes via host tools** (workspace Write/bash cannot reach KB):",
" `netx__kbWriteRef` / `netx__kbWriteMemory` / `netx__kbWriteDraft` / `netx__kbWriteSuggestion` /",
" `netx__kbUpdateLocal` / `netx__kbDeleteLocal` / `netx__kbListLocal`.",
"",
"### refs — site product knowledge (evolve every task)",
"- On any NE: **read** `refs/devices/<host_name>/` first; if missing, prove with netx then `kbWriteRef` to create/update.",
"- `area=devices` requires `device=<host_name>` (never UUID); primary file slug=`PROFILE`.",
"- Also: `inventory` (ledger), `topology`, `business`, `commands` (site-only), `handbooks`.",
"- Device facts → refs; episodic experience → memories (do not dump ledgers into diaries).",
"",
"### memories — write immediately when valuable",
"- `bucket=note` | `rca_review` | `ai_trace` — short beats lost.",
"- Open cases → `kbWriteDraft`. HQ pack gaps → `kbWriteSuggestion`."
] : [
"",
"No local write tools until `paths.local` is present in MANIFEST."
];
return {
description: "Operator knowledge-base context for this Host (paths + identity from MANIFEST).",
description: "Operator knowledge-base context for this Host (HQ pack + local evolution under paths.local).",
content: [
"## Knowledge base (configured)",
"",
@ -1379,6 +1416,25 @@ function createSuggestion(snapshot, args) {
relativePath: target.relativePath
};
}
function createRef(snapshot, args) {
const localRoot = requireLocalRoot(snapshot);
const name = refFileName(args.slug);
const target = resolveWritableLocalPath(localRoot, refDir(args.area, args.device), name);
if (existsSync4(target.absolutePath) && !args.overwrite) {
throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`);
}
mkdirSync(dirname3(target.absolutePath), { recursive: true });
const existed = existsSync4(target.absolutePath);
writeFileSync(target.absolutePath, args.body.endsWith(`
`) ? args.body : `${args.body}
`, "utf8");
return {
ok: true,
action: existed ? "updated" : "created",
absolutePath: target.absolutePath,
relativePath: target.relativePath
};
}
function updateLocalFile(snapshot, args) {
const localRoot = requireLocalRoot(snapshot);
const target = resolveExistingWritableFile(localRoot, args.path);
@ -1584,7 +1640,7 @@ function registerKbLocalTools(ctx, snapshot = getKbContext()) {
date: args.date != null ? String(args.date) : undefined,
overwrite: args.overwrite === true
})),
tool("netx__kbWriteDraft", `Create (or overwrite) a DRAFT case under ${localRoot}/drafts/. ` + "Filename is forced to DRAFT-YYYYMMDD-…; body gets status: draft frontmatter if missing. " + "Set overwrite=true to replace. Not for identity/ or formal RCA.", {
tool("netx__kbWriteDraft", `Create (or overwrite) a DRAFT case under ${localRoot}/drafts/. ` + "Filename is forced to DRAFT-YYYYMMDD-…; body gets status: draft frontmatter if missing. " + "Set overwrite=true to replace. Not for formal RCA.", {
slug: reqStr("Case stem (DRAFT- prefix added if missing)"),
body: reqStr("Markdown body (RCA-ish draft)"),
domain: str("Optional fault-domain subfolder under drafts/"),
@ -1597,7 +1653,7 @@ function registerKbLocalTools(ctx, snapshot = getKbContext()) {
date: args.date != null ? String(args.date) : undefined,
overwrite: args.overwrite === true
})),
tool("netx__kbWriteSuggestion", `Create (or overwrite) a suggestion under ${localRoot}/suggestions/{theory|improvement}/. ` + "Use for theory corrections or tool/process improvements — never rewrite identity/.", {
tool("netx__kbWriteSuggestion", `Create (or overwrite) a suggestion under ${localRoot}/suggestions/{theory|improvement}/. ` + "Use for HQ pack/theory corrections or tool/process improvements.", {
kind: {
type: "string",
required: true,
@ -1615,22 +1671,40 @@ function registerKbLocalTools(ctx, snapshot = getKbContext()) {
date: args.date != null ? String(args.date) : undefined,
overwrite: args.overwrite === true
})),
tool("netx__kbUpdateLocal", `Replace the body of an existing file under memories|drafts|suggestions ` + `(absolute path or path relative to ${localRoot}). ` + "Cannot touch identity/ or outside paths.local. Drafts keep status: draft.", {
tool("netx__kbWriteRef", `Create (or overwrite) site product-knowledge under ${localRoot}/refs/. ` + "area=inventory|devices|topology|business|commands|handbooks. " + "For devices: require device=host_name; default slug PROFILE for the ops profile. " + "Use for NE ledger, per-device profiles, topology/business notes, site command books — not diaries.", {
area: {
type: "string",
required: true,
enum: ["inventory", "devices", "topology", "business", "commands", "handbooks"],
description: "refs/ subtree"
},
slug: reqStr("Filename stem (e.g. PROFILE, neighbors, ledger)"),
body: reqStr("Full markdown body"),
device: str("Required when area=devices — host_name (never UUID)"),
overwrite: bool("Replace if the target file already exists")
}, (args) => createRef(liveSnapshot(), {
area: args.area,
slug: String(args.slug ?? ""),
body: String(args.body ?? ""),
device: args.device != null ? String(args.device) : undefined,
overwrite: args.overwrite === true
})),
tool("netx__kbUpdateLocal", `Replace the body of an existing file under memories|drafts|suggestions|refs ` + `(absolute path or path relative to ${localRoot}). ` + "Drafts keep status: draft.", {
path: reqStr("Absolute path or path relative to KB local root"),
body: reqStr("New full markdown body")
}, (args) => updateLocalFile(liveSnapshot(), {
path: String(args.path ?? ""),
body: String(args.body ?? "")
})),
tool("netx__kbDeleteLocal", `Delete one existing markdown under memories|drafts|suggestions ` + `(absolute or relative to ${localRoot}). Refuses identity/ and escapes.`, {
tool("netx__kbDeleteLocal", `Delete one existing markdown under memories|drafts|suggestions|refs ` + `(absolute or relative to ${localRoot}).`, {
path: reqStr("Absolute path or path relative to KB local root")
}, (args) => deleteLocalFile(liveSnapshot(), {
path: String(args.path ?? "")
})),
tool("netx__kbListLocal", `List recent .md files under ${localRoot} memories|drafts|suggestions (newest first). ` + "Does not include identity/. Read contents via absolute paths from the listing.", {
tool("netx__kbListLocal", `List recent .md files under ${localRoot} memories|drafts|suggestions|refs (newest first). ` + "Read contents via absolute paths from the listing.", {
root: {
type: "string",
enum: ["memories", "drafts", "suggestions", "all"],
enum: ["memories", "drafts", "suggestions", "refs", "all"],
description: "Subtree to list (default all writable)"
},
limit: num("Max entries 1–200 (default 50)")