@nurix/etna 0.19.7 → 0.19.9
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/data/harness-index.json +1 -1
- package/data/harnesses/code/harness.json +1 -1
- package/data/harnesses/code/rules/pnpm.md +54 -0
- package/data/harnesses/code/rules/typescript.md +2 -21
- package/data/harnesses/kb-agi/CLAUDE.md +33 -29
- package/data/harnesses/kb-agi/agents/kb-apply.md +34 -0
- package/data/harnesses/kb-agi/agents/kb-audit.md +49 -0
- package/data/harnesses/kb-agi/agents/kb-init.md +316 -0
- package/data/harnesses/kb-agi/harness.json +2 -2
- package/data/harnesses/kb-agi/scripts/kb-export.mjs +167 -0
- package/package.json +1 -1
- package/data/harnesses/code/rules/nustack.md +0 -150
- package/data/harnesses/kb-agi/agents/kb-agi-init.md +0 -285
- package/data/harnesses/kb-agi/agents/kb-catch-stale.md +0 -22
- package/data/harnesses/kb-agi/agents/kb-find-gaps.md +0 -27
- package/data/harnesses/kb-agi/agents/kb-fix-links.md +0 -22
- package/data/harnesses/kb-agi/agents/kb-refresh-frontdoor.md +0 -22
- package/data/harnesses/kb-agi/agents/kb-refresh-summaries.md +0 -22
- package/data/harnesses/kb-agi/agents/kb-sync-metadata.md +0 -25
- package/data/harnesses/kb-agi/rules/kb.md +0 -13
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// plugins/kb-agi/scripts/kb-export.mjs - Build the vault's derived wikilink graph.
|
|
3
|
+
//
|
|
4
|
+
// Run from the vault root (the directory holding meta/): `node .claude/scripts/kb-export.mjs`.
|
|
5
|
+
// Walks every content note, parses frontmatter hints and [[wikilinks]] per the dialect pinned
|
|
6
|
+
// in meta/contract.md, and writes meta/_derived/graph.json — nodes (path, title, folder,
|
|
7
|
+
// maturity, canonical, visibility, degrees), edges typed by form (link | anchor | embed),
|
|
8
|
+
// unresolved links, and title/basename collisions. Deterministic: same vault bytes → same
|
|
9
|
+
// graph.json (everything sorted, no timestamps). The graph is a regenerable derivative of the
|
|
10
|
+
// notes — never hand-edited; git records when it was built.
|
|
11
|
+
|
|
12
|
+
import { readdirSync, readFileSync, mkdirSync, writeFileSync, existsSync } from 'node:fs';
|
|
13
|
+
import { join, relative, sep, basename } from 'node:path';
|
|
14
|
+
|
|
15
|
+
const vaultRoot = process.cwd();
|
|
16
|
+
if (!existsSync(join(vaultRoot, 'meta'))) {
|
|
17
|
+
console.error('kb-export: no meta/ found in the current directory — run from the vault root.');
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Dialect (mirrors meta/contract.md): [[target]], [[target|alias]], [[target#heading]],
|
|
22
|
+
// ![[embed]]. Group 1 = embed marker, 2 = target, 3 = heading anchor, 4 = alias.
|
|
23
|
+
const WIKILINK_RE = /(!?)\[\[([^\]|#]+?)(#[^\]|]+)?(?:\|([^\]]+))?\]\]/g;
|
|
24
|
+
const FRONTMATTER_RE = /^---\r?\n([\s\S]+?)\r?\n---/;
|
|
25
|
+
|
|
26
|
+
// Skipped at every level: dot-entries (.claude, .obsidian, .vscode, …), node_modules,
|
|
27
|
+
// and meta/ — the control plane is not knowledge (contract: notes are the only canon).
|
|
28
|
+
const SKIP_NAMES = new Set(['node_modules', 'meta']);
|
|
29
|
+
|
|
30
|
+
function collectNotes(dir, out = []) {
|
|
31
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
32
|
+
if (entry.name.startsWith('.') || SKIP_NAMES.has(entry.name)) continue;
|
|
33
|
+
const full = join(dir, entry.name);
|
|
34
|
+
if (entry.isDirectory()) collectNotes(full, out);
|
|
35
|
+
else if (entry.isFile() && entry.name.endsWith('.md')) out.push(full);
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Minimal YAML reader for the contract's scalar hints; anything else is ignored.
|
|
41
|
+
function parseFrontmatter(content) {
|
|
42
|
+
const match = content.match(FRONTMATTER_RE);
|
|
43
|
+
if (!match) return { fields: {}, body: content };
|
|
44
|
+
const fields = {};
|
|
45
|
+
for (const line of match[1].split('\n')) {
|
|
46
|
+
const kv = line.match(/^([A-Za-z_][\w-]*):\s*(.*?)\s*$/);
|
|
47
|
+
if (kv) fields[kv[1]] = kv[2].replace(/^["']|["']$/g, '');
|
|
48
|
+
}
|
|
49
|
+
return { fields, body: content.slice(match[0].length) };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Blank out fenced and inline code so documentation examples never become edges,
|
|
53
|
+
// keeping offsets stable.
|
|
54
|
+
function stripCode(body) {
|
|
55
|
+
return body
|
|
56
|
+
.replace(/```[\s\S]*?```/g, (m) => ' '.repeat(m.length))
|
|
57
|
+
.replace(/`[^`\n]+`/g, (m) => ' '.repeat(m.length));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function parseNote(file) {
|
|
61
|
+
const content = readFileSync(file, 'utf8');
|
|
62
|
+
const { fields, body } = parseFrontmatter(content);
|
|
63
|
+
const rel = relative(vaultRoot, file).split(sep).join('/');
|
|
64
|
+
const id = rel.replace(/\.md$/, '');
|
|
65
|
+
const h1 = body.match(/^\s*#\s+(.+)$/m);
|
|
66
|
+
const links = [];
|
|
67
|
+
const stripped = stripCode(body);
|
|
68
|
+
let m;
|
|
69
|
+
WIKILINK_RE.lastIndex = 0;
|
|
70
|
+
while ((m = WIKILINK_RE.exec(stripped))) {
|
|
71
|
+
const target = (m[2] || '').trim();
|
|
72
|
+
if (!target) continue;
|
|
73
|
+
links.push({
|
|
74
|
+
target,
|
|
75
|
+
form: m[1] === '!' ? 'embed' : m[3] ? 'anchor' : 'link',
|
|
76
|
+
aliased: Boolean(m[4]),
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
id,
|
|
81
|
+
title: fields.title || (h1 ? h1[1].trim() : basename(file, '.md')),
|
|
82
|
+
folder: id.includes('/') ? id.slice(0, id.lastIndexOf('/')) : '',
|
|
83
|
+
maturity: fields.maturity || 'captured',
|
|
84
|
+
canonical: fields.canonical !== 'false',
|
|
85
|
+
visibility: fields.visibility === 'hidden' ? 'hidden' : 'public',
|
|
86
|
+
links,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const notes = collectNotes(vaultRoot).map(parseNote).sort((a, b) => a.id.localeCompare(b.id));
|
|
91
|
+
|
|
92
|
+
// Resolution maps: exact id/path first, then basename (Obsidian-style shortest name),
|
|
93
|
+
// case-insensitively as the last step. A basename shared by several notes is ambiguous.
|
|
94
|
+
const byId = new Map(notes.map((n) => [n.id.toLowerCase(), n.id]));
|
|
95
|
+
const byBase = new Map();
|
|
96
|
+
for (const n of notes) {
|
|
97
|
+
const base = basename(n.id).toLowerCase();
|
|
98
|
+
if (!byBase.has(base)) byBase.set(base, []);
|
|
99
|
+
byBase.get(base).push(n.id);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function resolveTarget(raw) {
|
|
103
|
+
const clean = raw.replace(/\.md$/, '');
|
|
104
|
+
const exact = byId.get(clean.toLowerCase());
|
|
105
|
+
if (exact) return { id: exact };
|
|
106
|
+
const candidates = byBase.get(basename(clean).toLowerCase()) || [];
|
|
107
|
+
if (candidates.length === 1) return { id: candidates[0] };
|
|
108
|
+
return { id: null, reason: candidates.length ? 'ambiguous' : 'missing' };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const edges = [];
|
|
112
|
+
const unresolved = [];
|
|
113
|
+
for (const note of notes) {
|
|
114
|
+
for (const link of note.links) {
|
|
115
|
+
const { id, reason } = resolveTarget(link.target);
|
|
116
|
+
if (id) edges.push({ source: note.id, target: id, form: link.form, aliased: link.aliased });
|
|
117
|
+
else unresolved.push({ source: note.id, target: link.target, reason });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
edges.sort((a, b) => a.source.localeCompare(b.source) || a.target.localeCompare(b.target) || a.form.localeCompare(b.form));
|
|
121
|
+
unresolved.sort((a, b) => a.source.localeCompare(b.source) || a.target.localeCompare(b.target));
|
|
122
|
+
|
|
123
|
+
const inDegree = new Map();
|
|
124
|
+
const outDegree = new Map();
|
|
125
|
+
for (const e of edges) {
|
|
126
|
+
outDegree.set(e.source, (outDegree.get(e.source) || 0) + 1);
|
|
127
|
+
inDegree.set(e.target, (inDegree.get(e.target) || 0) + 1);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Collisions break both wikilink resolution and retrieval — surfaced for the shape goal.
|
|
131
|
+
const collisions = [];
|
|
132
|
+
for (const [base, ids] of [...byBase.entries()].sort()) {
|
|
133
|
+
if (ids.length > 1) collisions.push({ kind: 'basename', value: base, notes: [...ids].sort() });
|
|
134
|
+
}
|
|
135
|
+
const byTitle = new Map();
|
|
136
|
+
for (const n of notes) {
|
|
137
|
+
const t = n.title.toLowerCase();
|
|
138
|
+
if (!byTitle.has(t)) byTitle.set(t, []);
|
|
139
|
+
byTitle.get(t).push(n.id);
|
|
140
|
+
}
|
|
141
|
+
for (const [title, ids] of [...byTitle.entries()].sort()) {
|
|
142
|
+
if (ids.length > 1) collisions.push({ kind: 'title', value: title, notes: [...ids].sort() });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const graph = {
|
|
146
|
+
schema: 'kb-graph/1',
|
|
147
|
+
nodes: notes.map((n) => ({
|
|
148
|
+
id: n.id,
|
|
149
|
+
title: n.title,
|
|
150
|
+
folder: n.folder,
|
|
151
|
+
maturity: n.maturity,
|
|
152
|
+
canonical: n.canonical,
|
|
153
|
+
visibility: n.visibility,
|
|
154
|
+
inDegree: inDegree.get(n.id) || 0,
|
|
155
|
+
outDegree: outDegree.get(n.id) || 0,
|
|
156
|
+
})),
|
|
157
|
+
edges,
|
|
158
|
+
unresolved,
|
|
159
|
+
collisions,
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
mkdirSync(join(vaultRoot, 'meta', '_derived'), { recursive: true });
|
|
163
|
+
writeFileSync(join(vaultRoot, 'meta', '_derived', 'graph.json'), JSON.stringify(graph, null, 2) + '\n');
|
|
164
|
+
console.log(
|
|
165
|
+
`kb-export: ${graph.nodes.length} notes, ${edges.length} edges, ` +
|
|
166
|
+
`${unresolved.length} unresolved, ${collisions.length} collision(s) → meta/_derived/graph.json`,
|
|
167
|
+
);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nurix/etna",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.9",
|
|
4
4
|
"description": "Bootstrap a project with a Nurix harness — copies the harness's CLAUDE.md, skills, agents, rules, and hooks directly into the project from a bundled payload (no marketplace fetch, no network).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
# Nustack — `@nurix/*` Packages: naming, monorepo layout, and publishing
|
|
2
|
-
|
|
3
|
-
How a `@nurix/*` multi-package repo is **named**, **structured**, and **shipped**. For consuming an already-published `@nurix/*` package (auth, install, imports), see the Consuming section of [`typescript.md`](typescript.md).
|
|
4
|
-
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
## 1. Package naming
|
|
8
|
-
|
|
9
|
-
Every workspace package is scoped to **`@nurix/`**. The segment after the scope encodes **intent** — the dot vs. hyphen is semantic, not cosmetic:
|
|
10
|
-
|
|
11
|
-
| Form | Meaning | Examples |
|
|
12
|
-
| ----------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
|
|
13
|
-
| `@nurix/workflows-sdk` (hyphen, single segment) | The **published, externally-destined** surface. A clean `@nurix/*` name is a published _contract_ — changing an export or peer dep is a breaking change for consumers outside this repo. | `@nurix/workflows-sdk` |
|
|
14
|
-
| `@nurix/workflows.<name>` (dot separator) | **Internal** to the workflows product — `private: true`, refactor freely, no external consumers. | `@nurix/workflows.agent-core`, `.worker`, `.gateway`, `.secrets`, `.connector`, `.playground`, `.playground-tests` |
|
|
15
|
-
| `@nurix/<name>` (plain, no `workflows` prefix) | Cross-repo **shared libs** owned _elsewhere_ and merely consumed here — never renamed. | `@nurix/contracts`, `@nurix/components` |
|
|
16
|
-
|
|
17
|
-
- The `<name>` segment is always **kebab-case** (`agent-core`, `playground-tests`).
|
|
18
|
-
- The dot is **not** a path separator, so subpath exports still resolve: `@nurix/workflows.agent-core/nodes`.
|
|
19
|
-
- The dot tracks "internal product package," not the `private` flag exactly — `@nurix/workflows.secrets` is `private: false` because it's extraction-bound (slated to move to a sibling repo/namespace), yet still wears the dotted internal name. Today `-sdk` is the only clean published form.
|
|
20
|
-
|
|
21
|
-
**Naming a new package:** internal to this repo? → `@nurix/workflows.<thing>`, `private: true`, kebab-case `<thing>` (the default). Externally consumed contract? → clean `@nurix/<thing>` (rare). Shared lib from another repo? → keep its existing `@nurix/<name>`.
|
|
22
|
-
|
|
23
|
-
---
|
|
24
|
-
|
|
25
|
-
## 2. Monorepo & package boundaries
|
|
26
|
-
|
|
27
|
-
One `tsconfig.base.json` at the repo root is the **single owner of TS policy** — the strict set (see [`typescript.md`](typescript.md)), `target`, and any option uniform across packages (here, `module`/`moduleResolution`). Each package's `tsconfig.json` is thin: it `extends` the base, then adds only its own *coordinates* (`rootDir`/`outDir`/`include`) and genuine *environment* overrides (`lib`/`types`/`jsx`, plus `module`/`moduleResolution` when a package's module system genuinely differs — a Worker, browser, or NodeNext package). That is environment, not policy. Never re-declare or weaken a *policy* option locally (no per-package `strict: false`, no widening `noUncheckedIndexedAccess`). A framework that owns its own base (e.g. an Astro app on `astro/tsconfigs/strict`) is the one allowed exception.
|
|
28
|
-
|
|
29
|
-
- **Internal packages are source-first.** A `private: true` workspace package exposes its API through the `exports` field pointing at TypeScript source (`"types"`/`"default"` → `./src/index.ts`) — consumed across the workspace with **no build step**. Prefer the `exports` field; don't add a bare `main`/`module` to new packages. Add one typed subpath per public surface (`"./nodes": { "types": "./src/nodes/index.ts", "default": "./src/nodes/index.ts" }`); consumers import the subpath, **never** a deep relative path into another package's `src/`.
|
|
30
|
-
- **Published packages build to `dist` — never ship raw `src`.** The moment a package crosses a publish boundary (`private: false`), it compiles with `tsc` (`declaration: true`, `declarationMap: true` from the base), ships `files: ["dist"]`, and points `exports`/`types` at `./dist/*` (use `publishConfig.exports` to swap source→dist on publish if the dev entry stays on source). `prepublishOnly` runs the build.
|
|
31
|
-
- **Declare host singletons as `peerDependencies`** on published libraries (`react`, `react-dom`, the framework client) so consumers keep one copy; never bundle them. Apps depend on them directly.
|
|
32
|
-
- **Single-source shared dep versions with pnpm `catalog:`** — `typescript`, `zod`, `vitest`, `react`, type packages. One version per dependency across the workspace so every package's types resolve identically; no per-manifest version drift.
|
|
33
|
-
- **Reach for TS project references only when it hurts** — adopt `references` once the typecheck/build graph is slow enough to feel. Until then, `pnpm -r typecheck` over independent `extends` configs is the correct default.
|
|
34
|
-
|
|
35
|
-
---
|
|
36
|
-
|
|
37
|
-
## 3. Publishing & the release pipeline
|
|
38
|
-
|
|
39
|
-
`@nurix/*` packages publish to **npmjs.org with `restricted` access** — only npm users with read rights to the `nurix` org can install them. Reference implementations: `~/dev/components` (`@nurix/components`, library) and `~/dev/etna/packages/cli` (`@nurix/etna`, CLI).
|
|
40
|
-
|
|
41
|
-
### 3.1 Prerequisites (one-time, org-wide)
|
|
42
|
-
|
|
43
|
-
1. **npm org `nurix`** exists and you're a member with publish rights — <https://www.npmjs.com/settings/nurix/packages>.
|
|
44
|
-
2. **An npm automation token** with `Read and write` for the `nurix` scope — npm → Generate New Token → Automation.
|
|
45
|
-
3. **The token added as a GitHub Actions secret** named `NPM_PUBLISH_TOKEN` on every publishing repo. `NPM_PUBLISH_TOKEN` is the convention; pick another name only with a strong reason.
|
|
46
|
-
|
|
47
|
-
### 3.2 `package.json` — required fields
|
|
48
|
-
|
|
49
|
-
```jsonc
|
|
50
|
-
{
|
|
51
|
-
"name": "@nurix/<package-name>", // The scope makes it discoverable to org members.
|
|
52
|
-
"version": "0.1.0", // Start at 0.1.0; the workflow bumps from here.
|
|
53
|
-
"private": false, // MUST be false (or omitted). `npm publish` refuses if true.
|
|
54
|
-
"publishConfig": { "access": "restricted" }, // Scoped + private: only org members can install.
|
|
55
|
-
"files": ["dist"] // Whitelist of what ships. Anything outside is excluded.
|
|
56
|
-
}
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
- **`publishConfig.access`** — `"restricted"` (recommended): visible only to `nurix`-org readers. `"public"`: world-readable (free, but only if you mean it). Omitting it on a scoped package defaults to `restricted`; be explicit anyway.
|
|
60
|
-
- **`files`** — without it, npm publishes everything not in `.gitignore`, **including source**. Library → `["dist"]`; CLI (etna) → `["bin/", "data/", "README.md"]`. Don't list `package.json`/`README.md` — npm always includes them.
|
|
61
|
-
- **Build hooks** — `prepack` for build steps that must produce files in `files` (also runs on install-as-git-dep); `prepublishOnly` for publish-only steps. Either alone is fine; `@nurix/components` runs both.
|
|
62
|
-
- **Other useful fields** — `type: "module"`, `engines.node`, `bin` (CLIs), `main`/`module`/`types`/`exports` (libraries), `sideEffects: ["**/*.css"]`, `repository` (with `directory` for monorepos), `license: "UNLICENSED"`.
|
|
63
|
-
|
|
64
|
-
### 3.3 The publish workflow (generic template)
|
|
65
|
-
|
|
66
|
-
The dual-trigger pattern: a push to the default branch auto-ships a patch; a manual dispatch picks minor/major.
|
|
67
|
-
|
|
68
|
-
```yaml
|
|
69
|
-
name: Publish to npm
|
|
70
|
-
on:
|
|
71
|
-
push:
|
|
72
|
-
branches: [dev] # your default branch
|
|
73
|
-
paths: # ONLY paths that change what ships — else every README edit publishes
|
|
74
|
-
- 'src/**'
|
|
75
|
-
- 'scripts/**'
|
|
76
|
-
- 'package.json'
|
|
77
|
-
- 'package-lock.json'
|
|
78
|
-
- '.github/workflows/publish.yml'
|
|
79
|
-
workflow_dispatch:
|
|
80
|
-
inputs:
|
|
81
|
-
bump: { description: 'Version bump', type: choice, options: [patch, minor, major], default: patch }
|
|
82
|
-
permissions: { contents: write } # push the version-bump commit + tag
|
|
83
|
-
concurrency: { group: publish, cancel-in-progress: false } # monorepo: group: publish-<pkg>
|
|
84
|
-
jobs:
|
|
85
|
-
publish:
|
|
86
|
-
runs-on: ubuntu-latest
|
|
87
|
-
steps:
|
|
88
|
-
- uses: actions/checkout@v5
|
|
89
|
-
with: { fetch-depth: 0, token: '${{ secrets.GITHUB_TOKEN }}' }
|
|
90
|
-
- uses: actions/setup-node@v5
|
|
91
|
-
with: { node-version: 22, registry-url: 'https://registry.npmjs.org' } # writes .npmrc using $NODE_AUTH_TOKEN
|
|
92
|
-
- run: npm ci # skip ONLY if the build is pure-Node with no deps (e.g. @nurix/etna)
|
|
93
|
-
- run: |
|
|
94
|
-
git config user.name "github-actions[bot]"
|
|
95
|
-
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
96
|
-
- run: npm version ${{ inputs.bump || 'patch' }} -m "chore: release v%s [skip ci]"
|
|
97
|
-
- run: npm publish # prepublishOnly / prepack runs the build
|
|
98
|
-
env: { NODE_AUTH_TOKEN: '${{ secrets.NPM_PUBLISH_TOKEN }}' }
|
|
99
|
-
- run: git push --follow-tags # pushes the bump commit AND the v<version> tag npm created
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
**Why dual-trigger is safe** — three safeguards prevent an infinite publish loop: (1) `paths:` scopes the push trigger to artifact-affecting files; (2) `[skip ci]` in the bump commit makes GitHub skip the workflow that bump would otherwise re-trigger; (3) `concurrency: cancel-in-progress: false` queues near-simultaneous merges instead of racing for a version number.
|
|
103
|
-
|
|
104
|
-
**`registry-url` is load-bearing** — without `registry-url: https://registry.npmjs.org` on `setup-node`, it never writes the `.npmrc` line that uses `$NODE_AUTH_TOKEN`, and the publish falls back to anonymous auth and fails.
|
|
105
|
-
|
|
106
|
-
### 3.4 The three ways to ship
|
|
107
|
-
|
|
108
|
-
- **A. Auto on merge (common).** Land on the default branch; if a `paths:`-filtered file changed, the workflow fires and ships a patch.
|
|
109
|
-
- **B. Manual dispatch (minor/major).** **Actions → Publish to npm → Run workflow**, pick `bump = minor|major`.
|
|
110
|
-
- **C. Local (CI down / one-off):** `npm version <patch|minor|major>` → `npm publish` → `git push --follow-tags`.
|
|
111
|
-
|
|
112
|
-
Local auth uses two tokens, two homes: a **read** token in `~/.npmrc` (`//registry.npmjs.org/:_authToken=…`) for `npm install`, and `NPM_PUBLISH_TOKEN` as an env var (sourced from `~/.zsh_secrets` or equivalent, **not** in `.npmrc` or the repo) for `npm publish`.
|
|
113
|
-
|
|
114
|
-
> **Agent guard — if `NPM_PUBLISH_TOKEN` isn't in the environment, do not work around it.** No `npm login` to swap the user's auth, no rewriting `~/.npmrc`, no substituting the read token (it lacks publish rights → `403`), no `--registry` overrides, no temp-file token. Instead **stop and report**: "This repo can't publish locally — `NPM_PUBLISH_TOKEN` isn't set. Source `~/.zsh_secrets` (or wherever you keep it) and re-run, or trigger the CI workflow." Some users publish exclusively from CI — that's the default path for a reason.
|
|
115
|
-
|
|
116
|
-
Verify after any flow: `https://www.npmjs.com/package/@nurix/<name>` and a clean-dir `npx @nurix/<name>@latest`.
|
|
117
|
-
|
|
118
|
-
### 3.5 Monorepo deviations
|
|
119
|
-
|
|
120
|
-
When the package lives in `packages/<pkg>/` of a multi-package repo:
|
|
121
|
-
|
|
122
|
-
- **`working-directory: packages/<pkg>`** on the bump and publish steps so they operate on the right `package.json`.
|
|
123
|
-
- **Scoped concurrency** — `group: publish-<pkg>` lets sibling packages publish in parallel.
|
|
124
|
-
- **Tag prefix — only with multiple tagging packages.** `npm version --tag-version-prefix="<pkg>-v"` (→ `<pkg>-v0.2.0`) is worth it only when more than one package cuts release tags, so they don't collide. A single publishable package (etna's case, even though it's laid out as a monorepo) uses plain `v0.2.0`.
|
|
125
|
-
|
|
126
|
-
A repo with one publishable package only needs `working-directory`; skip the prefix, and concurrency scoping is optional.
|
|
127
|
-
|
|
128
|
-
### 3.6 Common pitfalls
|
|
129
|
-
|
|
130
|
-
- **402 Payment Required** — `access: "public"` on an org without a plan for free public scoped packages. Use `"restricted"` (or upgrade the plan).
|
|
131
|
-
- **403 Forbidden** — `NPM_PUBLISH_TOKEN` missing/wrong, or its user lacks `nurix` write access, or it's a read-only (not Automation) token.
|
|
132
|
-
- **"Cannot publish over previously published version x.y.z"** — a bump ran but publish failed and was re-run without rebasing. `git reset` the bump and let CI re-bump, or `npm version patch && git push --follow-tags && npm publish`.
|
|
133
|
-
- **Publish succeeds but `npx @nurix/foo` is "command not found"** — no `bin` field, or `bin` points outside `files`, or the script is missing its `#!/usr/bin/env node` shebang.
|
|
134
|
-
- **"Git working directory not clean" on `npm version`** — the checkout has untracked/modified files; build only into `.gitignore`d paths (the components repo builds into `dist/`).
|
|
135
|
-
- **Published tarball contains source** — missing `files` in `package.json`. Add `["dist"]` and republish.
|
|
136
|
-
- **Auto-publish fires too often** — no `paths:` filter or one that's too broad. Tighten it to artifact-affecting files, or drop the push trigger for `workflow_dispatch`-only.
|
|
137
|
-
- **Workflow loops forever** — the bump commit lacks `[skip ci]`. Invoke `npm version` with `-m "chore: release v%s [skip ci]"`.
|
|
138
|
-
- **A normal commit silently skips its own push** — the inverse trap. GitHub scans the head commit's **whole message** (subject _and_ body) for skip directives (`[skip ci]`, `[ci skip]`, `[no ci]`, `[skip actions]`, `***NO_CI***`). If any hand-written commit merely _mentions_ the literal token in prose, the entire push is skipped and the publish never fires (symptom: branch moved, paths matched, Actions on — yet zero runs for the SHA). Never write the literal token in a commit message or PR body unless you mean it; to ship a push that carried it by accident, re-trigger via **Run workflow** / `gh workflow run … --ref dev`.
|
|
139
|
-
|
|
140
|
-
---
|
|
141
|
-
|
|
142
|
-
## 4. Quick checklist for a new `@nurix/*` package
|
|
143
|
-
|
|
144
|
-
- [ ] `package.json`: `name: "@nurix/X"`, `version: "0.1.0"`, `private: false`, `publishConfig.access: "restricted"`, `files: [...]`
|
|
145
|
-
- [ ] Build step wired into `prepack` (or `prepublishOnly`)
|
|
146
|
-
- [ ] Name follows §1; if it's a workspace member, boundaries follow §2
|
|
147
|
-
- [ ] `.github/workflows/publish.yml` from §3.3
|
|
148
|
-
- [ ] `NPM_PUBLISH_TOKEN` secret added to the repo
|
|
149
|
-
- [ ] First publish from local (`npm publish`) to verify the pipeline, then a no-op commit to verify CI
|
|
150
|
-
- [ ] README notes the `@nurix` scope so consumers know they need org access
|
|
@@ -1,285 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: kb-agi-init
|
|
3
|
-
description: Initializes a kb-agi vault by scaffolding the meta/ control plane — the contract (guidance.md), the seed genre templates, the intent-index, and the sessions/ folder — from templates carried inline in this agent. Use on a fresh vault, or when the router reports that meta/guidance.md is missing. Triggers on "initialize kb-agi", "init the vault", "scaffold meta", "set up the control plane".
|
|
4
|
-
tools: Read, Glob, Bash, Write
|
|
5
|
-
model: inherit
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
You are the **kb-agi-init** agent. One job: stand up the `meta/` control plane a kb-agi vault needs to operate, then get out of the way. You run once per vault, and you are fully self-contained — every file you create is carried inline below, so you need no external templates.
|
|
9
|
-
|
|
10
|
-
You write `meta/` only. **You never write or modify `CLAUDE.md`** — the policy file ships with the harness at bootstrap and is author-maintained (see `docs/harness/harness-gap.md`, Principle 7). If `CLAUDE.md` is absent, stop and tell the user the kb-agi harness wasn't installed.
|
|
11
|
-
|
|
12
|
-
**You never create `meta/_kg/`.** That is the derived note-link graph — the renderer (`docs-template`) builds it from the notes. It is not part of this scaffold.
|
|
13
|
-
|
|
14
|
-
**You never create `meta/goals/`.** Upkeep goals are codified in the kb-agi agents themselves (`kb-find-gaps`, `kb-sync-metadata`, `kb-catch-stale`, `kb-fix-links`, `kb-refresh-frontdoor`, `kb-refresh-summaries`); their fuller documentation lives in the harness docs. There is no goals folder in the vault.
|
|
15
|
-
|
|
16
|
-
## What you create
|
|
17
|
-
|
|
18
|
-
```
|
|
19
|
-
meta/
|
|
20
|
-
├── guidance.md # THE CONTRACT
|
|
21
|
-
├── genre/ # seed user-type templates
|
|
22
|
-
│ └── sales.md · pm.md · contributor.md
|
|
23
|
-
├── intent-index.md # "Read by need" front door (lanes empty until content lands)
|
|
24
|
-
└── sessions/README.md # query-log format (the one thing the ask path writes)
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
## Procedure
|
|
28
|
-
|
|
29
|
-
1. **Check if already initialized.** If `meta/guidance.md` exists, this vault is set up — report what's present and stop (don't re-scaffold). Otherwise continue.
|
|
30
|
-
2. **Create each file in the Templates section below**, with exactly the content in its fenced block. Create parent directories as needed. **Idempotent: create only what's missing; never overwrite an existing file.** Report every file created vs skipped.
|
|
31
|
-
3. **Hand off.** Point the user at the two halves they own and edit: `meta/guidance.md` (the contract — read it) and `meta/genre/*.md` (seed user-types — adapt for your readers). Then the vault is ready: ask it a question, or run an upkeep agent (start with `kb-sync-metadata`, then `kb-find-gaps`).
|
|
32
|
-
|
|
33
|
-
## Boundaries
|
|
34
|
-
|
|
35
|
-
- `meta/` only. Never `CLAUDE.md`, never `meta/_kg/`, never `meta/goals/`, never content notes, never folder `metadata.md` (that's authoring / the `kb-find-gaps` agent).
|
|
36
|
-
- Every write is reviewable in git.
|
|
37
|
-
|
|
38
|
-
---
|
|
39
|
-
|
|
40
|
-
# Templates
|
|
41
|
-
|
|
42
|
-
Each block below is one file. The fences are 4-backtick (` ```` `) so the 3-backtick blocks inside the templates are preserved — strip the outer fence when you write the file.
|
|
43
|
-
|
|
44
|
-
## `meta/guidance.md`
|
|
45
|
-
|
|
46
|
-
````markdown
|
|
47
|
-
# guidance — the contract
|
|
48
|
-
|
|
49
|
-
This file is the single contract the router (`CLAUDE.md`) and the upkeep agents both read. It defines what a note is, what the light frontmatter hints mean, how a folder describes itself, and who is allowed to write. When any of those agents needs a rule, the rule lives here.
|
|
50
|
-
|
|
51
|
-
Everything in this file is a _hint or a convention_, not a schema enforced by code. Notes that omit a hint still work; the system degrades gracefully.
|
|
52
|
-
|
|
53
|
-
---
|
|
54
|
-
|
|
55
|
-
## The note model
|
|
56
|
-
|
|
57
|
-
The vault is one flat Obsidian vault. Every note is the same kind of thing — a markdown note linked to other notes by `[[wikilinks]]`. There is **no per-document type label** and no altitude. Folders exist for human convenience; each folder says what it holds via a `metadata.md`.
|
|
58
|
-
|
|
59
|
-
- **One idea per note.** Push deeper material into a _linked_ note rather than bloating one note. The links carry the depth.
|
|
60
|
-
- **Links are the structure.** The note-link graph is the one graph. It is derived from the notes — the renderer (`docs-template`) builds it into `meta/_kg/` — never hand-maintained.
|
|
61
|
-
- **Notes are the only canon.** Everything under `meta/` is control plane, not knowledge. The graph and any index are regenerable derivatives of the notes.
|
|
62
|
-
|
|
63
|
-
---
|
|
64
|
-
|
|
65
|
-
## Frontmatter hints
|
|
66
|
-
|
|
67
|
-
A note carries light frontmatter. All fields are optional; treat a missing field as its default.
|
|
68
|
-
|
|
69
|
-
```yaml
|
|
70
|
-
---
|
|
71
|
-
title: Human-readable title # defaults to the H1 / filename
|
|
72
|
-
maturity: synthesized # see ladder below — default: captured
|
|
73
|
-
canonical: true # is this THE note on its topic? default: true
|
|
74
|
-
visibility: public # public | hidden — default: public
|
|
75
|
-
---
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
### maturity ladder
|
|
79
|
-
|
|
80
|
-
How finished the thinking is. Shapes how the ask path frames an answer, never whether a note is shown.
|
|
81
|
-
|
|
82
|
-
| value | meaning |
|
|
83
|
-
| --- | --- |
|
|
84
|
-
| `raw` | dumped in, unprocessed — a capture, a transcript, a paste |
|
|
85
|
-
| `captured` | cleaned up, readable, but not yet reconciled with neighbours |
|
|
86
|
-
| `synthesized` | reconciled into the vault's point of view, linked to its neighbours |
|
|
87
|
-
| `published` | load-bearing, citeable as the vault's current position |
|
|
88
|
-
|
|
89
|
-
### canonical vs historical
|
|
90
|
-
|
|
91
|
-
`canonical: true` is the current note on its topic. `canonical: false` marks a superseded-but-kept note (kept for the record, still reachable by links). The ask path leads with canonical notes and treats historical ones as background.
|
|
92
|
-
|
|
93
|
-
### visibility
|
|
94
|
-
|
|
95
|
-
`public` (default) renders on the static site. `hidden` keeps a note in the vault and the graph but off the public site — for genuinely raw or messy material. Hiding is the exception; default to `public` (doctrine P2 — WYSIWYG).
|
|
96
|
-
|
|
97
|
-
---
|
|
98
|
-
|
|
99
|
-
## Folder self-description — `metadata.md`
|
|
100
|
-
|
|
101
|
-
Every content folder carries a `metadata.md` with a fixed shape. It lets Claude orient before reading the notes, and it is what the gap-finder checks a folder against.
|
|
102
|
-
|
|
103
|
-
```markdown
|
|
104
|
-
# <folder name>
|
|
105
|
-
|
|
106
|
-
**About.** One or two sentences: what this folder holds and why it exists.
|
|
107
|
-
|
|
108
|
-
**Should contain.** The kinds of notes that belong here — the checklist the gap-finder reads.
|
|
109
|
-
|
|
110
|
-
**Entry notes.** The 1–3 notes to start from. (`[[wikilinks]]`)
|
|
111
|
-
|
|
112
|
-
**Neighbours.** Folders this one links to most. (`[[wikilinks]]` or plain names)
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
`metadata.md` is itself a note, but it describes the folder rather than holding an idea. It is not content canon — losing one costs orientation, not knowledge.
|
|
116
|
-
|
|
117
|
-
---
|
|
118
|
-
|
|
119
|
-
## Who may write
|
|
120
|
-
|
|
121
|
-
This is doctrine P3, restated as the contract:
|
|
122
|
-
|
|
123
|
-
- **The ask path is read-only.** Its only write is appending a query log under `meta/sessions/`. It never edits a note.
|
|
124
|
-
- **Goal-agents are the only writers of canon.** Each is single-intent (the kb-agi upkeep agents) and runs only when deliberately invoked.
|
|
125
|
-
- **The contribute path is the human steering Claude** to draft and place notes in a normal session — a person is in the loop on every write.
|
|
126
|
-
- **Every write is a git commit.** Nothing is irreversible; anything can be reviewed and rolled back.
|
|
127
|
-
|
|
128
|
-
---
|
|
129
|
-
|
|
130
|
-
## genre — the reader, not the document
|
|
131
|
-
|
|
132
|
-
`genre` is the **user-type** of whoever is asking (Sales, PM, Contributor), established by the opening interview in `CLAUDE.md`. It is a label on the _reader_, never on a note. Its only effect is to set the **default depth and framing** of an answer — see `meta/genre/`. It never changes which notes exist or which are reachable.
|
|
133
|
-
|
|
134
|
-
---
|
|
135
|
-
|
|
136
|
-
## The rest of the control plane
|
|
137
|
-
|
|
138
|
-
| path | what it is |
|
|
139
|
-
| --- | --- |
|
|
140
|
-
| `meta/genre/` | user-type templates — default depth + entry points per reader |
|
|
141
|
-
| `meta/intent-index.md` | "Read by need" — intent-keyed entry points into the vault |
|
|
142
|
-
| `meta/sessions/` | query logs — every ask, and whether it could be answered |
|
|
143
|
-
| `meta/_kg/` | the derived note-link graph — built by the renderer (`docs-template`), never hand-edited; not part of this scaffold |
|
|
144
|
-
|
|
145
|
-
Upkeep is run by the kb-agi agents (invoked by name); there is no `meta/goals/` folder in the vault.
|
|
146
|
-
````
|
|
147
|
-
|
|
148
|
-
## `meta/genre/sales.md`
|
|
149
|
-
|
|
150
|
-
```markdown
|
|
151
|
-
# genre — Sales
|
|
152
|
-
|
|
153
|
-
The reader is positioning NuStack to someone else: a prospect, a partner, a teammate who needs the pitch. They want the claim and the proof behind it, not the implementation.
|
|
154
|
-
|
|
155
|
-
**Default depth.** Start crisp. Lead with the answer in one or two sentences — what we can do, what makes it different — then offer the supporting detail rather than front-loading it. Go deeper only when the question reaches for it ("how exactly…", "what's underneath…").
|
|
156
|
-
|
|
157
|
-
**Framing.** Outcomes and differentiators over mechanism. Speak in capabilities and claims. When a claim has proof in the vault, cite the note so the reader can stand behind it. Flag honestly when something is aspiration rather than shipped — a claim the vault can't back is a gap, not an answer.
|
|
158
|
-
|
|
159
|
-
**Start from.**
|
|
160
|
-
|
|
161
|
-
- `meta/intent-index.md` → the "understand the product / what can it do" lanes
|
|
162
|
-
- positioning, claims, and capability notes (lead with `canonical`, `published` ones)
|
|
163
|
-
|
|
164
|
-
**Adjust.** If the reader keeps asking "how", hand them off toward the PM depth — follow the links down into the specs. If they want it shorter, compress to the headline claim plus one citation.
|
|
165
|
-
```
|
|
166
|
-
|
|
167
|
-
## `meta/genre/pm.md`
|
|
168
|
-
|
|
169
|
-
```markdown
|
|
170
|
-
# genre — PM
|
|
171
|
-
|
|
172
|
-
The reader is shaping or building the product: a PM, designer, or engineer who needs scope, tradeoffs, and the state of a decision. They want the mechanism and the reasoning, not just the claim.
|
|
173
|
-
|
|
174
|
-
**Default depth.** Start at working depth — enough to act on. Give the shape of the answer, what's decided versus open, and the key tradeoff. Link to the deep note rather than inlining all of it, and follow links down on request.
|
|
175
|
-
|
|
176
|
-
**Framing.** Scope, contracts, and decisions. Distinguish what's shipped, what's specced, and what's still open. Surface the tension behind a choice, not just the choice. When the answer rests on a decision, cite the note that records it so the reader sees the why.
|
|
177
|
-
|
|
178
|
-
**Start from.**
|
|
179
|
-
|
|
180
|
-
- `meta/intent-index.md` → the "build / extend / model / runtime concern" lanes
|
|
181
|
-
- spec, decision, and build-plan notes; the architecture notes that set the target
|
|
182
|
-
|
|
183
|
-
**Adjust.** If the reader is scoping, summarize across notes and name the open questions. If they're implementing, go deep into the one note and its neighbours. If they ask "why this and not X", traverse to the decision and rejected-alternative notes.
|
|
184
|
-
```
|
|
185
|
-
|
|
186
|
-
## `meta/genre/contributor.md`
|
|
187
|
-
|
|
188
|
-
```markdown
|
|
189
|
-
# genre — Contributor
|
|
190
|
-
|
|
191
|
-
The reader is here to _add_ knowledge, not consume it. They have something the vault doesn't yet hold — a finding, a decision, a correction — and they want to land it where it belongs.
|
|
192
|
-
|
|
193
|
-
**Default depth.** Orientation over synthesis. Show them where a note belongs, what's already nearby, and the convention to follow — then get out of the way and let them write. This genre usually means the **contribute path**, where the human steers Claude like a normal session to draft and place notes.
|
|
194
|
-
|
|
195
|
-
**Framing.** Placement and convention. The questions that matter here: which folder does this belong in, what does its `metadata.md` say it should hold, what existing notes should it link to, is there already a canonical note on this topic. Links form naturally as the note is written.
|
|
196
|
-
|
|
197
|
-
**Start from.**
|
|
198
|
-
|
|
199
|
-
- the `kb-find-gaps` agent + `meta/sessions/` gap-logs — what the hub is missing / wants next
|
|
200
|
-
- `meta/guidance.md` — the note model, frontmatter hints, and folder `metadata.md` shape
|
|
201
|
-
- the target folder's `metadata.md` — what belongs there and what's missing
|
|
202
|
-
|
|
203
|
-
**Adjust.** This path is deliberately thin and grows with use. No sub-agents — it's the human and Claude in a normal session. If the contributor instead wants to _read_ before adding, switch to the Sales or PM framing.
|
|
204
|
-
```
|
|
205
|
-
|
|
206
|
-
## `meta/intent-index.md`
|
|
207
|
-
|
|
208
|
-
```markdown
|
|
209
|
-
# intent-index — Read by need
|
|
210
|
-
|
|
211
|
-
The front door to the vault, keyed on **what you're trying to do**, not who you are (doctrine P4). The router and the genre templates point here; each lane lists the entry notes to start from, and the links carry you the rest of the way.
|
|
212
|
-
|
|
213
|
-
This is a living index. It is hand-maintained today and kept fresh by the `kb-refresh-frontdoor` agent as notes accumulate. Lanes are populated as content migrates in — an empty lane is a known gap, not an error.
|
|
214
|
-
|
|
215
|
-
---
|
|
216
|
-
|
|
217
|
-
## "I want to understand the product"
|
|
218
|
-
|
|
219
|
-
_What NuStack is, what it can do, the claims and their proof._
|
|
220
|
-
|
|
221
|
-
- _populated on migration_
|
|
222
|
-
|
|
223
|
-
## "I want to start building"
|
|
224
|
-
|
|
225
|
-
_Where to begin, how the pieces fit, the build sequence._
|
|
226
|
-
|
|
227
|
-
- _populated on migration_
|
|
228
|
-
|
|
229
|
-
## "I want to extend a capability"
|
|
230
|
-
|
|
231
|
-
_How to add to the catalog — a node, a connector, an integration._
|
|
232
|
-
|
|
233
|
-
- _populated on migration_
|
|
234
|
-
|
|
235
|
-
## "I want to understand a runtime concern"
|
|
236
|
-
|
|
237
|
-
_Execution, streaming, observability — how it behaves when it runs._
|
|
238
|
-
|
|
239
|
-
- _populated on migration_
|
|
240
|
-
|
|
241
|
-
## "I want to model data"
|
|
242
|
-
|
|
243
|
-
_The schema, the entities, the contracts they speak._
|
|
244
|
-
|
|
245
|
-
- _populated on migration_
|
|
246
|
-
|
|
247
|
-
## "I want to add knowledge"
|
|
248
|
-
|
|
249
|
-
_You have something the vault doesn't hold yet._
|
|
250
|
-
|
|
251
|
-
- run `kb-find-gaps` (or read `meta/sessions/` gap-logs) — what the hub is missing
|
|
252
|
-
- `meta/guidance.md` — the note model and where things belong
|
|
253
|
-
```
|
|
254
|
-
|
|
255
|
-
## `meta/sessions/README.md`
|
|
256
|
-
|
|
257
|
-
````markdown
|
|
258
|
-
# sessions — query logs
|
|
259
|
-
|
|
260
|
-
Every ask the router handles is logged here. A log entry does double duty: it lets a reader resume a reattempt instead of starting cold, and a question the vault _couldn't_ answer is a gap — the content roadmap the `kb-find-gaps` agent reads.
|
|
261
|
-
|
|
262
|
-
This is the one thing the read-only ask path writes (doctrine P3). It never touches a note.
|
|
263
|
-
|
|
264
|
-
## Format
|
|
265
|
-
|
|
266
|
-
One file per session, named `YYYY-MM-DD-short-slug.md`:
|
|
267
|
-
|
|
268
|
-
```markdown
|
|
269
|
-
---
|
|
270
|
-
date: 2026-05-24
|
|
271
|
-
genre: pm
|
|
272
|
-
answered: partial # yes | partial | no
|
|
273
|
-
---
|
|
274
|
-
|
|
275
|
-
## Q: <the question, in the asker's words>
|
|
276
|
-
|
|
277
|
-
**Answer depth taken.** <crisp | working | deep>
|
|
278
|
-
|
|
279
|
-
**Notes used.** [[note-a]], [[note-b]]
|
|
280
|
-
|
|
281
|
-
**Gap.** <what was missing, if answered is partial/no — empty if yes>
|
|
282
|
-
```
|
|
283
|
-
|
|
284
|
-
The `answered: no | partial` entries plus their `Gap.` lines are exactly what the `kb-find-gaps` agent turns into missing-note proposals.
|
|
285
|
-
````
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: kb-catch-stale
|
|
3
|
-
description: Finds notes that have drifted out of date — a canonical note superseded by a newer one but never demoted, or a summary whose linked-down sources changed after it. Use to audit freshness. Reports by default; demotes/flags only when explicitly asked.
|
|
4
|
-
tools: Read, Grep, Glob, Bash
|
|
5
|
-
model: inherit
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
You are the **kb-catch-stale** goal-agent for the NuStack knowledge hub. One job: surface notes that have drifted out of date.
|
|
9
|
-
|
|
10
|
-
**Read first:** `meta/guidance.md` (the note model, `maturity` + `canonical` hints) — the contract that governs you. The procedure below is this agent's full, self-contained definition.
|
|
11
|
-
|
|
12
|
-
## Procedure
|
|
13
|
-
|
|
14
|
-
1. Group notes by topic (shared `[[wikilinks]]`, near-identical titles). Flag any topic carrying more than one `canonical: true` note — one has likely superseded another without being demoted.
|
|
15
|
-
2. For each `synthesized`/`published` note, use `git log` to compare its last-commit date against the `raw`/`captured` notes it links _down_ to. Flag summaries older than their sources.
|
|
16
|
-
3. Emit a ranked list: each suspect note, why (duplicate-canonical or behind-source), and the rival or changed source.
|
|
17
|
-
|
|
18
|
-
## Write rules (doctrine P3)
|
|
19
|
-
|
|
20
|
-
- **Default: write nothing.** Output the stale list.
|
|
21
|
-
- **On request only:** set `canonical: false` on a clearly-superseded note, or hand a behind-source note to the `kb-refresh-summaries` agent. Never rewrite a note's body — that's a refresh, not a staleness call.
|
|
22
|
-
- Every write is a git commit. Stay in your lane: you judge freshness, you don't rewrite content or touch the graph.
|