Skip to content

Commit 19fb131

Browse files
committed
1.0.0-gamma.5: 3 automatic repairs before a stop (named levers, then the AI's own fix); a site nested in a folder is taken from that folder (from d68e89f)
1 parent c72e723 commit 19fb131

11 files changed

Lines changed: 98 additions & 51 deletions

File tree

‎.claude-plugin/marketplace.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
"source": "./",
1515
"displayName": "html2wp — HTML to WordPress",
1616
"description": "BETA. Convert a static HTML site into a standalone WordPress block theme, verified against the original at three viewport widths — and reviewed by you before handover. The conversion runs through a verified Astro 5 build, so the Astro project is a real output too — compared pixel by pixel against the source before anything WordPress-shaped is generated.",
17-
"version": "1.0.0-gamma.4",
17+
"version": "1.0.0-gamma.5",
1818
"author": {
1919
"name": "BELNEM s.r.o.",
2020
"email": "hello@html2wp.dev",

‎.claude-plugin/plugin.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "html2wp",
33
"displayName": "html2wp — HTML to WordPress",
44
"description": "BETA. Convert a static HTML site — or anything that builds to one (Lovable, Bolt, v0, Next export) — into a standalone WordPress block theme: verified pixel by pixel, pages, blog, forms, menus, SEO and redirects, with no plugin required and verification gates at every stage. Two modes: Full (every gate repaired until green, then your own page-by-page review) and Flash (every stage once, no repair loop, the gates reported). The gates catch most fidelity drift, not all of it.",
5-
"version": "1.0.0-gamma.4",
5+
"version": "1.0.0-gamma.5",
66
"author": {
77
"name": "BELNEM s.r.o.",
88
"email": "hello@html2wp.dev",

‎.codex-plugin/plugin.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "html2wp",
3-
"version": "1.0.0-gamma.4",
3+
"version": "1.0.0-gamma.5",
44
"description": "Convert static HTML or buildable web projects into verified, editable WordPress block themes.",
55
"author": {
66
"name": "BELNEM s.r.o.",

‎VERSION‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1.0.0-gamma.4
1+
1.0.0-gamma.5

‎skills/html2wp/SKILL.md‎

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -423,11 +423,20 @@ a stuck fix gives up; the attempts are in the report.
423423
`h2wp-signature: <key>` (make-zip, article-part.py, preflight-listings,
424424
check-manifest `--flash`, stage3-remote's WordPress, check-prereqs, the Woo
425425
audit's cart probe). `assets/repair-levers.json` maps each key to its
426-
levers, in order — the only repairs Flash may try for it. A failure with no
427-
signature has no lever: record it.
428-
- **The budget:** 4 attempts per run, at most 2 per stage, never the same lever
429-
on the same failure twice. `progress.sh` counts them and refuses (exit 3)
430-
anything beyond — that refusal is the rule.
426+
levers, in order — tried first, each once.
427+
- **Then the AI's own fix (`ai-fix`).** When a failure that would stop the run
428+
has spent its named levers, or no script named it (signature `unnamed`),
429+
the attempts left are yours: read the failing command's output and logs,
430+
find the cause, change only what causes it — never a gate, a check or its
431+
thresholds — run the failing command once, and say in the close note what
432+
you changed and why: `progress.sh repair <stage> ai-fix <signature|unnamed>`.
433+
A failure that is only recorded red (`warn`) gets its named levers, not
434+
`ai-fix`.
435+
- **The budget:** 9 attempts per run, at most 3 per stage, never the same
436+
named lever on the same failure twice. `progress.sh` counts them and refuses
437+
(exit 3) anything beyond — that refusal is the rule. A run stops only after
438+
its stage's 3 attempts; the app then offers Repair and continue, where the
439+
owner may pick a stronger model first.
431440
- **A repair happens INSIDE the stage that failed,** before the stage is
432441
closed — never as a second run of it:
433442

@@ -487,7 +496,7 @@ container has no `apply_patch` command.
487496
| mode | `progress.sh mode flash` | — | — |
488497
| -4 | `whats-here.sh {workspace}` (a finished or part-finished workspace: follow its route — a Continue resumes, never restarts); `allowance.sh` (print its line verbatim); `detect-project.py <project> --out {workspace}/detect.json` | — | kind `none`; a site over the page allowance (say it, with the service's words) |
489498
| -3 | `$S/check-prereqs.sh` — in the app exactly `/opt/html2wp/skills/html2wp/assets/scripts/check-prereqs.sh` | — | a missing tool (it prints what to install) |
490-
| -1 | by kind — `static-html`: `rsync -a --exclude .git --exclude node_modules --exclude .html2wp <project>/ {input}/`; `web-app`: `prerender-spa.py --project <project> --out {input} --no-verify --flash` (a group of pages — a route table's `/product/:id`, or pages sharing a path with one varying last segment like a TanStack `/blog/<slug>` — is recorded once, on its first page, and each other page shares that recording only when its controls are the same; the pages are captured in `--jobs` browsers, entrance motion is waited for at most 6 s; it says "N of M" as it goes); `static-site`: `static-site.py --project <project> --out {input}` (exit 3 → `prerender-spa.py … --no-verify --flash` instead, once); `html2wp-astro`: `detect-project.py <project> --prepare {workspace}` | — | no pages written (exit 2; 1 from static-site.py) |
499+
| -1 | by kind — `static-html`: `rsync -a --exclude .git --exclude node_modules --exclude .html2wp <root>/ {input}/` — `<root>` is detect.json's `root`, never `<project>` itself (a site nested in a folder, `<project>/<name>/index.html`, is taken from that folder); `web-app`: `prerender-spa.py --project <project> --out {input} --no-verify --flash` (a group of pages — a route table's `/product/:id`, or pages sharing a path with one varying last segment like a TanStack `/blog/<slug>` — is recorded once, on its first page, and each other page shares that recording only when its controls are the same; the pages are captured in `--jobs` browsers, entrance motion is waited for at most 6 s; it says "N of M" as it goes); `static-site`: `static-site.py --project <project> --out {input}` (exit 3 → `prerender-spa.py … --no-verify --flash` instead, once); `html2wp-astro`: `detect-project.py <project> --prepare {workspace}` | — | no pages written (exit 2; 1 from static-site.py) |
491500
| 0 | `cp -a {input} {workspace}/input-untouched`; `analyze-input.mjs {input} --out={workspace}/analysis.json`; `flash-manifest.py --analysis {workspace}/analysis.json --input {input} --workspace {workspace}`; decide (below); `check-manifest.py --manifest={workspace}/conversion-manifest.json --flash` | — | analyze exit 2 (refusal, key collision); check-manifest still red after its lever |
492501
| -1b | a shop: `capture-commerce-specimen.py --dist {input} --out {workspace}/style-specimens`; no shop: `skip` (it comes after stage 0 because stage 0 decides the shop) | a nonzero exit | — |
493502
| 0.5 | `optimize-images.py --input {input} --remote --apply --out {workspace}/optimize-images-report.json` (`skip` for `html2wp-astro`) | a nonzero exit | — |
@@ -510,7 +519,7 @@ Flash writes no `anchors` and no Gate-0 summary into the manifest — the
510519
decisions go into the report.
511520

512521
**The stop path** — only when there is no theme to deliver and the stage's
513-
levers are spent (or it has none): `progress.sh fail <stage> "<why>"`; write
522+
3 attempts are spent (named levers, then `ai-fix`): `progress.sh fail <stage> "<why>"`; write
514523
`CONVERSION-REPORT.md` with what ran, what stopped it (the script's own words),
515524
the repairs tried and what the owner can do; when stage 3 opened a service
516525
job, `send-verdicts.sh {workspace} --outcome=abandoned`; then
@@ -679,8 +688,8 @@ message (and Continue) on a stopped run comes with `H2WP_MODE=repair-stop`.
679688
the log is gone — that is reading, not a repair). What the owner wrote may
680689
name the fix; it still goes through a lever.
681690
2. **Spend the levers of that stage** ("Flash repairs", above): each owner
682-
message allows 2 attempts on the stage that stopped the run, apart from the
683-
run's own 4 —
691+
message allows 3 attempts on the stage that stopped the run (its named
692+
levers, then `ai-fix`), apart from the run's own 9 —
684693

685694
```
686695
progress.sh repair <the stopped stage> <lever> <signature>
@@ -694,7 +703,7 @@ message (and Continue) on a stopped run comes with `H2WP_MODE=repair-stop`.
694703
stage 6.5 and `write-result.py` included (Flash: the "Flash mode" table;
695704
the mode is the one the run started with, whatever `H2WP_MODE` says about
696705
the turn). Never the stages before it, never `mode`, never a new run.
697-
4. **Not fixed** (the attempts are spent, or no lever fits): write the stop
706+
4. **Not fixed** (the message's 3 attempts are spent): write the stop
698707
again — `write-result.py {workspace} --status stopped --stopped-stage
699708
<stage> --stopped-reason "<why>"` — and tell the owner plainly what could
700709
not be fixed and why, in their words, with what they can do (a different

‎skills/html2wp/VERSION‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1.0.0-gamma.4
1+
1.0.0-gamma.5

‎skills/html2wp/assets/repair-levers.json‎

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
{
22
"schema": "h2wp-repair-levers/1",
3-
"about": "Flash's repair budget (SKILL.md, \"Flash repairs\"). A refusing script prints `h2wp-signature: <key>`; the key names the failure, and its levers are the only repairs Flash may try for it, in this order. progress.sh repair counts every attempt: 4 per run, at most 2 per stage, never the same lever on the same failure twice. A remedy is followed by the failing command again, then progress.sh repaired.",
4-
"pool": 4,
5-
"perStage": 2,
6-
"perOwnerMessage": 2,
3+
"about": "Flash's repair budget (SKILL.md, \"Flash repairs\"). A refusing script prints `h2wp-signature: <key>`; the key names the failure, and its levers are the only repairs Flash may try for it, in this order. progress.sh repair counts every attempt: 9 per run, at most 3 per stage (3 per owner message on a stopped run), never the same named lever on the same failure twice; when a failure's named levers are spent, or no script named it (`unnamed`), the attempts left are the AI's own diagnosed fix (`ai-fix`). A remedy is followed by the failing command again, then progress.sh repaired.",
4+
"pool": 9,
5+
"perStage": 3,
6+
"perOwnerMessage": 3,
77
"signatures": {
88
"manifest-undecided": {
99
"stages": [
@@ -97,6 +97,13 @@
9797
"levers": [
9898
"prereqs-again"
9999
]
100+
},
101+
"unnamed": {
102+
"stages": [
103+
"*"
104+
],
105+
"what": "a failure that stops the run and that no script named with h2wp-signature",
106+
"levers": []
100107
}
101108
},
102109
"levers": {
@@ -156,6 +163,11 @@
156163
"label": "check the prerequisites again",
157164
"remedy": "The owner installed what check-prereqs named; run it again.",
158165
"again": "$S/check-prereqs.sh"
166+
},
167+
"ai-fix": {
168+
"label": "the AI's own diagnosed fix",
169+
"remedy": "Read the failing command's output and logs, find the cause, and change only what causes it (never a gate, a check or its thresholds). Say in the close note what you changed and why.",
170+
"again": "the failing command, once"
159171
}
160172
}
161173
}

‎skills/html2wp/assets/scripts/detect-project.py‎

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,16 @@
4949
def input_root(root):
5050
"""The project inside an upload: the directory itself when it holds a
5151
package.json or an index.html, else its one subdirectory (a ZIP that
52-
unpacked into a folder)."""
53-
if (root / "package.json").exists() or (root / "index.html").exists():
54-
return root
55-
children = [c for c in root.iterdir() if not c.name.startswith((".", "__MACOSX"))]
56-
if len(children) == 1 and children[0].is_dir() and not children[0].is_symlink():
57-
return children[0]
52+
unpacked into a folder, or a site in a named folder beside a README),
53+
a few levels down at most."""
54+
for _ in range(3):
55+
if (root / "package.json").exists() or (root / "index.html").exists():
56+
return root
57+
dirs = [c for c in root.iterdir() if c.is_dir() and not c.is_symlink()
58+
and not c.name.startswith((".", "__MACOSX")) and c.name != "node_modules"]
59+
if len(dirs) != 1 or any(c.suffix.lower() in (".html", ".htm") for c in root.iterdir() if c.is_file()):
60+
return root
61+
root = dirs[0]
5862
return root
5963

6064

‎skills/html2wp/assets/scripts/lib/repair_budget.py‎

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,12 @@
1414
failed is never started again — the repair happens inside it, before it is
1515
closed with done, warn or fail.
1616
17-
The caps (from the table): 4 attempts per run, at most 2 per stage, never the
18-
same lever on the same failure twice. A stopped run (result.json status
17+
The caps (from the table): 9 attempts per run, at most 3 per stage, never the
18+
same named lever on the same failure twice. When a failure's named levers are
19+
spent, or no script named it (signature `unnamed`), the attempts left are the
20+
AI's own diagnosed fix (lever `ai-fix`), each counted like any other. A stopped run (result.json status
1921
"stopped") is repaired only in a turn the owner started (H2WP_MODE=repair-stop):
20-
each owner message allows 2 attempts on the stage that stopped the run, apart
22+
each owner message allows 3 attempts on the stage that stopped the run, apart
2123
from the run's own 4 — a message is the app's turn id (H2WP_TURN), else (a
2224
CLI) the stopped result it answers. A fixed attempt there puts the stopped
2325
result aside and sets the run running again, so it continues from that stage;
@@ -93,9 +95,16 @@ def open_attempt(pf, rf, stage, lever, signature):
9395
if not sig:
9496
return refuse(f"no such failure signature '{signature}'. A script that refuses prints "
9597
f"`h2wp-signature: <key>`; the keys: {', '.join(sorted(signatures))}")
96-
if stage not in sig.get("stages", []):
98+
if stage not in sig.get("stages", []) and "*" not in sig.get("stages", []):
9799
return refuse(f"'{signature}' is a failure of stage {' or '.join(sig.get('stages', []))}, not of stage {stage}.")
98-
if lever not in sig.get("levers", []):
100+
if lever == "ai-fix":
101+
untried = [lv for lv in sig.get("levers", []) if not any(
102+
r.get("stage") == stage and r.get("signature") == signature and r.get("lever") == lv
103+
for r in progress.get("repairs") or [] if isinstance(r, dict))]
104+
if untried:
105+
return refuse(f"'{signature}' still has a named lever: {untried[0]}. The AI's own fix comes "
106+
"after the named levers are spent.")
107+
elif lever not in sig.get("levers", []):
99108
return refuse(f"'{lever}' is not a lever for '{signature}'. Its levers, in order: "
100109
f"{', '.join(sig.get('levers', []))}.")
101110
repairs = [r for r in progress.get("repairs") or [] if isinstance(r, dict)]
@@ -113,7 +122,7 @@ def open_attempt(pf, rf, stage, lever, signature):
113122
"the owner started (SKILL.md, \"A stopped run — repair, then continue\").")
114123
if stage != at:
115124
return refuse(f"the run stopped at stage {at}: only that stage's levers can be spent now.")
116-
by, turn, cap = "owner", owner_turn(result), int(meta.get("perOwnerMessage", 2))
125+
by, turn, cap = "owner", owner_turn(result), int(meta.get("perOwnerMessage", 3))
117126
mine = [r for r in repairs if r.get("by") == "owner" and r.get("turn") == turn]
118127
if len(mine) >= cap:
119128
return refuse(f"this message's {cap} repair attempts on stage {stage} are spent. Record the stop again "
@@ -125,25 +134,25 @@ def open_attempt(pf, rf, stage, lever, signature):
125134
return refuse(f"stage {stage} is not running. A repair happens INSIDE the stage that failed, before it "
126135
"is closed with done, warn or fail — never as a second run of it.")
127136
by, turn = "run", None
128-
pool, per = int(meta.get("pool", 4)), int(meta.get("perStage", 2))
137+
pool, per = int(meta.get("pool", 9)), int(meta.get("perStage", 3))
129138
mine = [r for r in repairs if r.get("by", "run") == "run"]
130139
if len(mine) >= pool:
131140
return refuse(f"the run's {pool} repair attempts are spent. Record stage {stage} red "
132141
"(progress.sh warn, or fail when there is no theme and no fallback) and go on.")
133142
if len([r for r in mine if r.get("stage") == stage]) >= per:
134143
return refuse(f"stage {stage} has had its {per} repair attempts. Record it red "
135144
"(progress.sh warn, or fail when there is no theme and no fallback) and go on.")
136-
if any(r.get("stage") == stage and r.get("signature") == signature and r.get("lever") == lever
145+
if lever != "ai-fix" and any(r.get("stage") == stage and r.get("signature") == signature and r.get("lever") == lever
137146
and r.get("by", "run") == by and (by == "run" or r.get("turn") == turn) for r in repairs):
138147
untried = [lv for lv in sig.get("levers", []) if not any(
139148
r.get("stage") == stage and r.get("signature") == signature and r.get("lever") == lv for r in repairs)]
140149
return refuse(f"'{lever}' was already tried on '{signature}' at stage {stage} — the same lever on the same "
141150
"failure again is a loop. "
142-
+ (f"Next lever: {untried[0]}." if untried else "No lever is left for it: record it red."))
151+
+ (f"Next lever: {untried[0]}." if untried else "Next: ai-fix, the AI's own diagnosed fix."))
143152

144153
n = len([r for r in repairs if r.get("stage") == stage and r.get("by", "run") == by
145154
and (by == "run" or r.get("turn") == turn)]) + 1
146-
of = int(meta.get("perOwnerMessage", 2)) if by == "owner" else int(meta.get("perStage", 2))
155+
of = int(meta.get("perOwnerMessage", 3)) if by == "owner" else int(meta.get("perStage", 3))
147156
row = {"stage": stage, "attempt": n, "of": of, "lever": lever, "signature": signature,
148157
"label": (levers.get(lever) or {}).get("label", lever), "what": sig.get("what", ""),
149158
"outcome": "open", "at": now(), "by": by}
@@ -155,7 +164,7 @@ def open_attempt(pf, rf, stage, lever, signature):
155164
progress["repairs"] = repairs
156165
write(pf, progress)
157166
spec = levers.get(lever) or {}
158-
where = f"pool {row['pool']}/{meta.get('pool', 4)}" if by == "run" else "the owner's message"
167+
where = f"pool {row['pool']}/{meta.get('pool', 9)}" if by == "run" else "the owner's message"
159168
print(f"\n repair {n}/{of} of stage {stage} ({where}) — {spec.get('label', lever)}")
160169
print(f" remedy: {spec.get('remedy', '')}")
161170
if spec.get("again"):

‎skills/html2wp/assets/scripts/stage2-gates.sh‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,4 +146,12 @@ done
146146
echo
147147
echo "==> stage2-gates: $(( T1 - T0 ))s wall clock"
148148
printf '%s' "$SUMMARY"
149+
# Gate A / A2 red never stops a run: they are recorded (warn) and the run goes
150+
# on. Only what the upload needs (chrome-groups, capture-chrome,
151+
# detect-collections; bits 4, 8, 16) can stop stage 2.
152+
if [ $(( STATUS & 28 )) -ne 0 ]; then
153+
echo "==> stage 2 verdict: the upload needs the FAILED step(s) above — repair them inside stage 2; stop only after its 3 attempts"
154+
elif [ "$STATUS" -ne 0 ]; then
155+
echo "==> stage 2 verdict: gate A/A2 red — record it (progress.sh warn 2 …) and go on to stage 3; this never stops the run"
156+
fi
149157
exit "$STATUS"

0 commit comments

Comments
 (0)