| name | goal-mode-tag-discipline |
| description | Use when you need exact regex/format details for goal-mode tag emission (parse-tags.mjs semantics, code-fence stripping, attribute quoting, escape-hatch regex, edge cases). Use BEFORE emitting a complex/long verdict or evidence block to verify the tag is parseable. |
v3.0 Status: Optional / Legacy Path
In v3.0+, tag emission is OPTIONAL. The preferred path is explicit CLI verbs:
<evidence/> tag → /goal-mode:goal-evidence-add --criterion N --file path[:line] --note "..."
<task-status>achieved</task-status> → /goal-mode:goal-achieve
<audit-verdict agent="X" status="GO">text</audit-verdict> → /goal-mode:goal-submit-verdict --agent X --status GO --text "text"
<review-request agents="X"/> → /goal-mode:goal-review-request then dispatch via Agent tool
The tag-emission path described below remains authoritative for:
- Codex
/goal and non-Claude-Code agents that can't invoke slash commands.
- Users who opt in to legacy v2 driver behaviour via
stopHookDriver: true config.
For all other Claude Code users in v3.0+, prefer the explicit CLI workflow — it removes the entire class of bugs around tag parsing, fenced-code stripping, attribute quoting, and silent-loop traps that this document spends most of its space describing.
Goal-mode tag emission — exact semantics
This skill is the precise reference for the parser in engine/parse-tags.mjs. Use it when you need to know:
- Which exact regex matches a tag
- How code regions are stripped (
engine/stop-hook.mjs::stripCodeRegions)
- What attribute quoting rules apply
- What the parser silently drops
- How escape-hatch is detected
For high-level behavior + when-to-emit guidance, use the using-goal-mode skill instead.
Pre-parse: code-region stripping
Before parsing tags, engine/stop-hook.mjs strips:
text.replace(/```[\s\S]*?```/g, '')
.replace(/`[^`\n]+`/g, '');
Implication:
- Tags inside ``` fenced blocks ARE removed before parsing.
- Tags inside
inline backticks ARE removed before parsing.
- Tags inside
<details>...</details> are NOT stripped — these are HTML, not Markdown code.
Rule for emission: put your machine-parsed tags in prose or inside <details> blocks. Never inside code fences.
Tag inventory + exact regexes
<evidence>
Self-closed: <evidence file="..." line="N" criterion="i" note="..." command="..." exit_code="N"/>
Paired: <evidence ...>note-body</evidence>
Regex: <evidence\b(${ATTRS_REGION})(?:\/>|>([\s\S]*?)<\/evidence>) (global)
Where ATTRS_REGION = (?:"[^"]*"|'[^']*'|[^>"'])*? — matches attribute name=value pairs in any order with double OR single quoting.
Validation:
criterion="N" — integer required. Missing or non-integer → tag silently dropped.
line, exit_code — optional integers. intOrNull(v) returns null on missing/non-int.
file, command, note — optional strings, default null/empty.
- Body wins over
note attribute: note = body.trim() || attrs.note || ''.
Out-of-range criterion: if criterion >= acceptance_criteria.length, the tag is recorded in cursor's evidence list but doesn't count toward coverage. The criterion at index N must exist in the task.
<task-status>
<task-status>pursuing|achieved|blocked</task-status>
Regex: <task-status>([\s\S]*?)<\/task-status> (global)
Validation:
- Value is case-insensitive (v2.0.3+): trimmed + lowercased → must be
pursuing, achieved, or blocked.
- Pre-v2.0.3: case-sensitive (silently dropped
ACHIEVED).
- Unknown values: silently dropped.
Multi-tag: if multiple <task-status> tags are emitted in one turn, applyMutations uses tags.find(t => t.kind === 'task-status') — first one wins.
<blocker>
<blocker>reason text</blocker>
Regex: <blocker>([\s\S]*?)<\/blocker> (global)
Validation:
- Empty / whitespace-only reason → tag silently dropped.
- Only consumed when paired with
<task-status>blocked</task-status>.
<review-request>
<review-request agents="reviewer-1,reviewer-2"/>
Regex: <review-request\b(${ATTRS_REGION})\/> (self-closed only)
Validation:
agents attribute required; comma-separated list, each trimmed; empty strings filtered out.
- Empty agents list → tag silently dropped.
- Paired form NOT supported — must be self-closed.
- Triggers
pursuing → review-pending transition ONLY when all criteria covered AND task is currently pursuing.
<audit-verdict>
<audit-verdict agent="reviewer-x" status="GO|NOGO|REVISE">
<verdict body text>
</audit-verdict>
Regex: <audit-verdict\b(${ATTRS_REGION})>([\s\S]*?)<\/audit-verdict> (global)
Validation:
agent attribute required (non-empty string).
status attribute required; case-normalized to UPPERCASE before enum check; must be GO, NOGO, or REVISE.
- Missing/unknown agent or status → silently dropped.
Reviewer-independence check (v2.0.0+): the engine reads the same turn's transcript and collects Agent(subagent_type=X) invocations. If agent in the verdict doesn't match any actually-dispatched subagent_type, the verdict is rejected (payload.rejected: true, payload.reason: 'no Agent dispatch detected — reviewer-independence violation') and does NOT advance the cursor.
Escape-hatch detection (v2.0.1 + v2.0.4)
Specific to <audit-verdict> when the reviewer's subagent_type is unavailable:
Pattern: status="REVISE" AND text matches /^\s*unavailable\b/i
Code: engine/apply-mutations.mjs:
const ESCAPE_HATCH_RE = /^\s*unavailable\b/i;
const isEscapeHatch = (v) => v.status === 'REVISE' && ESCAPE_HATCH_RE.test(v.text || '');
Examples that match:
<audit-verdict status="REVISE">unavailable; user must run /goal-approve</audit-verdict> ✓
<audit-verdict status="REVISE">UNAVAILABLE in environment</audit-verdict> ✓
<audit-verdict status="REVISE"> unavailable, please approve</audit-verdict> ✓ (leading whitespace OK)
Examples that DON'T match:
<audit-verdict status="NOGO">unavailable evidence</audit-verdict> ✗ (wrong status)
<audit-verdict status="REVISE">timing data is unavailable</audit-verdict> ✗ (substring, not prefix)
<audit-verdict status="REVISE">Couldn't dispatch the agent</audit-verdict> ✗ (doesn't start with "unavailable")
What escape-hatch does:
- Cursor →
blocked immediately
- State.lifecycle →
awaiting-manual-approval (v2.0.4)
cursor.blocker_reason filled with the unavailable agent names + recovery hint
- Stop hook renders
continuation-blocked.md ONCE, then suppresses
Attribute parsing nuances
Quoting
file="path/with spaces.ts" ✓ (double quotes)
file='path/with spaces.ts' ✓ (single quotes)
file=path-no-quotes ✗ (silently parses as empty)
note="contains a > char" ✓ (attr-region matcher is quote-aware)
note="contains "quoted" text" ✗ (embedded `"` breaks parsing; use single quotes around the value)
note="contains 'quoted' text" ✓ (mix)
Duplicate attributes
<evidence file="a" file="b" criterion="0"/>
Last-wins semantics: file="b".
Numeric attributes
criterion, line, exit_code — parsed via intOrNull(v):
"" (empty) → null
- Non-numeric → null
- Negative integers ACCEPTED at parse time (range-checking is the consumer's job; out-of-range
criterion is silently filtered from coverage)
HTML escapes
NOT decoded. <tag> passes through as-is. If your verdict body contains < or >, just write them — the attr-region matcher and body regex are quote-aware.
Nested tags
The parser is flat. Don't nest <evidence> inside <evidence> or <audit-verdict> inside <audit-verdict>. The outer parse will consume up to the first matching close tag, leaving the inner tag orphaned.
Tag visit order (within one turn's parse output)
parseTags() emits tags in this order:
- All
<evidence> (in source order)
- All
<task-status> (in source order)
- All
<blocker> (in source order)
- All
<review-request> (in source order)
- All
<audit-verdict> (in source order)
applyMutations then processes them in this order with kind-specific rules:
- Evidence loop — all
<evidence> tags push onto cursor's evidence array.
- Task-status (first only via
find()) — achieved checks coverage, transitions to review-pending or achieved. blocked increments review_attempts. pursuing resets to pursuing.
- Review-request — if cursor is pursuing AND all criteria covered, transitions to
review-pending.
- Audit-verdicts — only consumed when cursor is
review-pending. Filter by reviewer-independence (v2.0.0+) and escape-hatch (v2.0.1+).
- Terminal lifecycle checks —
achieved / unmet / budget-limited transitions.
What happens to malformed tags
| Defect | Result |
|---|
<task-status>frobnicate</task-status> | Silently dropped (not in STATUS_VALUES) |
<evidence note="no criterion"/> | Silently dropped (criterion required) |
<evidence criterion="abc"/> | Silently dropped (non-integer) |
<evidence criterion="0" file="x">body</evidence> with empty body and no note attr | Recorded with note="" |
<audit-verdict agent="x">no status</audit-verdict> | Silently dropped (status required) |
<audit-verdict status="GO">no agent</audit-verdict> | Silently dropped (agent required) |
<review-request/> (no agents) | Silently dropped |
<blocker></blocker> (empty) | Silently dropped |
Silent drops are intentional — the parser is fail-permissive so a single malformed tag doesn't break a multi-tag turn. The cost: no error message tells you what was dropped. Always preview your emission by re-reading the prose you generated, looking specifically at each tag.
Common emission mistakes
Mistake: tags inside fenced code
Here's my evidence:
\`\`\`
<evidence file="a" criterion="0"/>
<task-status>achieved</task-status>
\`\`\`
→ stripCodeRegions removes the entire fenced block before parsing. Tags lost. Engine fires same prompt next turn.
Mistake: paired review-request
<review-request agents="x">narrative text</review-request>
→ Parser only accepts self-closed form. Tag dropped.
Mistake: status string case
Pre-v2.0.3:
<task-status>Achieved</task-status>
→ Silently dropped.
v2.0.3+: normalized to achieved, accepted.
Mistake: fabricated audit-verdict
<audit-verdict agent="my-reviewer" status="GO">trust me, the code is correct</audit-verdict>
Without an Agent({subagent_type: "my-reviewer", ...}) call in the same turn's transcript:
- v1.x: would have been accepted (bug).
- v2.0.0+: rejected with
payload.rejected: true. Cursor doesn't advance. Engine surfaces "rejected verdicts" in the next Stop-hook prompt.
Mistake: wrong escape-hatch wording
<audit-verdict agent="x" status="REVISE">cannot dispatch this reviewer</audit-verdict>
→ Text doesn't start with "unavailable". Treated as a regular REVISE → fabricated (no Agent dispatch) → rejected. Does NOT trigger the v2.0.4 escape-hatch path.
Correct:
<audit-verdict agent="x" status="REVISE">unavailable; user must run /goal-approve</audit-verdict>
When to use this skill
Invoke goal-mode-tag-discipline skill:
- Before emitting a complex verdict (especially when relaying long subagent output through
<audit-verdict> body).
- When you've emitted tags but the engine fired the same prompt again — your tags may have been silently dropped; check the rules here.
- When constructing the escape-hatch verdict.
- When designing prompt templates for reviewer subagents — to know what format their output should take so it can be relayed cleanly.
Companion skill using-goal-mode covers the high-level behavior (when to do what, lifecycle states, recovery paths).