From goal-mode
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.
How this skill is triggered — by the user, by Claude, or both
Slash command
/goal-mode:goal-mode-tag-disciplineThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
In v3.0+, **tag emission is OPTIONAL**. The preferred path is explicit CLI verbs:
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 toolThe tag-emission path described below remains authoritative for:
/goal and non-Claude-Code agents that can't invoke slash commands.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.
This skill is the precise reference for the parser in engine/parse-tags.mjs. Use it when you need to know:
engine/stop-hook.mjs::stripCodeRegions)For high-level behavior + when-to-emit guidance, use the using-goal-mode skill instead.
Before parsing tags, engine/stop-hook.mjs strips:
text.replace(/```[\s\S]*?```/g, '') // fenced blocks (multiline)
.replace(/`[^`\n]+`/g, ''); // inline spans (single line)
Implication:
inline backticks ARE removed before parsing.<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.
<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.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:
pursuing, achieved, or blocked.ACHIEVED).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:
<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.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.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.
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:
blocked immediatelyawaiting-manual-approval (v2.0.4)cursor.blocker_reason filled with the unavailable agent names + recovery hintcontinuation-blocked.md ONCE, then suppressesfile="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)
<evidence file="a" file="b" criterion="0"/>
Last-wins semantics: file="b".
criterion, line, exit_code — parsed via intOrNull(v):
"" (empty) → nullcriterion is silently filtered from coverage)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.
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.
parseTags() emits tags in this order:
<evidence> (in source order)<task-status> (in source order)<blocker> (in source order)<review-request> (in source order)<audit-verdict> (in source order)applyMutations then processes them in this order with kind-specific rules:
<evidence> tags push onto cursor's evidence array.find()) — achieved checks coverage, transitions to review-pending or achieved. blocked increments review_attempts. pursuing resets to pursuing.review-pending.review-pending. Filter by reviewer-independence (v2.0.0+) and escape-hatch (v2.0.1+).achieved / unmet / budget-limited transitions.| 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.
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.
<review-request agents="x">narrative text</review-request>
→ Parser only accepts self-closed form. Tag dropped.
Pre-v2.0.3:
<task-status>Achieved</task-status>
→ Silently dropped.
v2.0.3+: normalized to achieved, accepted.
<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:
payload.rejected: true. Cursor doesn't advance. Engine surfaces "rejected verdicts" in the next Stop-hook prompt.<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>
Invoke goal-mode-tag-discipline skill:
<audit-verdict> body).Companion skill using-goal-mode covers the high-level behavior (when to do what, lifecycle states, recovery paths).
Creates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.
npx claudepluginhub lokafinnsw/claude-code-goal-mode --plugin goal-mode