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: "
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:
- Parse the ADO story's
simpleblock (Phase 1). - Dispatch parallel research subagents (Phase 2).
- Apply authoring writes via AEM MCP (Phase 3a) — this is in main context because audit log + rollback record require it.
- 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:
- Local session — if
INLINE_INPUT(text after the id in$ARGUMENTS) is non-empty →USER_INPUT="$INLINE_INPUT",TRIGGER_COMMENT_ID="". - Pipeline run (fired by an ADO comment) — else fetch comments and select the
triggering one:
Pick the comment that is (a) authored by a non-bot identity, (b) containsmcp__ado__wit_list_work_item_comments with workItemId=$TICKET_ID$TRIGGER_TOKEN, (c) has the highest ADO comment id (ids are monotonic). Strip the token →USER_INPUT; record its id asTRIGGER_COMMENT_ID. USER_INPUTmay 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.mdsays "never reuse old feature/bugfix branches for new work."/dx-simpleresume 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.
- Is there a fresh follow-up? Reopen only if
USER_INPUTis non-empty andTRIGGER_COMMENT_ID > COMMENT_CURSOR(numeric; treat an empty cursor as0).- 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.
- Otherwise → genuine no-op (stray re-trigger / service-hook replay / bare
token / already-consumed comment). Post once: "Already completed in PR. Reply
- 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. - Apply the follow-up as a delta. Read
work-plan.json+USER_INPUTand update the work-plan to satisfy the follow-up (e.g. change areplacementto call the existing util; add or adjust acode[]item). AppendUSER_INPUTtosimple-block.yaml'swhat(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_INPUTas the primary request).
- 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
- 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.
(Exit 3 =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"BRANCH-ADVANCED→ a concurrent resume is in flight; stop.) - Re-enter the edit phase exactly as a
resume-forwardwould: withlast-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 touchanswer-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.
- 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 storedcomment-cursor. If none qualifies → cheap exit (stray re-trigger; nothing to do). - Strip the token; the remainder is
continue-input. Apply it toblocked-at-phase(e.g. setsimple-block.yamlelementfor a G1 ambiguity; add a file/line/anchor hint for G4; pick the branch forambiguous-branch). - Commit the updated
comment-cursoratomically with / before re-entering the phase (L2) — viajqintoresume-state.jsonthensave-state.sh— otherwise a crash after applying the answer reprocesses the same comment. - 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). Setblocked-needs-inputagain; updatecomment-cursor. After the$MAX_ATTEMPTS-th failed cycle, downgrade toblocked-hardand 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
Fetch the work item:
mcp__ado__wit_get_work_item with id=$TICKET_IDWrite the description + comments to
$SPEC_DIR/raw-story.mdwith provenance frontmatter. Record the resolvedUSER_INPUT(the post-trigger instruction, see User input above) at the top ofraw-story.mdunder a## User inputheading so the change request and the report both preserve exactly what the human asked for.Parse the
simpleblock (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).
Semantic validation (Safeguard #1):
- Read
simple-block.yaml; extractpage-url,element,what. - QA Basic Auth (first navigation only): follow the
qa-basic-authrule. Resolve credentials in priority order:- Env vars:
QA_BASIC_AUTH_USER/QA_BASIC_AUTH_PASS .ai/config.yamlkeysaem.qa-basic-auth.username/aem.qa-basic-auth.passwordIf credentials are found ANDpage-urlis 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 thatQA_BASIC_AUTH_USER/QA_BASIC_AUTH_PASSenv vars are not set (seeqa-basic-authrule) and exit non-zero.
- Env vars:
- 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 textjcr-path=...→ use AEM MCPgetNodeContentto confirm node exists; locator match count = 1 if node exists, 0 if notdialog-title="..."→ ambiguous on its own; require AEM MCPscanPageComponentsto 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
- Read
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_screenshottakes afilenamebasename 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.jsonfor the visual-diff step.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 thesimpleblock, or describe the element more specifically." Exit non-zero. - If exactly 1: continue. Record
G1 = 1 matchinconfidence.json.
- If 0 matches: post ADO comment "Component not found on page. Verified by:
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.shexits 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 classwrong-targetand follow the ABORT path — this is a hard/terminal blocker (no re-ask loop: the human must re-trigger in the correct repo). PostREASONverbatim as the classified ADO comment. No JCR writes, no code edits. Verdict: fail. - If
DECISION=proceed: continue to G1. PersistAUTHORING_OWNERintoresume-state.json(authoring-ownerkey) — Phase 3a reads it. - Single-repo / unconfigured projects:
project.platformis unset and the block has noplatform, so the guard proceeds withAUTHORING_OWNER=true(today's behavior).
Phase 2: Classify (parallel subagents)
Dispatch THREE subagents in a single message (parallel):
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>\"}.")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.")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 targetmedium— at least one path has a target, but with ambiguity worth noting in the reportlow— 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 edgePhase 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[]:
Read current value:
mcp__plugin_dx-aem_AEM__getNodeContent with path=<jcr-path>Confirm
item.beforematches the actual current value. Idempotent re-entry (H1) / read-before-write: ifactual == 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, logunchangedinauthoring-diff.json, and record it as applied. Only abort"value drifted: expected <before>, got <actual>"whenactualis neitherbeforenorafter.Apply the write:
mcp__plugin_dx-aem_AEM__updateComponent with path=<jcr-path>, properties={<property>: <after>}Append to
$SPEC_DIR/authoring-diff.json:{ "writes": [{ "jcr-path": "...", "property": "...", "before": "...", "after": "...", "applied": true, "applied-at": "<ISO>" }] }Log to audit:
AUDIT_LOG_PREFIX=simple source .ai/lib/audit.sh _audit_append '{"ts":"<ISO>","action":"updateComponent","path":"<jcr>","property":"<prop>","ticket":"<id>"}'Checkpoint per JCR write (H1). AEM writes are external side-effects not transactional with the git commit. Commit
authoring-diff.jsonafter 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[]:
Read the file:
Read(file=<path>)Identify the exact match line. Fill in
match-line,match-context,replacement,rationale, andconfidence(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: redoccurrence inside .hero-cta selector". - Below 0.85: multiple matches with unclear precedence, or no clear anchor. Do NOT guess.
- High confidence (≥0.85): one unambiguous match in the file for the locator's context. Rationale example: "only
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>)G4 — Per-file edit confidence (HARD GATE):
bash $CLAUDE_PLUGIN_ROOT/skills/dx-simple/scripts/check-g4.sh "$SPEC_DIR/work-plan.json" 0.85Exit 4 → rollback authoring + exit non-zero. Record
G4status inconfidence.json.Apply the Edits (only after G4 passes):
Edit(file=<path>, old_string=<match-context>, new_string=<replacement>)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-fastif 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: trueto 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: trueflag 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:
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>Take AFTER screenshot:
mcp__plugin_dx-aem_playwright__browser_take_screenshot with filename=after.png cp .ai/playwright/screenshots/after.png $SPEC_DIR/after.pngRun 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"G5 — Visual non-target identical:
non-target-identical ≥ 99%G6 — Visual target changed:
region-diff ≥ 5%
Either gate fail:
- Authoring path → rollback authoring + exit non-zero.
- Code path with
force-visual-verify: trueAND 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
Activate authoring (only if work-plan has authoring items AND
simple-block.yamlactivate: trueAND all gates passed): For each unique JCR path inauthoring-diff.json:mcp__plugin_dx-aem_AEM__activatePage with path=<jcr-path>Log each activation to audit. Update
authoring-diff.jsonto setactivated: trueper item.Commit + PR (only if Phase 3b ran):
Idempotent (M5). PR creation is irreversible, so:
- Checkpoint
last-completed = Phase 6BEFORE 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_projectwithsourceRefName: refs/heads/<branch>,status: Active). If one exists, switch/dx-pr-committo 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
- Checkpoint
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).
Classify the blocker → set
blocker.class(needs-user-input|transient|hard),blocker.recoverable,blocker.reason,blocker.needs,blocked-at-phase, andstatusinresume-state.json(viajq). Use the Blocker taxonomy below.Re-ask loop (if this is a resumed
needs-inputrun whose gate failed again): ifanswer-attempts < $MAX_ATTEMPTS, bumpanswer-attempts, setstatus=blocked-needs-input, and post a sharper question (loop-safe — see comment format; never echo the literal token). Otherwise downgrade tostatus=blocked-hardand recommend DevAgent (M4).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 committedauthoring-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.)Write the failure
report.md(same template;verified: false, failing gate row markedfail, Recovery section populated — never the literal token, use@<keyword>).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.)Then discard the now-irrelevant source edits — spec files are committed, so they survive; code edits are replayed from
work-plan.jsonon resume (C2):git checkout -- .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.flagClean up orchestrating flag (leave
ado-comment-posted.flagfor the pipeline).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, nottransient(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.transientis 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
/dx-simple 9999999— runs the full pipeline against ticket 9999999. Pauses only on errors./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, ensureQA_BASIC_AUTH_USER/QA_BASIC_AUTH_PASSenv vars are set (seeqa-basic-authrule) — 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-AUTOMATIONto 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 write —
audit.shwithAUDIT_LOG_PREFIX=simple. - Don't echo file content — reference paths instead.