Skip to content

fix(core)!: PX2-R — registration becomes an unlock, not a gate - #623

Merged
KnockOutEZ merged 12 commits into
studio-handoff-corefrom
sd-336-fix-core-px2-r-registration-gate
Sep 8, 2026
Merged

fix(core)!: PX2-R — registration becomes an unlock, not a gate#623
KnockOutEZ merged 12 commits into
studio-handoff-corefrom
sd-336-fix-core-px2-r-registration-gate

Conversation

@KnockOutEZ

Copy link
Copy Markdown
Owner

Closes KnockOutEZ/wigolo-studio-run#336. Reconciles core to the CEO consulting-pass
amendment (PX brief §0a.1–5, 2026-09-03): PX2 shipped a hard registration gate on every
surface, and §0a.1 made that gate Studio-only. Core now runs unregistered, and registration
became an unlock.

Built per §0a, not per the issue's PX2-era text — §0a overrides both, and the issue itself
says so. The requireActivation seam and all of src/account/* stay: Studio and the unlock
machinery use them. Only core call-site policy flipped.

What changed

The gate is gone from every core surface. MCP tools/call, the daemon's /v1 and
firecrawl-compat routes (the REST_UNGATED_EXACT allowlist went with it), CLI one-shots, the
REPL, and serve start. All ten tools run on a machine that has never registered.

Registration is an unlock, and one file says what it unlocks. src/account/unlocks.ts
holds the four-line list, the "runs fully without an account" sentence and the §0a.4
telemetry claim, so the MCP footer, the per-session instructions notice and both init paths
cannot drift apart. No new tool and no description growth — the 400/400 budget is untouched;
src/instructions.ts swapped one line for one line.

One nudge, ever. src/account/nudge.ts counts successful runs in
<dataDir>/account/nudge.json and burns a flag when it hands the nudge to a surface. Counting
and claiming are separate calls so REST runs — which have no channel to print prose into —
still push the counter toward a nudge the user's next CLI or MCP call renders. A failed call
counts for nothing.

wigolo register --headless is two-stage and asks nothing: stage one mails the code and
creates no account, stage two finishes with --code. Marketing consent defaults to unticked
on both paths — an absent flag is a no.

The telemetry claim is one sentence everywhere, per §0a.4, and the absolutes it replaces
are retired. README, docs/cli.md, docs/getting-started.md, docs/privacy-security.md,
docs/troubleshooting.md, llms.txt, the site's How-it-works closer, the config TUI's help
and wigolo doctor all said something that is now false, or said the privacy story in their
own words. llms.txt additionally claimed telemetry was off by default, which was simply
wrong. These are factual fixes to otherwise-frozen public docs.

Evidence

Merge gate, all three, run once at the end:

npm test           → 978 files / 12081 passed, 0 failed, 36 skipped   (exit 0)
npx tsc --noEmit   → exit 0
npm run gate:studio→ exit 0   (tests/ type-check debt holds at baseline 341)

PX2 RC exit gate (RUN_PX2_RC=1, real packed tarball + real Postgres + real accounts
service): 11 of 12 arms pass, including every clause this issue owns — first-run setup
names the unlocks, all ten tools run unregistered over MCP, the nudge is quiet for N-1 /
loud on N with the list and the pinned claim / quiet for N more, registration completes
interactively AND headlessly with consent unticked, and WIGOLO_TELEMETRY=off sends zero
requests while on sends them.

The 12th arm — the registered ten-tool sweep — reds on browser_engine_unavailable and was
red before this branch touched the file. It is filed as KnockOutEZ/wigolo-studio-run#521
with what was ruled out, not waved through: an asserted wigolo warmup --browser in
beforeAll does not fix it, which points at a product disagreement between a warmup
that reports success and a fresh process that says the engine is absent. The RC suite is
opt-in and in neither npm test nor CI.

Two RC arms were wrong, and one of those was this issue's own. The nudge arm looped
past N asserting "exactly one nudge", but the nudge is an install-lifetime resource and the
ten-tool MCP arm spends it first — measured, the install sits at successful_runs: 5, nudged: true before that arm starts, so it red on a product behaving exactly as specified.
It now resets and drives the whole shape.

Both new arms proven able to fail. REGISTRATION_UNLOCKS emptied → the first-run arm
reds on first-run output omitted the unlock "sync — …", the footer arm reds on the MCP footer omitted the unlock "sync — …"; restored byte-identical afterwards.

The full suite caught one more, which is what it is for: the REPL history-mode file
mocks node:fs wholesale and stubs the activation module, which now needs the two nudge
exports or the whole file dies on a missing mock export.

Scope split

The server half of §0a.5 could not be fixed from this branch: KnockOutEZ/wigolo-accounts
defaults consent.marketing to true when a creation request omits the field, pinned at
tests/integration/auth-flow.test.ts:152 citing the superseded pin 8. The client fix here
narrows it — register now always sends an explicit boolean — but login omits the field and
so would any other client. Filed as KnockOutEZ/wigolo-studio-run#520 (ready,
lane:svc-auth, Q-lane so it cannot wedge PX2), with a line in known-issues.md.

Re-verified after merging the moved studio-handoff-core tip into the branch: the base had advanced onto src/cli/doctor.ts, which this PR also edits. Clean merge, and all three gates re-run green on the merged tree.

PX brief §0a.1-3 (CEO consulting pass 2026-09-03) amends PX2's hard gate to
Studio-only. Core now dispatches all ten tools unregistered on every surface —
MCP tools/call, the daemon's /v1 and firecrawl-compat routes, CLI one-shots,
the REPL and serve start. The requireActivation seam and src/account/* stay
put; only core call-site policy flips.

In place of the refusal, an unregistered install is told once what an account
adds: src/account/unlocks.ts owns the list, src/account/nudge.ts owns the
persisted once-only counter, and the MCP result footer, the CLI/REPL seams and
both init paths render it. REST counts runs but never renders — a typed
envelope has no place for prose.
…efault

PX brief §0a.2 and §0a.5. `--headless` (alias `--no-input`) asks nothing:
stage one mails the code to the address the human owns and stops without
creating anything; stage two takes `--code` and `--marketing-consent` as
flags. The served telemetry disclosure is still shown on both stages — headless
does not exempt an agent from relaying the wording the human is consenting to.

The interactive consent prompt reverses to `[y/N]` with a false default: consent
has to be an affirmative act. Product and security email stays transactional.
…nudge

Every arm that asserted a refusal is now the inverse assertion at the same
seam, keeping the property each one was written for: the compat family still
carries the daemon sweep because it bypasses dispatchTool, `serve` still gets
its own arm because its refusal was a process.exit, and the watch-scheduler arm
still exists because PX2's gate sat above it deliberately.

New arms pin the single nudge: quiet before N, once on N, never again, across a
server restart, never on a registered install, and never counted from a failed
call. Four mutations were run red and restored — flag never burned, threshold
lowered to 1, failed calls counted, activation check dropped.
Every refusal arm keeps its expensive fixture and inverts its claim: the fresh
install runs its first tool and all ten over MCP with no account, the
--omit=optional install runs before it registers, and the back-dated-clock arm
now asserts BOTH halves of the amendment — core keeps running on an entitlement
that is out of its window and out of grace, while whoami shows the surviving
requireActivation seam still calls it expired.

New arms cover the single nudge with its unlock list and the pinned telemetry
claim, and the headless two-stage registration, including that an omitted
--marketing-consent persists as false.
…default

Bare-Enter consent is asserted on the wire as false, not only in local state —
the account row is what a marketing send reads. Headless stage one is pinned to
create nothing: disclosure then request-code, no verify, no state file. Four
mutations were run red and restored: consent default flipped back to true,
headless flags defaulting to yes, stage one falling through to verify, and
--headless accepting a missing --email.
…blic surface

README, docs/cli.md and docs/privacy-security.md all still said the ten tools
needed an activated install — false since the amendment made the hard gate
Studio-only, and false in the two places a reader decides whether to install at
all. They now lead with what an account ADDS.

The telemetry sentence is pinned rather than paraphrased: doctor imports
TELEMETRY_CLAIM instead of spelling a ninth variant, and the docs carry the same
words the CLI and the first-run output show. "Nothing leaves your machine" is
retired where it read as an absolute; the off-switch paragraph keeps its
conditional claim in wording that cannot be quoted back as one.

Also documents the headless register flow and the unticked marketing default,
and corrects the telemetry comment in server.ts that still described a gate
returning above it.
…r exists

getting-started still had a step called "Activate this install" and quoted an
init hint the code no longer prints; troubleshooting had four rows and a whole
section explaining a refusal core cannot emit. Both now say the opposite, and
troubleshooting keeps a pointer for anyone on an older build who really is
seeing that line.

llms.txt said telemetry was off by default. It is on for a registered install
and has been since 0.3.0 — that one was simply wrong.

The two remaining absolutes (the site's "nothing leaves" closer and the config
TUI's help text) now carry the pinned sentence or a claim scoped to the off
state. doctor's test spells the sentence out as a literal rather than importing
the constant: the clause being tested is that six surfaces say the SAME words,
which an import cannot fail to satisfy.
… meets

The unlock list was pinned at e2e level only on the CLI nudge; first-run setup
was covered by a unit test that calls activationNextStepLines directly and never
exercises the code path that prints it. Composing the lines and printing them are
two claims and only one of them is what somebody installing wigolo sees, so this
adds an arm that drives the installed binary's real setup and reads its stdout —
including the negative half, that setup never quotes the deleted refusal or the
old register next-step.

Types the two diff helpers as the SDK Client instead of a hand-written
{ callTool } shape. The SDK signature is generic over the request schema so the
structural type was never assignable, and it was costing nine entries on the
tests/ type-check debt ratchet (350 against a baseline of 341).
The arm looped ten runs and asserted exactly one nudge, on the theory that a
generous margin past N is stronger than counting to N. It is not, because the
nudge is an install-lifetime resource and the arms share one install: the
ten-tool MCP arm crosses N first, spends the nudge into a result nobody is
reading for a footer, and the CLI arm then observes zero. Measured on the
fixture — the install sits at successful_runs 5 / nudged true before this arm
starts, so it red on a product behaving exactly as specified.

The arm now resets the counter and drives the whole shape: quiet for N-1, loud on
N with the unlock list and the pinned telemetry claim, quiet for N more. That is
both halves of "once, never repeated", and neither half depends on how many of
the previous arm's ten tools happened to succeed.

Adds the matching MCP arm, because the footer is where product law 9 puts the
interface for a terminal user with no plugin: N successful calls over the
protocol, the footer on exactly the Nth, absent on the next, and the tool's own
JSON still parseable in the first content block.

Also prints the fetch error text on the registered-tools diff seeding, which
asserted a boolean and told us nothing about why it was true.
…them

The registered ten-tool arm red twice on browser_engine_unavailable while
seeding the diff baseline, and the cause is a prerequisite the fixture never
had. installTarball is npm alone, so no engine is present; the fetch router
pins a host to the tier that last served it, the fixture's short pages escalate
once, and a later force_refresh therefore STARTS at the browser tier with no
lower-tier content to fall back to. The failure also kicked off a background
download that raced everything after it.

beforeAll now warms the engine and asserts it, which is what an ordinary install
does at setup and the only fix that stops an arm's result depending on how far a
download got. Asserted rather than best-effort for the reason rc-gate-env.ts
already gives: once the gate says it runs, a missing prerequisite throws instead
of reporting green about something it never exercised.
…footer's list

The REPL suite replaces node:fs wholesale and stubs the activation module, which
under PX2 existed to keep the gate from refusing before readline attached. §0a.1
deleted that gate; what runs on this path now is the nudge, which READS AND
WRITES the counter file — i.e. the mocked fs — so the module needs its two new
exports stubbed or the whole file dies on a missing mock export. Caught by the
full suite, which is what it is for.

The MCP footer arm now asserts the unlock LIST and the telemetry claim, not just
that a footer appeared: a footer that only invited the reader to register would
be the wall announced late, which is the thing the amendment removed. Both new
arms proven able to fail by emptying REGISTRATION_UNLOCKS and watching each go
red on the missing line, then restoring it byte-identical.

resetNudgeState creates its directory, because an arm run in isolation has not
run whichever surface would otherwise have created it and died on ENOENT before
asserting anything.
@coderabbitai

coderabbitai Bot commented Sep 8, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Team

Run ID: 73992584-b36e-4d30-87d2-1fd8b6393ba7

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@KnockOutEZ
KnockOutEZ merged commit abc6bf2 into studio-handoff-core Sep 8, 2026
20 checks passed
@KnockOutEZ
KnockOutEZ deleted the sd-336-fix-core-px2-r-registration-gate branch September 8, 2026 01:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant