# Nacl Tl Plan

> |

- **Type:** Skill
- **Install:** `agentstack add skill-itsalt-nacl-nacl-tl-plan`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [ITSalt](https://agentstack.voostack.com/s/itsalt)
- **Installs:** 0
- **Category:** [Agent Skills](https://agentstack.voostack.com/c/agent-skills)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [ITSalt](https://github.com/ITSalt)
- **Source:** https://github.com/ITSalt/NaCl/tree/main/nacl-tl-plan

## Install

```sh
agentstack add skill-itsalt-nacl-nacl-tl-plan
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## 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

```cypher
// 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:**
1. STOP -- planning is impossible without SA data in the graph.
2. Suggest user runs `/nacl-sa-architect` or `/nacl-sa-domain` first.

### Step 1.2: Get all modules with their UCs and entities

```cypher
// 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

```cypher
// 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

```cypher
// 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)

```cypher
// 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):

```cypher
// 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):

```cypher
// 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):**
1. `stale_set` = UCs from Signal 1 ∪ Signal 2 (both drift-confirmed).
2. `new_set` = UCs from `INCLUDES_UC {kind:'new'}` (or in-scope UCs) with **no** `GENERATES` edge yet.
3. Regenerate task files **only** for `stale_set ∪ new_set`. UCs not in either set are left untouched — their tasks and dev state survive.
4. Use `MERGE (t:Task {id: $taskId})` (Step 2.4) so re-running is idempotent at the node level: the same `UC###-BE`/`UC###-FE` ids are updated in place, never duplicated.
5. On each successful regeneration, stamp `planned_from_version` and **clear** the staleness flag (Step 2.4).
6. If `stale_set ∪ new_set` is 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:

```cypher
// 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.

```cypher
// 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

```cypher
// 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`:

1. **File absent on disk** → record `external-contract-missing` for `(uc_id,
   contract_id)`.
2. **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)

```text
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: BLOCKED` workflow detail `external-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](https://github.com/ITSalt)
- **Source:** [ITSalt/NaCl](https://github.com/ITSalt/NaCl)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-itsalt-nacl-nacl-tl-plan
- Seller: https://agentstack.voostack.com/s/itsalt
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
