mirror of
https://github.com/hansjone/netxops.git
synced 2026-10-09 02:40:45 +08:00
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:
parent
62d3f565ce
commit
0f31a6648a
16 changed files with 756 additions and 132 deletions
102
lib/index.js
102
lib/index.js
|
|
@ -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)")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue