nacl-goal
SkillAI & modelsSafety-first wrapper around Anthropic's /goal command. Resolves a high-level NaCl alias into a deterministic GOAL_PROOF completion condition that the transcript-only evaluator can actually verify. Two UX modes coexist: preview-by-default for the 2.10.0 aliases (wave / fix / validate / reopened-drain); autonomy-by-default for the 2.10.1 `intake` orchestrator alias (free-text/image goal → classified atoms → one PR → CI → staging) and the 2.18.0 `conduct` multi-cluster orchestrator (heterogeneous goal → clusters → one PR per cluster, wave-ordered). Use when: running a long NaCl loop autonomously, or the user says "/nacl-goal".
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-goal skill
What this skill tells your AI
The instructions your AI receives, as published by itsalt/nacl in nacl-goal/SKILL.md and read by ahel’s review.
Contract
Inputs this skill consumes:
<alias>— required positional. One of the named aliases fromnacl-goal/aliases.md(wave:<N>,fix:<BUG-NNN>,validate:<MOD-ID>,reopened-drain,intake,conduct,custom), or the special invocationsresumeandabort <run_id>.--start— optional flag. Without it the preview-mode aliases (wave,fix,validate,reopened-drain,custom) run in preview/dry-run mode only. Theintakealias has the inverse default: autonomy ON, with--plan-onlyas the opt-out (see §intakealias UX below).--tier=<S|M|L|XL>— optional override forcustomalias (mandatory for custom).--check-script=<path>— path to executable check script forcustomalias (mandatory for custom).--description="<one line>"— optional label recorded in the run file.intake-only opt-out flags:--plan-only,--strict,--target=<staging|dev-only>,--new-run. See §intakealias UX.
Outputs this skill produces:
- Without
--start: a preview block containing the full resolution: alias, tier, soft budget, check_script path, GOAL_PROOF template, human gates, permissions denylist, and (for Tier L/XL) estimated dollar cost fromnacl-goal/pricing.json. The exact--startcommand to copy-paste. - With
--start(2.10.0, Tier S/M): a warning that autonomous execution is 2.10.1 functionality, then issues/goalwith the composed condition. Does NOT produce a.tl/goal-runs/file in 2.10.0. - With
--start(2.10.0, Tier L/XL): structured refusalREFUSE_TIER_NOT_YET_ENABLED. - Refusal block (any tier, any phase) when a Tier-C gate is detected statically.
Downstream consumers of this output:
- Human user (preview, refusal, run summary)
.tl/goal-runs/— run files written on--start(enforced from 2.10.1)
Two-phase invocation (Architecture §2)
/goal starts a turn immediately on invocation. The preview/confirm UX lives
outside /goal, in this wrapper:
/nacl-goal <alias> # preview only — no /goal issued, no turn consumed
/nacl-goal <alias> --start # issues /goal with composed GOAL_PROOF condition
Preview output must include all of:
- Resolved alias name and canonical form
- Tier and full soft budget (turns, hours, observed token target) from the tier table below
check_scriptpath and how it is invoked each turn- Completion condition verbatim (including the GOAL_PROOF instruction block)
- Human gates that would block this alias (or
"none detected") - Permissions denylist that will be enforced
- For Tier L/XL: estimated dollar cost at current model pricing from
nacl-goal/pricing.json - The exact
--startcommand to copy-paste
--start behavior in 2.10.0
-
Tier S / Tier M: Issues
/goalwith the composed GOAL_PROOF condition, but emits this warning before doing so:WARNING (2.10.0): Autonomous execution via /nacl-goal --start is 2.10.1 functionality. In 2.10.0, /goal is issued but .tl/goal-runs/ write, concurrent-execution lock, crash/resume, and runtime gate detector are NOT active. Run interactively and monitor. -
Tier L / Tier XL: Refuses with
REFUSE_TIER_NOT_YET_ENABLED:REFUSE_TIER_NOT_YET_ENABLED Tier L and XL autonomous execution is not enabled in 2.10.0. Use /nacl-goal <alias> (preview) to inspect the plan. Autonomous Tier L/XL arrives in 2.10.1.
Tier table — v0 calibration defaults (Architecture §13)
All three columns are soft. /goal cannot hard-enforce them. A true hard cap
requires an external runner or Stop-hook script (future work, 2.10.2+).
Do not run XL unattended overnight in 2.10.0 or 2.10.1.
| Tier | turns_soft | wall_clock_soft | observed_token_target |
|---|---|---|---|
| S | 150 | 2 h | 3,000,000 |
| M | 500 | 6 h | 8,000,000 |
| L | 1,200 | 16 h | 20,000,000 |
| XL | 3,000 | 36 h | 50,000,000 |
Turn and wall-clock are surfaced through GOAL_PROOF every turn and trigger
GOAL_BUDGET_EXHAUSTED via the in-condition instruction. To be calibrated in
2.10.2 from aggregated .tl/goal-runs/.
GOAL_PROOF protocol (Architecture §1)
Every alias generates a /goal condition that instructs the primary session
to run the alias check script at the end of every turn and print a block of
this exact shape immediately after the raw command output:
GOAL_PROOF
alias: <alias>
tier: <S|M|L|XL>
check_command: <exact shell command run this turn>
result: GOAL_OK | GOAL_NOT_OK | GOAL_BLOCKED | GOAL_BUDGET_EXHAUSTED
evidence:
- <key>: <value>
- <key>: <value>
turns_so_far: <int>
observed_tokens: <int>
elapsed: <duration>
END_GOAL_PROOF
The evaluator (Haiku 4.5 by default) is transcript-only — it cannot run tools, read files, or execute commands. GOAL_PROOF surfaces machine-checkable state into the transcript so the evaluator's only job is: "did the last block have result == GOAL_OK AND does .tl/goal-runs/<run_id>.md exist."
This block is a wire format. Field renames and delimiter changes are major
version bumps. No narrative is permitted between the command output and the
GOAL_PROOF block. See docs/guides/goal-proof-protocol.md for full schema,
semantics, and examples.
Alias resolution and check scripts (Architecture §3)
Aliases and their binding contracts are defined in nacl-goal/aliases.md.
Do not duplicate alias definitions here — reference that file.
Check scripts shipped in 2.10.0 (stubs; truth-source wiring in progress):
nacl-goal/checks/wave.sh <N>
nacl-goal/checks/fix.sh <BUG-NNN>
nacl-goal/checks/validate.sh <MOD-ID>
nacl-goal/checks/reopened-drain.sh
Check scripts shipped in 2.10.1 (intake ships in PR1; the others remain deferred):
nacl-goal/checks/intake.sh --run-id <goal-run-id> # ✅ PR1
nacl-goal/checks/stubs-cleanup.sh <MOD-ID> # deferred
nacl-goal/checks/migrate-canary.sh # deferred
nacl-goal/checks/feature.sh <FR-NNN> # deferred
nacl-goal/checks/probe-stop-signals.sh (invoked each turn) # deferred
Check script shipped in 2.18.0 (conduct multi-cluster orchestrator):
nacl-goal/checks/conduct.sh --run-id <goal-run-id> # ✅ scans clusters/*/
Every check script:
- Takes its positional args per the contract in
nacl-goal/aliases.md - Reads its truth source directly (graph via Cypher, registry file, YouGile API, test runner)
- Prints stable, grep-friendly output followed immediately by a GOAL_PROOF block
- Always exits 0 — the evaluator cannot see exit codes; GOAL_PROOF carries the actual status
Structured refusal flow (Architecture §5)
Tier-C refusals fire at preview time wherever statically possible (by alias identity). The runtime gate detector catches dynamic crossings (2.10.1).
Every refusal must:
- Name the specific gate by its
REFUSE_*code fromnacl-goal/refusal-catalog.md - Cross-reference
nacl-tl-core/references/gate-fire-catalog.md - Offer a split-mode suggestion (interactive skill then wrapper)
- Print copy-paste commands for the interactive path
User-facing rendering follows the rendering rule in nacl-goal/refusal-catalog.md:
lead with the plain-language reason + copy-paste fallback; the gate code is a
trailing tag, not the headline; and step numbers / Tier-C never appear in
user-facing text. (Items 1–2 above are satisfied by the trailing tag and the
internal cross-reference — they are not the headline.)
Refusal codes (full catalog in nacl-goal/refusal-catalog.md):
REFUSE_HUMAN_GATE_BA_SA_HANDOFF
REFUSE_HUMAN_GATE_SA_PHASE_CONFIRMATION
REFUSE_HOTFIX_JUDGMENT
REFUSE_POST_CANARY_RETROSPECTIVE
REFUSE_PRODUCTION_MUTATION
REFUSE_UNTIERED_CUSTOM_GOAL
REFUSE_UNTRUSTED_WORKSPACE
REFUSE_HOOKS_DISABLED
REFUSE_CONCURRENT_GOAL_LOCKED
REFUSE_DANGEROUSLY_SKIP_PERMISSIONS
REFUSE_TIER_NOT_YET_ENABLED
Refusal codes are part of the wire format. Renaming or removing a code is a
major version bump for /nacl-goal.
Permissions denylist (Architecture §6)
/nacl-goal runs only in default permissions with explicit approvals, OR in
auto mode with the NaCl allowlist active.
Full text in docs/guides/goal-permissions.md. Brief summary:
Never allowed under any alias:
--dangerously-skip-permissions(triggersREFUSE_DANGEROUSLY_SKIP_PERMISSIONS)- Any mode that disables hooks (triggers
REFUSE_HOOKS_DISABLED) - Any workspace where workspace trust is not granted (
REFUSE_UNTRUSTED_WORKSPACE) git pushto any remotegit mergeintomain,master, orrelease/*- Any release-publishing action (
npm publish,gh release create, etc.) - Production DB migrations
rm -rfoutside the current workspace- Editing
.env*, secrets, credentials,.ssh/,~/.aws/,~/.config/gh - Changing CI/CD configuration or credentials
- Calling third-party paid APIs with side effects beyond test budget
Per-alias allowlist (positive grants):
- Local test execution
- Graph reads and writes scoped to current project
- Branch commits
gh pr create(but nevergh pr merge)- YouGile column moves within the project board
Custom alias (Architecture §12)
/nacl-goal custom \
--tier=<S|M|L|XL> # mandatory
--check-script=<path> # mandatory; must exist, be executable,
# and produce GOAL_PROOF-compatible output
--description="<one line>" # recorded in run file
--start # must be a separate invocation
Custom without --check-script returns REFUSE_UNTIERED_CUSTOM_GOAL.
Custom without --tier returns REFUSE_UNTIERED_CUSTOM_GOAL.
Custom may not target paths matching the Tier-C catalog in
nacl-goal/gate-fire-detector.md.
intake alias (2.10.1 — autonomous goal orchestrator)
intake is the FIRST alias with default_mode: autonomous. Where the four
2.10.0 aliases (wave, fix, validate, reopened-drain) require an
explicit --start to issue /goal, intake issues /goal by default and
provides opt-outs for previewing or strict mode.
This is intentional UX: /nacl-goal intake "<goal>" should be the short,
normal invocation. The user shouldn't need to remember internal flags or
gate names to drive a goal autonomously to a staging stand. See
[[feedback-autonomy-default-ux]] for the design rationale.
intake UX
/nacl-goal intake "<goal>"
Default behavior:
• autonomous execution is ON
• standard safe-exception envelope is ON (see nacl-goal/envelope.md)
• target = staging if config.yaml → deploy.staging.url exists,
otherwise PLAN_BLOCKED_STAGING_REQUIRED_BUT_MISSING
• branch_mode = current when invoked from a non-production branch:
atoms run ON the branch you are standing on, commits stay local,
ONE push at DELIVER (push_cadence = deferred). The preview prints
a one-line notice: "Running on your branch <name>; one push at
deliver; do not commit to this branch while the run is active."
From main/master/release/* the production refusal still fires —
create a working branch first.
• atoms BUG / TASK / FEATURE_SMALL run on that single branch, one PR
• atoms FEATURE_HEAVY → PLAN_BLOCKED with planning artifacts (no silent split)
• uncommitted changes (another agent's WIP) do NOT refuse the run in
branch_mode=current — see Flow step 3 "Smart WIP" for the
file-overlap protocol
Opt-outs (each disables a slice of the default):
--plan-only write planning artifacts only; no /goal, no branch, no PR,
no exception YAML, no source-code changes
--strict disable default safe-exception envelope; pre-flight refuses
if plan predicts a gate would need envelope auto-authorization
(PLAN_BLOCKED_STRICT_REQUIRES_INTERACTIVE_FLOW)
--branch=current run on the currently checked-out branch (default when on a
non-production branch)
--branch=new pre-2.14 behavior: create feature/goal-<short-hash>; requires
a clean worktree (PLAN_BLOCKED_DIRTY_WORKTREE applies)
--push=deferred atoms commit locally; single push at DELIVER (default when
branch_mode=current)
--push=per-atom push after every atom; PR opens on first push (default when
branch_mode=new — pre-2.14 behavior)
--push=none no push at all; run ends with local commits; ONLY valid with
--target=dev-only (with staging it is a usage error rejected
at argument parsing, before step 0 — no artifacts written);
deliver later with /nacl-tl-deliver
--target=staging require staging (default)
--target=dev-only local verify + PR only; final message MUST NOT claim staging
delivery; dev_verified is asserted via local /nacl-tl-verify
--new-run force fresh run-id even if goal_fingerprint matches an existing
run; does NOT close or reuse prior PR in 2.10.1
--budget=<profile> optional budget override (default Tier M: 200 turns / 3h / 4M tokens)
Backward-compat invariant: `--branch=new` reproduces the pre-2.14 flow
byte-for-byte (new goal branch, per-atom pushes, dirty-worktree refusal).
The default changed ONLY for invocations from an existing feature branch.
intake Flow (14 steps)
The Claude session running /nacl-goal intake executes the following flow.
For per-file schemas see nacl-goal/plan-lock-schema.md. For artifact
locations and idempotence see nacl-goal/run-artifacts.md. For the
exception envelope see nacl-goal/envelope.md. For gate prediction see
nacl-goal/gate-prediction.md. For retry semantics see
nacl-goal/retry-policy.md. For regression diff see
nacl-goal/regression-schema.md.
0. PRIVACY / IGNORE PRECHECK
verify .tl/goal-runs/ AND .tl/exceptions/goal-runs/ are gitignored
(use `git check-ignore` from project_root)
if either is NOT ignored:
→ PLAN_BLOCKED_GOAL_ARTIFACTS_NOT_GITIGNORED
The 2.10.1 wrapper does NOT auto-patch .gitignore. The user must do it.
Writing PII (user email, free-text goal, image refs) into a non-ignored
directory is irreversible if the user pushes by accident.
1. INIT_RUN
compute goal_fingerprint (see run-artifacts.md §Goal fingerprint)
acquire flock on .tl/goal-runs/index.lock (timeout 30s; else
PLAN_BLOCKED_INDEX_LOCK_BUSY)
consult index.json per the re-invocation rules in run-artifacts.md
(RESUME for transient interruptions; refuse for non-resumable terminal
states unless --new-run)
run_id = goal-intake-<utc-iso>-<short-hash>
mkdir .tl/goal-runs/<run_id>/{atoms/, planning/}
write request.json, budget.json
append index.json entry (state: "init", resumable: true)
atomic rename; release flock
2. RESOLVE_TARGET
--target=staging or default + deploy.staging.url present → deploy_target = staging
--target=dev-only → deploy_target = dev-only (WARN)
else → PLAN_BLOCKED_STAGING_REQUIRED_BUT_MISSING
3. PRECHECKS (Tier-C; /goal not yet issued)
on main/master/release/* → PLAN_BLOCKED_UNSAFE_PRODUCTION_MUTATION
(fires regardless of branch_mode; create a working branch first)
resolve branch_mode / push_cadence:
--branch absent → branch_mode = current (we are on a non-production branch)
--branch=new → branch_mode = new
push_cadence = --push if given, else deferred (current) / per-atom (new)
Smart WIP (branch_mode=current):
preexisting_dirty_files[] = paths from `git status --porcelain`
(including untracked); recorded in plan.lock.json at step 5
non-empty does NOT refuse — uncommitted files are presumed to be
another agent's in-flight work in the shared worktree. They are
never staged, never committed, never reverted by the goal run.
overlap resolution happens at step 5 (needs classified atoms);
hard runtime backstop at step 9 (commit-time collision gate)
branch_mode=new: dirty worktree → PLAN_BLOCKED_DIRTY_WORKTREE (pre-2.14 rule)
resolve baseline_command chain (config.yaml → package.json → pyproject → defaults)
missing → PLAN_BLOCKED_BASELINE_COMMAND_MISSING
capture regression-baseline.json per regression-schema.md
run the baseline in an ISOLATED throwaway worktree pinned to the
current HEAD sha (`git worktree add --detach <tmp> HEAD`), so other
agents' uncommitted WIP never contaminates the baseline; provision
deps per regression-schema.md §Worktree isolation; if provisioning
fails, fall back to in-tree run with worktree_isolated: false
(disclosed in GOAL_PROOF)
PLAN_BLOCKED_BASELINE_RED only fires if
(exit_code != 0 AND collected_count == 0)
OR (collected_count > 0 AND passed_count == 0)
i.e. zero-tests-collected runner error, or all-tests-failing.
missing gh auth / CI perms → PLAN_BLOCKED_GH_AUTH_OR_CI_PERMISSION_MISSING
4. CLASSIFY (/nacl-tl-intake --autonomous --yes --emit-state .tl/goal-runs/<run_id>/intake.json)
atoms[] with: id, type, linked_uc, evidence, confidence, risk_level,
depends_on, hard_refuse_triggers, trigger_evidence, spec_gap, residual_note,
diagnosis, skill_path.
Intake now SELF-DIAGNOSES before this policy applies (Step 2a.5 PROBE):
for every atom the graph alone did not resolve it verifies the competing
hypotheses against the actual code/DB (bounded read-only probes) and
derives a rubric score (nacl-tl-core/references/intake-scoring.md;
thresholds from the project's config.yaml -> intake.*, frozen into
diagnosis.threshold_used). "The graph didn't resolve it" alone never
reaches the user anymore.
--autonomous question policy (2.14+, probe-scored; see nacl-tl-intake
Step 2b case table):
HIGH L0/L1, HIGH spec-gap-no-hard-refuse → auto-route (as before)
HIGH + CODE (probe score >= high_confidence [0.9]) → auto-route
(as HIGH+GRAPH; "verified against the code")
HIGH L2/L3 launch-sanity (Template B) → auto-confirmed; informational line
MEDIUM with probe leaning (route_threshold [0.7] <= score <
high_confidence [0.9]) (Template D) → auto-route on the leading
hypothesis; alternative + blocking_fact tracked as residual_note
(reason medium_confidence_alternative);
pre-authorized via envelope.md gate `medium-confidence-routing`;
NEVER when the atom carries a hard_refuse_trigger
Sub-threshold (probe ran, score < route_threshold) (Template E)
→ ONE consolidated batch question,
asked HERE (pre-/goal interaction is allowed): list every unresolved
atom WITH its diagnosis (what was checked, per-hypothesis results,
leaning, blocking fact); the user answers once and the run proceeds
fully autonomously. Non-interactive session or declined →
PLAN_BLOCKED_AMBIGUOUS_CLASSIFICATION (as before)
hard_refuse (Template C) → unchanged: PLAN_BLOCKED_* below —
the user's "critical questions" always survive autonomy; a probe
never clears a hard_refuse_trigger
Refuse mapping (per plan-lock-schema.md §hard_refuse_triggers):
billing | destructive | l2_l3 | product_decision → _FEATURE_REQUIRES_PRODUCT_DECISION
(or _FEATURE_REQUIRES_HUMAN_PRODUCT_DECISION if FEATURE)
schema_migration | public_api_contract → _FEATURE_REQUIRES_SCHEMA_MIGRATION
auth_or_security | permissions → _FEATURE_REQUIRES_AUTH_OR_SECURITY_CHANGE
hotfix_or_release_routing → REFUSE (interactive)
FEATURE_HEAVY without trigger → write planning/feature-plan.md +
planning/open-decisions.md → PLAN_BLOCKED_FEATURE_REQUIRES_HUMAN_PRODUCT_DECISION
ambiguous AFTER the probe AND the consolidated batch → PLAN_BLOCKED_AMBIGUOUS_CLASSIFICATION
(MEDIUM-confidence atoms no longer reach this refusal — they auto-route;
sub-threshold atoms reach it only with their diagnosis attached)
PLAN_BLOCKED_PLAN_SPLIT_REQUIRED fires when EITHER:
(a) atoms touch >1 top-level module AND no dependency path connects the
atom groups AND total atoms >= 3; OR
(b) atoms require incompatible release targets; OR
(c) atoms require both normal feature-branch and hotfix/release routing; OR
(d) atoms imply mutually exclusive hard-refuse policies.
5. LOCK PLAN
ATOM ID INVARIANT: atom.id is assigned here once and is immutable for the run.
Form: atom-<short_sha256(type + linked_uc + normalized_title)[:12]>
Resume reads plan.lock.json and atoms/*.state.json — never re-classifies.
topological sort atoms by depends_on; cycle → PLAN_BLOCKED_ATOM_DEPENDENCY_CYCLE
tie-break: BUG before FEATURE_SMALL, then by id lexicographically
branch = feature/goal-<short-hash> (branch_mode=new)
branch = $(git rev-parse --abbrev-ref HEAD) (branch_mode=current)
branch_mode=current bookkeeping (recorded in plan.lock.json):
branch_base_sha = git merge-base <branch> <base_branch>
prior_unpushed_commits = git rev-list --count <base_branch>..HEAD
preexisting_dirty_files[] (snapshot from step 3)
WIP-overlap check (branch_mode=current, preexisting_dirty_files non-empty):
predict each atom's touch zone from the graph (linked UC → Module →
workspace directories + api-contracts paths) — coarse, directory-level
predicted zone ∩ preexisting_dirty_files == ∅ →
print one notice line ("N uncommitted files left untouched — presumed
another agent's work") and proceed
overlap → ONE consolidated plain-language question (pre-/goal, allowed):
per overlapping atom: continue anyway / commit those files into the
branch first / exclude the atom from this run
non-interactive session or declined → PLAN_BLOCKED_DIRTY_WORKTREE
(see refusal-catalog.md — the catalog entry covers both modes)
write plan.lock.json (incl. branch_mode, push_cadence), authorization.json
write atoms/<atom_id>.state.json with state="pending" for each atom
render initial PR body (per pr-body-template.md) to pr-body.md — WIP, atom table.
per-atom cadence: /nacl-tl-ship reads this file when it opens the PR on
the first push. deferred cadence: /nacl-tl-deliver reads it at the
single push. none: no PR in this run.
index.json state: "planned", resumable: true (flock-protected, atomic rename)
--plan-only: EXIT here
6. --STRICT PRE-FLIGHT (only when --strict)
for each atom: predict which gates would fire via gate-prediction.md
(uncertain prediction → block conservatively)
if any predicted gate ∈ envelope.md §Auto-enabled gates:
→ PLAN_BLOCKED_STRICT_REQUIRES_INTERACTIVE_FLOW
7. MATERIALIZE EXCEPTION ENVELOPE (skipped if --strict)
for each auto-enabled gate that the plan would hit:
write .tl/exceptions/goal-runs/<run_id>/EXC-goal-<gate>.yaml
owner: <git user.email>
reason: pre-authorized; goal fingerprint + sanitized_preview only
(full goal stays in gitignored request.json)
expires: issued_at + 3h
YAML sanitization rules per envelope.md §sanitized_preview (YAML-injection defense).
8. ISSUE /goal
composed condition = success_condition (per aliases.md §intake) + budget envelope
+ run_id binding
index.json state: "running", resumable: true
open progress.jsonl (wrapper-level events only; inner-skill summaries flow
through budget.json → inner_skill_runs[])
Shortened here. Read the whole file on GitHub.
Signals
- GitHub stars
- 27
- Forks
- 4
- Last commit
- Sep 2026
Advanced
- Item type
- skill
- Key
nacl-goal- Source
- github.com/itsalt/nacl