Install
$ agentstack add skill-itsalt-nacl-nacl-publish ✓ 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.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
/nacl-publish -- Граф -> Docmost + Excalidraw Boards
Назначение
Публикация данных из Neo4j-графа во внешние форматы:
- Docmost -- генерация markdown из графа (через
nacl-render md) и публикация страниц в Docmost - Excalidraw Boards -- генерация визуальных бордов из графа и привязка к страницам Docmost
Neo4j Graph
|
├── nacl-render md ──> Markdown ──> Docmost Pages (/nacl-publish docmost)
|
├── nacl-render excalidraw ──> .excalidraw files (/nacl-publish boards)
|
└── Excalidraw links ──> Docmost Pages updated (/nacl-publish boards-link)
Dependencies
nacl-core/SKILL.md-- shared Neo4j connection, schema, ID rulesnacl-render/SKILL.md-- md and excalidraw rendering logic- Neo4j MCP:
mcp__neo4j__read-cypher - Docmost MCP:
mcp__docmost__create_page,mcp__docmost__update_page,mcp__docmost__list_spaces,mcp__docmost__get_page,mcp__docmost__list_pages,mcp__docmost__search
Config Resolution
| Параметр | Источник (по приоритету) | Fallback | |----------|--------------------------|----------| | Docmost API URL | config.yaml → docmost.api_url | Использовать env-настройки Docmost MCP | | space_id (graph scope) | config.yaml → docmost.spaces.graph.space_id > docmost.spaces.sa.space_id > manifest space_id | Спросить пользователя через mcp__docmost__list_spaces | | root_page_id (graph scope) | config.yaml → docmost.spaces.graph.root_page_id > docmost.spaces.sa.root_page_id > manifest root_page_id | Создать новую корневую страницу | | Boards directory | config.yaml → graph.boards_dir | graph-infra/boards | | Neo4j Bolt port | config.yaml → graph.neo4j_bolt_port | 3587 |
Принцип: config.yaml — первый источник для адресации Docmost. Manifest (.docmost-sync.json) отвечает только за page-level sync state (page IDs, content hashes, last_updated). Если в проекте нет отдельного spaces.graph — fallback на spaces.sa, поскольку graph и SA публикуют один и тот же набор страниц.
Invocation
/nacl-publish [args]
Commands Overview
| Command | Description | Status | |---------|-------------|--------| | docmost | Full publish: generate md from graph, create/update all pages | Implemented | | docmost-incremental | Publish only nodes changed since last sync | Implemented | | docmost-preview | Preview one page in terminal (no publish) | Implemented | | boards | Generate all Excalidraw boards from graph | Implemented | | boards-link | Add board links/embeds to Docmost pages | Implemented | | full | Complete pipeline: docmost + boards + boards-link | Implemented |
Manifest: .docmost-sync.json
Located at project root (next to graph-infra/). This file tracks synchronization state between the graph and Docmost.
Structure
{
"project": "project-name",
"space_id": "019cd479-...",
"root_page_id": "019cd6de-...",
"last_sync": "2026-03-20T14:30:00Z",
"pages": {
"DE-Order": {
"page_id": "019cd6f0-...",
"parent_page_id": "019cd6df-33de-...",
"content_hash": "sha256:a1b2c3d4...",
"last_updated": "2026-03-20T14:30:00Z",
"source_type": "entity",
"source_id": "DE-Order"
},
"UC-101": {
"page_id": "019cd6f1-...",
"parent_page_id": "019cd6df-4348-...",
"content_hash": "sha256:e5f6a7b8...",
"last_updated": "2026-03-20T14:30:00Z",
"source_type": "uc",
"source_id": "UC-101"
}
},
"sections": {
"Архитектура": "019cd6df-2475-...",
"Domain Model": "019cd6df-33de-...",
"Use Cases": "019cd6df-4348-...",
"Интерфейсы": "019cd6df-5512-...",
"Трассировка": "019cd6df-6623-...",
"Роли и права": "019cd6df-7734-..."
}
}
Fields
| Field | Description | |-------|-------------| | project | Project name (from graph or user input) | | space_id | Cached Docmost space ID. Source of truth: config.yaml → docmost.spaces.graph.space_id (fallback spaces.sa.space_id). Manifest stores it for offline reference; config.yaml wins on conflict. | | root_page_id | Cached root page ID. Source of truth: config.yaml → docmost.spaces.graph.root_page_id (fallback spaces.sa.root_page_id). Manifest stores it for offline reference; config.yaml wins on conflict. | | last_sync | ISO 8601 timestamp of the last full or incremental sync | | pages | Map: logical page key -> {page_id, parent_page_id, content_hash, last_updated, source_type, source_id} | | sections | Map: section name -> Docmost page ID (section pages serve as parents) |
Content Hash
Compute SHA-256 of the generated markdown content (trimmed trailing whitespace per line):
echo "$CONTENT" | sed 's/[[:space:]]*$//' | shasum -a 256 | cut -d' ' -f1
Page Structure in Docmost
The hierarchy mirrors the project's artifact types, organized by modules:
{Project}/
├── Архитектура/
│ ├── Context Map
│ └── Модули
├── Domain Model/
│ ├── {Entity1}
│ ├── {Entity2}
│ └── ...
├── Use Cases/
│ ├── UC Index
│ ├── {UC-101}
│ ├── {UC-102}
│ └── ...
├── Интерфейсы/
│ ├── {Form1}
│ ├── {Form2}
│ └── ...
├── Трассировка/
│ └── BA -> SA Matrix
└── Роли и права/
└── Permission Matrix
Section-to-Graph Mapping
| Section | Graph Source | nacl-render command | |---------|-------------|---------------------| | Context Map | Module nodes + DEPENDS_ON edges | md domain-model (module overview) | | Domain Model / {Entity} | DomainEntity + attrs + rels | md entity | | UC Index | All UseCase nodes | md uc-index | | Use Cases / {UC} | UseCase + steps + forms | md uc | | Интерфейсы / {Form} | Form + fields + mapping | md form | | Трассировка | BA->SA handoff edges | md traceability | | Роли и права | SystemRole + Permission nodes | Custom query (see below) |
Pre-flight Checks
Before any Docmost command, verify:
- Docmost MCP available? Call
mcp__docmost__list_spaces.
- If fails ->
ERROR: Docmost MCP not available. Check that the MCP server is connected.
- Neo4j available? Call
mcp__neo4j__read-cypherwithRETURN 1.
- If fails ->
ERROR: Neo4j not available. Check config.yaml → graph.neo4j_bolt_port (default: 3587) and ensure Docker is running.
- Graph has data? Query:
``cypher MATCH (n) WITH labels(n) AS lbls, count(*) AS cnt UNWIND lbls AS lbl RETURN lbl, sum(cnt) AS total ORDER BY lbl ``
- If empty ->
WARNING: Graph is empty. Run /nacl-ba-from-board or seed data first.
- Manifest exists? Read
.docmost-sync.json.
- If missing and command is not
docmost(full) -> suggest running/nacl-publish docmostfirst.
Pre-publish reconciliation gate
nacl-publish writes externally-visible artifacts (Docmost pages, Excalidraw boards). Publishing inconsistent state to Docmost makes the drift visible to stakeholders and harder to retract than an internal .tl/ artifact. This gate is mandatory before any Docmost write (full docmost, docmost-incremental, and boards-link commands). Preview-only commands (docmost-preview) are exempt.
The gate compares the live graph (the source publish is about to derive from) against .tl/changelog.md (the record of what has been shipped). If these two disagree, the publish is refused.
Live graph reads only — no .cypher export fallback. Exports are stale by definition the moment the next graph mutation lands, and a publish run that consumed an export would push stale pages to Docmost. If the graph container is unreachable, the gate emits Status: BLOCKED with workflow detail graph_unavailable and the publish refuses. The fix is to bring the graph container up (docker compose up -d from graph-infra/), not to swap in an export. Operators who must publish under an unreachable graph file a W4 signed exception against gate graph-stale — the exception does NOT re-enable export fallback; it accepts that the publish ran against unreconciled state.
Step 1: Reach the live graph
Use the project-resolved Bolt endpoint:
RETURN 1 AS ok
On failure: HALT.
HALT — graph_unavailable (pre-publish reconciliation).
The live Neo4j graph is unreachable. A stale .cypher export is
NOT an acceptable substitute — publishing it to Docmost would
push out-of-date pages to stakeholders.
Resolution: bring the project graph container up and rerun, or
file a signed exception (.tl/exceptions/) against gate
`graph-stale`.
Status: BLOCKED (workflow detail: graph_unavailable)
Step 2: Read graph state and changelog
// Single round-trip read of the comparison surface:
OPTIONAL MATCH (fr:FeatureRequest)
WITH collect({ id: fr.id, release_tag: fr.release_tag }) AS fr_list
OPTIONAL MATCH (uc:UseCase)
WITH fr_list, collect(uc.id) AS uc_list
OPTIONAL MATCH (t:Task)
WITH fr_list, uc_list, collect({
id: t.id, release_tag: t.release_tag,
evidence: coalesce(t.verification_evidence, '')
}) AS task_list
RETURN fr_list, uc_list, task_list
Read .tl/changelog.md and parse the most recent released section: every FR-NNN / UC-NNN mentioned, the release tag header, and any release-status.json cross-reference.
Step 3: Cross-checks (publish-scope subset)
Reuses pairs from the W5 reconciliation taxonomy but scoped to the publish target:
| Pair | Sources | Assertion | |---|---|---| | P-P1 | .tl/changelog.md released FR list vs graph FeatureRequest | every released FR-NNN in the latest changelog section exists as FeatureRequest {id: 'FR-NNN'} in the live graph. | | P-P2 | .tl/changelog.md released UC list vs graph UseCase | every released UC-NNN in the latest changelog section exists as UseCase {id: 'UC-NNN'}. | | P-P3 | .tl/release-status.json.release_tag vs graph release_tag properties | if the JSON release tag is non-null, the live graph carries that tag on ≥1 FeatureRequest or Task node. (Skip if .tl/release-status.json is absent — the gate does not require it; absence is logged.) |
An assertion that fails under no active signed exception is a hard refusal.
Step 4: Refuse on disagreement
If any of P-P1 … P-P3 fails, refuse the publish:
REFUSED — changelog and live graph disagree.
nacl-publish writes externally-visible Docmost pages and refuses
to publish stale or contradictory state.
P-P1 changelog.md vs live graph FeatureRequest
.tl/changelog.md (section "0.18.0 ...") references FR-007;
live graph has NO FeatureRequest {id: 'FR-007'}.
Active signed exceptions against `graph-stale`: none.
Resolution options:
[1] Run /nacl-tl-conductor (or /nacl-sa-feature FR-007) to
reconcile graph state with the changelog claim.
[2] If the changelog is correct and the graph is genuinely
stale, file a signed exception against `graph-stale` and
rerun.
Status: BLOCKED (workflow detail: publish-drift)
Step 5: Record reconciliation evidence
On PASS (or PASS-under-exception), write the publish-side reconciliation evidence:
.tl/reconciliation/-publish.json
Same schema as the conductor's reconciliation artifact (see /home/project-owner/projects/NaCl/.tl/reconciliation/ _template.json) but with sources_checked scoped to the publish subset and terminal_status recorded against the publish gate specifically.
Only on terminal_status == VERIFIED does the publish proceed to the actual Docmost write steps.
Command: /nacl-publish docmost
Full publish -- generate markdown from graph for every artifact and create/update all pages in Docmost.
Workflow
Step 1: Init/Load Step 2: Create Step 3: Generate & Step 4: Save
── manifest ──── -> ── section pages ── -> ── publish pages ── -> ── manifest ──
Step 1: Resolve Docmost target, then Initialize or Load Manifest
Step 1a: Read config.yaml → docmost (first source for space_id / root_page_id):
- Try
docmost.spaces.graph.space_idanddocmost.spaces.graph.root_page_id. - If
spaces.graphis missing, fall back todocmost.spaces.sa.space_id/spaces.sa.root_page_id(graph and SA publish into the same Docmost area in most projects). - If both are empty -> mark
space_idandroot_page_idas "needs prompt".
Step 1b: If .docmost-sync.json does NOT exist (first run):
- If
space_idresolved fromconfig.yaml:
- Validate by calling
mcp__docmost__list_spaces-- confirm the space still exists. - Use the resolved
space_iddirectly. Do not prompt the user.
- Otherwise ask the user:
space_id-- which Docmost space to publish into? (list spaces viamcp__docmost__list_spaces)- After the user chooses, suggest writing the value to
config.yaml → docmost.spaces.graph.space_idso the next run is non-interactive.
- If
root_page_idresolved fromconfig.yaml:
- Use it as-is, do not create a new root page.
- Otherwise create a new root page:
`` mcp__docmost__create_page( title: "{project} -- Спецификация", content: "# {project}\n\nАвтоматически сгенерированная спецификация из графа знаний.", spaceId: "{space_id}" ) ` Save returned id as rootpageid and **suggest writing it to config.yaml → docmost.spaces.graph.rootpageid`**.
- Initialize manifest with empty
pagesandsections. Persistspace_id/root_page_idinto the manifest as cache, butconfig.yamlremains the source of truth on subsequent runs.
Step 1c: If .docmost-sync.json EXISTS:
- Read manifest.
- Reconcile
space_id/root_page_id:
- If
config.yamlhas values -> use them. If they differ from manifest, log a warning and update manifest to matchconfig.yaml(manifest is a cache, not the source of truth). - If
config.yamlis empty -> fall back to manifest values.
- Validate
space_idby callingmcp__docmost__list_spaces-- confirm the space still exists. - Continue with existing section pages from
sectionsmap.
Step 2: Create Section Pages
For each section in the hierarchy, create a parent page if not already in sections:
| Section | Title | Parent | |---------|-------|--------| | Архитектура | Архитектура | rootpageid | | Domain Model | Domain Model | rootpageid | | Use Cases | Use Cases | rootpageid | | Интерфейсы | Интерфейсы | rootpageid | | Трассировка | Трассировка | rootpageid | | Роли и права | Роли и права | rootpageid |
For each section not in sections map:
mcp__docmost__create_page(
title: "{section title}",
content: "# {section title}\n\nРаздел спецификации.",
spaceId: "{space_id}",
parentPageId: "{root_page_id}"
)
Save returned id to sections[title].
Order of creation: Архитектура, Domain Model, Use Cases, Интерфейсы, Трассировка, Роли и права. Sequential calls (Docmost does not guarantee ordering with parallel requests).
Step 3: Generate Content and Publish Pages
Process artifacts in this order:
3a: Architecture -- Context Map
- Use
nacl-render md domain-modellogic to generate the full domain model overview. - Also generate a module list page:
Query modules: ``cypher MATCH (m:Module) OPTIONAL MATCH (m)-[:CONTAINS_ENTITY]->(de:DomainEntity) OPTIONAL MATCH (m)-[:CONTAINS_UC]->(uc:UseCase) RETURN m.id AS id, m.name AS name, m.description AS description, count(DISTINCT de) AS entity_count, count(DISTINCT uc) AS uc_count ORDER BY m.id ``
Generate markdown: ```markdown # Модули
| ID | Модуль | Описание | Сущностей | Use Cases | |----|--------|----------|-----------|-----------| | {id} | {name} | {description} | {entitycount} | {uccount} | ```
- Publish to section
Архитектура:
- Page "Context Map" --
create_pageorupdate_pageundersections["Архитектура"] - Page "Модули" -- under
sections["Архитектура"]
3b: Domain Model -- Entities
- Query all DomainEntities:
```cypher MATCH (de:DomainEntity) RETURN de.id AS id, de.name AS name
…
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.