Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions .claude/helpers/graft-hooks.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#!/usr/bin/env node
const path = require('path');
const fs = require('fs');
const { pathToFileURL } = require('url');
const { execFileSync } = require('child_process');
const dir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
const BAKED = "C:\\Users\\Puper\\AppData\\Roaming\\npm\\node_modules\\@nanonets\\graft\\dist\\claude";

// The dist/claude dir of @nanonets/graft resolved from a base whose node_modules is searched.
function fromPkg(base) {
try {
const pkg = require.resolve('@nanonets/graft/package.json', { paths: [base] });
return path.join(path.dirname(pkg), 'dist', 'claude');
} catch { return null; }
}

// The global node_modules dir per npm (handles Homebrew/Windows/volta). Queried on demand.
function globalRoot() {
try {
const root = execFileSync('npm', ['root', '-g'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], shell: process.platform === 'win32' }).trim();
return root || null;
} catch { return null; /* npm unavailable */ }
}

// The version of the package a dist/claude dir belongs to, or null if unreadable.
function versionOf(distClaude) {
try {
return JSON.parse(fs.readFileSync(path.join(distClaude, '..', '..', 'package.json'), 'utf8')).version || null;
} catch { return null; }
}

// Numeric-dotted compare of the release part; an unreadable version loses to any known one.
function newer(a, b) {
if (!a) return false;
if (!b) return true;
const p = (v) => String(v).split('-')[0].split('.').map((n) => Number(n) || 0);
const pa = p(a), pb = p(b);
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
const d = (pa[i] || 0) - (pb[i] || 0);
if (d !== 0) return d > 0;
}
return false;
}

// The highest-versioned dir in `dirs` that actually contains `name`, or null.
function best(dirs, name) {
let bestDir = null, bestVer = null;
for (const d of dirs) {
if (!d || !fs.existsSync(path.join(d, name))) continue;
const v = versionOf(d);
if (bestDir === null || newer(v, bestVer)) { bestDir = d; bestVer = v; }
}
return bestDir;
}

function entry(name) {
// Cheap candidates first, and only shell out to npm when every one of them misses.
const cheap = [BAKED, fromPkg(dir), fromPkg(path.join(path.dirname(process.execPath), '..', 'lib'))];
const hit = best(cheap, name);
if (hit) return path.join(hit, name);
const gr = globalRoot();
const global = gr && path.join(gr, '@nanonets', 'graft', 'dist', 'claude');
if (global && fs.existsSync(path.join(global, name))) return path.join(global, name);
return path.join(dir, 'dist', 'claude', name); // last-ditch; import will no-op if absent
}

import(pathToFileURL(entry("hooks.js")).href).then((m) => m.main(process.argv[2])).catch(() => { /* graft unavailable — no-op */ });
67 changes: 67 additions & 0 deletions .claude/helpers/graft-statusline.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#!/usr/bin/env node
const path = require('path');
const fs = require('fs');
const { pathToFileURL } = require('url');
const { execFileSync } = require('child_process');
const dir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
const BAKED = "C:\\Users\\Puper\\AppData\\Roaming\\npm\\node_modules\\@nanonets\\graft\\dist\\claude";

// The dist/claude dir of @nanonets/graft resolved from a base whose node_modules is searched.
function fromPkg(base) {
try {
const pkg = require.resolve('@nanonets/graft/package.json', { paths: [base] });
return path.join(path.dirname(pkg), 'dist', 'claude');
} catch { return null; }
}

// The global node_modules dir per npm (handles Homebrew/Windows/volta). Queried on demand.
function globalRoot() {
try {
const root = execFileSync('npm', ['root', '-g'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], shell: process.platform === 'win32' }).trim();
return root || null;
} catch { return null; /* npm unavailable */ }
}

// The version of the package a dist/claude dir belongs to, or null if unreadable.
function versionOf(distClaude) {
try {
return JSON.parse(fs.readFileSync(path.join(distClaude, '..', '..', 'package.json'), 'utf8')).version || null;
} catch { return null; }
}

// Numeric-dotted compare of the release part; an unreadable version loses to any known one.
function newer(a, b) {
if (!a) return false;
if (!b) return true;
const p = (v) => String(v).split('-')[0].split('.').map((n) => Number(n) || 0);
const pa = p(a), pb = p(b);
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
const d = (pa[i] || 0) - (pb[i] || 0);
if (d !== 0) return d > 0;
}
return false;
}

// The highest-versioned dir in `dirs` that actually contains `name`, or null.
function best(dirs, name) {
let bestDir = null, bestVer = null;
for (const d of dirs) {
if (!d || !fs.existsSync(path.join(d, name))) continue;
const v = versionOf(d);
if (bestDir === null || newer(v, bestVer)) { bestDir = d; bestVer = v; }
}
return bestDir;
}

function entry(name) {
// Cheap candidates first, and only shell out to npm when every one of them misses.
const cheap = [BAKED, fromPkg(dir), fromPkg(path.join(path.dirname(process.execPath), '..', 'lib'))];
const hit = best(cheap, name);
if (hit) return path.join(hit, name);
const gr = globalRoot();
const global = gr && path.join(gr, '@nanonets', 'graft', 'dist', 'claude');
if (global && fs.existsSync(path.join(global, name))) return path.join(global, name);
return path.join(dir, 'dist', 'claude', name); // last-ditch; import will no-op if absent
}

import(pathToFileURL(entry("statusline.js")).href).then((m) => m.main()).catch(() => { /* graft unavailable — no-op */ });
37 changes: 37 additions & 0 deletions .claude/napkin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Napkin Runbook

## Curation Rules
- Re-prioritize on every read.
- Keep recurring, high-value notes only.
- Max 10 items per category.
- Each item includes date + "Do instead".

## User Directives
1. **[2026-10-20] Responder en español**
Do instead: escribe al usuario en español. Caveman en nivel lite, con artículos, porque el nivel full rompe la gramática.
2. **[2026-10-20] No dar la razón sin contraste**
Do instead: si el encargo choca con el repo o con otra regla del mismo encargo, dilo y aplica la variante que conserve el código estable.
3. **[2026-10-20] No inventar**
Do instead: si un comando, servidor MCP o skill no está, dilo. No rellenes con una configuración supuesta.
4. **[2026-10-20] Una funcionalidad, una rama**
Do instead: crea la rama antes de editar código de producto. No mezcles documentación de agente con un cambio de `lib/` en el mismo diff si el usuario pidió las dos cosas por separado.

## Execution & Validation (Highest Priority)
1. **[2026-10-20] Graft antes de abrir fuentes**
Do instead: `graft ask`, `graft grep`, `graft skeleton`, `graft callers` o `graft map`. Abre solo el tramo `file:line` que el grafo no haya cerrado.
2. **[2026-10-20] Plan en fichero**
Do instead: crea `docs/workflow/plans/AAAA-MM-DD-tema.md` antes de editar código.
3. **[2026-10-21] Git con dueño dudoso**
Do instead: no ejecutes `git config --global`. Usa `git -c safe.directory=D:/Scripts/BreakingLab` en el comando. `tea` dentro de esta carpeta falla por lo mismo: lánzalo desde otro directorio.
4. **[2026-10-21] Gitea antes que GitHub**
Do instead: el primer remoto es `https://gitea.tiizss.internal/tiizss/BreakingLab`. GitHub (`origin`, `TiiZss/BreakingLab`) va después. SSH al puerto 22 está cerrado; usa HTTPS.
5. **[2026-10-21] Correo de los commits hacia GitHub**
Do instead: no cambies `git config`. En el comando de commit exporta `GIT_AUTHOR_EMAIL` y `GIT_COMMITTER_EMAIL` a `9557246+TiiZss@users.noreply.github.com`. `github@tiizss.com` hace que GitHub rechace el push con `GH007`.
6. **[2026-10-20] Copia antes de reescribir lógica**
Do instead: copia los ficheros afectados a `.backups/AAAA-MM-DD/`. No archives el repo entero.

## Domain Behavior Guardrails
1. **[2026-10-20] Laboratorios vulnerables a propósito**
Do instead: el lanzador puede arrancar imágenes vulnerables. No añadas exploits ni copies skills ofensivas al repo. Una auditoría mira el script, los hooks y la exposición de red.
2. **[2026-10-20] Sin Python y sin UI**
Do instead: no crees `pyproject.toml` ni apliques skills de frontend. `uv` queda escrito para el día en que haya Python.
78 changes: 78 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
{
"statusLine": {
"type": "command",
"command": "node \"${CLAUDE_PROJECT_DIR:-.}/.claude/helpers/graft-statusline.cjs\""
},
"subagentStatusLine": {
"type": "command",
"command": "node \"${CLAUDE_PROJECT_DIR:-.}/.claude/helpers/graft-statusline.cjs\""
},
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit|MultiEdit",
"hooks": [
{
"type": "command",
"command": "node \"${CLAUDE_PROJECT_DIR:-.}/.claude/helpers/graft-hooks.cjs\" post-edit",
"timeout": 10000
}
]
},
{
"matcher": "Bash|mcp__graft__|Read|Grep|Glob",
"hooks": [
{
"type": "command",
"command": "node \"${CLAUDE_PROJECT_DIR:-.}/.claude/helpers/graft-hooks.cjs\" tool-savings",
"timeout": 8000
}
]
}
],
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "node \"${CLAUDE_PROJECT_DIR:-.}/.claude/helpers/graft-hooks.cjs\" prompt",
"timeout": 15000
}
]
}
],
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "node \"${CLAUDE_PROJECT_DIR:-.}/.claude/helpers/graft-hooks.cjs\" session-start",
"timeout": 8000
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "node \"${CLAUDE_PROJECT_DIR:-.}/.claude/helpers/graft-hooks.cjs\" stop",
"timeout": 8000
}
]
}
]
},
"footerLinksRegexes": [
"graft/[\\w./-]+\\.md"
],
"permissions": {
"allow": [
"Bash(graft:*)",
"Bash(npx graft:*)",
"Bash(graft-dev:*)",
"Bash(node dist/cli.js:*)"
]
}
}
21 changes: 21 additions & 0 deletions .cursor/hooks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"version": 1,
"hooks": {
"postToolUse": [
{
"matcher": "Read|Grep|Glob|Search|Shell",
"command": "node \"D:\\Scripts\\BreakingLab\\.cursor\\hooks\\graft-hooks.cjs\" cursor-post-tool"
}
],
"afterMCPExecution": [
{
"command": "node \"D:\\Scripts\\BreakingLab\\.cursor\\hooks\\graft-hooks.cjs\" cursor-mcp"
}
],
"sessionEnd": [
{
"command": "node \"D:\\Scripts\\BreakingLab\\.cursor\\hooks\\graft-hooks.cjs\" cursor-session-end"
Comment on lines +7 to +17
}
]
}
}
67 changes: 67 additions & 0 deletions .cursor/hooks/graft-hooks.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#!/usr/bin/env node
const path = require('path');
const fs = require('fs');
const { pathToFileURL } = require('url');
const { execFileSync } = require('child_process');
const dir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
const BAKED = "C:\\Users\\Puper\\AppData\\Roaming\\npm\\node_modules\\@nanonets\\graft\\dist\\claude";

// The dist/claude dir of @nanonets/graft resolved from a base whose node_modules is searched.
function fromPkg(base) {
try {
const pkg = require.resolve('@nanonets/graft/package.json', { paths: [base] });
return path.join(path.dirname(pkg), 'dist', 'claude');
} catch { return null; }
}

// The global node_modules dir per npm (handles Homebrew/Windows/volta). Queried on demand.
function globalRoot() {
try {
const root = execFileSync('npm', ['root', '-g'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], shell: process.platform === 'win32' }).trim();
return root || null;
} catch { return null; /* npm unavailable */ }
}

// The version of the package a dist/claude dir belongs to, or null if unreadable.
function versionOf(distClaude) {
try {
return JSON.parse(fs.readFileSync(path.join(distClaude, '..', '..', 'package.json'), 'utf8')).version || null;
} catch { return null; }
}

// Numeric-dotted compare of the release part; an unreadable version loses to any known one.
function newer(a, b) {
if (!a) return false;
if (!b) return true;
const p = (v) => String(v).split('-')[0].split('.').map((n) => Number(n) || 0);
const pa = p(a), pb = p(b);
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
const d = (pa[i] || 0) - (pb[i] || 0);
if (d !== 0) return d > 0;
}
return false;
}

// The highest-versioned dir in `dirs` that actually contains `name`, or null.
function best(dirs, name) {
let bestDir = null, bestVer = null;
for (const d of dirs) {
if (!d || !fs.existsSync(path.join(d, name))) continue;
const v = versionOf(d);
if (bestDir === null || newer(v, bestVer)) { bestDir = d; bestVer = v; }
}
return bestDir;
}

function entry(name) {
// Cheap candidates first, and only shell out to npm when every one of them misses.
const cheap = [BAKED, fromPkg(dir), fromPkg(path.join(path.dirname(process.execPath), '..', 'lib'))];
const hit = best(cheap, name);
if (hit) return path.join(hit, name);
const gr = globalRoot();
const global = gr && path.join(gr, '@nanonets', 'graft', 'dist', 'claude');
if (global && fs.existsSync(path.join(global, name))) return path.join(global, name);
return path.join(dir, 'dist', 'claude', name); // last-ditch; import will no-op if absent
}

import(pathToFileURL(entry("hooks.js")).href).then((m) => m.main(process.argv[2])).catch(() => { /* graft unavailable — no-op */ });
12 changes: 12 additions & 0 deletions .cursor/mcp.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"mcpServers": {
"graft": {
"command": "npx",
"args": [
"-y",
"@nanonets/graft",
"mcp"
]
}
}
}
43 changes: 43 additions & 0 deletions .cursor/rules/graft.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
---
description: Use the Graft context graph in graft/ before exploring source
alwaysApply: true
---
## Graft — repo context graph

This repo is indexed in `graft/`: small linked markdown nodes that explain each
system and carry exact file:line spans, kept in sync with the code through git.

For ANY task here — understanding how something works, finding where code lives,
or scoping a change — get context from the graph before grepping or opening
source files. Re-ask freely (it's cheap) and reuse literal identifiers you
already have (symbol, error string, file name) as the query. New to this repo?
Run `graft map` first — a token-budgeted orientation (dir clusters, hubs,
hotspots), no LLM, no key.

- Run `graft ask "<your question>" --source` → ranked nodes with the relevant
code spans inlined (each hit's ≤8-line crux by default; `--full` for whole
definitions when the crux isn't enough). Match the tool to the task shape:
for understanding or editing, the top node IS the answer — cite its
`covers:` file:line spans and edit straight from `--source`. For
exhaustive tasks ("every occurrence / every caller of this pattern"), ranked
results are top-N, not complete — run `graft grep "<literal>"` instead
(exhaustive over indexed files, grouped by enclosing symbol), falling back
to raw `grep -rn` only for unindexed files.
- `graft skeleton <file>` → every definition's signature + span, ~10× cheaper
than reading the file; use it to skim an API surface.
- `graft callers <symbol>` gives precomputed, exact edges — who calls this.
Add `--direction out` for what it calls, or `--depth N` to walk
transitively for the full blast radius. For structural questions, skip
ranking and use this directly.
- Or browse: `graft/INDEX.md` lists every node; follow the links.
- Monorepos and folders of multiple repos rank fairly across sub-projects —
hits carry `[scope/]` labels naming which one they're from. Narrow with
`graft ask "<task>" --in <scope>/` once you know where you're working.

If a returned span is truncated ("+N more lines"), open the file at that exact
range before finalizing. Only open source files when a node genuinely lacks a
needed detail, and then at the exact file:line the node points to — never
re-read whole files.

After big code changes, refresh the graph with `graft build` (deterministic,
no API key, $0).
Loading
Loading