Install
$ agentstack add skill-itsalt-nacl-nacl-core ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
Graph Core — Shared References
This skill is NOT invocable by users. It provides shared references, templates, and utilities for all nacl-* skills.
Neo4j Connection
All graph skills use Neo4j MCP tools:
mcp__neo4j__read-cypher— read-only queriesmcp__neo4j__write-cypher— create/update/deletemcp__neo4j__get-schema— introspect schema
Connection is managed by the MCP server (configured in .mcp.json at project root). Skills do NOT pass connection strings — they just call MCP tools.
Graph Config Resolution (execute at skill start)
Every graph skill MUST read config.yaml at the start to resolve graph settings.
Steps:
- Read
config.yamlfrom the project root (current working directory) - Extract the
graphsection - If
graphsection missing orconfig.yamlabsent → use all defaults - Store resolved values for use in error messages and board paths:
$neo4j_bolt_port= graph.neo4jboltport || 3587$neo4j_http_port= graph.neo4jhttpport || 3574$neo4j_password= graph.neo4jpassword || "neo4jgraph_dev"$excalidraw_port= graph.excalidraw_port || 3580$boards_dir= graph.boards_dir || "graph-infra/boards"$container_prefix= graph.container_prefix || project.name || "graph"
Configuration Resolution Table
| Data | Source priority | |------|---------------| | Neo4j Bolt port | graph.neo4j_bolt_port > fallback 3587 | | Neo4j HTTP port | graph.neo4j_http_port > fallback 3574 | | Neo4j password | graph.neo4j_password > fallback "neo4j_graph_dev" | | Excalidraw port | graph.excalidraw_port > fallback 3580 | | Boards directory | graph.boards_dir > fallback "graph-infra/boards" | | Container prefix | graph.container_prefix > project.name > fallback "graph" |
Important:
- Do NOT pass connection strings to MCP tools — the MCP server handles this
- These values are ONLY for: error messages, Docker commands, board file paths, Excalidraw URLs
- When showing error messages, always use resolved values:
bolt://localhost:{$neo4j_bolt_port}
Schema Reference
Schema files: graph-infra/schema/
ba-schema.cypher— BA layer (13 node types: ProcessGroup, BusinessProcess, WorkflowStep, BusinessEntity, EntityAttribute, EntityState, BusinessRole, BusinessRule, GlossaryTerm, SystemContext, Stakeholder, ExternalEntity, DataFlow)sa-schema.cypher— SA layer (12 node types)tl-schema.cypher— TL layer (3 node types)
Task.verification_evidence — Taxonomy
Task.verification_evidence is a string property on TL Task nodes that records how a task was verified. It is read by nacl-tl-release (Step 2 pre-merge gate and the final report Evidence-level column) and must be written by every skill that advances a Task to a terminal status. Leaving it NULL on a done task causes the release skill to surface a "Verification gap" — that is a data-integrity failure, not normal output.
Values
| Value | When to write | Written by | |---|---|---| | test-GREEN: | PASS + regression test ran RED→GREEN. ` is a repo-relative path to the test file (e.g. apps/web/src/__tests__/funnel.spec.ts) or to the task's regression-test record (.tl/tasks//regression-test.md). | conductor Phase 3, tl-full Phase 7, tl-fix terminal step, tl-deliver, tl-hotfix | | test-UNVERIFIED | Code applied but RED→GREEN not confirmed (no regression test, or sub-skill returned UNVERIFIED). Paired with t.status = 'verified-pending'. | Same writers as above | | no-test | PASS under explicit user --no-test (or equivalent) override. Paired with t.status = 'done'. | Same writers as above | | null (unset) | Reserved for t.status ∈ {'failed', 'blocked'}` — those tasks are excluded from release scope, so an evidence string would be misleading. | n/a |
Format rules
- Single string, no JSON.
test-GREENpayload after:is a forward-slash repo-relative path; no quoting, no scheme.- Use the test file path when one exists, otherwise the
.tl/tasks//regression-test.mdartifact path. test-UNVERIFIEDandno-testcarry no payload — exactly those literal strings.
Reader contract
nacl-tl-release parses verification_evidence like this:
- Prefix
test-GREEN:→Evidence level = test-GREEN, path extracted for the report column. - Literal
test-UNVERIFIED→Evidence level = test-UNVERIFIED. - Literal
no-test→Evidence level = no-test. NULL/ empty / unrecognised →Evidence level = unknown→ "Verification gap" footer.
Sources: nacl-tl-release/SKILL.md:183, :615-622.
Writer obligation
Every skill in this list MUST set verification_evidence in the same Cypher statement that sets t.status to a terminal value:
nacl-tl-conductor— Phase 3 graph-write block (PASS / UNVERIFIED writes).nacl-tl-full— Phase 7 final aggregation graph-write.nacl-tl-fix— terminal graph-write after fix verified.nacl-tl-deliver— stamping after delivery validation.nacl-tl-hotfix— hotfix Task node graph-write.
Skills that only read terminal status (nacl-tl-release, nacl-tl-deploy, nacl-tl-reconcile) do NOT write this field. They may, however, gate on it: if a writer leaves it NULL, an orchestrator gate (e.g. conductor Phase 4) MUST HALT and surface the contract violation.
Graph Skills Registry
BA Layer (14 skills)
| Skill | Purpose | |-------|---------| | nacl-ba-context | System boundaries → Neo4j | | nacl-ba-process | Business process map → Neo4j | | nacl-ba-workflow | Activity diagrams → Neo4j | | nacl-ba-entities | Entity catalog → Neo4j | | nacl-ba-roles | Business roles → Neo4j | | nacl-ba-glossary | Glossary → Neo4j | | nacl-ba-rules | Business rules → Neo4j | | nacl-ba-validate | L1-L8 + XL1-XL5 validation | | nacl-ba-handoff | BA→SA traceability | | nacl-ba-full | Full BA orchestrator (10 phases) | | nacl-ba-from-board | Board orchestrator (import/sync/enrich/validate/handoff) | | nacl-ba-import-doc | Document → Excalidraw | | nacl-ba-analyze | Board completeness analysis | | nacl-ba-sync | Board → Neo4j sync |
SA Layer (9 skills)
| Skill | Purpose | |-------|---------| | nacl-sa-architect | Module decomposition | | nacl-sa-domain | Domain model | | nacl-sa-roles | Role model + permissions | | nacl-sa-uc | UC registry + detailing | | nacl-sa-ui | UI architecture | | nacl-sa-validate | L1-L13 + XL6-XL9 validation | | nacl-sa-feature | Incremental feature specification | | nacl-sa-finalize | Statistics, ADR, readiness | | nacl-sa-full | Full SA orchestrator (10 phases) |
TL Layer (7 skills)
| Skill | Purpose | |-------|---------| | nacl-tl-plan | Task planning from graph | | nacl-tl-intake | Graph-aware triage | | nacl-tl-status | Status + SA coverage | | nacl-tl-next | Next task + SA context | | nacl-tl-full | Full lifecycle orchestrator | | nacl-tl-conductor | Batch process manager | | nacl-tl-hotfix | Emergency hotfix to main |
Output (2 skills)
| Skill | Purpose | |-------|---------| | nacl-render | Graph → Markdown + Excalidraw | | nacl-publish | Graph → Docmost |
Query library: graph-infra/queries/
ba-queries.cypher,sa-queries.cypher,handoff-queries.cypher,validation-queries.cypher,tl-queries.cypher
Excalidraw File-Based Workflow
Board files stored in: {$boards_dir}/*.excalidraw (where $boards_dir is resolved from config.yaml → graph.boards_dir, default: graph-infra/boards)
AI reads/writes .excalidraw JSON directly (no MCP server needed).
Excalidraw JSON Format
{
"type": "excalidraw",
"version": 2,
"elements": [...],
"appState": {"viewBackgroundColor": "#ffffff"}
}
Element Types Used
| BA Concept | Excalidraw Type | backgroundColor | customData.nodeType | |---|---|---|---| | WorkflowStep (бизнес-функция) | rectangle | #e8f5e9 (green) | WorkflowStep | | WorkflowStep (автоматизируется) | rectangle | #e3f2fd (blue) | WorkflowStep | | Decision | diamond | #fff3e0 (orange) | Decision | | BusinessEntity / Document | rectangle | #f3e5f5 (purple) | BusinessEntity | | BusinessRole (swimlane label) | rectangle | #fafafa (grey) | BusinessRole | | Annotation / Note | text | — | Annotation | | Connection | arrow | — | — |
Color Coding for Confidence
| Confidence | strokeColor | Meaning | |---|---|---| | High (confirmed) | #2e7d32 (green) | Clearly identified in source | | Medium (assumption) | #f57f17 (amber) | AI inferred from context | | Low (needs info) | #c62828 (red) | Missing information |
customData Structure
Every shape element MUST have customData:
{
"customData": {
"nodeId": "BP-001-S03", // Neo4j node id (null if not synced)
"nodeType": "WorkflowStep", // Graph node label
"confidence": "high", // high | medium | low
"sourceDoc": "process.docx", // source document (if from import)
"sourcePage": 3, // page number in source
"synced": false // true after /nacl-ba-sync
}
}
Excalidraw Element Template (rectangle with text)
To create a labeled rectangle, you need TWO elements:
- The shape (rectangle/diamond) with
boundElementspointing to text - The text element with
containerIdpointing back to shape
{
"id": "rect-001",
"type": "rectangle",
"x": 100, "y": 200,
"width": 200, "height": 60,
"strokeColor": "#2e7d32",
"backgroundColor": "#e3f2fd",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"seed": 12345,
"version": 1,
"versionNonce": 1,
"isDeleted": false,
"groupIds": [],
"frameId": null,
"boundElements": [{"id": "text-001", "type": "text"}],
"updated": 1,
"link": null,
"locked": false,
"customData": {
"nodeId": "BP-001-S03",
"nodeType": "WorkflowStep",
"confidence": "high",
"synced": false
}
}
Layout Guidelines
- Swimlanes: horizontal bands, 200px height each
- Steps: left-to-right flow, 220px spacing
- Documents: right column, 50px margin from steps
- Decisions: centered between alternative paths
- Arrows: use startBinding/endBinding for connected arrows
Board Meta Sidecar (.meta.json)
Every .excalidraw board file may have a companion sidecar file at {$boards_dir}/.meta.json. This file is the source of truth for "when was this board last generated from the graph" and "when was it last synced back". The NaCl Analyst Tool (analyst-tool/) reads the sidecar to display sync status in its sidebar tree — without the sidecar the tool cannot determine freshness. Skills are the canonical writers of this file; the tool only reads it (and may write optimistically as a fallback when a skill run fails partway through).
Schema
{
"lastGeneratedAt": "2026-05-03T18:42:00.000Z",
"lastGeneratedBy": "nacl-render",
"lastSyncedAt": "2026-05-03T19:00:00.000Z",
"lastSyncStatus": "ok",
"lastSyncRunId": "r-95f71b58",
"contentHashAtLastSync": "sha256:"
}
All six fields are nullable (null when not yet set). Dates are ISO-8601 UTC strings.
Field Semantics
| Field | Type | Writer | Null means | |---|---|---|---| | lastGeneratedAt | ISO-8601 string \| null | nacl-render | board was never generated from graph | | lastGeneratedBy | string \| null | nacl-render | board was never generated from graph | | lastSyncedAt | ISO-8601 string \| null | nacl-ba-sync (success only) | board was never synced to graph | | lastSyncStatus | "ok" \| "failed" \| null | nacl-ba-sync | no sync has been attempted | | lastSyncRunId | string \| null | nacl-ba-sync | invoked directly (not via analyst-tool), or no run ID available | | contentHashAtLastSync | "sha256:" \| null | nacl-render and nacl-ba-sync (success only) | no render or sync has completed |
Who Writes What
| Action | Fields written | |---|---| | Render from graph (success) | lastGeneratedAt, lastGeneratedBy, contentHashAtLastSync | | Sync to graph (success) | lastSyncedAt, lastSyncStatus = "ok", lastSyncRunId, contentHashAtLastSync | | Sync to graph (failure) | lastSyncStatus = "failed", lastSyncRunId | | Analyst-tool save | nothing — the tool does not write meta; hasUnsyncedEdits is derived at read time from mtime + recomputed hash |
Content Hash Algorithm
contentHashAtLastSync is a SHA-256 hash of a normalized representation of the board scene. The algorithm (canonical TypeScript implementation: analyst-tool/server/src/services/meta.ts, function computeBoardHash):
- Extract three top-level fields from the
.excalidrawJSON:elements(array),appState(object),files(object). - Normalize
appState: keep onlyviewBackgroundColorandgridSize; all other keys are dropped. Missing keys default tonull. - Normalize each element in
elements:
- Strip the following volatile per-element keys:
version,versionNonce,seed,updated. - Sort the remaining keys alphabetically (ascending).
- Produce a new object with only the sorted, non-volatile keys.
- Assemble the normalized scene object:
``json { "elements": [ ...normalized elements... ], "appState": { "viewBackgroundColor": , "gridSize": }, "files": } ``
- Serialize with
JSON.stringify(no extra whitespace, no key sorting at the top level — the top-level key order iselements,appState,filesas shown above). - Hash the UTF-8 bytes of the JSON string with SHA-256.
- Prefix the lowercase hex digest with
sha256:.
Result format: "sha256:".
Any re-implementation (Python, shell, etc.) must produce the same byte sequence as the TypeScript reference for identical input. The key sort in step 3 is per-element, not recursive — nested objects inside element properties are not sorted.
Atomic Write Requirement
To prevent the analyst-tool fs-watcher from reading a half-written file, the sidecar must be written atomically:
- Write the new JSON content to
.meta.json.tmp(same directory as the board). - Rename
.meta.json.tmp→.meta.json.
On POSIX filesystems rename(2) is atomic. On Windows the tool is expected to run on macOS/Linux so this guarantee holds.
Merge Rule
A skill never blindly overwrites the entire sidecar. Before writing, it reads the existing sidecar (or uses all-null defaults if missing), merges only the fields it owns (per the "Who Writes What" table above), and writes the merged result. This ensures nacl-render does not destroy lastSyncedAt set by a previous nacl-ba-sync run, and vice versa.
Analyst Tool Integration
The meta sidecar is also read by the local NaCl Analyst Tool (analyst-tool/), which uses it to display sync status in the sidebar tree. The tool writes meta optimistically as a fallback in case a skill run fails partway, but the skills themselves are the canonical writers per the table above.
Project Initialization
The /nacl-init skill creates or updates CLAUDE.md, config.yaml, and graph infrastructure for a project. Re-running /nacl-init on an existing project is idempotent and triggers automatic migration of legacy infrastructure (removing stale excalidraw containers, creating missing graph-infra/boards/, injecting missing project.id / project.name fields) — see nacl-init/SKILL.md § Auto-migrate Legacy Artefacts for details.
Project Registry (~/.nacl/projects.json)
The NaCl Analyst Tool discovers projects through a per-user registry at ~/.nacl/projects.json (override via NACL_HOME env var — see docs/configuration.md). The registry is written only by nacl-init (Step 2d); the analyst-tool and all other skills only read it. The canonical TypeScript types (ProjectRecord, ProjectRegistry) and the atomic-write implementation live in analyst-tool/server/src/services/project-registry.ts. Every entry has the fields id, name, root, createdAt, and
…
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: ITSalt
- Source: ITSalt/NaCl
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.