Install
$ agentstack add skill-itsalt-nacl-nacl-tl-intake ✓ 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-intake -- Graph-Aware Request Triage & Decomposition
Your Role
You are a product triage specialist with access to the project's Neo4j knowledge graph. When the user brings a batch of changes ("I want these 5 things"), you decompose them into independent work items, classify each using graph-based disambiguation, group related items into features, and then auto-execute the appropriate skills sequentially.
You are the universal entry point for any user request that contains multiple changes or where the type of work (feature vs bug vs task) is unclear -- and the project has a populated Neo4j graph.
Key advantage over nacl-tl-intake: Classification queries the graph for existing Use Cases. If a UC already exists and is detailed, the request is likely a BUG (existing behavior is broken). If no matching UC exists, it is a FEATURE (new behavior).
Key Principles
1. Source -> Extract -> Classify (via graph) -> Group -> Validate -> Confirm -> Execute
2. One feature = can ship independently with user value
3. Graph-first classification: query Neo4j before keyword heuristics
4. Propose decomposition, user confirms (graph reduces misclassification)
5. After confirmation -- full autopilot, no further prompts until done
Shared References
Read nacl-core/SKILL.md for:
- Neo4j MCP tool names and connection info (
mcp__neo4j__read-cypher,mcp__neo4j__write-cypher) - ID generation rules
- Schema files location (
graph-infra/schema/) - Query library location (
graph-infra/queries/)
Neo4j Tools
| Tool | Usage | |------|-------| | mcp__neo4j__read-cypher | Query existing UCs for classification |
Invocation
User describes what they want in natural language -- any format, any mix:
/nacl-tl-intake "I need these changes:
1. Add image format selection (16:9, 9:16, square)
2. Show the scene prompt alongside the final prompt
3. Allow editing the prompt and regenerating
4. Show regeneration attempts as tabs
5. Add image editing mode via inpainting"
Or even less structured:
/nacl-tl-intake "The share button doesn't work on mobile, also I want to add
a payment system, and we need to update the deploy docs for the new server"
Flags
| Flag | Description | |------|-------------| | --yes | Auto-confirm HIGH+GRAPH+no-spec-gap+L0/L1 atoms without prompting (see --yes flag behavior below). | | --autonomous | (2.14+) Set ONLY by /nacl-goal intake alongside --yes --emit-state. Widens the auto-route set per the "Autonomous" column of the Step 2b case table: Template B auto-confirms, probe-scored atoms (Step 2a.5) auto-route on the leading hypothesis when score >= intake.route_threshold (tracked residual_note below high_confidence), and only sub-threshold atoms are collected for ONE consolidated pre-/goal batch instead of per-atom prompts. Template C (hard_refuse) is untouched. A human typing /nacl-tl-intake --yes is NOT affected — this flag is never implied. | | --emit-state | Write the deterministic routing table as a JSON file (2.10.1+). When set, this skill writes the table to the given path INSTEAD of (or in addition to) interactive prompting. Used by /nacl-goal intake to capture classification artifacts for the wrapper. Format: see §--emit-state JSON schema below. | | --namespace= | (sa-feature passthrough, optional) Restrict classification to a single domain namespace. |
--emit-state JSON schema (2.10.1+)
When --emit-state is set, this skill writes a JSON file at ` with the following shape (per nacl-goal/plan-lock-schema.md` §intake.json):
{
"schema_version": 1,
"atoms": [
{
"id": "atom-[:12]",
"type": "BUG|TASK|FEATURE_SMALL|FEATURE_HEAVY",
"title": "",
"linked_uc": "UC-NNN|TECH-NNN|null",
"evidence": ["GRAPH", "CODE", "HEURISTIC", "USER_OVERRIDE"],
"confidence": "HIGH|MEDIUM|LOW",
"risk_level": "L0|L1|L2|L3",
"depends_on": ["atom-"],
"hard_refuse_triggers": [""],
"trigger_evidence": "",
"spec_gap": false,
"residual_note": null,
"diagnosis": null,
"skill_path": "nacl-tl-fix|nacl-tl-dev|nacl-sa-feature -> nacl-tl-dev"
}
],
"classification_metadata": {
"ambiguous": false,
"ambiguity_reason": null,
"requires_split": false,
"split_reason": null
}
}
FEATURE size class
The type field distinguishes FEATURE_SMALL (bounded — can be implemented inside one PR without product decisions) from FEATURE_HEAVY (requires migrations, auth/security, billing, multiple architecture trade-offs, or a product-decision the orchestrator can't make).
Classification rule for FEATURE atoms:
FEATURE_HEAVY if ANY of:
• hard_refuse_triggers contains any of: schema_migration, public_api_contract,
auth_or_security, permissions, billing, destructive_data_operation,
l2_l3_architecture, product_decision_required, hotfix_or_release_routing
• the atom would require >1 new HTTP route AND >1 new DB column AND >1 new graph entity
• the goal text mentions ≥3 of: pricing, payment, auth, role, permission,
migration, schema, refactor architecture, API contract
• the linked UC does not exist AND the goal requires a new bounded context
FEATURE_SMALL otherwise (the default for feature requests that have:
- a clear linked UC (existing or trivially creatable),
- no hard_refuse triggers,
- implementation fits one feature branch + one PR + one CI run)
depends_on hints
When this skill can infer that one atom semantically depends on another (e.g. atom A creates a route that atom B consumes), it populates atom_B.depends_on = ["atom-A"]. When uncertain, leave the list empty — the wrapper's topological sort tie-breaks by BUG-before-FEATURE then by id.
depends_on is a hint, not a hard constraint. The wrapper's plan lock applies it via topological sort and refuses cycles.
residual_note (aspect-split residual)
When an atom carries BOTH an unconditionally-correct defensive part AND a genuinely-ambiguous residual (see Step 2b "aspect split"), the shipped part keeps spec_gap: false and the residual is recorded here instead of blocking the user:
"residual_note": {
"summary": "",
"working_assumption": "",
"verify_by": "staging|user|null",
"followup_task": "",
"route": "note|L2-with-flag",
"reason": "spec_gap_residual|medium_confidence_alternative"
}
null when the atom has no residual. A non-null residual_note MUST carry a followup_task — a residual with no recorded follow-up is invalid and falls back to the prompt (see Step 2b binding rule + Step 6 durable sink).
reason (2.14+, optional — absent means spec_gap_residual): medium_confidence_alternative marks a residual created by the --autonomous auto-route of a MEDIUM-confidence atom — summary then records the plausible alternative classification and what would tip the scale, so the user can re-route after the fact. It is a first-class tracked follow-up, distinct from a spec-gap residual.
diagnosis (Step 2a.5 PROBE output)
Written for every atom the graph alone did not resolve (see Step 2a.5). null when the probe did not run (HIGH+GRAPH atoms). Readers MUST tolerate its absence (pre-probe artifacts).
"diagnosis": {
"hypotheses": [
{ "id": "H_bug", "statement": "", "verdict": "confirmed|refuted|inconclusive" }
],
"checks": [
{ "kind": "grep|read|db|git", "target": "", "result": "" }
],
"score": 0.95,
"threshold_used": 0.7,
"leaning": "BUG|FEATURE|TASK|null",
"blocking_fact": "|null",
"evidence_refs": [""]
}
score is rubric-derived (deterministic lookup on the verdict pattern — nacl-tl-core/references/intake-scoring.md), never free-form. threshold_used freezes the intake.route_threshold in effect so the wrapper and audit tooling interpret the routing without re-reading config.yaml. When the probe confirms a single hypothesis, add CODE to the atom's evidence — it means "verified against the actual codebase/DB".
hard_refuse_triggers closed set
The wrapper consumes this field mechanically (see nacl-goal/plan-lock-schema.md §hardrefusetriggers → refusal mapping). This skill MUST emit triggers ONLY from this closed set:
schema_migration— DB migration, message-contract changepublic_api_contract— public API surface modificationauth_or_security— authentication, authorization, security-policy changepermissions— role/permission matrix changebilling— payment, pricing, invoicing changedestructive_data_operation— bulk delete, data migration, backup-incompatible changel2_l3_architecture— bounded-context boundary change, cross-module contract changeproduct_decision_required— feature requires choosing between alternativeshotfix_or_release_routing— atom is L0/L1 emergency or release-pipeline-touching
Each trigger MUST also populate trigger_evidence with a short verbatim quote from the user's goal text.
--emit-state does NOT preclude interactive prompting. When both --emit-state and --yes are set, the skill writes the JSON AND auto-confirms eligible atoms. When --emit-state is set without --yes, the JSON is written first; then interactive prompts run as today (and the user can manually adjust the file afterwards).
Goal-context env vars (2.10.1+)
When this skill is invoked under /nacl-goal intake, the wrapper exports NACL_GOAL_RUN_ID (and the other goal env vars). This skill recognizes NACL_GOAL_RUN_ID only for logging purposes — classification logic is unaffected. The wrapper itself passes --autonomous --yes --emit-state .tl/goal-runs//intake.json so this skill does NOT need to read the env vars to know it's running under a goal.
Invariant: when these env vars are absent and --emit-state is not passed, this skill behaves exactly as today.
Language Rules
- This SKILL.md: English (instructions for Claude)
- User interaction: User's language (detect from conversation)
- Downstream skills receive instructions in their own language convention
- User-facing vocabulary (MANDATORY). Any string printed to the user MUST use observable, behavioral language and MUST NOT contain internal tokens —
L0/L1/L2/L3,spec_gap,POLICY_CALL, "re-anchor",gate_payload, gate names, or graph requirement IDs. Translate them: L1→ "a code-only fix (the spec already describes the right behaviour)";L2/L3→ "this also needs the spec/docs updated first"spec_gap/ "re-anchor to a global timeline" → "the spec doesn't currently say how X should behave"POLICY_CALL→ "a judgment call: was X always expected, or is it genuinely new?"
The --emit-state JSON, the decision tree, the case table, and the headline-selection rules are machine/Claude-facing and KEEP the canonical tokens — this rule scopes to user-shown prose only.
Workflow: 7 Steps
Step 0: SOURCE (YouGile or direct input)
Goal: Get the request -- either from YouGile or from user's message.
If YouGile is configured (config.yaml -> yougile):
- Check UserRequests column for new cards:
`` get_tasks(columnId: config.yougile.columns.user_requests) ``
- If cards found -> read the card description as input
- The card becomes the parent task -- all decomposed items become subtasks
- If no cards and user provided text -> use text directly (no parent task)
If YouGile is NOT configured:
- Use the user's message directly
- No parent task, no subtask linking
Configuration Resolution
Read config.yaml at project root. If not found, YouGile features are disabled.
| Data | Source priority | |------|---------------| | YouGile column IDs | config.yaml -> yougile.columns. | | YouGile sticker IDs | config.yaml -> yougile.stickers. | | YouGile API key | .mcp.json (MCP server env) | | Probe routing threshold | config.yaml -> intake.routethreshold; absent -> default 0.7 | | Probe high-confidence threshold | config.yaml -> intake.highconfidence; absent -> default 0.9 | | Probe rubric scores | config.yaml -> intake.scores.*; each key falls back independently to the defaults in nacl-tl-core/references/intake-scoring.md |
If config.yaml is missing or yougile section is empty -> skip YouGile integration, work from user input only.
Sanity clamp for intake.*: a value outside (0, 1], or route_threshold > high_confidence, is a broken config — warn the user and use the built-in defaults for the offending key(s). A broken config must not silently disable the question gate.
Step 1: EXTRACT (split into atoms)
Goal: Break the user's message (or YouGile card) into individual, distinct requests.
- Read the user's message or YouGile card description carefully
- Identify each separate change/request/wish
- Give each a sequential number and a short title
- If a request is ambiguous or contains multiple concerns, split further
Atoms extracted:
#1 "Image format selection (16:9, 9:16, square)"
#2 "Show scene prompt alongside final prompt"
#3 "Edit prompt and regenerate"
#4 "Regeneration attempts as tabs"
#5 "Image editing via inpainting"
Rule: When in doubt, split more. It's easier to merge than to split later.
Step 2: CLASSIFY (graph-aware, for each atom)
Goal: Determine the type of each atom using Neo4j graph disambiguation.
Step 2a: Query Neo4j for matching UCs
For each atom, extract 2-3 keywords and run the sa_find_uc_by_keywords query:
// sa_find_uc_by_keywords
// From: graph-infra/queries/sa-queries.cypher
MATCH (uc:UseCase)
WHERE toLower(uc.name) CONTAINS toLower($keywords)
OR toLower(coalesce(uc.description, '')) CONTAINS toLower($keywords)
RETURN uc.id AS id, uc.name AS name, uc.detail_status AS status
ORDER BY uc.id
Run one query per atom. Use the atom's core concept as $keywords (e.g., "image format", "share button", "deploy docs").
Step 2a.5: PROBE — verify competing hypotheses before any question
Trigger. Run this step for every atom that Step 2a left inconclusive:
- no matching UC found (would land MEDIUM in the Step 2b tree), OR
- UC matched only at
detail_status = draft | stub(MEDIUM), OR - Neo4j unavailable (would land LOW / HEURISTIC).
Skip it for atoms a detailed/approved UC already settled (HIGH+GRAPH) and for the aspects an explicit USER_OVERRIDE already answered (Step 2b-pre's recognizer scan still runs first; the probe covers only what the input did not resolve).
Why. This skill has read access to the code, the DB, and the graph. "The graph didn't resolve it" is not grounds to ask the user — it is grounds to investigate. The question gate fires only after the probe has run and genuinely failed to produce a confident call, and the question then carries the diagnosis.
Procedure (per atom):
- FORMULATE 2–3 falsifiable hypotheses. Canonical bug-vs-feature pair:
H_bug— the mechanism EXISTS in code (route / handler / table /
component / enum transition present) but mishandles or loses the record on a path that is supposed to work.
H_feature— the mechanism is ABSENT: nothing in the codebase
implements the behavior at all.
Add atom-specific hypotheses where the wording suggests them (e.g. H_data — the record is present in the DB but a query filter hides it).
- CHECK each hypothesis with bounded read-only probes. Closed list:
- Grep/Glob the codebase for the mechanism; Read the 1–3 most relevant
files the search surfaces.
- ONE read-only DB query via a configured project MCP (SELECT-only, or
read-cypher for graph-stored data) — best-effort: if no DB MCP is in scope, degrade to code-only probes (never blocks the step).
git log/git show(read-only) to date the mechanism if relevant.
Budget: max 6 tool calls + 1 DB query per atom. On budget exhaustion stop probi
…
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.