dx-simple

star 6

Apply a small AEM change (a11y label, color, spacing, copy, css-class, icon, focus trap, or other small behavior tweak) by splitting work into authoring (JCR writes) and code (file edits → PR) paths. Reads the ADO story directly — a structured ```simple``` block is recommended but optional. Trigger on "simple change", "small tweak", "apply tweak".

easingthemes By easingthemes schedule Updated 6/16/2026

name: dx-simple description: Apply a small AEM change (a11y label, color, spacing, copy, css-class, icon, focus trap, or other small behavior tweak) by splitting work into authoring (JCR writes) and code (file edits → PR) paths. Reads the ADO story directly — a structured simple block is recommended but optional. Trigger on "simple change", "small tweak", "apply tweak". when_to_use: "Use for small AEM changes — a11y labels, colors, spacing, copy, CSS classes, icons, focus traps. Trigger on 'simple change', 'small tweak', 'apply tweak', '@kai-simple', or when a ticket is a minor AEM adjustment." argument-hint: "" allowed-tools: ["read", "edit", "search", "write", "agent"] model: sonnet hooks: PreToolUse: - matcher: Bash command: ${CLAUDE_PLUGIN_ROOT}/skills/dx-simple/hooks/block-mvn-deploy.sh timeout: 5


You are the coordinator for /dx-simple — a tight, pipeline-grade skill that applies a small AEM change (authoring or code) under a strict confidence model. You do NOT implement anything in main context except:

  1. Parse the ADO story's simple block (Phase 1).
  2. Dispatch parallel research subagents (Phase 2).
  3. Apply authoring writes via AEM MCP (Phase 3a) — this is in main context because audit log + rollback record require it.
  4. Read the work-plan to decide control flow.

All other work — codebase reading, visual verify, compile, diff review — runs in context: fork subagents so the main context stays under ~30 turns.

Argument

$ARGUMENTS is <id-or-url> [free text] — the ADO work item ID (numeric, e.g. 9999999, or a full URL) optionally followed by a free-text instruction, just like /dx-simple 9999999 make the heading blue typed in a local session. Extract the numeric TICKET_ID (leading digits, or from the URL); anything after it is INLINE_INPUT. Always pass TICKET_ID (not the raw $ARGUMENTS) to the scripts and MCP calls below. If no id, ask once; if still none in pipeline mode, exit non-zero.

Pre-flight (HARD GATE — runs before anything else)

bash $CLAUDE_PLUGIN_ROOT/skills/dx-simple/scripts/preflight.sh

If it exits non-zero, STOP. Post the stderr to ADO as a comment:

mcp__ado__wit_add_work_item_comment with the preflight error text

Exit non-zero. No other action.

Recovery config (read once, after pre-flight)

Resumable recovery (TODO #141) is config-driven — never hardcode the token or the attempt cap. Read both from .ai/config.yaml (defaults applied if unset):

# Trigger keyword (default @kai-simple). The bot NEVER emits the literal token —
# it writes the keyword bare and instructs the human to prefix it with @.
TRIGGER_TOKEN=$(bash .ai/lib/dx-common.sh yaml-val 'dx-simple.recovery.trigger-token'); TRIGGER_TOKEN=${TRIGGER_TOKEN:-@kai-simple}
KEYWORD=${TRIGGER_TOKEN#@}                 # bare form used in bot-emitted text
MAX_ATTEMPTS=$(bash .ai/lib/dx-common.sh yaml-val 'dx-simple.recovery.max-attempts'); MAX_ATTEMPTS=${MAX_ATTEMPTS:-3}

The documented ADO Service Hook filter string MUST match trigger-token — this is the single source of truth (see the pipeline + website docs). The comment hook fires only on comments containing the token; the bot's own comments never contain the literal token, so they cannot self-trigger.

User input (the text after the trigger) — like local CLI args

The free text a human writes after the trigger is the primary instruction, exactly as if they ran /dx-simple <id> <instruction> locally. The token only fires the pipeline; the words after it are the prompt. Resolve USER_INPUT once, here:

  1. Local session — if INLINE_INPUT (text after the id in $ARGUMENTS) is non-empty → USER_INPUT="$INLINE_INPUT", TRIGGER_COMMENT_ID="".
  2. Pipeline run (fired by an ADO comment) — else fetch comments and select the triggering one:
    mcp__ado__wit_list_work_item_comments with workItemId=$TICKET_ID
    
    Pick the comment that is (a) authored by a non-bot identity, (b) contains $TRIGGER_TOKEN, (c) has the highest ADO comment id (ids are monotonic). Strip the token → USER_INPUT; record its id as TRIGGER_COMMENT_ID.
  3. USER_INPUT may be empty (a bare @<keyword> with no words) — then only the story body drives the change (today's behavior).

USER_INPUT is authoritative: where it conflicts with the story body, it wins. Phase 1 folds it into the change request on a fresh run (step 2d); Phase 0 uses it (with TRIGGER_COMMENT_ID vs COMMENT_CURSOR) to reopen a completed ticket.

Spec directory + branch (established by Phase 0)

SPEC_DIR and the per-ticket BRANCH are NOT computed here — Phase 0 (resume-check.sh) establishes them, because a fresh pipeline container can't recompute run 1's slug and must discover the ticket's existing branch + committed state. Phase 0 emits SPEC_DIR=… / BRANCH=…; capture those values.

Fresh-run state init (only when Phase 0 dispatch is fresh):

bash $CLAUDE_PLUGIN_ROOT/skills/dx-simple/scripts/update-progress.sh "$SPEC_DIR" "Preflight" "done"
echo '{"gates":{}}' > "$SPEC_DIR/confidence.json"
# resume-state.json — the durable status the next run dispatches on.
sed "s/{TICKET}/$TICKET_ID/" \
    "$CLAUDE_PLUGIN_ROOT/skills/dx-simple/templates/resume-state.json.tmpl" \
    > "$SPEC_DIR/resume-state.json"
# authoring-diff.json is created lazily by Phase 3a when the first write happens

On a resume dispatch, these files already exist on the branch (committed by a prior run) — do NOT re-initialize them; read them.

Touch the orchestration flag (in case a future orchestrator wraps this skill):

mkdir -p .ai/run-context
touch .ai/run-context/orchestrating.flag

State files written during the run (the skill creates these as it goes):

File Created in Purpose
resume-state.json Phase 0 (fresh) / read on resume Durable recovery status — status, last-completed-phase, blocked-at-phase, blocker{}, answer-attempts, comment-cursor, run-history[]. Phase 0 dispatches on it. Committed every checkpoint.
raw-story.md Phase 1 ADO story dump
simple-block.yaml Phase 1 (after parse) Parsed DoR block
before.png Phase 1 (after G1) Pre-change Chrome screenshot
locator-bbox.json Phase 1 (after G1) {x, y, w, h} of locator's bounding box
confidence.json initialized in pre-flight; appended per gate {"gates":{"G1":{"score":1,"status":"pass"}, ...}}
dialog-map.json Phase 2 (from dialog-inspector subagent) Field name → type + values
file-list.json Phase 2 (from file-resolver subagent) Source file paths
work-plan.json Phase 2 (classify-work.sh) Authoring + code arrays
authoring-diff.json Phase 3a (per write) Before/after for rollback
compile.log Phase 4 (per retry, overwritten) Build output, tail in report
after.png Phase 5 Post-change Chrome screenshot
visual-diff.json Phase 5 (visual-diff.sh) Overall + region pixel scores
diff-review.md Phase 5.5 (from dx-pr-reviewer) Reviewer findings
simple-progress.md initialized in pre-flight; updated per phase Phase status table
followup.md Reopen path (done + fresh comment) Verbatim post-trigger USER_INPUT that reopened a completed ticket; checkpointed for audit/report
report.md Phase 7 (rendered from template) Final human-readable report

Confidence model (gates G1, G3–G9)

This skill enforces 8 confidence gates. Any gate failure → ABORT path: classify the blocker, conditionally roll back authoring (terminal only — C3), persist state, ADO comment, verdict: fail (pipeline exits non-zero). Track scores in $SPEC_DIR/confidence.json; the final report quotes them. A blocker on a resumed run feeds the re-ask loop (see ABORT path) instead of a flat abort.

Gate Phase Threshold
G1 Locator match 1 exactly 1 DOM match for element
G3 Classification 2 high or medium
G4 Per-file edit confidence 3b ≥ 0.85
G5 Visual non-target identical 5 ≥ 99%
G6 Visual target changed 5 ≥ 5%
G7 Review blockers @ ≥80% conf 5.5 0
G8 Cost global ≤ $2 (configurable)
G9 Time global ≤ 12 min

Note: G2 (resource-type allowlist) was removed — the skill now runs on any component. The G* numbering is preserved for backwards compatibility with existing reports.

Flow

digraph dx_simple {
    "Preflight" [shape=box];
    "Phase 0: Resume (branch + dispatch)" [shape=box];
    "Read resume comment + apply answer" [shape=box];
    "Replay code edits from work-plan.json" [shape=box];
    "Phase 1: Fetch + extract change details" [shape=box];
    "Phase 0.5: Repo identity guard" [shape=box];
    "G1: locator match exactly 1?" [shape=diamond];
    "Phase 2: Classify (parallel subagents)" [shape=box];
    "G3: classification high or medium?" [shape=diamond];
    "Phase 3a: Apply authoring writes" [shape=box];
    "Phase 3b: Apply code edits" [shape=box];
    "G4: per-file edit confidence ≥85%?" [shape=diamond];
    "Scope-check ok?" [shape=diamond];
    "Phase 4: Compile (≤3 retries)" [shape=box];
    "Compile passed?" [shape=diamond];
    "Phase 5: Visual verify" [shape=box];
    "G5+G6: visual gates pass?" [shape=diamond];
    "One re-edit retry left?" [shape=diamond];
    "Re-edit with screenshot context" [shape=box];
    "Phase 5.5: Diff review (dx-pr-reviewer)" [shape=box];
    "G7: zero blockers ≥80% conf?" [shape=diamond];
    "Phase 6: Activate + Commit + PR" [shape=box];
    "Phase 7: Write report + ADO comment" [shape=doublecircle];

    // --- ABORT, expanded (classify → conditional rollback → persist → comment) ---
    "ABORT: classify blocker" [shape=box];
    "Terminal (hard/final)?" [shape=diamond];
    "Rollback authoring (terminal only)" [shape=box];
    "Keep authoring (recoverable)" [shape=box];
    "Write report.md (Recovery section)" [shape=box];
    "save-state.sh: commit + push state" [shape=box];
    "git checkout -- . (discard source edits)" [shape=box];
    "Post classified ADO comment + exit" [shape=doublecircle];

    // --- re-ask loop on a failed RESUMED gate ---
    "Resumed gate still fails?" [shape=diamond];
    "answer-attempts < max?" [shape=diamond];
    "Post sharper question (blocked-needs-input)" [shape=box];
    "Downgrade to blocked-hard → DevAgent" [shape=box];

    // --- terminal resume exits ---
    "Re-post hard note + exit" [shape=doublecircle];
    "No-op: already done (PR exists)" [shape=doublecircle];

    // --- reopen completed work on a fresh follow-up comment ---
    "Fresh follow-up trigger comment?" [shape=diamond];
    "Reopen: apply follow-up delta (reuse artifacts)" [shape=box];

    "Preflight" -> "Phase 0: Resume (branch + dispatch)";

    // Phase 0 dispatch out-edges
    "Phase 0: Resume (branch + dispatch)" -> "Phase 1: Fetch + extract change details" [label="fresh"];
    "Phase 0: Resume (branch + dispatch)" -> "Replay code edits from work-plan.json" [label="in-progress (past 3b)"];
    "Phase 0: Resume (branch + dispatch)" -> "Phase 1: Fetch + extract change details" [label="in-progress (at/before 3b) → last+1"];
    "Phase 0: Resume (branch + dispatch)" -> "Read resume comment + apply answer" [label="blocked-needs-input"];
    "Phase 0: Resume (branch + dispatch)" -> "Re-post hard note + exit" [label="blocked-hard"];
    "Phase 0: Resume (branch + dispatch)" -> "Fresh follow-up trigger comment?" [label="done"];
    "Phase 0: Resume (branch + dispatch)" -> "Read resume comment + apply answer" [label="ambiguous-branch (asks which branch)"];

    // done is a no-op UNLESS a new follow-up comment (id > comment-cursor) reopens it
    "Fresh follow-up trigger comment?" -> "Reopen: apply follow-up delta (reuse artifacts)" [label="yes (non-bot, id > cursor)"];
    "Fresh follow-up trigger comment?" -> "No-op: already done (PR exists)" [label="no"];
    // reuse committed story/plan/work-plan; re-enter the edit phase (resume-forward, last=Phase 2)
    "Reopen: apply follow-up delta (reuse artifacts)" -> "Phase 3a: Apply authoring writes" [label="reuse artifacts, edit-phase delta"];
    "Reopen: apply follow-up delta (reuse artifacts)" -> "Phase 1: Fetch + extract change details" [label="escape: different element/page"];

    "Read resume comment + apply answer" -> "Replay code edits from work-plan.json" [label="re-enter past 3b"];
    "Read resume comment + apply answer" -> "Phase 1: Fetch + extract change details" [label="re-enter ≤ 3b"];
    "Replay code edits from work-plan.json" -> "Phase 4: Compile (≤3 retries)" [label="re-enter target phase"];

    "Phase 1: Fetch + extract change details" -> "Phase 0.5: Repo identity guard";
    "Phase 0.5: Repo identity guard" -> "ABORT: classify blocker" [label="wrong target"];
    "Phase 0.5: Repo identity guard" -> "G1: locator match exactly 1?" [label="proceed"];
    "G1: locator match exactly 1?" -> "ABORT: classify blocker" [label="no"];
    "G1: locator match exactly 1?" -> "Phase 2: Classify (parallel subagents)" [label="yes"];
    "Phase 2: Classify (parallel subagents)" -> "G3: classification high or medium?";
    "G3: classification high or medium?" -> "ABORT: classify blocker" [label="no — abort"];
    "G3: classification high or medium?" -> "Phase 3a: Apply authoring writes" [label="yes — if authoring items"];
    "G3: classification high or medium?" -> "Phase 3b: Apply code edits" [label="yes — if code items"];
    "Phase 3a: Apply authoring writes" -> "Phase 5: Visual verify" [label="if no code items"];
    "Phase 3a: Apply authoring writes" -> "G4: per-file edit confidence ≥85%?" [label="if code items also queued"];
    "Phase 3b: Apply code edits" -> "G4: per-file edit confidence ≥85%?";
    "G4: per-file edit confidence ≥85%?" -> "ABORT: classify blocker" [label="no"];
    "G4: per-file edit confidence ≥85%?" -> "Scope-check ok?" [label="yes"];
    "Scope-check ok?" -> "ABORT: classify blocker" [label="no"];
    "Scope-check ok?" -> "Phase 4: Compile (≤3 retries)" [label="yes"];
    "Phase 4: Compile (≤3 retries)" -> "Compile passed?";
    "Compile passed?" -> "Phase 5: Visual verify" [label="yes"];
    "Compile passed?" -> "ABORT: classify blocker" [label="no (hard — after 3 retries)"];
    "Phase 5: Visual verify" -> "G5+G6: visual gates pass?";
    "G5+G6: visual gates pass?" -> "Phase 5.5: Diff review (dx-pr-reviewer)" [label="yes"];
    "G5+G6: visual gates pass?" -> "One re-edit retry left?" [label="no"];
    "One re-edit retry left?" -> "Re-edit with screenshot context" [label="yes"];
    "One re-edit retry left?" -> "ABORT: classify blocker" [label="no"];
    "Re-edit with screenshot context" -> "Phase 4: Compile (≤3 retries)";
    "Phase 5.5: Diff review (dx-pr-reviewer)" -> "G7: zero blockers ≥80% conf?";
    "G7: zero blockers ≥80% conf?" -> "ABORT: classify blocker" [label="no"];
    "G7: zero blockers ≥80% conf?" -> "Phase 6: Activate + Commit + PR" [label="yes"];
    "Phase 6: Activate + Commit + PR" -> "Phase 7: Write report + ADO comment";

    // A gate that fails on a RESUMED run feeds the re-ask loop instead of a flat abort.
    "ABORT: classify blocker" -> "Resumed gate still fails?";
    "Resumed gate still fails?" -> "answer-attempts < max?" [label="yes (this was a needs-input resume)"];
    "Resumed gate still fails?" -> "Terminal (hard/final)?" [label="no (first failure / non-input)"];
    "answer-attempts < max?" -> "Post sharper question (blocked-needs-input)" [label="yes"];
    "answer-attempts < max?" -> "Downgrade to blocked-hard → DevAgent" [label="no (cap reached)"];
    "Post sharper question (blocked-needs-input)" -> "Terminal (hard/final)?" [label="recoverable"];
    "Downgrade to blocked-hard → DevAgent" -> "Terminal (hard/final)?" [label="terminal"];

    "Terminal (hard/final)?" -> "Rollback authoring (terminal only)" [label="yes"];
    "Terminal (hard/final)?" -> "Keep authoring (recoverable)" [label="no"];
    "Rollback authoring (terminal only)" -> "Write report.md (Recovery section)";
    "Keep authoring (recoverable)" -> "Write report.md (Recovery section)";
    "Write report.md (Recovery section)" -> "save-state.sh: commit + push state";
    "save-state.sh: commit + push state" -> "git checkout -- . (discard source edits)";
    "git checkout -- . (discard source edits)" -> "Post classified ADO comment + exit";
}

Node Details

Phase 0: Resume (branch + dispatch)

Runs immediately after pre-flight, before Phase 1. It establishes the per-ticket branch (created early — decision #5), reverse-maps it to the committed spec dir, reads resume-state.json, and decides how to proceed. The per-ticket branch is the durable state store, so a crashed/blocked prior run is recoverable.

# Capture the key=value lines, then read each value (values like "Phase 2" contain
# spaces, so DON'T `eval` the block — read fields explicitly).
RC=$(bash $CLAUDE_PLUGIN_ROOT/skills/dx-simple/scripts/resume-check.sh "$TICKET_ID")
val() { sed -n "s/^$1=//p" <<<"$RC" | head -1; }
DISPATCH=$(val DISPATCH); BRANCH=$(val BRANCH); SPEC_DIR=$(val SPEC_DIR)
STATUS=$(val STATUS); RE_ENTER_PHASE=$(val RE_ENTER_PHASE)
REPLAY_CODE_EDITS=$(val REPLAY_CODE_EDITS); ANSWER_ATTEMPTS=$(val ANSWER_ATTEMPTS)
LAST_COMPLETED_PHASE=$(val LAST_COMPLETED_PHASE); BLOCKED_AT_PHASE=$(val BLOCKED_AT_PHASE)

resume-check.sh owns anchored discovery (feature/<id>-* / bugfix/<id>-* — never a bare *<id>* substring, so 123 can't match 1234), the 0 / 1 / >1 branch decision, and the reverse branch→spec-dir mapping. It delegates only the create-fresh path to shared/ensure-feature-branch.sh (M2 — the shared helper's contract is unchanged for its other consumers). It is read-only w.r.t. resume-state.json (never bumps answer-attempts — a crash must not burn the needs-input cap, M4).

Dispatch on DISPATCH:

DISPATCH meaning action
fresh no branch / no committed state run fresh-run state init (above), proceed to Phase 1.
resume-forward prior run crashed (in-progress) if REPLAY_CODE_EDITS=true, run Replay code edits first, then re-enter RE_ENTER_PHASE. Reuse committed discovery artifacts. Does NOT consume an answer-attempt.
resume-blocked-input recoverable block awaiting a human answer run Read resume comment + apply answer, then (replay if past 3b) re-enter RE_ENTER_PHASE.
resume-blocked-hard unrecoverable, or answer-attempts cap reached Re-post hard note + exit — re-post the "needs manual fix / re-tag DevAgent" note; exit non-zero.
done Phase 7 already succeeded Run Reopen completed work: if a fresh follow-up trigger comment exists (non-bot, contains the token, id > COMMENT_CURSOR) treat it as a continuation on the same branch. Otherwise no-op — comment "already completed in PR; reply @<keyword> <change> to request a follow-up, or open a new ticket"; exit 0.
ambiguous-branch >1 feature/<id>-* match post a needs-user-input comment asking which branch to resume (loop-safe format); set blocked-needs-input; exit. Do NOT pick one.

git-rules.md exception. git-rules.md says "never reuse old feature/bugfix branches for new work." /dx-simple resume is a deliberate, scoped exception — it reuses the branch for the same ticket, same work, continued, only when the anchored match is unique (ambiguous → blocker, not reuse).

Reopen completed work (follow-up comment)

Only on DISPATCH=done. A completed run is normally a no-op, but a reviewer often wants a small refinement ("reuse the existing query-param util", "tighten the spacing") rather than a whole new ticket. Treat the fresh trigger comment as a new prompt in a new session that already has all the prior artifacts — reuse them and do only the delta (decision: skip the steps that are already done — story fetch, locate, classify — exactly as a developer reopening the spec dir locally would). Refuse to reopen on a stale or already-consumed comment.

  1. Is there a fresh follow-up? Reopen only if USER_INPUT is non-empty and TRIGGER_COMMENT_ID > COMMENT_CURSOR (numeric; treat an empty cursor as 0).
    • Otherwise → genuine no-op (stray re-trigger / service-hook replay / bare token / already-consumed comment). Post once: "Already completed in PR. Reply @<keyword> <change> to request a follow-up, or open a new ticket." Exit 0.
  2. Reuse the committed artifacts on the branch — raw-story.md, simple-block.yaml, dialog-map.json, file-list.json, work-plan.json, confidence.json. Do not re-fetch the story, re-run G1 locate, or re-classify (Phase 2): they are already committed. This is the "new local session on an existing spec dir" behavior the user expects.
  3. Apply the follow-up as a delta. Read work-plan.json + USER_INPUT and update the work-plan to satisfy the follow-up (e.g. change a replacement to call the existing util; add or adjust a code[] item). Append USER_INPUT to simple-block.yaml's what (report context) and record it verbatim in $SPEC_DIR/followup.md.
    • Escape hatch: if the follow-up targets a different element or page than the committed locator, the reused discovery no longer holds — re-enter Phase 1 instead (full re-run with USER_INPUT as the primary request).
  4. Seed resume-forward + advance the cursor atomically, then checkpoint. Advancing the cursor before re-entry is the loop guard (L2): once this run completes to done, the same comment is at/below the cursor and cannot reopen again — a further follow-up needs a brand-new comment.
    jq --arg c "$TRIGGER_COMMENT_ID" \
       '.status="in-progress" | .["comment-cursor"]=$c
        | .["last-completed-phase"]="Phase 2"
        | .["run-history"] += [{"event":"reopen","comment":$c}]' \
       "$SPEC_DIR/resume-state.json" > "$SPEC_DIR/.rs.tmp" \
       && mv "$SPEC_DIR/.rs.tmp" "$SPEC_DIR/resume-state.json"
    bash $CLAUDE_PLUGIN_ROOT/skills/dx-simple/scripts/save-state.sh "$SPEC_DIR" "Phase 2"
    
    (Exit 3 = BRANCH-ADVANCED → a concurrent resume is in flight; stop.)
  5. Re-enter the edit phase exactly as a resume-forward would: with last-completed-phase = Phase 2, the next actionable phase is Phase 3a (authoring) / Phase 3b (code). Apply the updated work-plan, then flow forward — compile → visual verify → diff review → Phase 6 (update-mode amends the existing PR, M5) → Phase 7. Discovery and classification are skipped; only the delta is implemented and verified. This path does not touch answer-attempts (it is a new request, not a re-ask of a blocked gate).

Read resume comment + apply answer

Only on resume-blocked-input (or ambiguous-branch resolution). Select the human's answer and apply it before re-entering the blocked phase.

  1. Fetch comments (mcp__ado__wit_list_work_item_comments). Select the comment that is: (a) authored by a non-bot identity, (b) contains the trigger token ($TRIGGER_TOKEN), (c) has the highest ADO comment id (ids are monotonic — order by id, not timestamp), and (d) id > the stored comment-cursor. If none qualifies → cheap exit (stray re-trigger; nothing to do).
  2. Strip the token; the remainder is continue-input. Apply it to blocked-at-phase (e.g. set simple-block.yaml element for a G1 ambiguity; add a file/line/anchor hint for G4; pick the branch for ambiguous-branch).
  3. Commit the updated comment-cursor atomically with / before re-entering the phase (L2) — via jq into resume-state.json then save-state.sh — otherwise a crash after applying the answer reprocesses the same comment.
  4. Re-enter RE_ENTER_PHASE (replaying code edits first if past 3b). If the gate still fails: answer-attempts++, post a sharper question. When quoting the prior answer, never echo the literal token — render it as @<keyword> (H4). Set blocked-needs-input again; update comment-cursor. After the $MAX_ATTEMPTS-th failed cycle, downgrade to blocked-hard and recommend DevAgent (M4 — prevents infinite human↔bot ping-pong). Crashes/transient resumes never count against this cap.

Replay code edits from work-plan.json

Code edits are never committed (decision #3), so on any resume the working tree has none of them. Before re-entering any phase after Phase 3b (C2), deterministically re-apply them from work-plan.json onto the clean committed base:

For each item in work-plan.code[] (which carries filled match-context / replacement from run 1): Edit(file=<path>, old_string=<match-context>, new_string=<replacement>). Same base + unique match-context → identical diff.

Authoring is the opposite — JCR writes persist in AEM, so resume skips Phase 3a and reconciles via the committed authoring-diff.json (the Phase 3a before-check treats actual == target-after as already-applied → skip). The two halves of the resume contract are asymmetric: code replays, authoring reconciles.

Re-entering Phase 3b itself does NOT need a separate replay — 3b re-fills and re-applies the work-plan as part of its normal flow.

Phase 1: Fetch + extract change details

  1. Fetch the work item:

    mcp__ado__wit_get_work_item with id=$TICKET_ID
    

    Write the description + comments to $SPEC_DIR/raw-story.md with provenance frontmatter. Record the resolved USER_INPUT (the post-trigger instruction, see User input above) at the top of raw-story.md under a ## User input heading so the change request and the report both preserve exactly what the human asked for.

  2. Parse the simple block (recommended but not required):

    bash $CLAUDE_PLUGIN_ROOT/skills/dx-simple/scripts/parse-simple-block.sh \
         "$SPEC_DIR/raw-story.md" "$SPEC_DIR/simple-block.yaml"
    
    • Exit 0 (parsed) → continue to step 2a (fill in inferred fields if any are missing).
    • Exit 2 (no block) → fall through to step 2b (LLM extraction from story prose).
    • Exit 3 (malformed: missing page-url, duplicate field, or unclosed fence) → post the specific error from stderr as an ADO comment. STOP.

2a. Fill in missing fields (block was present but partial): Read simple-block.yaml. For each missing optional field, infer it from the surrounding story text and overwrite simple-block.yaml: - element missing → infer from the story's element references. Examples: "Language Selector button" → dialog-title="Language Selector"; "Get started heading" → heading-text="Get started". If the story gives a JCR path, use jcr-path=.... - what missing → use the story's natural-language description of the change in plain English (this is the input the model uses to decide whether the change is content, code, or both). The classifier (Phase 2) uses these as hints; nothing here gates execution.

2b. LLM extraction (no block found): read raw-story.md and extract the same fields directly from the story description, acceptance criteria, and comments: - page-url (REQUIRED): find the QA author URL in the story. If multiple, prefer the one nearest the change description; if still ambiguous, post the template at $CLAUDE_PLUGIN_ROOT/skills/dx-simple/templates/simple-block.md.tmpl as an ADO comment and STOP. - element: derive from the story's element references (visible text, dialog title, JCR path). - what: take the most specific change description in the story. Keep it as natural language — the model decides downstream whether each part is a content edit or a code edit. Write the inferred values to $SPEC_DIR/simple-block.yaml with a # inferred: true comment at the top so downstream phases can flag lower confidence in the report.

2d. Fold in USER_INPUT (the text the human typed after the trigger — see User input above). If non-empty, it is the primary change request: merge it into what in simple-block.yaml, overriding the story body on conflict (a human typing @<keyword> make it blue means make it blue). The classifier (Phase 2) and edit phases then act on USER_INPUT plus the story context. If USER_INPUT is empty (bare trigger), only the story body drives the change (today's behavior).

  1. Semantic validation (Safeguard #1):

    • Read simple-block.yaml; extract page-url, element, what.
    • QA Basic Auth (first navigation only): follow the qa-basic-auth rule. Resolve credentials in priority order:
      1. Env vars: QA_BASIC_AUTH_USER / QA_BASIC_AUTH_PASS
      2. .ai/config.yaml keys aem.qa-basic-auth.username / aem.qa-basic-auth.password If credentials are found AND page-url is not a localhost URL, embed them in the URL for this navigation: https://<user>:<pass>@<host>/path. Subsequent navigations (Phase 5) use the clean URL — the session cookie persists. Never log or echo the URL with embedded credentials. If no credentials are found and the URL is non-localhost: post ADO comment explaining that QA_BASIC_AUTH_USER / QA_BASIC_AUTH_PASS env vars are not set (see qa-basic-auth rule) and exit non-zero.
    • Navigate Chrome to the page URL (credential-embedded for first QA visit):
      mcp__plugin_dx-aem_playwright__browser_navigate with url=<page-url>
      
    • Take a snapshot:
      mcp__plugin_dx-aem_playwright__browser_snapshot
      
    • Match the locator. The locator is one of:
      • heading-text="..." / button-text="..." / link-text="..." → look for elements with that exact visible text
      • jcr-path=... → use AEM MCP getNodeContent to confirm node exists; locator match count = 1 if node exists, 0 if not
      • dialog-title="..." → ambiguous on its own; require AEM MCP scanPageComponents to resolve to a unique component instance
      • free-form description (from LLM extraction) → use the Chrome snapshot + scanPageComponents to find the single best match; if more than one element matches, ABORT G1 with the ambiguous-locator message
  2. Take BEFORE screenshot (Safeguard #6): capture, then move into the spec dir as before.png:

    mcp__plugin_dx-aem_playwright__browser_take_screenshot with filename=before.png
    cp .ai/playwright/screenshots/before.png $SPEC_DIR/before.png
    

    (browser_take_screenshot takes a filename basename and saves under the MCP --output-dir, .ai/playwright/screenshots/ — it does not honor an arbitrary path.) Also record the locator's bounding box (from snapshot) to $SPEC_DIR/locator-bbox.json for the visual-diff step.

  3. G1 — Locator match (HARD GATE):

    • If 0 matches: post ADO comment "Component not found on page. Verified by: . Check page-url + the element you wanted to change." Exit non-zero. NO file reads beyond this point.
    • If >1 matches: post "Ambiguous locator: matches. Add a jcr-path=... to the simple block, or describe the element more specifically." Exit non-zero.
    • If exactly 1: continue. Record G1 = 1 match in confidence.json.
  4. Update progress + checkpoint (commit + push recovery state to the branch):

    bash $CLAUDE_PLUGIN_ROOT/skills/dx-simple/scripts/update-progress.sh \
         "$SPEC_DIR" "Phase 1: Extract + locate" "done" "G1 passed"
    bash $CLAUDE_PLUGIN_ROOT/skills/dx-simple/scripts/save-state.sh "$SPEC_DIR" "Phase 1"
    

    If save-state.sh exits 3 (BRANCH-ADVANCED), STOP — a concurrent resume is in flight (H2); do not force-push.

Phase 0.5: Repo identity guard

Runs once, right after parse-simple-block.sh writes simple-block.yaml, before G1. Confirms this repo is a legitimate target for the ticket and records authoring ownership.

GUARD_OUT=$(bash $CLAUDE_PLUGIN_ROOT/skills/dx-simple/scripts/repo-guard.sh \
  "$SPEC_DIR/simple-block.yaml" ".ai/config.yaml") || GUARD_RC=$?
eval "$GUARD_OUT"   # sets DECISION, REASON?, AUTHORING_OWNER?
  • If DECISION=abort (exit 3): set blocker class wrong-target and follow the ABORT path — this is a hard/terminal blocker (no re-ask loop: the human must re-trigger in the correct repo). Post REASON verbatim as the classified ADO comment. No JCR writes, no code edits. Verdict: fail.
  • If DECISION=proceed: continue to G1. Persist AUTHORING_OWNER into resume-state.json (authoring-owner key) — Phase 3a reads it.
  • Single-repo / unconfigured projects: project.platform is unset and the block has no platform, so the guard proceeds with AUTHORING_OWNER=true (today's behavior).

Phase 2: Classify (parallel subagents)

Dispatch THREE subagents in a single message (parallel):

  1. Page resolver (reuse aem-page-finder, model: haiku):

    Agent(subagent_type: aem-page-finder, prompt: "Resolve page-url <page-url> to its JCR content path. Confirm the page exists on QA. Return JSON: {\"jcr-path\": \"/content/...\", \"language-master\": \"<path or null>\"}.")
    
  2. Dialog inspector (reuse aem-inspector, model: sonnet):

    Agent(subagent_type: aem-inspector, prompt: "For component at JCR path <jcr-path>, read sling:resourceType, then fetch /apps/<resource-type>/cq:dialog. Walk the dialog fields and return JSON:
    {
      \"jcr-path\": \"<jcr>\",
      \"resource-type\": \"<rtype>\",
      \"fields\": { \"<name>\": \"<type>\", ... },
      \"values\": { \"<name>\": \"<current value>\", ... }
    }
    Return only the JSON, no prose.")
    
  3. File resolver (reuse aem-file-resolver, model: haiku):

    Agent(subagent_type: aem-file-resolver, prompt: "Resolve source files for resource type <resource-type>. Return JSON: {\"files\": [{\"path\": \"...\"}, ...]}.")
    

Synthesize the three returns into $SPEC_DIR/dialog-map.json and $SPEC_DIR/file-list.json. Then build a baseline work-plan with the deterministic heuristic:

bash $CLAUDE_PLUGIN_ROOT/skills/dx-simple/scripts/classify-work.sh \
     "$SPEC_DIR/simple-block.yaml" \
     "$SPEC_DIR/dialog-map.json" \
     "$SPEC_DIR/file-list.json" \
     "$SPEC_DIR/work-plan.json"

classify-work.sh only suggests obvious matches based on keywords in what against dialog field names. You — the orchestrator — are responsible for the final classification. Read work-plan.json, read what and raw-story.md, and decide what to do:

  • The change is a content edit that matches a dialog field → keep / add an item in .authoring[].
  • The change involves hardcoded strings, JS behavior (focus traps, keyboard handlers, click handlers), HTL templates, or CSS classes → keep / add items in .code[].
  • The change requires both (e.g. "rename the heading and trap focus in the modal") → populate both arrays. Phase 3a and Phase 3b will both run.
  • The locator pointed at a component whose dialog has no matching field, but the what clearly describes a content change → that string is hardcoded; route to .code[] and let the file-resolver candidates in there carry it.

If the deterministic baseline missed items, append them to work-plan.json via Write. If the baseline included items the model rejects on inspection, remove them. Once work-plan.json reflects the model's final plan, set confidence."G3-classification" to one of:

  • high — at least one path (authoring or code) has an unambiguous target
  • medium — at least one path has a target, but with ambiguity worth noting in the report
  • low — neither path has a workable target → abort with G3 fail

Update progress + checkpoint:

bash $CLAUDE_PLUGIN_ROOT/skills/dx-simple/scripts/update-progress.sh \
     "$SPEC_DIR" "Phase 2: Classify" "done" "G3=<level>, authoring=<n>, code=<n>"
bash $CLAUDE_PLUGIN_ROOT/skills/dx-simple/scripts/save-state.sh "$SPEC_DIR" "Phase 2"

Phase 3a: Apply authoring writes

Runs whenever work-plan.authoring[] is non-empty. If work-plan.code[] is also non-empty, Phase 3b runs immediately after Phase 3a (sequential, not parallel — Phase 3a must record the JCR before-state in main context for rollback before Phase 3b touches anything).

Authoring-owner gate (multi-repo). First read authoring-owner from resume-state.json (set by Phase 0.5; defaults to true). If it is false, skip all authoring items — another repo owns authoring for this ticket on this AEM instance. Record in report.md: Authoring skipped — owned by the AEM-author-capable repo for this platform. Then:

  • if work-plan.code[] is also non-empty → fall through to Phase 3b (the edge Phase 3a -> G4 [if code items also queued]);
  • if there are no code items → this repo has nothing to apply → go to Phase 5 via the existing [if no code items] edge. With nothing written, visual verify/report simply confirms no change in this repo → write report, verdict: success (no-op).

Read-before-write idempotency (always — even when this repo IS the owner). Before each JCR write, getNodeContent the target property; if its current value already equals the target (item.after), skip the write and log unchanged in authoring-diff.json. This keeps recovery re-runs (#141) and any parallel same-instance run side-effect-free. (The per-item drift check below applies the same read; the gate here is the general rule it implements.)

For each item in work-plan.authoring[]:

  1. Read current value:

    mcp__plugin_dx-aem_AEM__getNodeContent with path=<jcr-path>
    

    Confirm item.before matches the actual current value. Idempotent re-entry (H1) / read-before-write: if actual == item.after, the target value is already in place — either a prior run that crashed mid-Phase-3a applied it, or a parallel same-instance run did → skip the write, log unchanged in authoring-diff.json, and record it as applied. Only abort "value drifted: expected <before>, got <actual>" when actual is neither before nor after.

  2. Apply the write:

    mcp__plugin_dx-aem_AEM__updateComponent with path=<jcr-path>, properties={<property>: <after>}
    
  3. Append to $SPEC_DIR/authoring-diff.json:

    { "writes": [{ "jcr-path": "...", "property": "...", "before": "...", "after": "...", "applied": true, "applied-at": "<ISO>" }] }
    
  4. Log to audit:

    AUDIT_LOG_PREFIX=simple source .ai/lib/audit.sh
    _audit_append '{"ts":"<ISO>","action":"updateComponent","path":"<jcr>","property":"<prop>","ticket":"<id>"}'
    
  5. Checkpoint per JCR write (H1). AEM writes are external side-effects not transactional with the git commit. Commit authoring-diff.json after each write, not once at end of phase — otherwise a crash after write 2 of 3 leaves the writes live on AEM but the diff uncommitted, and resume's before-check (now idempotent, step 1) would have no record of them:

    bash $CLAUDE_PLUGIN_ROOT/skills/dx-simple/scripts/save-state.sh "$SPEC_DIR" "Phase 3a"
    

If ANY write fails partway through:

  • Mark all subsequent items as applied: false
  • Classify the blocker and follow the ABORT path (authoring is kept on a recoverable block, rolled back only on terminal failure — C3)
  • Post ADO comment, exit non-zero

Update progress.

Phase 3b: Apply code edits

Runs whenever work-plan.code[] is non-empty — independently of whether Phase 3a ran. The code path now covers any change that touches source files: hardcoded strings, HTL templates, CSS classes, focus traps and other JS behavior, click/keyboard handlers, etc. The model chose this path in Phase 2 based on the what text and dialog map.

classify-work.sh populates .code[] with placeholder items (confidence=0, empty contexts) for each candidate file. The agent MUST fill them in (or remove them) and rewrite work-plan.json to disk BEFORE the G4 gate runs. For changes that add new code (focus traps, new event listeners) rather than replace an existing line, set match-context to the anchor line you're inserting after, and replacement to the anchor line followed by the new code.

For each item in work-plan.code[]:

  1. Read the file:

    Read(file=<path>)
    
  2. Identify the exact match line. Fill in match-line, match-context, replacement, rationale, and confidence (LLM self-rated, 0.0–1.0):

    • High confidence (≥0.85): one unambiguous match in the file for the locator's context. Rationale example: "only color: red occurrence inside .hero-cta selector".
    • Below 0.85: multiple matches with unclear precedence, or no clear anchor. Do NOT guess.
  3. Persist the updated work-plan to disk (the deterministic G4 check reads this file):

    Write(file=$SPEC_DIR/work-plan.json, content=<work-plan JSON with all .code[] items populated>)
    
  4. G4 — Per-file edit confidence (HARD GATE):

    bash $CLAUDE_PLUGIN_ROOT/skills/dx-simple/scripts/check-g4.sh "$SPEC_DIR/work-plan.json" 0.85
    

    Exit 4 → rollback authoring + exit non-zero. Record G4 status in confidence.json.

  5. Apply the Edits (only after G4 passes):

    Edit(file=<path>, old_string=<match-context>, new_string=<replacement>)
    
  6. After all code items applied: run scope-check:

    bash $CLAUDE_PLUGIN_ROOT/skills/dx-simple/scripts/scope-check.sh "$SPEC_DIR/work-plan.json"
    

    Exit 4 → ABORT (scope exceeded is hard).

Update progress + checkpoint (commits the filled work-plan.json so a later resume can replay the code edits — C2; the edits themselves stay uncommitted):

bash $CLAUDE_PLUGIN_ROOT/skills/dx-simple/scripts/save-state.sh "$SPEC_DIR" "Phase 3b"

Phase 4: Compile (≤3 retries)

Only if Phase 3b ran (code edits were applied). Skip if authoring-only.

Read build command from .ai/config.yaml:

  • Prefer dx-simple.build-compile-fast if set
  • Else build.compile-fast
  • Else build.compile
  • Else abort: "no compile command configured"
COMPILE=$(bash .ai/lib/dx-common.sh yaml-val 'dx-simple.build-compile-fast' || \
          bash .ai/lib/dx-common.sh yaml-val 'build.compile-fast' || \
          bash .ai/lib/dx-common.sh yaml-val 'build.compile')

Run the compile, capturing output to $SPEC_DIR/compile.log. Loop up to 3 attempts:

for ATTEMPT in 1 2 3; do
  $COMPILE > "$SPEC_DIR/compile.log" 2>&1 && break
  if [[ "$ATTEMPT" -eq 3 ]]; then
    # Rollback + exit
    bash $CLAUDE_PLUGIN_ROOT/skills/dx-simple/scripts/rollback-authoring.sh "$SPEC_DIR/authoring-diff.json"
    exit 1
  fi
  # Read the last 50 lines of compile.log, identify the error, edit the file, retry.
done

The agent's job between attempts: read compile.log tail, identify which file/line caused the error, Edit it, then re-loop.

Update progress with attempt count.

Phase 5: Visual verify

The classifier produces EITHER authoring OR code items, never both — classify-work.sh routes by G3 confidence. Visual-verify rules differ by path:

Authoring path (Phase 3a ran): mandatory if activate: true in the simple block (writes are visible on author immediately). Skip only when activate: false AND no rendered preview is expected.

Code path (Phase 3b ran): visual verify is skipped in pipeline mode because mvn compile (the only build allowed by block-mvn-deploy.sh) does NOT deploy to AEM. The QA author serves the old bundle, so before.png and after.png would be identical and G6 (target changed ≥5%) would always fail. The change is verified post-merge by the normal CI deploy + QA cycle.

  • To request visual verify for a code-path run anyway, add force-visual-verify: true to the simple block. The pipeline will navigate and screenshot, but G5/G6 are downgraded to WARN (recorded in report.md, do not abort). Useful only when an external job pre-deploys the branch to QA before SimpleAgent runs.
  • Local dev (non-pipeline) is free to run visual verify when the developer has deployed locally — the same force-visual-verify: true flag opts in.

When skipped: code-only non-visual changes (aria-label edits, copy in HTL, etc.) — recorded as "verify-deferred-to-qa" in the report.

If running:

  1. Navigate Chrome to the page (note: same QA author URL, NOT publish, so authoring writes are visible):

    mcp__plugin_dx-aem_playwright__browser_navigate with url=<page-url>
    
  2. Take AFTER screenshot:

    mcp__plugin_dx-aem_playwright__browser_take_screenshot with filename=after.png
    cp .ai/playwright/screenshots/after.png $SPEC_DIR/after.png
    
  3. Run visual-diff:

    BBOX=$(cat $SPEC_DIR/locator-bbox.json | jq -r '"\(.x),\(.y),\(.w),\(.h)"')
    bash $CLAUDE_PLUGIN_ROOT/skills/dx-simple/scripts/visual-diff.sh \
         "$SPEC_DIR/before.png" "$SPEC_DIR/after.png" "$BBOX" \
         > "$SPEC_DIR/visual-diff.json"
    
  4. G5 — Visual non-target identical: non-target-identical ≥ 99%

  5. G6 — Visual target changed: region-diff ≥ 5%

Either gate fail:

  • Authoring path → rollback authoring + exit non-zero.
  • Code path with force-visual-verify: true AND re-edit budget remaining → invoke ONE re-edit cycle: open the staged file, show the agent the screenshot diff, ask "this change didn't render as expected — re-edit." Then loop back to Phase 4 (recompile).
  • Code path WITHOUT force-visual-verify → unreachable (Phase 5 skipped — see selection rules above).

Phase 5.5: Diff review (only if code path)

If Phase 3b ran (code edits applied), invoke dx-pr-reviewer for a single-pass review on the working tree:

Agent(subagent_type: dx-pr-reviewer, prompt: "Review the staged diff (git diff HEAD) for the following changes. Context: this is a small ≤50-line tweak via /dx-simple on component <resource-type> — change described as <what>. Focus on:
- Does the change actually accomplish what the requirement says?
- Any obvious bug (wrong variable, missing semicolon, off-by-one)?
- Any accessibility regression (e.g., removing existing aria text)?
Return findings as JSON: [{ \"severity\": \"blocker|suggestion\", \"confidence\": 0.0-1.0, \"file\": \"...\", \"line\": N, \"comment\": \"...\" }]
")

Write the review to $SPEC_DIR/diff-review.md.

G7 — Zero blockers at ≥80% confidence:

  • Any severity: blocker AND confidence ≥ 0.8 → rollback authoring + exit non-zero, no PR.
  • Suggestions are recorded in PR description as "Reviewer notes" but do not block.

Phase 6: Activate + Commit + PR

  1. Activate authoring (only if work-plan has authoring items AND simple-block.yaml activate: true AND all gates passed): For each unique JCR path in authoring-diff.json:

    mcp__plugin_dx-aem_AEM__activatePage with path=<jcr-path>
    

    Log each activation to audit. Update authoring-diff.json to set activated: true per item.

  2. Commit + PR (only if Phase 3b ran):

    Idempotent (M5). PR creation is irreversible, so:

    • Checkpoint last-completed = Phase 6 BEFORE the create so a crash during create resumes back into Phase 6 (not past it):
      bash $CLAUDE_PLUGIN_ROOT/skills/dx-simple/scripts/save-state.sh "$SPEC_DIR" "Phase 6"
      
    • Detect an existing open PR for this branch first (mcp__ado__repo_list_pull_requests_by_repo_or_project with sourceRefName: refs/heads/<branch>, status: Active). If one exists, switch /dx-pr-commit to update-mode (push new commits + update the description) instead of creating a second PR — a crash between PR-create and the post-create checkpoint must not duplicate the PR.

    Delegate to /dx-pr-commit:

    Skill(/dx-pr-commit)
    

    The conventional commit message: feat(<scope>): <what summary>. Include in the PR body:

    • Link to report.md
    • Reviewer notes from Phase 5.5 (if any)
    • Authoring changes summary (if any, with "activated: yes" flag)
    • Before/after screenshots

Phase 7: Write report + ADO comment

Render $CLAUDE_PLUGIN_ROOT/skills/dx-simple/templates/report.md.tmpl into $SPEC_DIR/report.md, substituting all {PLACEHOLDER} tokens from confidence.json, work-plan.json, authoring-diff.json. Also substitute {PLUGIN_ROOT} with the literal value of $CLAUDE_PLUGIN_ROOT (so the printed revert command is copy-pasteable) and {SPEC_DIR} with the absolute spec directory.

Post a truncated version to ADO:

mcp__ado__wit_add_work_item_comment with id=<ticket>, comment=<truncated report>

After the comment posts successfully, record it so the pipeline's fallback step does not post a duplicate failure note:

mkdir -p .ai/run-context && touch .ai/run-context/ado-comment-posted.flag

Update final progress row to done, mark recovery state terminal, and checkpoint so a stray re-trigger short-circuits to the done no-op:

jq '.status = "done"' "$SPEC_DIR/resume-state.json" > "$SPEC_DIR/.rs.tmp" && mv "$SPEC_DIR/.rs.tmp" "$SPEC_DIR/resume-state.json"
bash $CLAUDE_PLUGIN_ROOT/skills/dx-simple/scripts/save-state.sh "$SPEC_DIR" "Phase 7"

Clean up (leave ado-comment-posted.flag in place — the pipeline inspects it):

rm -f .ai/run-context/orchestrating.flag

Return contract

When this skill is invoked from an orchestrator (future composition), emit at the end:

## Return
verdict: pass | warn | fail
summary: <one sentence>
artifacts:
  - $SPEC_DIR/report.md
  - $SPEC_DIR/work-plan.json
  - $SPEC_DIR/authoring-diff.json
next_action: <human-readable next step or "none">

If running standalone (no orchestrating.flag), also print a human summary above the Return block.

ABORT path (any gate failure)

Order matters. Classify first, then roll back authoring only if terminal (C3), then persist state before discarding the source edits (today's git checkout -- . wipes the uncommitted report + state — that is the bug this fixes).

  1. Classify the blocker → set blocker.class (needs-user-input | transient | hard), blocker.recoverable, blocker.reason, blocker.needs, blocked-at-phase, and status in resume-state.json (via jq). Use the Blocker taxonomy below.

    Re-ask loop (if this is a resumed needs-input run whose gate failed again): if answer-attempts < $MAX_ATTEMPTS, bump answer-attempts, set status=blocked-needs-input, and post a sharper question (loop-safe — see comment format; never echo the literal token). Otherwise downgrade to status=blocked-hard and recommend DevAgent (M4).

  2. Conditional authoring rollback (C3). Roll back JCR writes only when the blocker is terminal (hard, or final fail):

    # terminal only:
    bash $CLAUDE_PLUGIN_ROOT/skills/dx-simple/scripts/rollback-authoring.sh "$SPEC_DIR/authoring-diff.json"
    

    On a recoverable block (needs-input / transient), leave the authoring writes in place — they are recorded in the committed authoring-diff.json, and the resume run skips Phase 3a. (Rolling back here would lose the authoring half of a combined authoring+code change, since resume re-enters at Phase 3b, past 3a.)

  3. Write the failure report.md (same template; verified: false, failing gate row marked fail, Recovery section populated — never the literal token, use @<keyword>).

  4. Persist state — commit + push (the report + resume-state + authoring-diff):

    bash $CLAUDE_PLUGIN_ROOT/skills/dx-simple/scripts/save-state.sh "$SPEC_DIR" "<blocked-phase>"
    

    (Exit 3 = BRANCH-ADVANCED → a concurrent resume is in flight; stop.)

  5. Then discard the now-irrelevant source edits — spec files are committed, so they survive; code edits are replayed from work-plan.json on resume (C2):

    git checkout -- .
    
  6. Post the classified ADO comment (loop-safe format below) + record the flag so the pipeline fallback does not double-post:

    mkdir -p .ai/run-context && touch .ai/run-context/ado-comment-posted.flag
    
  7. Clean up orchestrating flag (leave ado-comment-posted.flag for the pipeline).

  8. Exit non-zero.

Blocker taxonomy

class examples recovery
needs-user-input G1 ambiguous/no match, missing page-url, missing QA Basic Auth credentials, G3 low classification, authoring value drifted, ambiguous-branch (M1) human replies @<keyword> <answer> → resume at blocked phase
transient MAX_TURNS / step timeout / MCP unreachable / network blip — infra only re-trigger (any @<keyword> reply, or re-run) → resume forward from last-completed-phase (replaying code edits if past 3b)
hard scope-check exceeded (>5 files / >50 lines / >10 writes), G7 real review blocker, any compile failure surviving Phase 4's in-run 3× retry (M3), answer-attempts cap reached not recoverable here → recommend re-tag KAI-DEV-AUTOMATION (DevAgent)
wrong-target Phase 0.5 repo identity guard aborted — ticket's platform/brand/scope does not match this repo's identity terminal — not recoverable here → human must re-trigger in the correct repo

Compile is hard, not transient (M3). Phase 4 already retries compile 3× against a deterministic edit; a cross-run re-trigger replays the identical edit, so it won't compile any better. transient is reserved for infra (MCP/network) and MAX_TURNS, which a genuinely fresh run can fix.

Resume jump table

blocked at needs from human re-enters at reuses / notes
G1 (0 or >1 locator matches) jcr-path / exact visible text Phase 1 (re-locate)
ambiguous-branch (M1) which branch to resume Phase 0 discovery
G3 (classification low) content vs code Phase 2 persisted dialog-map.json, file-list.json
G4 (edit confidence <0.85) file / line / anchor Phase 3b (re-fill + re-apply) work-plan.json. If 3a authoring was applied: NOT rolled back (C3); reconcile via committed authoring-diff.json.
compile fails (survives 3× retry) hard → DevAgent (M3)
G7 (review blocker) guidance, or hard Phase 3b re-edit (replay code edits first — C2) work-plan.json
crashed after 3b (in-progress) nothing (transient) last-completed-phase + 1 committed artifacts + replay code edits (C2)
crashed at/before 3b (in-progress) nothing (transient) last-completed-phase + 1 committed artifacts (no code edits exist yet)
crashed after Phase 6 PR-create (in-progress) nothing (transient) Phase 6 update-mode, not a second create (M5)

Blocker comment format (human + machine, loop-safe)

The literal trigger token never appears in the bot's own text (loop safety) — the bot says the keyword bare and instructs the user to prefix it with @. This holds even when quoting the human's prior answer in a re-ask (H4).

⚠️ /dx-simple paused on #<id> — needs your input

**What failed:** G1 locator — "Language Selector" matched 3 elements on the page.
**Recoverable:** yes (answer-attempt 1 of <max-attempts>)
**What I need:** reply on this ticket, beginning your comment with the kai-simple
   keyword (prefixed with @ so it re-triggers me), followed by a precise target, e.g.
   @<keyword> jcr-path=/content/site/.../languagenavigation

<!-- dx-simple:blocked phase=G1 reason=ambiguous-locator attempt=1 comment-cursor=<id> -->

The HTML marker lets the next run parse phase / attempt / comment-cursor deterministically without re-reading its own prose. Render the keyword in examples as @<keyword> (placeholder) so the example line cannot self-trigger the hook.

Examples

  1. /dx-simple 9999999 — runs the full pipeline against ticket 9999999. Pauses only on errors.

  2. /dx-simple https://dev.azure.com/org/proj/_workitems/edit/9999999 — same, but parses the ID from the URL.

Troubleshooting

  • "Component not found on page" — Locator did not match anything in Chrome snapshot. Check page-url (loads on QA?), element (matches visible element?), QA content sync (component exists on QA?). If the QA site requires HTTP Basic Auth, ensure QA_BASIC_AUTH_USER / QA_BASIC_AUTH_PASS env vars are set (see qa-basic-auth rule) — the skill embeds them in the first navigation URL automatically.

  • "Ambiguous locator" — Multiple DOM matches. Use jcr-path=... form for unambiguous targeting, or describe the element more specifically (e.g. add the surrounding section name).

  • "Edit confidence too low (G4)" — Agent couldn't identify which file/line to edit unambiguously. Either the source isn't deterministic from the locator, or the change is too ambiguous for /dx-simple. Re-tag as KAI-DEV-AUTOMATION to use full DevAgent.

  • "Visual verify failed (G5 or G6)" — Either the change caused side-effects (G5 fail: page-wide change) or no visible change rendered (G6 fail: edit was a no-op). Check the visual-diff.json + the before/after screenshots in spec dir.

Rules

  • Coordinator only — read state, dispatch subagents, never directly read codebase or write code outside the well-defined phases.
  • Strict gate enforcement — never proceed past a failing gate; never "best effort" guess.
  • Audit every AEM writeaudit.sh with AUDIT_LOG_PREFIX=simple.
  • Don't echo file content — reference paths instead.
Install via CLI
npx skills add https://github.com/easingthemes/dx-aem-flow --skill dx-simple
Repository Details
star Stars 6
call_split Forks 3
navigation Branch main
article Path SKILL.md
More from Creator
easingthemes
easingthemes Explore all skills →