Install
$ agentstack add skill-itsalt-nacl-nacl-tl-plan ✓ 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
/nacl-tl-plan -- Development Planning from Neo4j Graph
Purpose
Graph-powered replacement for /nacl-tl-plan. Reads the SA specification from Neo4j (modules, use cases, entities, dependencies) via Cypher queries and generates self-sufficient task files for dev agents (nacl-tl-dev-be, nacl-tl-dev-fe).
Critical difference from nacl-tl-plan:
| Aspect | nacl-tl-plan | nacl-tl-plan | |--------|---------|---------------| | Data source | ~70 markdown files in docs/ | Neo4j graph | | Tokens per UC | ~150K (read all docs) | ~550 (~50 query + ~500 response) | | Retrieval method | Read files sequentially | 1 Cypher query per UC | | Task file format | Standard .tl/tasks/ | IDENTICAL (dev agents unchanged) |
Shared references: nacl-core/SKILL.md
Neo4j Tools
| Tool | Usage | |------|-------| | mcp__neo4j__read-cypher | Read SA graph (modules, UCs, entities, deps) | | mcp__neo4j__write-cypher | Create Wave and Task nodes in the TL layer | | mcp__neo4j__get-schema | Introspect current graph schema before planning |
Invocation
/nacl-tl-plan [options]
Parameters
| Parameter | Values | Description | |-----------|--------|-------------| | scope | full (default) | Plan all UCs from the graph | | | module: | Plan only UCs in a specific module | | | uc:, | Plan only specific UCs | | --feature | FR-NNN | Plan only UCs from a feature request. Resolves the UC list and new/modified split from the graph ((:FeatureRequest)-[:INCLUDES_UC]->), falling back to .tl/feature-requests/FR-NNN.md only if the node is absent (Step 1.5b). | | wave-start | 0 (default) | Starting wave number (for incremental planning) | | --overwrite | (flag) | Destroy ALL existing Task/Wave nodes and re-plan from scratch. Default is incremental (Step 1.5b); use this only for an intentional clean rebuild. |
Workflow Overview
Phase 1: READ SA GRAPH
|
+-- Modules, UseCases, DomainEntities, Dependencies
+-- Priorities, SystemRoles
+-- External Contracts Gate (W6, Step 1.6) — BLOCKED if missing/stub
|
Phase 2: WAVE PLANNING
|
+-- Topological sort by DEPENDS_ON + priority
+-- Wave 0: TECH tasks (infra)
+-- Wave 1+: UC-BE before UC-FE, independent UCs in parallel
+-- Create Wave / Task nodes in Neo4j
|
Phase 3: TASK GENERATION
|
+-- For each UC: run sa_uc_full_context($ucId)
+-- Map query result to 8 task files
+-- Write files to .tl/tasks/UC###/
|
Phase 4: MASTER PLAN
|
+-- Update master-plan.md
+-- Update status.json
+-- Update changelog.md
Phase 1: Read SA Graph
Step 1.1: Pre-flight -- verify graph has SA data
// Pre-flight: count SA-layer nodes
MATCH (n)
WHERE n:Module OR n:UseCase OR n:DomainEntity OR n:Form OR n:Requirement OR n:SystemRole
RETURN labels(n)[0] AS label, count(n) AS count
ORDER BY label
If result is empty or all counts are 0:
- STOP -- planning is impossible without SA data in the graph.
- Suggest user runs
/nacl-sa-architector/nacl-sa-domainfirst.
Step 1.2: Get all modules with their UCs and entities
// All modules with UC and entity counts
MATCH (m:Module)
OPTIONAL MATCH (m)-[:CONTAINS_UC]->(uc:UseCase)
OPTIONAL MATCH (m)-[:CONTAINS_ENTITY]->(de:DomainEntity)
RETURN m.id AS module_id, m.name AS module_name,
count(DISTINCT uc) AS uc_count,
count(DISTINCT de) AS entity_count
ORDER BY m.id
Step 1.3: Get all UCs with priorities and dependencies
// sa_uc_dependencies -- all UCs with their DEPENDS_ON edges
MATCH (uc:UseCase)
OPTIONAL MATCH (uc)-[:DEPENDS_ON]->(dep:UseCase)
OPTIONAL MATCH (m:Module)-[:CONTAINS_UC]->(uc)
RETURN uc.id AS uc_id, uc.name AS uc_name,
uc.priority AS priority,
m.id AS module_id, m.name AS module_name,
collect(dep.id) AS depends_on
ORDER BY uc.priority DESC, uc.id
Step 1.4: Get complete domain model overview
// Domain entities with attribute count and relationships
MATCH (de:DomainEntity)
OPTIONAL MATCH (de)-[:HAS_ATTRIBUTE]->(da:DomainAttribute)
OPTIONAL MATCH (de)-[rel:RELATES_TO]->(de2:DomainEntity)
RETURN de.id AS entity_id, de.name AS entity_name,
count(DISTINCT da) AS attr_count,
collect(DISTINCT {target: de2.name, type: rel.rel_type, card: rel.cardinality}) AS relationships
ORDER BY de.id
Step 1.5: Check for existing TL-layer data (incremental planning)
// Count existing Task and Wave nodes
MATCH (n)
WHERE n:Task OR n:Wave
RETURN labels(n)[0] AS label, count(n) AS count
If Tasks/Waves already exist, the default is INCREMENTAL re-planning, not overwrite. A full overwrite destroys in-progress dev state and re-bakes every snapshot; it happens only when the user explicitly passes --overwrite. Otherwise run Step 1.5b to find the narrow set of UCs that actually changed since the last plan, and regenerate only those.
Step 1.5b: Detect which UCs need re-planning (idempotency)
A Task's .tl/tasks/UC###/*.md files embed a point-in-time snapshot of the SA graph (field types, role permissions, enum values — see "Self-Sufficiency"). That snapshot goes stale when its source UC changes. Two drift-confirmed signals identify the stale set precisely — no markdown diffing, no whole-layer nuke. Both require evidence of actual drift; neither fires on a UC that is already current.
Signal 1 — version drift (a Task baked from an older spec than its UC now carries):
// mcp__neo4j__read-cypher
// GUARD planned_from_version IS NOT NULL: a Task with no baseline (project upgraded to
// Фаза 0 but gap-closure baseline not yet run) is NOT treated as drifted here — that
// would over-flag every task on day one and reset in-progress work. Such a real change
// is still caught by Signal 2 (set by sa-feature step 3g / tl-fix L2-L3).
MATCH (uc:UseCase)-[:GENERATES]->(t:Task)
WHERE t.planned_from_version IS NOT NULL
AND coalesce(uc.spec_version, 0) > t.planned_from_version
RETURN DISTINCT uc.id AS uc_id, uc.spec_version AS current_version,
t.planned_from_version AS planned_version, 'spec-drift' AS reason
Signal 2 — explicit stale stamp (set by the write-skills at change time; catches changes even without a version bump, e.g. some tl-fix paths):
// mcp__neo4j__read-cypher
MATCH (uc:UseCase)
WHERE coalesce(uc.review_status,'current') = 'stale'
OR EXISTS { (uc)-[:GENERATES]->(t:Task) WHERE coalesce(t.review_status,'current')='stale' }
RETURN DISTINCT uc.id AS uc_id, uc.stale_origin AS origin
> Do NOT add a third "FR flagged modified" auto-detection arm keyed only on > INCLUDES_UC {kind:'modified'} + status='spec-complete'. spec-complete is a > sticky state — an FR that was never advanced keeps that edge forever, so such an > arm re-flags its UCs on every run even after they're current, regenerating > already-current tasks and resetting in-progress work (the exact churn the Signal-1 > guard prevents). sa-feature step 3g always bumps spec_version AND stamps stale > when it processes an FR, so Signals 1+2 already catch every real FR-driven change. > INCLUDES_UC {kind} is consumed for explicit --feature FR-NNN scoping only > (below), never for auto drift detection.
Incremental algorithm (default when tasks exist):
stale_set= UCs from Signal 1 ∪ Signal 2 (both drift-confirmed).new_set= UCs fromINCLUDES_UC {kind:'new'}(or in-scope UCs) with noGENERATESedge yet.- Regenerate task files only for
stale_set ∪ new_set. UCs not in either set are left untouched — their tasks and dev state survive. - Use
MERGE (t:Task {id: $taskId})(Step 2.4) so re-running is idempotent at the node level: the sameUC###-BE/UC###-FEids are updated in place, never duplicated. - On each successful regeneration, stamp
planned_from_versionand clear the staleness flag (Step 2.4). - If
stale_set ∪ new_setis empty, report "plan is current — nothing to regenerate" and stop.
First Фаза-0 plan on an existing project — baseline, don't regenerate. If Tasks exist but none has planned_from_version (project just upgraded), do NOT treat them as drifted. Baseline them once so future drift is detectable, without resetting any in-progress work:
// mcp__neo4j__write-cypher — one-time baseline (idempotent; only touches null ones)
MATCH (uc:UseCase)-[:GENERATES]->(t:Task)
WHERE t.planned_from_version IS NULL
SET t.planned_from_version = coalesce(uc.spec_version, 0)
After baselining, only a real spec_version bump (or a review_status='stale' stamp from nacl-sa-feature/nacl-tl-fix) marks a task for regeneration.
--feature FR-NNN: resolve the UC list from the graph, not the markdown file — read (fr:FeatureRequest {id:$frId})-[r:INCLUDES_UC]->(uc:UseCase) and use r.kind to split new vs modified. This finally consumes the INCLUDES_UC{kind} edges that nacl-sa-feature writes (previously written but never read). Fall back to the markdown UC list only if the FR node is absent.
// mcp__neo4j__read-cypher — resolve --feature scope from the graph
MATCH (fr:FeatureRequest {id:$frId})-[r:INCLUDES_UC]->(uc:UseCase)
RETURN uc.id AS uc_id, r.kind AS kind
ORDER BY uc.id
> Wave numbers for the regenerated/new tasks are NOT assigned by hand. Once you have > decided the feature's task list and dependency edges, hand them to the deterministic > planner in assign mode with waveStart = max(existing Wave.number) + 1 (Step 2.1). > The tool returns the global wave per task; feed those into Step 2.4. This is the path a > mature project almost always takes — keep it on the tool, not on reasoning.
Step 1.6: External Contracts Gate (W6)
Purpose. For every UC in planning scope, refuse to generate a task when the UC references an external provider/protocol whose .tl/external-contracts/.md is absent. This is a consumer-side read of the artifact written by nacl-sa-architect during its External Contracts phase (W6 plan brief, declared primary-owner exception). Strict-only — there is no inline --skip-external-contract flag.
Why this check exists. 13 of ~60 postmortem signals across two NaCl projects were external-API / wire-protocol gaps (kie.ai in both projects, TUS upload, base_url divergence, reverse-proxy URL scheme, ffmpeg/ffprobe runtime — see docs/retrospectives/project-beta-runtime-baseline.md §§ A1–A9, B1–B7). Local tests passed; the product did not work. The nacl-tl-sync Wire-Evidence Gate (W2) already downgrades sync to UNVERIFIED when wire-evidence is absent. W6 makes the artifact concrete upstream so the gate has something to point at.
1.6.1: Discover external dependencies
// mcp__neo4j__read-cypher
// Per-UC external-contract requirements via graph
MATCH (uc:UseCase)
WHERE uc.id IN $uc_ids
OPTIONAL MATCH (uc)-[:REQUIRES_EXTERNAL]->(ec:ExternalContract)
OPTIONAL MATCH (m:Module)-[:CONTAINS_UC]->(uc)
OPTIONAL MATCH (m)-[:DEPENDS_ON_EXTERNAL]->(mec:ExternalContract)
RETURN uc.id AS uc_id,
collect(DISTINCT {id: ec.id, name: ec.name, kind: ec.kind,
file_path: ec.file_path}) AS uc_direct,
collect(DISTINCT {id: mec.id, name: mec.name, kind: mec.kind,
file_path: mec.file_path}) AS via_module
The union of uc_direct and via_module is the set of external contracts the UC's tasks must reference at generation time.
1.6.2: File-system existence and stub check
For each ExternalContract row returned above, verify the file referenced by ec.file_path:
- File absent on disk → record
external-contract-missingfor `(uc_id,
contract_id)`.
- File present but required sections empty / "TBD" / stub → record
external-contract-stub. The required sections are 1–8 and 10–11 of the template (.tl/external-contracts/_template.md). Section 9 is required when ec.kind == 'provider'. Section 7 must be filled OR explicitly marked N/A — no file URLs.
Both conditions are blockers; the only override is a signed exception under the W4 schema (no inline flag).
1.6.3: Example check logic (pseudocode)
violations = []
for uc in scope:
required_contracts = graph_query(uc.id) # Step 1.6.1
for contract in required_contracts:
if not file_exists(contract.file_path):
violations.append({
uc_id: uc.id, contract_id: contract.id,
contract_name: contract.name, contract_kind: contract.kind,
reason: "external-contract-missing",
expected_path: contract.file_path,
remedy: "Run /nacl-sa-architect External Contracts phase OR file " +
"signed exception under W4 schema."
})
continue
sections = parse_required_sections(contract.file_path)
missing_required = []
for section in [1,2,3,4,5,6,7,8,10,11]:
if not sections[section].filled_non_stub:
missing_required.append(section)
if contract.kind == 'provider' and not sections[9].filled_non_stub:
missing_required.append(9)
if missing_required:
violations.append({
uc_id: uc.id, contract_id: contract.id,
contract_name: contract.name,
reason: "external-contract-stub",
missing_sections: missing_required,
expected_path: contract.file_path,
remedy: "Complete sections " + missing_required + " of " +
contract.file_path + " OR file signed exception (W4)."
})
if violations:
# Cluster violations per UC. Surface the full list.
for v in violations:
log(v.uc_id, v.contract_name, v.reason, v.expected_path)
emit_headline("PLAN HALTED — EXTERNAL_CONTRACT_MISSING")
emit_status("BLOCKED")
exit_without_writing_anything()
The example above is a sketch. The skill's implementation MUST:
- Surface every violation (do NOT short-circuit after the first one — the
operator needs the complete list to either author the missing contracts or to scope a signed exception).
- Refuse to write any TL node, any Wave node, any task file, or any
.tl/status.json / .tl/master-plan.md / .tl/changelog.md entry until the gate passes OR a signed exception covering every violation is on disk.
- Emit
Status: BLOCKEDworkflow detailexternal-contract-missing(when
the file is absent) or external-contract-stub (when sections are unfilled). See Output Summary below for the full headline / status contract.
1.6.4: Worked example — UC-300 referencing kie.ai
graph: UC-300 -[:REQUIRES_EXTERNAL]-> (ExternalContract {id: 'ext-kie',
name: 'kie.ai', kind: 'provider',
file_path: '.tl/external-contracts/kie.md'})
case A: .tl/external-contracts/kie.md exists, all required sections filled
→ gate PASSES; UC-300 task generation proceeds.
case B: .tl/external-contracts/kie.md absent
→ gate FAILS with external-contract-missing.
→ headline: PLAN HALTED — EXTERNAL_CONTRACT_MISSING
→ status: BLOCKED
→ remedy: /nacl-sa-architect External Contracts phase, or signed
exception under W4 schema.
case C: file exists but Section 9 (Model namespace) is "TBD"
→ gate FAILS with external-contract-stub; missing_sections: [9].
→ headline: PLAN HALTED — EXTERNAL_CONTRACT_STUB
→ status: BLOCKED
The same flow applies for any protocol (TUS, SSE, etc.) — the kind property on ExternalContract toggles the Section-9 requirement only.
1.6.5: Strict-only language
There is no inline --skip-external-contract flag. There is no gate_mode: legacy carve-out. The project_kind: prototype config does NOT relax this gate; prototypes are the PR/CI carve-out, not the contract carve-out. The only override is a signed exception under the W4 schema covering every violation by (uc_id, contract_id) tuple.
Phase 2: Wave Planning
Step 2.1: Compute the wave plan (deterministic — both planning paths)
Do not assign waves by hand — a missed dependency edge silently produces an FE-
…
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.