/nacl-tl-intake -- Graph-Aware Request Triage & Decomposition
SkillProductivityGraph-aware request triage: queries Neo4j to disambiguate features vs bugs. Routes features to nacl-sa-feature, bugs to nacl-tl-fix, tasks to nacl-tl-dev.Use when: triage with graph context, batch of changes with graph, or the user says "/nacl-tl-intake".
Available today. Use it from your connected AI after setup.
No other account needed.
Add ahel to your AI once: Claude, ChatGPT, Cursor, Claude Code or Codex. Then ask it to use this.
Then ask your AI: use the /nacl-tl-intake -- Graph-Aware Request Triage & Decomposition skill
What this skill tells your AI
The instructions your AI receives, as published by itsalt/nacl in nacl-tl-intake/SKILL.md and read by ahel’s review.
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 <path> | 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=<DOMAIN> | (sa-feature passthrough, optional) Restrict classification to a single domain namespace. |
--emit-state JSON schema (2.10.1+)
When --emit-state <path> is set, this skill writes a JSON file at <path> with the following shape (per nacl-goal/plan-lock-schema.md §intake.json):
{
"schema_version": 1,
"atoms": [
{
"id": "atom-<short_sha256(type + linked_uc + normalized_title)>[:12]",
"type": "BUG|TASK|FEATURE_SMALL|FEATURE_HEAVY",
"title": "<one-line atom summary>",
"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>"],
"trigger_evidence": "<short quote from goal text justifying each trigger>",
"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": "<plain-language open question, no internal tokens>",
"working_assumption": "<what the fix proceeds on>",
"verify_by": "staging|user|null",
"followup_task": "<YouGile child-task id OR .tl/open-questions.md anchor>",
"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": "<falsifiable statement>", "verdict": "confirmed|refuted|inconclusive" }
],
"checks": [
{ "kind": "grep|read|db|git", "target": "<path or query>", "result": "<one line>" }
],
"score": 0.95,
"threshold_used": 0.7,
"leaning": "BUG|FEATURE|TASK|null",
"blocking_fact": "<plain-language fact preventing a confident call>|null",
"evidence_refs": ["<file:line | query summary | sha>"]
}
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 §hard_refuse_triggers → 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 <path> 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/<run_id>/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-stateJSON, 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.route_threshold; absent -> default 0.7 |
| Probe high-confidence threshold | config.yaml -> intake.high_confidence; 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-cypherfor 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 probing and score what you have — deep diagnosis is
/nacl-tl-fixPhase A's job, not intake's. Never write anything during a probe. -
RECORD a verdict per hypothesis —
confirmed(direct positive evidence) /refuted(direct negative evidence) /inconclusive— each with a one-lineevidence_ref(file:line, query result summary, or sha). -
SCORE the leading hypothesis via the deterministic rubric in
nacl-tl-core/references/intake-scoring.md(lookup on the verdict pattern — the number is derived, never invented; defaults below, each overridable viaconfig.yaml -> intake.scores.*):Verdict pattern score leader confirmed, ALL alternatives refuted 0.95 leader confirmed, ≥1 alternative inconclusive (none confirmed) 0.8 leader supported indirectly, all alternatives refuted 0.75 leader supported indirectly, alternatives inconclusive 0.55 ≥2 hypotheses confirmed (contradiction) 0.4 all inconclusive / budget exhausted 0.2 -
DERIVE the classification (thresholds from Configuration Resolution; defaults in brackets):
score >= high_confidence [0.9]→ that type, confidence HIGH, addCODEtoevidence. The atom now follows the HIGH rows of the Step 2b case table (auto-route / launch-sanity), exactly like a graph-backed one.route_threshold [0.7] <= score < high_confidence [0.9]→ confidence MEDIUM, addCODEtoevidence, setdiagnosis.leaning. Routing per the MEDIUM row of the case table (auto-route on the leader under--autonomous, with the alternative +blocking_factrecorded as a trackedresidual_note, reasonmedium_confidence_alternative).score < route_threshold [0.7]→ keep the Step 2a confidence (MEDIUM, or LOW when Neo4j was down). This — and ONLY this — is the path that reaches the question gate (Template E), and the question MUST carry the diagnosis.- Hard-refuse triggers are evaluated independently and are NEVER cleared
by a probe: no score auto-routes an atom carrying one (Template C /
PLAN_BLOCKED_*path is untouched).
-
WRITE the
diagnosisobject onto the atom (schema in §--emit-state) — includingscoreandthreshold_used— for the emitted state file and for Step 2d's plain-language presentation.
The probe uses only read-only tools (Grep/Glob/Read/Bash); the DB probe is best-effort and skipped when no DB MCP is reachable from the agent context.
Step 2b: Classify based on graph results — then require per-atom confirmation
Apply this decision tree to each atom, using the query results:
Did sa_find_uc_by_keywords return matching UCs?
|
+-- YES, matching UC found with detail_status = 'detailed' or 'approved':
| The behavior IS specified. Is the atom reporting broken/wrong behavior?
| -> YES (matches existing spec — behavior is broken):
| BUG (confidence: HIGH, evidence: GRAPH, spec_gap: false)
| -> NO, wants different existing behavior:
| FEATURE (confidence: HIGH, evidence: GRAPH, spec_gap: false)
| -> SPEC_GAP: atom names a sub-aspect the UC does NOT currently specify
| (per-X qualifier, refinement noun, UI element, or artifact type
| absent from the matched UC's name/description/forms/fields):
| BUG, spec_gap: true. Resolve via Step 2b-pre + 2b-split below:
| ship any unconditionally-correct defensive part at L1; route the
| ambiguous residual at L2-with-flag (or as a tracked NOTE); block
| for a human decision ONLY when the residual carries a
| hard_refuse_trigger. NOT an unconditional user gate.
|
+-- YES, matching UC found with detail_status = 'draft' or 'stub':
| The behavior is partially specified.
| -> Likely FEATURE (confidence: MEDIUM, evidence: GRAPH)
| (Step 2a.5 PROBE has run — its score may have upgraded this
| to HIGH or MEDIUM with CODE evidence; use the post-probe values)
|
+-- NO matching UC found:
| The behavior is NOT specified IN THE GRAPH — but Step 2a.5 PROBE has
| checked the code/DB. Use the post-probe confidence/evidence; the
| phrasing heuristics below apply only when the probe stayed sub-threshold:
| -> "X doesn't work" phrasing: BUG (confidence: MEDIUM, evidence: GRAPH)
| -> "Add X" / "I want X": FEATURE (confidence: MEDIUM, evidence: GRAPH)
| -> Infrastructure/docs/process: TASK (confidence: MEDIUM, evidence: GRAPH)
|
+-- Neo4j UNAVAILABLE (connection error):
Fall back to keyword-based classification (Step 2c)
All atoms get confidence: LOW, evidence: HEURISTIC
(Step 2a.5 PROBE still runs — grep/read/DB probes need no graph;
a confirmed single hypothesis upgrades the atom via CODE evidence)
SPEC_GAP detection heuristics (any one sufficient to set spec_gap: true):
- Atom mentions a per-X qualifier (per-iteration, per-step, per-attempt, per-row, per-version) that does not appear in the matched UC's
nameordescription. - Atom requests a refinement noun — naming convention, ordering, chronology, count, format detail, label, sort order — that is not mentioned in the matched UC's acceptance criteria / form fields.
- Atom names a UI element or artifact type not reachable from the matched UC via
UseCase -[:HAS_FORM]-> Form -[:HAS_FIELD]-> FieldorUseCase -[:PRODUCES]-> Artifact(text-level check for now; structured Cypher follow-up is out of scope). - Reasoning paragraph for the atom would naturally contain the phrase "spec gap also present" or "UC-X does not currently specify ..." — this is the signal the skill already writes today but does not act on.
When spec_gap: true, the classification stays BUG with HIGH confidence. The bug-vs-feature distinction used to be an unconditional user gate; it is now resolved by the two rules below, and surfaced to the user ONLY when the input has not already answered it AND getting the residual wrong is genuinely costly.
Step 2b-pre: honor a decision the input already made
Before firing ANY per-atom gate, scan the atom's own source span (the slice of the user's request / card that produced this atom) for an explicit decision answering the gate's question. Three recognizers, each requiring an explicit token:
- Explicit level expectation — a named level or its plain equivalent, e.g. "expect L1", "this is just a code fix", "L2 only if the contract pins X".
- Explicit sub-mode handling instruction — an imperative for a specific failure mode, e.g. "guard regardless", "reproduce on first", "downgrade/close if not reproducible".
- Explicit bug-vs-feature call — e.g. "this is a bug, the spec was incomplete" / "treat as new scope".
If a recognizer fires, record the resolution and skip that gate's prompt. Provenance: add USER_OVERRIDE to evidence and capture the verbatim source slice in trigger_evidence (same pattern as hard_refuse triggers) — this is what keeps the audit honest. On ANY ambiguity, do NOT guess — fall through to the (plain-language) prompt; a too-eager skip is worse than a too-eager gate. A recognizer may answer only PART of the question (the sub-mode but not bug-vs-feature) — resolve only what it answers; the rest continues to Step 2b-split.
Step 2b-split: ship what is unconditionally correct, defer only the genuine residual
A spec_gap atom often bundles two separable parts:
Shortened here. Read the whole file on GitHub.
Signals
- GitHub stars
- 27
- Forks
- 4
- Last commit
- Sep 2026
Advanced
- Item type
- skill
- Key
nacl-tl-intake- Source
- github.com/itsalt/nacl