claude-dev-env 1.66.1 → 1.67.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-dev-env",
3
- "version": "1.66.1",
3
+ "version": "1.67.0",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {
@@ -9,12 +9,12 @@ Create a source-grounded plan packet through the Claude Code Workflow runtime. T
9
9
 
10
10
  ## Launch
11
11
 
12
- Call the workflow with the user request and current working directory:
12
+ Call the workflow with the user request and current working directory. The payload goes in `args` — the Workflow tool exposes `args` to the script as its global `args`, and substitutes the user's full request for `$ARGUMENTS`:
13
13
 
14
14
  ```js
15
15
  Workflow({
16
16
  scriptPath: "$HOME/.claude/skills/anthropic-plan/workflow/plan-packet.mjs",
17
- input: {
17
+ args: {
18
18
  task: "$ARGUMENTS",
19
19
  cwd: "<current working directory>"
20
20
  }
@@ -23,6 +23,10 @@ Workflow({
23
23
 
24
24
  If the Workflow tool is unavailable, say `anthropic-plan requires the Workflow tool; aborting` and stop.
25
25
 
26
+ ## Self-healing writes
27
+
28
+ The workflow writes the packet into the live checkout under `docs/plans/<slug>/`. When a session isolates writes into a worktree and blocks a direct write, the workflow stages each packet file through the Write tool — so the plain-language and historical-clutter checks still run — then copies the staged tree into the checkout. The packet lands under `docs/plans/<slug>/` in either session mode.
29
+
26
30
  ## Workflow Contract
27
31
 
28
32
  The workflow handles the full planning loop:
@@ -34,8 +38,10 @@ The workflow handles the full planning loop:
34
38
  5. Run `scripts/validate_packet.py`.
35
39
  6. Spawn `plan-packet-validator` in fresh context.
36
40
  7. Repair packet findings up to the workflow cap.
37
- 8. Return packet path, validation state, and findings.
38
- 9. Stop before implementation.
41
+ 8. Run the reuse audit: search the codebase for existing equivalents of each new file/symbol the packet introduces, write `validation/reuse-audit.md`, and gate approval on any unjustified reproduction.
42
+ 9. Build a single-file offline visual HTML of the finished packet from `templates/visual-plan.template.html` and write it beside the packet as `visual-plan.html`.
43
+ 10. Return packet path, validation state, and findings.
44
+ 11. Stop before implementation.
39
45
 
40
46
  ## Packet Shape
41
47
 
@@ -59,6 +65,12 @@ The deterministic validator checks required files, placeholders, `Open Questions
59
65
 
60
66
  The `plan-packet-validator` agent checks source accuracy, scope, enough implementation detail for a blind build agent, real TDD order, invented APIs or commands, and end-to-end acceptance criteria.
61
67
 
68
+ The reuse audit writes `validation/reuse-audit.md` with a per-item verdict for every new file or symbol the packet introduces and gates approval on any unjustified reproduction of existing behavior.
69
+
70
+ ## Visualize
71
+
72
+ After validation and before approval, the workflow builds a single-file offline visual HTML of the finished packet from `templates/visual-plan.template.html` and writes it beside the packet as `visual-plan.html`. The file inlines all CSS and JavaScript and references no external assets, so it opens offline. It renders the packet as diagrams and compact cards — a stat hero, scenario strips, is/isn't cards, edit-recipe step sequences for the file-by-file change, reuse-audit verdict badges, and a checklist — rather than reproducing the markdown. Every label is written for the reviewer: the diagram says what each step does in plain words and leaves out code symbols (function names, selectors, test names), while each touched file keeps its repo-relative path dimmed for the build agent. Because it is generated after validation, `visual-plan.html` is not a required packet file.
73
+
62
74
  ## Rules
63
75
 
64
76
  - Write docs only.
@@ -25,6 +25,7 @@ ALL_REQUIRED_RELATIVE_PATHS: tuple[str, ...] = (
25
25
  "validation/validator-report.md",
26
26
  "validation/deterministic-checks.md",
27
27
  "validation/unresolved-risks.md",
28
+ "validation/reuse-audit.md",
28
29
  "handoff/build-prompt.md",
29
30
  "handoff/review-prompt.md",
30
31
  "handoff/verification-commands.md",
@@ -76,6 +76,14 @@ def valid_markdown_for(relative_path: str) -> str:
76
76
  "Use only this packet. Read README.md, then context/source-map.md, then implementation/steps.md. "
77
77
  "Do not rely on prior chat history."
78
78
  )
79
+ if relative_path == "validation/reuse-audit.md":
80
+ return (
81
+ "# Reuse Audit\n\n"
82
+ "| Item | Kind | Verdict | Searched | Found | Decision | Evidence |\n"
83
+ "|---|---|---|---|---|---|---|\n"
84
+ "| authenticate_user | helper | reused | shared_utils/auth | existing public helper | call it directly | src/auth.py:12 |\n\n"
85
+ "Summary: reused 1.\n"
86
+ )
79
87
  return f"# {relative_path}\n\nGrounded implementation detail for this packet file.\n"
80
88
 
81
89
 
@@ -403,3 +411,45 @@ def test_bullet_list_steps_naming_test_contract_passes(tmp_path: Path) -> None:
403
411
  validator_run = run_validator(packet_directory)
404
412
 
405
413
  assert validator_run.returncode == 0, validator_run.stderr
414
+
415
+
416
+ def test_missing_reuse_audit_file_fails(tmp_path: Path) -> None:
417
+ packet_directory = tmp_path / "docs" / "plans" / "add-login"
418
+ write_valid_packet(packet_directory)
419
+ (packet_directory / "validation" / "reuse-audit.md").unlink()
420
+
421
+ validator_run = run_validator(packet_directory)
422
+
423
+ assert validator_run.returncode == 2
424
+ assert "missing required file: validation/reuse-audit.md" in validator_run.stderr
425
+
426
+
427
+ def test_reuse_audit_without_verdict_keyword_fails(tmp_path: Path) -> None:
428
+ packet_directory = tmp_path / "docs" / "plans" / "add-login"
429
+ write_valid_packet(packet_directory)
430
+ (packet_directory / "validation" / "reuse-audit.md").write_text(
431
+ "# Reuse Audit\n\nNo audit was performed for this packet.\n",
432
+ encoding="utf-8",
433
+ )
434
+
435
+ validator_run = run_validator(packet_directory)
436
+
437
+ assert validator_run.returncode == 2
438
+ assert "reuse-audit.md must record a reuse verdict for each new item" in validator_run.stderr
439
+
440
+
441
+ def test_reuse_audit_with_verdict_keyword_passes(tmp_path: Path) -> None:
442
+ packet_directory = tmp_path / "docs" / "plans" / "add-login"
443
+ write_valid_packet(packet_directory)
444
+ (packet_directory / "validation" / "reuse-audit.md").write_text(
445
+ "# Reuse Audit\n\n"
446
+ "| Item | Kind | Verdict | Searched | Found | Decision | Evidence |\n"
447
+ "|---|---|---|---|---|---|---|\n"
448
+ "| open_postgres_connection | helper | reused | shared_utils/theme_db | existing public helper | call it | shared_utils/theme_db/connection.py:20 |\n\n"
449
+ "Summary: reused 1.\n",
450
+ encoding="utf-8",
451
+ )
452
+
453
+ validator_run = run_validator(packet_directory)
454
+
455
+ assert validator_run.returncode == 0, validator_run.stderr
@@ -52,6 +52,7 @@ def validate_packet(packet_directory: Path) -> list[str]:
52
52
  all_errors.extend(packet_json_errors(packet_directory))
53
53
  all_errors.extend(source_map_errors(packet_directory))
54
54
  all_errors.extend(tdd_plan_errors(packet_directory))
55
+ all_errors.extend(reuse_audit_errors(packet_directory))
55
56
  all_errors.extend(implementation_step_errors(packet_directory))
56
57
  all_errors.extend(build_prompt_errors(packet_directory))
57
58
  return all_errors
@@ -185,6 +186,25 @@ def tdd_plan_errors(packet_directory: Path) -> list[str]:
185
186
  return []
186
187
 
187
188
 
189
+ def reuse_audit_errors(packet_directory: Path) -> list[str]:
190
+ """Return errors for a reuse audit that records no per-item verdict.
191
+
192
+ Args:
193
+ packet_directory: Directory that should contain validation/reuse-audit.md.
194
+
195
+ Returns:
196
+ An error string when the reuse audit names no reuse verdict, else an empty list.
197
+ """
198
+ reuse_audit_file = packet_directory / "validation" / "reuse-audit.md"
199
+ if not reuse_audit_file.is_file():
200
+ return []
201
+ reuse_audit_text = reuse_audit_file.read_text(encoding="utf-8").lower()
202
+ verdict_keywords = ("reused", "extract", "justified", "config-local", "reproduction")
203
+ if not any(each_keyword in reuse_audit_text for each_keyword in verdict_keywords):
204
+ return ["reuse-audit.md must record a reuse verdict for each new item"]
205
+ return []
206
+
207
+
188
208
  def implementation_step_errors(packet_directory: Path) -> list[str]:
189
209
  """Return errors for implementation steps without test coverage.
190
210
 
@@ -0,0 +1,7 @@
1
+ # Reuse Audit
2
+
3
+ | Item | Kind | Verdict | Searched | Found | Decision | Evidence |
4
+ |---|---|---|---|---|---|---|
5
+ | _send_failure_alert | helper | reused | shared_utils/alerts | existing public helper covers the alert send | call the existing helper | shared_utils/alerts/notify.py:48 |
6
+
7
+ Summary: reused 1, extract-to-shared 0, new-justified 0, config-local 0, unjustified-reproduction 0.
@@ -0,0 +1,572 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Visual Plan — Component Gallery Template</title>
7
+ <style>
8
+ :root{
9
+ --bg:#0a0e16; --panel:#121a28; --panel2:#0f1622; --line:#223049;
10
+ --ink:#e6edf7; --muted:#93a4bd; --faint:#5f7390;
11
+ --fail:#ef4444; --sent:#22c55e; --skip:#647892; --amber:#f59e0b;
12
+ --mod:#a78bfa; --volt:#4cc9f0; --volt2:#4361ee;
13
+ --shadow:0 10px 30px rgba(0,0,0,.45);
14
+ }
15
+ *{box-sizing:border-box}
16
+ html,body{margin:0;padding:0}
17
+ body{
18
+ background:
19
+ radial-gradient(1100px 520px at 80% -10%, rgba(67,97,238,.16), transparent 60%),
20
+ radial-gradient(900px 480px at 0% 0%, rgba(76,201,240,.12), transparent 55%),
21
+ var(--bg);
22
+ color:var(--ink);
23
+ font:15px/1.5 ui-sans-serif,system-ui,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
24
+ -webkit-font-smoothing:antialiased;
25
+ }
26
+ .wrap{max-width:1080px;margin:0 auto;padding:34px 22px 90px}
27
+ .mono{font-family:ui-monospace,"Cascadia Code","SF Mono",Consolas,monospace}
28
+
29
+ /* ---------- header ---------- */
30
+ .kicker{display:inline-flex;align-items:center;gap:8px;font-size:12px;letter-spacing:.14em;
31
+ text-transform:uppercase;color:var(--volt);font-weight:700}
32
+ .kicker .dot{width:8px;height:8px;border-radius:50%;background:var(--volt);
33
+ box-shadow:0 0 12px var(--volt)}
34
+ h1{font-size:clamp(26px,4vw,40px);line-height:1.08;margin:14px 0 8px;letter-spacing:-.5px}
35
+ .sub{color:var(--muted);max-width:680px;font-size:16px}
36
+ .sub b{color:var(--ink)}
37
+
38
+ /* ---------- hero stat ---------- */
39
+ .hero{display:grid;grid-template-columns:1fr auto 1fr;gap:18px;align-items:stretch;
40
+ margin:26px 0 8px}
41
+ .stat{background:linear-gradient(180deg,var(--panel),var(--panel2));border:1px solid var(--line);
42
+ border-radius:16px;padding:20px 22px;box-shadow:var(--shadow)}
43
+ .stat .lab{font-size:12px;letter-spacing:.1em;text-transform:uppercase;color:var(--muted);font-weight:700}
44
+ .stat .big{font-size:clamp(40px,7vw,68px);font-weight:800;line-height:1;margin-top:8px;letter-spacing:-2px}
45
+ .stat .cap{color:var(--faint);font-size:13px;margin-top:6px}
46
+ .stat.bad{border-color:rgba(239,68,68,.4)}
47
+ .stat.bad .big{color:var(--fail)}
48
+ .stat.good{border-color:rgba(76,201,240,.45)}
49
+ .stat.good .big{color:var(--volt)}
50
+ .arrowcol{display:flex;align-items:center;justify-content:center;color:var(--faint);font-size:30px}
51
+
52
+ /* ---------- section ---------- */
53
+ section{margin-top:40px}
54
+ .h{display:flex;align-items:baseline;gap:12px;margin-bottom:6px}
55
+ .h .n{font-family:ui-monospace,monospace;color:var(--volt);font-weight:700;font-size:14px;
56
+ border:1px solid var(--line);border-radius:8px;padding:2px 9px;background:var(--panel2)}
57
+ .h h2{font-size:21px;margin:0;letter-spacing:-.3px}
58
+ .h .lead{color:var(--muted);font-size:14px;margin:0 0 0 auto;text-align:right;max-width:420px}
59
+ .card{background:linear-gradient(180deg,var(--panel),var(--panel2));border:1px solid var(--line);
60
+ border-radius:16px;padding:22px;box-shadow:var(--shadow);margin-top:14px}
61
+
62
+ /* ---------- cascade bar ---------- */
63
+ .cascade{display:flex;gap:2px;height:64px;align-items:flex-end;margin:6px 0 4px;flex-wrap:nowrap;overflow:hidden}
64
+ .cascade i{flex:1;background:linear-gradient(180deg,#ff6a6a,var(--fail));border-radius:2px 2px 0 0;
65
+ height:100%;opacity:.9;animation:rise .5s ease backwards}
66
+ .cascade i.ok{background:linear-gradient(180deg,#56e08a,var(--sent));height:62%}
67
+ @keyframes rise{from{transform:scaleY(.1);opacity:.2}to{transform:scaleY(1)}}
68
+ .meta{display:flex;gap:18px;flex-wrap:wrap;color:var(--muted);font-size:13px;margin-top:14px}
69
+ .pill{display:inline-flex;align-items:center;gap:7px;background:var(--panel2);border:1px solid var(--line);
70
+ border-radius:999px;padding:5px 12px;font-size:12.5px}
71
+ .pill .k{color:var(--faint)}
72
+ .pill .v{font-weight:700}
73
+ .pill .v.r{color:var(--fail)} .pill .v.g{color:var(--sent)} .pill .v.s{color:var(--skip)}
74
+ .rootcause{display:flex;gap:12px;align-items:center;margin-top:16px;padding:12px 14px;
75
+ border:1px dashed rgba(245,158,11,.45);background:rgba(245,158,11,.07);border-radius:12px;color:#f8d99a}
76
+ .rootcause b{color:#ffd27a}
77
+
78
+ /* ---------- the rule banner ---------- */
79
+ .rule{display:grid;grid-template-columns:auto 1fr;gap:18px;align-items:center;
80
+ border:1px solid rgba(76,201,240,.4);border-radius:18px;padding:22px 24px;
81
+ background:linear-gradient(110deg,rgba(67,97,238,.16),rgba(76,201,240,.07));box-shadow:var(--shadow)}
82
+ .bolt{font-size:40px;filter:drop-shadow(0 0 16px rgba(76,201,240,.6))}
83
+ .rule .txt{font-size:clamp(18px,2.6vw,26px);font-weight:700;line-height:1.25;letter-spacing:-.3px}
84
+ .rule .txt .hl{color:var(--volt)}
85
+ .rule .txt small{display:block;color:var(--muted);font-weight:500;font-size:13px;margin-top:8px;letter-spacing:0}
86
+
87
+ /* ---------- scenarios / row strips ---------- */
88
+ .scn{margin-top:16px;border:1px solid var(--line);border-radius:14px;padding:16px 18px;background:var(--panel2)}
89
+ .scn.headline{border-color:rgba(76,201,240,.4);background:linear-gradient(180deg,rgba(76,201,240,.06),var(--panel2))}
90
+ .scn-top{display:flex;align-items:center;gap:10px;margin-bottom:14px;flex-wrap:wrap}
91
+ .scn-title{font-weight:700;font-size:15px}
92
+ .tag{font-size:11px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;border-radius:999px;padding:3px 10px}
93
+ .tag.stop{color:#ffd0d0;background:rgba(239,68,68,.16);border:1px solid rgba(239,68,68,.4)}
94
+ .tag.safe{color:#bff3d2;background:rgba(34,197,94,.14);border:1px solid rgba(34,197,94,.38)}
95
+ .tag.drain{color:#cfe0f5;background:rgba(100,120,146,.18);border:1px solid var(--line)}
96
+ .scn-note{margin-left:auto;color:var(--muted);font-size:13px}
97
+
98
+ .strip{display:flex;gap:10px;align-items:stretch;flex-wrap:wrap}
99
+ .row{width:78px;border-radius:12px;border:1px solid var(--line);background:var(--panel);
100
+ padding:10px 8px 8px;text-align:center;position:relative}
101
+ .row .ic{width:30px;height:30px;border-radius:9px;margin:2px auto 7px;display:flex;align-items:center;
102
+ justify-content:center;font-size:17px;font-weight:800}
103
+ .row.fail .ic{background:rgba(239,68,68,.16);color:var(--fail);border:1px solid rgba(239,68,68,.4)}
104
+ .row.sent .ic{background:rgba(34,197,94,.16);color:var(--sent);border:1px solid rgba(34,197,94,.4)}
105
+ .row.skip .ic{background:rgba(100,120,146,.18);color:#aebacd;border:1px solid var(--line)}
106
+ .row .rl{font-size:10.5px;color:var(--faint);text-transform:uppercase;letter-spacing:.05em}
107
+ .row .cnt{margin-top:8px;font-size:11px;color:var(--muted)}
108
+ .pips{display:flex;gap:4px;justify-content:center;margin-top:5px}
109
+ .pips u{width:9px;height:9px;border-radius:50%;background:#26334a;display:block}
110
+ .pips u.on{background:var(--fail);box-shadow:0 0 7px rgba(239,68,68,.7)}
111
+ .row.ghost{opacity:.32;filter:grayscale(.4)}
112
+ .row.ghost .ic{border-style:dashed}
113
+ .ghostlab{font-size:10px;color:var(--faint);margin-top:6px;font-style:italic}
114
+
115
+ .trip{display:flex;flex-direction:column;align-items:center;justify-content:center;width:96px;
116
+ border-radius:12px;border:1px solid rgba(239,68,68,.55);
117
+ background:linear-gradient(180deg,rgba(239,68,68,.22),rgba(239,68,68,.08))}
118
+ .trip .b{font-size:22px}
119
+ .trip .t{font-weight:800;color:#ff8c8c;font-size:13px;letter-spacing:.04em}
120
+ .trip .e{font-size:10.5px;color:#ffb3b3;margin-top:2px}
121
+ .arrow{display:flex;align-items:center;color:var(--faint);font-size:18px;padding:0 1px}
122
+
123
+ .legend{display:flex;gap:16px;flex-wrap:wrap;margin-top:8px;color:var(--muted);font-size:12.5px}
124
+ .legend span{display:inline-flex;align-items:center;gap:7px}
125
+ .sw{width:13px;height:13px;border-radius:4px;display:inline-block}
126
+ .sw.f{background:var(--fail)} .sw.s{background:var(--sent)} .sw.k{background:var(--skip)}
127
+ .sw.m{background:var(--mod)} .sw.a{background:var(--amber)}
128
+
129
+ /* ---------- two-col ---------- */
130
+ .cols{display:grid;grid-template-columns:1fr 1fr;gap:16px}
131
+ @media(max-width:760px){.cols{grid-template-columns:1fr}.hero{grid-template-columns:1fr}.arrowcol{transform:rotate(90deg)}}
132
+ .mean{border-radius:14px;padding:18px;border:1px solid var(--line);background:var(--panel2)}
133
+ .mean.is{border-color:rgba(34,197,94,.4);background:linear-gradient(180deg,rgba(34,197,94,.07),var(--panel2))}
134
+ .mean.isnt{border-color:rgba(239,68,68,.32)}
135
+ .mean .lab{font-size:12px;letter-spacing:.1em;text-transform:uppercase;font-weight:700;margin-bottom:10px}
136
+ .mean.is .lab{color:var(--sent)} .mean.isnt .lab{color:#ff9c9c}
137
+ .mean ul{margin:0;padding-left:0;list-style:none}
138
+ .mean li{display:flex;gap:9px;align-items:flex-start;margin:7px 0;font-size:14px}
139
+ .mean li .m{flex:0 0 auto;font-weight:800}
140
+ .mean.is li .m{color:var(--sent)} .mean.isnt li .m{color:var(--fail)}
141
+ .mean.isnt li{color:var(--muted);text-decoration:line-through;text-decoration-color:rgba(239,68,68,.5)}
142
+
143
+ /* ---------- edit-recipe (the change, in human steps) ---------- */
144
+ .reclegend{display:flex;gap:20px;flex-wrap:wrap;margin:14px 0 0;color:var(--muted);font-size:13.5px}
145
+ .reclegend span{display:inline-flex;align-items:center;gap:8px}
146
+ .recipe{border:1px solid var(--line);border-radius:16px;background:linear-gradient(180deg,var(--panel),var(--panel2));
147
+ box-shadow:var(--shadow);padding:18px 20px;margin-top:14px}
148
+ .rhead{display:flex;align-items:center;gap:12px;flex-wrap:wrap}
149
+ .rhead .what{font-size:17px;color:var(--ink);font-weight:700;letter-spacing:-.2px}
150
+ .rhead .sz{margin-left:auto;font-size:12.5px;color:var(--faint);border:1px solid var(--line);border-radius:7px;
151
+ padding:3px 9px;background:var(--panel);white-space:nowrap}
152
+ .rpath{font-family:ui-monospace,monospace;font-size:12.5px;color:var(--faint);opacity:.9;margin-top:6px;word-break:break-all}
153
+ .oprow{display:flex;gap:9px;align-items:stretch;flex-wrap:wrap;margin-top:16px}
154
+ .opnode{flex:1 1 0;min-width:166px;border:1px solid var(--line);border-radius:12px;background:var(--panel2);
155
+ padding:14px 14px 13px;position:relative}
156
+ .opnode .ix{width:24px;height:24px;border-radius:7px;display:flex;align-items:center;justify-content:center;
157
+ font-size:13px;font-weight:800;margin-bottom:10px;border:1px solid var(--line)}
158
+ .opnode .op{font-size:15px;color:var(--ink);font-weight:600;line-height:1.32}
159
+ .opnode .sub{font-size:12.5px;color:var(--muted);margin-top:5px;line-height:1.4}
160
+ .opnode .tg{position:absolute;top:13px;right:13px;font-size:10px;font-weight:800;letter-spacing:.05em;
161
+ text-transform:uppercase;border-radius:999px;padding:3px 8px}
162
+ .opnode.reused{border-color:rgba(34,197,94,.4)}
163
+ .opnode.reused .ix{color:var(--sent);background:rgba(34,197,94,.14);border-color:rgba(34,197,94,.4)}
164
+ .opnode.reused .tg{color:#bff3d2;background:rgba(34,197,94,.14);border:1px solid rgba(34,197,94,.4)}
165
+ .opnode.mod{border-color:rgba(167,139,250,.5)}
166
+ .opnode.mod .ix{color:var(--mod);background:rgba(167,139,250,.16);border-color:rgba(167,139,250,.5)}
167
+ .opnode.mod .tg{color:#d8c9ff;background:rgba(167,139,250,.16);border:1px solid rgba(167,139,250,.5)}
168
+ .opnode.new{border-color:rgba(245,158,11,.45)}
169
+ .opnode.new .ix{color:var(--amber);background:rgba(245,158,11,.14);border-color:rgba(245,158,11,.45)}
170
+ .opnode.new .tg{color:#ffe2b0;background:rgba(245,158,11,.14);border:1px solid rgba(245,158,11,.45)}
171
+ .opconn{display:flex;align-items:center;color:var(--faint);font-size:18px}
172
+ @media(max-width:760px){.opconn{display:none}.opnode{flex:1 1 100%}}
173
+ .alsoadd{display:flex;align-items:center;gap:11px;flex-wrap:wrap;margin-top:14px;padding-top:13px;
174
+ border-top:1px solid var(--line);font-size:13.5px;color:var(--muted)}
175
+ .alsoadd .plus{flex:0 0 auto;width:21px;height:21px;border-radius:6px;display:flex;align-items:center;justify-content:center;
176
+ font-weight:800;font-size:14px;color:var(--amber);background:rgba(245,158,11,.14);border:1px solid rgba(245,158,11,.45)}
177
+ .alsoadd .txt b{color:var(--ink);font-weight:600}
178
+ .alsoadd .ap{font-family:ui-monospace,monospace;font-size:11.5px;color:var(--faint);opacity:.8;word-break:break-all}
179
+ .alsoadd .nt{margin-left:auto;font-size:10px;font-weight:800;text-transform:uppercase;letter-spacing:.05em;border-radius:999px;
180
+ padding:3px 8px;color:#ffe2b0;background:rgba(245,158,11,.16);border:1px solid rgba(245,158,11,.45);white-space:nowrap}
181
+ .nochange{margin-top:16px;color:var(--muted);font-size:13px;display:flex;gap:10px;flex-wrap:wrap;align-items:center}
182
+ .nochange .lk{font-family:ui-monospace,monospace;font-size:12px;color:var(--faint);border:1px solid var(--line);
183
+ border-radius:7px;padding:3px 8px;background:var(--panel)}
184
+ .nochange .lk::before{content:"🔒 ";}
185
+
186
+ /* ---------- tests ---------- */
187
+ .tests{display:grid;grid-template-columns:1fr 1fr;gap:14px}
188
+ @media(max-width:760px){.tests{grid-template-columns:1fr}}
189
+ .test{border:1px solid var(--line);border-radius:14px;padding:16px;background:var(--panel2);position:relative}
190
+ .test .rid{position:absolute;top:14px;right:14px;font-size:10px;font-weight:800;letter-spacing:.1em;
191
+ color:#ff9c9c;background:rgba(239,68,68,.14);border:1px solid rgba(239,68,68,.35);border-radius:6px;padding:2px 7px}
192
+ .test .tn{font-size:14.5px;color:var(--ink);font-weight:600;padding-right:54px;line-height:1.35}
193
+ .test .pin{color:var(--muted);font-size:13px;margin-top:10px}
194
+ .test .pin b{color:var(--volt)}
195
+
196
+ /* ---------- acceptance ---------- */
197
+ .acc{display:grid;grid-template-columns:1fr 1fr;gap:9px 18px}
198
+ @media(max-width:760px){.acc{grid-template-columns:1fr}}
199
+ .ck{display:flex;gap:10px;align-items:flex-start;font-size:13.5px;color:var(--muted)}
200
+ .ck .b{flex:0 0 auto;width:18px;height:18px;border-radius:5px;background:rgba(34,197,94,.16);
201
+ border:1px solid rgba(34,197,94,.45);color:var(--sent);display:flex;align-items:center;justify-content:center;
202
+ font-size:12px;font-weight:800;margin-top:1px}
203
+ .ck b{color:var(--ink);font-weight:600}
204
+
205
+ footer{margin-top:50px;color:var(--faint);font-size:12px;text-align:center;border-top:1px solid var(--line);padding-top:18px}
206
+ .codeflow{font-family:ui-monospace,monospace;font-size:12px;color:var(--muted);line-height:1.9;
207
+ background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:14px 16px;margin-top:14px;overflow-x:auto}
208
+ .codeflow .v{color:var(--volt)} .codeflow .g{color:var(--sent)} .codeflow .r{color:var(--fail)} .codeflow .c{color:var(--faint)}
209
+
210
+ /* reuse audit */
211
+ .auditsum{display:flex;gap:8px;flex-wrap:wrap;margin-top:8px}
212
+ .searchlog{margin-top:14px;border:1px dashed var(--line);border-radius:12px;padding:13px 15px;background:var(--panel)}
213
+ .searchlog .t{font-size:11px;text-transform:uppercase;letter-spacing:.08em;color:var(--faint);font-weight:700;margin-bottom:8px}
214
+ .searchlog code{display:block;font-family:ui-monospace,monospace;font-size:12px;color:var(--muted);margin:4px 0;white-space:pre-wrap;word-break:break-word}
215
+ .searchlog code b{color:var(--volt)}
216
+ .searchlog code .hit{color:var(--sent)}
217
+ .audit{display:flex;flex-direction:column;gap:12px;margin-top:14px}
218
+ .arow{border:1px solid var(--line);border-radius:14px;background:var(--panel2);padding:14px 16px}
219
+ .arow.x{border-color:rgba(76,201,240,.4);background:linear-gradient(180deg,rgba(76,201,240,.06),var(--panel2))}
220
+ .ahead{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
221
+ .ahead .sym{font-size:14.5px;color:var(--ink);font-weight:700;letter-spacing:-.2px;word-break:break-word}
222
+ .badge{font-size:10px;font-weight:800;letter-spacing:.06em;text-transform:uppercase;border-radius:999px;padding:3px 10px;white-space:nowrap}
223
+ .badge.reuse{color:#bff3d2;background:rgba(34,197,94,.14);border:1px solid rgba(34,197,94,.42)}
224
+ .badge.extract{color:#cfe6ff;background:rgba(76,201,240,.16);border:1px solid rgba(76,201,240,.5)}
225
+ .badge.newj{color:#ffe2b0;background:rgba(245,158,11,.14);border:1px solid rgba(245,158,11,.42)}
226
+ .badge.cfg{color:#cdd8ea;background:rgba(100,120,146,.16);border:1px solid var(--line)}
227
+ .lines{margin-top:10px;display:grid;grid-template-columns:80px 1fr;gap:5px 12px;font-size:13px}
228
+ .lines .k{color:var(--faint);font-size:11px;text-transform:uppercase;letter-spacing:.05em;padding-top:2px}
229
+ .lines .v{color:var(--muted)}
230
+ .lines .v b{color:var(--ink);font-weight:600}
231
+ .lines .v .mono{font-family:ui-monospace,monospace;font-size:12px;color:var(--volt)}
232
+ .grow{margin-top:14px;display:flex;gap:11px;align-items:center;border:1px solid rgba(76,201,240,.4);
233
+ background:rgba(76,201,240,.06);border-radius:12px;padding:12px 14px;color:#cfe6ff;font-size:13.5px}
234
+ </style>
235
+ </head>
236
+ <body>
237
+ <div class="wrap">
238
+
239
+ <!-- COMPONENT: kicker + hero title — use for: the packet name, the change title, and a one/two-sentence summary of the problem and the fix -->
240
+ <div class="kicker"><span class="dot"></span> Plan packet · short context line goes here</div>
241
+ <h1>Plan Title Goes Here</h1>
242
+ <p class="sub">One or two sentences that state the problem and the fix. Wrap the key terms
243
+ in <b>bold</b> so the reader catches the <b>core idea</b> at a glance.</p>
244
+
245
+ <!-- COMPONENT: stat hero — use for: a single before → after comparison, the headline metric that motivates the change (current bad number vs target good number) -->
246
+ <div class="hero">
247
+ <div class="stat bad">
248
+ <div class="lab">Before · context label</div>
249
+ <div class="big">120</div>
250
+ <div class="cap">the current cost or count, in plain terms</div>
251
+ </div>
252
+ <div class="arrowcol">➜</div>
253
+ <div class="stat good">
254
+ <div class="lab">After · context label</div>
255
+ <div class="big">4</div>
256
+ <div class="cap">the target cost or count once the change lands</div>
257
+ </div>
258
+ </div>
259
+
260
+ <!-- COMPONENT: section header — use for: the numbered title + right-aligned lead paragraph that opens every section below -->
261
+ <section>
262
+ <div class="h"><span class="n">01</span><h2>Section title for the motivating incident</h2>
263
+ <p class="lead">A short right-aligned lead that frames what this section shows and why it matters.</p>
264
+ </div>
265
+
266
+ <!-- COMPONENT: cascade bar — use for: a run or batch result rendered as many small bars (one bar per item), with summary pills and a root-cause banner; the bars are drawn by the inline script from generic counts -->
267
+ <div class="card">
268
+ <div class="cascade" id="cascade"></div>
269
+ <div class="meta">
270
+ <span class="pill"><span class="k">batch result</span> <span class="v g">success 20</span> · <span class="v s">skipped 0</span> · <span class="v r">failed 24</span></span>
271
+ <span class="pill"><span class="k">started</span> <span class="v">00:00:00</span></span>
272
+ <span class="pill"><span class="k">log</span> <span class="v mono">run_log_name.log</span></span>
273
+ </div>
274
+ <div class="rootcause">⚠️ <div><b>Root cause:</b> one short sentence naming the systemic fault that made every item fail, and why per-item retry could not heal it.</div></div>
275
+ </div>
276
+ </section>
277
+
278
+ <!-- COMPONENT: labeled rule banner — use for: the single headline rule or decision the plan introduces, stated once in large type with a short clarifying note -->
279
+ <section>
280
+ <div class="rule">
281
+ <div class="bolt">⚡</div>
282
+ <div class="txt">
283
+ <span class="hl">The headline condition</span> → the action that follows when it is met.
284
+ <small>A short clarifying note: what the rule does and does not do, any fixed value, and why there is no flag or toggle.</small>
285
+ </div>
286
+ </div>
287
+ </section>
288
+
289
+ <!-- COMPONENT: scenario row-strip with state pips — use for: walking through concrete item sequences; each .row is one item with a state (fail/sent/skip), optional counter and pips, and a .trip marker when the rule fires -->
290
+ <section>
291
+ <div class="h"><span class="n">02</span><h2>How the rule behaves on real sequences</h2>
292
+ <p class="lead">One short line on the shared state the sequences ride on. Each strip below is one item sequence.</p>
293
+ </div>
294
+
295
+ <!-- COMPONENT: legend — use for: a small swatch key that maps each state color to its meaning before the scenario strips -->
296
+ <div class="legend">
297
+ <span><i class="sw f"></i> failure → counter +1</span>
298
+ <span><i class="sw s"></i> success → counter resets to 0</span>
299
+ <span><i class="sw k"></i> benign skip → counter resets to 0</span>
300
+ </div>
301
+
302
+ <div class="scn headline">
303
+ <div class="scn-top">
304
+ <span class="scn-title">Headline scenario — the rule fires</span>
305
+ <span class="tag stop">trips</span>
306
+ <span class="scn-note">a short note on the outcome</span>
307
+ </div>
308
+ <div class="strip">
309
+ <div class="row fail"><div class="ic">✕</div><div class="rl">Item 1</div><div class="cnt">counter → 1</div><div class="pips"><u class="on"></u><u></u></div></div>
310
+ <div class="arrow">→</div>
311
+ <div class="row fail"><div class="ic">✕</div><div class="rl">Item 2</div><div class="cnt">counter → 2</div><div class="pips"><u class="on"></u><u class="on"></u></div></div>
312
+ <div class="arrow">→</div>
313
+ <div class="trip"><div class="b">⚡</div><div class="t">STOP</div><div class="e">1 alert sent</div></div>
314
+ <div class="arrow">→</div>
315
+ <div class="row fail ghost"><div class="ic">✕</div><div class="rl">Item 3</div><div class="ghostlab">never reached</div></div>
316
+ </div>
317
+ </div>
318
+
319
+ <div class="cols">
320
+ <div class="scn">
321
+ <div class="scn-top"><span class="scn-title">Many benign skips in a row</span><span class="tag drain">drains fully</span></div>
322
+ <div class="strip">
323
+ <div class="row skip"><div class="ic">⊘</div><div class="rl">skip</div><div class="pips"><u></u><u></u></div></div>
324
+ <div class="row skip"><div class="ic">⊘</div><div class="rl">skip</div><div class="pips"><u></u><u></u></div></div>
325
+ <div class="row skip"><div class="ic">⊘</div><div class="rl">skip</div><div class="pips"><u></u><u></u></div></div>
326
+ </div>
327
+ <div class="scn-note" style="margin:12px 0 0">counter stays 0 · no action taken</div>
328
+ </div>
329
+
330
+ <div class="scn">
331
+ <div class="scn-top"><span class="scn-title">A success resets the counter</span><span class="tag safe">no trip</span></div>
332
+ <div class="strip">
333
+ <div class="row fail"><div class="ic">✕</div><div class="rl">fail</div><div class="cnt">→ 1</div><div class="pips"><u class="on"></u><u></u></div></div>
334
+ <div class="arrow">→</div>
335
+ <div class="row sent"><div class="ic">✓</div><div class="rl">sent</div><div class="cnt">→ 0</div><div class="pips"><u></u><u></u></div></div>
336
+ <div class="arrow">→</div>
337
+ <div class="row fail"><div class="ic">✕</div><div class="rl">fail</div><div class="cnt">→ 1</div><div class="pips"><u class="on"></u><u></u></div></div>
338
+ </div>
339
+ <div class="scn-note" style="margin:12px 0 0">1 → 0 → 1 · never reaches the limit</div>
340
+ </div>
341
+ </div>
342
+ </section>
343
+
344
+ <!-- COMPONENT: code-flow block — use for: a short pseudocode or call-trace excerpt showing the precise signal the code reads, with .v/.g/.r/.c spans tinting key tokens and comments -->
345
+ <section>
346
+ <div class="h"><span class="n">03</span><h2>The signal the code reads</h2>
347
+ <p class="lead">One line on which value carries the signal and why it is the precise test.</p>
348
+ </div>
349
+ <div class="card" style="padding:18px 20px">
350
+ <div class="codeflow"><span class="c"># per item, inside the loop body</span><br>
351
+ count_before = state.<span class="v">failure_count</span><br>
352
+ outcome = <span class="c">await</span> pipeline.run_single(item)<br>
353
+ item_failed = state.<span class="v">failure_count</span> &gt; count_before&nbsp;&nbsp;<span class="c"># the precise failure test</span><br>
354
+ <br>
355
+ <span class="r">failure</span> → state.consecutive += 1&nbsp;&nbsp;<span class="c"># reaches the limit → record_stop(); return True</span><br>
356
+ <span class="g">success / skip</span> → state.consecutive = 0<br>
357
+ <br>
358
+ <span class="c"># stop signal travels up — no new teardown:</span><br>
359
+ return True → caller returns → runner <span class="v">finally</span>: report · summary · close
360
+ </div>
361
+ <div class="scn-note" style="margin:12px 0 0">A note on an edge case the signal already handles without extra branching.</div>
362
+ </div>
363
+ </section>
364
+
365
+ <!-- COMPONENT: is/isn't comparison cards — use for: scoping a term or decision; the left card lists what the change IS, the right card lists what it is NOT (struck through) -->
366
+ <section>
367
+ <div class="h"><span class="n">04</span><h2>What the change means here</h2></div>
368
+ <div class="cols">
369
+ <div class="mean is">
370
+ <div class="lab">✓ It is</div>
371
+ <ul>
372
+ <li><span class="m">→</span> The first thing the change actually does</li>
373
+ <li><span class="m">→</span> The second thing, reusing an existing path</li>
374
+ <li><span class="m">→</span> The third thing, through existing teardown</li>
375
+ </ul>
376
+ </div>
377
+ <div class="mean isnt">
378
+ <div class="lab">✕ It is not</div>
379
+ <ul>
380
+ <li><span class="m">×</span> A behavior the change deliberately avoids</li>
381
+ <li><span class="m">×</span> A second non-goal</li>
382
+ <li><span class="m">×</span> A configurable knob the change does not add</li>
383
+ </ul>
384
+ </div>
385
+ </div>
386
+ </section>
387
+
388
+ <!-- COMPONENT: edit-recipe step sequences — use for: the files the change touches. Describe each file by WHAT IT ACCOMPLISHES in plain language, as an ordered recipe of colored steps (reused / modified / new) — name the behavior, never the code symbol. Keep the file's repo-relative path in .rpath, dimmed, for the build agent. Fold a trivial one-line change into the recipe it supports as an "Also adds" line rather than giving it its own card. List untouched areas as dimmed paths below. -->
389
+ <section>
390
+ <div class="h"><span class="n">05</span><h2>The change — file by file</h2>
391
+ <p class="lead">Each file is described by what it accomplishes, in plain terms — an ordered recipe of steps, colored kept · changed · added.</p>
392
+ </div>
393
+
394
+ <!-- COMPONENT: recipe legend — use for: the three change-state colors, shown once above the recipes -->
395
+ <div class="reclegend">
396
+ <span><i class="sw s"></i> reused — existing behavior, kept</span>
397
+ <span><i class="sw m"></i> modified — existing behavior, changed</span>
398
+ <span><i class="sw a"></i> new — added by this change</span>
399
+ </div>
400
+
401
+ <!-- one recipe per touched file: a plain-language title, the dimmed path, an ordered row of colored steps, and an optional folded one-liner -->
402
+ <div class="recipe">
403
+ <div class="rhead">
404
+ <span class="what">What this file accomplishes, in plain language</span>
405
+ <span class="sz">~12–20 lines · 1 function</span>
406
+ </div>
407
+ <div class="rpath">path/relative/to/repo/root/the_changed_file.py</div>
408
+ <div class="oprow">
409
+ <div class="opnode reused"><div class="ix">1</div><div class="op">The first step, in human terms</div><div class="sub">a short clause on what it does and why</div><span class="tg">reused</span></div>
410
+ <div class="opconn">→</div>
411
+ <div class="opnode mod"><div class="ix">2</div><div class="op">The step that changes existing behavior</div><div class="sub">name the behavior, not the symbol</div><span class="tg">modified</span></div>
412
+ <div class="opconn">→</div>
413
+ <div class="opnode new"><div class="ix">3</div><div class="op">The step this change adds</div><div class="sub">what becomes true that wasn't before</div><span class="tg">new</span></div>
414
+ </div>
415
+ <div class="alsoadd">
416
+ <span class="plus">+</span>
417
+ <span class="txt"><b>Also adds a small supporting one-liner</b> folded into the file it serves, in plain language</span>
418
+ <span class="ap">path/relative/to/repo/root/supporting_file.py</span>
419
+ <span class="nt">new · 1 line</span>
420
+ </div>
421
+ </div>
422
+
423
+ <div class="recipe">
424
+ <div class="rhead">
425
+ <span class="what">The tests — proven on real data</span>
426
+ <span class="sz">new · across the test files</span>
427
+ </div>
428
+ <div class="rpath">path/relative/to/repo/root/tests/test_the_behavior.py</div>
429
+ <div class="oprow">
430
+ <div class="opnode new"><div class="ix">1</div><div class="op">The behavior the first test pins</div><div class="sub">stated as what it proves, not the function name</div><span class="tg">new</span></div>
431
+ <div class="opconn">→</div>
432
+ <div class="opnode new"><div class="ix">2</div><div class="op">The behavior the second test pins</div><div class="sub">one human clause each</div><span class="tg">new</span></div>
433
+ <div class="opconn">→</div>
434
+ <div class="opnode reused"><div class="ix">3</div><div class="op">An existing test extended</div><div class="sub">what the extension now also covers</div><span class="tg">reused</span></div>
435
+ </div>
436
+ </div>
437
+
438
+ <div class="nochange">Left untouched:
439
+ <span class="lk">path/to/untouched_area_one.py</span>
440
+ <span class="lk">path/to/untouched_area_two.py</span>
441
+ <span class="lk">path/to/untouched_area_three.py</span>
442
+ </div>
443
+ </section>
444
+
445
+ <!-- COMPONENT: reuse-audit verdict badges + search log + audit rows — use for: showing every new symbol was checked against the codebase first; the badge summary counts verdicts, the search log lists the searches run, each audit row records searched / found / verdict, and the .grow banner notes any scope growth -->
446
+ <section>
447
+ <div class="h"><span class="n">06</span><h2>Reuse audit — why each new piece exists</h2>
448
+ <p class="lead">Every new symbol checked against the codebase before building, with the searches and the verdict.</p>
449
+ </div>
450
+
451
+ <div class="card" style="padding:20px">
452
+ <div class="auditsum">
453
+ <span class="badge reuse">3 reused</span>
454
+ <span class="badge extract">1 extracted → shared</span>
455
+ <span class="badge newj">1 new (justified)</span>
456
+ <span class="badge cfg">1 config-local</span>
457
+ </div>
458
+
459
+ <div class="searchlog">
460
+ <div class="t">Searches run</div>
461
+ <code><b>grep -i</b> "\b(term_one|term_two)\b" --glob *.py → <span class="hit">existing_module · ExistingHelper</span></code>
462
+ <code><b>grep -i</b> "alternate_term" shared/ → <span class="hit">shared/example_helper.py</span></code>
463
+ <code><b>read</b> example_runner.py:100-140 · example_constants.py:11 · example_service.py:180</code>
464
+ </div>
465
+
466
+ <div class="audit">
467
+
468
+ <div class="arow x">
469
+ <div class="ahead"><span class="sym">the new shared kernel</span><span class="badge extract">extract → shared</span></div>
470
+ <div class="lines">
471
+ <span class="k">searched</span><span class="v">repo-wide for an existing helper that does this exact thing</span>
472
+ <span class="k">found</span><span class="v"><b>a second caller already hand-rolls this pattern</b> as a local copy with a different constant. A related helper exists but solves a different problem.</span>
473
+ <span class="k">verdict</span><span class="v">Reproduction. Extract one shared helper; both callers use it. Per-caller policy stays at the call site.</span>
474
+ </div>
475
+ </div>
476
+
477
+ <div class="arow">
478
+ <div class="ahead"><span class="sym">the existing action helper</span><span class="badge reuse">reused</span></div>
479
+ <div class="lines">
480
+ <span class="k">searched</span><span class="v">the existing action path in the service module</span>
481
+ <span class="k">found</span><span class="v">an existing helper already used by sibling paths</span>
482
+ <span class="k">verdict</span><span class="v">Reused verbatim — the change adds no new code here.</span>
483
+ </div>
484
+ </div>
485
+
486
+ <div class="arow">
487
+ <div class="ahead"><span class="sym">the new thin method</span><span class="badge newj">new (justified)</span></div>
488
+ <div class="lines">
489
+ <span class="k">searched</span><span class="v">the sibling methods that already perform the action</span>
490
+ <span class="k">found</span><span class="v">the siblings each mutate counters this method must leave untouched</span>
491
+ <span class="k">verdict</span><span class="v">Can't reuse — a new thin method that calls the shared action helper.</span>
492
+ </div>
493
+ </div>
494
+
495
+ <div class="arow">
496
+ <div class="ahead"><span class="sym">the new constants</span><span class="badge cfg">config-local</span></div>
497
+ <div class="lines">
498
+ <span class="k">searched</span><span class="v">the config module for an existing matching constant</span>
499
+ <span class="k">found</span><span class="v">no matching constant; a sibling uses a different value</span>
500
+ <span class="k">verdict</span><span class="v">Add to config/ — the value can't be shared because it differs.</span>
501
+ </div>
502
+ </div>
503
+
504
+ </div>
505
+
506
+ <div class="grow">⤴ This audit grows the change set — the new shared helper plus the second-caller rewire shown above.</div>
507
+ </div>
508
+ </section>
509
+
510
+ <!-- COMPONENT: test cards — use for: the test-first plan; each card carries a RED id, the behavior the test proves stated as a plain sentence (not the function name), and a one-line "Pins:" statement of what it locks in -->
511
+ <section>
512
+ <div class="h"><span class="n">07</span><h2>Test-first — written before the code</h2></div>
513
+ <div class="tests">
514
+ <div class="test"><span class="rid">RED 1</span>
515
+ <div class="tn">The action fires once the limit is reached</div>
516
+ <div class="pin">Pins: <b>the action fires at the limit</b> — one alert, nothing past it.</div>
517
+ </div>
518
+ <div class="test"><span class="rid">RED 2</span>
519
+ <div class="tn">A run of harmless skips never fires it</div>
520
+ <div class="pin">Pins the <b>must-not</b>: many skips drain fully, zero actions.</div>
521
+ </div>
522
+ <div class="test"><span class="rid">RED 3</span>
523
+ <div class="tn">A success in between resets the count</div>
524
+ <div class="pin">Pins the <b>reset</b>: fail → success → fail never reaches the limit.</div>
525
+ </div>
526
+ <div class="test"><span class="rid">RED 4</span>
527
+ <div class="tn">The alert names the step that failed</div>
528
+ <div class="pin">Pins the alert: the recorded step matches what was named.</div>
529
+ </div>
530
+ </div>
531
+ </section>
532
+
533
+ <!-- COMPONENT: acceptance checklist — use for: the "done when" criteria; each .ck is one checked item with a bold lead phrase and a short clause naming the observable outcome -->
534
+ <section>
535
+ <div class="h"><span class="n">08</span><h2>Done when</h2>
536
+ <p class="lead">A short line on the test-count change and that baseline failures elsewhere stay unchanged.</p>
537
+ </div>
538
+ <div class="card">
539
+ <div class="acc">
540
+ <div class="ck"><span class="b">✓</span><div><b>The action fires at the limit</b> — next item never reached, one alert.</div></div>
541
+ <div class="ck"><span class="b">✓</span><div><b>Benign skips never fire</b> — the run drains, counter at 0.</div></div>
542
+ <div class="ck"><span class="b">✓</span><div><b>A success or skip resets</b> the counter to 0.</div></div>
543
+ <div class="ck"><span class="b">✓</span><div><b>The alert names the step</b> via the named constant.</div></div>
544
+ <div class="ck"><span class="b">✓</span><div><b>Counters stay in sync</b> — the change adds neither failure nor name.</div></div>
545
+ <div class="ck"><span class="b">✓</span><div><b>Reuses the existing path</b> — no new teardown.</div></div>
546
+ <div class="ck"><span class="b">✓</span><div><b>The loop compares the named constant</b>, not a literal.</div></div>
547
+ <div class="ck"><span class="b">✓</span><div><b>The code-rules gate</b> passes on the changed lines.</div></div>
548
+ </div>
549
+ </div>
550
+ </section>
551
+
552
+ <!-- COMPONENT: footer — use for: the closing line naming the packet slug and the validator status -->
553
+ <footer>Plan title · plan packet <span class="mono">plan-slug-goes-here</span> · validator: approved, 0 open product questions</footer>
554
+ </div>
555
+
556
+ <script>
557
+ (function(){
558
+ var cascade=document.getElementById('cascade');
559
+ if(!cascade)return;
560
+ var total=44, failures=24, fragment=document.createDocumentFragment();
561
+ for(var i=0;i<total;i++){
562
+ var bar=document.createElement('i');
563
+ if(i>=failures)bar.className='ok';
564
+ bar.style.animationDelay=(i*7)+'ms';
565
+ bar.style.height=(i>=failures?62:(55+((i*13)%45)))+'%';
566
+ fragment.appendChild(bar);
567
+ }
568
+ cascade.appendChild(fragment);
569
+ })();
570
+ </script>
571
+ </body>
572
+ </html>
@@ -20,6 +20,13 @@ def test_skill_invokes_plan_packet_workflow() -> None:
20
20
  assert "docs/plans/<slug>/" in skill_text
21
21
 
22
22
 
23
+ def test_skill_launches_workflow_with_args_payload() -> None:
24
+ skill_text = SKILL_PATH.read_text(encoding="utf-8")
25
+
26
+ assert "args:" in skill_text
27
+ assert "input:" not in skill_text
28
+
29
+
23
30
  def test_skill_no_longer_mentions_single_home_plan_file() -> None:
24
31
  skill_text = SKILL_PATH.read_text(encoding="utf-8")
25
32
 
@@ -27,6 +34,14 @@ def test_skill_no_longer_mentions_single_home_plan_file() -> None:
27
34
  assert "single-file" not in skill_text.lower()
28
35
 
29
36
 
37
+ def test_skill_documents_self_healing_writes() -> None:
38
+ skill_text = SKILL_PATH.read_text(encoding="utf-8").lower()
39
+
40
+ assert "worktree" in skill_text
41
+ assert "stages" in skill_text
42
+ assert "copies" in skill_text
43
+
44
+
30
45
  def test_skill_names_validator_and_stop_before_code_rules() -> None:
31
46
  skill_text = SKILL_PATH.read_text(encoding="utf-8")
32
47
 
@@ -63,6 +63,30 @@ test('semantic validator uses a dedicated validator agent with structured schema
63
63
  assert.match(validatorBody, /blind build agent/);
64
64
  });
65
65
 
66
+ test('writer self-heals a blocked write by staging and copying into place', () => {
67
+ const writerPrompt = functionBody('writePacketPrompt');
68
+ assert.match(writerPrompt, /stage/i);
69
+ assert.match(writerPrompt, /copy/i);
70
+ assert.match(writerPrompt, /recover/i);
71
+ assert.doesNotMatch(writerPrompt, /stop immediately/i);
72
+ });
73
+
74
+ test('packet write schema carries the recovery signal', () => {
75
+ const writeSchema = functionBody('packetWriteSchema');
76
+ assert.match(writeSchema, /recovered/);
77
+ });
78
+
79
+ test('workflow proceeds to validation without failing closed on a blocked write', () => {
80
+ const runBody = functionBody('runPlanPacketWorkflow');
81
+ const writeIndex = runBody.indexOf('await writePacket');
82
+ const deterministicIndex = runBody.indexOf('runDeterministicValidation');
83
+ assert.ok(writeIndex !== -1 && deterministicIndex !== -1);
84
+ assert.ok(writeIndex < deterministicIndex);
85
+ const betweenWriteAndValidation = runBody.slice(writeIndex, deterministicIndex);
86
+ assert.doesNotMatch(betweenWriteAndValidation, /return/);
87
+ assert.match(runBody, /recovered/);
88
+ });
89
+
66
90
  test('workflow stops before implementation work', () => {
67
91
  const runBody = functionBody('runPlanPacketWorkflow');
68
92
  assert.match(runBody, /implementationStarted:\s*false/);
@@ -77,3 +101,94 @@ test('workflow fails closed when a phase errors', () => {
77
101
  assert.match(runBody, /validationPassed:\s*false/);
78
102
  assert.match(runBody, /approvalRequired:\s*true/);
79
103
  });
104
+
105
+ test('repair schema carries the recovery signal', () => {
106
+ const repairSchemaBody = functionBody('repairSchema');
107
+ assert.match(repairSchemaBody, /recovered/);
108
+ assert.match(repairSchemaBody, /recoveryNote/);
109
+ });
110
+
111
+ test('workflow folds repair-path recovery into the top-level recovered signal', () => {
112
+ const runBody = functionBody('runPlanPacketWorkflow');
113
+ const repairCallMatch = /const\s+(\w+)\s*=\s*await repairPacket\(/.exec(runBody);
114
+ assert.notEqual(repairCallMatch, null, 'expected the repair result to be captured');
115
+ const repairResultName = repairCallMatch[1];
116
+ const recordRecoveryMatch = new RegExp(`recordRecovery\\(${repairResultName}\\)`).exec(runBody);
117
+ assert.notEqual(recordRecoveryMatch, null, 'expected the repair result to feed the recovery signal');
118
+ });
119
+
120
+ test('workflow error path returns the recovery keys', () => {
121
+ const runBody = functionBody('runPlanPacketWorkflow');
122
+ const catchIndex = runBody.indexOf('catch (');
123
+ assert.notEqual(catchIndex, -1, 'expected a catch block');
124
+ const catchBody = runBody.slice(catchIndex);
125
+ assert.match(catchBody, /\brecovered\b/);
126
+ assert.match(catchBody, /\brecoveryNote\b/);
127
+ });
128
+
129
+ test('workflow declares a reuse audit phase', () => {
130
+ assert.match(workflowSource, /Reuse audit/);
131
+ assert.match(workflowSource, /title:\s*'Reuse audit'/);
132
+ });
133
+
134
+ test('reuse audit runner uses a structured schema and the reuse audit phase', () => {
135
+ const reuseAuditBody = functionBody('runReuseAudit');
136
+ assert.match(reuseAuditBody, /schema:\s*reuseAuditSchema\(\)/);
137
+ assert.match(reuseAuditBody, /phase:\s*'Reuse audit'/);
138
+ });
139
+
140
+ test('reuse audit schema gates on allJustified', () => {
141
+ const reuseAuditSchemaBody = functionBody('reuseAuditSchema');
142
+ assert.match(reuseAuditSchemaBody, /allJustified/);
143
+ assert.match(reuseAuditSchemaBody, /findings/);
144
+ assert.match(reuseAuditSchemaBody, /summary/);
145
+ });
146
+
147
+ test('reuse audit prompt searches shared_utils for existing equivalents', () => {
148
+ const reuseAuditPromptBody = functionBody('reuseAuditPrompt');
149
+ assert.match(reuseAuditPromptBody, /shared_utils/);
150
+ assert.match(reuseAuditPromptBody, /reuse-audit\.md/);
151
+ assert.match(reuseAuditPromptBody, /unjustified-reproduction/);
152
+ });
153
+
154
+ test('workflow runs the reuse audit after writing the packet', () => {
155
+ const runBody = functionBody('runPlanPacketWorkflow');
156
+ const writeIndex = runBody.indexOf('await writePacket');
157
+ const reuseAuditIndex = runBody.indexOf('runReuseAudit');
158
+ assert.ok(writeIndex !== -1 && reuseAuditIndex !== -1);
159
+ assert.ok(writeIndex < reuseAuditIndex);
160
+ });
161
+
162
+ test('workflow folds the reuse audit gate into the clean validation check', () => {
163
+ const runBody = functionBody('runPlanPacketWorkflow');
164
+ assert.match(runBody, /reuseAudit\.allJustified/);
165
+ assert.match(runBody, /reuseAuditFindings/);
166
+ });
167
+
168
+ test('workflow declares a visualize phase', () => {
169
+ assert.match(workflowSource, /title:\s*'Visualize'/);
170
+ });
171
+
172
+ test('workflow runs the visualize phase after validation', () => {
173
+ const runBody = functionBody('runPlanPacketWorkflow');
174
+ const visualHtmlIndex = runBody.indexOf('runVisualHtml(packetPath)');
175
+ const validationLoopIndex = runBody.indexOf('while (!hasCleanValidation(');
176
+ assert.ok(visualHtmlIndex !== -1 && validationLoopIndex !== -1);
177
+ assert.ok(visualHtmlIndex > validationLoopIndex);
178
+ });
179
+
180
+ test('visual html schema carries the html path', () => {
181
+ const visualHtmlSchemaBody = functionBody('visualHtmlSchema');
182
+ assert.match(visualHtmlSchemaBody, /htmlPath/);
183
+ });
184
+
185
+ test('visual html prompt names the template and the output file', () => {
186
+ const visualHtmlPromptBody = functionBody('visualHtmlPrompt');
187
+ assert.match(visualHtmlPromptBody, /visual-plan\.template\.html/);
188
+ assert.match(visualHtmlPromptBody, /visual-plan\.html/);
189
+ });
190
+
191
+ test('workflow returns the visual html path', () => {
192
+ const runBody = functionBody('runPlanPacketWorkflow');
193
+ assert.match(runBody, /visualHtmlPath/);
194
+ });
@@ -6,6 +6,8 @@ export const meta = {
6
6
  { title: 'Discover', detail: 'Resolve repo root, read instructions, inspect matching source files, tests, configs, docs, skills, hooks, agents, and workflows.' },
7
7
  { title: 'Write packet', detail: 'Create the required docs/plans/<slug>/ tree with a thin README hub and detailed second-level docs.' },
8
8
  { title: 'Validate', detail: 'Run scripts/validate_packet.py, spawn plan-packet-validator in fresh context, and repair findings up to the cap.' },
9
+ { title: 'Reuse audit', detail: 'Search the codebase for existing equivalents of each new symbol or file the packet introduces; write validation/reuse-audit.md with a per-item verdict; gate approval on any unjustified reproduction.' },
10
+ { title: 'Visualize', detail: 'Build a single-file offline visual HTML of the finished packet from the visual-plan template; write it beside the packet as visual-plan.html.' },
9
11
  { title: 'Approval', detail: 'Return the packet path and validation verdict, then stop before implementation work.' },
10
12
  ],
11
13
  }
@@ -44,8 +46,10 @@ function packetWriteSchema() {
44
46
  slug: { type: 'string' },
45
47
  filesWritten: { type: 'array', items: { type: 'string' } },
46
48
  summary: { type: 'string' },
49
+ recovered: { type: 'boolean' },
50
+ recoveryNote: { type: 'string' },
47
51
  },
48
- required: ['packetPath', 'slug', 'filesWritten', 'summary'],
52
+ required: ['packetPath', 'slug', 'filesWritten', 'summary', 'recovered', 'recoveryNote'],
49
53
  }
50
54
  }
51
55
 
@@ -70,8 +74,52 @@ function repairSchema() {
70
74
  properties: {
71
75
  repaired: { type: 'boolean' },
72
76
  summary: { type: 'string' },
77
+ recovered: { type: 'boolean' },
78
+ recoveryNote: { type: 'string' },
73
79
  },
74
- required: ['repaired', 'summary'],
80
+ required: ['repaired', 'summary', 'recovered', 'recoveryNote'],
81
+ }
82
+ }
83
+
84
+ function reuseAuditSchema() {
85
+ return {
86
+ type: 'object',
87
+ additionalProperties: false,
88
+ properties: {
89
+ allJustified: { type: 'boolean' },
90
+ findings: {
91
+ type: 'array',
92
+ items: {
93
+ type: 'object',
94
+ additionalProperties: false,
95
+ properties: {
96
+ item: { type: 'string' },
97
+ kind: { type: 'string' },
98
+ verdict: { type: 'string' },
99
+ searched: { type: 'string' },
100
+ found: { type: 'string' },
101
+ decision: { type: 'string' },
102
+ evidence: { type: 'string' },
103
+ },
104
+ required: ['item', 'kind', 'verdict', 'searched', 'found', 'decision', 'evidence'],
105
+ },
106
+ },
107
+ summary: { type: 'string' },
108
+ },
109
+ required: ['allJustified', 'findings', 'summary'],
110
+ }
111
+ }
112
+
113
+ function visualHtmlSchema() {
114
+ return {
115
+ type: 'object',
116
+ additionalProperties: false,
117
+ properties: {
118
+ htmlPath: { type: 'string' },
119
+ sectionsBuilt: { type: 'array', items: { type: 'string' } },
120
+ summary: { type: 'string' },
121
+ },
122
+ required: ['htmlPath', 'sectionsBuilt', 'summary'],
75
123
  }
76
124
  }
77
125
 
@@ -163,6 +211,8 @@ function writePacketPrompt(runInput, packetPath, discoverySummary) {
163
211
  `Discovery summary:\n${discoverySummary}\n\n` +
164
212
  `${packetContractText()}\n\n` +
165
213
  `Use the templates in the anthropic-plan skill if helpful. Write docs only. Do not edit source code. Do not run implementation commands. ` +
214
+ `Write every packet file with the Write tool at the packet path. ` +
215
+ `If the Write tool is blocked by a worktree or isolation guard, recover automatically: write each file with the Write tool under a writable temporary directory such as $CLAUDE_JOB_DIR/tmp/anthropic-plan/<slug> (so the content checks still run), then copy the staged tree into the packet path with a filesystem copy (cp -r, Copy-Item, or equivalent). Set recovered=true with recoveryNote describing the staging path and copy; otherwise set recovered=false with an empty recoveryNote. ` +
166
216
  `After writing, ensure packet.json includes schemaVersion 1, slug, repoRoot, packetPath, sourceFiles, assumptions, and validator fields.`
167
217
  )
168
218
  }
@@ -184,12 +234,40 @@ function semanticValidationPrompt(packetPath) {
184
234
  )
185
235
  }
186
236
 
187
- function repairPrompt(packetPath, deterministicValidation, semanticValidation) {
237
+ function repairPrompt(packetPath, deterministicValidation, semanticValidation, reuseAudit) {
188
238
  return (
189
239
  `Repair only the plan packet at ${packetPath}. Do not edit source code.\n\n` +
190
240
  `Deterministic validation findings:\n${JSON.stringify(deterministicValidation.findings || [])}\n\n` +
191
241
  `Semantic validation findings:\n${JSON.stringify(semanticValidation.findings || [])}\n\n` +
192
- `Make the packet pass by correcting documentation, adding missing source grounding, removing placeholders, strengthening TDD steps, and updating validation/validator-report.md.`
242
+ `Reuse audit findings:\n${JSON.stringify(reuseAudit?.findings || [])}\n\n` +
243
+ `For each reuse audit finding marked unjustified-reproduction, either record the reuse decision in the packet that justifies the new code, or change the plan to reuse the existing public helper or extract it to shared_utils; update validation/reuse-audit.md accordingly. ` +
244
+ `Make the packet pass by correcting documentation, adding missing source grounding, removing placeholders, strengthening TDD steps, and updating validation/validator-report.md. ` +
245
+ `If the Edit or Write tool is blocked by a worktree or isolation guard, recover automatically: stage the corrected files under a writable temporary directory with the Write tool, then copy them over the packet path with a filesystem copy. Set recovered=true with recoveryNote describing the staging path and copy; otherwise set recovered=false with an empty recoveryNote.`
246
+ )
247
+ }
248
+
249
+ function reuseAuditPrompt(packetPath) {
250
+ return (
251
+ `Run the reuse audit for the plan packet at ${packetPath}. Resolve the repo root from packet.json. Do not edit source code; only write the packet doc.\n\n` +
252
+ `Read implementation/file-plan.md, spec/interfaces.md, implementation/tdd-plan.md, and spec/scope.md in the packet to enumerate every new file, public symbol, helper, and constant the build introduces.\n\n` +
253
+ `For each item, search the codebase with grep, serena, or zoekt — repo-wide and specifically under shared_utils — for an existing implementation or near-equivalent behavior.\n\n` +
254
+ `Assign exactly one verdict per item from: reused (an existing public helper is used), extract-to-shared (an equivalent exists but is not shared or public and should be extracted), new-justified (genuinely new, with the reason reuse or extract was rejected), config-local (a constant living in config/), or unjustified-reproduction (reproduces existing behavior that could be made public or extracted, with no recorded justification).\n\n` +
255
+ `Write validation/reuse-audit.md into the packet: a markdown table with columns Item, Kind, Verdict, Searched, Found, Decision, Evidence using real file:line evidence, plus a one-line summary of verdict counts. Write concrete content only — no angle-bracket placeholder tokens and no todo, tbd, or placeholder words.\n\n` +
256
+ `Return the structured object. Set allJustified=false when any finding has verdict unjustified-reproduction.`
257
+ )
258
+ }
259
+
260
+ function visualHtmlPrompt(packetPath) {
261
+ return (
262
+ `Build a single-file, offline, diagram-first visual HTML of the finished plan packet at ${packetPath}. Do not edit source code or the packet markdown; only write the HTML view.\n\n` +
263
+ `Read the style template first and reuse its CSS and section components exactly:\n` +
264
+ `$HOME/.claude/skills/anthropic-plan/templates/visual-plan.template.html\n\n` +
265
+ `Then read the packet: README.md, packet.json, every file under spec/ and implementation/, and validation/reuse-audit.md. Translate the packet into the template's visual vocabulary — stat hero, scenario row strips, is/isn't cards, edit-recipe step sequences, verdict badges, and a checklist. Show the plan as diagrams and compact cards, never walls of prose, and never paste the markdown verbatim.\n\n` +
266
+ `Write for the reviewer — a person reading the plan, not the computer that runs the code. State every label as what a step accomplishes, in plain language. Drop code symbols from the picture: no function names, selector strings, call traces, or snake_case test names in the visible diagram — those stay in the packet markdown for the build agent. Keep each touched file's repo-relative path, but dim it (the .rpath / .ap style) so it sits quietly beneath the human description.\n\n` +
267
+ `Render the change (section 05) as edit-recipe step sequences, one recipe per touched file: a plain-language title for what the file accomplishes, the dimmed repo-relative path, then an ordered row of colored steps — reused (green), modified (violet), new (amber). Fold a trivial one-line change into the recipe it supports as an "Also adds" line rather than giving it its own card. Name each test by the behavior it proves, not its function name.\n\n` +
268
+ `Surface validation/reuse-audit.md as a Reuse audit section with one verdict badge per item (reused, extract-to-shared, new-justified, config-local, unjustified-reproduction), each item titled in plain language with its file path dimmed.\n\n` +
269
+ `Write the result to ${packetPath}/visual-plan.html. Inline all CSS and JavaScript; make no network calls and reference no external assets, so the file opens offline. If the Write tool is blocked by a worktree or isolation guard, stage the file under $CLAUDE_JOB_DIR/tmp with the Write tool, then copy it to the packet path.\n\n` +
270
+ `Return htmlPath set to the written file path, sectionsBuilt listing the section names you included, and a one-line summary.`
193
271
  )
194
272
  }
195
273
 
@@ -231,8 +309,26 @@ async function runSemanticValidator(packetPath) {
231
309
  })
232
310
  }
233
311
 
234
- async function repairPacket(packetPath, deterministicValidation, semanticValidation) {
235
- return agent(repairPrompt(packetPath, deterministicValidation, semanticValidation), {
312
+ async function runReuseAudit(packetPath) {
313
+ return agent(reuseAuditPrompt(packetPath), {
314
+ label: `plan-packet-reuse-audit`,
315
+ phase: 'Reuse audit',
316
+ schema: reuseAuditSchema(),
317
+ agentType: 'general-purpose',
318
+ })
319
+ }
320
+
321
+ async function runVisualHtml(packetPath) {
322
+ return agent(visualHtmlPrompt(packetPath), {
323
+ label: `plan-packet-visual-html`,
324
+ phase: 'Visualize',
325
+ schema: visualHtmlSchema(),
326
+ agentType: 'general-purpose',
327
+ })
328
+ }
329
+
330
+ async function repairPacket(packetPath, deterministicValidation, semanticValidation, reuseAudit) {
331
+ return agent(repairPrompt(packetPath, deterministicValidation, semanticValidation, reuseAudit), {
236
332
  label: `plan-packet-repair`,
237
333
  phase: 'Validate',
238
334
  schema: repairSchema(),
@@ -248,23 +344,47 @@ async function runPlanPacketWorkflow(rawInput) {
248
344
  let packetWrite = null
249
345
  let deterministicValidation = null
250
346
  let semanticValidation = null
347
+ let reuseAudit = null
348
+ let recovered = false
349
+ let recoveryNote = ''
350
+ let visualHtmlPath = ''
351
+ const visualHtmlFindings = []
352
+ const recordRecovery = (recovery) => {
353
+ if (recovery?.recovered !== true) return
354
+ recovered = true
355
+ recoveryNote = recovery.recoveryNote || recoveryNote
356
+ }
251
357
 
252
358
  try {
253
359
  const discoverySummary = await discoverContext(runInput, packetPath)
254
360
  packetWrite = await writePacket(runInput, packetPath, discoverySummary)
361
+ recordRecovery(packetWrite)
362
+ reuseAudit = await runReuseAudit(packetPath)
255
363
  deterministicValidation = await runDeterministicValidation(packetPath)
256
364
  semanticValidation = await runSemanticValidator(packetPath)
257
365
  const hasCleanValidation = () =>
258
- deterministicValidation?.passed === true && semanticValidation && semanticValidation.allPassed === true
366
+ deterministicValidation?.passed === true &&
367
+ semanticValidation &&
368
+ semanticValidation.allPassed === true &&
369
+ reuseAudit &&
370
+ reuseAudit.allJustified === true
259
371
 
260
372
  while (!hasCleanValidation() && repairLoops < policy.maxRepairLoops) {
261
373
  repairLoops += 1
262
- await repairPacket(packetPath, deterministicValidation, semanticValidation)
374
+ const repair = await repairPacket(packetPath, deterministicValidation, semanticValidation, reuseAudit)
375
+ recordRecovery(repair)
376
+ reuseAudit = await runReuseAudit(packetPath)
263
377
  deterministicValidation = await runDeterministicValidation(packetPath)
264
378
  semanticValidation = await runSemanticValidator(packetPath)
265
379
  }
266
380
 
267
381
  const passed = hasCleanValidation()
382
+ try {
383
+ const visualHtml = await runVisualHtml(packetPath)
384
+ visualHtmlPath = visualHtml?.htmlPath || ''
385
+ } catch (visualHtmlError) {
386
+ visualHtmlFindings.push(String(visualHtmlError?.message || visualHtmlError))
387
+ }
268
388
  return {
269
389
  packetPath: packetWrite?.packetPath || packetPath,
270
390
  slug: packetWrite?.slug || slugFromTask(runInput.task || runInput.prompt || runInput.arguments),
@@ -272,8 +392,13 @@ async function runPlanPacketWorkflow(rawInput) {
272
392
  repairLoops,
273
393
  deterministicFindings: deterministicValidation?.findings || [],
274
394
  semanticFindings: semanticValidation?.findings || [],
395
+ reuseAuditFindings: reuseAudit?.findings || [],
275
396
  implementationStarted: false,
276
397
  approvalRequired: true,
398
+ recovered,
399
+ recoveryNote,
400
+ visualHtmlPath,
401
+ visualHtmlFindings,
277
402
  }
278
403
  } catch (workflowError) {
279
404
  return {
@@ -290,8 +415,13 @@ async function runPlanPacketWorkflow(rawInput) {
290
415
  detail: String(workflowError?.message || workflowError),
291
416
  },
292
417
  ],
418
+ reuseAuditFindings: reuseAudit?.findings || [],
293
419
  implementationStarted: false,
294
420
  approvalRequired: true,
421
+ recovered,
422
+ recoveryNote,
423
+ visualHtmlPath,
424
+ visualHtmlFindings,
295
425
  }
296
426
  }
297
427
  }